feat(auth): access token 缩到 2 小时 + refresh token 机制 #3
@@ -1,4 +1,5 @@
|
|||||||
const store = require('../../utils/store.js');
|
const store = require('../../utils/store.js');
|
||||||
|
const recordTypes = require('../../utils/recordTypes.js');
|
||||||
const api = require('../../utils/api.js');
|
const api = require('../../utils/api.js');
|
||||||
const upload = require('../../utils/upload.js');
|
const upload = require('../../utils/upload.js');
|
||||||
|
|
||||||
@@ -15,6 +16,21 @@ function daysLater(n) {
|
|||||||
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
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) {
|
function fmtAgo(iso) {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
@@ -43,16 +59,15 @@ const GENDERS = ['男孩', '女孩', '不确定'];
|
|||||||
const STAGES = ['刚到家 0-30 天', '幼年期', '成年期', '老年期'];
|
const STAGES = ['刚到家 0-30 天', '幼年期', '成年期', '老年期'];
|
||||||
|
|
||||||
// 任务可关联的记录弹层类型
|
// 任务可关联的记录弹层类型
|
||||||
const TASK_SHEETS = [
|
// 任务能关联的记录类型。原来这里也是写死的 6 种,后台加了类型这个下拉里看不到,
|
||||||
{ key: '', label: '不关联' },
|
// 于是「自定义任务关联新类型」这条路是断的。改成读同一份类型缓存
|
||||||
{ key: 'weight', label: '体重' },
|
function taskSheets() {
|
||||||
{ key: 'poop', label: '便便' },
|
const list = [{ key: '', label: '不关联' }];
|
||||||
{ key: 'food', label: '饮食' },
|
(recordTypes.groups() || []).forEach((g) =>
|
||||||
{ key: 'symptom', label: '异常' },
|
g.items.forEach((t) => list.push({ key: t.code, label: t.label })),
|
||||||
{ key: 'vaccine', label: '疫苗' },
|
);
|
||||||
{ key: 'medicine', label: '用药' },
|
return list;
|
||||||
];
|
}
|
||||||
const TASK_SHEET_LABELS = TASK_SHEETS.map((s) => s.label);
|
|
||||||
|
|
||||||
// 提醒类型(与后端 model.Reminder* 常量对应)
|
// 提醒类型(与后端 model.Reminder* 常量对应)
|
||||||
const REMINDER_TYPES = [
|
const REMINDER_TYPES = [
|
||||||
@@ -63,6 +78,26 @@ const REMINDER_TYPES = [
|
|||||||
];
|
];
|
||||||
const REMINDER_LABELS = REMINDER_TYPES.map((t) => t.label);
|
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 POOP = ['正常', '软便', '拉稀'];
|
||||||
const FOOD = ['正常', '偏少', '不吃'];
|
const FOOD = ['正常', '偏少', '不吃'];
|
||||||
const COST = ['食品', '医疗', '用品'];
|
const COST = ['食品', '医疗', '用品'];
|
||||||
@@ -83,6 +118,13 @@ Component({
|
|||||||
segSel: {},
|
segSel: {},
|
||||||
optSel: {},
|
optSel: {},
|
||||||
dateVals: { vaccine: '', deworm: '' },
|
dateVals: { vaccine: '', deworm: '' },
|
||||||
|
// 通用简易记录(洗护/清洁那 15 种共用)
|
||||||
|
isSimple: false,
|
||||||
|
simpleLabel: '',
|
||||||
|
simpleDate: '',
|
||||||
|
simpleNote: '',
|
||||||
|
simpleImages: [],
|
||||||
|
simplePlaceholder: '',
|
||||||
wInput: '',
|
wInput: '',
|
||||||
wNote: '',
|
wNote: '',
|
||||||
poopNote: '',
|
poopNote: '',
|
||||||
@@ -95,7 +137,7 @@ Component({
|
|||||||
postImages: [],
|
postImages: [],
|
||||||
manageTasks: [],
|
manageTasks: [],
|
||||||
taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 },
|
taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 },
|
||||||
taskSheetLabels: TASK_SHEET_LABELS,
|
taskSheetLabels: [],
|
||||||
remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' },
|
remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' },
|
||||||
remTypeLabels: REMINDER_LABELS,
|
remTypeLabels: REMINDER_LABELS,
|
||||||
myPets: [],
|
myPets: [],
|
||||||
@@ -155,6 +197,7 @@ Component({
|
|||||||
patch.costNote = '';
|
patch.costNote = '';
|
||||||
patch.manageTasks = [];
|
patch.manageTasks = [];
|
||||||
patch.taskForm = { id: '', title: '', description: '', priority: '', sheetIdx: 0 };
|
patch.taskForm = { id: '', title: '', description: '', priority: '', sheetIdx: 0 };
|
||||||
|
patch.taskSheetLabels = taskSheets().map((x) => x.label);
|
||||||
patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' };
|
patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' };
|
||||||
patch.exportText = '';
|
patch.exportText = '';
|
||||||
patch.fbContent = '';
|
patch.fbContent = '';
|
||||||
@@ -172,6 +215,18 @@ Component({
|
|||||||
patch.kbHeight = 0;
|
patch.kbHeight = 0;
|
||||||
patch.riskData = null;
|
patch.riskData = null;
|
||||||
patch.saving = false;
|
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') {
|
if (type === 'weight') {
|
||||||
patch.wInput = (pet.weight || '').replace('kg', '');
|
patch.wInput = (pet.weight || '').replace('kg', '');
|
||||||
@@ -399,6 +454,24 @@ Component({
|
|||||||
const optSel = Object.assign({}, this.data.optSel, { [group]: Number(index) });
|
const optSel = Object.assign({}, this.data.optSel, { [group]: Number(index) });
|
||||||
this.setData({ optSel });
|
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) {
|
onDate(e) {
|
||||||
this.setData({ [`dateVals.${e.currentTarget.dataset.key}`]: e.detail.value });
|
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) }); },
|
onTaskSheet(e) { this.setData({ 'taskForm.sheetIdx': Number(e.detail.value) }); },
|
||||||
onEditTaskItem(e) {
|
onEditTaskItem(e) {
|
||||||
const t = this.data.manageTasks[e.currentTarget.dataset.index];
|
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;
|
if (sheetIdx < 0) sheetIdx = 0;
|
||||||
this.setData({
|
this.setData({
|
||||||
taskForm: { id: t.id, title: t.title, description: t.description || '', priority: t.priority || '', sheetIdx },
|
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 f = this.data.taskForm;
|
||||||
const title = (f.title || '').trim();
|
const title = (f.title || '').trim();
|
||||||
if (!title) return wx.showToast({ title: '填个任务名', icon: 'none' });
|
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 id = store.currentPetId();
|
||||||
const p = f.id ? api.updateTask(f.id, body) : api.createTask(id, body);
|
const p = f.id ? api.updateTask(f.id, body) : api.createTask(id, body);
|
||||||
p.then(() => {
|
p.then(() => {
|
||||||
@@ -713,6 +788,20 @@ Component({
|
|||||||
// 依据当前弹层类型组装一条健康记录
|
// 依据当前弹层类型组装一条健康记录
|
||||||
buildRecord() {
|
buildRecord() {
|
||||||
const t = this.data.innerType;
|
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) {
|
switch (t) {
|
||||||
case 'poop': {
|
case 'poop': {
|
||||||
const s = POOP[this.segIdx('poopState')];
|
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>
|
<button class="btn btn-primary btn-block" bindtap="onSavePhoto">选择图片上传</button>
|
||||||
</block>
|
</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'}}">
|
<block wx:elif="{{innerType === 'reminders'}}">
|
||||||
<view class="sheet-h3">提醒中心</view>
|
<view class="sheet-h3">提醒中心</view>
|
||||||
|
|||||||
+18
-13
@@ -2,6 +2,7 @@ const store = require('../../utils/store.js');
|
|||||||
const api = require('../../utils/api.js');
|
const api = require('../../utils/api.js');
|
||||||
const { toastErr } = require('../../utils/ui.js');
|
const { toastErr } = require('../../utils/ui.js');
|
||||||
const { syncTabBar } = require('../../utils/tabbar.js');
|
const { syncTabBar } = require('../../utils/tabbar.js');
|
||||||
|
const recordTypes = require('../../utils/recordTypes.js');
|
||||||
|
|
||||||
function todayStr() {
|
function todayStr() {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
@@ -52,18 +53,14 @@ function mapTask(t) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快速记录入口。颜色按类别分组(体重/疫苗=橙、便便/饮食=绿、异常/用药=蓝、消费/照片=紫),
|
// 快速记录入口取后端类型表的前 8 个(按分组顺序再按 sort)。
|
||||||
// 靠颜色和图标区分,不再靠 emoji。
|
// 原来这里是 8 项写死的,后台加一种类型首页看不到;现在调 sort 就能换首屏露出哪几个。
|
||||||
const QUICK_ITEMS = [
|
// 首页只放 8 个是版式限制(4 列 2 行),全部 24 种在记录页
|
||||||
{ type: 'weight', icon: 'weight', label: '体重', tone: 'tone-1' },
|
function quickFrom(groups) {
|
||||||
{ type: 'poop', icon: 'poop', label: '便便', tone: 'tone-2' },
|
const flat = [];
|
||||||
{ type: 'food', icon: 'food', label: '饮食', tone: 'tone-2' },
|
(groups || []).forEach((g) => g.items.forEach((t) => flat.push(t)));
|
||||||
{ type: 'symptom', icon: 'symptom', label: '异常', tone: 'tone-3' },
|
return flat.slice(0, 8);
|
||||||
{ type: 'cost', icon: 'cost', label: '消费', tone: 'tone-4' },
|
}
|
||||||
{ type: 'medicine', icon: 'medicine', label: '用药', tone: 'tone-3' },
|
|
||||||
{ type: 'photo', icon: 'photo', label: '照片', tone: 'tone-4' },
|
|
||||||
{ type: 'vaccine', icon: 'vaccine', label: '疫苗', tone: 'tone-1' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 门面上的状态 chip:健康状态 + 最多两条洞察
|
// 门面上的状态 chip:健康状态 + 最多两条洞察
|
||||||
function heroChips(summary) {
|
function heroChips(summary) {
|
||||||
@@ -82,7 +79,7 @@ Page({
|
|||||||
pet: {},
|
pet: {},
|
||||||
summary: { greeting: '', insights: [], week: [], advice: '', health_pct: 0, health_status: '正常' },
|
summary: { greeting: '', insights: [], week: [], advice: '', health_pct: 0, health_status: '正常' },
|
||||||
tasks: [],
|
tasks: [],
|
||||||
quickItems: QUICK_ITEMS,
|
quickItems: [],
|
||||||
heroChips: [],
|
heroChips: [],
|
||||||
undoneText: '',
|
undoneText: '',
|
||||||
firstArticle: null,
|
firstArticle: null,
|
||||||
@@ -113,6 +110,7 @@ Page({
|
|||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
syncTabBar(this);
|
syncTabBar(this);
|
||||||
|
this.loadTypes();
|
||||||
store
|
store
|
||||||
.ready()
|
.ready()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -122,6 +120,13 @@ Page({
|
|||||||
})
|
})
|
||||||
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
||||||
},
|
},
|
||||||
|
loadTypes() {
|
||||||
|
if (this.data.quickItems.length) return;
|
||||||
|
recordTypes
|
||||||
|
.load()
|
||||||
|
.then((groups) => this.setData({ quickItems: quickFrom(groups) }))
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
loadAll() {
|
loadAll() {
|
||||||
this.loadSummary();
|
this.loadSummary();
|
||||||
this.loadTasks();
|
this.loadTasks();
|
||||||
|
|||||||
@@ -111,8 +111,8 @@
|
|||||||
<view class="link" bindtap="goRecord">更多</view>
|
<view class="link" bindtap="goRecord">更多</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="quick-grid">
|
<view class="quick-grid">
|
||||||
<view wx:for="{{quickItems}}" wx:key="type" class="quick {{item.tone}}" data-type="{{item.type}}" bindtap="openSheet">
|
<view wx:for="{{quickItems}}" wx:key="code" class="quick {{item.tone}}" data-type="{{item.code}}" bindtap="openSheet">
|
||||||
<pt-icon name="{{item.icon}}" size="{{44}}"></pt-icon>{{item.label}}
|
<pt-icon name="{{item.icon}}" size="{{44}}" fallback="note"></pt-icon>{{item.label}}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const store = require('../../utils/store.js');
|
|||||||
const api = require('../../utils/api.js');
|
const api = require('../../utils/api.js');
|
||||||
const { toastErr } = require('../../utils/ui.js');
|
const { toastErr } = require('../../utils/ui.js');
|
||||||
const { syncTabBar } = require('../../utils/tabbar.js');
|
const { syncTabBar } = require('../../utils/tabbar.js');
|
||||||
|
const recordTypes = require('../../utils/recordTypes.js');
|
||||||
|
|
||||||
function pad(n) {
|
function pad(n) {
|
||||||
return n < 10 ? '0' + n : '' + n;
|
return n < 10 ? '0' + n : '' + n;
|
||||||
@@ -18,32 +19,22 @@ function fmtTime(iso) {
|
|||||||
}
|
}
|
||||||
// 记录类型 → 配色。和首页快速记录入口用同一套分组:
|
// 记录类型 → 配色。和首页快速记录入口用同一套分组:
|
||||||
// 体重/疫苗=橙、便便/饮食=绿、异常/用药=蓝、消费/照片=紫
|
// 体重/疫苗=橙、便便/饮食=绿、异常/用药=蓝、消费/照片=紫
|
||||||
const TYPE_TONE = {
|
// 时间轴每条记录的色调跟着它所属的分组走,和「记一笔」的宫格是同一套色。
|
||||||
weight: 'tone-1', vaccine: 'tone-1', deworm: 'tone-1',
|
// 原来这里是 9 种写死的映射,新类型会全掉到 tone-1,一片橙色分不出类别
|
||||||
poop: 'tone-2', food: 'tone-2',
|
function toneOf(type) {
|
||||||
symptom: 'tone-3', medicine: 'tone-3',
|
const t = recordTypes.get(type);
|
||||||
cost: 'tone-4', photo: 'tone-4',
|
return (t && t.tone) || 'tone-1';
|
||||||
};
|
}
|
||||||
|
|
||||||
// 记一笔的九个入口
|
// 「记一笔」的入口不再写死在这里:类型和分组由后端 record_types 表提供
|
||||||
const RECORD_ITEMS = [
|
// (后台可配),前端只负责渲染。utils/recordTypes.js 是两处共用的缓存。
|
||||||
{ type: 'weight', icon: 'weight', label: '体重', tone: 'tone-1' },
|
|
||||||
{ type: 'food', icon: 'food', label: '饮食', tone: 'tone-2' },
|
|
||||||
{ type: 'poop', icon: 'poop', label: '便便', tone: 'tone-2' },
|
|
||||||
{ type: 'symptom', icon: 'symptom', label: '异常', tone: 'tone-3' },
|
|
||||||
{ type: 'medicine', icon: 'medicine', label: '用药', tone: 'tone-3' },
|
|
||||||
{ type: 'cost', icon: 'cost', label: '消费', tone: 'tone-4' },
|
|
||||||
{ type: 'vaccine', icon: 'vaccine', label: '疫苗', tone: 'tone-1' },
|
|
||||||
{ type: 'deworm', icon: 'deworm', label: '驱虫', tone: 'tone-1' },
|
|
||||||
{ type: 'photo', icon: 'photo', label: '照片', tone: 'tone-4' },
|
|
||||||
];
|
|
||||||
|
|
||||||
function mapRecord(r) {
|
function mapRecord(r) {
|
||||||
return {
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
// 图标按 type 取(9 值枚举,可靠);老数据 icon 字段里的 emoji 只作兜底
|
// 图标按 type 取(9 值枚举,可靠);老数据 icon 字段里的 emoji 只作兜底
|
||||||
type: r.type || r.icon || '',
|
type: r.type || r.icon || '',
|
||||||
tone: TYPE_TONE[r.type] || 'tone-1',
|
tone: toneOf(r.type),
|
||||||
title: r.title,
|
title: r.title,
|
||||||
desc: fmtTime(r.occurred_at) + (r.description ? '|' + r.description : ''),
|
desc: fmtTime(r.occurred_at) + (r.description ? '|' + r.description : ''),
|
||||||
image: r.image_url || '',
|
image: r.image_url || '',
|
||||||
@@ -111,7 +102,7 @@ Page({
|
|||||||
recHasMore: false,
|
recHasMore: false,
|
||||||
recFilters: REC_FILTERS,
|
recFilters: REC_FILTERS,
|
||||||
recFilterIdx: 0,
|
recFilterIdx: 0,
|
||||||
recordItems: RECORD_ITEMS,
|
typeGroups: [],
|
||||||
insights: [],
|
insights: [],
|
||||||
trendSvg: '',
|
trendSvg: '',
|
||||||
trendStats: null,
|
trendStats: null,
|
||||||
@@ -133,6 +124,7 @@ Page({
|
|||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
syncTabBar(this);
|
syncTabBar(this);
|
||||||
|
this.loadTypes();
|
||||||
store
|
store
|
||||||
.ready()
|
.ready()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -184,6 +176,15 @@ Page({
|
|||||||
const it = this.data.insights[e.currentTarget.dataset.index];
|
const it = this.data.insights[e.currentTarget.dataset.index];
|
||||||
if (it && it.action) this.openSheetType(it.action);
|
if (it && it.action) this.openSheetType(it.action);
|
||||||
},
|
},
|
||||||
|
// 类型只在缓存没热时真正请求;弹层也读这份缓存判断走哪种表单,
|
||||||
|
// 所以这一步必须在用户能点开弹层之前完成
|
||||||
|
loadTypes() {
|
||||||
|
if (this.data.typeGroups.length) return;
|
||||||
|
recordTypes
|
||||||
|
.load()
|
||||||
|
.then((groups) => this.setData({ typeGroups: groups }))
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
loadRecords() {
|
loadRecords() {
|
||||||
const id = store.currentPetId();
|
const id = store.currentPetId();
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|||||||
@@ -11,13 +11,20 @@
|
|||||||
<view class="btn btn-dark" bindtap="onAddPet"><pt-icon name="plus" size="{{30}}"></pt-icon>建一份档案</view>
|
<view class="btn btn-dark" bindtap="onAddPet"><pt-icon name="plus" size="{{30}}"></pt-icon>建一份档案</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 记一笔:分组来自后端 record_types,后台加一种不用发版。
|
||||||
|
每组一个色系,靠浅色圆底 + 分组配色出彩,图标仍是线性的 -->
|
||||||
<view class="card">
|
<view class="card">
|
||||||
<view class="section-head"><view class="sh-title">记一笔</view></view>
|
<view class="section-head"><view class="sh-title">记一笔</view></view>
|
||||||
<view class="quick-grid cols-3">
|
<view wx:for="{{typeGroups}}" wx:key="key" class="tg {{item.tone}}">
|
||||||
<view wx:for="{{recordItems}}" wx:key="type" class="quick {{item.tone}}" data-type="{{item.type}}" bindtap="openSheet">
|
<view class="tg-head"><view class="tg-bar"></view>{{item.label}}<text class="tg-n">{{item.items.length}}</text></view>
|
||||||
<pt-icon name="{{item.icon}}" size="{{44}}"></pt-icon>{{item.label}}
|
<view class="quick-grid cols-3">
|
||||||
|
<view wx:for="{{item.items}}" wx:for-item="t" wx:key="code" class="quick {{item.tone}}"
|
||||||
|
data-type="{{t.code}}" bindtap="openSheet">
|
||||||
|
<pt-icon name="{{t.icon}}" size="{{44}}" fallback="note"></pt-icon>{{t.label}}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view wx:if="{{!typeGroups.length}}" class="empty">还没有配置可记事项</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view wx:if="{{insights.length}}" class="card">
|
<view wx:if="{{insights.length}}" class="card">
|
||||||
|
|||||||
@@ -57,3 +57,19 @@
|
|||||||
.ins-title{flex:1;font-size:var(--fs-md);font-weight:var(--fw-b);line-height:1.4}
|
.ins-title{flex:1;font-size:var(--fs-md);font-weight:var(--fw-b);line-height:1.4}
|
||||||
.ins-detail{color:var(--text-2);font-size:var(--fs-sm);line-height:1.6;margin-top:var(--sp-2)}
|
.ins-detail{color:var(--text-2);font-size:var(--fs-sm);line-height:1.6;margin-top:var(--sp-2)}
|
||||||
.ins-ev{color:var(--muted);font-size:var(--fs-cap);margin-top:var(--sp-2)}
|
.ins-ev{color:var(--muted);font-size:var(--fs-cap);margin-top:var(--sp-2)}
|
||||||
|
|
||||||
|
/* ===== 记一笔的分组 =====
|
||||||
|
四组各一个色系,色值全走 token 的 soft/ink 二件套,没有新色。
|
||||||
|
.quick 的配色由 .tone-N 决定(app.wxss 里已有),这里只管组标题和留白 */
|
||||||
|
.tg{margin-bottom:var(--sp-4)}
|
||||||
|
.tg:last-of-type{margin-bottom:0}
|
||||||
|
.tg-head{
|
||||||
|
display:flex;align-items:center;gap:var(--sp-2);
|
||||||
|
font-size:var(--fs-md);font-weight:var(--fw-b);margin-bottom:var(--sp-3);
|
||||||
|
}
|
||||||
|
.tg-bar{width:6rpx;height:26rpx;border-radius:3rpx;background:currentColor}
|
||||||
|
.tg-n{margin-left:auto;font-size:var(--fs-cap);font-weight:var(--fw);color:var(--muted2)}
|
||||||
|
.tg.tone-1 .tg-head{color:var(--primary-ink)}
|
||||||
|
.tg.tone-2 .tg-head{color:var(--green-ink)}
|
||||||
|
.tg.tone-3 .tg-head{color:var(--blue-ink)}
|
||||||
|
.tg.tone-4 .tg-head{color:var(--purple-ink)}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ const api = {
|
|||||||
|
|
||||||
// 用户
|
// 用户
|
||||||
userSummary: () => request({ url: '/api/user/summary' }),
|
userSummary: () => request({ url: '/api/user/summary' }),
|
||||||
|
// 记录类型(后台可配)。分组好的,前端直接渲染
|
||||||
|
recordTypes: () => request({ url: '/api/record-types' }),
|
||||||
updateDecoration: (body) => request({ url: '/api/user/decoration', method: 'PUT', data: body }),
|
updateDecoration: (body) => request({ url: '/api/user/decoration', method: 'PUT', data: body }),
|
||||||
|
|
||||||
// 宠物
|
// 宠物
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// 记录类型缓存。类型从后端来(后台可配),记录页要拿它渲染分组宫格,
|
||||||
|
// 弹层要拿它判断走专用表单还是通用简易表单。两边必须是同一份数据——
|
||||||
|
// 各自请求一次的话,后台改完配置两边会短暂不一致。
|
||||||
|
const api = require('./api.js');
|
||||||
|
|
||||||
|
// 四个分组对应的色系。这是纯表现层的东西,不进后端配置:
|
||||||
|
// 后端存 group key,色值留在前端,改配色不用动数据
|
||||||
|
const GROUP_TONE = {
|
||||||
|
daily: 'tone-1',
|
||||||
|
health: 'tone-2',
|
||||||
|
care: 'tone-4',
|
||||||
|
clean: 'tone-3',
|
||||||
|
};
|
||||||
|
|
||||||
|
let groups = null; // [{ key, label, items: [...] }]
|
||||||
|
let byCode = {};
|
||||||
|
let loading = null;
|
||||||
|
|
||||||
|
function index(gs) {
|
||||||
|
groups = (gs || []).map((g) => ({
|
||||||
|
...g,
|
||||||
|
tone: GROUP_TONE[g.key] || 'tone-1',
|
||||||
|
items: (g.items || []).map((t) => ({ ...t, tone: GROUP_TONE[g.key] || 'tone-1' })),
|
||||||
|
}));
|
||||||
|
byCode = {};
|
||||||
|
groups.forEach((g) => g.items.forEach((t) => (byCode[t.code] = t)));
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load 只真正请求一次;并发调用共用同一个 promise
|
||||||
|
function load() {
|
||||||
|
if (groups) return Promise.resolve(groups);
|
||||||
|
if (loading) return loading;
|
||||||
|
loading = api
|
||||||
|
.recordTypes()
|
||||||
|
.then((gs) => {
|
||||||
|
loading = null;
|
||||||
|
return index(gs);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
loading = null;
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get 是同步的,给弹层用——弹层打开时不该再等一次网络请求。
|
||||||
|
// 记录页/首页先 load 过,缓存已经热了;万一没热,返回 undefined,
|
||||||
|
// 调用方按「专用表单」处理(那 9 种的分支是写死在 wxml 里的,不依赖这份缓存)
|
||||||
|
function get(code) {
|
||||||
|
return byCode[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSimple 走通用简易表单的类型。缓存没热时返回 false,
|
||||||
|
// 宁可打不开也不要用错的表单存下一条字段不全的记录
|
||||||
|
function isSimple(code) {
|
||||||
|
const t = byCode[code];
|
||||||
|
return !!t && t.form === 'simple';
|
||||||
|
}
|
||||||
|
|
||||||
|
function label(code) {
|
||||||
|
const t = byCode[code];
|
||||||
|
return (t && t.label) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台改了配置之后强制重取
|
||||||
|
function reset() {
|
||||||
|
groups = null;
|
||||||
|
byCode = {};
|
||||||
|
loading = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// groups 同步读缓存,给「任务关联记录类型」那个下拉用
|
||||||
|
function all() {
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { load, get, isSimple, label, reset, groups: all, GROUP_TONE };
|
||||||
Reference in New Issue
Block a user