Summary: - Complete IIT Manager Agent MVP Day 1 (12.5% progress) - Database: Create iit_schema with 5 tables (IitProject, IitPendingAction, IitTaskRun, IitUserMapping, IitAuditLog) - Backend: Add module structure (577 lines) and types (223 lines) - WeChat: Configure Enterprise WeChat app (CorpID, AgentID, Secret) - WeChat: Obtain web authorization and JS-SDK authorization - WeChat: Configure trusted domain (iit.xunzhengyixue.com) - Frontend: Deploy v1.2 with WeChat domain verification file - Frontend: Fix CRLF issue in docker-entrypoint.sh (CRLF -> LF) - Testing: 11/11 database CRUD tests passed - Testing: Access Token retrieval test passed - Docs: Create module status and development guide - Docs: Update MVP task list with Day 1 completion - Docs: Rename deployment doc to SAE real-time status record - Deployment: Update frontend internal IP to 172.17.173.80 Technical Details: - Prisma: Multi-schema support (iit_schema) - pg-boss: Job queue integration prepared - Taro 4.x: Framework selected for WeChat Mini Program - Shadow State: Architecture foundation laid - Docker: Fix entrypoint script line endings for Linux container Status: Day 1/14 complete, ready for Day 2 REDCap integration
50 KiB
50 KiB
全文复筛前端开发计划
文档版本: v1.0
创建日期: 2025-11-23
最后更新: 2025-11-23
预计工期: 2.5天
开发阶段: 全文复筛模块 - 前端UI实现
📋 目录
1. 开发目标
1.1 核心目标
实现全文复筛模块的完整前端UI,包括:
- 4个核心页面
- 支持独立运行和衔接标题摘要初筛两种模式
- 实时任务进度监控
- 双模型判断对比
- 简单PDF全文预览
- Excel结果导出
1.2 设计原则
- 灵活性 - 既能独立运行,也能衔接标题摘要初筛
- 实时性 - 长时间LLM任务需要实时进度反馈
- 可视化 - 冲突检测、PICS符合性清晰展示
- 易用性 - 简化操作流程,降低学习成本
- 一致性 - 与标题摘要初筛保持UI风格一致
2. 页面架构
2.1 路由设计
/asl/fulltext-screening
├── /settings - 设置与启动页面
├── /progress/:taskId - 任务进度监控页面
├── /workbench/:taskId - 审核工作台页面
└── /results/:taskId - 结果展示页面
2.2 页面流转
设置页面 (Settings)
↓ 点击"开始全文复筛"
↓ POST /api/v1/asl/fulltext-screening/tasks
↓
进度监控页面 (Progress)
↓ 轮询 GET /tasks/:taskId/progress (每3秒)
↓ status === 'completed'
↓
审核工作台 (Workbench)
↓ 人工审核冲突和待定文献
↓ PUT /results/:resultId/decision
↓ 点击"完成审核"
↓
结果展示页面 (Results)
↓ GET /tasks/:taskId/results
↓ GET /tasks/:taskId/export (下载Excel)
2.3 灵活导航
用户可以通过左侧导航随时在4个页面之间跳转:
- 进度页面未完成时,工作台显示"部分结果"
- 任何阶段都可以导出当前状态的Excel
- 支持返回设置页面重新开始
3. 详细设计
3.1 设置与启动页面 (Settings)
3.1.1 页面结构
┌─────────────────────────────────────────────┐
│ 全文复筛 / 设置与启动 │
├─────────────────────────────────────────────┤
│ │
│ 📋 PICOS标准 [调整标准] │
│ ┌─────────────────────────────────────────┐ │
│ │ P: 2型糖尿病成人患者 │ │
│ │ I: SGLT2抑制剂 │ │
│ │ C: 安慰剂或常规降糖疗法 │ │
│ │ O: 心血管事件、死亡率 │ │
│ │ S: 随机对照试验 (RCT) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ⚙️ 模型配置 │
│ ┌─────────────────────────────────────────┐ │
│ │ Model A: [DeepSeek-V3 ▼] │ │
│ │ Model B: [Qwen-Max ▼] │ │
│ │ 并发数: [3 ] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 📚 全文文献管理 (5篇) [📤 上传PDF] │
│ ┌─────────────────────────────────────────┐ │
│ │ ☑ 文献A.pdf ✅ 已上传 [查看][删除] │ │
│ │ ☑ 文献B.pdf ✅ 已上传 [查看][删除] │ │
│ │ ☑ 文献C.pdf ⏳ 上传中... 45% │ │
│ │ □ 文献D.pdf ❌ 上传失败 [重试] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [开始全文复筛] ← 大绿色按钮 │
└─────────────────────────────────────────────┘
3.1.2 功能模块
(1)PICOS标准卡片
- 显示当前PICOS标准(来自项目或手动编辑)
- "[调整标准]" 按钮 → 打开编辑弹窗
- 支持临时调整本次筛选标准
(2)模型配置
- Model A 下拉选择:DeepSeek-V3 / Qwen-Max / GPT-4o / Claude-Sonnet-4
- Model B 下拉选择:同上
- 并发数输入:1-10,默认3
(3)文献管理表格
- 显示已上传的PDF列表
- 显示上传状态:已上传/上传中/上传失败
- 操作列:查看、删除、重试
- "[📤 上传PDF]" 按钮 → 打开上传弹窗
(4)开始复筛按钮
- 当所有PDF上传成功后,按钮变为可用(绿色)
- 点击后跳转到进度监控页面
3.1.3 PICOS编辑弹窗
触发: 点击 "[调整标准]" 按钮
设计:
┌─────────────────────────────────────────────┐
│ 📋 编辑PICOS标准 [×] │
├─────────────────────────────────────────────┤
│ │
│ Population (人群) * │
│ ┌─────────────────────────────────────────┐ │
│ │ 2型糖尿病成人患者 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Intervention (干预) * │
│ ┌─────────────────────────────────────────┐ │
│ │ SGLT2抑制剂 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Comparison (对照) │
│ ┌─────────────────────────────────────────┐ │
│ │ 安慰剂或其他常规降糖疗法 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Outcome (结局) * │
│ ┌─────────────────────────────────────────┐ │
│ │ 心血管事件、全因死亡、卒中复发 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ Study Design (研究设计) * │
│ ┌─────────────────────────────────────────┐ │
│ │ 随机对照试验 (RCT) │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ⚠️ 注意:修改PICOS标准会影响AI筛选结果 │
│ │
│ [取消] [保存修改] │
└─────────────────────────────────────────────┘
字段说明:
- 所有字段支持多行文本输入
- P/I/O/S 为必填项(*标记)
- 保存后自动关闭弹窗,刷新PICOS卡片
3.1.4 上传PDF弹窗
触发: 点击 "[📤 上传PDF]" 按钮
设计:
┌─────────────────────────────────────────────┐
│ 📤 上传PDF文件 [×] │
├─────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ │ │
│ │ 📁 拖拽文件到此处 │ │
│ │ 或点击选择文件 │ │
│ │ │ │
│ │ • 支持格式:PDF │ │
│ │ • 单个文件最大:50MB │ │
│ │ • 支持批量上传(最多20篇) │ │
│ │ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 上传列表: │
│ ┌─────────────────────────────────────────┐ │
│ │ ✅ 文献A.pdf (2.3MB) │ │
│ │ ⏳ 文献B.pdf (5.1MB) - 上传中... 67% │ │
│ │ ❌ 文献C.pdf (120MB) - 文件过大 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [取消] [开始上传] │
└─────────────────────────────────────────────┘
功能说明:
- 使用 Ant Design
Upload.Dragger组件 - 支持拖拽和点击选择
- 自动验证文件格式和大小
- 显示实时上传进度
- 上传完成后自动关闭弹窗,刷新文献列表
3.2 任务进度监控页面 (Progress)
3.2.1 页面结构
┌─────────────────────────────────────────────┐
│ 全文复筛 / 任务进度 │
├─────────────────────────────────────────────┤
│ │
│ 🤖 AI全文复筛进行中... │
│ │
│ ████████████░░░░░░░░ 60% │
│ │
│ 📊 实时统计 │
│ ┌─────────────────────────────────────────┐ │
│ │ 当前进度:3 / 5 篇 │ │
│ │ ✅ 成功:2篇 │ │
│ │ ❌ 失败:1篇 │ │
│ │ ⚠️ 降级模式:0篇 │ │
│ │ │ │
│ │ 💰 Token消耗:45,234 │ │
│ │ 💵 成本:¥0.1523 │ │
│ │ │ │
│ │ ⏱️ 已用时:3分25秒 │ │
│ │ 📈 预计剩余:2分10秒 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 📝 处理日志 (自动滚动) │
│ ┌─────────────────────────────────────────┐ │
│ │ 10:24:30 ✅ PMID123 处理完成 │ │
│ │ DeepSeek: ✓ Qwen: ✓ │ │
│ │ Token: 12,345 成本: ¥0.0412 │ │
│ │ │ │
│ │ 10:25:15 ⏳ PMID456 正在处理... │ │
│ │ DeepSeek: 处理中... │ │
│ │ │ │
│ │ 10:26:02 ❌ PMID789 处理失败 │ │
│ │ 错误: PDF提取失败 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [取消任务] [查看已完成结果] │
└─────────────────────────────────────────────┘
3.2.2 功能说明
(1)进度条
- 显示整体进度百分比
- 使用 Ant Design
Progress组件 - 根据
processedCount / totalCount计算
(2)实时统计卡片
- 当前进度、成功数、失败数、降级数
- Token消耗和成本统计
- 已用时间和预计剩余时间(基于平均处理速度)
(3)处理日志
- 实时显示每篇文献的处理状态
- 自动滚动到最新日志
- 显示详细的错误信息
(4)操作按钮
- "取消任务" - 中断当前任务
- "查看已完成结果" - 跳转到审核工作台(显示部分结果)
3.2.3 技术要点
轮询机制:
const { data: task, refetch } = useQuery({
queryKey: ['fulltextTask', taskId],
queryFn: () => api.getTaskProgress(taskId),
refetchInterval: (data) => {
// 任务进行中:每3秒轮询一次
// 任务完成或失败:停止轮询
return data?.status === 'processing' ? 3000 : false;
},
});
// 任务完成后自动跳转
useEffect(() => {
if (task?.status === 'completed') {
setTimeout(() => {
navigate(`/asl/fulltext-screening/workbench/${taskId}`);
}, 2000); // 延迟2秒,让用户看到完成状态
}
}, [task?.status]);
3.3 审核工作台页面 (Workbench)
3.3.1 页面结构
┌──────────────────────────────────────────────────────────────┐
│ 全文复筛 / 审核工作台 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 📋 PICOS标准 (点击展开) ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ P: 2型糖尿病成人患者 | I: SGLT2抑制剂 | ... │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 📊 统计概览 │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 总数: 5 | 一致: 3 | 冲突: 2 | 待定: 0 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ 🔍 筛选: [全部▼] [冲突▼] [一致▼] 🔎 搜索: [____]│
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 文献 | DS判断(PICS) | Q3判断(PICS) | 冲突 | 决策 | 操作│ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ [+] │ │ │ │ │ │ │
│ │ PMID │ ✓ ✓ ✓ ✓ 纳入 │ ✓ ✓ ✓ ✓ 纳入│ 一致 │纳入 │详情 │ │
│ │ 123 │ │ │ │ │ │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ [+] │ │ │ │ │ │ │
│ │ PMID │ ✓ ✓ ✗ ✓ 排除 │ ✓ ✓ ✓ ✓ 纳入│❗冲突│[▼] │详情 │ │
│ │ 456 │ │ │ │ │ │ │
│ │ │ ├──────────────────────────────────────────────────│ │
│ │ └─> │ 💡 冲突原因:对照组(C)判断不一致 │ │
│ │ │ • DS: C不符合(未提及对照组) │ │
│ │ │ • Q3: C符合(安慰剂对照) │ │
│ │ └──────────────────────────────────────────────────│ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ [完成审核,进入结果页面] │
└──────────────────────────────────────────────────────────────┘
3.3.2 功能模块
(1)PICOS标准折叠面板
- 默认折叠,节省空间
- 点击展开显示完整PICOS标准
- 供审核时参考
(2)统计概览卡片
- 显示总数、一致数、冲突数、待定数
- 快速了解整体情况
(3)筛选和搜索
- 按冲突状态筛选:全部/仅冲突/仅一致
- 按决策筛选:全部/纳入/排除/待定
- 搜索:支持PMID、标题关键词
(4)双模型判断表格
- 显示PICS四维度符合性(✓/✗/?)
- 显示双模型结论(纳入/排除)
- 冲突行自动高亮(红色背景)
- 展开行显示冲突详情
(5)最终决策列
- 一致文献:自动采用AI决策
- 冲突文献:显示下拉框,需人工决策
- 支持三个选项:纳入/排除/待定
(6)操作列
- "详情" 按钮 → 打开详情抽屉
3.3.3 详情抽屉设计
布局: 右侧滑出(宽度800px)
┌────────────────────────────────────────┐
│ 文献详情 - PMID456 [×] │
├────────────────────────────────────────┤
│ [AI判断对比] [PDF全文] [12字段详情] │ ← Tab切换
├────────────────────────────────────────┤
│ │
│ 📄 基本信息 │
│ • 标题:Effect of SGLT2 inhibitors... │
│ • 作者:Zhang W, Li H, Wang Y │
│ • 期刊:JAMA 2023;15(3):e124 │
│ • 年份:2023 │
│ │
│ ───────────────────────────────────── │
│ │
│ 🤖 AI判断对比 │
│ │
│ ┌──────────────┬──────────────┐ │
│ │ DeepSeek-V3 │ Qwen-Max │ │
│ ├──────────────┼──────────────┤ │
│ │ P: ✓ 符合 │ P: ✓ 符合 │ │
│ │ 成人T2DM患 │ 成人T2DM │ │
│ │ 者,年龄... │ 患者 │ │
│ │ │ │ │
│ │ I: ✓ 符合 │ I: ✓ 符合 │ │
│ │ SGLT2抑制剂│ 达格列净 │ │
│ │ 达格列净... │ │ │
│ │ │ │ │
│ │ C: ✗ 不符合 │ C: ✓ 符合 │ ← 冲突│
│ │ 未明确对照 │ 安慰剂对 │ │
│ │ 组,仅提及 │ 照,双盲 │ │
│ │ "常规治疗" │ │ │
│ │ │ │ │
│ │ S: ✓ 符合 │ S: ✓ 符合 │ │
│ │ 随机对照试 │ RCT,多 │ │
│ │ 验,多中心 │ 中心研究 │ │
│ │ │ │ │
│ │ 结论:排除 │ 结论:纳入 │ │
│ └──────────────┴──────────────┘ │
│ │
│ ⚠️ 冲突提示:两个模型在对照组(C)判断 │
│ 上存在分歧,建议查看原文验证 │
│ │
│ ───────────────────────────────────── │
│ │
│ ✏️ 人工决策 │
│ 最终决策:[纳入 ▼] │
│ 决策理由: │
│ ┌──────────────────────────────────┐ │
│ │ 虽然DS判断对照组不符合,但查看 │ │
│ │ 原文后确认为安慰剂对照,符合纳入 │ │
│ │ 标准。 │ │
│ └──────────────────────────────────┘ │
│ │
│ [取消] [保存决策] │
└────────────────────────────────────────┘
Tab 2: PDF全文预览
┌────────────────────────────────────────┐
│ 文献详情 - PMID456 [×] │
├────────────────────────────────────────┤
│ [AI判断对比] [PDF全文] [12字段详情] │
├────────────────────────────────────────┤
│ │
│ 📄 PDF预览 │
│ ┌────────────────────────────────────┐ │
│ │ │ │
│ │ [PDF内容渲染区域] │ │
│ │ │ │
│ │ 使用 react-pdf 渲染 │ │
│ │ │ │
│ │ 支持: │ │
│ │ • 翻页(上一页/下一页) │ │
│ │ • 缩放(放大/缩小/适应) │ │
│ │ • 跳页(输入页码直接跳转) │ │
│ │ │ │
│ └────────────────────────────────────┘ │
│ │
│ 页码:3 / 15 [<] [>] [跳转__] │
│ 缩放:[100% ▼] [适应宽度] [适应高度] │
│ │
└────────────────────────────────────────┘
Tab 3: 12字段详情
┌────────────────────────────────────────┐
│ 文献详情 - PMID456 [×] │
├────────────────────────────────────────┤
│ [AI判断对比] [PDF全文] [12字段详情] │
├────────────────────────────────────────┤
│ │
│ 📋 12字段完整性评估 │
│ │
│ ▼ 1. 文献来源 │
│ 存在性: ✅ 完整 │
│ 第一作者: Zhang W │
│ 年份: 2023 │
│ 期刊: JAMA │
│ │
│ ▼ 2. 研究类型 │
│ 存在性: ✅ 完整 │
│ 类型: RCT │
│ │
│ ▶ 3. 研究设计细节 (点击展开) │
│ │
│ ▶ 4. 疾病诊断标准 │
│ │
│ ▶ 5. 人群特征 ⭐ (关键字段) │
│ │
│ ... (共12个字段) │
│ │
└────────────────────────────────────────┘
3.4 结果展示页面 (Results)
3.4.1 页面结构
┌─────────────────────────────────────────────┐
│ 全文复筛 / 结果展示 │
├─────────────────────────────────────────────┤
│ │
│ 📊 总体统计 │
│ ┌───────────┬───────────┬───────────┐ │
│ │ 总计复筛 │ 最终纳入 │ 排除 │ │
│ │ 100 │ 55 │ 45 │ │
│ └───────────┴───────────┴───────────┘ │
│ │
│ 📈 PRISMA流程图统计 │
│ ┌─────────────────────────────────────────┐ │
│ │ 排除原因统计: │ │
│ │ • 排除文献总数: 45篇 │ │
│ │ • 非随机对照研究(S): 5篇 │ │
│ │ • 非目标人群(P): 7篇 │ │
│ │ • 干预/对照不符(I/C): 18篇 │ │
│ │ • 结局指标不符(O): 9篇 │ │
│ │ • 其他原因: 6篇 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 💰 成本统计 │
│ ┌─────────────────────────────────────────┐ │
│ │ • DeepSeek-V3: Token 245K, 成本 ¥0.82 │ │
│ │ • Qwen-Max: Token 198K, 成本 ¥1.35 │ │
│ │ • 总计: Token 443K, 成本 ¥2.17 │ │
│ │ • 平均单篇成本: ¥0.0217 │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 📋 文献列表 [导出Excel]│
│ ┌─────────────────────────────────────────┐ │
│ │ [最终纳入 (55)] [排除 (45)] ← Tab │ │
│ │ │ │
│ │ 🔎 搜索: [________] [筛选▼] │ │
│ │ │ │
│ │ PMID | 研究ID | 来源 | 决策 | 方式 │ │
│ │ 123 | A1 2021│ ... | 纳入 | AI纳入 │ │
│ │ 456 | B2 2022│ ... | 纳入 | 人工审核 │ │
│ │ ... │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
3.4.2 功能说明
(1)总体统计卡片
- 显示总数、纳入数、排除数
- 大数字突出显示
- 绿色(纳入)、红色(排除)配色
(2)PRISMA统计卡片
- 按排除原因分类统计
- 符合PRISMA流程图要求
- 便于撰写系统评价报告
(3)成本统计卡片
- 双模型Token和成本对比
- 显示平均单篇成本
- 便于成本控制和优化
(4)文献列表
- Tab切换:最终纳入 / 排除
- 显示文献基本信息
- 显示决策方式:AI纳入/AI排除/人工审核
- 支持搜索和筛选
(5)导出Excel按钮
- 下载4-Sheet Excel报告
- 包含:纳入文献、排除文献、PRISMA统计、成本统计
4. 组件清单
4.1 页面组件(Page Components)
| 组件名 | 文件路径 | 功能描述 | 预计工作量 |
|---|---|---|---|
FulltextScreeningSettings |
pages/FulltextScreeningSettings.tsx |
设置与启动页面 | 4小时 |
FulltextScreeningProgress |
pages/FulltextScreeningProgress.tsx |
任务进度监控页面 | 3小时 |
FulltextScreeningWorkbench |
pages/FulltextScreeningWorkbench.tsx |
审核工作台页面 | 6小时 |
FulltextScreeningResults |
pages/FulltextScreeningResults.tsx |
结果展示页面 | 3小时 |
4.2 功能组件(Feature Components)
| 组件名 | 文件路径 | 功能描述 | 预计工作量 |
|---|---|---|---|
PICOSCard |
components/PICOSCard.tsx |
PICOS标准展示卡片(可折叠) | 1小时 |
PICOSEditModal |
components/PICOSEditModal.tsx |
PICOS编辑弹窗 | 2小时 |
PDFUploadModal |
components/PDFUploadModal.tsx |
PDF上传弹窗 | 2小时 |
LiteratureTable |
components/LiteratureTable.tsx |
文献管理表格 | 2小时 |
ProgressMonitor |
components/ProgressMonitor.tsx |
进度监控组件 | 2小时 |
DualModelJudgmentTable |
components/DualModelJudgmentTable.tsx |
双模型判断表格 | 4小时 |
LiteratureDetailDrawer |
components/LiteratureDetailDrawer.tsx |
文献详情抽屉 | 4小时 |
PDFViewer |
components/PDFViewer.tsx |
PDF预览组件 | 2小时 |
FieldsCollapse |
components/FieldsCollapse.tsx |
12字段折叠面板 | 2小时 |
PRISMAStatistics |
components/PRISMAStatistics.tsx |
PRISMA统计卡片 | 1小时 |
4.3 复用组件(Reused Components)
| 组件名 | 来源 | 功能描述 |
|---|---|---|
ASLLayout |
标题摘要初筛 | 左侧导航布局 |
JudgmentBadge |
标题摘要初筛 | PICOS判断标签(✓/✗/?) |
ConclusionTag |
标题摘要初筛 | 决策标签(纳入/排除) |
4.4 Hooks(自定义钩子)
| Hook名 | 文件路径 | 功能描述 | 预计工作量 |
|---|---|---|---|
useFulltextTask |
hooks/useFulltextTask.ts |
管理全文复筛任务状态 | 1小时 |
useTaskProgress |
hooks/useTaskProgress.ts |
轮询任务进度 | 1小时 |
useTaskResults |
hooks/useTaskResults.ts |
获取任务结果 | 1小时 |
5. 开发排期
5.1 Day 6 - 基础页面(8小时)
上午(4小时)
5.1.1 设置与启动页面基础(2小时)
- 创建页面文件和路由
- 实现PICOS展示卡片
- 实现模型配置表单
- 实现文献列表表格(基础版)
5.1.2 PICOS编辑和PDF上传弹窗(2小时)
- 实现PICOS编辑弹窗
- 实现PDF上传弹窗(Ant Design Upload)
- 集成后端PDF上传API
- 实时显示上传进度
下午(4小时)
5.1.3 任务进度监控页面(4小时)
- 创建页面文件和路由
- 实现进度条和统计卡片
- 实现轮询机制(React Query)
- 实现处理日志滚动显示
- 任务完成后自动跳转
5.2 Day 7 - 核心功能(10小时)
上午(5小时)
5.2.1 审核工作台页面基础(3小时)
- 创建页面文件和路由
- 实现PICOS折叠面板
- 实现统计概览卡片
- 实现筛选和搜索功能
- 实现双模型判断表格(基础版)
5.2.2 冲突可视化(2小时)
- 冲突行高亮显示
- 展开行显示冲突详情
- 冲突原因文字说明
- 最终决策下拉框(含验证)
下午(5小时)
5.2.3 文献详情抽屉 - AI判断对比Tab(2小时)
- 创建抽屉组件(Ant Design Drawer)
- 实现基本信息展示
- 实现双模型判断对比(左右分栏)
- 高亮冲突字段
- 人工决策表单(含提交)
5.2.4 PDF预览Tab(2小时)
- 集成react-pdf库
- 实现基础PDF渲染
- 实现翻页功能(上一页/下一页/跳页)
- 实现缩放功能(放大/缩小/适应)
- 优化加载性能
5.2.5 12字段详情Tab(1小时)
- 实现12字段折叠面板(Ant Design Collapse)
- 显示每个字段的存在性、完整性
- 支持展开/折叠单个字段
5.3 Day 8 - 结果页面与联调(6小时)
上午(3小时)
5.3.1 结果展示页面(3小时)
- 创建页面文件和路由
- 实现总体统计卡片
- 实现PRISMA统计卡片
- 实现成本统计卡片
- 实现文献列表(Tab切换)
- 实现Excel导出功能
下午(3小时)
5.3.2 前后端联调(2小时)
- 测试完整流程(设置→进度→工作台→结果)
- 测试PDF上传
- 测试实时进度轮询
- 测试人工决策提交
- 测试Excel导出下载
5.3.3 Bug修复和优化(1小时)
- 修复发现的Bug
- 优化UI细节
- 优化性能(懒加载、虚拟滚动等)
- 补充错误处理和提示
6. 技术实现
6.1 技术栈
- 框架: React 18 + TypeScript 5
- 路由: React Router DOM v6
- 状态管理: @tanstack/react-query (React Query v5)
- UI组件: Ant Design v5
- PDF预览: react-pdf v7
- 文件上传: Ant Design Upload + axios
- 样式: TailwindCSS v3
6.2 关键技术实现
6.2.1 PDF上传
import { Upload, message } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface';
const PDFUploadModal = () => {
const [fileList, setFileList] = useState<UploadFile[]>([]);
const customRequest = async ({ file, onProgress, onSuccess, onError }: any) => {
try {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post(
'/api/v1/asl/literatures/upload-pdf',
formData,
{
onUploadProgress: (e) => {
const percent = Math.round((e.loaded / e.total!) * 100);
onProgress({ percent });
},
}
);
onSuccess(response.data);
message.success(`${file.name} 上传成功`);
} catch (error) {
onError(error);
message.error(`${file.name} 上传失败`);
}
};
return (
<Upload.Dragger
multiple
accept=".pdf"
fileList={fileList}
customRequest={customRequest}
onChange={({ fileList }) => setFileList(fileList)}
beforeUpload={(file) => {
// 验证文件大小(最大50MB)
const isLt50M = file.size / 1024 / 1024 < 50;
if (!isLt50M) {
message.error('文件大小不能超过50MB!');
}
return isLt50M;
}}
>
<p className="ant-upload-drag-icon">📁</p>
<p className="ant-upload-text">拖拽文件到此处,或点击选择文件</p>
<p className="ant-upload-hint">
支持格式:PDF | 单个文件最大:50MB | 支持批量上传(最多20篇)
</p>
</Upload.Dragger>
);
};
6.2.2 任务进度轮询
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
const useTaskProgress = (taskId: string) => {
const navigate = useNavigate();
const { data: task, isLoading } = useQuery({
queryKey: ['fulltextTask', taskId],
queryFn: async () => {
const res = await axios.get(`/api/v1/asl/fulltext-screening/tasks/${taskId}/progress`);
return res.data.data;
},
refetchInterval: (data) => {
// 任务进行中:每3秒轮询
// 任务完成或失败:停止轮询
return data?.status === 'processing' ? 3000 : false;
},
refetchOnWindowFocus: false,
});
// 任务完成后自动跳转
useEffect(() => {
if (task?.status === 'completed') {
setTimeout(() => {
navigate(`/asl/fulltext-screening/workbench/${taskId}`);
}, 2000);
}
}, [task?.status, taskId, navigate]);
return { task, isLoading };
};
6.2.3 PDF预览组件
import { useState } from 'react';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
// 配置PDF.js worker
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
interface PDFViewerProps {
url: string;
}
const PDFViewer: React.FC<PDFViewerProps> = ({ url }) => {
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const [scale, setScale] = useState<number>(1.0);
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
setNumPages(numPages);
setPageNumber(1);
};
const changePage = (offset: number) => {
setPageNumber((prev) => Math.min(Math.max(1, prev + offset), numPages));
};
const changeScale = (newScale: number) => {
setScale(Math.min(Math.max(0.5, newScale), 2.0));
};
return (
<div className="pdf-viewer">
<Document
file={url}
onLoadSuccess={onDocumentLoadSuccess}
loading={<div>加载中...</div>}
error={<div>PDF加载失败</div>}
>
<Page
pageNumber={pageNumber}
scale={scale}
renderTextLayer={true}
renderAnnotationLayer={true}
/>
</Document>
<div className="pdf-controls">
<div className="pagination">
<button onClick={() => changePage(-1)} disabled={pageNumber <= 1}>
上一页
</button>
<span>
页码:{pageNumber} / {numPages}
</span>
<button onClick={() => changePage(1)} disabled={pageNumber >= numPages}>
下一页
</button>
</div>
<div className="zoom">
<button onClick={() => changeScale(scale - 0.1)}>缩小</button>
<span>{Math.round(scale * 100)}%</span>
<button onClick={() => changeScale(scale + 0.1)}>放大</button>
<button onClick={() => setScale(1.0)}>重置</button>
</div>
</div>
</div>
);
};
export default PDFViewer;
6.2.4 双模型判断表格
interface DualModelJudgmentTableProps {
results: FulltextScreeningResult[];
onUpdateDecision: (resultId: string, decision: string) => void;
onOpenDetail: (result: FulltextScreeningResult) => void;
}
const DualModelJudgmentTable: React.FC<DualModelJudgmentTableProps> = ({
results,
onUpdateDecision,
onOpenDetail,
}) => {
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const columns: ColumnsType<FulltextScreeningResult> = [
{
title: '文献',
dataIndex: 'literatureId',
key: 'literatureId',
render: (_, record) => record.literature.pmid,
},
{
title: 'DS判断(PICS)',
key: 'modelA',
render: (_, record) => {
const { modelAFields, modelAOverall } = record;
return (
<div className="flex items-center gap-1">
<JudgmentBadge value={modelAFields?.field5?.assessment} dim="P" />
<JudgmentBadge value={modelAFields?.field6?.assessment} dim="I" />
<JudgmentBadge value={modelAFields?.field7?.assessment} dim="C" />
<JudgmentBadge value={modelAFields?.field2?.assessment} dim="S" />
<ConclusionTag conclusion={modelAOverall?.overall_decision} />
</div>
);
},
},
{
title: 'Q3判断(PICS)',
key: 'modelB',
render: (_, record) => {
const { modelBFields, modelBOverall } = record;
return (
<div className="flex items-center gap-1">
<JudgmentBadge value={modelBFields?.field5?.assessment} dim="P" />
<JudgmentBadge value={modelBFields?.field6?.assessment} dim="I" />
<JudgmentBadge value={modelBFields?.field7?.assessment} dim="C" />
<JudgmentBadge value={modelBFields?.field2?.assessment} dim="S" />
<ConclusionTag conclusion={modelBOverall?.overall_decision} />
</div>
);
},
},
{
title: '冲突',
key: 'conflict',
render: (_, record) => {
const hasConflict = record.conflictSeverity !== null;
return hasConflict ? (
<Tag color="red">冲突</Tag>
) : (
<Tag color="green">一致</Tag>
);
},
},
{
title: '决策',
key: 'decision',
render: (_, record) => {
const hasConflict = record.conflictSeverity !== null;
if (!hasConflict && record.finalDecision) {
return <ConclusionTag conclusion={record.finalDecision} />;
}
return (
<Select
value={record.finalDecision || 'pending'}
onChange={(value) => onUpdateDecision(record.id, value)}
style={{ width: 100 }}
>
<Select.Option value="include">纳入</Select.Option>
<Select.Option value="exclude">排除</Select.Option>
<Select.Option value="pending">待定</Select.Option>
</Select>
);
},
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Button type="link" onClick={() => onOpenDetail(record)}>
详情
</Button>
),
},
];
return (
<Table
columns={columns}
dataSource={results}
rowKey="id"
expandable={{
expandedRowKeys: expandedKeys,
onExpand: (expanded, record) => {
if (expanded) {
setExpandedKeys([...expandedKeys, record.id]);
} else {
setExpandedKeys(expandedKeys.filter((k) => k !== record.id));
}
},
expandedRowRender: (record) => {
if (!record.conflictFields || record.conflictFields.length === 0) {
return null;
}
return (
<div className="conflict-detail p-4 bg-red-50">
<h4 className="font-semibold mb-2">💡 冲突详情:</h4>
<ul className="list-disc pl-6">
{record.conflictFields.map((field) => (
<li key={field}>
{field}: Model A vs Model B 判断不一致
</li>
))}
</ul>
</div>
);
},
rowExpandable: (record) =>
record.conflictFields && record.conflictFields.length > 0,
}}
rowClassName={(record) =>
record.conflictSeverity ? 'bg-red-50' : ''
}
/>
);
};
6.2.5 Excel导出
const handleExportExcel = async (taskId: string) => {
try {
message.loading({ content: '正在生成Excel...', key: 'export' });
const response = await axios.get(
`/api/v1/asl/fulltext-screening/tasks/${taskId}/export`,
{
responseType: 'blob', // 重要:指定响应类型为blob
}
);
// 创建下载链接
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
// 从响应头获取文件名
const contentDisposition = response.headers['content-disposition'];
const filename = contentDisposition
? contentDisposition.split('filename=')[1].replace(/"/g, '')
: `全文复筛结果_${taskId}.xlsx`;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
// 清理
link.remove();
window.URL.revokeObjectURL(url);
message.success({ content: '导出成功!', key: 'export' });
} catch (error) {
message.error({ content: '导出失败', key: 'export' });
console.error('Export error:', error);
}
};
7. 测试计划
7.1 功能测试
测试用例:
| 编号 | 测试项 | 测试步骤 | 预期结果 |
|---|---|---|---|
| T1 | PDF上传 | 上传单个PDF | 上传成功,显示在文献列表 |
| T2 | PDF批量上传 | 上传5个PDF | 全部上传成功 |
| T3 | PDF大小验证 | 上传>50MB的PDF | 提示文件过大,上传失败 |
| T4 | PICOS编辑 | 修改P字段并保存 | PICOS卡片更新 |
| T5 | 创建任务 | 点击"开始全文复筛" | 跳转到进度页面 |
| T6 | 进度轮询 | 停留在进度页面 | 每3秒更新一次进度 |
| T7 | 自动跳转 | 等待任务完成 | 自动跳转到工作台 |
| T8 | 冲突可视化 | 查看工作台冲突行 | 红色高亮,显示冲突详情 |
| T9 | 人工决策 | 修改冲突文献决策 | 保存成功,刷新列表 |
| T10 | PDF预览 | 打开详情抽屉查看PDF | PDF正常渲染,可翻页缩放 |
| T11 | Excel导出 | 点击"导出Excel" | 下载4-Sheet Excel文件 |
7.2 性能测试
| 测试项 | 测试条件 | 性能指标 |
|---|---|---|
| 页面加载 | 首次访问 | < 2秒 |
| PDF上传 | 10MB文件 | < 5秒 |
| 进度轮询 | 轮询100次 | 无内存泄漏 |
| PDF渲染 | 15页PDF | < 3秒 |
| 表格渲染 | 100条数据 | < 1秒 |
7.3 兼容性测试
| 浏览器 | 版本 | 测试结果 |
|---|---|---|
| Chrome | 最新版 | ✅ 通过 |
| Firefox | 最新版 | ✅ 通过 |
| Edge | 最新版 | ✅ 通过 |
| Safari | 最新版 | ⚠️ PDF预览需额外测试 |
📝 附录
A. API接口清单
| 接口 | 方法 | 路径 | 功能 |
|---|---|---|---|
| 上传PDF | POST | /api/v1/asl/literatures/upload-pdf |
上传单个PDF文件 |
| 创建任务 | POST | /api/v1/asl/fulltext-screening/tasks |
创建全文复筛任务 |
| 获取进度 | GET | /api/v1/asl/fulltext-screening/tasks/:taskId/progress |
获取任务进度 |
| 获取结果 | GET | /api/v1/asl/fulltext-screening/tasks/:taskId/results |
获取任务结果列表 |
| 更新决策 | PUT | /api/v1/asl/fulltext-screening/results/:resultId/decision |
人工更新决策 |
| 导出Excel | GET | /api/v1/asl/fulltext-screening/tasks/:taskId/export |
导出Excel报告 |
B. 数据结构
FulltextScreeningTask:
interface FulltextScreeningTask {
id: string;
projectId: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
totalCount: number;
processedCount: number;
successCount: number;
failedCount: number;
degradedCount: number;
totalTokens: number;
totalCost: number;
createdAt: Date;
startedAt: Date | null;
completedAt: Date | null;
}
FulltextScreeningResult:
interface FulltextScreeningResult {
id: string;
taskId: string;
literatureId: string;
literature: {
pmid: string;
title: string;
authors: string;
journal: string;
publicationYear: number;
};
modelAName: string;
modelAStatus: 'success' | 'failed';
modelAFields: any; // 12字段
modelAOverall: {
overall_decision: 'include' | 'exclude';
overall_reasoning: string;
};
modelBName: string;
modelBStatus: 'success' | 'failed';
modelBFields: any;
modelBOverall: any;
conflictFields: string[];
conflictSeverity: 'high' | 'medium' | 'low' | null;
finalDecision: 'include' | 'exclude' | 'pending' | null;
reviewedBy: string | null;
reviewedAt: Date | null;
}
文档维护:
- 开发过程中如有调整,及时更新本文档
- 每个页面开发完成后,更新完成状态
- 遇到技术难点,记录在文档中
相关文档:
文档版本历史:
- v1.0 (2025-11-23) - 初始版本,Day 5完成后创建