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, '保存失败'); }); }, });