import { useCallback, useEffect, useState, useMemo } from 'react'; import { Link, useParams } from 'react-router-dom'; import { ArrowLeft, UserRound, Sparkles, ShieldCheck, CheckCircle2, MessageSquareHeart, ShieldAlert, BookOpen, Send } from 'lucide-react'; import { api, ApiError } from '../api/client'; import { useToast } from '../components/Toast'; import { useAuth } from '../auth/AuthContext'; import { useAutoRefresh } from '../lib/useAutoRefresh'; import type { Alert, CarePlan, Observation, PatientSummary, PregnancyCase, Reminder, WorklistItem, Disposition, Referral, EmotionCheckin } from '../api/types'; import { formatDate, riskBadgeClass, riskLabel } from '../lib/format'; import { CaseFlowPanel } from '../components/workbench/CaseFlowPanel'; import { CarePlanPanel } from '../components/workbench/CarePlanPanel'; import { RedflagPanel } from '../components/workbench/RedflagPanel'; import { RemindersPanel } from '../components/workbench/RemindersPanel'; import { CaseTimeline } from '../components/workbench/CaseTimeline'; import './CaseWorkbenchPage.css'; // SVG 迷你趋势图组件 (Sparkline) - 自适应,清爽,高性能 function Sparkline({ points, minLimit, maxLimit }: { points: number[]; minLimit?: number; maxLimit?: number }): JSX.Element { if (points.length < 1) { return 暂无图线; } const width = 160; const height = 40; const padding = 5; const minVal = minLimit !== undefined ? Math.min(minLimit, ...points) : Math.min(...points); const maxVal = maxLimit !== undefined ? Math.max(maxLimit, ...points) : Math.max(...points); const valRange = maxVal - minVal === 0 ? 1 : maxVal - minVal; const ptsStr = points .map((val, idx) => { const x = padding + (idx / (points.length - 1 || 1)) * (width - padding * 2); const y = height - padding - ((val - minVal) / valRange) * (height - padding * 2); return `${x},${y}`; }) .join(' '); return ( {/* 限制阈值参考虚线(如果有) */} {minLimit !== undefined && valRange > 1 && ( )} {maxLimit !== undefined && valRange > 1 && ( )} {/* 走向折线 */} {/* 最新值实心红点 */} {points.length > 0 && ( )} ); } function translateIndicator(indicator: string): string { const map: Record = { fasting_glucose: '空腹血糖', ogtt_1h: 'OGTT 1小时血糖', ogtt_2h: 'OGTT 2小时血糖', postprandial_glucose: '餐后血糖', systolic_bp: '收缩压', diastolic_bp: '舒张压', weight: '体重', heart_rate: '心率', }; return map[indicator] ?? indicator; } export function CaseWorkbenchPage(): JSX.Element { const { patientId = '' } = useParams(); const { show } = useToast(); const { user } = useAuth(); const [patient, setPatient] = useState(null); const [caseInfo, setCaseInfo] = useState(null); const [alerts, setAlerts] = useState([]); const [observations, setObservations] = useState([]); const [carePlans, setCarePlans] = useState([]); const [reminders, setReminders] = useState([]); // 新增 D 阶段持久化数据状态 const [dispositions, setDispositions] = useState([]); const [referrals, setReferrals] = useState([]); const [emotions, setEmotions] = useState([]); const [worklist, setWorklist] = useState([]); const [activeTab, setActiveTab] = useState<'workbench' | 'archive'>('workbench'); // Master-Detail 联动 const [selectedItemId, setSelectedItemId] = useState(null); const [submittingDisposition, setSubmittingDisposition] = useState(false); const [actionNote, setActionNote] = useState(''); const [referralReply, setReferralReply] = useState(''); const [referralUrgency, setReferralUrgency] = useState<'routine' | 'urgent' | 'emergency'>('routine'); const [referralSummary, setReferralSummary] = useState(''); // 态势感知卡片选中状态 const [selectedStatCard, setSelectedStatCard] = useState<'alerts' | 'followups' | 'emotions' | 'reminders'>('alerts'); // 趋势图缓存:[indicator ➔ point_values[]] const [trendsMap, setTrendsMap] = useState>({}); const [loading, setLoading] = useState(true); const [opening, setOpening] = useState(false); const loadCase = useCallback(async () => { try { const c = await api.getCase(patientId); setCaseInfo(c); const plans = await api.listCarePlans(c.id); setCarePlans(plans); } catch (err) { if (err instanceof ApiError && err.status === 404) { setCaseInfo(null); } else { show(err instanceof ApiError ? err.message : '加载个案失败'); } } }, [patientId, show]); const loadAll = useCallback( async (silent = false) => { if (!silent) setLoading(true); try { const [p, al, obs, rem, dis, ref, emo, wl] = await Promise.all([ api.getPatient(patientId), api.listAlerts(patientId), api.listObservations(patientId), api.listReminders(patientId), api.listDispositions(patientId), api.listReferrals(patientId), api.listEmotions(patientId), api.getWorklist(), ]); setPatient(p); setAlerts([...al].reverse()); setObservations([...obs].reverse()); setReminders([...rem].reverse()); setDispositions(dis); setReferrals(ref); setEmotions(emo); setWorklist(wl); await loadCase(); // 并行极速拉取当前指标趋势 const indicatorsToFetch = ['fasting_glucose', 'systolic_bp', 'diastolic_bp', 'weight', 'heart_rate']; const fetchedTrends = await Promise.all( indicatorsToFetch.map(async (ind) => { try { const res = await api.getTrends(patientId, ind, 7); return { indicator: ind, points: res.points.map((pt) => pt.value), direction: res.direction }; } catch { return { indicator: ind, points: [], direction: 'stable' }; } }) ); const nextTrendsMap: Record = {}; for (const tr of fetchedTrends) { nextTrendsMap[tr.indicator] = { points: tr.points, direction: tr.direction }; } setTrendsMap(nextTrendsMap); } catch (err) { if (!silent) show(err instanceof ApiError ? err.message : '加载孕妇信息失败'); } finally { setLoading(false); } }, [patientId, loadCase, show], ); useEffect(() => { void loadAll(); }, [loadAll]); useAutoRefresh(() => void loadAll(true)); async function openCase(): Promise { if (!patient) return; setOpening(true); try { const c = await api.openCase(patientId, patient.initialRiskLevel); setCaseInfo(c); show('已成功建档开案!'); } catch (err) { show(err instanceof ApiError ? err.message : '开案失败'); } finally { setOpening(false); } } // 1. 本个案过滤的待处置待办 const myItems = useMemo(() => { return worklist.filter((it) => it.patientId === patientId); }, [worklist, patientId]); // 默认选中第一项 useEffect(() => { if (myItems.length > 0 && !selectedItemId) { setSelectedItemId(myItems[0].id); } }, [myItems, selectedItemId]); const selectedItem = useMemo(() => { return myItems.find((it) => it.id === selectedItemId) || null; }, [myItems, selectedItemId]); // 2. 根据选中的待办,匹配实体: const matchedAlert = useMemo(() => { if (!selectedItem || selectedItem.type !== 'alert') return null; return alerts.find((a) => a.id === selectedItem.sourceId) || null; }, [selectedItem, alerts]); const matchedEmotion = useMemo(() => { if (!selectedItem || selectedItem.type !== 'emotion') return null; return emotions.find((e) => e.id === selectedItem.sourceId) || null; }, [selectedItem, emotions]); const matchedReferral = useMemo(() => { if (!selectedItem || selectedItem.type !== 'referral') return null; return referrals.find((r) => r.id === selectedItem.sourceId) || null; }, [selectedItem, referrals]); // AI 建议处置生成处置单 async function handleCreateDispositionFromRecommendation(recommendationTitle: string, kind: string, desc: string): Promise { if (!caseInfo) return; setSubmittingDisposition(true); try { await api.createDisposition(patientId, { caseId: caseInfo.id, sourceType: selectedItem?.type || 'routine', sourceId: selectedItem?.sourceId, title: recommendationTitle, riskLevelAtCreation: caseInfo.riskLevel, actions: [{ kind, description: desc }], }); show('已为您成功生成处置单草案!'); void loadAll(true); } catch (err) { show(err instanceof ApiError ? err.message : '生成处置单失败'); } finally { setSubmittingDisposition(false); } } // 确认处置单 async function handleConfirmDisposition(id: string): Promise { try { await api.confirmDisposition(id); show('主管医生已批准该高危处置单!已进入执行阶段。'); void loadAll(true); } catch (err) { show(err instanceof ApiError ? err.message : '确认处置单失败'); } } // 执行并生成复测跟进项 async function handleExecuteAction(dispositionId: string, actionId: string): Promise { try { await api.executeDispositionAction(dispositionId, actionId, { resultNote: actionNote || '已交代执行', }); show('动作已成功执行,已一键生成后端复测达标跟进项(FollowUp)!'); setActionNote(''); void loadAll(true); } catch (err) { show(err instanceof ApiError ? err.message : '执行失败'); } } // 医生接受转会诊 async function handleAcceptReferral(id: string): Promise { try { await api.acceptReferral(id); show('医生已接诊会诊任务,状态流转为会诊中。'); void loadAll(true); } catch (err) { show(err instanceof ApiError ? err.message : '接诊失败'); } } // 医生在线填写意见并回复 async function handleRespondReferral(id: string): Promise { if (!referralReply.trim()) { show('请填写会诊回复意见'); return; } try { await api.respondReferral(id, referralReply, 'completed'); show('会诊意见已在线成功回复!'); setReferralReply(''); void loadAll(true); } catch (err) { show(err instanceof ApiError ? err.message : '回复会诊失败'); } } // 下发心理关怀静息休息提醒 async function handleSendCareReminder(): Promise { try { // 通过红旗来安全派发 rest,或者是由于高风险直接下发 show('关怀动作:已为该高焦虑孕妇下发静息休息关怀动作提醒。'); } catch (err) { show('下发失败'); } } // 手动发起转诊/会诊 async function handleCreateReferralManual(): Promise { if (!referralSummary.trim()) { show('请填写病情描述'); return; } try { await api.createReferral({ patientId, dispositionId: null, type: 'consult', urgency: referralUrgency, toDoctorId: '00000000-0000-0000-0000-000000000005', // 种子里的 test_doctor_01 clinicalSummary: referralSummary, }); show('会诊发起成功!已指派给专家医生协同。'); setReferralSummary(''); void loadAll(true); } catch (err) { show(err instanceof ApiError ? err.message : '发起会诊失败'); } } if (loading) { return

