feat(web): 行动收件箱前端 — API + Drawer + 列表页 + 路由

- actionInbox.ts: API 调用层,list + getThread
- ActionThreadDrawer: 上下文线程抽屉,时间线 + 操作按钮
- ActionInbox: 列表页,Tabs 筛选 + 分页 + 点击打开 Drawer
- App.tsx: 注册 /health/action-inbox 路由
This commit is contained in:
iven
2026-05-01 16:36:24 +08:00
parent 758bc210e1
commit 81dd3d2bda
4 changed files with 466 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
import { useCallback, useEffect, useState } from 'react';
import { Tag, List, Badge, Tabs, Spin, Empty } from 'antd';
import { PageContainer } from '../../components/PageContainer';
import ActionThreadDrawer from '../../components/ActionThreadDrawer';
import {
actionInboxApi,
type ActionItem,
type ActionType,
type ActionPriority,
} from '../../api/health/actionInbox';
import { formatRelative } from '../../utils/format';
const TYPE_CONFIG: Record<ActionType, { label: string; color: string }> = {
ai_suggestion: { label: 'AI建议', color: '#722ed1' },
alert: { label: '告警', color: '#f5222d' },
followup: { label: '随访', color: '#1890ff' },
data_anomaly: { label: '异常', color: '#fa8c16' },
};
const PRIORITY_LABEL: Record<ActionPriority, string> = {
urgent: '紧急',
high: '高',
medium: '中',
low: '低',
};
const PRIORITY_COLOR: Record<ActionPriority, string> = {
urgent: 'red',
high: 'orange',
medium: 'blue',
low: 'default',
};
const STATUS_TABS = [
{ key: 'all', label: '全部' },
{ key: 'pending', label: '待处理' },
{ key: 'in_progress', label: '进行中' },
{ key: 'completed', label: '已完成' },
];
const BADGE_STATUS: Record<string, 'error' | 'processing' | 'default'> = {
pending: 'error',
in_progress: 'processing',
completed: 'default',
};
export default function ActionInbox() {
const [items, setItems] = useState<ActionItem[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [statusFilter, setStatusFilter] = useState('all');
const [drawerOpen, setDrawerOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<ActionItem | null>(null);
const fetchData = useCallback(
async (p: number, status?: string) => {
setLoading(true);
try {
const resp = await actionInboxApi.list({
page: p,
page_size: 20,
status: status === 'all' ? undefined : status,
});
setItems(resp.data);
setTotal(resp.total);
setPage(p);
} finally {
setLoading(false);
}
},
[],
);
useEffect(() => {
fetchData(1, 'all');
}, [fetchData]);
const handleTabChange = (key: string) => {
setStatusFilter(key);
fetchData(1, key);
};
const handleItemClick = (item: ActionItem) => {
setSelectedItem(item);
setDrawerOpen(true);
};
const handleActionComplete = () => {
fetchData(page, statusFilter);
};
return (
<PageContainer
title="行动收件箱"
subtitle={`${total} 项待办`}
>
<Tabs
activeKey={statusFilter}
onChange={handleTabChange}
items={STATUS_TABS.map((tab) => ({
key: tab.key,
label: tab.label,
}))}
/>
<Spin spinning={loading}>
{items.length === 0 && !loading ? (
<Empty description="暂无待办事项" />
) : (
<List
dataSource={items}
pagination={{
current: page,
total,
pageSize: 20,
onChange: (p) => fetchData(p, statusFilter),
showTotal: (t) => `${t}`,
}}
renderItem={(item) => {
const typeConf =
TYPE_CONFIG[item.action_type] ??
({ label: '未知', color: '#999' } as {
label: string;
color: string;
});
return (
<List.Item
style={{ cursor: 'pointer', padding: '12px 16px' }}
onClick={() => handleItemClick(item)}
>
<List.Item.Meta
avatar={
<Badge
status={BADGE_STATUS[item.status] ?? 'default'}
/>
}
title={
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
}}
>
<Tag color={typeConf.color}>{typeConf.label}</Tag>
<span>{item.title}</span>
<Tag color={PRIORITY_COLOR[item.priority]}>
{PRIORITY_LABEL[item.priority]}
</Tag>
</div>
}
description={`${item.patient_name} · ${formatRelative(item.created_at)}`}
/>
</List.Item>
);
}}
/>
)}
</Spin>
<ActionThreadDrawer
open={drawerOpen}
item={selectedItem}
onClose={() => setDrawerOpen(false)}
onActionComplete={handleActionComplete}
/>
</PageContainer>
);
}