Files
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/04-开发计划/05-全文复筛前端开发计划.md
HaHafeng 5f1e7af92c feat(dc): Complete Tool B frontend development with UI optimization
- Implement Tool B 5-step workflow (upload, schema, processing, verify, result)
- Add back navigation button to Portal
- Optimize Step 2 field list styling to match prototype
- Fix step 3 label: 'dual-blind' to 'dual-model'
- Create API service layer with 7 endpoints
- Integrate Tool B route into DC module
- Add comprehensive TypeScript types

Components (~1100 lines):
- index.tsx: Main Tool B entry with state management
- Step1Upload.tsx: File upload and health check
- Step2Schema.tsx: Smart template configuration
- Step3Processing.tsx: Dual-model extraction progress
- Step4Verify.tsx: Conflict verification workbench
- Step5Result.tsx: Result display
- StepIndicator.tsx: Step progress component
- api/toolB.ts: API service layer

Status: Frontend complete, ready for API integration
2025-12-03 09:36:35 +08:00

50 KiB
Raw Blame History

全文复筛前端开发计划

文档版本: v1.0
创建日期: 2025-11-23
最后更新: 2025-11-23
预计工期: 2.5天
开发阶段: 全文复筛模块 - 前端UI实现


📋 目录


1. 开发目标

1.1 核心目标

实现全文复筛模块的完整前端UI包括

  • 4个核心页面
  • 支持独立运行和衔接标题摘要初筛两种模式
  • 实时任务进度监控
  • 双模型判断对比
  • 简单PDF全文预览
  • Excel结果导出

1.2 设计原则

  1. 灵活性 - 既能独立运行,也能衔接标题摘要初筛
  2. 实时性 - 长时间LLM任务需要实时进度反馈
  3. 可视化 - 冲突检测、PICS符合性清晰展示
  4. 易用性 - 简化操作流程,降低学习成本
  5. 一致性 - 与标题摘要初筛保持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 功能模块

1PICOS标准卡片

  • 显示当前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 功能模块

1PICOS标准折叠面板

  • 默认折叠,节省空间
  • 点击展开显示完整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总体统计卡片

  • 显示总数、纳入数、排除数
  • 大数字突出显示
  • 绿色(纳入)、红色(排除)配色

2PRISMA统计卡片

  • 按排除原因分类统计
  • 符合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判断对比Tab2小时

  • 创建抽屉组件Ant Design Drawer
  • 实现基本信息展示
  • 实现双模型判断对比(左右分栏)
  • 高亮冲突字段
  • 人工决策表单(含提交)

5.2.4 PDF预览Tab2小时

  • 集成react-pdf库
  • 实现基础PDF渲染
  • 实现翻页功能(上一页/下一页/跳页)
  • 实现缩放功能(放大/缩小/适应)
  • 优化加载性能

5.2.5 12字段详情Tab1小时

  • 实现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完成后创建