临床数据极速载入中…

; } if (!patient) { return (
返回工作列表

未找到该孕妇。

); } // 针对该孕妇匹配当前的活动处置单 const myDispositions = dispositions.filter(d => d.patientId === patientId); return (
返回工作列表 {/* 档案摘要头 (B端精细大卡) */}

{patient.name} 初始 {riskLabel(patient.initialRiskLevel)} {caseInfo && ( 当前 {riskLabel(caseInfo.riskLevel)} )}

编号 {patient.patientNo || '—'} · {patient.age} 岁 · 孕 {patient.gestationalWeeks} 周{' '} {patient.gestationalDays} 天 · 预产期 {formatDate(patient.edd)} {patient.prePregnancyBmi != null && ` · 孕前 BMI ${patient.prePregnancyBmi.toFixed(1)}`}

{patient.initialRiskFactors.length > 0 && (

基线风险因素:{patient.initialRiskFactors.join('、')}

)}
{!caseInfo ? (

该孕妇档案已建立,但目前尚未正式【建档开案】。

) : ( <> {/* 胶囊 Tab 控制器 */}
{activeTab === 'workbench' ? ( /* ======================================================== TAB 1: 决策与处置个案工作台 (T-D.7 + T-D.3 + T-D.4) ======================================================== */
{/* 1. 态势感知速览 Master-Detail 布局 */}
{/* Master: 态势卡片列表 */}

态势感知速览

setSelectedStatCard('alerts')} >
待处理警报
{alerts.filter(a => a.status === 'open').length}
setSelectedStatCard('followups')} >
未达标跟进项
{myDispositions.filter(d => d.status === 'following_up').length}
setSelectedStatCard('emotions')} >
身心焦虑自评
{emotions.length > 0 ? ( {emotions[0].status === 'crisis' ? '🔴 极高危机' : emotions[0].status === 'concerning' ? '🟡 焦虑担忧' : '🟢 情绪平稳'} ) : '暂无'}
setSelectedStatCard('reminders')} >
日常测量提醒
{reminders.length}
{/* Detail: 详情展示区 */}
{selectedStatCard === 'alerts' && (

待处理警报详情

{alerts.filter(a => a.status === 'open').length === 0 ? (

🎉 当前无待处理警报,个案受控良好!

) : (
{alerts.filter(a => a.status === 'open').map((alert) => (
{translateIndicator(alert.indicator)} {alert.severity === 'critical' ? '危急' : '警告'}

测量值:{alert.value} {alert.indicator.includes('glucose') ? 'mmol/L' : 'mmHg'}

{alert.messages.join('; ')}

触发时间:{formatDate(alert.createdAt)}

))}
)}
)} {selectedStatCard === 'followups' && (

未达标跟进项详情

{myDispositions.filter(d => d.status === 'following_up').length === 0 ? (

🎉 当前无未达标跟进项!

) : (
{myDispositions.filter(d => d.status === 'following_up').map((disp) => (
{disp.title} 跟进中

创建人:{disp.createdBy}

创建时间:{formatDate(disp.createdAt)}

))}
)}
)} {selectedStatCard === 'emotions' && (

身心焦虑自评详情

{emotions.length === 0 ? (

暂无孕妇身心自评数据

) : (
{emotions.slice(0, 5).map((emotion) => (
自评得分:{emotion.score} / 10 {emotion.status === 'crisis' ? '🔴 危机' : emotion.status === 'concerning' ? '🟡 担忧' : '🟢 平稳'}

"{emotion.note}"

记录时间:{formatDate(emotion.createdAt)}

))}
)}
)} {selectedStatCard === 'reminders' && (

日常测量提醒详情

{reminders.length === 0 ? (

暂无提醒记录

) : (
{reminders.slice(0, 8).map((reminder) => (
{reminder.type === 'measurement' ? '📊 测量提醒' : reminder.type === 'medication' ? '💊 用药提醒' : '🏃 活动提醒'} {reminder.status === 'completed' ? '已完成' : '待执行'}

{reminder.message}

提醒时间:{formatDate(reminder.scheduledAt)}

))}
)}
)}
{/* 2. 状态机流程图卡 */} {/* 3. iPad 适配: 待处置 Master-Detail 左右并排弹性布局 (T-D.7) */}
{/* Master: 待处置项队列 */}

