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 @@ 照顾备注