281 lines
8.6 KiB
TypeScript
281 lines
8.6 KiB
TypeScript
import { getProfile } from '@/utils/profile'
|
||
|
||
export interface IHealthRecord {
|
||
id: string
|
||
type: string
|
||
time: string
|
||
date?: string
|
||
[key: string]: any
|
||
}
|
||
|
||
export interface IRecordMeta {
|
||
icon: string
|
||
label: string
|
||
color: string
|
||
}
|
||
|
||
export interface IRecordStats {
|
||
label: string
|
||
type: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'volcano' | 'lightblue' | 'cyan' | 'pink' | 'purple'
|
||
}
|
||
|
||
const MONTH_KEEP = 12
|
||
|
||
function getMonthKey(timestamp: number) {
|
||
const d = new Date(timestamp)
|
||
return `health_${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||
}
|
||
|
||
export function getDateStr(timestamp: number) {
|
||
const d = new Date(timestamp)
|
||
return d.toISOString().slice(0, 10)
|
||
}
|
||
|
||
export function getMonthStr(timestamp: number): string {
|
||
const d = new Date(timestamp)
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
return `${y}-${m}`
|
||
}
|
||
|
||
export function getTimeStr(timestamp: number) {
|
||
const d = new Date(timestamp)
|
||
return d.toTimeString().slice(0, 5)
|
||
}
|
||
|
||
function cleanOldMonths() {
|
||
const cutoff = new Date()
|
||
cutoff.setMonth(cutoff.getMonth() - MONTH_KEEP)
|
||
const cutoffKey = `health_${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, '0')}`
|
||
const keys = wx.getStorageInfoSync().keys
|
||
keys.forEach((key) => {
|
||
if (key.startsWith('health_') && key < cutoffKey) {
|
||
wx.removeStorageSync(key)
|
||
}
|
||
})
|
||
}
|
||
|
||
function addRecord(type: string, data: Record<string, any>, timestamp: number) {
|
||
const key = getMonthKey(timestamp)
|
||
const all = wx.getStorageSync(key) || {}
|
||
const date = getDateStr(timestamp)
|
||
|
||
if (!all[date])
|
||
all[date] = []
|
||
|
||
all[date].push({
|
||
id: Date.now() + Math.random().toString(36).slice(2, 6),
|
||
time: getTimeStr(timestamp),
|
||
type,
|
||
...data,
|
||
})
|
||
|
||
wx.setStorageSync(key, all)
|
||
}
|
||
|
||
export function addWeight(value: number, note: string, timestamp: number) {
|
||
addRecord('weight', { value, note }, timestamp)
|
||
}
|
||
|
||
export function addSleep(startTime: number, endTime: number, duration: number, note: string, timestamp: number) {
|
||
addRecord('sleep', { startTime, endTime, duration, note }, timestamp)
|
||
}
|
||
|
||
export function addSport(category: string, duration: number, note: string, timestamp: number) {
|
||
addRecord('sport', { category, duration, note }, timestamp)
|
||
}
|
||
|
||
export function addBloodPressure(systolic: number, diastolic: number, heartRate: number, note: string, timestamp: number) {
|
||
addRecord('bloodPressure', { systolic, diastolic, heartRate, note }, timestamp)
|
||
}
|
||
|
||
export function addBloodSugar(value: number, period: string, note: string, timestamp: number) {
|
||
addRecord('bloodSugar', { value, period, note }, timestamp)
|
||
}
|
||
|
||
export function addTemperature(value: number, location: string, note: string, timestamp: number) {
|
||
addRecord('temperature', { value, location, note }, timestamp)
|
||
}
|
||
|
||
function getAllMonthKeys(): string[] {
|
||
return wx.getStorageInfoSync().keys.filter(k => k.startsWith('health_')).sort()
|
||
}
|
||
|
||
// ─── 根据时间戳反推它属于哪个月的 key ───
|
||
function getMonthKeyForTimestamp(timestamp: number): string {
|
||
const d = new Date(timestamp)
|
||
return `health_${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||
}
|
||
|
||
// ─── 根据 "YYYY-MM-DD" 反推月份 key ───
|
||
function getMonthKeyForDate(dateStr: string): string {
|
||
const [y, m] = dateStr.split('-')
|
||
return `health_${y}-${m}`
|
||
}
|
||
|
||
export function getDayRecords(date: string): IHealthRecord[] {
|
||
const key = getMonthKeyForDate(date)
|
||
const all = wx.getStorageSync(key) || {}
|
||
const dayRecords = all[date] || []
|
||
return [...dayRecords].sort((a, b) => a.time.localeCompare(b.time))
|
||
}
|
||
|
||
export function getMonthRecords(yearMonth: string, type?: string): IHealthRecord[] {
|
||
const key = `health_${yearMonth}`
|
||
const all = wx.getStorageSync(key) || {}
|
||
|
||
const result: IHealthRecord[] = []
|
||
Object.keys(all).forEach((date) => {
|
||
all[date].forEach((r: IHealthRecord) => {
|
||
if (!type || r.type === type) {
|
||
result.push({ date, ...r })
|
||
}
|
||
})
|
||
})
|
||
|
||
return sortRecords(result)
|
||
}
|
||
|
||
// 排序:先按 date 正序,再按 time 正序
|
||
function sortRecords(records: IHealthRecord[]): IHealthRecord[] {
|
||
return records.sort((a, b) => {
|
||
const dateCmp = a.date.localeCompare(b.date)
|
||
if (dateCmp !== 0)
|
||
return dateCmp
|
||
return a.time.localeCompare(b.time)
|
||
})
|
||
}
|
||
|
||
const META_MAP: Record<string, IRecordMeta> = {
|
||
weight: { icon: '⚖️', label: '体重', color: 'bg-[#E89E3A]' },
|
||
sleep: { icon: '😴', label: '睡眠', color: 'bg-[#2E5B8C]' },
|
||
bloodPressure: { icon: '❤️', label: '血压', color: 'bg-[#C04A3C]' },
|
||
bloodSugar: { icon: '📈', label: '血糖', color: 'bg-[#9C27B0]' },
|
||
sport: { icon: '🏃', label: '运动', color: 'bg-[#3A7C5B]' },
|
||
temperature: { icon: '🌡️', label: '体温', color: 'bg-[#D32F2F]' },
|
||
}
|
||
|
||
export function getRecordMeta(type: string): IRecordMeta {
|
||
return META_MAP[type] || { icon: '📋', label: '其他', color: 'bg-gray-400' }
|
||
}
|
||
|
||
export function getRecordValue(record: IHealthRecord): string {
|
||
switch (record.type) {
|
||
case 'weight':
|
||
return `${record.value} kg`
|
||
case 'bloodPressure':
|
||
return `${record.systolic}/${record.diastolic} mmHg${record.heartRate ? ` · 心率 ${record.heartRate}` : ''}`
|
||
case 'bloodSugar':
|
||
return `${record.value} mmol/L · ${record.period}`
|
||
case 'heartRate':
|
||
return `${record.value} bpm`
|
||
case 'temperature':
|
||
return `${record.value} ℃${record.location ? ` · ${record.location}` : ''}`
|
||
case 'sport':
|
||
return `${record.category} · ${record.duration}min`
|
||
case 'sleep':
|
||
return `${new Date(record.startTime).toTimeString().slice(0, 5)} ~ ${new Date(record.endTime).toTimeString().slice(0, 5)}(${record.duration}h)`
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
export function getRecordStatus(record: IHealthRecord): IRecordStats {
|
||
const profile = getProfile()
|
||
|
||
switch (record.type) {
|
||
case 'bloodPressure':
|
||
if (record.systolic >= 140 || record.diastolic >= 90) {
|
||
return { label: '偏高', type: 'danger' }
|
||
}
|
||
if (record.systolic < 90 || record.diastolic < 60) {
|
||
return { label: '偏低', type: 'danger' }
|
||
}
|
||
return { label: '正常', type: 'primary' }
|
||
|
||
case 'weight': {
|
||
if (!profile.height) {
|
||
return { label: '身高未知', type: 'default' }
|
||
}
|
||
const bmi = (record.value * 10000) / (profile.height * profile.height)
|
||
if (bmi < 18.5)
|
||
return { label: '偏瘦', type: 'warning' }
|
||
if (bmi < 24)
|
||
return { label: '正常', type: 'primary' }
|
||
if (bmi < 28)
|
||
return { label: '偏重', type: 'danger' }
|
||
return { label: '肥胖', type: 'danger' }
|
||
}
|
||
|
||
case 'bloodSugar': {
|
||
const period = record.period || ''
|
||
let limit = 11.1
|
||
if (period.includes('空腹') || period.includes('睡前'))
|
||
limit = 6.1
|
||
else if (period.includes('餐后1h'))
|
||
limit = 11.1
|
||
else if (period.includes('餐后2h'))
|
||
limit = 7.8
|
||
|
||
if (record.value < 3.9)
|
||
return { label: '偏低', type: 'danger' }
|
||
if (record.value >= limit)
|
||
return { label: '偏高', type: 'danger' }
|
||
return { label: '正常', type: 'primary' }
|
||
}
|
||
|
||
case 'heartRate':
|
||
if (record.value < 60)
|
||
return { label: '偏慢', type: 'warning' }
|
||
if (record.value <= 100)
|
||
return { label: '正常', type: 'primary' }
|
||
return { label: '偏快', type: 'warning' }
|
||
|
||
case 'temperature':
|
||
if (record.value < 36.1)
|
||
return { label: '偏低', type: 'warning' }
|
||
if (record.value <= 37.2)
|
||
return { label: '正常', type: 'primary' }
|
||
return { label: '偏高', type: 'danger' }
|
||
|
||
case 'sport':
|
||
return record.duration >= 30
|
||
? { label: '达标', type: 'success' }
|
||
: { label: '偏少', type: 'warning' }
|
||
|
||
case 'sleep':
|
||
if (record.duration >= 7)
|
||
return { label: '充足', type: 'success' }
|
||
if (record.duration >= 6)
|
||
return { label: '良好', type: 'primary' }
|
||
return { label: '不足', type: 'warning' }
|
||
|
||
default:
|
||
return { label: '未知', type: 'default' }
|
||
}
|
||
}
|
||
|
||
const WEEKDAY = ['日', '一', '二', '三', '四', '五', '六']
|
||
|
||
export function groupByDate(records: IHealthRecord[]): Array<{
|
||
date: string
|
||
weekday: string
|
||
items: IHealthRecord[]
|
||
}> {
|
||
const map: Record<string, IHealthRecord[]> = {}
|
||
records.forEach((r) => {
|
||
if (!r.date)
|
||
return
|
||
if (!map[r.date])
|
||
map[r.date] = []
|
||
map[r.date].push(r)
|
||
})
|
||
return Object.keys(map)
|
||
.sort((a, b) => b.localeCompare(a)) // 日期倒序,最新的在上
|
||
.map(date => ({
|
||
date,
|
||
weekday: WEEKDAY[new Date(date).getDay()],
|
||
items: map[date].sort((a, b) => a.time.localeCompare(b.time)), // 当天按时间正序
|
||
}))
|
||
}
|