- 首页:移除渐变头部改为平铺背景,铃铛图标替代消息按钮 - 首页:体征数值与单位内联显示(同一行 baseline 对齐) - 健康页:标题改为"健康数据",整体样式贴近原型紧凑风格 - 我的页:移除渐变头部改为平铺卡片,积分/打卡分两个独立卡片 - 我的页:菜单使用 emoji 图标替代文字图标,间距更紧凑
238 lines
8.8 KiB
TypeScript
238 lines
8.8 KiB
TypeScript
import { View, Text } from '@tarojs/components';
|
||
import { useState } from 'react';
|
||
import Taro, { useDidShow } from '@tarojs/taro';
|
||
import { useAuthStore } from '../../stores/auth';
|
||
import { useHealthStore } from '../../stores/health';
|
||
import ProgressRing from '../../components/ProgressRing';
|
||
import Loading from '../../components/Loading';
|
||
import { trackPageView } from '@/services/analytics';
|
||
import * as appointmentApi from '@/services/appointment';
|
||
import * as followupApi from '@/services/followup';
|
||
import './index.scss';
|
||
|
||
interface UpcomingItem {
|
||
id: string;
|
||
title: string;
|
||
subtitle: string;
|
||
type: 'appointment' | 'followup';
|
||
icon: string;
|
||
}
|
||
|
||
export default function Index() {
|
||
const { user, currentPatient } = useAuthStore();
|
||
const { todaySummary, loading, refreshToday } = useHealthStore();
|
||
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
|
||
const [upcomingLoading, setUpcomingLoading] = useState(false);
|
||
|
||
useDidShow(() => {
|
||
refreshToday();
|
||
loadUpcoming();
|
||
trackPageView('home');
|
||
});
|
||
|
||
const loadUpcoming = async () => {
|
||
const patientId = useAuthStore.getState().currentPatient?.id;
|
||
if (!patientId) return;
|
||
setUpcomingLoading(true);
|
||
try {
|
||
const items: UpcomingItem[] = [];
|
||
const [apptRes, taskRes] = await Promise.allSettled([
|
||
appointmentApi.listAppointments(patientId, 1),
|
||
followupApi.listTasks(patientId, 'pending'),
|
||
]);
|
||
if (apptRes.status === 'fulfilled') {
|
||
for (const a of apptRes.value.data.slice(0, 2)) {
|
||
if (a.status === 'pending' || a.status === 'confirmed') {
|
||
items.push({
|
||
id: a.id,
|
||
title: `${a.appointment_date} ${a.start_time}`,
|
||
subtitle: `${a.doctor_name || '医护'} · ${a.department || '门诊'}`,
|
||
type: 'appointment',
|
||
icon: '约',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
if (taskRes.status === 'fulfilled') {
|
||
for (const t of taskRes.value.data.slice(0, 1)) {
|
||
items.push({
|
||
id: t.id,
|
||
title: t.follow_up_type,
|
||
subtitle: `${t.content_template?.slice(0, 20) || '随访任务'} · 截止 ${t.planned_date}`,
|
||
type: 'followup',
|
||
icon: '随',
|
||
});
|
||
}
|
||
}
|
||
setUpcomingItems(items.slice(0, 3));
|
||
} catch {
|
||
setUpcomingItems([]);
|
||
} finally {
|
||
setUpcomingLoading(false);
|
||
}
|
||
};
|
||
|
||
const hour = new Date().getHours();
|
||
const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好';
|
||
const displayName = user?.display_name || currentPatient?.name || '访客';
|
||
|
||
// 计算今日体征完成度(4 个指标:血压/心率/血糖/体重)
|
||
const summary = todaySummary || {};
|
||
const indicators = [
|
||
!!summary.blood_pressure,
|
||
!!summary.heart_rate,
|
||
!!summary.blood_sugar,
|
||
!!summary.weight,
|
||
];
|
||
const completedCount = indicators.filter(Boolean).length;
|
||
const progressPercent = Math.round((completedCount / 4) * 100);
|
||
|
||
const indicatorCapsules = [
|
||
{ label: '血压', done: !!summary.blood_pressure },
|
||
{ label: '心率', done: !!summary.heart_rate },
|
||
{ label: '血糖', done: !!summary.blood_sugar },
|
||
{ label: '体重', done: !!summary.weight },
|
||
];
|
||
|
||
const healthItems = [
|
||
{ label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'blood_pressure_systolic' },
|
||
{ label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' },
|
||
{ label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar_fasting' },
|
||
{ label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' },
|
||
];
|
||
|
||
const getStatusTag = (status?: string) => {
|
||
if (status === 'high' || status === 'low') return { label: status === 'high' ? '偏高' : '偏低', cls: 'tag-warn' };
|
||
if (status === 'normal') return { label: '正常', cls: 'tag-ok' };
|
||
return null;
|
||
};
|
||
|
||
return (
|
||
<View className='home-page'>
|
||
{/* 区域 1:问候 + 日期 + 消息入口 */}
|
||
<View className='greeting-section'>
|
||
<View className='greeting-left'>
|
||
<Text className='greeting-text'>{greeting},{displayName}</Text>
|
||
<Text className='greeting-date'>
|
||
{new Date().toLocaleDateString('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })}
|
||
</Text>
|
||
</View>
|
||
<View
|
||
className='greeting-bell'
|
||
onClick={() => Taro.switchTab({ url: '/pages/messages/index' })}
|
||
>
|
||
<Text className='greeting-bell-icon'>🔔</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 区域 2:今日体征完成度 */}
|
||
<View
|
||
className='checkin-card'
|
||
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
|
||
>
|
||
<View className='checkin-left'>
|
||
<ProgressRing percent={progressPercent} />
|
||
</View>
|
||
<View className='checkin-right'>
|
||
<Text className='checkin-title'>
|
||
{completedCount === 4 ? '今日体征已全部记录' : completedCount === 0 ? '今日尚未记录体征' : `今日已记录 ${completedCount}/4 项`}
|
||
</Text>
|
||
<View className='checkin-capsules'>
|
||
{indicatorCapsules.map((cap) => (
|
||
<Text
|
||
key={cap.label}
|
||
className={`capsule ${cap.done ? 'capsule-done' : 'capsule-pending'}`}
|
||
>
|
||
{cap.done ? '✓' : ''}{cap.label}
|
||
</Text>
|
||
))}
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 区域 3:今日体征 2x2 网格 */}
|
||
<View className='vitals-section'>
|
||
{loading && !todaySummary ? (
|
||
<Loading />
|
||
) : (
|
||
<View className='vitals-grid'>
|
||
{healthItems.map((item) => {
|
||
const tag = getStatusTag(item.status);
|
||
return (
|
||
<View
|
||
className='vital-card'
|
||
key={item.label}
|
||
onClick={() => Taro.navigateTo({ url: `/pages/pkg-health/trend/index?indicator=${item.indicator}` })}
|
||
>
|
||
<Text className='vital-label'>{item.label}</Text>
|
||
<View className='vital-value-row'>
|
||
<Text className='vital-value'>{item.value}</Text>
|
||
<Text className='vital-unit'>{item.unit}</Text>
|
||
</View>
|
||
<View className='vital-bottom'>
|
||
{tag && <Text className={`vital-tag ${tag.cls}`}>{tag.label}</Text>}
|
||
{!item.status && <Text className='vital-tag tag-empty'>未记录</Text>}
|
||
</View>
|
||
</View>
|
||
);
|
||
})}
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 区域 4:今日待办(≤3 条) */}
|
||
<View className='todo-section'>
|
||
<Text className='section-title'>今日待办</Text>
|
||
{upcomingLoading ? (
|
||
<Loading />
|
||
) : upcomingItems.length === 0 ? (
|
||
<View className='todo-empty'>
|
||
<Text className='todo-empty-text'>今天没有待办事项</Text>
|
||
</View>
|
||
) : (
|
||
<View className='todo-list'>
|
||
{upcomingItems.map((item) => (
|
||
<View
|
||
key={item.id}
|
||
className='todo-item'
|
||
onClick={() => {
|
||
if (item.type === 'appointment') {
|
||
Taro.navigateTo({ url: '/pages/appointment/index' });
|
||
} else {
|
||
Taro.navigateTo({ url: `/pages/followup/detail/index?id=${item.id}` });
|
||
}
|
||
}}
|
||
>
|
||
<View className='todo-icon-wrap'>
|
||
<Text className='todo-icon-char'>{item.icon}</Text>
|
||
</View>
|
||
<View className='todo-info'>
|
||
<Text className='todo-title'>{item.title}</Text>
|
||
<Text className='todo-sub'>{item.subtitle}</Text>
|
||
</View>
|
||
<Text className='todo-arrow'>›</Text>
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 区域 5:快捷操作 */}
|
||
<View className='action-section'>
|
||
<View
|
||
className='action-btn action-primary'
|
||
onClick={() => Taro.switchTab({ url: '/pages/health/index' })}
|
||
>
|
||
<Text className='action-btn-text'>记录体征</Text>
|
||
</View>
|
||
<View
|
||
className='action-btn action-outline'
|
||
onClick={() => Taro.navigateTo({ url: '/pages/appointment/create/index' })}
|
||
>
|
||
<Text className='action-btn-text'>预约挂号</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|