feat(health): 趋势图升级为 ECharts 折线图 + 缓存 TTL 5分钟
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user