feat(web): Home.tsx 集成统一工作台 — 医生行动收件箱 + 主任团队概览
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 医生/护士角色:待办任务行替换为行动收件箱(TodoList) + AI 概览面板
- 主任角色:在最近动态下方新增 TeamOverviewPanel 团队概览
- 所有角色:点击待办项可打开 ActionDetailDrawer 查看详情和操作
- admin/operator 角色保持原有待办任务+最近动态布局
This commit is contained in:
iven
2026-05-01 21:22:28 +08:00
parent ab2c9bbc43
commit 4aa014de0d

View File

@@ -30,6 +30,11 @@ import { listPendingTasks, type TaskInfo } from '../api/workflowTasks';
import { pointsApi, type PersonalStats } from '../api/health/points'; import { pointsApi, type PersonalStats } from '../api/health/points';
import { useStatsData } from './health/StatisticsDashboard/useStatsData'; import { useStatsData } from './health/StatisticsDashboard/useStatsData';
import { useCountUp } from '../hooks/useCountUp'; import { useCountUp } from '../hooks/useCountUp';
import TodoList from './health/components/workbench/TodoList';
import AiInsightPanel from './health/components/workbench/AiInsightPanel';
import TeamOverviewPanel from './health/components/workbench/TeamOverviewPanel';
import ActionDetailDrawer from './health/components/workbench/ActionDetailDrawer';
import type { ActionItem } from '../api/health/actionInbox';
// --- Shared utilities --- // --- Shared utilities ---
@@ -172,6 +177,8 @@ export default function Home() {
const [pendingTasks, setPendingTasks] = useState<TaskInfo[]>([]); const [pendingTasks, setPendingTasks] = useState<TaskInfo[]>([]);
const [recentActivities, setRecentActivities] = useState<AuditLogItem[]>([]); const [recentActivities, setRecentActivities] = useState<AuditLogItem[]>([]);
const [activitiesLoading, setActivitiesLoading] = useState(true); const [activitiesLoading, setActivitiesLoading] = useState(true);
const [drawerItem, setDrawerItem] = useState<ActionItem | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const statsData = useStatsData(); const statsData = useStatsData();
const loading = personalLoading || statsData.loading; const loading = personalLoading || statsData.loading;
@@ -262,75 +269,94 @@ export default function Home() {
{/* 待办任务 + 最近活动 */} {/* 待办任务 + 最近活动 */}
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}> <Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={24} lg={14}> {(role === 'doctor' || role === 'nurse') ? (
<div className="erp-content-card erp-fade-in erp-fade-in-delay-2"> <>
<div className="erp-section-header"> <Col xs={24} lg={14}>
<CheckCircleOutlined className="erp-section-icon" /> <div className="erp-content-card erp-fade-in erp-fade-in-delay-2">
<span className="erp-section-title"></span> <div className="erp-section-header">
<span style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--erp-text-secondary)' }}> <CheckCircleOutlined className="erp-section-icon" />
{pendingTasks.length} <span className="erp-section-title"></span>
</span> </div>
</div> <TodoList onItemClick={(item) => { setDrawerItem(item); setDrawerOpen(true); }} />
<div className="erp-task-list"> </div>
{pendingTasks.length === 0 ? ( </Col>
<Empty description="暂无待办任务" image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Col xs={24} lg={10}>
) : ( <AiInsightPanel />
pendingTasks.map((task) => ( </Col>
<div </>
key={task.id} ) : (
className="erp-task-item" <>
style={{ '--task-color': 'var(--erp-primary)' } as React.CSSProperties} <Col xs={24} lg={14}>
onClick={() => handleNavigate('/workflow')} <div className="erp-content-card erp-fade-in erp-fade-in-delay-2">
role="button" <div className="erp-section-header">
tabIndex={0} <CheckCircleOutlined className="erp-section-icon" />
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate('/workflow'); }} <span className="erp-section-title"></span>
> <span style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--erp-text-secondary)' }}>
<div className="erp-task-item-icon"><PartitionOutlined /></div> {pendingTasks.length}
<div className="erp-task-item-content"> </span>
<div className="erp-task-item-title">{task.node_name || task.definition_name || '流程任务'}</div> </div>
<div className="erp-task-item-meta"> <div className="erp-task-list">
<span>{task.definition_name || '工作流'}</span> {pendingTasks.length === 0 ? (
<span>{task.status === 'pending' ? '待处理' : task.status}</span> <Empty description="暂无待办任务" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : (
pendingTasks.map((task) => (
<div
key={task.id}
className="erp-task-item"
style={{ '--task-color': 'var(--erp-primary)' } as React.CSSProperties}
onClick={() => handleNavigate('/workflow')}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === 'Enter') handleNavigate('/workflow'); }}
>
<div className="erp-task-item-icon"><PartitionOutlined /></div>
<div className="erp-task-item-content">
<div className="erp-task-item-title">{task.node_name || task.definition_name || '流程任务'}</div>
<div className="erp-task-item-meta">
<span>{task.definition_name || '工作流'}</span>
<span>{task.status === 'pending' ? '待处理' : task.status}</span>
</div>
</div>
<span className="erp-task-priority erp-task-priority-medium"></span>
<RightOutlined style={{ color: 'var(--erp-text-tertiary)', fontSize: 12 }} />
</div> </div>
</div> ))
<span className="erp-task-priority erp-task-priority-medium"></span> )}
<RightOutlined style={{ color: 'var(--erp-text-tertiary)', fontSize: 12 }} /> </div>
</div> </div>
)) </Col>
)}
</div>
</div>
</Col>
<Col xs={24} lg={10}> <Col xs={24} lg={10}>
<div className="erp-content-card erp-fade-in erp-fade-in-delay-3" style={{ height: '100%' }}> <div className="erp-content-card erp-fade-in erp-fade-in-delay-3" style={{ height: '100%' }}>
<div className="erp-section-header"> <div className="erp-section-header">
<ClockCircleOutlined className="erp-section-icon" /> <ClockCircleOutlined className="erp-section-icon" />
<span className="erp-section-title"></span> <span className="erp-section-title"></span>
</div> </div>
<div className="erp-activity-list"> <div className="erp-activity-list">
{activitiesLoading ? ( {activitiesLoading ? (
<div style={{ textAlign: 'center', padding: 24 }}><Spin /></div> <div style={{ textAlign: 'center', padding: 24 }}><Spin /></div>
) : recentActivities.length === 0 ? ( ) : recentActivities.length === 0 ? (
<Empty description="暂无动态" image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty description="暂无动态" image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : ( ) : (
recentActivities.map((log) => ( recentActivities.map((log) => (
<div key={log.id} className="erp-activity-item"> <div key={log.id} className="erp-activity-item">
<div className="erp-activity-dot"> <div className="erp-activity-dot">
{RESOURCE_ICONS[log.resource_type] || <FileTextOutlined />} {RESOURCE_ICONS[log.resource_type] || <FileTextOutlined />}
</div> </div>
<div className="erp-activity-content"> <div className="erp-activity-content">
<div className="erp-activity-text"> <div className="erp-activity-text">
{formatActionLabel(log.action)}{formatResourceLabel(log.resource_type)} {formatActionLabel(log.action)}{formatResourceLabel(log.resource_type)}
</div>
<div className="erp-activity-time">{formatTimeAgo(log.created_at)}</div>
</div>
</div> </div>
<div className="erp-activity-time">{formatTimeAgo(log.created_at)}</div> ))
</div> )}
</div> </div>
)) </div>
)} </Col>
</div> </>
</div> )}
</Col>
</Row> </Row>
{/* 快捷入口 */} {/* 快捷入口 */}
@@ -361,6 +387,26 @@ export default function Home() {
</div> </div>
</Col> </Col>
</Row> </Row>
{/* 主任团队概览 */}
{role === 'admin' && (
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col span={24}>
<TeamOverviewPanel />
</Col>
</Row>
)}
{/* 行动详情抽屉 */}
<ActionDetailDrawer
item={drawerItem}
open={drawerOpen}
onClose={() => { setDrawerOpen(false); setDrawerItem(null); }}
onActionComplete={() => {
setDrawerOpen(false);
setDrawerItem(null);
}}
/>
</div> </div>
); );
} }