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:
2025-12-02 21:53:24 +08:00
parent f240aa9236
commit d4d33528c7
83 changed files with 21863 additions and 1601 deletions

View 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;