227 lines
7.5 KiB
JavaScript
227 lines
7.5 KiB
JavaScript
// pages/walking/walking — 2/2b 遛狗中(进行/暂停同页)
|
||
// 计时基于时间戳差值(切后台不丢,R5);GPS 距离可选累计、按 stride 换算狗步(R3/R6)
|
||
const { call } = require('../../utils/cloud.js');
|
||
const store = require('../../utils/store.js');
|
||
const { formatDuration } = require('../../utils/format.js');
|
||
const { BASE_STRIDE, dogStepsFromMeters } = require('../../utils/dogStep.js');
|
||
|
||
// 两点球面距离(米)
|
||
function haversine(lat1, lon1, lat2, lon2) {
|
||
const R = 6371000;
|
||
const toRad = (d) => (d * Math.PI) / 180;
|
||
const dLat = toRad(lat2 - lat1);
|
||
const dLon = toRad(lon2 - lon1);
|
||
const a = Math.sin(dLat / 2) ** 2 +
|
||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||
return 2 * R * Math.asin(Math.sqrt(a));
|
||
}
|
||
|
||
Page({
|
||
data: {
|
||
status: 'idle', // idle | selecting | walking | paused
|
||
timeText: '00:00',
|
||
pooped: false,
|
||
dogs: [],
|
||
selectedIds: [],
|
||
selectedMap: {}, // id→true:供 WXML 判断选中(WXML 表达式不能调用 indexOf)
|
||
activeDogNames: '',
|
||
showEndConfirm: false,
|
||
gps: 'off', // off | on | denied
|
||
distanceText: '—',
|
||
stepsText: '—',
|
||
},
|
||
|
||
_timer: null,
|
||
_locOn: false,
|
||
_locCb: null,
|
||
_lastPoint: null,
|
||
_stride: 0.35,
|
||
|
||
onLoad() {
|
||
const dogs = store.getState().dogs || [];
|
||
this.setData({ dogs });
|
||
|
||
const session = store.getWalkSession();
|
||
if (session) { this.resumeFrom(session); return; }
|
||
if (dogs.length === 0) {
|
||
wx.showToast({ title: '先添加一只狗狗', icon: 'none' });
|
||
setTimeout(() => wx.navigateBack(), 1200);
|
||
return;
|
||
}
|
||
if (dogs.length === 1) this.startSession([dogs[0]._id]);
|
||
else {
|
||
const def = store.getState().currentDogId || dogs[0]._id;
|
||
this.setData({ status: 'selecting', selectedIds: [def], selectedMap: this.buildSelectMap([def]) });
|
||
}
|
||
},
|
||
|
||
onUnload() { this.clearTimer(); this.stopLocation(); },
|
||
|
||
// ===== 多狗选择 =====
|
||
// WXML 表达式不支持方法调用(如 selectedIds.indexOf),用 id→true 映射给视图层判断选中
|
||
buildSelectMap(ids) {
|
||
const map = {};
|
||
ids.forEach((id) => { map[id] = true; });
|
||
return map;
|
||
},
|
||
toggleSelect(e) {
|
||
const id = e.currentTarget.dataset.id;
|
||
const ids = this.data.selectedIds.slice();
|
||
const i = ids.indexOf(id);
|
||
if (i >= 0) ids.splice(i, 1); else ids.push(id);
|
||
this.setData({ selectedIds: ids, selectedMap: this.buildSelectMap(ids) });
|
||
},
|
||
confirmSelect() {
|
||
if (!this.data.selectedIds.length) return wx.showToast({ title: '至少选一只', icon: 'none' });
|
||
this.startSession(this.data.selectedIds);
|
||
},
|
||
|
||
// ===== 会话生命周期 =====
|
||
startSession(dogIds) {
|
||
const session = { dogIds, startAt: Date.now(), pausedTotal: 0, pausedAt: null, pooped: false, meters: 0, status: 'walking' };
|
||
store.setWalkSession(session);
|
||
store.set({ currentDogId: dogIds[0] });
|
||
this.setActiveNames(dogIds);
|
||
this.setData({ status: 'walking', pooped: false });
|
||
this.startTimer();
|
||
this.startLocation();
|
||
},
|
||
|
||
resumeFrom(session) {
|
||
this.setActiveNames(session.dogIds);
|
||
this.setData({ status: session.status, pooped: session.pooped });
|
||
if (session.status === 'walking') { this.startTimer(); this.startLocation(); }
|
||
else this.renderTime();
|
||
this.refreshGps();
|
||
},
|
||
|
||
setActiveNames(dogIds) {
|
||
const map = {};
|
||
(this.data.dogs || []).forEach((d) => { map[d._id] = d.name; });
|
||
this.setData({ activeDogNames: dogIds.map((id) => map[id] || '狗狗').join('、') });
|
||
},
|
||
|
||
// ===== 计时(时间戳差值)=====
|
||
elapsedSec() {
|
||
const s = store.getWalkSession();
|
||
if (!s) return 0;
|
||
const end = s.status === 'paused' ? s.pausedAt : Date.now();
|
||
return Math.max(0, Math.floor((end - s.startAt - s.pausedTotal) / 1000));
|
||
},
|
||
renderTime() { this.setData({ timeText: formatDuration(this.elapsedSec()) }); },
|
||
startTimer() { this.clearTimer(); this.renderTime(); this._timer = setInterval(() => this.renderTime(), 1000); },
|
||
clearTimer() { if (this._timer) { clearInterval(this._timer); this._timer = null; } },
|
||
|
||
// ===== GPS 距离累计(可选增强,R6)=====
|
||
startLocation() {
|
||
if (this._locOn) return;
|
||
const s = store.getWalkSession();
|
||
const primary = (this.data.dogs || []).find((d) => s && d._id === s.dogIds[0]);
|
||
this._stride = (primary && (primary.stride || BASE_STRIDE[primary.sizeLevel])) || 0.35;
|
||
this._lastPoint = null;
|
||
this._locCb = (res) => this.onLoc(res);
|
||
wx.startLocationUpdate({
|
||
success: () => {
|
||
this._locOn = true;
|
||
wx.onLocationChange(this._locCb);
|
||
this.setData({ gps: 'on' });
|
||
this.refreshGps();
|
||
},
|
||
fail: () => { this.setData({ gps: 'denied' }); }, // 未授权 → 降级,只保留时长
|
||
});
|
||
},
|
||
onLoc(res) {
|
||
const s = store.getWalkSession();
|
||
if (!s || s.status !== 'walking') { this._lastPoint = null; return; } // 暂停不累计
|
||
if (res.accuracy && res.accuracy > 50) return; // 精度差丢弃
|
||
const p = { lat: res.latitude, lon: res.longitude };
|
||
if (this._lastPoint) {
|
||
const seg = haversine(this._lastPoint.lat, this._lastPoint.lon, p.lat, p.lon);
|
||
if (seg >= 2 && seg <= 50) { // 过滤静止抖动(<2m)与跳点(>50m)
|
||
s.meters = (s.meters || 0) + seg;
|
||
store.setWalkSession(s);
|
||
this.refreshGps();
|
||
}
|
||
}
|
||
this._lastPoint = p;
|
||
},
|
||
refreshGps() {
|
||
if (this.data.gps !== 'on') return;
|
||
const s = store.getWalkSession();
|
||
const m = (s && s.meters) || 0;
|
||
this.setData({
|
||
distanceText: (m / 1000).toFixed(2),
|
||
stepsText: String(dogStepsFromMeters(m, this._stride) || 0),
|
||
});
|
||
},
|
||
stopLocation() {
|
||
if (this._locOn) {
|
||
wx.offLocationChange(this._locCb);
|
||
wx.stopLocationUpdate();
|
||
this._locOn = false;
|
||
}
|
||
},
|
||
|
||
// ===== 便便打卡 =====
|
||
togglePoop() {
|
||
const s = store.getWalkSession();
|
||
if (!s) return;
|
||
s.pooped = !s.pooped;
|
||
store.setWalkSession(s);
|
||
this.setData({ pooped: s.pooped });
|
||
},
|
||
|
||
// ===== 暂停 / 继续 =====
|
||
pause() {
|
||
const s = store.getWalkSession();
|
||
if (!s || s.status !== 'walking') return;
|
||
s.status = 'paused';
|
||
s.pausedAt = Date.now();
|
||
store.setWalkSession(s);
|
||
this.clearTimer();
|
||
this._lastPoint = null;
|
||
this.setData({ status: 'paused' });
|
||
this.renderTime();
|
||
},
|
||
resume() {
|
||
const s = store.getWalkSession();
|
||
if (!s || s.status !== 'paused') return;
|
||
s.pausedTotal += Date.now() - s.pausedAt;
|
||
s.pausedAt = null;
|
||
s.status = 'walking';
|
||
store.setWalkSession(s);
|
||
this.setData({ status: 'walking' });
|
||
this.startTimer();
|
||
},
|
||
|
||
// ===== 结束 =====
|
||
askEnd() { this.setData({ showEndConfirm: true }); },
|
||
cancelEnd() { this.setData({ showEndConfirm: false }); },
|
||
|
||
async confirmEnd() {
|
||
const s = store.getWalkSession();
|
||
if (!s) return;
|
||
this.clearTimer();
|
||
this.stopLocation();
|
||
const duration = this.elapsedSec();
|
||
const endAt = Date.now();
|
||
const distanceMeters = this.data.gps === 'on' ? Math.round(s.meters || 0) : null;
|
||
this.setData({ showEndConfirm: false });
|
||
|
||
const res = await call('walkSave', {
|
||
action: 'finish',
|
||
dogIds: s.dogIds,
|
||
startAt: s.startAt,
|
||
endAt,
|
||
duration,
|
||
distanceMeters, // 米;未授权定位时为 null(R6)
|
||
pooped: s.pooped,
|
||
}, { loading: true, loadingText: '生成成果卡' });
|
||
if (!res.ok) return;
|
||
|
||
store.setWalkSession(null);
|
||
getApp().globalData.lastSummary = res.data;
|
||
wx.redirectTo({ url: '/pages/summary/summary' });
|
||
},
|
||
});
|