# 上传任务列表组件 — 设计规格 > **日期**: 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 **职责**:所有任务数据的获取、缓存、状态解析、轮询控制。 **导出接口**: ```javascript 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 } } ``` **状态解析函数核心逻辑**: ```javascript 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**: ```javascript { 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()` 成功回调中: ```javascript // 上传成功后 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)` 接口,参数: ```javascript { 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` 模式一致。