Files
hms/apps/miniprogram/src/pages/index/index.tsx
iven d2baacae7e
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
feat(health): Phase 4 跨模块集成与架构优化 — 通知/标签/待办/数据录入
后端:
- erp-message: 添加 appointment.created/confirmed/cancelled 事件监听,自动发送站内通知
- erp-health: 新增 GET /health/patient-tags 标签列表端点 + list_tags service
- wechat-templates: 添加 isTemplateConfigured 运行时校验

前端:
- 新增 Zustand useHealthStore 共享患者/医生名称缓存
- PatientTagManage: UUID 输入替换为 Checkbox 标签选择器
- VitalSignsTab: 添加体征数据录入 Modal (血压/心率/体重/血糖)
- LabReportsTab: 添加化验报告创建 Modal
- HealthRecordsTab: 添加健康记录创建 Modal
- patients API: 添加 TagItem 类型 + listTags 方法

小程序:
- 首页待办事项接入预约和随访 API,替换硬编码 EmptyState
2026-04-25 20:10:50 +08:00

184 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 EmptyState from '../../components/EmptyState';
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';
}
export default function Index() {
const { user, currentPatient, restore: restoreAuth } = useAuthStore();
const { todaySummary, loading, refreshToday } = useHealthStore();
const [upcomingItems, setUpcomingItems] = useState<UpcomingItem[]>([]);
const [upcomingLoading, setUpcomingLoading] = useState(false);
useDidShow(() => {
restoreAuth();
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, 3)) {
if (a.status === 'pending' || a.status === 'confirmed') {
items.push({
id: a.id,
title: `预约: ${a.appointment_date} ${a.start_time}`,
subtitle: `${a.doctor_name || '医护'} · ${a.status === 'pending' ? '待确认' : '已确认'}`,
type: 'appointment',
});
}
}
}
if (taskRes.status === 'fulfilled') {
for (const t of taskRes.value.data.slice(0, 2)) {
items.push({
id: t.id,
title: `随访: ${t.task_type}`,
subtitle: `${t.description?.slice(0, 30) || ''} · 截止 ${t.due_date}`,
type: 'followup',
});
}
}
setUpcomingItems(items);
} catch {
setUpcomingItems([]);
} finally {
setUpcomingLoading(false);
}
};
const hour = new Date().getHours();
const greeting = hour < 12 ? '早上好' : hour < 18 ? '下午好' : '晚上好';
const displayName = user?.display_name || currentPatient?.name || '访客';
const quickServices = [
{ label: '预约挂号', icon: '📅', path: '/pages/appointment/create/index' },
{ label: '健康录入', icon: '📊', path: '/pages/health/input/index' },
{ label: '健康趋势', icon: '📈', path: '/pages/health/trend/index' },
{ label: '资讯文章', icon: '📰', path: '/pages/article/index' },
];
const handleServiceClick = (path: string) => {
// tabBar 页面必须使用 switchTab其他页面用 navigateTo
const isTabBar = ['pages/index/index', 'pages/health/index', 'pages/appointment/index', 'pages/article/index', 'pages/profile/index']
.some((p) => path.includes(p));
if (isTabBar) {
Taro.switchTab({ url: path });
} else {
Taro.navigateTo({ url: path });
}
};
const healthItems = [
{ label: '血压', value: todaySummary?.blood_pressure ? `${todaySummary.blood_pressure.systolic}/${todaySummary.blood_pressure.diastolic}` : '--/--', unit: 'mmHg', status: todaySummary?.blood_pressure?.status },
{ label: '心率', value: todaySummary?.heart_rate ? `${todaySummary.heart_rate.value}` : '--', unit: 'bpm', status: todaySummary?.heart_rate?.status },
{ label: '血糖', value: todaySummary?.blood_sugar ? `${todaySummary.blood_sugar.value}` : '--', unit: 'mmol/L', status: todaySummary?.blood_sugar?.status },
{ label: '体重', value: todaySummary?.weight ? `${todaySummary.weight.value}` : '--', unit: 'kg', status: todaySummary?.weight?.status },
];
const getStatusLabel = (status?: string) => {
if (status === 'high') return '偏高 ▲';
if (status === 'low') return '偏低 ▼';
if (status === 'normal') return '正常';
return '';
};
return (
<View className='index-page'>
<View className='greeting-bar'>
<View className='greeting-text'>
<Text className='greeting-hello'>{greeting}</Text>
<Text className='greeting-name'>{displayName}</Text>
</View>
<Text className='greeting-date'>{new Date().toLocaleDateString('zh-CN')}</Text>
</View>
<View className='health-card'>
<Text className='section-title'></Text>
{loading && !todaySummary ? (
<Loading />
) : (
<View className='health-grid'>
{healthItems.map((item) => (
<View className={`health-item ${item.status === 'high' || item.status === 'low' ? 'health-item-warn' : item.status === 'normal' ? 'health-item-ok' : ''}`} key={item.label}>
<Text className='health-label'>{item.label}</Text>
<Text className='health-value'>{item.value}</Text>
<View className='health-item-bottom'>
<Text className='health-unit'>{item.unit}</Text>
{item.status && <Text className={`health-status ${item.status}`}>{getStatusLabel(item.status)}</Text>}
</View>
</View>
))}
</View>
)}
</View>
<View className='quick-services'>
<Text className='section-title'></Text>
<View className='service-grid'>
{quickServices.map((svc) => (
<View className='service-item' key={svc.label} onClick={() => handleServiceClick(svc.path)}>
<Text className='service-icon'>{svc.icon}</Text>
<Text className='service-label'>{svc.label}</Text>
</View>
))}
</View>
</View>
<View className='upcoming'>
<Text className='section-title'></Text>
{upcomingLoading ? (
<Loading />
) : upcomingItems.length === 0 ? (
<EmptyState icon='📋' text='暂无待办事项' hint='预约挂号后将在此显示' />
) : (
<View className='upcoming-list'>
{upcomingItems.map((item) => (
<View
key={item.id}
className='upcoming-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='upcoming-item-main'>
<Text className='upcoming-item-title'>{item.title}</Text>
<Text className='upcoming-item-sub'>{item.subtitle}</Text>
</View>
<Text className='upcoming-item-arrow'></Text>
</View>
))}
</View>
)}
</View>
</View>
);
}