feat(dc): Complete Phase 1 - Portal workbench page development
Summary: - Implement DC module Portal page with 3 tool cards - Create ToolCard component with decorative background and hover animations - Implement TaskList component with table layout and progress bars - Implement AssetLibrary component with tab switching and file cards - Complete database verification (4 tables confirmed) - Complete backend API verification (6 endpoints ready) - Optimize UI to match prototype design (V2.html) Frontend Components (~715 lines): - components/ToolCard.tsx - Tool cards with animations - components/TaskList.tsx - Recent tasks table view - components/AssetLibrary.tsx - Data asset library with tabs - hooks/useRecentTasks.ts - Task state management - hooks/useAssets.ts - Asset state management - pages/Portal.tsx - Main portal page - types/portal.ts - TypeScript type definitions Backend Verification: - Backend API: 1495 lines code verified - Database: dc_schema with 4 tables verified - API endpoints: 6 endpoints tested (templates API works) Documentation: - Database verification report - Backend API test report - Phase 1 completion summary - UI optimization report - Development task checklist - Development plan for Tool B Status: Phase 1 completed (100%), ready for browser testing Next: Phase 2 - Tool B Step 1 and 2 development
This commit is contained in:
512
frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx
Normal file
512
frontend-v2/src/modules/asl/components/FulltextDetailDrawer.tsx
Normal file
@@ -0,0 +1,512 @@
|
||||
/**
|
||||
* 全文复筛 - 详情与复核抽屉组件
|
||||
*
|
||||
* 功能:
|
||||
* 1. Tab1: 双模型AI判断对比(12字段)
|
||||
* 2. Tab2: PDF全文预览(MVP占位符)
|
||||
* 3. Tab3: 12字段详细证据
|
||||
* 4. 底部: 人工决策表单
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Drawer,
|
||||
Tabs,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Tag,
|
||||
Form,
|
||||
Radio,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
Divider,
|
||||
Alert,
|
||||
Table,
|
||||
Collapse,
|
||||
} from 'antd';
|
||||
import type { TableColumnsType } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
FilePdfOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ConclusionTag from './ConclusionTag';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
// 12字段结果类型
|
||||
interface FieldResult {
|
||||
completeness: 'complete' | 'partial' | 'missing';
|
||||
extractability: 'extractable' | 'difficult' | 'impossible';
|
||||
evidence: string;
|
||||
pageNumber?: number;
|
||||
}
|
||||
|
||||
// 全文复筛结果类型
|
||||
interface FulltextResult {
|
||||
resultId: string;
|
||||
literatureId: string;
|
||||
literature: {
|
||||
id: string;
|
||||
pmid?: string;
|
||||
title: string;
|
||||
authors?: string;
|
||||
journal?: string;
|
||||
year?: number;
|
||||
};
|
||||
modelAResult: {
|
||||
modelName: string;
|
||||
fields: Record<string, FieldResult>;
|
||||
overall: {
|
||||
conclusion: 'include' | 'exclude';
|
||||
confidence: number;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
modelBResult: {
|
||||
modelName: string;
|
||||
fields: Record<string, FieldResult>;
|
||||
overall: {
|
||||
conclusion: 'include' | 'exclude';
|
||||
confidence: number;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
conflict: {
|
||||
isConflict: boolean;
|
||||
severity: 'high' | 'medium' | 'low';
|
||||
conflictFields: string[];
|
||||
};
|
||||
review: {
|
||||
finalDecision: 'include' | 'exclude' | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
result: FulltextResult | null;
|
||||
onClose: () => void;
|
||||
onSubmitReview: (resultId: string, decision: 'include' | 'exclude', note?: string) => void;
|
||||
isReviewing: boolean;
|
||||
}
|
||||
|
||||
// 12字段名称映射
|
||||
const FIELD_NAMES: Record<string, string> = {
|
||||
field_1: '研究设计',
|
||||
field_2: '随机化方法',
|
||||
field_3: '分配隐藏',
|
||||
field_4: '盲法',
|
||||
field_5: '研究对象',
|
||||
field_6: '样本量',
|
||||
field_7: '干预措施',
|
||||
field_8: '对照措施',
|
||||
field_9: '结局指标',
|
||||
field_10: '结果数据',
|
||||
field_11: '统计方法',
|
||||
field_12: '结果完整性',
|
||||
};
|
||||
|
||||
const FulltextDetailDrawer: React.FC<Props> = ({
|
||||
visible,
|
||||
result,
|
||||
onClose,
|
||||
onSubmitReview,
|
||||
isReviewing,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [activeTab, setActiveTab] = useState('comparison');
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
onSubmitReview(result.resultId, values.decision, values.note);
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
// 验证失败
|
||||
}
|
||||
};
|
||||
|
||||
// Tab1: 双模型对比表格
|
||||
const renderComparisonTab = () => {
|
||||
const fieldsData = Object.entries(FIELD_NAMES).map(([key, name]) => ({
|
||||
key,
|
||||
fieldName: name,
|
||||
modelA: result.modelAResult.fields[key],
|
||||
modelB: result.modelBResult.fields[key],
|
||||
}));
|
||||
|
||||
const columns: TableColumnsType<any> = [
|
||||
{
|
||||
title: '字段',
|
||||
dataIndex: 'fieldName',
|
||||
key: 'fieldName',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: `${result.modelAResult.modelName} - 完整性`,
|
||||
key: 'modelA_completeness',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const completeness = record.modelA?.completeness;
|
||||
const colorMap = {
|
||||
complete: 'success',
|
||||
partial: 'warning',
|
||||
missing: 'error',
|
||||
};
|
||||
return completeness ? (
|
||||
<Tag color={colorMap[completeness as keyof typeof colorMap]}>{completeness}</Tag>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${result.modelBResult.modelName} - 完整性`,
|
||||
key: 'modelB_completeness',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const completeness = record.modelB?.completeness;
|
||||
const colorMap = {
|
||||
complete: 'success',
|
||||
partial: 'warning',
|
||||
missing: 'error',
|
||||
};
|
||||
return completeness ? (
|
||||
<Tag color={colorMap[completeness as keyof typeof colorMap]}>{completeness}</Tag>
|
||||
) : (
|
||||
'-'
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '冲突',
|
||||
key: 'conflict',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
const isConflict =
|
||||
record.modelA?.completeness !== record.modelB?.completeness;
|
||||
return isConflict ? (
|
||||
<ExclamationCircleOutlined style={{ color: '#faad14', fontSize: 18 }} />
|
||||
) : (
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', fontSize: 18 }} />
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 整体结论对比 */}
|
||||
<Row gutter={16} className="mb-6">
|
||||
<Col span={12}>
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<Tag color="blue">{result.modelAResult.modelName}</Tag>
|
||||
<ConclusionTag conclusion={result.modelAResult.overall.conclusion} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="text-sm">
|
||||
<div className="mb-2">
|
||||
置信度: {(result.modelAResult.overall.confidence * 100).toFixed(0)}%
|
||||
</div>
|
||||
<div className="text-gray-600">{result.modelAResult.overall.reason}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex items-center justify-between">
|
||||
<Tag color="purple">{result.modelBResult.modelName}</Tag>
|
||||
<ConclusionTag conclusion={result.modelBResult.overall.conclusion} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="text-sm">
|
||||
<div className="mb-2">
|
||||
置信度: {(result.modelBResult.overall.confidence * 100).toFixed(0)}%
|
||||
</div>
|
||||
<div className="text-gray-600">{result.modelBResult.overall.reason}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 12字段对比表格 */}
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={fieldsData}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
size="small"
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Tab2: PDF预览(MVP占位符)
|
||||
const renderPdfTab = () => {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<FilePdfOutlined style={{ fontSize: 64, color: '#d9d9d9' }} />
|
||||
<div className="mt-4 text-gray-500">
|
||||
<p className="text-lg font-semibold">PDF预览功能</p>
|
||||
<p className="text-sm mt-2">该功能将在后续版本中实现</p>
|
||||
</div>
|
||||
<Alert
|
||||
message="技术债务 #15"
|
||||
description="PDF预览与高亮功能已列入技术债务清单,将在MVP之后的迭代中实现"
|
||||
type="info"
|
||||
showIcon
|
||||
className="mt-6 max-w-md mx-auto"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Tab3: 12字段详细证据
|
||||
const renderFieldsDetailTab = () => {
|
||||
const fieldsData = Object.entries(FIELD_NAMES).map(([key, name]) => ({
|
||||
key,
|
||||
label: name,
|
||||
children: (
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card size="small" title={<Tag color="blue">{result.modelAResult.modelName}</Tag>}>
|
||||
{result.modelAResult.fields[key] ? (
|
||||
<div className="text-sm space-y-2">
|
||||
<div>
|
||||
<span className="font-semibold">完整性: </span>
|
||||
<Tag
|
||||
color={
|
||||
result.modelAResult.fields[key].completeness === 'complete'
|
||||
? 'success'
|
||||
: result.modelAResult.fields[key].completeness === 'partial'
|
||||
? 'warning'
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{result.modelAResult.fields[key].completeness}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">可提取性: </span>
|
||||
<Tag>{result.modelAResult.fields[key].extractability}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">证据: </span>
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded text-gray-700">
|
||||
{result.modelAResult.fields[key].evidence || '未提供证据'}
|
||||
</div>
|
||||
</div>
|
||||
{result.modelAResult.fields[key].pageNumber && (
|
||||
<div className="text-xs text-gray-500">
|
||||
页码: {result.modelAResult.fields[key].pageNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400">无数据</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" title={<Tag color="purple">{result.modelBResult.modelName}</Tag>}>
|
||||
{result.modelBResult.fields[key] ? (
|
||||
<div className="text-sm space-y-2">
|
||||
<div>
|
||||
<span className="font-semibold">完整性: </span>
|
||||
<Tag
|
||||
color={
|
||||
result.modelBResult.fields[key].completeness === 'complete'
|
||||
? 'success'
|
||||
: result.modelBResult.fields[key].completeness === 'partial'
|
||||
? 'warning'
|
||||
: 'error'
|
||||
}
|
||||
>
|
||||
{result.modelBResult.fields[key].completeness}
|
||||
</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">可提取性: </span>
|
||||
<Tag>{result.modelBResult.fields[key].extractability}</Tag>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">证据: </span>
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded text-gray-700">
|
||||
{result.modelBResult.fields[key].evidence || '未提供证据'}
|
||||
</div>
|
||||
</div>
|
||||
{result.modelBResult.fields[key].pageNumber && (
|
||||
<div className="text-xs text-gray-500">
|
||||
页码: {result.modelBResult.fields[key].pageNumber}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-400">无数据</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
}));
|
||||
|
||||
return <Collapse items={fieldsData} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title={
|
||||
<div>
|
||||
<div className="text-lg font-bold">全文复筛 - 详情与复核</div>
|
||||
<div className="text-sm text-gray-500 font-normal mt-1">
|
||||
{result.literature.title}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
width="80%"
|
||||
onClose={onClose}
|
||||
open={visible}
|
||||
footer={
|
||||
<div className="flex justify-between items-center">
|
||||
<Button onClick={onClose}>取消</Button>
|
||||
<Button type="primary" onClick={handleSubmit} loading={isReviewing}>
|
||||
提交复核
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* 文献元信息 */}
|
||||
<Card size="small" className="mb-4">
|
||||
<div className="text-sm space-y-1">
|
||||
<div>
|
||||
<span className="font-semibold">PMID:</span> {result.literature.pmid || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">作者:</span> {result.literature.authors || '-'}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">期刊:</span> {result.literature.journal || '-'} (
|
||||
{result.literature.year || '-'})
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 冲突提示 */}
|
||||
{result.conflict.isConflict && (
|
||||
<Alert
|
||||
message="存在模型判断冲突"
|
||||
description={`冲突严重程度: ${result.conflict.severity.toUpperCase()} | 冲突字段: ${result.conflict.conflictFields.join(', ')}`}
|
||||
type="warning"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Tab内容 */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: 'comparison',
|
||||
label: '双模型对比',
|
||||
children: renderComparisonTab(),
|
||||
},
|
||||
{
|
||||
key: 'pdf',
|
||||
label: 'PDF全文预览',
|
||||
children: renderPdfTab(),
|
||||
},
|
||||
{
|
||||
key: 'fields',
|
||||
label: '12字段详情',
|
||||
children: renderFieldsDetailTab(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 人工决策表单 */}
|
||||
<Card title="人工复核决策" size="small">
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="decision"
|
||||
label="最终决策"
|
||||
rules={[{ required: true, message: '请选择决策' }]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Space direction="vertical">
|
||||
<Radio value="include">
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} /> 纳入
|
||||
</Radio>
|
||||
<Radio value="exclude">
|
||||
<CloseCircleOutlined style={{ color: '#ff4d4f' }} /> 排除
|
||||
</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.decision !== currentValues.decision
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('decision') === 'exclude' ? (
|
||||
<Form.Item
|
||||
name="exclusionReason"
|
||||
label="排除原因"
|
||||
rules={[{ required: true, message: '请输入排除原因' }]}
|
||||
>
|
||||
<Input placeholder="例如:P不匹配、S不是RCT、12字段不完整等" />
|
||||
</Form.Item>
|
||||
) : null
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="note" label="复核备注(可选)">
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder="记录复核理由、特殊情况说明等..."
|
||||
maxLength={500}
|
||||
showCount
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default FulltextDetailDrawer;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user