feat(miniprogram): 老年友好版本全面重设计 — 5→4 Tab + 首页/健康/消息/我的重写
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- TabBar 从 5 Tab 调整为 4 Tab(首页/健康/消息/我的)
- 首页重写为 5 区域布局:问候+进度环+体征2x2+待办+快捷操作
- 健康页重写:体征录入大输入框+趋势柱状图+BLE设备卡片
- 新建消息页:咨询对话+系统通知双 Tab
- 我的页调整:菜单高度64px+新增积分商城入口
- 设计系统更新:色彩对比度提升(WCAG AA)+触控参数+老年友好 mixin
- 新增 ProgressRing 组件(CSS conic-gradient 实现)
- 修复 diagnoses 页面 $suc-l 未定义变量
This commit is contained in:
iven
2026-04-30 22:51:05 +08:00
parent 813843e8cc
commit 50772878da
14 changed files with 1256 additions and 771 deletions

View File

@@ -9,9 +9,6 @@
/* ─── 页头 ─── */
.health-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 32px 8px;
}
@@ -22,248 +19,272 @@
color: $tx;
}
.health-add-btn {
background: $pri;
padding: 10px 28px;
/* ─── 类型 Tab ─── */
.vital-tabs {
display: flex;
padding: 12px 24px;
gap: 12px;
}
.vital-tab {
flex: 1;
height: $tab-h;
border-radius: $r;
background: $surface-alt;
@include flex-center;
position: relative;
&:active {
opacity: 0.85;
}
&.vital-tab-active {
background: $pri;
.vital-tab-text {
color: #fff;
}
}
}
.vital-tab-text {
font-size: 26px;
font-weight: 600;
color: $tx2;
}
.vital-tab-dot {
position: absolute;
top: 10px;
right: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background: $wrn;
}
/* ─── 录入区 ─── */
.input-section {
margin: 0 24px 24px;
background: $card;
border-radius: $r;
padding: 24px;
box-shadow: $shadow-sm;
}
.input-group {
margin-bottom: 20px;
}
.input-label {
font-size: 26px;
color: $tx;
font-weight: 600;
display: block;
margin-bottom: 12px;
}
.input-field {
height: 56px;
background: $bg;
border: 2px solid $bd;
border-radius: $r-sm;
padding: 0 20px;
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px;
color: $tx;
width: 100%;
box-sizing: border-box;
}
.input-ref {
font-size: 22px;
color: $tx2;
display: block;
margin-top: 12px;
}
/* ─── 血糖时段选择 ─── */
.period-group {
display: flex;
gap: 12px;
margin-top: 16px;
}
.period-btn {
flex: 1;
height: $btn-primary-h;
border-radius: $r-sm;
background: $surface-alt;
@include flex-center;
&.period-active {
background: $pri;
.period-btn-text {
color: #fff;
}
}
&:active {
opacity: 0.85;
}
}
.health-add-text {
.period-btn-text {
font-size: 26px;
color: #fff;
font-weight: 600;
color: $tx2;
}
/* ─── 快捷操作 ─── */
.health-actions-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 16px 24px 24px;
/* ─── 保存按钮 ─── */
.save-btn {
@include btn-primary;
margin-top: 24px;
border-radius: $r;
}
.action-item {
flex: 1;
min-width: 100px;
.save-btn-text {
font-size: 28px;
font-weight: 600;
color: #fff;
}
/* ─── 趋势图 ─── */
.trend-section {
margin: 0 24px 24px;
}
.trend-empty {
background: $card;
border-radius: $r;
padding: 20px 12px;
padding: 36px;
text-align: center;
}
.trend-empty-text {
font-size: 24px;
color: $tx2;
}
.trend-chart {
background: $card;
border-radius: $r;
padding: 24px;
box-shadow: $shadow-sm;
}
.trend-bars {
display: flex;
align-items: flex-end;
height: 200px;
gap: 8px;
}
.trend-bar-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
box-shadow: $shadow-sm;
height: 100%;
justify-content: flex-end;
}
&:active {
opacity: 0.7;
.trend-bar {
width: 100%;
max-width: 40px;
border-radius: 6px 6px 0 0;
min-height: 16px;
&.trend-bar-normal {
background: $pri;
}
&.trend-bar-warn {
background: $wrn;
}
}
.action-icon {
width: 72px;
height: 72px;
border-radius: 50%;
@include flex-center;
&.icon-primary { background: $pri-l; }
&.icon-accent { background: $acc-l; }
&.icon-warn { background: $wrn-l; }
.trend-bar-label {
font-size: 22px;
color: $tx2;
margin-top: 8px;
}
.action-char {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 32px;
font-weight: bold;
color: $pri;
.icon-accent & { color: $acc; }
.icon-warn & { color: $wrn; }
/* ─── BLE 设备卡片 ─── */
.device-section {
margin: 0 24px 16px;
}
.action-label {
font-size: 24px;
color: $tx;
font-weight: 500;
}
/* ─── 通用 section ─── */
.health-section {
margin: 0 24px 28px;
}
/* ─── 体征概览 ─── */
.vitals-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.vital-card {
.device-card {
display: flex;
align-items: center;
background: $card;
border-radius: $r;
padding: 24px 20px;
padding: 24px;
box-shadow: $shadow-sm;
transition: opacity 0.2s;
&:active {
opacity: 0.7;
opacity: 0.85;
}
}
.vital-label {
.device-icon {
width: 56px;
height: 56px;
border-radius: $r-sm;
background: $pri-l;
@include flex-center;
margin-right: 16px;
flex-shrink: 0;
}
.device-icon-text {
font-size: 26px;
font-weight: bold;
color: $pri;
}
.device-info {
flex: 1;
min-width: 0;
}
.device-name {
font-size: 28px;
font-weight: 600;
color: $tx;
display: block;
margin-bottom: 4px;
}
.device-desc {
font-size: 22px;
color: $tx2;
display: block;
margin-bottom: 10px;
}
.vital-value {
@include serif-number;
font-size: 44px;
font-weight: bold;
color: $tx;
display: block;
margin-bottom: 8px;
line-height: 1.1;
}
.vital-bottom {
display: flex;
justify-content: space-between;
align-items: center;
}
.vital-unit {
font-size: 20px;
.device-arrow {
font-size: 32px;
color: $tx3;
flex-shrink: 0;
}
.vital-tag {
@include tag($acc-l, $acc);
&.tag-warn {
@include tag($wrn-l, $wrn);
}
}
.vital-ref {
font-size: 20px;
color: $tx3;
margin-top: 8px;
display: block;
}
.vital-bar-track {
height: 6px;
background: $bd-l;
border-radius: 3px;
margin-top: 12px;
overflow: hidden;
}
.vital-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
&.bar-green { background: $acc; }
&.bar-orange { background: $wrn; }
&.bar-red { background: $dan; }
}
/* ─── 趋势入口 — 水平滚动卡片 ─── */
.trend-scroll {
white-space: nowrap;
width: 100%;
}
.trend-card {
display: inline-flex;
flex-direction: column;
align-items: center;
width: 200px;
/* ─── 健康资讯入口 ─── */
.article-entry {
margin: 0 24px 24px;
background: $card;
border-radius: $r;
padding: 24px 16px;
margin-right: 16px;
padding: 24px;
box-shadow: $shadow-sm;
vertical-align: top;
&:active {
opacity: 0.7;
opacity: 0.85;
}
}
.trend-card-icon {
width: 64px;
height: 64px;
border-radius: $r;
background: $pri-l;
@include flex-center;
margin-bottom: 12px;
}
.trend-card-char {
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 28px;
font-weight: bold;
color: $pri;
}
.trend-card-label {
.article-entry-text {
font-size: 26px;
color: $tx;
font-weight: 500;
white-space: nowrap;
}
.trend-card-arrow {
font-size: 22px;
color: $tx3;
margin-top: 8px;
}
/* ─── 最近监测 ─── */
.record-card {
background: $card;
border-radius: $r;
padding: 20px 24px;
margin-bottom: 12px;
box-shadow: $shadow-sm;
}
.record-date {
font-size: 24px;
color: $pri;
font-weight: 600;
display: block;
margin-bottom: 12px;
}
.record-data {
display: flex;
gap: 32px;
flex-wrap: wrap;
}
.record-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.record-item-label {
font-size: 22px;
color: $tx3;
}
.record-item-value {
@include serif-number;
font-size: 26px;
color: $tx;
font-weight: 500;
}

View File

@@ -1,235 +1,330 @@
import { useState } from 'react';
import { View, Text, ScrollView } from '@tarojs/components';
import { View, Text, Input } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { useHealthStore } from '../../stores/health';
import { listDailyMonitoring, DailyMonitoring } from '../../services/health';
import { usePointsStore } from '../../stores/points';
import { useAuthStore } from '../../stores/auth';
import { trackEvent } from '../../services/analytics';
import { inputVitalSign, getTrend } from '../../services/health';
import Loading from '../../components/Loading';
import './index.scss';
const QUICK_ACTIONS = [
{ label: '日常上报', char: '日', bg: 'icon-primary' },
{ label: '体征录入', char: '录', bg: 'icon-accent' },
{ label: '查看趋势', char: '势', bg: 'icon-warn' },
type VitalType = 'blood_pressure' | 'heart_rate' | 'blood_sugar' | 'weight';
const VITAL_TABS: { key: VitalType; label: string }[] = [
{ key: 'blood_pressure', label: '血压' },
{ key: 'heart_rate', label: '心率' },
{ key: 'blood_sugar', label: '血糖' },
{ key: 'weight', label: '体重' },
];
const TREND_LINKS = [
{ label: '血压趋势', indicator: 'blood_pressure_systolic', char: '' },
{ label: '心率趋势', indicator: 'heart_rate', char: '' },
{ label: '血糖趋势', indicator: 'blood_sugar_fasting', char: '' },
];
const REF_RANGES: Record<VitalType, { range: string; warn: string }> = {
blood_pressure: { range: '收缩压 90-140 / 舒张压 60-90 mmHg', warn: '血压偏高,确认提交?' },
heart_rate: { range: '60-100 bpm', warn: '心率异常,确认提交?' },
blood_sugar: { range: '空腹 3.9-6.1 / 餐后 <7.8 mmol/L', warn: '血糖偏高,确认提交?' },
weight: { range: '根据 BMI 18.5-24 计算', warn: '' },
};
function getStatusTag(status?: string) {
if (status === 'high') return { label: '偏高', cls: 'tag-warn' };
if (status === 'low') return { label: '偏低', cls: 'tag-warn' };
if (status === 'normal') return { label: '正常', cls: 'tag-ok' };
return null;
}
/** 根据 status 计算 sparkline bar 的颜色 */
function getBarColor(status?: string): string {
if (status === 'normal') return 'bar-green';
if (status === 'high' || status === 'low') return 'bar-orange';
return 'bar-green';
}
/** 计算数值在参考范围中的位置百分比 (0-100) */
function getBarPercent(value: number | undefined, ref?: string): number {
if (!value || !ref) return 50;
const match = ref.match(/([\d.]+)\s*[-]\s*([\d.]+)/);
if (!match) return 50;
const low = parseFloat(match[1]);
const high = parseFloat(match[2]);
if (high <= low) return 50;
// 将值映射到 0-100 范围,参考范围占据中间 70%15%-85%
const range = high - low;
const normalized = (value - low + range * 0.3) / (range * 1.6);
return Math.max(5, Math.min(95, normalized * 100));
interface TrendPoint {
date: string;
value: number;
}
export default function Health() {
const { todaySummary, loading, refreshToday } = useHealthStore();
const { checkinStatus, refresh: refreshPoints } = usePointsStore();
const { todaySummary, loading, refreshToday, getTrend: fetchTrend } = useHealthStore();
const { currentPatient } = useAuthStore();
const [recentRecords, setRecentRecords] = useState<DailyMonitoring[]>([]);
const [activeTab, setActiveTab] = useState<VitalType>('blood_pressure');
const [systolic, setSystolic] = useState('');
const [diastolic, setDiastolic] = useState('');
const [heartRateVal, setHeartRateVal] = useState('');
const [sugarVal, setSugarVal] = useState('');
const [sugarPeriod, setSugarPeriod] = useState<'fasting' | 'postprandial'>('fasting');
const [weightVal, setWeightVal] = useState('');
const [saving, setSaving] = useState(false);
const [trendData, setTrendData] = useState<TrendPoint[]>([]);
const [trendLoading, setTrendLoading] = useState(false);
useDidShow(() => {
refreshToday();
refreshPoints();
loadRecentRecords();
loadTrend(activeTab);
});
const loadRecentRecords = async () => {
if (currentPatient) {
try {
const resp = await listDailyMonitoring(currentPatient.id, { page: 1, page_size: 3 });
setRecentRecords(resp.data || []);
} catch {
// daily monitoring API 可能不可用
const loadTrend = async (type: VitalType) => {
setTrendLoading(true);
try {
const indicatorMap: Record<VitalType, string> = {
blood_pressure: 'blood_pressure_systolic',
heart_rate: 'heart_rate',
blood_sugar: 'blood_sugar_fasting',
weight: 'weight',
};
const points = await fetchTrend(indicatorMap[type], '7d');
setTrendData(points);
} catch {
setTrendData([]);
} finally {
setTrendLoading(false);
}
};
const handleTabChange = (tab: VitalType) => {
setActiveTab(tab);
loadTrend(tab);
};
const getWarnStatus = (type: VitalType): string | null => {
if (type === 'blood_pressure') {
const sys = parseFloat(systolic);
const dia = parseFloat(diastolic);
if (sys > 140 || dia > 90) return REF_RANGES.blood_pressure.warn;
} else if (type === 'heart_rate') {
const val = parseFloat(heartRateVal);
if (val > 100 || val < 60) return REF_RANGES.heart_rate.warn;
} else if (type === 'blood_sugar') {
const val = parseFloat(sugarVal);
if (sugarPeriod === 'fasting' && val > 6.1) return REF_RANGES.blood_sugar.warn;
if (sugarPeriod === 'postprandial' && val > 7.8) return REF_RANGES.blood_sugar.warn;
}
return null;
};
const handleSave = async () => {
const patientId = currentPatient?.id;
if (!patientId) {
Taro.showToast({ title: '请先登录', icon: 'none' });
return;
}
const warnMsg = getWarnStatus(activeTab);
if (warnMsg) {
const { confirm } = await Taro.showModal({
title: '异常提示',
content: warnMsg,
confirmText: '确认提交',
cancelText: '再看看',
});
if (!confirm) return;
}
setSaving(true);
try {
switch (activeTab) {
case 'blood_pressure': {
const sys = parseFloat(systolic);
const dia = parseFloat(diastolic);
if (!sys || !dia) { Taro.showToast({ title: '请填写完整', icon: 'none' }); return; }
await inputVitalSign(patientId, {
indicator_type: 'blood_pressure',
value: sys,
extra: { systolic: sys, diastolic: dia },
});
setSystolic('');
setDiastolic('');
break;
}
case 'heart_rate': {
const val = parseFloat(heartRateVal);
if (!val) { Taro.showToast({ title: '请填写心率', icon: 'none' }); return; }
await inputVitalSign(patientId, { indicator_type: 'heart_rate', value: val });
setHeartRateVal('');
break;
}
case 'blood_sugar': {
const val = parseFloat(sugarVal);
if (!val) { Taro.showToast({ title: '请填写血糖值', icon: 'none' }); return; }
await inputVitalSign(patientId, { indicator_type: 'blood_sugar', value: val });
setSugarVal('');
break;
}
case 'weight': {
const val = parseFloat(weightVal);
if (!val) { Taro.showToast({ title: '请填写体重', icon: 'none' }); return; }
await inputVitalSign(patientId, { indicator_type: 'weight', value: val });
setWeightVal('');
break;
}
}
Taro.showToast({ title: '保存成功', icon: 'success' });
refreshToday(true);
loadTrend(activeTab);
} catch {
Taro.showToast({ title: '保存失败', icon: 'none' });
} finally {
setSaving(false);
}
};
const goToInput = () => {
Taro.navigateTo({ url: '/pages/pkg-health/input/index' });
};
const goToDailyMonitoring = () => {
Taro.navigateTo({ url: '/pages/pkg-health/daily-monitoring/index' });
};
const goToTrend = (indicator: string) => {
Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${indicator}` });
};
const goToMall = () => {
Taro.switchTab({ url: '/pages/mall/index' });
};
const summary = todaySummary || {};
const items = [
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '--/--', unit: 'mmHg', indicator: 'blood_pressure_systolic', status: summary.blood_pressure?.status, ref: summary.blood_pressure?.reference_range, numValue: summary.blood_pressure?.systolic },
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '--', unit: 'bpm', indicator: 'heart_rate', status: summary.heart_rate?.status, ref: summary.heart_rate?.reference_range, numValue: summary.heart_rate?.value },
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '--', unit: 'mmol/L', indicator: 'blood_sugar_fasting', status: summary.blood_sugar?.status, ref: summary.blood_sugar?.reference_range, numValue: summary.blood_sugar?.value },
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '--', unit: 'kg', indicator: 'weight', status: summary.weight?.status, ref: summary.weight?.reference_range, numValue: summary.weight?.value },
];
const quickActions = [
{ ...QUICK_ACTIONS[0], action: goToDailyMonitoring },
{ ...QUICK_ACTIONS[1], action: goToInput },
{ ...QUICK_ACTIONS[2], action: () => goToTrend('blood_pressure_systolic') },
];
const trendLinks = TREND_LINKS;
const formatBp = (record: DailyMonitoring) => {
const parts: string[] = [];
if (record.morning_bp_systolic && record.morning_bp_diastolic) {
parts.push(`${record.morning_bp_systolic}/${record.morning_bp_diastolic}`);
}
if (record.evening_bp_systolic && record.evening_bp_diastolic) {
parts.push(`${record.evening_bp_systolic}/${record.evening_bp_diastolic}`);
}
return parts.length > 0 ? parts.join(' ') : '--';
};
const maxTrendValue = Math.max(...trendData.map((d) => d.value), 1);
const dayLabels = ['日', '一', '二', '三', '四', '五', '六'];
return (
<View className='health-page'>
{/* 页头 */}
<View className='health-header'>
<Text className='health-title'></Text>
<View className='health-add-btn' onClick={goToInput}>
<Text className='health-add-text'></Text>
<Text className='health-title'></Text>
</View>
{/* 类型 Tab */}
<View className='vital-tabs'>
{VITAL_TABS.map((tab) => {
const hasData = tab.key === 'blood_pressure' ? !!todaySummary?.blood_pressure
: tab.key === 'heart_rate' ? !!todaySummary?.heart_rate
: tab.key === 'blood_sugar' ? !!todaySummary?.blood_sugar
: !!todaySummary?.weight;
return (
<View
key={tab.key}
className={`vital-tab ${activeTab === tab.key ? 'vital-tab-active' : ''}`}
onClick={() => handleTabChange(tab.key)}
>
<Text className='vital-tab-text'>{tab.label}</Text>
{!hasData && <View className='vital-tab-dot' />}
</View>
);
})}
</View>
{/* 录入区 */}
<View className='input-section'>
{activeTab === 'blood_pressure' && (
<View className='input-group'>
<Text className='input-label'></Text>
<Input
className='input-field'
type='number'
placeholder='如 130'
value={systolic}
onInput={(e) => setSystolic(e.detail.value)}
/>
<Text className='input-label' style='margin-top:20px;'></Text>
<Input
className='input-field'
type='number'
placeholder='如 85'
value={diastolic}
onInput={(e) => setDiastolic(e.detail.value)}
/>
<Text className='input-ref'>{REF_RANGES.blood_pressure.range}</Text>
</View>
)}
{activeTab === 'heart_rate' && (
<View className='input-group'>
<Text className='input-label'></Text>
<Input
className='input-field'
type='digit'
placeholder='如 72'
value={heartRateVal}
onInput={(e) => setHeartRateVal(e.detail.value)}
/>
<Text className='input-ref'>{REF_RANGES.heart_rate.range}</Text>
</View>
)}
{activeTab === 'blood_sugar' && (
<View className='input-group'>
<Text className='input-label'></Text>
<Input
className='input-field'
type='digit'
placeholder='如 5.6'
value={sugarVal}
onInput={(e) => setSugarVal(e.detail.value)}
/>
<View className='period-group'>
<View
className={`period-btn ${sugarPeriod === 'fasting' ? 'period-active' : ''}`}
onClick={() => setSugarPeriod('fasting')}
>
<Text className='period-btn-text'></Text>
</View>
<View
className={`period-btn ${sugarPeriod === 'postprandial' ? 'period-active' : ''}`}
onClick={() => setSugarPeriod('postprandial')}
>
<Text className='period-btn-text'> 2h</Text>
</View>
</View>
<Text className='input-ref'>{REF_RANGES.blood_sugar.range}</Text>
</View>
)}
{activeTab === 'weight' && (
<View className='input-group'>
<Text className='input-label'> (kg)</Text>
<Input
className='input-field'
type='digit'
placeholder='如 65.5'
value={weightVal}
onInput={(e) => setWeightVal(e.detail.value)}
/>
<Text className='input-ref'>{REF_RANGES.weight.range}</Text>
</View>
)}
<View className='save-btn' onClick={handleSave}>
<Text className='save-btn-text'>{saving ? '保存中...' : '保存'}</Text>
</View>
</View>
{/* 快捷操作 + 打卡状态紧凑合并 */}
<View className='health-actions-row'>
{quickActions.map((a) => (
<View className='action-item' key={a.label} onClick={a.action}>
<View className={`action-icon ${a.bg}`}>
<Text className='action-char'>{a.char}</Text>
</View>
<Text className='action-label'>{a.label}</Text>
</View>
))}
{checkinStatus && (
<View
className='action-item checkin-badge'
onClick={!checkinStatus.checked_in_today ? goToMall : undefined}
>
<View className={`action-icon ${checkinStatus.checked_in_today ? 'icon-accent' : 'icon-warn'}`}>
<Text className='action-char'></Text>
</View>
<Text className='action-label'>
{checkinStatus.checked_in_today
? (checkinStatus.consecutive_days > 0 ? `已打卡${checkinStatus.consecutive_days}` : '已打卡')
: '去打卡'}
</Text>
</View>
)}
</View>
{/* 今日体征概览 */}
<View className='health-section'>
<Text className='section-title'></Text>
{loading && !todaySummary ? (
{/* 趋势图 */}
<View className='trend-section'>
<Text className='section-title'> 7 </Text>
{trendLoading ? (
<Loading />
) : trendData.length === 0 ? (
<View className='trend-empty'>
<Text className='trend-empty-text'></Text>
</View>
) : (
<View className='vitals-grid'>
{items.map((item) => {
const tag = getStatusTag(item.status);
const barColor = getBarColor(item.status);
const barPercent = getBarPercent(item.numValue, item.ref);
return (
<View className='vital-card' key={item.label} onClick={() => goToTrend(item.indicator)}>
<Text className='vital-label'>{item.label}</Text>
<Text className='vital-value'>{item.value}</Text>
<View className='vital-bottom'>
<Text className='vital-unit'>{item.unit}</Text>
{tag && <Text className={`vital-tag ${tag.cls}`}>{tag.label}</Text>}
<View className='trend-chart'>
<View className='trend-bars'>
{trendData.map((point, i) => {
const heightPct = Math.max(8, (point.value / maxTrendValue) * 100);
const isAbnormal = activeTab === 'blood_pressure' ? point.value > 140
: activeTab === 'heart_rate' ? (point.value > 100 || point.value < 60)
: activeTab === 'blood_sugar' ? point.value > 6.1
: false;
const dayOfWeek = new Date(point.date).getDay();
return (
<View className='trend-bar-col' key={i}>
<View
className={`trend-bar ${isAbnormal ? 'trend-bar-warn' : 'trend-bar-normal'}`}
style={`height:${heightPct}%;`}
/>
<Text className='trend-bar-label'>{dayLabels[dayOfWeek]}</Text>
</View>
{/* Sparkline bar */}
{item.ref && item.numValue != null && (
<View className='vital-bar-track'>
<View className={`vital-bar-fill ${barColor}`} style={`width: ${barPercent}%`} />
</View>
)}
{item.ref && <Text className='vital-ref'> {item.ref}</Text>}
</View>
);
})}
);
})}
</View>
</View>
)}
</View>
{/* 趋势快捷入口 — 水平滚动卡片 */}
<View className='health-section'>
<Text className='section-title'></Text>
<ScrollView className='trend-scroll' scrollX>
{trendLinks.map((t) => (
<View className='trend-card' key={t.label} onClick={() => goToTrend(t.indicator)}>
<View className='trend-card-icon'>
<Text className='trend-card-char'>{t.char}</Text>
</View>
<Text className='trend-card-label'>{t.label}</Text>
<Text className='trend-card-arrow'> </Text>
</View>
))}
</ScrollView>
{/* BLE 设备卡片 */}
<View className='device-section'>
<View
className='device-card'
onClick={() => Taro.navigateTo({ url: '/pages/device-sync/index' })}
>
<View className='device-icon'>
<Text className='device-icon-text'></Text>
</View>
<View className='device-info'>
<Text className='device-name'></Text>
<Text className='device-desc'></Text>
</View>
<Text className='device-arrow'></Text>
</View>
</View>
{/* 最近监测记录 */}
{recentRecords.length > 0 && (
<View className='health-section'>
<Text className='section-title'></Text>
{recentRecords.map((record) => (
<View className='record-card' key={record.id}>
<Text className='record-date'>{record.record_date}</Text>
<View className='record-data'>
<View className='record-item'>
<Text className='record-item-label'></Text>
<Text className='record-item-value'>{formatBp(record)}</Text>
</View>
{record.weight != null && (
<View className='record-item'>
<Text className='record-item-label'></Text>
<Text className='record-item-value'>{record.weight} kg</Text>
</View>
)}
{record.blood_sugar != null && (
<View className='record-item'>
<Text className='record-item-label'></Text>
<Text className='record-item-value'>{record.blood_sugar} mmol/L</Text>
</View>
)}
</View>
</View>
))}
</View>
)}
{/* 健康资讯入口 */}
<View
className='article-entry'
onClick={() => Taro.navigateTo({ url: '/pages/article/index' })}
>
<Text className='article-entry-text'> </Text>
</View>
</View>
);
}