0.0.0
This commit is contained in:
226
miniprogram/pages/walking/walking.js
Normal file
226
miniprogram/pages/walking/walking.js
Normal file
@@ -0,0 +1,226 @@
|
||||
// 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' });
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user