feat(health): Phase 4 跨模块集成与架构优化 — 通知/标签/待办/数据录入
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

后端:
- 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
This commit is contained in:
iven
2026-04-25 20:10:50 +08:00
parent 5b520a168c
commit d2baacae7e
14 changed files with 667 additions and 222 deletions

View File

@@ -127,6 +127,43 @@
margin: 0 24px;
}
.upcoming-list {
background: $card;
border-radius: $r;
overflow: hidden;
}
.upcoming-item {
display: flex;
align-items: center;
padding: 24px 28px;
border-bottom: 1px solid $bd;
&:last-child { border-bottom: none; }
}
.upcoming-item-main {
flex: 1;
}
.upcoming-item-title {
font-size: 28px;
color: $tx;
display: block;
margin-bottom: 6px;
}
.upcoming-item-sub {
font-size: 22px;
color: $tx3;
display: block;
}
.upcoming-item-arrow {
font-size: 36px;
color: $tx3;
padding-left: 12px;
}
.empty-hint {
background: $card;
border-radius: $r;

View File

@@ -1,22 +1,75 @@
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 || '访客';
@@ -97,7 +150,33 @@ export default function Index() {
<View className='upcoming'>
<Text className='section-title'></Text>
<EmptyState icon='📋' text='暂无待办事项' hint='预约挂号后将在此显示' />
{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>
);

View File

@@ -1,5 +1,17 @@
// 微信订阅消息模板 ID — 需在微信公众平台注册后填入
// 注册路径:公众平台 → 功能 → 订阅消息 → 添加模板
// TODO: 上线前必须配置
export const TEMPLATE_IDS = {
APPOINTMENT_REMINDER: '',
FOLLOWUP_REMINDER: '',
REPORT_NOTIFICATION: '',
};
} as const;
/** 检查模板 ID 是否已配置,未配置时返回 false 并打印警告 */
export function isTemplateConfigured(key: keyof typeof TEMPLATE_IDS): boolean {
if (!TEMPLATE_IDS[key]) {
console.warn(`[wechat-templates] 模板 ${key} 未配置,请在微信公众平台注册并填入 ID`);
return false;
}
return true;
}