diff --git a/pets-fe/README.md b/pets-fe/README.md
new file mode 100644
index 0000000..d49987e
--- /dev/null
+++ b/pets-fe/README.md
@@ -0,0 +1,20 @@
+# 肉垫计划小程序
+
+用微信开发者工具导入 `pets-fe`,AppID 见 `project.config.json`。接口基础地址在 `utils/config.js`,正式环境为 `https://pet.sundynix.cn/api`。请求封装会追加 `/pets`、`/walks` 等路径,不会再追加一层 `/api`。
+
+## 核心流程
+
+- 宠物:添加、编辑、上传头像、归档;档案使用文件 ID,服务端返回可显示的头像地址。
+- 日常任务:按日期查看完成情况,新增、编辑、删除任务,记录或取消当天完成状态。
+- 遛狗:可一次选择多只宠物,记录轨迹、暂停、继续、结束;历史页可查看路线和同行宠物。
+- 位置分享:开始遛狗后主动开启,分享给微信好友;结束遛狗自动关闭。定位更新发生在小程序打开期间,后台持续定位还需微信对应类目与接口权限。
+- 约遛:发起活动、选择地点和同行宠物、报名、查看报名状态、退出或取消活动。
+
+## 真机检查
+
+1. 登录后添加至少两只宠物,确认头像上传与档案编辑。
+2. 新建喝水、喂食任务,编辑时间并切换日期,验证完成状态。
+3. 同时选两只宠物开始遛狗,确认定位、暂停/继续、分享和结束;在历史页查看记录。
+4. 用另一个微信账号报名约遛,确认已报名和退出状态。
+
+小程序源码约 600 KB,低于微信主包 2 MB 限制。真机调试前请确认小程序后台已配置 `pet.sundynix.cn` 为合法请求域名。
diff --git a/pets-fe/app.js b/pets-fe/app.js
index 8a2d4a6..8d6cf13 100644
--- a/pets-fe/app.js
+++ b/pets-fe/app.js
@@ -7,21 +7,10 @@ App({
if (this.globalData.token) return this.globalData.token;
if (!this.loginPromise)
this.loginPromise = (async () => {
- const { isSimulator } = require("./utils/config");
- const { request } = require("./utils/api");
- let data;
- try {
+ const { request } = require("./utils/api");
const { code } = await wx.login();
- data = await request("/auth/login", "POST", { code }, false);
- } catch (error) {
- if (!isSimulator) throw error;
- data = await request(
- "/auth/dev",
- "POST",
- { key: "wechat-devtools", nickname: "本地铲屎官" },
- false,
- );
- }
+ if (!code) throw new Error("微信登录未取得凭证,请重试");
+ const data = await request("/auth/login", "POST", { code }, false);
this.globalData.token = data.token;
this.globalData.user = data.user;
wx.setStorageSync("pet_token", data.token);
diff --git a/pets-fe/app.json b/pets-fe/app.json
index 4720527..501cfe2 100644
--- a/pets-fe/app.json
+++ b/pets-fe/app.json
@@ -1,5 +1,5 @@
{
- "pages": ["pages/home/home", "pages/meetups/meetups", "pages/walk/walk", "pages/tasks/tasks", "pages/pets/pets", "pages/pet-edit/pet-edit", "pages/meetup-edit/meetup-edit", "pages/share/share"],
+ "pages": ["pages/home/home", "pages/meetups/meetups", "pages/walk/walk", "pages/tasks/tasks", "pages/pets/pets", "pages/pet-edit/pet-edit", "pages/meetup-edit/meetup-edit", "pages/share/share", "pages/walk-records/walk-records"],
"window": { "navigationStyle": "custom", "backgroundColor": "#f6f9f6", "backgroundTextStyle": "dark" },
"permission": { "scope.userLocation": { "desc": "遛狗时记录轨迹,并在你主动开启分享后展示实时位置" } },
"requiredPrivateInfos": ["getLocation", "chooseLocation"],
diff --git a/pets-fe/components/pet-picker/index.js b/pets-fe/components/pet-picker/index.js
index a065da5..16720d5 100644
--- a/pets-fe/components/pet-picker/index.js
+++ b/pets-fe/components/pet-picker/index.js
@@ -2,6 +2,7 @@ Component({
properties: {
pets: { type: Array, value: [] },
selected: { type: Array, value: [] },
+ disabled: { type: Boolean, value: false },
},
data: { viewPets: [] },
observers: {
@@ -16,11 +17,18 @@ Component({
},
methods: {
toggle(e) {
+ if (this.data.disabled) return;
const id = e.currentTarget.dataset.id;
const selected = this.data.selected.includes(id)
? this.data.selected.filter((x) => x !== id)
: [...this.data.selected, id];
this.triggerEvent("change", { selected });
},
+ selectAll() {
+ if (this.data.disabled) return;
+ const ids = this.data.pets.map((pet) => pet.id);
+ const allSelected = ids.every((id) => this.data.selected.includes(id));
+ this.triggerEvent("change", { selected: allSelected ? [] : ids });
+ },
},
});
diff --git a/pets-fe/components/pet-picker/index.wxml b/pets-fe/components/pet-picker/index.wxml
index 90c68bf..65456f2 100644
--- a/pets-fe/components/pet-picker/index.wxml
+++ b/pets-fe/components/pet-picker/index.wxml
@@ -1,4 +1,7 @@
+
+ ✓全选 / 清空
+
pets.some((p) => p.id === id)),
@@ -50,14 +51,14 @@ Page({
wx.redirectTo({ url: "/pages/tasks/tasks" });
},
goWalks() {
- wx.redirectTo({ url: "/pages/walk/walk" });
+ wx.navigateTo({ url: "/pages/walk-records/walk-records" });
},
async complete(e) {
const id = e.currentTarget.dataset.id,
task = this.data.tasks.find((t) => t.id === id);
try {
await secure("/tasks/" + id + "/completion", "PUT", {
- date: new Date().toLocaleDateString("sv-SE"),
+ date: localDate(),
completed: !task.completed,
});
await this.load();
diff --git a/pets-fe/pages/meetup-edit/meetup-edit.js b/pets-fe/pages/meetup-edit/meetup-edit.js
index ab0a991..6a1b8c2 100644
--- a/pets-fe/pages/meetup-edit/meetup-edit.js
+++ b/pets-fe/pages/meetup-edit/meetup-edit.js
@@ -11,6 +11,7 @@ Page({
date: "",
time: "",
max_people: 8,
+ saving: false,
},
onLoad() {
this.load();
@@ -47,6 +48,7 @@ Page({
}
},
async save() {
+ if (this.data.saving) return;
const d = this.data;
if (
!d.title.trim() ||
@@ -58,6 +60,16 @@ Page({
wx.showToast({ title: "请填完时间地点并选择狗狗", icon: "none" });
return;
}
+ const startsAt = new Date(d.date + "T" + d.time + ":00+08:00");
+ if (startsAt <= new Date()) {
+ wx.showToast({ title: "请选择未来的时间", icon: "none" });
+ return;
+ }
+ if (Number(d.max_people) < 2 || Number(d.max_people) > 100) {
+ wx.showToast({ title: "人数应为 2 到 100", icon: "none" });
+ return;
+ }
+ this.setData({ saving: true });
try {
await secure("/meetups", "POST", {
title: d.title,
@@ -65,13 +77,15 @@ Page({
place_name: d.place_name,
latitude: d.latitude,
longitude: d.longitude,
- starts_at: new Date(d.date + "T" + d.time + ":00+08:00").toISOString(),
+ starts_at: startsAt.toISOString(),
max_people: Number(d.max_people),
pet_ids: d.selected,
});
wx.navigateBack();
} catch (e) {
toast(e);
+ } finally {
+ this.setData({ saving: false });
}
},
});
diff --git a/pets-fe/pages/meetup-edit/meetup-edit.wxml b/pets-fe/pages/meetup-edit/meetup-edit.wxml
index 6493ebd..c3dad85 100644
--- a/pets-fe/pages/meetup-edit/meetup-edit.wxml
+++ b/pets-fe/pages/meetup-edit/meetup-edit.wxml
@@ -49,5 +49,5 @@
带哪些狗狗?可以多选
-
+
diff --git a/pets-fe/pages/meetups/meetups.js b/pets-fe/pages/meetups/meetups.js
index 119c867..19bfc70 100644
--- a/pets-fe/pages/meetups/meetups.js
+++ b/pets-fe/pages/meetups/meetups.js
@@ -1,6 +1,6 @@
const { secure, toast } = require("../../utils/api");
Page({
- data: { meetups: [], pets: [], selected: [], active: null, userId: "" },
+ data: { meetups: [], pets: [], selected: [], active: null, userId: "", joined: false, busy: false },
onShow() {
this.load();
},
@@ -26,46 +26,83 @@ Page({
create() {
wx.navigateTo({ url: "/pages/meetup-edit/meetup-edit" });
},
- open(e) {
- this.setData({
- active: this.data.meetups[e.currentTarget.dataset.index],
- selected: [],
- });
+ async open(e) {
+ const summary = this.data.meetups[e.currentTarget.dataset.index];
+ try {
+ const active = await secure("/meetups/" + summary.id);
+ const participant = (active.people || []).find((person) => person.user_id === this.data.userId);
+ this.setData({
+ active: { ...active, date: new Date(active.starts_at).toLocaleString("zh-CN") },
+ joined: !!participant,
+ selected: participant ? participant.pet_ids : [],
+ });
+ } catch (error) {
+ toast(error);
+ }
},
close() {
- this.setData({ active: null });
+ this.setData({ active: null, joined: false, selected: [] });
},
pick(e) {
this.setData({ selected: e.detail.selected });
},
async join() {
+ if (this.data.busy) return;
if (!this.data.selected.length) {
wx.showToast({ title: "请选择同行的狗狗", icon: "none" });
return;
}
+ this.setData({ busy: true });
try {
await secure("/meetups/" + this.data.active.id + "/join", "POST", {
pet_ids: this.data.selected,
});
wx.showToast({ title: "报名成功", icon: "success" });
this.close();
- this.load();
+ await this.load();
} catch (e) {
toast(e);
+ } finally {
+ this.setData({ busy: false });
+ }
+ },
+ async leave() {
+ if (this.data.busy) return;
+ const result = await wx.showModal({ title: "退出约遛", content: "确定不参加这场约遛了吗?" });
+ if (!result.confirm) return;
+ this.setData({ busy: true });
+ try {
+ await secure("/meetups/" + this.data.active.id + "/join", "DELETE");
+ this.close();
+ await this.load();
+ } catch (error) {
+ toast(error);
+ } finally {
+ this.setData({ busy: false });
+ }
+ },
+ location() {
+ const active = this.data.active;
+ if (active && active.latitude && active.longitude) {
+ wx.openLocation({ latitude: active.latitude, longitude: active.longitude, name: active.place_name, scale: 16 });
}
},
async cancel() {
+ if (this.data.busy) return;
const r = await wx.showModal({
title: "取消约遛",
content: "确定取消这场约遛吗?",
});
if (!r.confirm) return;
+ this.setData({ busy: true });
try {
await secure("/meetups/" + this.data.active.id + "/cancel", "POST");
this.close();
- this.load();
+ await this.load();
} catch (e) {
toast(e);
+ } finally {
+ this.setData({ busy: false });
}
},
});
diff --git a/pets-fe/pages/meetups/meetups.wxml b/pets-fe/pages/meetups/meetups.wxml
index d0b5500..9d4ad6a 100644
--- a/pets-fe/pages/meetups/meetups.wxml
+++ b/pets-fe/pages/meetups/meetups.wxml
@@ -11,7 +11,7 @@
- 附近的约遛
+ 即将开始的约遛
+ 发起约遛
×
- 📍 {{active.place_name}}
+ 📍 {{active.place_name}} {{active.latitude?'· 点开地图':''}}
🗓 {{active.date}}
🐕 {{active.participants}} / {{active.max_people}} 人已报名
{{active.description}}
-
+
带哪几只?可以多选
-
+
-
+
+
diff --git a/pets-fe/pages/pet-edit/pet-edit.js b/pets-fe/pages/pet-edit/pet-edit.js
index 7c1a19f..f532f37 100644
--- a/pets-fe/pages/pet-edit/pet-edit.js
+++ b/pets-fe/pages/pet-edit/pet-edit.js
@@ -12,6 +12,7 @@ Page({
care_notes: "",
avatar_file_id: "",
avatar_url: "",
+ saving: false,
},
onLoad(options) {
if (options.id) {
@@ -45,6 +46,7 @@ Page({
}
},
async save() {
+ if (this.data.saving) return;
if (!this.data.name.trim()) {
wx.showToast({ title: "请输入名字", icon: "none" });
return;
@@ -61,6 +63,7 @@ Page({
care_notes: this.data.care_notes,
avatar_file_id: this.data.avatar_file_id || null,
};
+ this.setData({ saving: true });
try {
await secure(
"/pets" + (this.data.id ? "/" + this.data.id : ""),
@@ -70,6 +73,8 @@ Page({
wx.navigateBack();
} catch (e) {
toast(e);
+ } finally {
+ this.setData({ saving: false });
}
},
async archive() {
diff --git a/pets-fe/pages/pet-edit/pet-edit.wxml b/pets-fe/pages/pet-edit/pet-edit.wxml
index 7b51217..502fc78 100644
--- a/pets-fe/pages/pet-edit/pet-edit.wxml
+++ b/pets-fe/pages/pet-edit/pet-edit.wxml
@@ -59,6 +59,6 @@
照顾备注
-
+
diff --git a/pets-fe/pages/share/share.js b/pets-fe/pages/share/share.js
index 709f326..d6a5548 100644
--- a/pets-fe/pages/share/share.js
+++ b/pets-fe/pages/share/share.js
@@ -9,15 +9,23 @@ Page({
},
onLoad(options) {
this.token = options.token;
+ if (!this.token) this.setData({ status: "分享链接无效" });
+ },
+ onShow() {
+ if (!this.token) return;
this.load();
+ clearInterval(this.timer);
this.timer = setInterval(() => this.load(), 15000);
},
+ onHide() {
+ clearInterval(this.timer);
+ },
onUnload() {
clearInterval(this.timer);
},
async load() {
try {
- const p = await request("/share/walk/" + this.token);
+ const p = await request("/share/walk/" + this.token, "GET", null, false);
this.setData({
latitude: p.latitude,
longitude: p.longitude,
diff --git a/pets-fe/pages/share/share.wxml b/pets-fe/pages/share/share.wxml
index 05d60b5..a7c26ec 100644
--- a/pets-fe/pages/share/share.wxml
+++ b/pets-fe/pages/share/share.wxml
@@ -1,7 +1,7 @@
肉垫计划 · 位置分享
找到小短腿的脚印 🐾
-