feat(iit): Complete V3.1 QC engine + GCP business reports + AI timeline + bug fixes
V3.1 QC Engine: - QcExecutor unified entry + D1-D7 dimension engines + three-level aggregation - HealthScoreEngine + CompletenessEngine + ProtocolDeviationEngine + QcAggregator - B4 flexible cron scheduling (project-level cronExpression + pg-boss dispatcher) - Prisma migrations for qc_field_status, event_status, project_stats GCP Business Reports (Phase A - 4 reports): - D1 Eligibility: record_summary full list + qc_field_status D1 overlay - D2 Completeness: data entry rate and missing rate aggregation - D3/D4 Query Tracking: severity distribution from qc_field_status - D6 Protocol Deviation: D6 dimension filtering - 4 frontend table components + ReportsPage 5-tab restructure AI Timeline Enhancement: - SkillRunner outputs totalRules (33 actual rules vs 1 skill) - iitQcCockpitController severity mapping fix (critical->red, warning->yellow) - AiStreamPage expandable issue detail table with Chinese labels - Event label localization (eventLabel from backend) Business-side One-click Batch QC: - DashboardPage batch QC button with SyncOutlined icon - Auto-refresh QcReport cache after batch execution Bug Fixes: - dimension_code -> rule_category in 4 SQL queries - D1 eligibility data source: record_summary full + qc_field_status overlay - Timezone UTC -> Asia/Shanghai (QcReportService toBeijingTime helper) - Pass rate calculation: passed/totalEvents instead of passed/totalRecords Docs: - Update IIT module status with GCP reports and bug fix milestones - Update system status doc v6.6 with IIT progress Tested: Backend compiles, frontend linter clean, batch QC verified Made-with: Cursor
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* AI 实时工作流水页 (Level 2)
|
||||
*
|
||||
* 以 Timeline 展示 Agent 每次质控的完整动作链,AI 白盒化。
|
||||
* 显示中文事件名、实际规则数、五层定位详情、最终判定状态。
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
Button,
|
||||
Badge,
|
||||
Pagination,
|
||||
Collapse,
|
||||
Table,
|
||||
} from 'antd';
|
||||
import {
|
||||
ThunderboltOutlined,
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
RobotOutlined,
|
||||
ApiOutlined,
|
||||
BellOutlined,
|
||||
FileSearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import * as iitProjectApi from '../api/iitProjectApi';
|
||||
import type { TimelineItem } from '../api/iitProjectApi';
|
||||
@@ -34,10 +38,10 @@ import { useIitProject } from '../context/IitProjectContext';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const STATUS_DOT: Record<string, { color: string; icon: React.ReactNode }> = {
|
||||
PASS: { color: 'green', icon: <CheckCircleOutlined /> },
|
||||
FAIL: { color: 'red', icon: <CloseCircleOutlined /> },
|
||||
WARNING: { color: 'orange', icon: <WarningOutlined /> },
|
||||
const STATUS_DOT: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
|
||||
PASS: { color: 'green', icon: <CheckCircleOutlined />, label: '通过' },
|
||||
FAIL: { color: 'red', icon: <CloseCircleOutlined />, label: '严重' },
|
||||
WARNING: { color: 'orange', icon: <WarningOutlined />, label: '警告' },
|
||||
};
|
||||
|
||||
const TRIGGER_TAG: Record<string, { color: string; label: string }> = {
|
||||
@@ -82,19 +86,52 @@ const AiStreamPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const timelineItems = items.map((item) => {
|
||||
const dotCfg = STATUS_DOT[item.status] || { color: 'gray', icon: <ClockCircleOutlined /> };
|
||||
const dotCfg = STATUS_DOT[item.status] || { color: 'gray', icon: <ClockCircleOutlined />, 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) => (
|
||||
<Space size={4}>
|
||||
<Text>{v || r.ruleId}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: '字段', dataIndex: 'field', width: 110, render: (v: string) => v ? <Text code>{v}</Text> : '—' },
|
||||
{ title: '问题描述', dataIndex: 'message', ellipsis: true },
|
||||
{
|
||||
title: '严重度',
|
||||
dataIndex: 'severity',
|
||||
width: 80,
|
||||
render: (s: string) => (
|
||||
<Tag color={s === 'critical' ? 'error' : 'warning'}>
|
||||
{s === 'critical' ? '严重' : '警告'}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '实际值',
|
||||
dataIndex: 'actualValue',
|
||||
width: 90,
|
||||
render: (v: string) => v ?? '—',
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
color: dotCfg.color as any,
|
||||
dot: dotCfg.icon,
|
||||
children: (
|
||||
<div style={{ paddingBottom: 8 }}>
|
||||
{/* Header line */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
||||
<Text strong style={{ fontSize: 13, fontFamily: 'monospace' }}>{timeStr}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>{dateStr}</Text>
|
||||
@@ -102,11 +139,10 @@ const AiStreamPage: React.FC = () => {
|
||||
{triggerCfg.label}
|
||||
</Tag>
|
||||
<Tag color={dotCfg.color} style={{ fontSize: 10, lineHeight: '18px', padding: '0 6px' }}>
|
||||
{item.status}
|
||||
{dotCfg.label}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
{/* Description — the AI action chain */}
|
||||
<div style={{
|
||||
background: '#f8fafc',
|
||||
borderRadius: 8,
|
||||
@@ -118,18 +154,20 @@ const AiStreamPage: React.FC = () => {
|
||||
<Space wrap size={4} style={{ marginBottom: 4 }}>
|
||||
<RobotOutlined style={{ color: '#3b82f6' }} />
|
||||
<Text>扫描受试者 <Text code>{item.recordId}</Text></Text>
|
||||
{item.formName && <Text type="secondary">[{item.formName}]</Text>}
|
||||
{eventLabel && <Tag color="geekblue">{eventLabel}</Tag>}
|
||||
</Space>
|
||||
|
||||
<div style={{ marginLeft: 20 }}>
|
||||
<Space size={4}>
|
||||
<ApiOutlined style={{ color: '#8b5cf6' }} />
|
||||
<Text>执行 {item.details.rulesEvaluated} 条规则</Text>
|
||||
<Text>执行 <Text strong>{item.details.rulesEvaluated}</Text> 条规则</Text>
|
||||
<Text type="success">→ {item.details.rulesPassed} 通过</Text>
|
||||
{item.details.rulesFailed > 0 && (
|
||||
<Text type="danger">/ {item.details.rulesFailed} 失败</Text>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{(red > 0 || yellow > 0) && (
|
||||
<div style={{ marginLeft: 20, marginTop: 2 }}>
|
||||
<Space size={4}>
|
||||
@@ -141,6 +179,40 @@ const AiStreamPage: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{issues.length > 0 && (
|
||||
<Collapse
|
||||
ghost
|
||||
size="small"
|
||||
style={{ marginTop: 4 }}
|
||||
items={[{
|
||||
key: '1',
|
||||
label: (
|
||||
<Space size={4}>
|
||||
<FileSearchOutlined style={{ color: '#64748b' }} />
|
||||
<Text type="secondary">查看 {issues.length} 条问题详情</Text>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Table
|
||||
dataSource={issues}
|
||||
rowKey={(_, i) => `issue-${i}`}
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={issueColumns}
|
||||
/>
|
||||
),
|
||||
}]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{issues.length === 0 && item.status === 'PASS' && (
|
||||
<div style={{ marginLeft: 20, marginTop: 2 }}>
|
||||
<Text type="success" style={{ fontSize: 12 }}>
|
||||
<CheckCircleOutlined /> 所有规则检查通过,数据质量合格
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
@@ -149,7 +221,6 @@ const AiStreamPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<Tag icon={<ThunderboltOutlined />} color="processing">实时</Tag>
|
||||
@@ -172,7 +243,6 @@ const AiStreamPage: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<Card>
|
||||
{items.length > 0 ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user