feat(health): 趋势图升级为 ECharts 折线图 + 缓存 TTL 5分钟

This commit is contained in:
iven
2026-04-24 12:38:07 +08:00
parent 7b5b00fbac
commit a9861a0cde
2 changed files with 46 additions and 31 deletions

View File

@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { View, Text } from '@tarojs/components'; import { View, Text } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro'; import Taro, { useRouter } from '@tarojs/taro';
import { useHealthStore } from '../../../stores/health'; import { useHealthStore } from '@/stores/health';
import TrendChart from '@/components/TrendChart';
import './index.scss'; import './index.scss';
const RANGE_OPTIONS = [ const RANGE_OPTIONS = [
@@ -10,6 +11,15 @@ const RANGE_OPTIONS = [
{ value: '90d', label: '90天' }, { value: '90d', label: '90天' },
]; ];
const INDICATOR_META: Record<string, { label: string; unit: string; refMin?: number; refMax?: number }> = {
blood_pressure_systolic: { label: '收缩压', unit: 'mmHg', refMin: 90, refMax: 140 },
blood_pressure_diastolic: { label: '舒张压', unit: 'mmHg', refMin: 60, refMax: 90 },
heart_rate: { label: '心率', unit: 'bpm', refMin: 60, refMax: 100 },
blood_sugar_fasting: { label: '空腹血糖', unit: 'mmol/L', refMin: 3.9, refMax: 6.1 },
blood_sugar_postprandial: { label: '餐后血糖', unit: 'mmol/L', refMin: 3.9, refMax: 7.8 },
weight: { label: '体重', unit: 'kg' },
};
export default function Trend() { export default function Trend() {
const router = useRouter(); const router = useRouter();
const indicator = router.params.indicator || 'heart_rate'; const indicator = router.params.indicator || 'heart_rate';
@@ -21,12 +31,12 @@ export default function Trend() {
getTrend(indicator, range).then(setPoints); getTrend(indicator, range).then(setPoints);
}, [indicator, range]); }, [indicator, range]);
const maxVal = points.length ? Math.max(...points.map((p) => p.value)) : 1; const meta = INDICATOR_META[indicator] || { label: indicator, unit: '' };
return ( return (
<View className='trend-page'> <View className='trend-page'>
<View className='trend-header'> <View className='trend-header'>
<Text className='trend-title'>{indicator.replace(/_/g, ' ')} </Text> <Text className='trend-title'>{meta.label} </Text>
<View className='trend-tabs'> <View className='trend-tabs'>
{RANGE_OPTIONS.map((opt) => ( {RANGE_OPTIONS.map((opt) => (
<View <View
@@ -40,34 +50,27 @@ export default function Trend() {
</View> </View>
</View> </View>
{/* 简易柱状图 */} {/* ECharts 折线图 */}
<View className='trend-chart'> <View className='trend-chart-container'>
{points.length === 0 ? ( <TrendChart
<Text className='trend-empty'></Text> data={points}
) : ( referenceMin={meta.refMin}
<View className='chart-bars'> referenceMax={meta.refMax}
{points.slice(-14).map((p, i) => ( unit={meta.unit}
<View className='chart-bar-wrap' key={i}> />
<View
className='chart-bar'
style={{ height: `${(p.value / maxVal) * 100}%` }}
/>
<Text className='chart-bar-date'>{p.date.slice(5)}</Text>
</View>
))}
</View>
)}
</View> </View>
{/* 数据列表 */} {/* 数据列表 */}
<View className='trend-list'> {points.length > 0 && (
{points.slice().reverse().map((p, i) => ( <View className='trend-list'>
<View className='trend-item' key={i}> {points.slice().reverse().map((p, i) => (
<Text className='trend-item-date'>{p.date}</Text> <View className='trend-item' key={i}>
<Text className='trend-item-value'>{p.value}</Text> <Text className='trend-item-date'>{p.date}</Text>
</View> <Text className='trend-item-value'>{p.value}{meta.unit ? ` ${meta.unit}` : ''}</Text>
))} </View>
</View> ))}
</View>
)}
</View> </View>
); );
} }

View File

@@ -1,14 +1,22 @@
import { create } from 'zustand'; import { create } from 'zustand';
import * as healthApi from '@/services/health'; import * as healthApi from '@/services/health';
interface CachedTrend {
data: { date: string; value: number }[];
cachedAt: number;
}
interface HealthState { interface HealthState {
todaySummary: healthApi.TodaySummary | null; todaySummary: healthApi.TodaySummary | null;
trendData: Record<string, { date: string; value: number }[]>; trendData: Record<string, CachedTrend>;
loading: boolean; loading: boolean;
refreshToday: () => Promise<void>; refreshToday: () => Promise<void>;
getTrend: (indicator: string, range: string) => Promise<{ date: string; value: number }[]>; getTrend: (indicator: string, range: string) => Promise<{ date: string; value: number }[]>;
clearCache: () => void;
} }
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
export const useHealthStore = create<HealthState>((set, get) => ({ export const useHealthStore = create<HealthState>((set, get) => ({
todaySummary: null, todaySummary: null,
trendData: {}, trendData: {},
@@ -27,15 +35,19 @@ export const useHealthStore = create<HealthState>((set, get) => ({
getTrend: async (indicator: string, range: string) => { getTrend: async (indicator: string, range: string) => {
const cacheKey = `${indicator}_${range}`; const cacheKey = `${indicator}_${range}`;
const cached = get().trendData[cacheKey]; const cached = get().trendData[cacheKey];
if (cached) return cached; if (cached && Date.now() - cached.cachedAt < CACHE_TTL) {
return cached.data;
}
try { try {
const resp = await healthApi.getTrend(indicator, range); const resp = await healthApi.getTrend(indicator, range);
const points = resp.data_points || []; const points = resp.data_points || [];
set((s) => ({ trendData: { ...s.trendData, [cacheKey]: points } })); set((s) => ({ trendData: { ...s.trendData, [cacheKey]: { data: points, cachedAt: Date.now() } } }));
return points; return points;
} catch { } catch {
return []; return [];
} }
}, },
clearCache: () => set({ trendData: {}, todaySummary: null }),
})); }));