feat(fe): 记录页 24 种 4 组,弹层加通用简易表单
## 通用简易表单
洗护/清洁那 15 种只需要「什么时候 + 备注 + 可选照片」。弹层现在是 28 个
wx:elif 手写分支(402 行),各写一个会推到 700 行,而且每加一种类型都要发版。
加一个 wx:elif="{{isSimple}}",isSimple 由 JS 按后端返回的 form 字段算,
和上面那 9 种永不重叠。插在 photo 之后、提醒中心之前,没动任何现有分支的顺序。
备注给了逐类型的具体占位提示(「用了什么沐浴露、有没有吹干」),
比「请输入备注」有用得多——用户看到提示才知道这栏该写什么。
## 一个不写就会静默出错的地方
dayToISO():后端用 time.Parse(time.RFC3339) 解 occurred_at,只给日期
("2026-07-30")它解不出来会**静默回退成 time.Now()**——用户选「昨天洗澡」
会存成今天,而且不报错。
时区必须带真实偏移:转成 UTC 的 Z 形式,落库再转回本地时会把边界日期挪一天
(首页任务重复生成那个 bug 就是这么来的)。取正午同样是为了远离日界。
## 四处写死的类型列表全删
RECORD_ITEMS record.js 记录页 9 宫格
QUICK_ITEMS home.js 首页 8 宫格
TYPE_TONE record.js 时间轴色调(9 种写死,新类型会全掉到橙色一片分不清)
TASK_SHEETS 弹层 任务关联记录类型的下拉(只有 6 种,加了类型选不到)
全部改成读 utils/recordTypes.js 这份共用缓存。缓存 load() 只真正请求一次,
并发调用共用同一个 promise;isSimple/label/groups 是同步的,弹层打开时
不该再等一次网络。
首页 8 宫格改成取后端前 8 个(按分组顺序再按 sort),后台调 sort 就能换首屏
露出哪几个;全部 24 种在记录页。
分组色系(daily橙/health绿/care紫/clean蓝)留在前端,后端只存 group key——
改配色不用动数据。
## 联调(预生产库实跑)
15 种新类型 全部落库,且选「昨天」没被存成今天(15/15)
原有 9 种 逐一回归,写库 emoji 原样;weight 回写宠物档案 → 4.5kg ✓
时间轴 24 条混排,色调分布 6/7/5/6,没有一条查不到类型
体重趋势 仍只有 1 个点(没被 24 条记录污染)
周报/首页汇总 正常
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const store = require('../../utils/store.js');
|
||||
const recordTypes = require('../../utils/recordTypes.js');
|
||||
const api = require('../../utils/api.js');
|
||||
const upload = require('../../utils/upload.js');
|
||||
|
||||
@@ -15,6 +16,21 @@ function daysLater(n) {
|
||||
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
||||
}
|
||||
|
||||
// "2026-07-30" → "2026-07-30T12:00:00+08:00"
|
||||
//
|
||||
// 后端用 time.Parse(time.RFC3339) 解 occurred_at,只给日期它解不出来会静默
|
||||
// 回退成 time.Now()——用户选了「昨天洗澡」会存成今天,而且不报错。
|
||||
// 时区必须带真实偏移:转成 UTC 的 Z 形式,落库再转回本地时会把边界日期挪一天
|
||||
// (首页任务那个 bug 就是这么来的)。取正午同样是为了远离日界。
|
||||
function dayToISO(day) {
|
||||
if (!day) return '';
|
||||
const off = -new Date().getTimezoneOffset(); // 东八区是 +480
|
||||
const sign = off >= 0 ? '+' : '-';
|
||||
const a = Math.abs(off);
|
||||
const p = (x) => (x < 10 ? '0' + x : '' + x);
|
||||
return day + 'T12:00:00' + sign + p(Math.floor(a / 60)) + ':' + p(a % 60);
|
||||
}
|
||||
|
||||
// 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义
|
||||
function fmtAgo(iso) {
|
||||
if (!iso) return '';
|
||||
@@ -43,16 +59,15 @@ 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);
|
||||
// 任务能关联的记录类型。原来这里也是写死的 6 种,后台加了类型这个下拉里看不到,
|
||||
// 于是「自定义任务关联新类型」这条路是断的。改成读同一份类型缓存
|
||||
function taskSheets() {
|
||||
const list = [{ key: '', label: '不关联' }];
|
||||
(recordTypes.groups() || []).forEach((g) =>
|
||||
g.items.forEach((t) => list.push({ key: t.code, label: t.label })),
|
||||
);
|
||||
return list;
|
||||
}
|
||||
|
||||
// 提醒类型(与后端 model.Reminder* 常量对应)
|
||||
const REMINDER_TYPES = [
|
||||
@@ -63,6 +78,26 @@ const REMINDER_TYPES = [
|
||||
];
|
||||
const REMINDER_LABELS = REMINDER_TYPES.map((t) => t.label);
|
||||
|
||||
// 简易表单的备注提示。给一句具体的比「请输入备注」有用得多——
|
||||
// 用户看到「用了什么沐浴露、有没有吹干」才知道这栏该写什么
|
||||
const SIMPLE_HINT = {
|
||||
water: '大概喝了多少、换水了没有',
|
||||
bath: '用了什么沐浴露、有没有吹干',
|
||||
nail: '剪了几只爪、有没有出血',
|
||||
ear: '耳道干不干净、有没有异味',
|
||||
tooth: '用了什么牙膏、配合度怎么样',
|
||||
brush: '掉毛多不多、有没有打结',
|
||||
groom: '在哪家做的、剪了什么造型、花了多少',
|
||||
litter: '换了多少、用的什么砂',
|
||||
litterbox: '洗了几个、有没有消毒',
|
||||
bowl: '有没有用洗碗液、有没有滑腻感',
|
||||
waterbowl: '滤芯还好吗、有没有水垢',
|
||||
clean: '消了哪些地方、用的什么消毒液',
|
||||
checkup: '在哪家做的、结果怎么样、下次什么时候',
|
||||
vet: '什么症状、医生怎么说、开了什么药',
|
||||
supplement: '吃的什么、多大剂量、吃多久',
|
||||
};
|
||||
|
||||
const POOP = ['正常', '软便', '拉稀'];
|
||||
const FOOD = ['正常', '偏少', '不吃'];
|
||||
const COST = ['食品', '医疗', '用品'];
|
||||
@@ -83,6 +118,13 @@ Component({
|
||||
segSel: {},
|
||||
optSel: {},
|
||||
dateVals: { vaccine: '', deworm: '' },
|
||||
// 通用简易记录(洗护/清洁那 15 种共用)
|
||||
isSimple: false,
|
||||
simpleLabel: '',
|
||||
simpleDate: '',
|
||||
simpleNote: '',
|
||||
simpleImages: [],
|
||||
simplePlaceholder: '',
|
||||
wInput: '',
|
||||
wNote: '',
|
||||
poopNote: '',
|
||||
@@ -95,7 +137,7 @@ Component({
|
||||
postImages: [],
|
||||
manageTasks: [],
|
||||
taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 },
|
||||
taskSheetLabels: TASK_SHEET_LABELS,
|
||||
taskSheetLabels: [],
|
||||
remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' },
|
||||
remTypeLabels: REMINDER_LABELS,
|
||||
myPets: [],
|
||||
@@ -155,6 +197,7 @@ Component({
|
||||
patch.costNote = '';
|
||||
patch.manageTasks = [];
|
||||
patch.taskForm = { id: '', title: '', description: '', priority: '', sheetIdx: 0 };
|
||||
patch.taskSheetLabels = taskSheets().map((x) => x.label);
|
||||
patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' };
|
||||
patch.exportText = '';
|
||||
patch.fbContent = '';
|
||||
@@ -172,6 +215,18 @@ Component({
|
||||
patch.kbHeight = 0;
|
||||
patch.riskData = null;
|
||||
patch.saving = false;
|
||||
patch.simpleNote = '';
|
||||
patch.simpleImages = [];
|
||||
}
|
||||
// 是否走通用简易表单。缓存热的时候是同步的;万一没热就按「不是」处理,
|
||||
// 那 9 种的分支写死在 wxml 里不依赖这份缓存,不会因此打不开
|
||||
const simple = recordTypes.isSimple(type);
|
||||
patch.isSimple = simple;
|
||||
if (simple) {
|
||||
const label = recordTypes.label(type);
|
||||
patch.simpleLabel = label;
|
||||
patch.simpleDate = daysLater(0);
|
||||
patch.simplePlaceholder = SIMPLE_HINT[type] || ('这次' + label + '的情况,几个字就行');
|
||||
}
|
||||
if (type === 'weight') {
|
||||
patch.wInput = (pet.weight || '').replace('kg', '');
|
||||
@@ -399,6 +454,24 @@ Component({
|
||||
const optSel = Object.assign({}, this.data.optSel, { [group]: Number(index) });
|
||||
this.setData({ optSel });
|
||||
},
|
||||
onSimpleDate(e) {
|
||||
this.setData({ simpleDate: e.detail.value });
|
||||
},
|
||||
onSimpleNote(e) {
|
||||
this.setData({ simpleNote: e.detail.value });
|
||||
},
|
||||
onPickSimpleImage() {
|
||||
upload
|
||||
.chooseAndUploadImage()
|
||||
.then((f) => this.setData({ simpleImages: [f] }))
|
||||
.catch((e) => {
|
||||
if (e && e.canceled) return;
|
||||
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
onRemoveSimpleImage() {
|
||||
this.setData({ simpleImages: [] });
|
||||
},
|
||||
onDate(e) {
|
||||
this.setData({ [`dateVals.${e.currentTarget.dataset.key}`]: e.detail.value });
|
||||
},
|
||||
@@ -608,7 +681,8 @@ Component({
|
||||
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 || ''));
|
||||
const sheets = taskSheets();
|
||||
let sheetIdx = 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 },
|
||||
@@ -618,7 +692,8 @@ Component({
|
||||
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 sheets = taskSheets();
|
||||
const body = { title, description: f.description, priority: f.priority, sheet_type: (sheets[f.sheetIdx] || sheets[0]).key };
|
||||
const id = store.currentPetId();
|
||||
const p = f.id ? api.updateTask(f.id, body) : api.createTask(id, body);
|
||||
p.then(() => {
|
||||
@@ -713,6 +788,20 @@ Component({
|
||||
// 依据当前弹层类型组装一条健康记录
|
||||
buildRecord() {
|
||||
const t = this.data.innerType;
|
||||
// 简易类型统一在这里组装。icon 故意留空:那 9 种老类型的 icon 里存的是
|
||||
// emoji(写库值,不能动),新类型要是存 pt-icon 名,这一列就变成两套语义了。
|
||||
// 渲染本来就按 type 取图标(type code 和图标名是同一个词),不需要这一列
|
||||
if (this.data.isSimple) {
|
||||
const img = (this.data.simpleImages || [])[0];
|
||||
return {
|
||||
type: t,
|
||||
title: this.data.simpleLabel || t,
|
||||
description: (this.data.simpleNote || '').trim(),
|
||||
occurred_at: dayToISO(this.data.simpleDate),
|
||||
image_file_id: img ? img.id : '',
|
||||
image_url: img ? img.url : '',
|
||||
};
|
||||
}
|
||||
switch (t) {
|
||||
case 'poop': {
|
||||
const s = POOP[this.segIdx('poopState')];
|
||||
|
||||
@@ -232,6 +232,32 @@ module.exports.sel = function (map, key, index, def) {
|
||||
<button class="btn btn-primary btn-block" bindtap="onSavePhoto">选择图片上传</button>
|
||||
</block>
|
||||
|
||||
<!-- 通用简易记录。洗护/清洁那 15 种共用这一支:
|
||||
它们只需要「什么时候 + 备注 + 可选照片」,各写一个 wx:elif 会把这个
|
||||
文件从 400 行推到 700 行,而且每加一种类型都要发版。
|
||||
isSimple 由 JS 按后端返回的 form 字段算,和上面那 9 种永不重叠 -->
|
||||
<block wx:elif="{{isSimple}}">
|
||||
<view class="sheet-h3">记录{{simpleLabel}}</view>
|
||||
<view class="sheet-p">选个时间、写句备注就行,需要的话可以附张照片。</view>
|
||||
<view class="field"><label>什么时候</label>
|
||||
<picker mode="date" value="{{simpleDate}}" bindchange="onSimpleDate">
|
||||
<view class="picker-box">{{simpleDate}}</view>
|
||||
</picker></view>
|
||||
<view class="field"><label>备注</label>
|
||||
<textarea class="textarea" placeholder="{{simplePlaceholder}}" placeholder-class="placeholder"
|
||||
value="{{simpleNote}}" bindinput="onSimpleNote"></textarea></view>
|
||||
<view class="field"><label>照片(可选)</label>
|
||||
<view class="img-picker">
|
||||
<view wx:for="{{simpleImages}}" wx:key="id" class="img-thumb">
|
||||
<image src="{{item.url}}" mode="aspectFill"></image>
|
||||
<view class="img-del" catchtap="onRemoveSimpleImage" data-index="{{index}}"><pt-icon name="close" size="{{24}}"></pt-icon></view>
|
||||
</view>
|
||||
<view wx:if="{{simpleImages.length < 1}}" class="img-add" bindtap="onPickSimpleImage"><pt-icon name="plus" size="{{48}}"></pt-icon></view>
|
||||
</view>
|
||||
</view>
|
||||
<button class="btn btn-primary btn-block" bindtap="onSave">保存记录</button>
|
||||
</block>
|
||||
|
||||
<!-- 提醒中心 -->
|
||||
<block wx:elif="{{innerType === 'reminders'}}">
|
||||
<view class="sheet-h3">提醒中心</view>
|
||||
|
||||
Reference in New Issue
Block a user