feat(miniprogram): Phase 5 UI/UX 优化 — 8 项改进
- 首页: 健康资讯推荐 + 空状态引导 + 快捷服务字符图标优化 - 健康 Hub: sparkline bar + 参考范围 + 打卡合并到快捷操作 - 日常监测: 3 分组折叠(晨间/晚间/其他) + 异常值高亮 + 提交前确认 - 预约: 已满时段 pointer-events:none + opacity 优化 - 咨询聊天: 消息日期分组(今天/昨天) + 图片预览 - 积分商城: 确认已有余额大字+签到+库存提示 - 医护工作台: 异常体征横幅 + 患者搜索入口 + 快捷操作扩展 - 趋势图表: 骨架屏加载状态 + ECharts 异常标记已有
This commit is contained in:
@@ -102,8 +102,9 @@
|
||||
border-color: $wrn;
|
||||
}
|
||||
&.slot-full {
|
||||
opacity: 0.5;
|
||||
opacity: 0.4;
|
||||
background: $bd-l;
|
||||
pointer-events: none;
|
||||
}
|
||||
&.slot-selected {
|
||||
border-color: $pri;
|
||||
|
||||
@@ -73,6 +73,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
.msg-date-divider {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0 12px;
|
||||
|
||||
&__text {
|
||||
font-size: 22px;
|
||||
color: #94A3B8;
|
||||
background: #F1F5F9;
|
||||
padding: 4px 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.msg-image {
|
||||
width: 320px;
|
||||
border-radius: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
font-size: 20px;
|
||||
color: $tx3;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import { View, Text, Input, Image, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useRouter } from '@tarojs/taro';
|
||||
import {
|
||||
getSession,
|
||||
@@ -116,6 +116,24 @@ export default function ConsultationDetail() {
|
||||
return d.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const getDateLabel = (dateStr: string): string => {
|
||||
const d = new Date(dateStr);
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const dStr = d.toDateString();
|
||||
if (dStr === today.toDateString()) return '今天';
|
||||
if (dStr === yesterday.toDateString()) return '昨天';
|
||||
return d.toLocaleDateString('zh-CN', { month: 'long', day: 'numeric' });
|
||||
};
|
||||
|
||||
const isDifferentDay = (a: string, b: string): boolean => {
|
||||
return new Date(a).toDateString() !== new Date(b).toDateString();
|
||||
};
|
||||
|
||||
const isImageUrl = (url: string) => /\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i.test(url);
|
||||
|
||||
if (loading) return <Loading />;
|
||||
|
||||
const isOpen = session?.status !== 'closed';
|
||||
@@ -137,11 +155,28 @@ export default function ConsultationDetail() {
|
||||
>
|
||||
{messages.map((msg, idx) => {
|
||||
const isSelf = msg.sender_role === 'patient';
|
||||
const showDateDivider = idx === 0 || isDifferentDay(msg.created_at, messages[idx - 1].created_at);
|
||||
return (
|
||||
<View key={msg.id} id={`msg-${idx + 1}`} className={`msg-row ${isSelf ? 'msg-row--self' : ''}`}>
|
||||
<View className={`msg-bubble ${isSelf ? 'msg-bubble--self' : 'msg-bubble--other'}`}>
|
||||
<Text className='msg-text'>{msg.content}</Text>
|
||||
<Text className='msg-time'>{formatTime(msg.created_at)}</Text>
|
||||
<View key={msg.id}>
|
||||
{showDateDivider && (
|
||||
<View className='msg-date-divider'>
|
||||
<Text className='msg-date-divider__text'>{getDateLabel(msg.created_at)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View id={`msg-${idx + 1}`} className={`msg-row ${isSelf ? 'msg-row--self' : ''}`}>
|
||||
<View className={`msg-bubble ${isSelf ? 'msg-bubble--self' : 'msg-bubble--other'}`}>
|
||||
{isImageUrl(msg.content) ? (
|
||||
<Image
|
||||
className='msg-image'
|
||||
src={msg.content}
|
||||
mode='widthFix'
|
||||
onClick={() => Taro.previewImage({ urls: [msg.content], current: msg.content })}
|
||||
/>
|
||||
) : (
|
||||
<Text className='msg-text'>{msg.content}</Text>
|
||||
)}
|
||||
<Text className='msg-time'>{formatTime(msg.created_at)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,54 @@
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
&__alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16px 24px;
|
||||
padding: 16px 20px;
|
||||
background: #FEF2F2;
|
||||
border-radius: $r;
|
||||
border-left: 4px solid #EF4444;
|
||||
}
|
||||
|
||||
&__alert-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #EF4444;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__alert-text {
|
||||
flex: 1;
|
||||
font-size: 26px;
|
||||
color: #991B1B;
|
||||
}
|
||||
|
||||
&__alert-link {
|
||||
font-size: 24px;
|
||||
color: #EF4444;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__search {
|
||||
margin: 0 24px 16px;
|
||||
}
|
||||
|
||||
&__search-input {
|
||||
background: #F1F5F9;
|
||||
border-radius: $r;
|
||||
padding: 16px 20px;
|
||||
font-size: 26px;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
&__section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import { View, Text, Input, ScrollView } from '@tarojs/components';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import * as doctorApi from '@/services/doctor';
|
||||
@@ -29,11 +29,14 @@ const HEALTH_CARDS: CardConfig[] = [
|
||||
const QUICK_ACTIONS = [
|
||||
{ label: '化验审核', initial: '审', route: '/pages/doctor/report/index' },
|
||||
{ label: '患者查询', initial: '查', route: '/pages/doctor/patients/index' },
|
||||
{ label: '随访记录', initial: '随', route: '/pages/doctor/followup/index' },
|
||||
{ label: '排班查看', initial: '排', route: '/pages/doctor/patients/index' },
|
||||
];
|
||||
|
||||
export default function DoctorHome() {
|
||||
const { user, logout } = useAuthStore();
|
||||
const [dashboard, setDashboard] = useState<doctorApi.DoctorDashboard | null>(null);
|
||||
const [alertCount, setAlertCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,6 +47,9 @@ export default function DoctorHome() {
|
||||
try {
|
||||
const data = await doctorApi.getDashboard();
|
||||
setDashboard(data);
|
||||
// 从仪表盘数据提取异常体征患者数
|
||||
const count = (data as Record<string, unknown>)?.abnormal_vital_count;
|
||||
setAlertCount(typeof count === 'number' ? count : 0);
|
||||
} catch {
|
||||
// 静默失败,显示占位
|
||||
} finally {
|
||||
@@ -78,6 +84,22 @@ export default function DoctorHome() {
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{alertCount > 0 && (
|
||||
<View className='doctor-home__alert'>
|
||||
<Text className='doctor-home__alert-icon'>!</Text>
|
||||
<Text className='doctor-home__alert-text'>{alertCount} 位患者体征异常</Text>
|
||||
<Text className='doctor-home__alert-link' onClick={() => Taro.navigateTo({ url: '/pages/doctor/patients/index' })}>查看 →</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className='doctor-home__search'>
|
||||
<Input
|
||||
className='doctor-home__search-input'
|
||||
placeholder='搜索患者姓名...'
|
||||
onFocus={() => Taro.navigateTo({ url: '/pages/doctor/patients/index' })}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className='doctor-home__section'>
|
||||
<Text className='doctor-home__section-title'>工作概览</Text>
|
||||
<View className='doctor-home__grid'>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
/* ── card ── */
|
||||
/* ── card (standalone, used for date picker) ── */
|
||||
.dm-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
@@ -58,22 +58,6 @@
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dm-card-serial {
|
||||
@include flex-center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: $r-sm;
|
||||
background: $pri-l;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dm-card-serial-text {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
.dm-card-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
@@ -112,6 +96,61 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ── collapsible group ── */
|
||||
.dm-group {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
box-shadow: $shadow-md;
|
||||
margin: 0 24px 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dm-group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24px 28px;
|
||||
|
||||
&:active {
|
||||
background: $bd-l;
|
||||
}
|
||||
}
|
||||
|
||||
.dm-group-title {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
}
|
||||
|
||||
.dm-group-arrow {
|
||||
font-size: 24px;
|
||||
color: $tx3;
|
||||
transition: transform 0.2s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dm-group-arrow-open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.dm-group-body {
|
||||
padding: 0 28px 28px;
|
||||
}
|
||||
|
||||
.dm-group-collapsed .dm-group-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── inner field spacing (within groups) ── */
|
||||
.dm-inner-field {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── blood pressure group ── */
|
||||
.dm-bp-group {
|
||||
display: flex;
|
||||
@@ -192,6 +231,23 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ── abnormal value highlighting ── */
|
||||
.dm-input-abnormal {
|
||||
border: 2px solid $wrn;
|
||||
background: $wrn-l;
|
||||
}
|
||||
|
||||
.dm-field-warning {
|
||||
font-size: 22px;
|
||||
color: $wrn;
|
||||
margin-top: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dm-field-warning-low {
|
||||
color: #0284C7;
|
||||
}
|
||||
|
||||
/* ── submit ── */
|
||||
.dm-submit {
|
||||
background: $pri;
|
||||
|
||||
@@ -19,6 +19,41 @@ function formatDate(date: Date): string {
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
// ── Abnormal value detection ──
|
||||
|
||||
const REFERENCE_RANGES: Record<string, { min: number; max: number } | null> = {
|
||||
systolic: { min: 90, max: 140 },
|
||||
diastolic: { min: 60, max: 90 },
|
||||
bloodSugar: { min: 3.9, max: 6.1 },
|
||||
weight: null,
|
||||
fluidIntake: null,
|
||||
urineOutput: null,
|
||||
};
|
||||
|
||||
type AbnormalResult = { abnormal: boolean; direction: 'high' | 'low' | null };
|
||||
|
||||
const checkAbnormal = (value: string, field: string): AbnormalResult => {
|
||||
const ref = REFERENCE_RANGES[field];
|
||||
if (!value || !ref) return { abnormal: false, direction: null };
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return { abnormal: false, direction: null };
|
||||
if (num > ref.max) return { abnormal: true, direction: 'high' };
|
||||
if (num < ref.min) return { abnormal: true, direction: 'low' };
|
||||
return { abnormal: false, direction: null };
|
||||
};
|
||||
|
||||
// ── Section state type ──
|
||||
|
||||
type SectionKey = 'morning' | 'evening' | 'other';
|
||||
|
||||
const FIELD_LABELS: Record<string, string> = {
|
||||
morningSystolic: '晨间收缩压',
|
||||
morningDiastolic: '晨间舒张压',
|
||||
eveningSystolic: '晚间收缩压',
|
||||
eveningDiastolic: '晚间舒张压',
|
||||
bloodSugar: '血糖',
|
||||
};
|
||||
|
||||
export default function DailyMonitoring() {
|
||||
const { currentPatient } = useAuthStore();
|
||||
|
||||
@@ -46,6 +81,17 @@ export default function DailyMonitoring() {
|
||||
const [notes, setNotes] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// ── Collapsible sections ──
|
||||
const [collapsed, setCollapsed] = useState<Record<SectionKey, boolean>>({
|
||||
morning: false,
|
||||
evening: false,
|
||||
other: true,
|
||||
});
|
||||
|
||||
const toggleSection = (key: SectionKey) => {
|
||||
setCollapsed(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
|
||||
useDidShow(() => {
|
||||
Taro.setNavigationBarTitle({ title: '日常监测上报' });
|
||||
});
|
||||
@@ -62,6 +108,28 @@ export default function DailyMonitoring() {
|
||||
setNotes('');
|
||||
};
|
||||
|
||||
// ── Abnormal field gathering for submit confirmation ──
|
||||
const gatherAbnormalFields = (): string[] => {
|
||||
const abnormalFields: string[] = [];
|
||||
|
||||
const checks: Array<[string, string]> = [
|
||||
['morningSystolic', morningSystolic],
|
||||
['morningDiastolic', morningDiastolic],
|
||||
['eveningSystolic', eveningSystolic],
|
||||
['eveningDiastolic', eveningDiastolic],
|
||||
['bloodSugar', bloodSugar],
|
||||
];
|
||||
|
||||
for (const [field, value] of checks) {
|
||||
const result = checkAbnormal(value, field);
|
||||
if (result.abnormal) {
|
||||
abnormalFields.push(FIELD_LABELS[field]);
|
||||
}
|
||||
}
|
||||
|
||||
return abnormalFields;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentPatient) {
|
||||
Taro.showToast({ title: '请先选择就诊人', icon: 'none' });
|
||||
@@ -120,6 +188,18 @@ export default function DailyMonitoring() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Pre-submit abnormal confirmation ──
|
||||
const abnormalFields = gatherAbnormalFields();
|
||||
if (abnormalFields.length > 0) {
|
||||
const confirmed = await Taro.showModal({
|
||||
title: '数值异常提醒',
|
||||
content: `以下指标超出正常范围:${abnormalFields.join('、')}。确认提交?`,
|
||||
confirmText: '确认提交',
|
||||
cancelText: '返回修改',
|
||||
});
|
||||
if (!confirmed.confirm) return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await createDailyMonitoring({
|
||||
@@ -164,6 +244,13 @@ export default function DailyMonitoring() {
|
||||
|
||||
const isToday = recordDate === today;
|
||||
|
||||
// ── Abnormal state helpers for rendering ──
|
||||
const morningSysAbnormal = checkAbnormal(morningSystolic, 'systolic');
|
||||
const morningDiaAbnormal = checkAbnormal(morningDiastolic, 'diastolic');
|
||||
const eveningSysAbnormal = checkAbnormal(eveningSystolic, 'systolic');
|
||||
const eveningDiaAbnormal = checkAbnormal(eveningDiastolic, 'diastolic');
|
||||
const bloodSugarAbnormal = checkAbnormal(bloodSugar, 'bloodSugar');
|
||||
|
||||
return (
|
||||
<View className='dm-page'>
|
||||
{/* 页面标题 */}
|
||||
@@ -175,12 +262,9 @@ export default function DailyMonitoring() {
|
||||
<Text className='dm-hero-sub'>每日健康数据上报</Text>
|
||||
</View>
|
||||
|
||||
{/* 日期选择 */}
|
||||
{/* 日期选择 (standalone card) */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>1</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>记录日期</Text>
|
||||
{isToday && (
|
||||
<Text className='dm-card-badge'>今日</Text>
|
||||
@@ -199,176 +283,185 @@ export default function DailyMonitoring() {
|
||||
</Picker>
|
||||
</View>
|
||||
|
||||
{/* 晨起血压 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>2</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>晨起血压</Text>
|
||||
{/* ── Group 1: 晨间体征 (default open) ── */}
|
||||
<View className={`dm-group${collapsed.morning ? ' dm-group-collapsed' : ''}`}>
|
||||
<View className='dm-group-header' onClick={() => toggleSection('morning')}>
|
||||
<Text className='dm-group-title'>晨间体征</Text>
|
||||
<Text className={`dm-group-arrow${collapsed.morning ? '' : ' dm-group-arrow-open'}`}>▸</Text>
|
||||
</View>
|
||||
<View className='dm-bp-group'>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box'
|
||||
placeholder='如 120'
|
||||
value={morningSystolic}
|
||||
onInput={(e) => setMorningSystolic(e.detail.value)}
|
||||
/>
|
||||
<View className='dm-group-body'>
|
||||
<View className='dm-bp-group'>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className={`dm-input-box${morningSysAbnormal.abnormal ? ' dm-input-abnormal' : ''}`}
|
||||
placeholder='如 120'
|
||||
value={morningSystolic}
|
||||
onInput={(e) => setMorningSystolic(e.detail.value)}
|
||||
/>
|
||||
{morningSysAbnormal.abnormal && (
|
||||
<Text className={`dm-field-warning${morningSysAbnormal.direction === 'low' ? ' dm-field-warning-low' : ''}`}>
|
||||
{morningSysAbnormal.direction === 'high' ? '偏高' : '偏低'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='dm-bp-divider'>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-bp-slash'>/</Text>
|
||||
<View className='dm-bp-line' />
|
||||
</View>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className={`dm-input-box${morningDiaAbnormal.abnormal ? ' dm-input-abnormal' : ''}`}
|
||||
placeholder='如 80'
|
||||
value={morningDiastolic}
|
||||
onInput={(e) => setMorningDiastolic(e.detail.value)}
|
||||
/>
|
||||
{morningDiaAbnormal.abnormal && (
|
||||
<Text className={`dm-field-warning${morningDiaAbnormal.direction === 'low' ? ' dm-field-warning-low' : ''}`}>
|
||||
{morningDiaAbnormal.direction === 'high' ? '偏高' : '偏低'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View className='dm-bp-divider'>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-bp-slash'>/</Text>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-field-unit'>mmHg</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ── Group 2: 晚间体征 (default open) ── */}
|
||||
<View className={`dm-group${collapsed.evening ? ' dm-group-collapsed' : ''}`}>
|
||||
<View className='dm-group-header' onClick={() => toggleSection('evening')}>
|
||||
<Text className='dm-group-title'>晚间体征</Text>
|
||||
<Text className={`dm-group-arrow${collapsed.evening ? '' : ' dm-group-arrow-open'}`}>▸</Text>
|
||||
</View>
|
||||
<View className='dm-group-body'>
|
||||
<View className='dm-bp-group'>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className={`dm-input-box${eveningSysAbnormal.abnormal ? ' dm-input-abnormal' : ''}`}
|
||||
placeholder='如 120'
|
||||
value={eveningSystolic}
|
||||
onInput={(e) => setEveningSystolic(e.detail.value)}
|
||||
/>
|
||||
{eveningSysAbnormal.abnormal && (
|
||||
<Text className={`dm-field-warning${eveningSysAbnormal.direction === 'low' ? ' dm-field-warning-low' : ''}`}>
|
||||
{eveningSysAbnormal.direction === 'high' ? '偏高' : '偏低'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View className='dm-bp-divider'>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-bp-slash'>/</Text>
|
||||
<View className='dm-bp-line' />
|
||||
</View>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className={`dm-input-box${eveningDiaAbnormal.abnormal ? ' dm-input-abnormal' : ''}`}
|
||||
placeholder='如 80'
|
||||
value={eveningDiastolic}
|
||||
onInput={(e) => setEveningDiastolic(e.detail.value)}
|
||||
/>
|
||||
{eveningDiaAbnormal.abnormal && (
|
||||
<Text className={`dm-field-warning${eveningDiaAbnormal.direction === 'low' ? ' dm-field-warning-low' : ''}`}>
|
||||
{eveningDiaAbnormal.direction === 'high' ? '偏高' : '偏低'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>舒张压</Text>
|
||||
<Text className='dm-field-unit'>mmHg</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* ── Group 3: 其他指标 (default collapsed) ── */}
|
||||
<View className={`dm-group${collapsed.other ? ' dm-group-collapsed' : ''}`}>
|
||||
<View className='dm-group-header' onClick={() => toggleSection('other')}>
|
||||
<Text className='dm-group-title'>其他指标</Text>
|
||||
<Text className={`dm-group-arrow${collapsed.other ? '' : ' dm-group-arrow-open'}`}>▸</Text>
|
||||
</View>
|
||||
<View className='dm-group-body'>
|
||||
{/* 体重 */}
|
||||
<View className='dm-inner-field'>
|
||||
<Text className='dm-field-label'>体重</Text>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 65.0'
|
||||
value={weight}
|
||||
onInput={(e) => setWeight(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>kg</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 血糖 */}
|
||||
<View className='dm-inner-field'>
|
||||
<Text className='dm-field-label'>血糖</Text>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className={`dm-input-box dm-input-flex${bloodSugarAbnormal.abnormal ? ' dm-input-abnormal' : ''}`}
|
||||
placeholder='如 5.6'
|
||||
value={bloodSugar}
|
||||
onInput={(e) => setBloodSugar(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>mmol/L</Text>
|
||||
</View>
|
||||
{bloodSugarAbnormal.abnormal && (
|
||||
<Text className={`dm-field-warning${bloodSugarAbnormal.direction === 'low' ? ' dm-field-warning-low' : ''}`}>
|
||||
{bloodSugarAbnormal.direction === 'high' ? '偏高' : '偏低'}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 饮水量 */}
|
||||
<View className='dm-inner-field'>
|
||||
<Text className='dm-field-label'>饮水量</Text>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 2000'
|
||||
value={fluidIntake}
|
||||
onInput={(e) => setFluidIntake(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>ml</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 尿量 */}
|
||||
<View className='dm-inner-field'>
|
||||
<Text className='dm-field-label'>尿量</Text>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 1500'
|
||||
value={urineOutput}
|
||||
onInput={(e) => setUrineOutput(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>ml</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 备注 */}
|
||||
<View className='dm-inner-field'>
|
||||
<Text className='dm-field-label'>备注</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box'
|
||||
placeholder='如 80'
|
||||
value={morningDiastolic}
|
||||
onInput={(e) => setMorningDiastolic(e.detail.value)}
|
||||
className='dm-input-box dm-input-full'
|
||||
placeholder='如:头晕、乏力等(可选)'
|
||||
value={notes}
|
||||
onInput={(e) => setNotes(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='dm-field-unit'>mmHg</Text>
|
||||
</View>
|
||||
|
||||
{/* 晚间血压 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>3</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>晚间血压</Text>
|
||||
</View>
|
||||
<View className='dm-bp-group'>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>收缩压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box'
|
||||
placeholder='如 120'
|
||||
value={eveningSystolic}
|
||||
onInput={(e) => setEveningSystolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
<View className='dm-bp-divider'>
|
||||
<View className='dm-bp-line' />
|
||||
<Text className='dm-bp-slash'>/</Text>
|
||||
<View className='dm-bp-line' />
|
||||
</View>
|
||||
<View className='dm-bp-field'>
|
||||
<Text className='dm-field-label'>舒张压</Text>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box'
|
||||
placeholder='如 80'
|
||||
value={eveningDiastolic}
|
||||
onInput={(e) => setEveningDiastolic(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Text className='dm-field-unit'>mmHg</Text>
|
||||
</View>
|
||||
|
||||
{/* 体重 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>4</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>体重</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 65.0'
|
||||
value={weight}
|
||||
onInput={(e) => setWeight(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>kg</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 血糖 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>5</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>血糖</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 5.6'
|
||||
value={bloodSugar}
|
||||
onInput={(e) => setBloodSugar(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>mmol/L</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 饮水量 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>6</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>饮水量</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 2000'
|
||||
value={fluidIntake}
|
||||
onInput={(e) => setFluidIntake(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>ml</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 尿量 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>7</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>尿量</Text>
|
||||
</View>
|
||||
<View className='dm-single-row'>
|
||||
<Input
|
||||
type='digit'
|
||||
className='dm-input-box dm-input-flex'
|
||||
placeholder='如 1500'
|
||||
value={urineOutput}
|
||||
onInput={(e) => setUrineOutput(e.detail.value)}
|
||||
/>
|
||||
<Text className='dm-unit-inline'>ml</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 备注 */}
|
||||
<View className='dm-card'>
|
||||
<View className='dm-card-header'>
|
||||
<View className='dm-card-serial'>
|
||||
<Text className='dm-card-serial-text'>8</Text>
|
||||
</View>
|
||||
<Text className='dm-card-title'>备注</Text>
|
||||
</View>
|
||||
<Input
|
||||
className='dm-input-box dm-input-full'
|
||||
placeholder='如:头晕、乏力等(可选)'
|
||||
value={notes}
|
||||
onInput={(e) => setNotes(e.detail.value)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 提交 */}
|
||||
|
||||
@@ -88,57 +88,6 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── 打卡卡片 ─── */
|
||||
.checkin-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px 28px;
|
||||
margin: 0 24px 24px;
|
||||
box-shadow: $shadow-sm;
|
||||
}
|
||||
|
||||
.checkin-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.checkin-done {
|
||||
font-size: 28px;
|
||||
color: $acc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.checkin-streak {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
.checkin-pending {
|
||||
font-size: 28px;
|
||||
color: $tx2;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkin-go {
|
||||
background: $pri;
|
||||
border-radius: $r-sm;
|
||||
padding: 12px 28px;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-go-text {
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ─── 通用 section ─── */
|
||||
.health-section {
|
||||
margin: 0 24px 28px;
|
||||
@@ -206,6 +155,23 @@
|
||||
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; }
|
||||
}
|
||||
|
||||
/* ─── 趋势入口 ─── */
|
||||
.trend-row {
|
||||
background: $card;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import Taro, { useDidShow } from '@tarojs/taro';
|
||||
import { useHealthStore } from '../../stores/health';
|
||||
import { listDailyMonitoring, DailyMonitoring } from '../../services/health';
|
||||
@@ -16,6 +16,27 @@ function getStatusTag(status?: string) {
|
||||
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));
|
||||
}
|
||||
|
||||
export default function Health() {
|
||||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||||
const { currentPatient } = useAuthStore();
|
||||
@@ -63,10 +84,10 @@ export default function Health() {
|
||||
|
||||
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 },
|
||||
{ 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 },
|
||||
{ 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 },
|
||||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '--', unit: 'kg', indicator: 'weight', status: summary.weight?.status, ref: summary.weight?.reference_range },
|
||||
{ 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 = [
|
||||
@@ -102,7 +123,7 @@ export default function Health() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 快捷操作 */}
|
||||
{/* 快捷操作 + 打卡状态紧凑合并 */}
|
||||
<View className='health-actions-row'>
|
||||
{quickActions.map((a) => (
|
||||
<View className='action-item' key={a.label} onClick={a.action}>
|
||||
@@ -112,30 +133,22 @@ export default function Health() {
|
||||
<Text className='action-label'>{a.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 打卡状态 */}
|
||||
{checkinStatus && (
|
||||
<View className='checkin-card'>
|
||||
<View className='checkin-info'>
|
||||
{checkinStatus.checked_in_today ? (
|
||||
<>
|
||||
<Text className='checkin-done'>今日已打卡</Text>
|
||||
{checkinStatus.consecutive_days > 0 && (
|
||||
<Text className='checkin-streak'>连续 {checkinStatus.consecutive_days} 天</Text>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Text className='checkin-pending'>今日未打卡</Text>
|
||||
)}
|
||||
</View>
|
||||
{!checkinStatus.checked_in_today && (
|
||||
<View className='checkin-go' onClick={goToMall}>
|
||||
<Text className='checkin-go-text'>去打卡</Text>
|
||||
{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>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<Text className='action-label'>
|
||||
{checkinStatus.checked_in_today
|
||||
? (checkinStatus.consecutive_days > 0 ? `已打卡${checkinStatus.consecutive_days}天` : '已打卡')
|
||||
: '去打卡'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 今日体征概览 */}
|
||||
<View className='health-section'>
|
||||
@@ -146,6 +159,8 @@ export default function Health() {
|
||||
<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>
|
||||
@@ -154,6 +169,12 @@ export default function Health() {
|
||||
<Text className='vital-unit'>{item.unit}</Text>
|
||||
{tag && <Text className={`vital-tag ${tag.cls}`}>{tag.label}</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>
|
||||
);
|
||||
@@ -162,20 +183,20 @@ export default function Health() {
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 趋势快捷入口 */}
|
||||
{/* 趋势快捷入口 — 水平滚动卡片 */}
|
||||
<View className='health-section'>
|
||||
<Text className='section-title'>健康趋势</Text>
|
||||
<View className='trend-row'>
|
||||
<ScrollView className='trend-scroll' scrollX>
|
||||
{trendLinks.map((t) => (
|
||||
<View className='trend-item' key={t.label} onClick={() => goToTrend(t.indicator)}>
|
||||
<View className='trend-icon'>
|
||||
<Text className='trend-char'>{t.char}</Text>
|
||||
<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-label'>{t.label}</Text>
|
||||
<Text className='trend-arrow'>›</Text>
|
||||
<Text className='trend-card-label'>{t.label}</Text>
|
||||
<Text className='trend-card-arrow'>查看 ›</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* 最近监测记录 */}
|
||||
|
||||
@@ -261,3 +261,69 @@
|
||||
color: $tx3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── 健康空状态 ─── */
|
||||
.health-empty {
|
||||
background: $bg;
|
||||
border-radius: $r-sm;
|
||||
padding: 40px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.health-empty-text {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
color: $tx2;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.health-empty-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 24px 0 0;
|
||||
}
|
||||
|
||||
.health-empty-btn {
|
||||
background: $pri;
|
||||
border-radius: $r;
|
||||
padding: 16px 40px;
|
||||
}
|
||||
|
||||
.health-empty-btn-text {
|
||||
color: #fff;
|
||||
font-size: 26px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── 健康资讯 ─── */
|
||||
.articles-section {
|
||||
margin: 0 24px 24px;
|
||||
}
|
||||
|
||||
.article-card {
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.article-card-title {
|
||||
font-size: 28px;
|
||||
color: $tx;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-card-meta {
|
||||
font-size: 22px;
|
||||
color: $tx3;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import Loading from '../../components/Loading';
|
||||
import { trackPageView } from '@/services/analytics';
|
||||
import * as appointmentApi from '@/services/appointment';
|
||||
import * as followupApi from '@/services/followup';
|
||||
import * as articleApi from '../../services/article';
|
||||
import './index.scss';
|
||||
|
||||
interface UpcomingItem {
|
||||
@@ -24,14 +25,25 @@ export default function Index() {
|
||||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||||
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
|
||||
const [upcomingLoading, setUpcomingLoading] = useState(false);
|
||||
const [articles, setArticles] = useState<articleApi.Article[]>([]);
|
||||
|
||||
useDidShow(() => {
|
||||
restoreAuth();
|
||||
refreshToday();
|
||||
loadUpcoming();
|
||||
loadArticles();
|
||||
trackPageView('home');
|
||||
});
|
||||
|
||||
const loadArticles = async () => {
|
||||
try {
|
||||
const res = await articleApi.listArticles({ page: 1, page_size: 2 });
|
||||
setArticles(res.data || []);
|
||||
} catch {
|
||||
// 文章接口可能不可用
|
||||
}
|
||||
};
|
||||
|
||||
const loadUpcoming = async () => {
|
||||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||||
if (!patientId) return;
|
||||
@@ -81,11 +93,11 @@ export default function Index() {
|
||||
const displayName = user?.display_name || currentPatient?.name || '访客';
|
||||
|
||||
const quickServices = [
|
||||
{ label: '预约挂号', path: '/pages/appointment/create/index' },
|
||||
{ label: '健康录入', path: '/pages/health/input/index' },
|
||||
{ label: '健康趋势', path: '/pages/health/trend/index' },
|
||||
{ label: '资讯文章', path: '/pages/article/index' },
|
||||
{ label: 'AI 报告', path: '/pages/ai-report/list/index' },
|
||||
{ label: '预约挂号', char: '约', path: '/pages/appointment/create/index' },
|
||||
{ label: '健康录入', char: '录', path: '/pages/health/input/index' },
|
||||
{ label: '健康趋势', char: '势', path: '/pages/health/trend/index' },
|
||||
{ label: '资讯文章', char: '文', path: '/pages/article/index' },
|
||||
{ label: 'AI 报告', char: 'AI', path: '/pages/ai-report/list/index' },
|
||||
];
|
||||
|
||||
const handleServiceClick = (path: string) => {
|
||||
@@ -121,6 +133,15 @@ export default function Index() {
|
||||
<Text className='section-title'>今日健康</Text>
|
||||
{loading && !todaySummary ? (
|
||||
<Loading />
|
||||
) : !todaySummary || (!todaySummary.blood_pressure && !todaySummary.heart_rate && !todaySummary.blood_sugar && !todaySummary.weight) ? (
|
||||
<View className='health-empty'>
|
||||
<Text className='health-empty-text'>今天还没录入数据</Text>
|
||||
<View className='health-empty-action'>
|
||||
<View className='health-empty-btn' onClick={() => Taro.navigateTo({ url: '/pages/health/input/index' })}>
|
||||
<Text className='health-empty-btn-text'>点击开始记录</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='health-grid'>
|
||||
{healthItems.map((item) => {
|
||||
@@ -147,7 +168,7 @@ export default function Index() {
|
||||
{quickServices.map((svc) => (
|
||||
<View className='service-btn' key={svc.label} onClick={() => handleServiceClick(svc.path)}>
|
||||
<View className='service-icon-wrap'>
|
||||
<Text className='service-icon-text'>{svc.label[0]}</Text>
|
||||
<Text className='service-icon-text'>{svc.char}</Text>
|
||||
</View>
|
||||
<Text className='service-label'>{svc.label}</Text>
|
||||
</View>
|
||||
@@ -190,6 +211,25 @@ export default function Index() {
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 健康资讯 */}
|
||||
{articles.length > 0 && (
|
||||
<View className='articles-section'>
|
||||
<Text className='section-title'>健康资讯</Text>
|
||||
{articles.map((article) => (
|
||||
<View
|
||||
className='article-card'
|
||||
key={article.id}
|
||||
onClick={() => Taro.navigateTo({ url: `/pages/article/detail/index?id=${article.id}` })}
|
||||
>
|
||||
<Text className='article-card-title'>{article.title}</Text>
|
||||
<Text className='article-card-meta'>
|
||||
{article.category_name || '健康'}{article.published_at ? ` · ${article.published_at.slice(0, 10)}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user