feat(iit): Implement event-level QC architecture V3.1 with dynamic rule filtering, report deduplication and AI intent enhancement

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 21:22:11 +08:00
parent 45c7b32dbb
commit 7a299e8562
51 changed files with 10638 additions and 184 deletions

View File

@@ -0,0 +1,149 @@
/**
* 风险热力图组件
*
* 展示受试者 × 表单/访视 矩阵
* - 绿色圆点:通过
* - 黄色图标:警告(可点击)
* - 红色图标:严重(可点击)
* - 灰色圆点:未开始
*/
import React from 'react';
import { Spin, Tag, Tooltip } from 'antd';
import {
CheckOutlined,
ExclamationOutlined,
CloseOutlined,
} from '@ant-design/icons';
import type { HeatmapData, HeatmapCell } from '../../types/qcCockpit';
interface RiskHeatmapProps {
data: HeatmapData;
onCellClick: (cell: HeatmapCell) => void;
loading?: boolean;
}
const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading }) => {
const getStatusTag = (status: string) => {
switch (status) {
case 'enrolled':
return <Tag color="cyan"></Tag>;
case 'screening':
return <Tag color="blue"></Tag>;
case 'completed':
return <Tag color="green"></Tag>;
case 'withdrawn':
return <Tag color="default">退</Tag>;
default:
return <Tag>{status}</Tag>;
}
};
const renderCell = (cell: HeatmapCell) => {
const hasIssues = cell.status === 'warning' || cell.status === 'fail';
// ✅ 所有单元格都可点击,便于查看详情
const handleClick = () => onCellClick(cell);
// 有问题的单元格:显示可点击图标
if (hasIssues) {
const iconClass = `qc-heatmap-cell-icon ${cell.status}`;
const icon = cell.status === 'fail'
? <CloseOutlined />
: <ExclamationOutlined />;
return (
<Tooltip title={`${cell.issueCount} 个问题,点击查看详情`}>
<div className={iconClass} onClick={handleClick} style={{ cursor: 'pointer' }}>
{icon}
</div>
</Tooltip>
);
}
// 通过或待检查:显示小圆点(也可点击)
const dotClass = `qc-heatmap-cell-dot ${cell.status === 'pass' ? 'pass' : 'pending'}`;
const tooltipText = cell.status === 'pass'
? '已通过,点击查看数据'
: '未质控,点击查看数据';
return (
<Tooltip title={tooltipText}>
<div
className={dotClass}
onClick={handleClick}
style={{
backgroundColor: cell.status === 'pass' ? '#52c41a' : '#d9d9d9',
cursor: 'pointer',
}}
/>
</Tooltip>
);
};
return (
<div className="qc-heatmap">
{/* 头部 */}
<div className="qc-heatmap-header">
<h3 className="qc-heatmap-title"> (Risk Heatmap)</h3>
<div className="qc-heatmap-legend">
<span className="qc-heatmap-legend-item">
<span className="qc-heatmap-legend-dot pass" />
</span>
<span className="qc-heatmap-legend-item">
<span className="qc-heatmap-legend-dot warning" />
(Query)
</span>
<span className="qc-heatmap-legend-item">
<span className="qc-heatmap-legend-dot fail" />
</span>
<span className="qc-heatmap-legend-item">
<span className="qc-heatmap-legend-dot pending" />
</span>
</div>
</div>
{/* 表格内容 */}
<div className="qc-heatmap-body">
<Spin spinning={loading}>
<table className="qc-heatmap-table">
<thead>
<tr>
<th className="subject-header"> ID</th>
<th></th>
{data.columns.map((col, idx) => (
<th key={idx}>
{col.split('(')[0]}<br />
<small style={{ fontWeight: 'normal' }}>
{col.includes('(') ? `(${col.split('(')[1]}` : ''}
</small>
</th>
))}
</tr>
</thead>
<tbody>
{data.rows.map((row, rowIdx) => (
<tr key={rowIdx}>
<td className="subject-cell">{row.recordId}</td>
<td>{getStatusTag(row.status)}</td>
{row.cells.map((cell, cellIdx) => (
<td key={cellIdx}>
<div className="qc-heatmap-cell">
{renderCell(cell)}
</div>
</td>
))}
</tr>
))}
</tbody>
</table>
</Spin>
</div>
</div>
);
};
export default RiskHeatmap;