/** * 项目健康度大盘 (Level 1) * * 健康度评分 + 核心数据卡片 + 趋势折线图 + 热力图 + 事件预警 */ import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Row, Col, Statistic, Progress, Empty, Tag, Typography, Space, Tooltip, Badge, Alert, } from 'antd'; import { CheckCircleOutlined, WarningOutlined, CloseCircleOutlined, TeamOutlined, AlertOutlined, RiseOutlined, ThunderboltOutlined, FileSearchOutlined, } from '@ant-design/icons'; import * as iitProjectApi from '../api/iitProjectApi'; import { useIitProject } from '../context/IitProjectContext'; const { Text, Title } = Typography; const DashboardPage: React.FC = () => { const navigate = useNavigate(); const { projectId } = useIitProject(); const [stats, setStats] = useState(null); const [heatmap, setHeatmap] = useState(null); const [trend, setTrend] = useState([]); const [criticalEvents, setCriticalEvents] = useState([]); const [equeryStats, setEqueryStats] = useState(null); const fetchData = useCallback(async () => { if (!projectId) return; try { const [cockpitData, trendData, eventsData, eqStats] = await Promise.allSettled([ iitProjectApi.getQcCockpitData(projectId), iitProjectApi.getTrend(projectId, 30), iitProjectApi.getCriticalEvents(projectId, { status: 'open', pageSize: 5 }), iitProjectApi.getEqueryStats(projectId), ]); if (cockpitData.status === 'fulfilled') { setStats(cockpitData.value.stats); setHeatmap(cockpitData.value.heatmap); } if (trendData.status === 'fulfilled') setTrend(trendData.value); if (eventsData.status === 'fulfilled') setCriticalEvents(eventsData.value.items); if (eqStats.status === 'fulfilled') setEqueryStats(eqStats.value); } catch { /* non-fatal */ } }, [projectId]); useEffect(() => { fetchData(); }, [fetchData]); // Health Score = weighted average of passRate, eQuery backlog, critical events const passRate = stats?.passRate ?? 0; const pendingEq = equeryStats?.pending ?? 0; const criticalCount = criticalEvents.length; let healthScore = passRate; if (pendingEq > 10) healthScore -= 10; else if (pendingEq > 5) healthScore -= 5; if (criticalCount > 0) healthScore -= criticalCount * 5; healthScore = Math.max(0, Math.min(100, Math.round(healthScore))); const healthColor = healthScore >= 80 ? '#52c41a' : healthScore >= 60 ? '#faad14' : '#ff4d4f'; const healthLabel = healthScore >= 80 ? '良好' : healthScore >= 60 ? '需关注' : '风险'; return (
{/* Health Score */} (
{healthScore}
{healthLabel}
)} /> 项目健康度评分 基于质控通过率、待处理 eQuery、重大事件综合计算 质控通过率 {passRate}% 待处理 eQuery 0 ? '#faad14' : '#52c41a' }}>{pendingEq} 活跃重大事件 0 ? '#ff4d4f' : '#52c41a' }}>{criticalCount}
{/* Core Stats Cards */} navigate(`/iit/reports?projectId=${projectId}`)}> } /> navigate(`/iit/equery?projectId=${projectId}`)}> } valueStyle={{ color: pendingEq > 0 ? '#f59e0b' : undefined }} /> 总计 {equeryStats?.total ?? 0} | 已关闭 {equeryStats?.closed ?? 0} } valueStyle={{ color: criticalCount > 0 ? '#ef4444' : undefined }} /> SAE + 重大方案偏离 } /> 通过 {stats?.passedRecords ?? 0} | 失败 {stats?.failedRecords ?? 0} {/* Critical Event Alerts */} {criticalEvents.length > 0 && (
{criticalEvents.map((evt) => ( } message={ {evt.eventType} 受试者 {evt.recordId} {evt.title} } description={ 检出时间: {new Date(evt.detectedAt).toLocaleString('zh-CN')} | 检出方式: {evt.detectedBy === 'ai' ? 'AI 自动' : '手动'} } style={{ marginBottom: 8 }} closable /> ))}
)} {/* Trend Chart (Simple CSS-based) */} 质控通过率趋势(近30天) } style={{ marginBottom: 16 }} > {trend.length > 0 ? (
{trend.map((point, idx) => { const barColor = point.passRate >= 80 ? '#52c41a' : point.passRate >= 60 ? '#faad14' : '#ff4d4f'; return (
); })}
{trend[0]?.date} {trend[trend.length - 1]?.date}
) : ( )} {/* Heatmap */} 风险热力图(受试者 × 表单) } style={{ marginBottom: 16 }} > {heatmap && heatmap.rows?.length > 0 ? (
{(heatmap.columns || []).map((col: string, i: number) => ( ))} {heatmap.rows.map((row: any, ri: number) => ( {(row.cells || []).map((cell: any, ci: number) => { const bg = cell.status === 'pass' ? '#dcfce7' : cell.status === 'warning' ? '#fef3c7' : cell.status === 'fail' ? '#fecaca' : '#f1f5f9'; const border = cell.status === 'pass' ? '#86efac' : cell.status === 'warning' ? '#fcd34d' : cell.status === 'fail' ? '#fca5a5' : '#e2e8f0'; return ( ); })} ))}
受试者 {col}
{row.recordId}
0 ? 700 : 400, color: cell.status === 'fail' ? '#dc2626' : cell.status === 'warning' ? '#d97706' : '#16a34a', }}> {cell.issueCount > 0 ? cell.issueCount : '✓'}
) : ( )}
{/* Quick Links */} navigate(`/iit/stream?projectId=${projectId}`)} style={{ textAlign: 'center' }} >
AI 实时工作流水
查看 Agent 每步推理与操作
navigate(`/iit/equery?projectId=${projectId}`)} style={{ textAlign: 'center' }} >
eQuery 管理 {pendingEq > 0 && }
处理 AI 生成的电子质疑
navigate(`/iit/reports?projectId=${projectId}`)} style={{ textAlign: 'center' }} >
报告与事件
质控报告 + 重大事件归档
); }; export default DashboardPage;