Files
hms/apps/web/src/pages/health/components/workbench/ActionDetailDrawer.tsx
iven ab2c9bbc43 feat(web): 工作台面板组件 — AiInsightPanel / TeamOverviewPanel / ActionDetailDrawer
- AiInsightPanel: 工作台统计概览(待处理/AI建议/紧急告警/到期随访+完成率)
- TeamOverviewPanel: 主任团队概览(成员列表+风险分布+完成率进度条)
- ActionDetailDrawer: 待办详情抽屉(患者信息+操作时间线+快捷操作按钮)
2026-05-01 21:19:46 +08:00

168 lines
5.0 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Drawer, Descriptions, Tag, Steps, Button, Space, Spin, message } from 'antd';
import {
CheckOutlined,
CloseOutlined,
EnterOutlined,
} from '@ant-design/icons';
import {
actionInboxApi,
type ActionItem,
type ThreadResponse,
type ActionType,
} from '../../../../api/health/actionInbox';
const TYPE_CONFIG: Record<ActionType, { label: string; color: string }> = {
ai_suggestion: { label: 'AI 建议', color: 'blue' },
alert: { label: '告警', color: 'red' },
followup: { label: '随访', color: 'green' },
data_anomaly: { label: '数据异常', color: 'orange' },
};
const PRIORITY_COLOR: Record<string, string> = {
urgent: 'red',
high: 'volcano',
medium: 'orange',
low: 'default',
};
const STATUS_STEP: Record<string, { title: string; description: string }> = {
pending: { title: '待处理', description: '等待处理' },
in_progress: { title: '处理中', description: '正在处理' },
completed: { title: '已完成', description: '已处理完毕' },
dismissed: { title: '已忽略', description: '已标记忽略' },
};
interface ActionDetailDrawerProps {
item: ActionItem | null;
open: boolean;
onClose: () => void;
onActionComplete?: () => void;
}
export default function ActionDetailDrawer({
item,
open,
onClose,
onActionComplete,
}: ActionDetailDrawerProps) {
const [thread, setThread] = useState<ThreadResponse | null>(null);
const [loading, setLoading] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
useEffect(() => {
if (!item || !open) {
setThread(null);
return;
}
setLoading(true);
actionInboxApi
.getThread(item.source_ref)
.then(setThread)
.finally(() => setLoading(false));
}, [item, open]);
if (!item) return null;
const typeCfg = TYPE_CONFIG[item.action_type];
const handleAction = async (actionKey: string) => {
if (!thread) return;
setActionLoading(true);
try {
const actionDef = thread.available_actions.find((a) => a.key === actionKey);
if (!actionDef?.api_endpoint) {
message.warning('该操作暂未实现');
return;
}
// TODO: 调用实际 API 执行操作
message.success('操作成功');
onActionComplete?.();
onClose();
} catch {
message.error('操作失败');
} finally {
setActionLoading(false);
}
};
const currentStepIdx = thread
? ['pending', 'in_progress', 'completed', 'dismissed'].indexOf(thread.action_item.status)
: 0;
return (
<Drawer
title={
<Space>
<Tag color={typeCfg.color}>{typeCfg.label}</Tag>
<Tag color={PRIORITY_COLOR[item.priority]}>{item.priority}</Tag>
<span>{item.title}</span>
</Space>
}
open={open}
onClose={onClose}
width={520}
extra={
thread?.available_actions.length ? (
<Space>
{thread.available_actions.map((action) => (
<Button
key={action.key}
type={action.variant === 'primary' ? 'primary' : 'default'}
danger={action.variant === 'danger'}
icon={
action.key === 'approve' ? <CheckOutlined /> :
action.key === 'dismiss' ? <CloseOutlined /> :
<EnterOutlined />
}
loading={actionLoading}
onClick={() => handleAction(action.key)}
>
{action.label}
</Button>
))}
</Space>
) : undefined
}
>
{loading ? (
<div style={{ textAlign: 'center', padding: 40 }}><Spin /></div>
) : (
<>
<Descriptions column={2} size="small" bordered style={{ marginBottom: 24 }}>
<Descriptions.Item label="患者">{item.patient_name}</Descriptions.Item>
<Descriptions.Item label="创建时间">
{new Date(item.created_at).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="摘要" span={2}>{item.summary}</Descriptions.Item>
</Descriptions>
{thread && (
<Steps
current={currentStepIdx}
size="small"
direction="vertical"
items={thread.thread.map((evt) => ({
title: STATUS_STEP[evt.status]?.title ?? evt.label,
description: (
<div>
<div>{evt.detail || evt.label}</div>
{evt.timestamp && (
<div style={{ fontSize: 12, color: '#999' }}>
{new Date(evt.timestamp).toLocaleString()}
</div>
)}
{evt.operator && (
<div style={{ fontSize: 12, color: '#666' }}>: {evt.operator}</div>
)}
</div>
),
}))}
/>
)}
</>
)}
</Drawer>
);
}