feat: 每日任务顺延与增删改 + 计划到期续期 + 靠谱养护模板 + 图片URL修复

后端
- 每日任务惰性生成:某天为空则从最近一天顺延复制(done 重置),首天用模板
- 任务增删改 API(POST /pets/:id/tasks、PUT/DELETE /tasks/:id),改动自动顺延
- 30 天计划到期自动归档并按当前阶段生成新一轮
- 内置养护模板重写为兽医常识向:狗每阶段含遛狗/牵引/狂犬,猫含猫砂/梳毛/饮水
- 文件 URL 改为按 object_name + 当前配置动态拼接,换 IP/域名不再有旧地址

小程序
- 首页今日任务加「管理」入口:增删改任务弹层
- 记录时间轴显示照片缩略图(可全屏预览)+ 拍照成功提示
- 社区发布条精简为单个「发布图文」按钮,去掉头像/输入框误触

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-06 10:06:13 +08:00
parent 2f5dd6eefc
commit 28b95a6468
21 changed files with 478 additions and 81 deletions
+1
View File
@@ -57,6 +57,7 @@ page{scrollbar-width:none;-ms-overflow-style:none}
.link{
color:var(--primary-dark);font-weight:900;font-size:26rpx;
}
.head-links{display:flex;align-items:center;gap:26rpx}
/* 页面标题 */
.page-title{margin:4rpx 0 28rpx}
@@ -20,6 +20,18 @@ function ageFromBirthday(bday) {
const GENDERS = ['男孩', '女孩', '不确定'];
const STAGES = ['刚到家 0-30 天', '幼年期', '成年期', '老年期'];
// 任务可关联的记录弹层类型
const TASK_SHEETS = [
{ key: '', label: '不关联' },
{ key: 'weight', label: '体重' },
{ key: 'poop', label: '便便' },
{ key: 'food', label: '饮食' },
{ key: 'symptom', label: '异常' },
{ key: 'vaccine', label: '疫苗' },
{ key: 'medicine', label: '用药' },
];
const TASK_SHEET_LABELS = TASK_SHEETS.map((s) => s.label);
const POOP = ['正常', '软便', '拉稀'];
const FOOD = ['正常', '偏少', '不吃'];
const COST = ['食品', '医疗', '用品'];
@@ -44,6 +56,9 @@ Component({
costAmount: '',
postContent: '',
postImages: [],
manageTasks: [],
taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 },
taskSheetLabels: TASK_SHEET_LABELS,
addName: '',
addBirthday: '',
addWeight: '',
@@ -85,6 +100,8 @@ Component({
patch.costAmount = '';
patch.postContent = '';
patch.postImages = [];
patch.manageTasks = [];
patch.taskForm = { id: '', title: '', description: '', priority: '', sheetIdx: 0 };
patch.addName = '';
patch.addBirthday = '';
patch.addWeight = '';
@@ -127,7 +144,9 @@ Component({
// 按弹层类型拉取真实数据
loadSheetData(type) {
const id = store.currentPetId();
if (type === 'comments' && this.data.postId) {
if (type === 'manageTasks') {
this.loadManageTasks();
} else if (type === 'comments' && this.data.postId) {
api.listComments(this.data.postId).then((page) => this.setData({ comments: page.list || [] })).catch(() => {});
} else if (type === 'reminders' && id) {
api
@@ -195,6 +214,53 @@ Component({
onWNote(e) { this.setData({ wNote: e.detail.value }); },
onCostInput(e) { this.setData({ costAmount: e.detail.value }); },
onPostInput(e) { this.setData({ postContent: e.detail.value }); },
// ---- 管理任务 ----
loadManageTasks() {
const id = store.currentPetId();
if (!id) return;
api.getTasks(id).then((tasks) => this.setData({ manageTasks: tasks || [] })).catch(() => {});
},
onTaskTitle(e) { this.setData({ 'taskForm.title': e.detail.value }); },
onTaskDesc(e) { this.setData({ 'taskForm.description': e.detail.value }); },
onTaskPriority() {
this.setData({ 'taskForm.priority': this.data.taskForm.priority === '重要' ? '' : '重要' });
},
onTaskSheet(e) { this.setData({ 'taskForm.sheetIdx': Number(e.detail.value) }); },
onEditTaskItem(e) {
const t = this.data.manageTasks[e.currentTarget.dataset.index];
let sheetIdx = TASK_SHEETS.findIndex((s) => s.key === (t.sheet_type || ''));
if (sheetIdx < 0) sheetIdx = 0;
this.setData({
taskForm: { id: t.id, title: t.title, description: t.description || '', priority: t.priority || '', sheetIdx },
});
},
onSaveTask() {
const f = this.data.taskForm;
const title = (f.title || '').trim();
if (!title) return wx.showToast({ title: '填个任务名', icon: 'none' });
const body = { title, description: f.description, priority: f.priority, sheet_type: TASK_SHEETS[f.sheetIdx].key };
const id = store.currentPetId();
const p = f.id ? api.updateTask(f.id, body) : api.createTask(id, body);
p.then(() => {
this.setData({ taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 } });
this.loadManageTasks();
this.triggerEvent('saved');
}).catch((e) => wx.showToast({ title: e.message || '保存失败', icon: 'none' }));
},
onDeleteTaskItem(e) {
const t = this.data.manageTasks[e.currentTarget.dataset.index];
wx.showModal({
title: '删除任务',
content: '确定删除「' + t.title + '」?',
success: (r) => {
if (!r.confirm) return;
api.deleteTask(t.id).then(() => {
this.loadManageTasks();
this.triggerEvent('saved');
}).catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
},
});
},
onPickPostImages() {
const left = 9 - this.data.postImages.length;
if (left <= 0) return wx.showToast({ title: '最多 9 张', icon: 'none' });
@@ -326,6 +392,7 @@ Component({
api.createRecord(id, { type: 'photo', icon: '📷', title: '成长照片', image_file_id: f.id, image_url: f.url }),
)
.then((saved) => {
wx.showToast({ title: '照片已保存', icon: 'success' });
this.triggerEvent('saved', saved);
this.close();
})
@@ -273,6 +273,34 @@ module.exports.sel = function (map, key, index, def) {
<button class="btn btn-primary btn-block" bindtap="onCreatePost">发布到宠友圈</button>
</block>
<!-- 管理任务 -->
<block wx:elif="{{innerType === 'manageTasks'}}">
<view class="sheet-h3">管理任务</view>
<view class="sheet-p">增删改今日任务,改动会自动顺延到以后每天。</view>
<view wx:for="{{manageTasks}}" wx:key="id" class="mtask-row">
<view class="mtask-body">
<text class="mtask-b">{{item.title}}<text wx:if="{{item.priority}}" class="mtask-pri">{{item.priority}}</text></text>
<view wx:if="{{item.description}}" class="mtask-p">{{item.description}}</view>
</view>
<view class="mtask-op" catchtap="onEditTaskItem" data-index="{{index}}">改</view>
<view class="mtask-op del" catchtap="onDeleteTaskItem" data-index="{{index}}">删</view>
</view>
<view wx:if="{{!manageTasks.length}}" class="sheet-p">今天还没有任务,下面加一条吧。</view>
<view class="field" style="margin-top:12rpx"><label>{{taskForm.id ? '修改任务' : '新增任务'}}</label>
<input class="input" placeholder="任务名,例如:喂驱虫药" placeholder-class="placeholder" value="{{taskForm.title}}" bindinput="onTaskTitle"/></view>
<view class="field"><input class="input" placeholder="说明(可选)" placeholder-class="placeholder" value="{{taskForm.description}}" bindinput="onTaskDesc"/></view>
<view class="field"><label>优先级 / 关联记录</label>
<view class="mtask-form-row">
<view class="seg-btn {{taskForm.priority === '重要' ? 'selected' : ''}}" catchtap="onTaskPriority">重要</view>
<picker mode="selector" range="{{taskSheetLabels}}" value="{{taskForm.sheetIdx}}" bindchange="onTaskSheet">
<view class="picker-box">关联:{{taskSheetLabels[taskForm.sheetIdx]}}</view>
</picker>
</view>
</view>
<button class="btn btn-primary btn-block" bindtap="onSaveTask">{{taskForm.id ? '保存修改' : '添加任务'}}</button>
</block>
<!-- 评论 -->
<block wx:elif="{{innerType === 'comments'}}">
<view class="sheet-h3">评论</view>
@@ -35,6 +35,18 @@
.picker-box{color:var(--text)}
.del-btn{background:var(--red-soft);color:var(--red)}
/* 管理任务 */
.mtask-row{display:flex;align-items:center;gap:16rpx;padding:20rpx 0;border-bottom:1rpx solid var(--line)}
.mtask-body{flex:1;min-width:0}
.mtask-b{font-size:28rpx;font-weight:700}
.mtask-pri{margin-left:12rpx;font-size:20rpx;font-weight:700;color:var(--primary-dark);background:#FFF2DC;padding:2rpx 12rpx;border-radius:999rpx}
.mtask-p{margin-top:6rpx;font-size:24rpx;color:var(--muted)}
.mtask-op{flex:none;font-size:26rpx;font-weight:700;color:var(--primary-dark);padding:8rpx 18rpx;background:#FBFAF8;border-radius:16rpx}
.mtask-op.del{color:var(--red)}
.mtask-form-row{display:flex;align-items:center;gap:16rpx}
.mtask-form-row .seg-btn{flex:none;padding:0 28rpx;height:72rpx;line-height:72rpx}
.mtask-form-row .picker-box{flex:1}
/* 图片选择器 */
.img-picker{display:flex;flex-wrap:wrap;gap:16rpx}
.img-thumb{position:relative;width:160rpx;height:160rpx;border-radius:20rpx;overflow:hidden}
+2 -4
View File
@@ -15,10 +15,8 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; }
class="feed-tab {{feedIdx === index ? 'active' : ''}}" data-index="{{index}}" bindtap="switchFeedTab">{{item}}</view>
</scroll-view>
<view class="compose-card" data-type="createPost" bindtap="openSheet">
<view class="post-avatar">{{pet.emoji}}</view>
<view class="compose-input">分享一下今天和 {{pet.name}} 的日常...</view>
<button class="btn btn-primary btn-small" catchtap="openSheet" data-type="createPost">发布</button>
<view class="compose-card">
<button class="btn btn-primary btn-block" bindtap="openSheet" data-type="createPost"> 发布图文</button>
</view>
<view wx:for="{{posts}}" wx:key="id" class="post-card">
+1 -9
View File
@@ -10,15 +10,7 @@
}
.feed-tab.active{color:var(--primary-dark);background:#FFF4E4;border-color:#FFD39A}
.compose-card{
display:flex;gap:20rpx;align-items:center;background:#fff;border-radius:44rpx;
box-shadow:var(--shadow);padding:24rpx;margin-bottom:28rpx;
}
.compose-input{
flex:1;height:76rpx;line-height:76rpx;border-radius:999rpx;background:#FBFAF8;
border:1rpx solid var(--line);color:var(--muted);padding:0 26rpx;font-size:26rpx;
overflow:hidden;white-space:nowrap;text-overflow:ellipsis;
}
.compose-card{margin-bottom:28rpx}
.post-card{background:#fff;border-radius:48rpx;box-shadow:var(--shadow);padding:30rpx;margin-bottom:28rpx}
.post-head{display:flex;align-items:center;gap:20rpx;margin-bottom:22rpx}
+4 -1
View File
@@ -133,7 +133,10 @@ Page({
this.setData({ sheetShow: false });
this.loadAll();
},
onSheetSave() {},
onSheetSave() {
this.loadTasks();
this.loadSummary();
},
onAddPet() {
this.openSheetType('addPet');
},
+5 -2
View File
@@ -42,8 +42,11 @@
<view class="card">
<view class="section-head">
<view class="sh-title">{{taskLabel}}</view>
<view wx:if="{{selectedDate && selectedDate !== todayDate}}" class="link" bindtap="backToToday">回今天</view>
<view wx:else class="link" bindtap="completeTasks">全部完成</view>
<view class="head-links">
<view class="link" data-type="manageTasks" bindtap="openSheet">管理</view>
<view wx:if="{{selectedDate && selectedDate !== todayDate}}" class="link" bindtap="backToToday">回今天</view>
<view wx:else class="link" bindtap="completeTasks">全部完成</view>
</view>
</view>
<view class="task-list">
<view wx:for="{{tasks}}" wx:key="title" class="task {{item.done ? 'done' : ''}}" data-index="{{index}}" bindtap="onTapTask">
+5
View File
@@ -19,6 +19,7 @@ function mapRecord(r) {
icon: r.icon || '✍️',
title: r.title,
desc: fmtTime(r.occurred_at) + (r.description ? '' + r.description : ''),
image: r.image_url || '',
};
}
@@ -151,6 +152,10 @@ Page({
const i = e.currentTarget.dataset.index;
this.setData({ selDot: this.data.trendDots[i], selIdx: i });
},
previewImage(e) {
const src = e.currentTarget.dataset.src;
if (src) wx.previewImage({ urls: [src], current: src });
},
openSheet(e) {
this.openSheetType(e.currentTarget.dataset.type);
},
+4 -1
View File
@@ -53,7 +53,10 @@
<view class="health-timeline">
<view wx:for="{{timeline}}" wx:key="index" class="health-event">
<view class="event-dot">{{item.icon}}</view>
<view><text class="he-b">{{item.title}}</text><view class="he-p">{{item.desc}}</view></view>
<view class="he-body">
<text class="he-b">{{item.title}}</text><view class="he-p">{{item.desc}}</view>
<image wx:if="{{item.image}}" class="he-img" src="{{item.image}}" mode="aspectFill" bindtap="previewImage" data-src="{{item.image}}"></image>
</view>
</view>
<view wx:if="{{timeline.length === 0}}" class="tl-empty">还没有记录,用上面的按钮记一条吧</view>
</view>
+2
View File
@@ -44,6 +44,8 @@
}
.he-b{font-size:28rpx;font-weight:700}
.he-p{color:var(--muted);font-size:24rpx;line-height:1.45;margin-top:6rpx}
.he-body{flex:1;min-width:0}
.he-img{margin-top:14rpx;width:220rpx;height:220rpx;border-radius:20rpx}
/* 页面不自身滚动,改由 page-scroll 承载滚动(隐藏滚动条)*/
page{height:100vh;overflow:hidden;display:flex;flex-direction:column}
+3
View File
@@ -58,6 +58,9 @@ const api = {
// 任务
getTasks: (id, date) => request({ url: `/api/pets/${id}/tasks${date ? `?date=${date}` : ''}` }),
createTask: (id, body) => request({ url: `/api/pets/${id}/tasks`, method: 'POST', data: body }),
updateTask: (taskId, body) => request({ url: `/api/tasks/${taskId}`, method: 'PUT', data: body }),
deleteTask: (taskId) => request({ url: `/api/tasks/${taskId}`, method: 'DELETE' }),
toggleTask: (taskId) => request({ url: `/api/tasks/${taskId}/toggle`, method: 'POST' }),
completeAllTasks: (id) => request({ url: `/api/pets/${id}/tasks/complete-all`, method: 'POST' }),
+1 -1
View File
@@ -1,5 +1,5 @@
// 后端地址。本地联调:微信开发者工具需在「详情 → 本地设置」勾选「不校验合法域名」。
// 上线时改为你的 https 域名,并在微信公众平台配置 request 合法域名。
const BASE_URL = 'http://192.168.31.4:8080';
const BASE_URL = 'http://192.168.31.3:8080';
module.exports = { BASE_URL };