feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
const { secure, toast } = require("../../utils/api");
|
||||
Page({
|
||||
data: {
|
||||
pets: [],
|
||||
selected: [],
|
||||
walk: null,
|
||||
points: [],
|
||||
polyline: [],
|
||||
latitude: 31.23,
|
||||
longitude: 121.47,
|
||||
duration: "00:00",
|
||||
distance: "0.00",
|
||||
share: false,
|
||||
},
|
||||
onShow() {
|
||||
this.load();
|
||||
this.clock = setInterval(() => this.tick(), 1000);
|
||||
},
|
||||
onHide() {
|
||||
this.stopTimers();
|
||||
},
|
||||
onUnload() {
|
||||
this.stopTimers();
|
||||
},
|
||||
stopTimers() {
|
||||
clearInterval(this.clock);
|
||||
clearInterval(this.tracker);
|
||||
},
|
||||
async load() {
|
||||
try {
|
||||
const [pets, walks] = await Promise.all([
|
||||
secure("/pets"),
|
||||
secure("/walks"),
|
||||
]);
|
||||
const selected = (wx.getStorageSync("walk_pet_ids") || []).filter((id) =>
|
||||
pets.some((p) => p.id === id),
|
||||
);
|
||||
const walk =
|
||||
walks.find((w) => w.status === "active" || w.status === "paused") ||
|
||||
null;
|
||||
this.setData({ pets, selected, walk });
|
||||
if (walk) {
|
||||
this.renderWalk(walk);
|
||||
this.track();
|
||||
}
|
||||
} catch (e) {
|
||||
toast(e);
|
||||
}
|
||||
},
|
||||
pick(e) {
|
||||
if (this.data.walk) return;
|
||||
this.setData({ selected: e.detail.selected });
|
||||
wx.setStorageSync("walk_pet_ids", e.detail.selected);
|
||||
},
|
||||
async start() {
|
||||
if (!this.data.selected.length) {
|
||||
wx.showToast({ title: "请选择同行的狗狗", icon: "none" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const walk = await secure("/walks", "POST", {
|
||||
pet_ids: this.data.selected,
|
||||
});
|
||||
this.setData({ walk });
|
||||
this.track();
|
||||
this.point();
|
||||
} catch (e) {
|
||||
toast(e);
|
||||
}
|
||||
},
|
||||
track() {
|
||||
clearInterval(this.tracker);
|
||||
this.tracker = setInterval(() => this.point(), 15000);
|
||||
},
|
||||
async point() {
|
||||
if (!this.data.walk || this.data.walk.status !== "active") return;
|
||||
try {
|
||||
const pos = await wx.getLocation({ type: "gcj02" });
|
||||
const walk = await secure(
|
||||
"/walks/" + this.data.walk.id + "/points",
|
||||
"POST",
|
||||
{ latitude: pos.latitude, longitude: pos.longitude },
|
||||
);
|
||||
this.renderWalk(walk);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
renderWalk(walk) {
|
||||
const pts = (walk.points || []).map((p) => ({
|
||||
latitude: p.latitude,
|
||||
longitude: p.longitude,
|
||||
}));
|
||||
const last = pts[pts.length - 1];
|
||||
this.setData({
|
||||
walk,
|
||||
points: pts,
|
||||
polyline:
|
||||
pts.length > 1 ? [{ points: pts, color: "#268e6a", width: 5 }] : [],
|
||||
latitude: last ? last.latitude : this.data.latitude,
|
||||
longitude: last ? last.longitude : this.data.longitude,
|
||||
distance: (walk.distance_meters / 1000).toFixed(2),
|
||||
share: walk.share_enabled,
|
||||
});
|
||||
this.tick();
|
||||
},
|
||||
tick() {
|
||||
const walk = this.data.walk;
|
||||
if (!walk) return;
|
||||
const end = walk.ended_at
|
||||
? new Date(walk.ended_at)
|
||||
: walk.status === "paused"
|
||||
? new Date(walk.paused_at || Date.now())
|
||||
: new Date();
|
||||
const sec = Math.max(
|
||||
0,
|
||||
Math.floor((end - new Date(walk.started_at)) / 1000) -
|
||||
(walk.paused_seconds || 0),
|
||||
);
|
||||
this.setData({
|
||||
duration:
|
||||
String(Math.floor(sec / 60)).padStart(2, "0") +
|
||||
":" +
|
||||
String(sec % 60).padStart(2, "0"),
|
||||
});
|
||||
},
|
||||
async action(e) {
|
||||
const kind = e.currentTarget.dataset.kind;
|
||||
try {
|
||||
const walk = await secure(
|
||||
"/walks/" + this.data.walk.id + "/" + kind,
|
||||
"POST",
|
||||
);
|
||||
this.renderWalk(walk);
|
||||
if (kind === "finish") {
|
||||
clearInterval(this.tracker);
|
||||
wx.showToast({ title: "今天的快乐已记录", icon: "success" });
|
||||
}
|
||||
} catch (err) {
|
||||
toast(err);
|
||||
}
|
||||
},
|
||||
async shareChange(e) {
|
||||
try {
|
||||
const walk = await secure(
|
||||
"/walks/" + this.data.walk.id + "/share",
|
||||
"PUT",
|
||||
{ enabled: e.detail.value },
|
||||
);
|
||||
this.renderWalk(walk);
|
||||
} catch (err) {
|
||||
toast(err);
|
||||
}
|
||||
},
|
||||
onShareAppMessage() {
|
||||
const walk = this.data.walk;
|
||||
return {
|
||||
title: "肉垫计划 · 看看我们遛到哪儿了 🐾",
|
||||
path:
|
||||
walk && walk.share_enabled
|
||||
? "/pages/share/share?token=" + walk.share_token
|
||||
: "/pages/home/home",
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "usingComponents": { "tab-bar": "/components/tab-bar/index", "pet-picker": "/components/pet-picker/index" } }
|
||||
@@ -0,0 +1,80 @@
|
||||
<view class="screen walk-screen">
|
||||
<view class="eyebrow">PAWPRINT WALK</view>
|
||||
<view class="title">一起去兜风 🐾</view>
|
||||
<view class="map-box">
|
||||
<map
|
||||
id="walk-map"
|
||||
latitude="{{latitude}}"
|
||||
longitude="{{longitude}}"
|
||||
scale="16"
|
||||
show-location="{{walk!=null}}"
|
||||
polyline="{{polyline}}"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
<view class="map-tag">
|
||||
{{walk?walk.status==='active'?'记录中':'已暂停':'等待出发'}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="card session">
|
||||
<view class="eyebrow">{{walk?'快乐进行时':'出门走走吧'}}</view>
|
||||
<view class="session-title">
|
||||
{{walk?'跟小短腿一起探索':'选择今天同行的狗狗'}}
|
||||
</view>
|
||||
<view class="stats">
|
||||
<view>
|
||||
<strong>{{duration}}</strong>
|
||||
<text>时长</text>
|
||||
</view>
|
||||
<view>
|
||||
<strong>{{distance}}</strong>
|
||||
<text>公里</text>
|
||||
</view>
|
||||
<view>
|
||||
<strong>{{points.length}}</strong>
|
||||
<text>足迹</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="picker-title">同行伙伴 · 可多选</view>
|
||||
<pet-picker
|
||||
pets="{{pets}}"
|
||||
selected="{{walk?walk.pet_ids:selected}}"
|
||||
bind:change="pick"
|
||||
/>
|
||||
<view wx:if="{{!pets.length}}" class="empty">
|
||||
先到「宠物」添加你的柯基吧 🐕
|
||||
</view>
|
||||
<button
|
||||
wx:if="{{!walk||walk.status==='finished'}}"
|
||||
class="primary main-button"
|
||||
bindtap="start"
|
||||
>
|
||||
开始遛狗 →
|
||||
</button>
|
||||
<view wx:else class="actions">
|
||||
<button
|
||||
class="secondary"
|
||||
data-kind="{{walk.status==='active'?'pause':'resume'}}"
|
||||
bindtap="action"
|
||||
>
|
||||
{{walk.status==='active'?'暂停':'继续'}}
|
||||
</button>
|
||||
<button class="primary" data-kind="finish" bindtap="action">
|
||||
结束遛狗
|
||||
</button>
|
||||
</view>
|
||||
<view wx:if="{{walk&&walk.status!=='finished'}}" class="share-row">
|
||||
<view>
|
||||
<strong>分享实时位置</strong>
|
||||
<text>打开后可分享给朋友,结束遛狗会自动关闭</text>
|
||||
</view>
|
||||
<switch color="#278465" checked="{{share}}" bindchange="shareChange" />
|
||||
</view>
|
||||
<button wx:if="{{share}}" class="share-button" open-type="share">
|
||||
发给朋友看位置 ↗
|
||||
</button>
|
||||
</view>
|
||||
<view class="privacy">
|
||||
🔒 只有你主动打开分享时,朋友才能查看这次遛狗的位置。
|
||||
</view>
|
||||
</view>
|
||||
<tab-bar current="/pages/walk/walk" />
|
||||
@@ -0,0 +1,105 @@
|
||||
.walk-screen {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.walk-screen > .eyebrow,
|
||||
.walk-screen > .title {
|
||||
margin-left: 32rpx;
|
||||
}
|
||||
.map-box {
|
||||
height: 450rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.map-tag {
|
||||
position: absolute;
|
||||
top: 25rpx;
|
||||
left: 30rpx;
|
||||
background: white;
|
||||
color: #288363;
|
||||
padding: 12rpx 21rpx;
|
||||
border-radius: 99rpx;
|
||||
font-size: 23rpx;
|
||||
box-shadow: 0 8rpx 25rpx #173e3622;
|
||||
}
|
||||
.session {
|
||||
margin: -30rpx 25rpx 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 32rpx;
|
||||
}
|
||||
.session-title {
|
||||
font-size: 35rpx;
|
||||
font-weight: 800;
|
||||
margin: 12rpx 0 30rpx;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
border-bottom: 2rpx solid #edf1eb;
|
||||
padding-bottom: 25rpx;
|
||||
}
|
||||
.stats > view {
|
||||
flex: 1;
|
||||
border-right: 2rpx solid #e9efea;
|
||||
padding-left: 22rpx;
|
||||
}
|
||||
.stats > view:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
.stats > view:last-child {
|
||||
border: 0;
|
||||
}
|
||||
.stats strong {
|
||||
display: block;
|
||||
font-size: 38rpx;
|
||||
}
|
||||
.stats text {
|
||||
font-size: 22rpx;
|
||||
color: #99aa9d;
|
||||
}
|
||||
.picker-title {
|
||||
font-weight: 700;
|
||||
margin: 26rpx 0 18rpx;
|
||||
}
|
||||
.main-button {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 14rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
.actions button {
|
||||
flex: 1;
|
||||
}
|
||||
.share-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
.share-row strong,
|
||||
.share-row text {
|
||||
display: block;
|
||||
}
|
||||
.share-row text {
|
||||
font-size: 21rpx;
|
||||
color: #90a199;
|
||||
max-width: 450rpx;
|
||||
margin-top: 5rpx;
|
||||
}
|
||||
.share-button {
|
||||
margin-top: 20rpx;
|
||||
background: #e6f1e8;
|
||||
color: #267d59;
|
||||
border-radius: 20rpx;
|
||||
font-size: 25rpx;
|
||||
}
|
||||
.privacy {
|
||||
font-size: 22rpx;
|
||||
color: #739180;
|
||||
background: #e9f3e7;
|
||||
border-radius: 22rpx;
|
||||
padding: 22rpx;
|
||||
margin: 20rpx 25rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user