/** * AI 实时工作流水页 (Level 2) * * 以 Timeline 展示 Agent 每次质控的完整动作链,AI 白盒化。 * 显示中文事件名、实际规则数、五层定位详情、最终判定状态。 */ import React, { useState, useEffect, useCallback } from 'react'; import { Card, Timeline, Empty, Tag, Typography, Space, DatePicker, Button, Badge, Pagination, Collapse, Table, } from 'antd'; import { ThunderboltOutlined, CheckCircleOutlined, CloseCircleOutlined, WarningOutlined, SyncOutlined, ClockCircleOutlined, RobotOutlined, ApiOutlined, BellOutlined, FileSearchOutlined, } from '@ant-design/icons'; import * as iitProjectApi from '../api/iitProjectApi'; import type { TimelineItem } from '../api/iitProjectApi'; import { useIitProject } from '../context/IitProjectContext'; const { Text } = Typography; const STATUS_DOT: Record = { PASS: { color: 'green', icon: , label: '通过' }, FAIL: { color: 'red', icon: , label: '严重' }, WARNING: { color: 'orange', icon: , label: '警告' }, }; const TRIGGER_TAG: Record = { webhook: { color: 'blue', label: 'EDC 触发' }, cron: { color: 'purple', label: '定时巡查' }, manual: { color: 'cyan', label: '手动执行' }, batch: { color: 'geekblue', label: '批量质控' }, }; const AiStreamPage: React.FC = () => { const { projectId } = useIitProject(); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [dateFilter, setDateFilter] = useState(undefined); const [loading, setLoading] = useState(false); const fetchData = useCallback(async () => { if (!projectId) return; setLoading(true); try { const result = await iitProjectApi.getTimeline(projectId, { page, pageSize: 30, date: dateFilter, }); setItems(result.items); setTotal(result.total); } catch { setItems([]); } finally { setLoading(false); } }, [projectId, page, dateFilter]); useEffect(() => { fetchData(); }, [fetchData]); const handleRefresh = () => { setPage(1); fetchData(); }; const timelineItems = items.map((item) => { const dotCfg = STATUS_DOT[item.status] || { color: 'gray', icon: , label: '未知' }; const triggerCfg = TRIGGER_TAG[item.triggeredBy] || { color: 'default', label: item.triggeredBy }; const { red, yellow } = item.details.issuesSummary; const time = new Date(item.time); const timeStr = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); const dateStr = time.toLocaleDateString('zh-CN'); const eventLabel = item.eventLabel || ''; const issues = item.details.issues || []; const issueColumns = [ { title: '规则', dataIndex: 'ruleName', width: 160, render: (v: string, r: any) => ( {v || r.ruleId} ), }, { title: '字段', dataIndex: 'field', width: 110, render: (v: string) => v ? {v} : '—' }, { title: '问题描述', dataIndex: 'message', ellipsis: true }, { title: '严重度', dataIndex: 'severity', width: 80, render: (s: string) => ( {s === 'critical' ? '严重' : '警告'} ), }, { title: '实际值', dataIndex: 'actualValue', width: 90, render: (v: string) => v ?? '—', }, ]; return { color: dotCfg.color as any, dot: dotCfg.icon, children: (
{timeStr} {dateStr} {triggerCfg.label} {dotCfg.label}
扫描受试者 {item.recordId} {eventLabel && {eventLabel}}
执行 {item.details.rulesEvaluated} 条规则 → {item.details.rulesPassed} 通过 {item.details.rulesFailed > 0 && ( / {item.details.rulesFailed} 失败 )}
{(red > 0 || yellow > 0) && (
0 ? '#ef4444' : '#f59e0b' }} /> {red > 0 && } {red > 0 && 严重问题} {yellow > 0 && } {yellow > 0 && 警告}
)} {issues.length > 0 && ( 查看 {issues.length} 条问题详情 ), children: ( `issue-${i}`} size="small" pagination={false} columns={issueColumns} /> ), }]} /> )} {issues.length === 0 && item.status === 'PASS' && (
所有规则检查通过,数据质量合格
)} ), }; }); return (
} color="processing">实时 条工作记录 { setDateFilter(d ? d.format('YYYY-MM-DD') : undefined); setPage(1); }} allowClear />
{items.length > 0 ? ( <>
`共 ${t} 条`} size="small" />
) : ( )}
); }; export default AiStreamPage;