feat(iit): V3.2 data consistency + project isolation + admin config redesign + Chinese labels
Summary: - Refactor timeline API to read from qc_field_status (SSOT) instead of qc_logs - Add field-issues paginated API with severity/dimension/recordId filters - Add LEFT JOIN field_metadata + qc_event_status for Chinese display names - Implement per-project ChatOrchestrator cache and SessionMemory isolation - Redesign admin IIT config tabs (REDCap -> Fields -> KB -> Rules -> Members) - Add AI-powered QC rule generation (D3 programmatic + D1/D5/D6 LLM-based) - Add clickable warning/critical detail Modal in ReportsPage - Auto-dispatch eQuery after batch QC via DailyQcOrchestrator - Update module status documentation to v3.2 Backend changes: - iitQcCockpitController: rewrite getTimeline from qc_field_status, add getFieldIssues - iitQcCockpitRoutes: add field-issues route - ChatOrchestrator: per-projectId cached instances - SessionMemory: keyed by userId::projectId - WechatCallbackController: resolve projectId from iitUserMapping - iitRuleSuggestionService: dimension-based suggest + generateD3Rules - iitBatchController: call DailyQcOrchestrator after batch QC Frontend changes: - AiStreamPage: adapt to new timeline structure with dimension tags - ReportsPage: clickable stats cards with issue detail Modal - IitProjectDetailPage: reorder tabs, add AI rule generation UI - iitProjectApi: add TimelineIssue, FieldIssueItem types and APIs Status: TypeScript compilation verified, no new lint errors Made-with: Cursor
This commit is contained in:
@@ -423,30 +423,32 @@ export async function getQcRecordDetail(
|
||||
|
||||
// ==================== AI 时间线 ====================
|
||||
|
||||
export interface TimelineIssue {
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
ruleCategory?: string;
|
||||
field?: string;
|
||||
fieldLabel?: string;
|
||||
eventId?: string;
|
||||
eventLabel?: string;
|
||||
formName?: string;
|
||||
message: string;
|
||||
severity: string;
|
||||
actualValue?: string;
|
||||
expectedValue?: string;
|
||||
}
|
||||
|
||||
export interface TimelineItem {
|
||||
id: string;
|
||||
type: 'qc_check';
|
||||
time: string;
|
||||
recordId: string;
|
||||
eventLabel?: string;
|
||||
formName?: string;
|
||||
status: string;
|
||||
triggeredBy: string;
|
||||
description: string;
|
||||
details: {
|
||||
rulesEvaluated: number;
|
||||
rulesPassed: number;
|
||||
rulesFailed: number;
|
||||
issuesSummary: { red: number; yellow: number };
|
||||
issues?: Array<{
|
||||
ruleId: string;
|
||||
ruleName: string;
|
||||
field?: string;
|
||||
message: string;
|
||||
severity: string;
|
||||
actualValue?: string;
|
||||
expectedValue?: string;
|
||||
}>;
|
||||
issues: TimelineIssue[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -459,6 +461,49 @@ export async function getTimeline(
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
// ==================== 字段级问题查询 ====================
|
||||
|
||||
export interface FieldIssueItem {
|
||||
id: string;
|
||||
recordId: string;
|
||||
eventId: string;
|
||||
eventLabel?: string;
|
||||
formName: string;
|
||||
fieldName: string;
|
||||
fieldLabel?: string;
|
||||
ruleCategory: string;
|
||||
ruleName: string;
|
||||
ruleId: string;
|
||||
severity: string;
|
||||
status: string;
|
||||
message: string;
|
||||
actualValue?: string;
|
||||
expectedValue?: string;
|
||||
lastQcAt: string;
|
||||
}
|
||||
|
||||
export interface FieldIssuesSummary {
|
||||
totalIssues: number;
|
||||
bySeverity: Record<string, number>;
|
||||
byDimension: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface FieldIssuesResponse {
|
||||
items: FieldIssueItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
summary: FieldIssuesSummary;
|
||||
}
|
||||
|
||||
export async function getFieldIssues(
|
||||
projectId: string,
|
||||
params?: { page?: number; pageSize?: number; severity?: string; dimension?: string; recordId?: string }
|
||||
): Promise<FieldIssuesResponse> {
|
||||
const response = await apiClient.get(`${BASE_URL}/${projectId}/qc-cockpit/field-issues`, { params });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
// ==================== 重大事件 ====================
|
||||
|
||||
export interface CriticalEvent {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* AI 实时工作流水页 (Level 2)
|
||||
*
|
||||
* 以 Timeline 展示 Agent 每次质控的完整动作链,AI 白盒化。
|
||||
* 显示中文事件名、实际规则数、五层定位详情、最终判定状态。
|
||||
* 以 Timeline 展示 Agent 质控结果,数据来源: qc_field_status (SSOT)。
|
||||
* 按受试者分组展示问题详情,支持按维度分组查看。
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
@@ -28,16 +28,20 @@ import {
|
||||
SyncOutlined,
|
||||
ClockCircleOutlined,
|
||||
RobotOutlined,
|
||||
ApiOutlined,
|
||||
BellOutlined,
|
||||
FileSearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import * as iitProjectApi from '../api/iitProjectApi';
|
||||
import type { TimelineItem } from '../api/iitProjectApi';
|
||||
import type { TimelineItem, TimelineIssue } from '../api/iitProjectApi';
|
||||
import { useIitProject } from '../context/IitProjectContext';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const DIMENSION_LABELS: Record<string, string> = {
|
||||
D1: '入选/排除', D2: '完整性', D3: '准确性', D4: '质疑管理',
|
||||
D5: '安全性', D6: '方案偏离', D7: '药物管理',
|
||||
};
|
||||
|
||||
const STATUS_DOT: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
|
||||
PASS: { color: 'green', icon: <CheckCircleOutlined />, label: '通过' },
|
||||
FAIL: { color: 'red', icon: <CloseCircleOutlined />, label: '严重' },
|
||||
@@ -66,7 +70,7 @@ const AiStreamPage: React.FC = () => {
|
||||
try {
|
||||
const result = await iitProjectApi.getTimeline(projectId, {
|
||||
page,
|
||||
pageSize: 30,
|
||||
pageSize: 20,
|
||||
date: dateFilter,
|
||||
});
|
||||
setItems(result.items);
|
||||
@@ -85,6 +89,59 @@ const AiStreamPage: React.FC = () => {
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const issueColumns = [
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'ruleCategory',
|
||||
width: 90,
|
||||
render: (v: string) => {
|
||||
const label = DIMENSION_LABELS[v] || v;
|
||||
return <Tag color="blue">{v ? `${v} ${label}` : '—'}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
dataIndex: 'ruleName',
|
||||
width: 160,
|
||||
render: (v: string, r: TimelineIssue) => <Text>{v || r.ruleId || '—'}</Text>,
|
||||
},
|
||||
{
|
||||
title: '字段',
|
||||
dataIndex: 'field',
|
||||
width: 140,
|
||||
render: (v: string, r: TimelineIssue) => {
|
||||
const label = r.fieldLabel || v;
|
||||
return label ? <Text>{label}</Text> : '—';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '事件',
|
||||
dataIndex: 'eventId',
|
||||
width: 140,
|
||||
render: (v: string, r: TimelineIssue) => {
|
||||
const label = r.eventLabel || v;
|
||||
return label || '—';
|
||||
},
|
||||
},
|
||||
{ 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 ?? '—',
|
||||
},
|
||||
];
|
||||
|
||||
const timelineItems = items.map((item) => {
|
||||
const dotCfg = STATUS_DOT[item.status] || { color: 'gray', icon: <ClockCircleOutlined />, label: '未知' };
|
||||
const triggerCfg = TRIGGER_TAG[item.triggeredBy] || { color: 'default', label: item.triggeredBy };
|
||||
@@ -93,42 +150,18 @@ const AiStreamPage: React.FC = () => {
|
||||
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 ?? '—',
|
||||
},
|
||||
];
|
||||
// 按维度分组
|
||||
const groupedByDimension = issues.reduce<Record<string, TimelineIssue[]>>((acc, iss) => {
|
||||
const key = iss.ruleCategory || '其他';
|
||||
if (!acc[key]) acc[key] = [];
|
||||
acc[key].push(iss);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
color: dotCfg.color as any,
|
||||
color: dotCfg.color as string,
|
||||
dot: dotCfg.icon,
|
||||
children: (
|
||||
<div style={{ paddingBottom: 8 }}>
|
||||
@@ -153,29 +186,20 @@ const AiStreamPage: React.FC = () => {
|
||||
}}>
|
||||
<Space wrap size={4} style={{ marginBottom: 4 }}>
|
||||
<RobotOutlined style={{ color: '#3b82f6' }} />
|
||||
<Text>扫描受试者 <Text code>{item.recordId}</Text></Text>
|
||||
{eventLabel && <Tag color="geekblue">{eventLabel}</Tag>}
|
||||
<Text>受试者 <Text code>{item.recordId}</Text></Text>
|
||||
</Space>
|
||||
|
||||
<div style={{ marginLeft: 20 }}>
|
||||
<Space size={4}>
|
||||
<ApiOutlined style={{ color: '#8b5cf6' }} />
|
||||
<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}>
|
||||
<Space size={8}>
|
||||
<BellOutlined style={{ color: red > 0 ? '#ef4444' : '#f59e0b' }} />
|
||||
{red > 0 && <Badge count={red} style={{ backgroundColor: '#ef4444' }} />}
|
||||
{red > 0 && <Text type="danger">严重问题</Text>}
|
||||
{yellow > 0 && <Badge count={yellow} style={{ backgroundColor: '#f59e0b' }} />}
|
||||
{yellow > 0 && <Text style={{ color: '#d97706' }}>警告</Text>}
|
||||
{red > 0 && <><Badge count={red} style={{ backgroundColor: '#ef4444' }} /><Text type="danger">严重问题</Text></>}
|
||||
{yellow > 0 && <><Badge count={yellow} style={{ backgroundColor: '#f59e0b' }} /><Text style={{ color: '#d97706' }}>警告</Text></>}
|
||||
{Object.entries(groupedByDimension).map(([dim, dimIssues]) => (
|
||||
<Tag key={dim} color="processing" style={{ fontSize: 10 }}>
|
||||
{dim} {DIMENSION_LABELS[dim] || ''}: {dimIssues.length}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
@@ -200,6 +224,7 @@ const AiStreamPage: React.FC = () => {
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={issueColumns}
|
||||
scroll={{ x: 800 }}
|
||||
/>
|
||||
),
|
||||
}]}
|
||||
@@ -225,7 +250,7 @@ const AiStreamPage: React.FC = () => {
|
||||
<Space>
|
||||
<Tag icon={<ThunderboltOutlined />} color="processing">实时</Tag>
|
||||
<Badge count={total} overflowCount={9999} style={{ backgroundColor: '#3b82f6' }}>
|
||||
<Text type="secondary">条工作记录</Text>
|
||||
<Text type="secondary">位受试者</Text>
|
||||
</Badge>
|
||||
</Space>
|
||||
<Space>
|
||||
@@ -251,9 +276,9 @@ const AiStreamPage: React.FC = () => {
|
||||
<Pagination
|
||||
current={page}
|
||||
total={total}
|
||||
pageSize={30}
|
||||
pageSize={20}
|
||||
onChange={setPage}
|
||||
showTotal={(t) => `共 ${t} 条`}
|
||||
showTotal={(t) => `共 ${t} 位受试者`}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
Select,
|
||||
Badge,
|
||||
Spin,
|
||||
Modal,
|
||||
Pagination,
|
||||
} from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
@@ -38,9 +40,10 @@ import {
|
||||
DatabaseOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ExceptionOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import * as iitProjectApi from '../api/iitProjectApi';
|
||||
import type { QcReport, CriticalEvent } from '../api/iitProjectApi';
|
||||
import type { QcReport, CriticalEvent, FieldIssueItem, FieldIssuesSummary } from '../api/iitProjectApi';
|
||||
import { useIitProject } from '../context/IitProjectContext';
|
||||
|
||||
const EligibilityTable = lazy(() => import('../components/reports/EligibilityTable'));
|
||||
@@ -62,6 +65,16 @@ const ReportsPage: React.FC = () => {
|
||||
const [ceTotal, setCeTotal] = useState(0);
|
||||
const [ceStatusFilter, setCeStatusFilter] = useState<string | undefined>(undefined);
|
||||
|
||||
// 问题详情 Modal
|
||||
const [issueModalOpen, setIssueModalOpen] = useState(false);
|
||||
const [issueModalSeverity, setIssueModalSeverity] = useState<string | undefined>(undefined);
|
||||
const [issueModalDimension, setIssueModalDimension] = useState<string | undefined>(undefined);
|
||||
const [issueItems, setIssueItems] = useState<FieldIssueItem[]>([]);
|
||||
const [issueTotal, setIssueTotal] = useState(0);
|
||||
const [issuePage, setIssuePage] = useState(1);
|
||||
const [issueSummary, setIssueSummary] = useState<FieldIssuesSummary | null>(null);
|
||||
const [issueLoading, setIssueLoading] = useState(false);
|
||||
|
||||
const fetchReport = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
setLoading(true);
|
||||
@@ -82,6 +95,34 @@ const ReportsPage: React.FC = () => {
|
||||
|
||||
useEffect(() => { fetchReport(); }, [fetchReport]);
|
||||
|
||||
const fetchIssues = useCallback(async (severity?: string, dimension?: string, pg = 1) => {
|
||||
if (!projectId) return;
|
||||
setIssueLoading(true);
|
||||
try {
|
||||
const data = await iitProjectApi.getFieldIssues(projectId, {
|
||||
page: pg,
|
||||
pageSize: 20,
|
||||
severity,
|
||||
dimension,
|
||||
});
|
||||
setIssueItems(data.items);
|
||||
setIssueTotal(data.total);
|
||||
setIssueSummary(data.summary);
|
||||
} catch {
|
||||
setIssueItems([]);
|
||||
} finally {
|
||||
setIssueLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
const openIssueModal = (severity?: string) => {
|
||||
setIssueModalSeverity(severity);
|
||||
setIssueModalDimension(undefined);
|
||||
setIssuePage(1);
|
||||
setIssueModalOpen(true);
|
||||
fetchIssues(severity, undefined, 1);
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (!projectId) return;
|
||||
setRefreshing(true);
|
||||
@@ -117,8 +158,12 @@ const ReportsPage: React.FC = () => {
|
||||
<Col span={4}><Card size="small">
|
||||
<Statistic title="通过率" value={summary.passRate} suffix="%" valueStyle={{ color: summary.passRate >= 80 ? '#52c41a' : summary.passRate >= 60 ? '#faad14' : '#ff4d4f' }} />
|
||||
</Card></Col>
|
||||
<Col span={4}><Card size="small"><Statistic title="严重问题" value={summary.criticalIssues} prefix={<WarningOutlined />} valueStyle={{ color: '#ff4d4f' }} /></Card></Col>
|
||||
<Col span={4}><Card size="small"><Statistic title="警告" value={summary.warningIssues} prefix={<AlertOutlined />} valueStyle={{ color: '#faad14' }} /></Card></Col>
|
||||
<Col span={4}><Card size="small" hoverable onClick={() => openIssueModal('critical')} style={{ cursor: 'pointer' }}>
|
||||
<Statistic title={<Space size={4}>严重问题 <EyeOutlined style={{ fontSize: 12, color: '#999' }} /></Space>} value={summary.criticalIssues} prefix={<WarningOutlined />} valueStyle={{ color: '#ff4d4f' }} />
|
||||
</Card></Col>
|
||||
<Col span={4}><Card size="small" hoverable onClick={() => openIssueModal('warning')} style={{ cursor: 'pointer' }}>
|
||||
<Statistic title={<Space size={4}>警告 <EyeOutlined style={{ fontSize: 12, color: '#999' }} /></Space>} value={summary.warningIssues} prefix={<AlertOutlined />} valueStyle={{ color: '#faad14' }} />
|
||||
</Card></Col>
|
||||
<Col span={4}><Card size="small"><Statistic title="待处理 Query" value={summary.pendingQueries} /></Card></Col>
|
||||
</Row>
|
||||
|
||||
@@ -223,6 +268,117 @@ const ReportsPage: React.FC = () => {
|
||||
|
||||
const lazyFallback = <Spin style={{ display: 'block', margin: '40px auto' }} />;
|
||||
|
||||
const DIMENSION_LABELS: Record<string, string> = {
|
||||
D1: '入选/排除', D2: '完整性', D3: '准确性', D4: '质疑管理',
|
||||
D5: '安全性', D6: '方案偏离', D7: '药物管理',
|
||||
};
|
||||
|
||||
const issueDetailColumns = [
|
||||
{ title: '受试者', dataIndex: 'recordId', width: 80, render: (v: string) => <Text strong>{v}</Text> },
|
||||
{
|
||||
title: '事件',
|
||||
dataIndex: 'eventLabel',
|
||||
width: 130,
|
||||
render: (v: string, r: FieldIssueItem) => v || r.eventId || '—',
|
||||
},
|
||||
{
|
||||
title: '字段',
|
||||
dataIndex: 'fieldLabel',
|
||||
width: 130,
|
||||
render: (v: string, r: FieldIssueItem) => (v || r.fieldName) ? <Text>{v || r.fieldName}</Text> : '—',
|
||||
},
|
||||
{
|
||||
title: '维度',
|
||||
dataIndex: 'ruleCategory',
|
||||
width: 100,
|
||||
render: (v: string) => <Tag color="blue">{v ? `${v} ${DIMENSION_LABELS[v] || ''}` : '—'}</Tag>,
|
||||
},
|
||||
{ title: '规则', dataIndex: 'ruleName', width: 140, ellipsis: true },
|
||||
{ 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 ?? '—' },
|
||||
{ title: '检出时间', dataIndex: 'lastQcAt', width: 150, render: (d: string) => d ? new Date(d).toLocaleString('zh-CN') : '—' },
|
||||
];
|
||||
|
||||
const issueModal = (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
{issueModalSeverity === 'critical' ? <WarningOutlined style={{ color: '#ff4d4f' }} /> : <AlertOutlined style={{ color: '#faad14' }} />}
|
||||
{issueModalSeverity === 'critical' ? '严重问题详情' : issueModalSeverity === 'warning' ? '警告详情' : '所有问题详情'}
|
||||
<Badge count={issueTotal} style={{ backgroundColor: issueModalSeverity === 'critical' ? '#ff4d4f' : '#faad14' }} />
|
||||
</Space>
|
||||
}
|
||||
open={issueModalOpen}
|
||||
onCancel={() => setIssueModalOpen(false)}
|
||||
width={1100}
|
||||
footer={null}
|
||||
>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Space wrap>
|
||||
<Select
|
||||
placeholder="按维度筛选"
|
||||
allowClear
|
||||
style={{ width: 160 }}
|
||||
value={issueModalDimension}
|
||||
onChange={(val) => {
|
||||
setIssueModalDimension(val);
|
||||
setIssuePage(1);
|
||||
fetchIssues(issueModalSeverity, val, 1);
|
||||
}}
|
||||
options={
|
||||
issueSummary
|
||||
? Object.entries(issueSummary.byDimension).map(([dim, cnt]) => ({
|
||||
value: dim,
|
||||
label: `${dim} ${DIMENSION_LABELS[dim] || ''} (${cnt})`,
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
{issueSummary && (
|
||||
<Space size={12}>
|
||||
<Text type="secondary">
|
||||
严重: <Text type="danger" strong>{issueSummary.bySeverity.critical || 0}</Text>
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
警告: <Text style={{ color: '#faad14' }} strong>{issueSummary.bySeverity.warning || 0}</Text>
|
||||
</Text>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
dataSource={issueItems}
|
||||
rowKey="id"
|
||||
columns={issueDetailColumns}
|
||||
size="small"
|
||||
loading={issueLoading}
|
||||
pagination={false}
|
||||
scroll={{ x: 1000, y: 400 }}
|
||||
/>
|
||||
|
||||
<div style={{ textAlign: 'center', marginTop: 12 }}>
|
||||
<Pagination
|
||||
current={issuePage}
|
||||
total={issueTotal}
|
||||
pageSize={20}
|
||||
onChange={(pg) => {
|
||||
setIssuePage(pg);
|
||||
fetchIssues(issueModalSeverity, issueModalDimension, pg);
|
||||
}}
|
||||
showTotal={(t) => `共 ${t} 条`}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
@@ -282,6 +438,7 @@ const ReportsPage: React.FC = () => {
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
{issueModal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user