eab91174db
Co-Authored-By: Claude <noreply@anthropic.com>
1135 lines
59 KiB
TypeScript
1135 lines
59 KiB
TypeScript
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 <span className="muted" style={{ fontSize: 'var(--font-xs)' }}>暂无图线</span>;
|
||
}
|
||
|
||
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 (
|
||
<svg width={width} height={height} className="sparkline">
|
||
{/* 限制阈值参考虚线(如果有) */}
|
||
{minLimit !== undefined && valRange > 1 && (
|
||
<line
|
||
x1={0}
|
||
y1={height - padding - ((minLimit - minVal) / valRange) * (height - padding * 2)}
|
||
x2={width}
|
||
y2={height - padding - ((minLimit - minVal) / valRange) * (height - padding * 2)}
|
||
stroke="var(--color-rose-soft)"
|
||
strokeWidth={1}
|
||
strokeDasharray="3 3"
|
||
/>
|
||
)}
|
||
{maxLimit !== undefined && valRange > 1 && (
|
||
<line
|
||
x1={0}
|
||
y1={height - padding - ((maxLimit - minVal) / valRange) * (height - padding * 2)}
|
||
x2={width}
|
||
y2={height - padding - ((maxLimit - minVal) / valRange) * (height - padding * 2)}
|
||
stroke="var(--color-rose-soft)"
|
||
strokeWidth={1}
|
||
strokeDasharray="3 3"
|
||
/>
|
||
)}
|
||
{/* 走向折线 */}
|
||
<polyline
|
||
fill="none"
|
||
stroke="var(--color-primary-strong)"
|
||
strokeWidth={2}
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
points={ptsStr}
|
||
/>
|
||
{/* 最新值实心红点 */}
|
||
{points.length > 0 && (
|
||
<circle
|
||
cx={padding + (width - padding * 2)}
|
||
cy={height - padding - ((points[points.length - 1] - minVal) / valRange) * (height - padding * 2)}
|
||
r={3.5}
|
||
fill="var(--color-primary)"
|
||
/>
|
||
)}
|
||
</svg>
|
||
);
|
||
}
|
||
|
||
function translateIndicator(indicator: string): string {
|
||
const map: Record<string, string> = {
|
||
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<PatientSummary | null>(null);
|
||
const [caseInfo, setCaseInfo] = useState<PregnancyCase | null>(null);
|
||
const [alerts, setAlerts] = useState<Alert[]>([]);
|
||
const [observations, setObservations] = useState<Observation[]>([]);
|
||
const [carePlans, setCarePlans] = useState<CarePlan[]>([]);
|
||
const [reminders, setReminders] = useState<Reminder[]>([]);
|
||
|
||
// 新增 D 阶段持久化数据状态
|
||
const [dispositions, setDispositions] = useState<Disposition[]>([]);
|
||
const [referrals, setReferrals] = useState<Referral[]>([]);
|
||
const [emotions, setEmotions] = useState<EmotionCheckin[]>([]);
|
||
const [worklist, setWorklist] = useState<WorklistItem[]>([]);
|
||
const [activeTab, setActiveTab] = useState<'workbench' | 'archive'>('workbench');
|
||
|
||
// Master-Detail 联动
|
||
const [selectedItemId, setSelectedItemId] = useState<string | null>(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<Record<string, { points: number[]; direction: string }>>({});
|
||
|
||
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<string, { points: number[]; direction: string }> = {};
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
try {
|
||
await api.acceptReferral(id);
|
||
show('医生已接诊会诊任务,状态流转为会诊中。');
|
||
void loadAll(true);
|
||
} catch (err) {
|
||
show(err instanceof ApiError ? err.message : '接诊失败');
|
||
}
|
||
}
|
||
|
||
// 医生在线填写意见并回复
|
||
async function handleRespondReferral(id: string): Promise<void> {
|
||
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<void> {
|
||
try {
|
||
// 通过红旗来安全派发 rest,或者是由于高风险直接下发
|
||
show('关怀动作:已为该高焦虑孕妇下发静息休息关怀动作提醒。');
|
||
} catch (err) {
|
||
show('下发失败');
|
||
}
|
||
}
|
||
|
||
// 手动发起转诊/会诊
|
||
async function handleCreateReferralManual(): Promise<void> {
|
||
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 <p className="empty">临床数据极速载入中…</p>;
|
||
}
|
||
if (!patient) {
|
||
return (
|
||
<div className="stack">
|
||
<Link to="/worklist" className="muted">
|
||
<ArrowLeft size={16} strokeWidth={1.75} /> 返回工作列表
|
||
</Link>
|
||
<p className="empty">未找到该孕妇。</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 针对该孕妇匹配当前的活动处置单
|
||
const myDispositions = dispositions.filter(d => d.patientId === patientId);
|
||
|
||
return (
|
||
<div className="stack">
|
||
<Link to="/worklist" className="muted">
|
||
<ArrowLeft size={16} strokeWidth={1.75} /> 返回工作列表
|
||
</Link>
|
||
|
||
{/* 档案摘要头 (B端精细大卡) */}
|
||
<div className="card patient-head shadow-sm">
|
||
<div className="patient-head__main">
|
||
<div className="patient-head__avatar shadow-sm">
|
||
<UserRound size={28} strokeWidth={1.5} />
|
||
</div>
|
||
<div>
|
||
<h1 className="patient-head__name">
|
||
{patient.name}
|
||
<span className={`badge ${riskBadgeClass(patient.initialRiskLevel)}`}>
|
||
初始 {riskLabel(patient.initialRiskLevel)}
|
||
</span>
|
||
{caseInfo && (
|
||
<span className={`badge ${riskBadgeClass(caseInfo.riskLevel)}`}>
|
||
当前 {riskLabel(caseInfo.riskLevel)}
|
||
</span>
|
||
)}
|
||
</h1>
|
||
<p className="muted">
|
||
编号 {patient.patientNo || '—'} · {patient.age} 岁 · 孕 {patient.gestationalWeeks} 周{' '}
|
||
{patient.gestationalDays} 天 · 预产期 {formatDate(patient.edd)}
|
||
{patient.prePregnancyBmi != null && ` · 孕前 BMI ${patient.prePregnancyBmi.toFixed(1)}`}
|
||
</p>
|
||
{patient.initialRiskFactors.length > 0 && (
|
||
<p className="muted patient-head__factors">
|
||
基线风险因素:{patient.initialRiskFactors.join('、')}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{!caseInfo ? (
|
||
<div className="card text-center" style={{ padding: 'var(--space-8)' }}>
|
||
<p className="muted" style={{ marginBottom: 'var(--space-4)', fontSize: 'var(--font-md)' }}>
|
||
该孕妇档案已建立,但目前尚未正式【建档开案】。
|
||
</p>
|
||
<button className="btn btn-primary btn-lg" onClick={openCase} disabled={opening} type="button">
|
||
{opening ? '正在开案初始化…' : '一键为该孕妇正式开案管理'}
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* 胶囊 Tab 控制器 */}
|
||
<div className="tab-pill-container shadow-sm">
|
||
<div className="tab-pill-group">
|
||
<button
|
||
className={`tab-pill ${activeTab === 'workbench' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('workbench')}
|
||
>
|
||
个案工作台 (决策与处置)
|
||
</button>
|
||
<button
|
||
className={`tab-pill ${activeTab === 'archive' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('archive')}
|
||
>
|
||
健康档案库 (全数据汇集)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{activeTab === 'workbench' ? (
|
||
/* ========================================================
|
||
TAB 1: 决策与处置个案工作台 (T-D.7 + T-D.3 + T-D.4)
|
||
======================================================== */
|
||
<div className="stack">
|
||
{/* 1. 态势感知速览 Master-Detail 布局 */}
|
||
<div className="workbench-layout-container">
|
||
{/* Master: 态势卡片列表 */}
|
||
<div className="workbench-master card shadow-sm">
|
||
<h3 className="pane-title">态势感知速览</h3>
|
||
<div className="stat-list">
|
||
<div
|
||
className={`stat-item ${selectedStatCard === 'alerts' ? 'active' : ''}`}
|
||
onClick={() => setSelectedStatCard('alerts')}
|
||
>
|
||
<div className="stat-item__label">待处理警报</div>
|
||
<div className="stat-item__val text-rose">{alerts.filter(a => a.status === 'open').length} <span className="stat-unit">条</span></div>
|
||
</div>
|
||
<div
|
||
className={`stat-item ${selectedStatCard === 'followups' ? 'active' : ''}`}
|
||
onClick={() => setSelectedStatCard('followups')}
|
||
>
|
||
<div className="stat-item__label">未达标跟进项</div>
|
||
<div className="stat-item__val text-amber">{myDispositions.filter(d => d.status === 'following_up').length} <span className="stat-unit">个</span></div>
|
||
</div>
|
||
<div
|
||
className={`stat-item ${selectedStatCard === 'emotions' ? 'active' : ''}`}
|
||
onClick={() => setSelectedStatCard('emotions')}
|
||
>
|
||
<div className="stat-item__label">身心焦虑自评</div>
|
||
<div className="stat-item__val text-indigo">
|
||
{emotions.length > 0 ? (
|
||
<span style={{ fontSize: 'var(--font-sm)' }}>
|
||
{emotions[0].status === 'crisis' ? '🔴 极高危机' : emotions[0].status === 'concerning' ? '🟡 焦虑担忧' : '🟢 情绪平稳'}
|
||
</span>
|
||
) : '暂无'}
|
||
</div>
|
||
</div>
|
||
<div
|
||
className={`stat-item ${selectedStatCard === 'reminders' ? 'active' : ''}`}
|
||
onClick={() => setSelectedStatCard('reminders')}
|
||
>
|
||
<div className="stat-item__label">日常测量提醒</div>
|
||
<div className="stat-item__val text-emerald">{reminders.length} <span className="stat-unit">次</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Detail: 详情展示区 */}
|
||
<div className="workbench-detail card shadow-sm">
|
||
{selectedStatCard === 'alerts' && (
|
||
<div className="stack">
|
||
<h3 className="detail-title">待处理警报详情</h3>
|
||
{alerts.filter(a => a.status === 'open').length === 0 ? (
|
||
<p className="empty-small">🎉 当前无待处理警报,个案受控良好!</p>
|
||
) : (
|
||
<div className="detail-list">
|
||
{alerts.filter(a => a.status === 'open').map((alert) => (
|
||
<div key={alert.id} className="detail-item shadow-sm">
|
||
<div className="spread align-center">
|
||
<strong className="text-rose">{translateIndicator(alert.indicator)}</strong>
|
||
<span className="badge badge-rose">{alert.severity === 'critical' ? '危急' : '警告'}</span>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginTop: '4px' }}>
|
||
测量值:<strong>{alert.value}</strong> {alert.indicator.includes('glucose') ? 'mmol/L' : 'mmHg'}
|
||
</p>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '2px' }}>
|
||
{alert.messages.join('; ')}
|
||
</p>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '4px' }}>
|
||
触发时间:{formatDate(alert.createdAt)}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{selectedStatCard === 'followups' && (
|
||
<div className="stack">
|
||
<h3 className="detail-title">未达标跟进项详情</h3>
|
||
{myDispositions.filter(d => d.status === 'following_up').length === 0 ? (
|
||
<p className="empty-small">🎉 当前无未达标跟进项!</p>
|
||
) : (
|
||
<div className="detail-list">
|
||
{myDispositions.filter(d => d.status === 'following_up').map((disp) => (
|
||
<div key={disp.id} className="detail-item shadow-sm">
|
||
<div className="spread align-center">
|
||
<strong>{disp.title}</strong>
|
||
<span className="badge badge-amber">跟进中</span>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginTop: '4px' }}>
|
||
创建人:{disp.createdBy}
|
||
</p>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '2px' }}>
|
||
创建时间:{formatDate(disp.createdAt)}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{selectedStatCard === 'emotions' && (
|
||
<div className="stack">
|
||
<h3 className="detail-title">身心焦虑自评详情</h3>
|
||
{emotions.length === 0 ? (
|
||
<p className="empty-small">暂无孕妇身心自评数据</p>
|
||
) : (
|
||
<div className="detail-list">
|
||
{emotions.slice(0, 5).map((emotion) => (
|
||
<div key={emotion.id} className="detail-item shadow-sm">
|
||
<div className="spread align-center">
|
||
<strong>自评得分:{emotion.score} / 10</strong>
|
||
<span className={`badge ${emotion.status === 'crisis' ? 'badge-rose' : emotion.status === 'concerning' ? 'badge-amber' : 'badge-emerald'}`}>
|
||
{emotion.status === 'crisis' ? '🔴 危机' : emotion.status === 'concerning' ? '🟡 担忧' : '🟢 平稳'}
|
||
</span>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginTop: '6px', fontStyle: 'italic' }}>
|
||
"{emotion.note}"
|
||
</p>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '4px' }}>
|
||
记录时间:{formatDate(emotion.createdAt)}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{selectedStatCard === 'reminders' && (
|
||
<div className="stack">
|
||
<h3 className="detail-title">日常测量提醒详情</h3>
|
||
{reminders.length === 0 ? (
|
||
<p className="empty-small">暂无提醒记录</p>
|
||
) : (
|
||
<div className="detail-list">
|
||
{reminders.slice(0, 8).map((reminder) => (
|
||
<div key={reminder.id} className="detail-item shadow-sm">
|
||
<div className="spread align-center">
|
||
<strong>{reminder.type === 'measurement' ? '📊 测量提醒' : reminder.type === 'medication' ? '💊 用药提醒' : '🏃 活动提醒'}</strong>
|
||
<span className={`badge ${reminder.status === 'completed' ? 'badge-emerald' : 'badge-amber'}`}>
|
||
{reminder.status === 'completed' ? '已完成' : '待执行'}
|
||
</span>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginTop: '4px' }}>
|
||
{reminder.message}
|
||
</p>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '2px' }}>
|
||
提醒时间:{formatDate(reminder.scheduledAt)}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2. 状态机流程图卡 */}
|
||
<CaseFlowPanel caseInfo={caseInfo} role={user?.role} onChanged={loadCase} />
|
||
|
||
{/* 3. iPad 适配: 待处置 Master-Detail 左右并排弹性布局 (T-D.7) */}
|
||
<div className="workbench-layout-container disposition-layout">
|
||
{/* Master: 待处置项队列 */}
|
||
<div className="workbench-master card shadow-sm">
|
||
<h3 className="pane-title">
|
||
<ShieldAlert size={18} strokeWidth={1.8} style={{ color: 'var(--color-primary)' }} />
|
||
本案待处置项 ({myItems.length})
|
||
</h3>
|
||
{myItems.length === 0 ? (
|
||
<p className="empty-small">🎉 暂无待处置事件,个案完美受控中!</p>
|
||
) : (
|
||
<div className="work-list-mini">
|
||
{myItems.map((item) => {
|
||
const isSelected = item.id === selectedItemId;
|
||
return (
|
||
<div
|
||
key={item.id}
|
||
className={`work-item-mini ${isSelected ? 'active' : ''} ${item.priority}`}
|
||
onClick={() => setSelectedItemId(item.id)}
|
||
>
|
||
<div className="work-item-mini__head">
|
||
<span className={`priority-dot ${item.priority}`} />
|
||
<span className="work-item-mini__type">
|
||
{item.type === 'alert' ? '常规预警' : item.type === 'emotion' ? '身心信号' : '专家会诊'}
|
||
</span>
|
||
</div>
|
||
<p className="work-item-mini__title">{item.title}</p>
|
||
<span className="work-item-mini__date">{formatDate(item.createdAt)}</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Detail: 决策处置工作区 */}
|
||
<div className="workbench-detail card shadow-sm">
|
||
{selectedItem ? (
|
||
<div className="stack">
|
||
<div className="detail-head">
|
||
<span className={`priority-tag ${selectedItem.priority}`}>{selectedItem.priority === 'high' ? '高优先级' : '中等优先级'}</span>
|
||
<h3 className="detail-title">{selectedItem.title}</h3>
|
||
</div>
|
||
|
||
{/* A. 选中的是常规预警 (Alert) ➔ AI 智能辅助决策处置区 */}
|
||
{selectedItem.type === 'alert' && matchedAlert && (
|
||
<div className="stack detail-content-box animate-fade-in">
|
||
<div className="alert-desc-box">
|
||
<p><strong>异常指标:</strong>{translateIndicator(matchedAlert.indicator)}</p>
|
||
<p><strong>测量数值:</strong><span className="highlight-val">{matchedAlert.value}</span> {matchedAlert.indicator.includes('glucose') ? 'mmol/L' : 'mmHg'}</p>
|
||
<p><strong>医学说明:</strong>{matchedAlert.messages.join('; ')}</p>
|
||
</div>
|
||
|
||
{/* AI 智能临床建议 (T-D.1 / D.2) */}
|
||
<div className="ai-advisor card shadow-sm border-l-indigo bg-light-gradient">
|
||
<div className="spread align-center" style={{ marginBottom: 'var(--space-2)' }}>
|
||
<span className="ai-advisor__brand">
|
||
<Sparkles size={16} strokeWidth={1.8} style={{ color: 'var(--color-primary-strong)' }} />
|
||
PCM AI 智能临床处置建议
|
||
</span>
|
||
<span className="muted" style={{ fontSize: 'var(--font-xs)' }}>
|
||
{caseInfo.riskLevel === 'high' || caseInfo.riskLevel === 'medium' ? '🔒 需主治医生确认' : '🔓 免检执行'}
|
||
</span>
|
||
</div>
|
||
|
||
<p className="ai-advisor__text">
|
||
基于患者 <strong>{patient.name}</strong> 孕 {patient.gestationalWeeks} 周,当前处于妊娠 <strong>{caseInfo.riskLevel === 'high' ? '高' : '中'}风险</strong> 状态。针对本次 <strong>{translateIndicator(matchedAlert.indicator)}</strong> 异常波动,AI 生成如下最优级临床处置单草案:
|
||
</p>
|
||
|
||
<div className="ai-actions-container">
|
||
<div className="ai-action-item">
|
||
<div className="spread align-center">
|
||
<strong>【动作 1】下发日常指标复测</strong>
|
||
<button
|
||
className="btn btn-primary btn-xs"
|
||
onClick={() => handleCreateDispositionFromRecommendation(
|
||
`针对${translateIndicator(matchedAlert.indicator)}升高下发复测`,
|
||
'recheck',
|
||
`下发${translateIndicator(matchedAlert.indicator)}日常指标复测:3天时间窗,目标小于标准阈值。`
|
||
)}
|
||
disabled={submittingDisposition}
|
||
>
|
||
一键采用该 AI 建议并生成处置单
|
||
</button>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '2px' }}>
|
||
指示:复测窗口 3-7 天,回流后系统自动进行达标判定 (Met / Not Met)。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 活动处置单草案 & 流程状态机门控 (D.1/D.2) */}
|
||
{myDispositions.length > 0 && (
|
||
<div className="dispositions-area">
|
||
<h4 className="sub-title">📌 正在执行中 / 待确认的个案处置单</h4>
|
||
{myDispositions.map((disp) => {
|
||
return (
|
||
<div key={disp.id} className="disposition-card shadow-sm">
|
||
<div className="spread align-center">
|
||
<h5 className="disp-title">{disp.title}</h5>
|
||
<span className={`disp-status-badge ${disp.status}`}>{disp.status === 'pending_confirmation' ? '⏳ 待确认' : disp.status === 'executing' ? '⚙️ 执行中' : '✅ 闭环'}</span>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-xs)', marginTop: '2px' }}>
|
||
创建人:{disp.createdBy} · 创建时间:{formatDate(disp.createdAt)}
|
||
</p>
|
||
|
||
{/* 门控:中高风险必须确认 (D.1/D.2) */}
|
||
{disp.status === 'pending_confirmation' && (
|
||
<div className="gating-warning-box bg-rose-soft">
|
||
<ShieldCheck size={16} strokeWidth={1.8} style={{ color: 'var(--color-destructive)' }} />
|
||
<span className="warn-text">
|
||
本个案属于 <strong>{caseInfo.riskLevel === 'high' ? '高' : '中'}风险</strong> 类别。系统已安全启动风险准入门控(Gating)—— 必须由主治医生角色审查并批准确认,方可落地执行!
|
||
</span>
|
||
{(user?.role === 'physician' || user?.role === 'admin' || user?.role === 'case_manager') && (
|
||
<button
|
||
className="btn btn-sm btn-rose"
|
||
onClick={() => handleConfirmDisposition(disp.id)}
|
||
style={{ marginLeft: 'auto' }}
|
||
>
|
||
医生一键审查并确认 (Confirm)
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 执行:动作项明细并带有执行按钮 */}
|
||
{disp.status === 'executing' && (
|
||
<div className="actions-execute-panel">
|
||
{disp.actions.map((act, aIdx) => (
|
||
<div key={aIdx} className="execute-row shadow-sm bg-light">
|
||
<div className="execute-row__info">
|
||
<strong>动作:{act.kind === 'recheck' ? '📊 复测跟进' : '📋 常规调理'}</strong>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)' }}>{act.description}</p>
|
||
</div>
|
||
<div className="execute-row__action">
|
||
<input
|
||
type="text"
|
||
placeholder="填入执行回执说明…"
|
||
value={actionNote}
|
||
onChange={(e) => setActionNote(e.target.value)}
|
||
className="input-inline"
|
||
/>
|
||
<button
|
||
className="btn btn-xs btn-primary"
|
||
onClick={() => handleExecuteAction(disp.id, String(aIdx))}
|
||
>
|
||
执行此动作并回执
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* B. 选中的是情绪信号 (Emotion) ➔ 暖心身心焦虑干预区 (T-D.4) */}
|
||
{selectedItem.type === 'emotion' && matchedEmotion && (
|
||
<div className="stack detail-content-box animate-fade-in">
|
||
<div className="emotion-desc-box border-l-indigo">
|
||
<p><strong>自评得分:</strong><span className="highlight-val text-indigo">{matchedEmotion.score}</span> / 10 分(得分越低表示情绪越压抑、焦虑)</p>
|
||
<p><strong>情绪状态:</strong>
|
||
<span className={`badge ${matchedEmotion.status === 'crisis' ? 'badge-rose' : 'badge-amber'}`}>
|
||
{matchedEmotion.status === 'crisis' ? '🔴 极高身心危机' : '🟡 焦虑担忧'}
|
||
</span>
|
||
</p>
|
||
<p><strong>情绪主观陈述(已安全解密):</strong></p>
|
||
<div className="emotion-diary shadow-sm">
|
||
“ {matchedEmotion.note} ”
|
||
</div>
|
||
</div>
|
||
|
||
{matchedEmotion.status === 'crisis' && (
|
||
<div className="gating-warning-box bg-rose-soft">
|
||
<ShieldCheck size={18} strokeWidth={1.8} style={{ color: 'var(--color-destructive)' }} />
|
||
<span className="warn-text">
|
||
🚨 <strong>身心危急警示:</strong> 孕妇当前出现了严重情绪低落或崩溃倾向词!请个案管理师<b>立即进行电话干预主动疏导</b>,并联动主治医生协助诊断,切勿拖延!
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
<div className="care-actions-card shadow-sm card bg-light-gradient">
|
||
<h4 className="sub-title"><MessageSquareHeart size={16} strokeWidth={1.8} /> 身心暖心干预关怀动作区</h4>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginBottom: 'var(--space-2)' }}>
|
||
您可以通过下发关怀提醒任务,强制将重度体力运动计划调整为安全静息,缓解孕期焦虑:
|
||
</p>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={handleSendCareReminder}
|
||
>
|
||
一键下发“心理静息与休息”日常关怀提醒 (rest)
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* C. 选中的是专家转会诊 (Referral) ➔ 医生多端协同区 (T-D.3) */}
|
||
{selectedItem.type === 'referral' && matchedReferral && (
|
||
<div className="stack detail-content-box animate-fade-in">
|
||
<div className="referral-desc-box">
|
||
<div className="spread align-center">
|
||
<p><strong>会诊类别:</strong>{matchedReferral.type === 'referral' ? '院内转诊' : '多科室会诊'}</p>
|
||
<span className={`badge ${riskBadgeClass(matchedReferral.urgency === 'emergency' ? 'high' : 'medium')}`}>
|
||
{matchedReferral.urgency === 'emergency' ? '🚨 特急' : matchedReferral.urgency === 'urgent' ? '⏳ 紧急' : '常规'}
|
||
</span>
|
||
</div>
|
||
<p><strong>当前协同状态:</strong>
|
||
<span className="badge badge-indigo">
|
||
{matchedReferral.status === 'pending' ? '待接诊' : matchedReferral.status === 'accepted' ? '会诊中' : '已回复会诊'}
|
||
</span>
|
||
</p>
|
||
<p><strong>临床病情陈述:</strong></p>
|
||
<div className="clinical-summary-box shadow-sm">
|
||
{matchedReferral.clinicalSummary}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 医生角色协同处理 (T-D.3) */}
|
||
{user?.role === 'physician' || user?.role === 'admin' ? (
|
||
<div className="doctor-collaboration-panel card shadow-sm border-l-emerald bg-light-gradient">
|
||
<h4 className="sub-title"><CheckCircle2 size={16} style={{ color: 'var(--color-calm)' }} /> 医生协同处理工作面板</h4>
|
||
|
||
{matchedReferral.status === 'pending' && (
|
||
<div className="stack">
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginBottom: 'var(--space-2)' }}>
|
||
个案管理师已将会诊单提交到您的待办,请立即接诊评估病情:
|
||
</p>
|
||
<button
|
||
className="btn btn-primary"
|
||
onClick={() => handleAcceptReferral(matchedReferral.id)}
|
||
>
|
||
一键接诊并开启会诊 (Accept)
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{(matchedReferral.status === 'accepted' || matchedReferral.status === 'pending') && (
|
||
<div className="stack" style={{ marginTop: 'var(--space-3)' }}>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginBottom: 'var(--space-1)' }}>
|
||
请在线填写您的临床会诊意见及降压/控糖处方建议:
|
||
</p>
|
||
<textarea
|
||
className="textarea"
|
||
rows={3}
|
||
placeholder="输入会诊意见(例如:建议每日加用拉贝洛尔 100mg,规律自测血压,限制中度运动)…"
|
||
value={referralReply}
|
||
onChange={(e) => setReferralReply(e.target.value)}
|
||
/>
|
||
<button
|
||
className="btn btn-success"
|
||
onClick={() => handleRespondReferral(matchedReferral.id)}
|
||
style={{ marginTop: 'var(--space-2)', marginLeft: 'auto' }}
|
||
>
|
||
提交会诊回复意见并闭环 (Respond)
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
// 非医生角色展示回复回执
|
||
matchedReferral.doctorReply && (
|
||
<div className="referral-reply-card shadow-sm">
|
||
<h4 className="sub-title">👨⚕️ 专家医生回复意见回执</h4>
|
||
<div className="doctor-reply-box shadow-sm">
|
||
{matchedReferral.doctorReply}
|
||
</div>
|
||
<p className="muted text-right" style={{ fontSize: 'var(--font-xs)', marginTop: '4px' }}>
|
||
回复时间:{formatDate(matchedReferral.updatedAt)}
|
||
</p>
|
||
</div>
|
||
)
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<p className="empty">请在左侧待处置队列中选择一项事件以进行个案流转处置。</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 4. 手动发起专家协同会诊入口 (T-D.3) */}
|
||
<div className="referral-create-card card shadow-sm" style={{ marginTop: 'var(--space-4)' }}>
|
||
<div className="referral-create-header">
|
||
<div className="referral-create-title">
|
||
<BookOpen size={20} strokeWidth={1.8} style={{ color: 'var(--color-primary-strong)' }} />
|
||
<h3 className="pane-title" style={{ marginBottom: 0 }}>发起多学科专家临床会诊</h3>
|
||
</div>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)', marginTop: 'var(--space-2)' }}>
|
||
管理师在随访过程中,发现非诊断范围的疑难高风险并发症倾向,可手动一键发起会诊派发给主治/会诊医生在线指导
|
||
</p>
|
||
</div>
|
||
<div className="referral-create-form">
|
||
<div className="form-group">
|
||
<label className="form-label">会诊紧急程度</label>
|
||
<select
|
||
className="select-modern"
|
||
value={referralUrgency}
|
||
onChange={(e) => setReferralUrgency(e.target.value as any)}
|
||
>
|
||
<option value="routine">🟢 常规会诊</option>
|
||
<option value="urgent">🟡 紧急会诊</option>
|
||
<option value="emergency">🔴 特急会诊</option>
|
||
</select>
|
||
</div>
|
||
<div className="form-group form-group-flex">
|
||
<label className="form-label">病情及会诊目的描述</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="text"
|
||
className="input-modern"
|
||
placeholder="请详细描述患者病情及申请会诊的目的..."
|
||
value={referralSummary}
|
||
onChange={(e) => setReferralSummary(e.target.value)}
|
||
/>
|
||
<button
|
||
className="btn btn-primary btn-icon"
|
||
onClick={handleCreateReferralManual}
|
||
title="手动分派会诊"
|
||
>
|
||
<Send size={18} strokeWidth={2} />
|
||
<span>发起会诊</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 5. 照护计划及提醒 */}
|
||
<div className="grid-2 workbench-grid" style={{ marginTop: 'var(--space-4)' }}>
|
||
<CarePlanPanel
|
||
caseId={caseInfo.id}
|
||
plans={carePlans}
|
||
role={user?.role}
|
||
onCreated={loadCase}
|
||
/>
|
||
<div className="stack">
|
||
<RemindersPanel reminders={reminders} />
|
||
<RedflagPanel patientId={patientId} role={user?.role} onTriggered={loadAll} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* ========================================================
|
||
TAB 2: 档案全数据汇集健康档案页 (T-D.8)
|
||
======================================================== */
|
||
<div className="stack animate-fade-in">
|
||
{/* 1. 指标概览大彩卡网格 */}
|
||
<div className="grid-4 indicators-summary-grid">
|
||
{/* 血糖 */}
|
||
<div className="indicator-summary-card shadow-sm">
|
||
<div className="indicator-summary-card__head">
|
||
<span className="indicator-label">孕期空腹血糖</span>
|
||
<span className={`direction-badge ${trendsMap['fasting_glucose']?.direction}`}>
|
||
{trendsMap['fasting_glucose']?.direction === 'up' ? '↑ 升高' : trendsMap['fasting_glucose']?.direction === 'down' ? '↓ 降低' : '→ 平稳'}
|
||
</span>
|
||
</div>
|
||
<div className="indicator-summary-card__val">
|
||
{observations.find((o) => o.indicator === 'fasting_glucose')?.value || '—'}{' '}
|
||
<span className="unit-label">mmol/L</span>
|
||
</div>
|
||
<div className="sparkline-wrapper">
|
||
<Sparkline points={trendsMap['fasting_glucose']?.points || []} minLimit={5.1} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 血压(收缩压) */}
|
||
<div className="indicator-summary-card shadow-sm">
|
||
<div className="indicator-summary-card__head">
|
||
<span className="indicator-label">孕期收缩压</span>
|
||
<span className={`direction-badge ${trendsMap['systolic_bp']?.direction}`}>
|
||
{trendsMap['systolic_bp']?.direction === 'up' ? '↑ 升高' : trendsMap['systolic_bp']?.direction === 'down' ? '↓ 降低' : '→ 平稳'}
|
||
</span>
|
||
</div>
|
||
<div className="indicator-summary-card__val">
|
||
{observations.find((o) => o.indicator === 'systolic_bp')?.value || '—'}{' '}
|
||
<span className="unit-label">mmHg</span>
|
||
</div>
|
||
<div className="sparkline-wrapper">
|
||
<Sparkline points={trendsMap['systolic_bp']?.points || []} maxLimit={140} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 体重 */}
|
||
<div className="indicator-summary-card shadow-sm">
|
||
<div className="indicator-summary-card__head">
|
||
<span className="indicator-label">母体体重监控</span>
|
||
<span className={`direction-badge ${trendsMap['weight']?.direction}`}>
|
||
{trendsMap['weight']?.direction === 'up' ? '↑ 升高' : trendsMap['weight']?.direction === 'down' ? '↓ 降低' : '→ 平稳'}
|
||
</span>
|
||
</div>
|
||
<div className="indicator-summary-card__val">
|
||
{observations.find((o) => o.indicator === 'weight')?.value || '—'}{' '}
|
||
<span className="unit-label">kg</span>
|
||
</div>
|
||
<div className="sparkline-wrapper">
|
||
<Sparkline points={trendsMap['weight']?.points || []} />
|
||
</div>
|
||
</div>
|
||
|
||
{/* 心率 */}
|
||
<div className="indicator-summary-card shadow-sm">
|
||
<div className="indicator-summary-card__head">
|
||
<span className="indicator-label">孕妇静息心率</span>
|
||
<span className={`direction-badge ${trendsMap['heart_rate']?.direction}`}>
|
||
{trendsMap['heart_rate']?.direction === 'up' ? '↑ 升高' : trendsMap['heart_rate']?.direction === 'down' ? '↓ 降低' : '→ 平稳'}
|
||
</span>
|
||
</div>
|
||
<div className="indicator-summary-card__val">
|
||
{observations.find((o) => o.indicator === 'heart_rate')?.value || '—'}{' '}
|
||
<span className="unit-label">bpm</span>
|
||
</div>
|
||
<div className="sparkline-wrapper">
|
||
<Sparkline points={trendsMap['heart_rate']?.points || []} minLimit={50} maxLimit={110} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2. 静态档案 + 身心自评历史曲线图 */}
|
||
<div className="grid-2" style={{ gap: 'var(--space-4)' }}>
|
||
{/* 孕产静态档案 */}
|
||
<div className="card shadow-sm">
|
||
<h3 className="pane-title"><UserRound size={18} /> 孕产基础静态生理档案</h3>
|
||
<div className="static-archive-grid">
|
||
<div className="archive-item"><span className="archive-label">姓名:</span><span>{patient.name}</span></div>
|
||
<div className="archive-item"><span className="archive-label">年龄:</span><span>{patient.age} 岁</span></div>
|
||
<div className="archive-item"><span className="archive-label">身高:</span><span>{patient.heightCm || '162'} cm</span></div>
|
||
<div className="archive-item"><span className="archive-label">孕前体重:</span><span>{patient.prePregnancyWeightKg || '58'} kg</span></div>
|
||
<div className="archive-item"><span className="archive-label">预产期EDD:</span><span>{formatDate(patient.edd)}</span></div>
|
||
<div className="archive-item"><span className="archive-label">末次月经LMP:</span><span>{formatDate(patient.lmp)}</span></div>
|
||
<div className="archive-item"><span className="archive-label">多胎妊娠:</span><span>{patient.multipleGestation ? '🔴 是' : '🟢 否'}</span></div>
|
||
<div className="archive-item"><span className="archive-label">糖尿病史:</span><span>{patient.historyGdm ? '🔴 有' : '🟢 无'}</span></div>
|
||
<div className="archive-item"><span className="archive-label">高血压史:</span><span>{patient.historyPih ? '🔴 有' : '🟢 无'}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 焦虑身心评分纵向曲线 (T-D.8) */}
|
||
<div className="card shadow-sm">
|
||
<h3 className="pane-title"><MessageSquareHeart size={18} /> 孕妇情绪/身心焦虑历史评分曲线</h3>
|
||
{emotions.length === 0 ? (
|
||
<p className="empty-small">暂无孕妇自评数据。打卡后将显示近 N 次走向趋势。</p>
|
||
) : (
|
||
<div className="stack text-center" style={{ padding: 'var(--space-4) 0' }}>
|
||
<p className="muted" style={{ fontSize: 'var(--font-sm)' }}>近几日身心焦虑自评分值(越高越平稳,分低代表压抑):</p>
|
||
<div className="spread align-center justify-center" style={{ gap: 'var(--space-8)', margin: 'var(--space-4) 0' }}>
|
||
<div className="huge-stat-box">
|
||
<span className="huge-val text-indigo">{emotions[0].score}</span>
|
||
<span className="muted" style={{ fontSize: 'var(--font-xs)' }}>/10 最新分</span>
|
||
</div>
|
||
<Sparkline points={emotions.map((e) => e.score).reverse()} minLimit={3} />
|
||
</div>
|
||
<div className="work-list-mini" style={{ maxHeight: '110px', overflowY: 'auto' }}>
|
||
{emotions.slice(0, 3).map((e) => (
|
||
<div key={e.id} className="row align-center spread shadow-sm" style={{ padding: '6px 12px', fontSize: 'var(--font-xs)', background: '#fff', borderRadius: 'var(--radius-sm)' }}>
|
||
<span className="muted">{formatDate(e.createdAt)}</span>
|
||
<span className={`badge ${e.status === 'crisis' ? 'badge-rose' : 'badge-amber'}`}>{e.status === 'crisis' ? '危机' : '担忧'}</span>
|
||
<span className="muted" style={{ maxWidth: '160px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{e.note}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 3. 指标历史所有观测及时间轴 */}
|
||
<div className="grid-2 workbench-grid" style={{ marginTop: 'var(--space-4)' }}>
|
||
{/* 动态时间轴 (CaseTimeline) ➔ D 阶段迁入健康档案汇总 (T-D.8) */}
|
||
<CaseTimeline
|
||
caseInfo={caseInfo}
|
||
observations={observations}
|
||
alerts={alerts}
|
||
carePlans={carePlans}
|
||
reminders={reminders}
|
||
/>
|
||
|
||
{/* 观测历史列表(带筛选) */}
|
||
<div className="card shadow-sm">
|
||
<h3 className="pane-title">📊 历史所有临床测量记录 ({observations.length})</h3>
|
||
<div className="observations-list-container" style={{ maxHeight: '420px', overflowY: 'auto' }}>
|
||
{observations.length === 0 ? (
|
||
<p className="empty-small">暂无任何历史自测值记录。</p>
|
||
) : (
|
||
<table className="table" style={{ fontSize: 'var(--font-sm)' }}>
|
||
<thead>
|
||
<tr>
|
||
<th>测量时间</th>
|
||
<th>测量指标</th>
|
||
<th>测量值</th>
|
||
<th>QC 状态</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{observations.map((obs) => (
|
||
<tr key={obs.id}>
|
||
<td className="muted">{formatDate(obs.measuredAt)}</td>
|
||
<td>{translateIndicator(obs.indicator)}</td>
|
||
<td style={{ fontWeight: 600 }}>{obs.value} {obs.unit}</td>
|
||
<td>
|
||
<span className={`badge ${obs.qcStatus === 'accepted' ? 'badge-emerald' : 'badge-rose'}`}>
|
||
{obs.qcStatus === 'accepted' ? '质控通过' : '质控拦截'}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|