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

> **日期**: 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` 模式一致。
