This commit is contained in:
2026-06-25 18:24:29 +08:00
commit a1190d7d9a
90 changed files with 5459 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
// pages/walking/walking — 2/2b 遛狗中(进行/暂停同页)
// 计时基于时间戳差值切后台不丢R5GPS 距离可选累计、按 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, // 米;未授权定位时为 nullR6
pooped: s.pooped,
}, { loading: true, loadingText: '生成成果卡' });
if (!res.ok) return;
store.setWalkSession(null);
getApp().globalData.lastSummary = res.data;
wx.redirectTo({ url: '/pages/summary/summary' });
},
});