// 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' }); }, });