Features: - PatientWechatCallbackController for URL verification and message handling - PatientWechatService for template and customer messages - Support for secure mode (message encryption/decryption) - Simplified route /wechat/patient/callback for WeChat config - Event handlers for subscribe/unsubscribe/text messages - Template message for visit reminders Technical details: - Reuse @wecom/crypto for encryption (compatible with Official Account) - Relaxed Fastify schema validation to prevent early request blocking - Access token caching (7000s with 5min pre-refresh) - Comprehensive logging for debugging Testing: Local URL verification passed, ready for SAE deployment Status: Code complete, waiting for WeChat platform configuration
1285 lines
50 KiB
Markdown
1285 lines
50 KiB
Markdown
# 全文复筛前端开发计划
|
||
|
||
> **文档版本:** v1.0
|
||
> **创建日期:** 2025-11-23
|
||
> **最后更新:** 2025-11-23
|
||
> **预计工期:** 2.5天
|
||
> **开发阶段:** 全文复筛模块 - 前端UI实现
|
||
|
||
---
|
||
|
||
## 📋 目录
|
||
|
||
- [1. 开发目标](#1-开发目标)
|
||
- [2. 页面架构](#2-页面架构)
|
||
- [3. 详细设计](#3-详细设计)
|
||
- [4. 组件清单](#4-组件清单)
|
||
- [5. 开发排期](#5-开发排期)
|
||
- [6. 技术实现](#6-技术实现)
|
||
- [7. 测试计划](#7-测试计划)
|
||
|
||
---
|
||
|
||
## 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 功能模块
|
||
|
||
**(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 技术要点
|
||
|
||
**轮询机制**:
|
||
```typescript
|
||
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上传
|
||
|
||
```typescript
|
||
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 任务进度轮询
|
||
|
||
```typescript
|
||
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预览组件
|
||
|
||
```typescript
|
||
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 双模型判断表格
|
||
|
||
```typescript
|
||
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导出
|
||
|
||
```typescript
|
||
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**:
|
||
```typescript
|
||
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**:
|
||
```typescript
|
||
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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**文档维护**:
|
||
- 开发过程中如有调整,及时更新本文档
|
||
- 每个页面开发完成后,更新完成状态
|
||
- 遇到技术难点,记录在文档中
|
||
|
||
**相关文档**:
|
||
- [全文复筛开发计划](./04-全文复筛开发计划.md)
|
||
- [API设计规范](../02-技术设计/02-API设计规范.md)
|
||
- [数据库设计](../02-技术设计/01-数据库设计.md)
|
||
- [技术债务清单](../06-技术债务/技术债务清单.md)
|
||
|
||
---
|
||
|
||
**文档版本历史**:
|
||
- v1.0 (2025-11-23) - 初始版本,Day 5完成后创建
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|