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

273 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 上传任务列表组件 — 设计规格
> **日期**: 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` 模式一致。