/** * 全文复筛 - 结果页面 * * 功能: * 1. 统计概览卡片(总数/纳入/排除/待复核) * 2. PRISMA式排除原因统计 * 3. Tab切换结果列表 * 4. Excel导出(前端生成) */ import { useState } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { Card, Statistic, Row, Col, Tabs, Table, Button, Alert, Progress, message, Tooltip, Empty, Spin, Tag, Space, } from 'antd'; import type { TableColumnsType } from 'antd'; import { DownloadOutlined, CheckCircleOutlined, CloseCircleOutlined, QuestionCircleOutlined, WarningOutlined, FileExcelOutlined, } from '@ant-design/icons'; import { useQuery } from '@tanstack/react-query'; import ConclusionTag from '../components/ConclusionTag'; // 结果类型 interface FulltextResultItem { resultId: string; literature: { id: string; pmid?: string; title: string; authors?: string; journal?: string; year?: number; }; aiConsensus: 'agree_include' | 'agree_exclude' | 'conflict'; fieldsPassRate: string; // "10/12" exclusionReason?: string; finalDecision: 'include' | 'exclude' | null; reviewStatus: 'pending' | 'reviewed' | 'conflict'; } const FulltextResults = () => { const { taskId } = useParams<{ taskId: string }>(); const [searchParams] = useSearchParams(); const projectId = searchParams.get('projectId') || ''; const [activeTab, setActiveTab] = useState<'all' | 'included' | 'excluded' | 'pending'>('all'); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [expandedRowKeys, setExpandedRowKeys] = useState([]); // 获取统计数据 const { data: statsData, isLoading: statsLoading } = useQuery({ queryKey: ['fulltextResultsStats', taskId], queryFn: async () => { // TODO: 调用API return { total: 50, included: 35, excluded: 10, pending: 5, conflict: 3, includedRate: 70, excludedRate: 20, pendingRate: 10, exclusionReasons: { 'P不匹配(人群)': 2, 'I不匹配(干预)': 1, 'S不匹配(研究设计)': 3, '12字段不完整': 4, }, }; }, enabled: !!taskId, }); // 获取结果列表 const { data: resultsData, isLoading: resultsLoading } = useQuery({ queryKey: ['fulltextResultsList', taskId, activeTab], queryFn: async () => { // TODO: 调用API return { items: [] as FulltextResultItem[], total: 0, }; }, enabled: !!taskId, }); const stats = statsData; const results = resultsData?.items || []; // 导出Excel const handleExport = async (filter: 'all' | 'included' | 'excluded' | 'pending') => { try { message.loading({ content: '正在生成Excel...', key: 'export' }); // TODO: 前端生成Excel或调用后端API await new Promise(resolve => setTimeout(resolve, 1000)); message.success({ content: '导出成功', key: 'export' }); } catch (error) { message.error('导出失败: ' + (error as Error).message); } }; // 导出选中项 const handleExportSelected = () => { if (selectedRowKeys.length === 0) { message.warning('请先选择要导出的记录'); return; } message.success(`导出 ${selectedRowKeys.length} 条记录成功`); }; // 表格列定义 const columns: TableColumnsType = [ { title: '#', width: 50, render: (_, __, index) => index + 1, }, { title: '文献标题', dataIndex: ['literature', 'title'], width: 350, ellipsis: { showTitle: false }, render: (text, record) => ( toggleRowExpanded(record.resultId)} > {text} ), }, { title: 'AI共识', dataIndex: 'aiConsensus', width: 120, align: 'center', render: (value) => { if (value === 'agree_include') { return (
一致纳入
(DS✓ QW✓)
); } if (value === 'agree_exclude') { return (
一致排除
(DS✗ QW✗)
); } return (
冲突
(DS≠QW)
); }, }, { title: '12字段通过率', dataIndex: 'fieldsPassRate', width: 120, align: 'center', render: (text) => { const [pass, total] = text.split('/').map(Number); const rate = (pass / total) * 100; const color = rate >= 80 ? 'success' : rate >= 60 ? 'warning' : 'error'; return {text}; }, }, { title: '排除原因', dataIndex: 'exclusionReason', width: 150, ellipsis: true, render: (text) => text || '-', }, { title: '人工决策', dataIndex: 'finalDecision', width: 100, align: 'center', render: (value, record) => { if (!value) { return 未复核; } return (
{record.aiConsensus === 'conflict' ? '(推翻冲突)' : '(与AI一致)'}
); }, }, { title: '状态', dataIndex: 'reviewStatus', width: 90, align: 'center', render: (status) => { const statusMap = { conflict: { text: '有冲突', color: 'warning' }, reviewed: { text: '已复核', color: 'success' }, pending: { text: 'AI一致', color: 'default' }, }; const config = statusMap[status as keyof typeof statusMap]; return {config.text}; }, }, ]; // 切换行展开 const toggleRowExpanded = (key: React.Key) => { setExpandedRowKeys((prev) => prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key] ); }; // 展开行渲染 const expandedRowRender = (record: FulltextResultItem) => { return (
文献信息
PMID: {record.literature.pmid || '-'}
作者: {record.literature.authors || '-'}
期刊: {record.literature.journal || '-'} ({record.literature.year || '-'})
AI评估摘要
AI共识: {record.aiConsensus === 'conflict' ? '存在冲突' : '意见一致'}
12字段通过率: {record.fieldsPassRate}
{record.exclusionReason &&
排除原因: {record.exclusionReason}
}
); }; if (!taskId) { return (
); } if (statsLoading) { return (
); } if (!stats) { return (
); } return (
{/* 标题 */}

全文复筛 - 结果

12字段评估结果统计、PRISMA排除分析、批量导出

{/* 统计概览 */} } /> } /> } /> 0 ? '#faad14' : '#999' }} prefix={} /> {stats.conflict > 0 && (
其中 {stats.conflict} 篇有冲突
)}
{/* 待复核提示 */} {stats.conflict > 0 && ( )} {/* PRISMA排除原因统计 */} {stats.excluded > 0 && (
{Object.entries(stats.exclusionReasons) .sort(([, a], [, b]) => b - a) .map(([reason, count]) => (
{reason} {count}篇 ({((count / stats.excluded) * 100).toFixed(1)}%)
))}
)} {/* 结果列表 */} setActiveTab(key as any)} items={[ { key: 'all', label: `全部 (${stats.total})` }, { key: 'included', label: `已纳入 (${stats.included})` }, { key: 'excluded', label: `已排除 (${stats.excluded})` }, { key: 'pending', label: `待复核 (${stats.pending})` }, ]} tabBarExtraContent={ {selectedRowKeys.length > 0 && ( )} } /> setSelectedRowKeys(keys as string[]), }} columns={columns} dataSource={results} rowKey="resultId" loading={resultsLoading} expandable={{ expandedRowRender, expandedRowKeys, onExpand: (expanded, record) => toggleRowExpanded(record.resultId), expandIcon: () => null, }} pagination={{ pageSize: 20, showSizeChanger: false, showTotal: (total) => `共 ${total} 条记录`, }} locale={{ emptyText: , }} scroll={{ x: 1100 }} bordered /> ); }; export default FulltextResults;