feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
const { secure, toast } = require("../../utils/api");
|
||||
const today = () => new Date().toLocaleDateString("sv-SE");
|
||||
Page({
|
||||
data: {
|
||||
pets: [],
|
||||
tasks: [],
|
||||
date: today(),
|
||||
showForm: false,
|
||||
title: "",
|
||||
petIndex: 0,
|
||||
category: "food",
|
||||
time_of_day: "08:30",
|
||||
repeat_rule: "daily",
|
||||
done: 0,
|
||||
},
|
||||
onShow() {
|
||||
this.load();
|
||||
},
|
||||
async load() {
|
||||
try {
|
||||
const [pets, result] = await Promise.all([
|
||||
secure("/pets"),
|
||||
secure("/tasks?date=" + this.data.date),
|
||||
]);
|
||||
const tasks = result.items.map((t) => ({
|
||||
...t,
|
||||
petName: (pets.find((p) => p.id === t.pet_id) || {}).name || "宠物",
|
||||
}));
|
||||
this.setData({
|
||||
pets,
|
||||
tasks,
|
||||
done: tasks.filter((t) => t.completed).length,
|
||||
});
|
||||
} catch (e) {
|
||||
toast(e);
|
||||
}
|
||||
},
|
||||
async toggle(e) {
|
||||
const t = this.data.tasks[e.currentTarget.dataset.index];
|
||||
try {
|
||||
await secure("/tasks/" + t.id + "/completion", "PUT", {
|
||||
date: this.data.date,
|
||||
completed: !t.completed,
|
||||
});
|
||||
this.load();
|
||||
} catch (err) {
|
||||
toast(err);
|
||||
}
|
||||
},
|
||||
open() {
|
||||
this.setData({ showForm: true });
|
||||
},
|
||||
close() {
|
||||
this.setData({ showForm: false });
|
||||
},
|
||||
field(e) {
|
||||
this.setData({ [e.currentTarget.dataset.key]: e.detail.value });
|
||||
},
|
||||
petChange(e) {
|
||||
this.setData({ petIndex: Number(e.detail.value) });
|
||||
},
|
||||
timeChange(e) {
|
||||
this.setData({ time_of_day: e.detail.value });
|
||||
},
|
||||
categoryChange(e) {
|
||||
this.setData({ category: e.detail.value });
|
||||
},
|
||||
repeatChange(e) {
|
||||
this.setData({ repeat_rule: e.detail.value });
|
||||
},
|
||||
async save() {
|
||||
if (!this.data.pets.length || !this.data.title.trim()) {
|
||||
wx.showToast({ title: "请选择宠物并填写任务名", icon: "none" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await secure("/tasks", "POST", {
|
||||
pet_id: this.data.pets[this.data.petIndex].id,
|
||||
title: this.data.title,
|
||||
category: this.data.category,
|
||||
time_of_day: this.data.time_of_day,
|
||||
repeat_rule: this.data.repeat_rule,
|
||||
});
|
||||
this.setData({ showForm: false, title: "" });
|
||||
this.load();
|
||||
} catch (e) {
|
||||
toast(e);
|
||||
}
|
||||
},
|
||||
async remove(e) {
|
||||
const t = this.data.tasks[e.currentTarget.dataset.index];
|
||||
const result = await wx.showModal({
|
||||
title: "删除任务",
|
||||
content: "确定删除「" + t.title + "」吗?",
|
||||
});
|
||||
if (!result.confirm) return;
|
||||
try {
|
||||
await secure("/tasks/" + t.id, "DELETE");
|
||||
this.load();
|
||||
} catch (err) {
|
||||
toast(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{ "usingComponents": { "tab-bar": "/components/tab-bar/index" } }
|
||||
@@ -0,0 +1,134 @@
|
||||
<view class="screen">
|
||||
<view class="eyebrow">LITTLE THINGS, BIG LOVE</view>
|
||||
<view class="title">今日小任务 ☑</view>
|
||||
<view class="summary">
|
||||
<view>今天也要照顾好它们</view>
|
||||
<strong>
|
||||
{{done}}
|
||||
<text>/ {{tasks.length}}</text>
|
||||
</strong>
|
||||
<view class="progress">
|
||||
<view style="width:{{tasks.length?done/tasks.length*100:0}}%"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="section-title">{{date}} 的日常</text>
|
||||
<text class="add" bindtap="open">+ 添加任务</text>
|
||||
</view>
|
||||
<view class="card list">
|
||||
<view wx:for="{{tasks}}" wx:key="id" class="task">
|
||||
<view
|
||||
class="check {{item.completed?'done':''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="toggle"
|
||||
>
|
||||
{{item.completed?'✓':''}}
|
||||
</view>
|
||||
<view class="icon">
|
||||
{{item.category==='water'?'💧':item.category==='food'?'🥣':'🐾'}}
|
||||
</view>
|
||||
<view class="info" data-index="{{index}}" bindtap="toggle">
|
||||
<strong>{{item.title}}</strong>
|
||||
<text>
|
||||
{{item.petName}} · {{item.time_of_day}} ·
|
||||
{{item.repeat_rule==='daily'?'每天':item.repeat_rule==='weekdays'?'工作日':'周末'}}
|
||||
</text>
|
||||
</view>
|
||||
<text class="remove" data-index="{{index}}" bindtap="remove">×</text>
|
||||
</view>
|
||||
<view wx:if="{{!tasks.length}}" class="empty">
|
||||
从喝水、喂食开始,安排温柔的小日常 ✨
|
||||
</view>
|
||||
</view>
|
||||
<button class="primary" bindtap="open">+ 新建日常任务</button>
|
||||
</view>
|
||||
<view wx:if="{{showForm}}" class="overlay">
|
||||
<view class="sheet">
|
||||
<view class="row">
|
||||
<strong>新建小任务</strong>
|
||||
<text bindtap="close">×</text>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">任务名称</view>
|
||||
<input
|
||||
placeholder="比如:更换饮水"
|
||||
value="{{title}}"
|
||||
data-key="title"
|
||||
bindinput="field"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">照顾谁</view>
|
||||
<picker
|
||||
range="{{pets}}"
|
||||
range-key="name"
|
||||
value="{{petIndex}}"
|
||||
bindchange="petChange"
|
||||
>
|
||||
<view class="picker">{{pets[petIndex].name||'请先添加宠物'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">类型</view>
|
||||
<radio-group bindchange="categoryChange">
|
||||
<label>
|
||||
<radio value="food" checked="{{category==='food'}}" color="#194f42" />
|
||||
喂食
|
||||
</label>
|
||||
<label>
|
||||
<radio
|
||||
value="water"
|
||||
checked="{{category==='water'}}"
|
||||
color="#194f42"
|
||||
/>
|
||||
喝水
|
||||
</label>
|
||||
<label>
|
||||
<radio
|
||||
value="other"
|
||||
checked="{{category==='other'}}"
|
||||
color="#194f42"
|
||||
/>
|
||||
其他
|
||||
</label>
|
||||
</radio-group>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">时间</view>
|
||||
<picker mode="time" value="{{time_of_day}}" bindchange="timeChange">
|
||||
<view class="picker">{{time_of_day}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">重复</view>
|
||||
<radio-group bindchange="repeatChange">
|
||||
<label>
|
||||
<radio
|
||||
value="daily"
|
||||
checked="{{repeat_rule==='daily'}}"
|
||||
color="#194f42"
|
||||
/>
|
||||
每天
|
||||
</label>
|
||||
<label>
|
||||
<radio
|
||||
value="weekdays"
|
||||
checked="{{repeat_rule==='weekdays'}}"
|
||||
color="#194f42"
|
||||
/>
|
||||
工作日
|
||||
</label>
|
||||
<label>
|
||||
<radio
|
||||
value="weekends"
|
||||
checked="{{repeat_rule==='weekends'}}"
|
||||
color="#194f42"
|
||||
/>
|
||||
周末
|
||||
</label>
|
||||
</radio-group>
|
||||
</view>
|
||||
<button class="primary" bindtap="save">保存任务</button>
|
||||
</view>
|
||||
</view>
|
||||
<tab-bar current="/pages/tasks/tasks" />
|
||||
@@ -0,0 +1,123 @@
|
||||
.summary {
|
||||
background: #194f42;
|
||||
color: white;
|
||||
border-radius: 34rpx;
|
||||
padding: 34rpx;
|
||||
}
|
||||
.summary > view:first-child {
|
||||
font-size: 24rpx;
|
||||
color: #bbe3c4;
|
||||
}
|
||||
.summary strong {
|
||||
display: block;
|
||||
font-size: 55rpx;
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
.summary strong text {
|
||||
font-size: 30rpx;
|
||||
color: #b4d8be;
|
||||
}
|
||||
.progress {
|
||||
height: 13rpx;
|
||||
background: #3f7566;
|
||||
border-radius: 99rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress view {
|
||||
height: 100%;
|
||||
background: #e9ad6a;
|
||||
border-radius: 99rpx;
|
||||
}
|
||||
.add {
|
||||
color: #2b8a63;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.list {
|
||||
padding: 8rpx 26rpx;
|
||||
}
|
||||
.task {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid #eef2ee;
|
||||
}
|
||||
.task:last-child {
|
||||
border: 0;
|
||||
}
|
||||
.check {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
border: 3rpx solid #cbdcd1;
|
||||
border-radius: 13rpx;
|
||||
text-align: center;
|
||||
line-height: 37rpx;
|
||||
color: white;
|
||||
}
|
||||
.check.done {
|
||||
background: #389867;
|
||||
border-color: #389867;
|
||||
}
|
||||
.icon {
|
||||
width: 65rpx;
|
||||
height: 65rpx;
|
||||
background: #eff5ee;
|
||||
border-radius: 18rpx;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 34rpx;
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
}
|
||||
.info strong,
|
||||
.info text {
|
||||
display: block;
|
||||
}
|
||||
.info text {
|
||||
font-size: 21rpx;
|
||||
color: #92a498;
|
||||
margin-top: 5rpx;
|
||||
}
|
||||
.remove {
|
||||
font-size: 36rpx;
|
||||
color: #c4d0c7;
|
||||
}
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #102a24a0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.sheet {
|
||||
width: 100%;
|
||||
background: #f6f9f6;
|
||||
border-radius: 35rpx 35rpx 0 0;
|
||||
padding: 32rpx 32rpx calc(env(safe-area-inset-bottom) + 30rpx);
|
||||
max-height: 85vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.sheet > .row strong {
|
||||
font-size: 33rpx;
|
||||
}
|
||||
.sheet > .row text {
|
||||
font-size: 43rpx;
|
||||
}
|
||||
.sheet radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15rpx;
|
||||
}
|
||||
.sheet label {
|
||||
font-size: 23rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.picker {
|
||||
background: white;
|
||||
padding: 23rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user