Files
sundynix-pets/pets-fe/pages/record/record.js
T
Blizzard 2fd25e26ef feat(fe): 其余 8 页 + 三个薄组件套用新设计语言
页面:
- record:3×3 emoji 九宫格(184rpx 高、全站最大的点击块)换成图标 + 4 色分类,
  和首页的 8 宫格合并成同一套 .quick-grid,此前它俩是同一个东西的两个副本
- record/plan/community:三套横向切换全部换成 seg-tabs
- record/plan/community/report/learn:砍掉与 nav-bar 重复的 46rpx 大标题,
  每页白赚约 130rpx 首屏
- 时间轴图标改按 HealthRecord.Type 取(9 值枚举,可靠),老数据里的 emoji
  只作兜底;写库的 icon 字段一个字符没动
- profile:设置行加分类图标 + 右侧箭头换图标
- community/report:主操作改深色 CTA,btn-primary 继续留给弹层的「保存」
- 6 套空态写法统一成 .empty

组件:
- nav-bar 的 ‹ 和 fab 的 "AI" 文字换成图标;fab 从蓝渐变改深色,
  和全站唯一主 CTA 的语义对齐
- pet-switch 的「+」换图标;宠物自己的 emoji 是用户数据,保留

pt-icon 开了 virtualHost,组件标签上的 class 不生效,需要定位的地方
(profile 行图标、箭头)都用外层 view 包了一层。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:54:18 +08:00