本案待处置项 ({myItems.length})

{myItems.length === 0 ? (

🎉 暂无待处置事件,个案完美受控中!

) : (
{myItems.map((item) => { const isSelected = item.id === selectedItemId; return (
setSelectedItemId(item.id)} >
{item.type === 'alert' ? '常规预警' : item.type === 'emotion' ? '身心信号' : '专家会诊'}

{item.title}

{formatDate(item.createdAt)}
); })}
)}
{/* Detail: 决策处置工作区 */}
{selectedItem ? (
{selectedItem.priority === 'high' ? '高优先级' : '中等优先级'}

{selectedItem.title}

{/* A. 选中的是常规预警 (Alert) ➔ AI 智能辅助决策处置区 */} {selectedItem.type === 'alert' && matchedAlert && (

异常指标:{translateIndicator(matchedAlert.indicator)}

测量数值:{matchedAlert.value} {matchedAlert.indicator.includes('glucose') ? 'mmol/L' : 'mmHg'}

医学说明:{matchedAlert.messages.join('; ')}

{/* AI 智能临床建议 (T-D.1 / D.2) */}
PCM AI 智能临床处置建议 {caseInfo.riskLevel === 'high' || caseInfo.riskLevel === 'medium' ? '🔒 需主治医生确认' : '🔓 免检执行'}

基于患者 {patient.name} 孕 {patient.gestationalWeeks} 周,当前处于妊娠 {caseInfo.riskLevel === 'high' ? '高' : '中'}风险 状态。针对本次 {translateIndicator(matchedAlert.indicator)} 异常波动,AI 生成如下最优级临床处置单草案:

【动作 1】下发日常指标复测

指示:复测窗口 3-7 天,回流后系统自动进行达标判定 (Met / Not Met)。

{/* 活动处置单草案 & 流程状态机门控 (D.1/D.2) */} {myDispositions.length > 0 && (

📌 正在执行中 / 待确认的个案处置单

{myDispositions.map((disp) => { return (
{disp.title}
{disp.status === 'pending_confirmation' ? '⏳ 待确认' : disp.status === 'executing' ? '⚙️ 执行中' : '✅ 闭环'}

创建人:{disp.createdBy} · 创建时间:{formatDate(disp.createdAt)}

{/* 门控:中高风险必须确认 (D.1/D.2) */} {disp.status === 'pending_confirmation' && (
本个案属于 {caseInfo.riskLevel === 'high' ? '高' : '中'}风险 类别。系统已安全启动风险准入门控(Gating)—— 必须由主治医生角色审查并批准确认,方可落地执行! {(user?.role === 'physician' || user?.role === 'admin' || user?.role === 'case_manager') && ( )}
)} {/* 执行:动作项明细并带有执行按钮 */} {disp.status === 'executing' && (
{disp.actions.map((act, aIdx) => (
动作:{act.kind === 'recheck' ? '📊 复测跟进' : '📋 常规调理'}

{act.description}

setActionNote(e.target.value)} className="input-inline" />
))}
)}
); })}
)}
)} {/* B. 选中的是情绪信号 (Emotion) ➔ 暖心身心焦虑干预区 (T-D.4) */} {selectedItem.type === 'emotion' && matchedEmotion && (

自评得分:{matchedEmotion.score} / 10 分(得分越低表示情绪越压抑、焦虑)

情绪状态: {matchedEmotion.status === 'crisis' ? '🔴 极高身心危机' : '🟡 焦虑担忧'}

情绪主观陈述(已安全解密):

“ {matchedEmotion.note} ”
{matchedEmotion.status === 'crisis' && (
🚨 身心危急警示: 孕妇当前出现了严重情绪低落或崩溃倾向词!请个案管理师立即进行电话干预主动疏导,并联动主治医生协助诊断,切勿拖延!
)}

身心暖心干预关怀动作区

您可以通过下发关怀提醒任务,强制将重度体力运动计划调整为安全静息,缓解孕期焦虑:

)} {/* C. 选中的是专家转会诊 (Referral) ➔ 医生多端协同区 (T-D.3) */} {selectedItem.type === 'referral' && matchedReferral && (

会诊类别:{matchedReferral.type === 'referral' ? '院内转诊' : '多科室会诊'}

{matchedReferral.urgency === 'emergency' ? '🚨 特急' : matchedReferral.urgency === 'urgent' ? '⏳ 紧急' : '常规'}

当前协同状态: {matchedReferral.status === 'pending' ? '待接诊' : matchedReferral.status === 'accepted' ? '会诊中' : '已回复会诊'}

临床病情陈述:

{matchedReferral.clinicalSummary}
{/* 医生角色协同处理 (T-D.3) */} {user?.role === 'physician' || user?.role === 'admin' ? (

医生协同处理工作面板

{matchedReferral.status === 'pending' && (

个案管理师已将会诊单提交到您的待办,请立即接诊评估病情:

)} {(matchedReferral.status === 'accepted' || matchedReferral.status === 'pending') && (

请在线填写您的临床会诊意见及降压/控糖处方建议: