Files
aue/docs/superpowers/specs/2026-05-29-upload-task-panel-design.md
2026-06-03 13:16:30 +08:00

12 KiB
Raw Permalink Blame History

上传任务列表组件 — 设计规格

日期: 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_statusexam_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_messageprocess_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_statusexam_status 字段在前端做状态判断。


6. 筛选与分页

6.1 状态筛选选项

筛选值 匹配条件
all 显示全部
processing 向量化中 OR 出题中
success 向量化成功 AND (未出题 OR 出题成功)
failed 向量化失败 OR 出题失败
pending UPLOADED 或 UNGENERATED 且无进行中的步骤

6.2 分页

前端分页,与现有 paginatedDocuments 模式一致。