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
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user