257 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const store = require('../../utils/store.js');
const api = require('../../utils/api.js');
const { toastErr } = require('../../utils/ui.js');
function pad(n) {
return n < 10 ? '0' + n : '' + n;
}
function fmtTime(iso) {
if (!iso) return '';
const d = new Date(iso);
const now = new Date();
const hm = pad(d.getHours()) + ':' + pad(d.getMinutes());
if (d.toDateString() === now.toDateString()) return '今天 ' + hm;
const y = new Date(now.getTime() - 86400000);
if (d.toDateString() === y.toDateString()) return '昨天 ' + hm;
return d.getMonth() + 1 + '月' + d.getDate() + '日';
}
// 记录类型 → 配色。和首页快速记录入口用同一套分组:
// 体重/疫苗=橙、便便/饮食=绿、异常/用药=蓝、消费/照片=紫
const TYPE_TONE = {
weight: 'tone-1', vaccine: 'tone-1', deworm: 'tone-1',
poop: 'tone-2', food: 'tone-2',
symptom: 'tone-3', medicine: 'tone-3',
cost: 'tone-4', photo: 'tone-4',
};
// 记一笔的九个入口
const RECORD_ITEMS = [
{ 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) {
return {
id: r.id,
// 图标按 type 取(9 值枚举,可靠);老数据 icon 字段里的 emoji 只作兜底
type: r.type || r.icon || '',
tone: TYPE_TONE[r.type] || 'tone-1',
title: r.title,
desc: fmtTime(r.occurred_at) + (r.description ? '' + r.description : ''),
image: r.image_url || '',
};
}
// 时间轴类型筛选
const REC_FILTERS = [
{ key: '', label: '全部' },
{ key: 'weight', label: '体重' },
{ key: 'poop', label: '便便' },
{ key: 'food', label: '饮食' },
{ key: 'symptom', label: '异常' },
{ key: 'medicine', label: '用药' },
{ key: 'vaccine', label: '疫苗' },
{ key: 'cost', label: '消费' },
{ key: 'photo', label: '照片' },
];
function fmtDay(iso) {
if (!iso) return '';
const d = new Date(iso);
return d.getMonth() + 1 + '月' + d.getDate() + '日';
}
// 用真实体重点生成趋势图:返回 { svg(折线), dots(可点圆点,坐标为百分比) }
function buildChart(points) {
const vals = points.map((p) => Number(p.value) || 0);
const W = 300, H = 100, pad = 12, n = vals.length;
let min = Math.min.apply(null, vals);
let max = Math.max.apply(null, vals);
if (max === min) max = min + 1;
const xs = (i) => (n === 1 ? W / 2 : pad + ((W - 2 * pad) * i) / (n - 1));
const ys = (v) => H - pad - ((H - 2 * pad) * (v - min)) / (max - min);
const pts = vals.map((v, i) => xs(i).toFixed(1) + ',' + ys(v).toFixed(1)).join(' ');
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="none">' +
'<polyline points="' + pts + '" fill="none" stroke="#73BE9D" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>' +
'</svg>';
const dots = points.map((p, i) => ({
xp: Number(((xs(i) / W) * 100).toFixed(2)),
yp: Number(((ys(vals[i]) / H) * 100).toFixed(2)),
value: vals[i],
date: fmtDay(p.occurred_at),
note: p.note || '',
delta: i > 0 ? Number((vals[i] - vals[i - 1]).toFixed(2)) : null,
}));
return { svg: 'data:image/svg+xml,' + encodeURIComponent(svg), dots };
}
const REC_PAGE = 6;
Page({
data: {
pet: {},
timeline: [],
recPage: 1,
recTotal: 0,
recHasMore: false,
recFilters: REC_FILTERS,
recFilterIdx: 0,
recordItems: RECORD_ITEMS,
trendSvg: '',
trendStats: null,
trendDots: [],
selDot: null,
selIdx: -1,
sheetShow: false,
sheetType: '',
},
onLoad() {
this._unsub = store.subscribe((pet) => {
this.setData({ pet });
if (!this._inited) return; // 首次由 onShow 加载
this.loadRecords();
});
},
onUnload() {
if (this._unsub) this._unsub();
},
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 2 });
}
store
.ready()
.then(() => {
this._inited = true;
this.setData({ pet: store.getPet() });
this.loadRecords();
})
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
},
curFilter() {
return this.data.recFilters[this.data.recFilterIdx].key;
},
// 来自 seg-tabs 的 change 事件
onFilterTap(e) {
const i = Number(e.detail.index);
if (i === this.data.recFilterIdx) return;
this.setData({ recFilterIdx: i }, () => this.loadRecords());
},
// 长按删除一条记录
onDeleteRecord(e) {
const r = this.data.timeline[e.currentTarget.dataset.index];
if (!r || !r.id) return;
wx.showModal({
title: '删除记录',
content: '确定删除「' + r.title + '」?删除后不可恢复。',
confirmColor: '#D9534F',
success: (res) => {
if (!res.confirm) return;
api
.deleteRecord(r.id)
.then(() => {
wx.showToast({ title: '已删除', icon: 'success' });
this.loadRecords();
})
.catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
},
});
},
loadRecords() {
const id = store.currentPetId();
if (!id) return;
api
.getRecords(id, { page: 1, pageSize: REC_PAGE, type: this.curFilter() })
.then((res) => {
const list = (res.list || []).map(mapRecord);
this.setData({ timeline: list, recPage: 1, recTotal: res.total || 0, recHasMore: list.length < (res.total || 0) });
})
.catch((e) => toastErr(e));
this.loadTrend();
},
loadMoreRecords() {
const id = store.currentPetId();
if (!id) return;
const next = this.data.recPage + 1;
api
.getRecords(id, { page: next, pageSize: REC_PAGE, type: this.curFilter() })
.then((res) => {
const merged = this.data.timeline.concat((res.list || []).map(mapRecord));
this.setData({ timeline: merged, recPage: next, recTotal: res.total || 0, recHasMore: merged.length < (res.total || 0) });
})
.catch(() => {});
},
collapseRecords() {
this.loadRecords();
},
loadTrend() {
const id = store.currentPetId();
if (!id) {
this.setData({ trendSvg: '', trendStats: null, trendDots: [], selDot: null, selIdx: -1 });
return;
}
api
.weightTrend(id)
.then((points) => {
points = points || [];
if (points.length < 2) {
this.setData({ trendSvg: '', trendStats: null, trendDots: [], selDot: null, selIdx: -1 });
return;
}
const vals = points.map((p) => Number(p.value) || 0);
const latest = vals[vals.length - 1];
const delta = Number((latest - vals[0]).toFixed(2));
const chart = buildChart(points);
const lastIdx = chart.dots.length - 1;
this.setData({
trendSvg: chart.svg,
trendDots: chart.dots,
selDot: chart.dots[lastIdx], // 默认选中最新一次
selIdx: lastIdx,
trendStats: { latest, delta, count: points.length },
});
})
.catch(() => {});
},
onTapPoint(e) {
const i = e.currentTarget.dataset.index;
this.setData({ selDot: this.data.trendDots[i], selIdx: i });
},
previewImage(e) {
const src = e.currentTarget.dataset.src;
if (src) wx.previewImage({ urls: [src], current: src });
},
openSheet(e) {
this.openSheetType(e.currentTarget.dataset.type);
},
openSheetType(type) {
this.setData({ sheetType: type, sheetShow: true });
},
closeSheet() {
this.setData({ sheetShow: false });
},
onSheetSaved() {
this.loadRecords();
},
onAddPet() {
this.openSheetType('addPet');
},
onFab() {
this.openSheetType('ai');
},
onShareAppMessage() {
return { title: '用肉垫计划记录毛孩子的成长', path: '/pages/record/record' };
},
onShareTimeline() {
return { title: '用肉垫计划记录毛孩子的成长' };
},
});