12 KiB
12 KiB
上传任务列表组件 — 设计规格
日期: 2026-05-29 目标: 在 ReadModule 中嵌入上传任务列表组件,实现文件上传后自动跟踪向量化+出题状态,支持轮询刷新、分页、筛选
1. 数据源分析
1.1 后端 file 表关键字段(来自 file.sql)
| 字段 | 类型 | 用途 | 可能值 |
|---|---|---|---|
process_step_status |
varchar(32) | 流程状态(主状态) | UPLOADED / VECTORIZING / VECTORIZED / VECTORIZE_FAILED / EXAM_GENERATING / EXAM_GENERATED / EXAM_GENERATE_FAILED / COMPLETED |
process_step_message |
varchar(512) | 流程步骤信息(向量化详情) | "向量化失败: 确保collection存在失败..." / "文件不存在于服务器" / null |
exam_status |
varchar(20) | 出题状态 | UNGENERATED / GENERATING / GENERATED / FAILED |
process_message |
text | 处理消息(出题详情) | "生成成功,共4道题" / "生成失败: 所有题目保存失败..." / null |
1.2 状态映射规则
向量化状态(从 process_step_status + process_step_message 判断):
| process_step_status | 判定结果 | UI 展示 |
|---|---|---|
UPLOADED |
待向量化 | ⏳ 灰色 - 等待中 |
VECTORIZING |
向量化中 | 🔄 蓝色 - 处理中(动画) |
VECTORIZED |
向量化成功 | ✅ 绿色 - 成功 |
VECTORIZE_FAILED |
向量化失败 | ❌ 红色 - 失败(显示 process_step_message) |
出题状态(从 exam_status + process_message 判断):
| exam_status | 判定结果 | UI 展示 |
|---|---|---|
UNGENERATED |
未出题 | ⏸ 灰色 - 未开始 |
GENERATING |
出题中 | 🔄 蓝色 - 处理中(动画) |
GENERATED |
出题成功 | ✅ 绿色 - 成功(显示题目数) |
FAILED |
出题失败 | ❌ 红色 - 失败(显示 process_message) |
1.3 触发条件
- 上传成功后:新上传的文件自动加入任务列表
- 页面加载时:加载最近 N 条有处理状态的文件(非 UPLOADED/COMPLETED 的活跃任务)
- 轮询间隔:5 秒一次,仅对"处理中"状态的任务轮询
2. 组件架构
ReadModule.vue
├── 文件列表 Tab(已有)
├── 文件审批 Tab(已有)
└── [新增] 上传任务面板 (UploadTaskPanel)
├── 任务头部统计栏(总数 / 处理中 / 成功 / 失败)
├── 筛选工具栏(状态筛选 + 搜索 + 手动刷新按钮)
├── 任务列表(分页展示)
│ └── UploadTaskItem × N
│ ├── 文件基本信息(名称、大小、部门、时间)
│ ├── 向量化状态条
│ └── 出题状态条
└── 分页器
2.1 文件结构
| 文件 | 职责 | 预估行数 |
|---|---|---|
src/components/exam/composables/useUploadTasks.js |
任务数据管理、轮询逻辑、状态解析 | ~250 行 |
src/components/exam/UploadTaskPanel.vue |
任务列表面板容器 | ~180 行 |
src/components/exam/UploadTaskItem.vue |
单个任务卡片 | ~150 行 |
修改 src/components/ReadModule.vue |
嵌入 UploadTaskPanel,上传后触发添加任务 | ~30 行改动 |
3. 各组件详细设计
3.1 useUploadTasks.js — Composable
职责:所有任务数据的获取、缓存、状态解析、轮询控制。
导出接口:
export function useUploadTasks() {
// 状态
const tasks = ref([]) // 任务列表原始数据
const loading = ref(false)
const polling = ref(false)
// 筛选
const statusFilter = ref('all') // all | vectorizing | vectorized | vector_failed | exam_generating | generated | failed
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
// 统计
const stats = computed(() => ({ total, processing, success, failed }))
const filteredTasks = computed(() => { /* 筛选+分页 */ })
// 核心方法
function addTask(fileData) // 上传成功后调用,将文件加入任务列表
function parseVectorStatus(task) // 解析向量化状态 → { phase, status, label, message, color }
function parseExamStatus(task) // 解析出题状态 → { phase, status, label, message, color }
function startPolling() // 启动定时轮询
function stopPolling() // 停止轮询
function refreshTasks() // 手动刷新
function fetchTasks() // 从后端获取任务列表 API
return { tasks, loading, polling, statusFilter, currentPage, pageSize,
stats, filteredTasks, addTask, parseVectorStatus, parseExamStatus,
startPolling, stopPolling, refreshTasks, fetchTasks }
}
状态解析函数核心逻辑:
function parseVectorStatus(task) {
const pss = task.processStepStatus || task.process_step_status || ''
const psm = task.processStepMessage || task.process_step_message || ''
if (['VECTORIZING'].includes(pss))
return { phase: 'vectorize', status: 'processing', label: '向量化中', message: '正在处理...', color: 'info', animating: true }
if (pss === 'VECTORIZED')
return { phase: 'vectorize', status: 'success', label: '向量化完成', message: null, color: 'success' }
if (pss === 'VECTORIZE_FAILED')
return { phase: 'vectorize', status: 'error', label: '向量化失败', message: psm || '未知错误', color: 'error' }
if (['UPLOADED'].includes(pss))
return { phase: 'vectorize', status: 'pending', label: '等待向量化', message: null, color: 'pending' }
if (['EXAM_GENERATING', 'EXAM_GENERATED', 'EXAM_GENERATE_FAILED', 'COMPLETED'].includes(pss))
return { phase: 'vectorize', status: 'success', label: '已入库', message: null, color: 'success' }
return { phase: 'vectorize', status: 'unknown', label: '未知', message: null, color: 'pending' }
}
function parseExamStatus(task) {
const es = task.examStatus || task.exam_status || ''
const pm = task.processMessage || task.process_message || ''
if (es === 'GENERATING')
return { phase: 'exam', status: 'processing', label: '生成题目中...', message: null, color: 'info', animating: true }
if (es === 'GENERATED') {
const match = pm?.match(/共(\d+)道题/)
return { phase: 'exam', status: 'success', label: match ? `生成${match[1]}道题` : '题目生成完成', message: pm, color: 'success' }
}
if (es === 'FAILED')
return { phase: 'exam', status: 'error', label: '出题失败', message: pm || '未知错误', color: 'error' }
if (es === 'UNGENERATED')
return { phase: 'exam', status: 'pending', label: '未出题', message: null, color: 'pending' }
return { phase: 'exam', status: 'unknown', label: '-', message: null, color: 'pending' }
}
轮询策略:
- 仅当存在
status === 'processing' && animating === true的任务时启动轮询 - 调用
fileAPI.getFiles({ pageNum: 1, pageSize: 50 })获取最新数据 - 对比
process_step_status和exam_status变化,更新对应任务 - 全部任务完成后自动停止轮询
- 用户关闭面板或切换 Tab 时停止轮询
3.2 UploadTaskPanel.vue — 面板容器
Props: 无(内部使用 composable)
布局:
┌─────────────────────────────────────────────┐
│ 📋 上传任务 [🔄 刷新] │
├─────────────────────────────────────────────┤
│ 全部(12) 处理中(3) 成功(7) 失败(2) │ ← 统计标签栏
├─────────────────────────────────────────────┤
│ [状态筛选 ▾] [🔍 搜索...] │ ← 工具栏
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ 📄 longrule.docx 采购部门 │ │
│ │ 14KB · 2026-05-29 12:01 │ │
│ │ ✅ 向量化完成 │ │ ← 向量化状态条
│ │ ✅ 生成4道题 │ │ ← 出题状态条
│ └─────────────────────────────────────┘ │
│ ... │
│ [< 1 2 >] 共12条 │ ← 分页
└─────────────────────────────────────────────┘
样式规范:全部使用 design-tokens.css 变量,与 ReadModule 现有风格一致(白色扁平设计)。
3.3 UploadTaskItem.vue — 任务卡片
Props:
{
task: Object, // 原始任务数据(file 表记录)
vectorStatus: Object, // parseVectorStatus() 返回值
examStatus: Object // parseExamStatus() 返回值
}
每个状态条的视觉设计:
状态条通用结构:
┌────────────────────────────────────────────┐
│ [图标] 状态文字 详情/错误消息 │
└────────────────────────────────────────────┘
| 状态 | 图标 | 文字颜色 | 背景 | 特殊效果 |
|---|---|---|---|---|
| pending(等待) | ⏸ 时钟 | --text-disabled | --bg-container | 无 |
| processing(处理中) | 🔄 spinner | --color-info | --bg-info | 脉冲动画 |
| success(成功) | ✓ CheckCircleOutlined | --color-success | --bg-success | 无 |
| error(失败) | ✗ CloseCircleOutlined | --color-error | --bg-error | 可展开查看错误消息 |
| unknown(未知) | - 问号 | --text-disabled | --bg-container | 无 |
错误消息展开:点击 error 状态条可展开/收起完整的 process_step_message 或 process_message 文本。
4. 与 ReadModule 集成方式
4.1 位置
在 ReadModule 的 Tab 栏下方、内容区上方,以可折叠面板形式呈现。默认折叠,上传后自动展开。
4.2 上传成功后联动
在 handleUploadSubmit() 成功回调中:
// 上传成功后
const result = await fileAPI.uploadFile(formData)
if (result.data.code === 200) {
const newFile = result.data.data
uploadTasks.addTask(newFile) // 添加到任务列表
uploadTasks.startPolling() // 启动轮询
showUploadTaskPanel.value = true // 展开任务面板
}
4.3 生命周期
onMounted: 如果有未完成的任务,自动加载并启动轮询onUnmounted: 停止轮询清理定时器- 切换到其他 Tab: 暂停轮询(可选)
5. API 调用
复用已有的 fileAPI.getFiles(params) 接口,参数:
{ pageNum: 1, pageSize: 100 } // 获取足够多的记录用于前端筛选
无需新增后端接口。通过 process_step_status 和 exam_status 字段在前端做状态判断。
6. 筛选与分页
6.1 状态筛选选项
| 筛选值 | 匹配条件 |
|---|---|
all |
显示全部 |
processing |
向量化中 OR 出题中 |
success |
向量化成功 AND (未出题 OR 出题成功) |
failed |
向量化失败 OR 出题失败 |
pending |
UPLOADED 或 UNGENERATED 且无进行中的步骤 |
6.2 分页
前端分页,与现有 paginatedDocuments 模式一致。