96c92f2cfb
- 去掉首页「全部完成」按钮,避免一键刷完不留记录 - 勾选带记录类型的任务 → 跳记录表单,保存成功后才回填任务完成, 返回首页 onShow 自动刷新任务勾选 + 最近记录(带 taskId 传参) - 只对未完成任务传 taskId,避免重复完成把已完成任务 toggle 回去 - 无记录类型的任务(如遛狗)仍直接勾选完成 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
171 lines
5.7 KiB
JavaScript
171 lines
5.7 KiB
JavaScript
const store = require('../../utils/store.js');
|
||
const api = require('../../utils/api.js');
|
||
const upload = require('../../utils/upload.js');
|
||
const recordTypes = require('../../utils/recordTypes.js');
|
||
const { toastErr } = require('../../utils/ui.js');
|
||
|
||
function pad2(n) {
|
||
return n < 10 ? '0' + n : '' + n;
|
||
}
|
||
function today() {
|
||
const d = new Date();
|
||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
||
}
|
||
// "2026-07-30" → "2026.07.30"
|
||
function fmtDate(s) {
|
||
return (s || '').replace(/-/g, '.');
|
||
}
|
||
// 后端用 time.Parse(time.RFC3339) 解 occurred_at,只给日期它解不出来会静默回退成
|
||
// time.Now()——选「昨天」会存成今天且不报错。时区要带真实偏移:转成 UTC 的 Z 形式
|
||
// 落库再转回本地时会把边界日期挪一天。取正午同样是为了远离日界。
|
||
function dayToISO(day) {
|
||
if (!day) return '';
|
||
const off = -new Date().getTimezoneOffset();
|
||
const sign = off >= 0 ? '+' : '-';
|
||
const a = Math.abs(off);
|
||
return day + 'T12:00:00' + sign + pad2(Math.floor(a / 60)) + ':' + pad2(a % 60);
|
||
}
|
||
|
||
Page({
|
||
data: {
|
||
code: '',
|
||
type: {},
|
||
fields: [],
|
||
values: [], // 和 fields 同下标
|
||
pet: {},
|
||
date: '',
|
||
dateLabel: '',
|
||
note: '',
|
||
images: [],
|
||
saving: false,
|
||
},
|
||
onLoad(q) {
|
||
const code = (q && q.type) || '';
|
||
// 从首页「今日任务」勾选进来会带 taskId:这一笔记录保存后要把那条任务标完成
|
||
this.taskId = (q && q.taskId) || '';
|
||
const d = today();
|
||
this.setData({ code, date: d, dateLabel: fmtDate(d) });
|
||
store
|
||
.ready()
|
||
.then(() => recordTypes.load())
|
||
.then(() => this.applyType())
|
||
.catch((e) => toastErr(e));
|
||
},
|
||
applyType() {
|
||
const t = recordTypes.get(this.data.code);
|
||
if (!t) {
|
||
wx.showToast({ title: '这个记录类型不存在了', icon: 'none' });
|
||
return setTimeout(() => wx.navigateBack(), 800);
|
||
}
|
||
const fields = t.fields || [];
|
||
// 单选默认不预选:预选了用户不点也会存下一个他没确认过的值
|
||
this.setData({ type: t, fields, values: fields.map(() => ''), pet: store.getPet() || {} });
|
||
},
|
||
// 多宠时切换记到哪只身上。用 actionSheet 而不是自己画一个选择器
|
||
pickPet() {
|
||
const pets = store.getPets() || [];
|
||
if (pets.length < 2) return;
|
||
wx.showActionSheet({
|
||
itemList: pets.map((p) => p.name),
|
||
success: (r) => {
|
||
const p = pets[r.tapIndex];
|
||
store.switchPet(p.id);
|
||
this.setData({ pet: p });
|
||
},
|
||
fail: () => {},
|
||
});
|
||
},
|
||
onDate(e) {
|
||
this.setData({ date: e.detail.value, dateLabel: fmtDate(e.detail.value) });
|
||
},
|
||
onField(e) {
|
||
this.setData({ ['values[' + e.currentTarget.dataset.index + ']']: e.detail.value });
|
||
},
|
||
onPickOpt(e) {
|
||
const { index, val } = e.currentTarget.dataset;
|
||
// 再点一下取消选择——单选组里没有「不选」按钮,只能靠反选
|
||
this.setData({ ['values[' + index + ']']: this.data.values[index] === val ? '' : val });
|
||
},
|
||
onNote(e) {
|
||
this.setData({ note: e.detail.value });
|
||
},
|
||
onPickImage() {
|
||
upload
|
||
.chooseAndUploadImage()
|
||
.then((f) => this.setData({ images: this.data.images.concat([f]) }))
|
||
.catch((e) => {
|
||
if (e && e.canceled) return;
|
||
toastErr(e, '上传失败');
|
||
});
|
||
},
|
||
onRemoveImage(e) {
|
||
const imgs = this.data.images.slice();
|
||
imgs.splice(e.currentTarget.dataset.index, 1);
|
||
this.setData({ images: imgs });
|
||
},
|
||
|
||
// 按字段配置组装请求体。哪个字段落到 num_value / category 完全由后端的
|
||
// to_category / kind 决定,前端不认识「体重」「金额」这些业务词
|
||
buildBody() {
|
||
const { fields, values } = this.data;
|
||
const body = { type: this.data.code, occurred_at: dayToISO(this.data.date) };
|
||
const parts = [];
|
||
fields.forEach((f, i) => {
|
||
const v = (values[i] || '').toString().trim();
|
||
if (!v) return;
|
||
if (f.kind === 'number') {
|
||
const n = parseFloat(v);
|
||
if (!isNaN(n)) {
|
||
body.num_value = n;
|
||
parts.push(v + (f.unit || ''));
|
||
}
|
||
return;
|
||
}
|
||
if (f.to_category) body.category = v;
|
||
parts.push(v);
|
||
});
|
||
body.title = parts.length ? this.data.type.label + ':' + parts.join(' ') : this.data.type.label;
|
||
body.description = (this.data.note || '').trim();
|
||
const img = this.data.images[0];
|
||
if (img) {
|
||
body.image_file_id = img.id;
|
||
body.image_url = img.url;
|
||
}
|
||
return body;
|
||
},
|
||
// 必填校验:数值字段空着存下来是一条没有数值的体重记录,趋势图上是个洞
|
||
missingField() {
|
||
const { fields, values } = this.data;
|
||
for (let i = 0; i < fields.length; i++) {
|
||
if (fields[i].kind === 'number' && !(values[i] || '').toString().trim()) {
|
||
return fields[i].label;
|
||
}
|
||
}
|
||
return '';
|
||
},
|
||
onSave() {
|
||
if (this.data.saving) return;
|
||
const id = store.currentPetId();
|
||
if (!id) return wx.showToast({ title: '先建一份宠物档案', icon: 'none' });
|
||
const miss = this.missingField();
|
||
if (miss) return wx.showToast({ title: '请填写' + miss, icon: 'none' });
|
||
|
||
this.setData({ saving: true });
|
||
api
|
||
.createRecord(id, this.buildBody())
|
||
.then(() => {
|
||
// 从任务勾选进来的:记录存好后把那条任务标完成(失败不影响记录已保存)
|
||
if (this.taskId) return api.toggleTask(this.taskId).catch(() => {});
|
||
})
|
||
.then(() => {
|
||
this.setData({ saving: false });
|
||
wx.showToast({ title: this.taskId ? '已完成' : '已记录', icon: 'success' });
|
||
setTimeout(() => wx.navigateBack(), 600);
|
||
})
|
||
.catch((e) => {
|
||
this.setData({ saving: false });
|
||
toastErr(e, '保存失败');
|
||
});
|
||
},
|
||
});
|