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

718 lines
21 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.
# 文件上传与任务状态管理优化设计文档
**方案选择**A - 渐进式增强(用户体验优先)
**版本**V1.0
**日期**2026-05-30
**状态**:待审核
---
## 1. 设计概述
### 1.1 项目背景
基于《文件管理接口文档》V2.12.0)规范,对现有文件上传功能和文件列表下的任务向量化/出题状态组件进行系统性用户体验优化。
### 1.2 优化目标
- **主要目标**:提升用户交互体验,增强功能易用性和反馈机制
- **次要目标**:改善性能表现,优化错误处理流程
- **非目标**:不进行架构重构,不改变核心数据流,不引入新技术栈
### 1.3 设计原则
1. **KISS原则**:保持简单,避免过度工程化
2. **增量改进**:每个优化点独立可回滚
3. **向后兼容**不破坏现有API和组件接口
4. **用户驱动**:所有改进围绕实际使用场景
5. **渐进增强**:在现有代码基础上添加功能,不重写
---
## 2. 当前实现分析
### 2.1 现有架构
```
前端组件结构:
├── FileSelector.vue # 文件选择器用于AI对话中选择文件
├── exam/
│ ├── UploadTaskItem.vue # 任务状态展示卡片
│ ├── composables/
│ │ ├── useUploadTasks.js # 任务状态管理逻辑
│ │ └── useTaskManager.js # 任务管理器
│ ├── GeneratePanel.vue # 向量化/出题面板
│ └── ExamModuleContainer.vue
├── api/
│ └── file.js # 文件API接口定义
└── views/
└── ReaderPage.vue # 文档阅读页面
```
### 2.2 已识别问题清单
#### **P0 - 必须修复(影响核心功能)**
| 问题ID | 问题描述 | 影响范围 | 严重程度 |
|--------|---------|----------|----------|
| P0-01 | 无上传进度条显示 | 文件上传体验 | 🔴 高 |
| P0-02 | 错误状态无重试机制 | 任务失败处理 | 🔴 高 |
| P0-03 | 轮询策略固定5秒间隔 | 性能浪费 | 🟡 中 |
#### **P1 - 应该改进(显著提升体验)**
| 问题ID | 问题描述 | 影响范围 | 严重程度 |
|--------|---------|----------|----------|
| P1-01 | 不支持拖拽上传 | 上传便捷性 | 🟡 中 |
| P1-02 | 无前端文件验证 | 错误预防 | 🟡 中 |
| P1-03 | 错误提示不够友好 | 用户理解 | 🟡 中 |
| P1-04 | 不支持批量操作 | 效率提升 | 🟢 低 |
| P1-05 | 移动端适配不完善 | 多设备支持 | 🟢 低 |
#### **P2 - 可以优化(锦上添花)**
| 问题ID | 问题描述 | 影响范围 | 严重程度 |
|--------|---------|----------|----------|
| P2-01 | 无状态变更通知提醒 | 用户感知 | 🟢 低 |
| P2-02 | 缺少加载骨架屏 | 视觉体验 | 🟢 低 |
| P2-03 | 任务列表无虚拟滚动 | 大列表性能 | 🟢 低 |
---
## 3. 详细设计方案
### 3.1 模块一:文件上传增强(优先级:🔴 最高)
#### **3.1.1 功能需求**
##### **FR-01: 上传进度条**
- **描述**:显示实时上传百分比和预计剩余时间
- **触发条件**:文件大小 > 1MB 或上传时间 > 2秒时自动显示
- **UI设计**
```
┌─────────────────────────────────────┐
│ 📄 制度管理办法.docx (2.3MB) │
│ ████████████░░░░░░ 65% 剩余 3秒 │
└─────────────────────────────────────┘
```
##### **FR-02: 拖拽上传支持**
- **描述**:支持将文件从桌面/资源管理器拖拽到上传区域
- **技术方案**HTML5 Drag and Drop API
- **交互细节**
- 拖拽悬停时:边框高亮 + 提示文字"释放以上传"
- 拖拽离开时:恢复原状
- 支持多文件同时拖拽
##### **FR-03: 前端文件验证**
- **验证规则**
- ✅ 支持的文件类型:`.docx, .pdf, .txt, .md, .xlsx, .pptx`
- ✅ 最大文件大小50MB可配置
- ✅ 文件名长度≤200字符
- ✅ 特殊字符检查(禁止 `\ / : * ? " < > |`
- **错误提示**
- 类型不支持:"❌ 不支持的文件格式,请上传 .docx/.pdf/.txt 等文档"
- 文件过大:"❌ 文件超过50MB限制请压缩后重试"
- 其他错误:"⚠️ 文件验证失败:{具体原因}"
##### **FR-04: 上传前预览**
- **预览信息**
- 文件图标(根据扩展名显示不同图标)
- 文件名(高亮显示)
- 文件大小格式化为KB/MB
- 文件类型标签(如"Word文档"、"PDF文件"
##### **FR-05: 友好的错误处理**
- **错误分类**
- 🌐 **网络错误**:连接超时、服务器无响应
- 📁 **文件错误**:格式不支持、文件损坏
- 🔐 **权限错误**:未登录、无上传权限
- ⚙️ **服务器错误**500内部错误、存储空间不足
- **错误展示**
- 图标 + 标题 + 详细说明 + 操作建议
- 示例:
```
⚠️ 上传失败
原因:服务器响应超时(>60秒
建议:
• 检查网络连接是否正常
• 尝试压缩文件后重新上传
• 如持续失败,请联系管理员
[重新上传] [取消]
```
#### **3.1.2 技术实现方案**
##### **修改文件清单**
| 文件路径 | 改动类型 | 改动量 | 说明 |
|---------|---------|--------|------|
| `src/components/FileSelector.vue` | 修改 | ~150行 | 添加拖拽、验证、进度条 |
| `src/api/file.js` | 修改 | ~20行 | 添加onUploadProgress回调 |
| `src/utils/fileValidator.js` | 新增 | ~80行 | 文件验证工具函数 |
| `src/components/ui/ProgressBar.vue` | 新增 | ~60行 | 可复用进度条组件 |
| `src/components/ui/DropZone.vue` | 新增 | ~90行 | 拖拽上传区域组件 |
##### **核心代码示例**
**Axios进度监听** (`src/api/file.js`)
```javascript
uploadFile: (formData, onProgress) => {
return apiClient.post('/file/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
)
onProgress({
loaded: progressEvent.loaded,
total: progressEvent.total,
percent: percentCompleted
})
}
}
})
}
```
**拖拽上传组件** (`src/components/ui/DropZone.vue`)
```vue
<template>
<div
class="drop-zone"
:class="{ 'is-dragging': isDragging }"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver"
@dragleave.prevent="onDragLeave"
@drop.prevent="onDrop"
@click="$refs.fileInput.click()"
>
<div v-if="!isDragging" class="drop-zone-content">
<InboxOutlined class="drop-icon" />
<p class="drop-text">拖拽文件到此处,或<span class="link">点击选择</span></p>
<p class="drop-hint">支持 .docx .pdf .txt .xlsx .pptx最大 50MB</p>
</div>
<div v-else class="drop-zone-active">
<span class="active-text">📥 释放文件以上传</span>
</div>
<input
ref="fileInput"
type="file"
multiple
:accept="acceptedTypes"
@change="onFileSelected"
style="display: none"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { InboxOutlined } from '@ant-design/icons-vue'
const props = defineProps({
acceptedTypes: {
type: String,
default: '.docx,.pdf,.txt,.md,.xlsx,.pptx'
},
maxSize: {
type: Number,
default: 50 * 1024 * 1024 // 50MB
}
})
const emit = defineEmits(['files-selected', 'error'])
const isDragging = ref(false)
const fileInput = ref(null)
function onDragEnter(e) {
isDragging.value = true
}
function onDragOver(e) {
e.dataTransfer.dropEffect = 'copy'
}
function onDragLeave(e) {
if (!e.currentTarget.contains(e.relatedTarget)) {
isDragging.value = false
}
}
function onDrop(e) {
isDragging.value = false
const files = Array.from(e.dataTransfer.files)
validateAndEmit(files)
}
function onFileSelected(e) {
const files = Array.from(e.target.files)
validateAndEmit(files)
// 重置input以允许重复选择相同文件
e.target.value = ''
}
function validateAndEmit(files) {
const validFiles = []
const errors = []
files.forEach(file => {
// 验证文件类型
const ext = '.' + file.name.split('.').pop().toLowerCase()
if (!props.acceptedTypes.includes(ext)) {
errors.push({ file: file.name, reason: '不支持的文件格式' })
return
}
// 验证文件大小
if (file.size > props.maxSize) {
errors.push({ file: file.name, reason: `文件过大 (${formatSize(file.size)},限制${formatSize(props.maxSize)})` })
return
}
validFiles.push(file)
})
if (validFiles.length > 0) {
emit('files-selected', validFiles)
}
if (errors.length > 0) {
emit('error', errors)
}
}
</script>
```
---
### 3.2 模块二:任务状态优化(优先级:🟡 高)
#### **3.2.1 功能需求**
##### **FR-06: 一键重试按钮**
- **触发条件**:任务状态为 error 时显示
- **位置**UploadTaskItem 组件的错误标签旁
- **行为**
1. 点击后立即调用重新处理API
2. 按钮变为加载状态(转圈图标)
3. 成功后自动刷新任务列表
4. 失败后显示错误详情
- **UI设计**
```
❌ 向量化失败 [🔄 重试] [📋 详情]
```
##### **FR-07: 批量删除功能**
- **使用场景**:清理大量失败/已完成的历史任务
- **交互流程**
1. 进入批量模式(勾选框出现)
2. 选择多个任务(支持全选)
3. 点击"删除选中项(N)"按钮
4. 弹出确认对话框(显示即将删除的任务数)
5. 执行删除并显示进度
- **权限控制**:仅管理员可见此功能
##### **FR-08: Toast通知系统**
- **触发事件**
- ✅ 上传成功:"✅ 文件「{filename}」上传成功,正在处理中..."
- ✅ 向量化完成:"🎉 「{filename}」向量化完成,准备生成题目..."
- ✅ 出题完成:"📝 「{filename}」已生成{count}道题目"
- ❌ 处理失败:"❌ 「{filename}」处理失败:{原因}"
- **配置选项**
- 显示时长:成功=3秒失败=5秒
- 位置:右下角
- 可关闭:是
- 堆叠方式垂直堆叠最多3条同时显示
##### **FR-09: 智能轮询策略**
- **当前问题**固定5秒轮询无论是否有活跃任务
- **优化方案**
| 任务状态 | 轮询间隔 | 说明 |
|---------|---------|------|
| 有处理中任务 | 3秒 | 快速反馈 |
| 全部完成/失败 | 10秒 | 降低频率 |
| 页面不可见 | 30秒 | 节省资源 |
| 无任何任务 | 停止轮询 | 完全停止 |
- **实现方式**:使用 `document.visibilitychange` 事件检测页面可见性
##### **FR-10: 加载骨架屏**
- **应用场景**
1. 首次加载任务列表时
2. 刷新任务状态时(>500ms
- **UI效果**:灰色脉冲动画块模拟真实内容布局
- **组件复用**:创建通用 SkeletonCard 组件
#### **3.2.2 技术实现方案**
##### **修改文件清单**
| 文件路径 | 改动类型 | 改动量 | 说明 |
|---------|---------|--------|------|
| `src/components/exam/UploadTaskItem.vue` | 修改 | ~40行 | 添加重试按钮 |
| `src/components/exam/composables/useUploadTasks.js` | 修改 | ~80行 | 智能轮询+批量操作 |
| `src/components/ui/SkeletonCard.vue` | 新增 | ~50行 | 骨架屏组件 |
| `src/components/ui/ToastNotification.vue` | 新增 | ~90行 | 通知组件 |
| `src/utils/notification.js` | 新增 | ~60行 | 通知工具函数 |
##### **核心代码示例**
**智能轮询** (`useUploadTasks.js` 修改部分)
```javascript
// 新增智能轮询配置
const POLL_CONFIG = {
active: 3000, // 有活跃任务时3秒
idle: 10000, // 全部空闲时10秒
hidden: 30000, // 页面隐藏时30秒
maxInterval: 30000 // 最大间隔上限
}
let currentInterval = POLL_CONFIG.idle
let visibilityHandler = null
function updatePollingStrategy() {
const hasActiveTasks = shouldPoll()
const isHidden = document.hidden
let newInterval
if (isHidden) {
newInterval = POLL_CONFIG.hidden
} else if (hasActiveTasks) {
newInterval = POLL_CONFIG.active
} else {
newInterval = POLL_CONFIG.idle
}
// 仅当间隔变化时才重启定时器
if (newInterval !== currentInterval) {
currentInterval = newInterval
stopPolling()
if (tasks.value.length > 0) {
startPolling()
}
console.log(`[useUploadTasks] 轮询间隔调整为 ${currentInterval}ms`)
}
}
// 监听页面可见性
function setupVisibilityListener() {
if (visibilityHandler) return
visibilityHandler = () => {
updatePollingStrategy()
}
document.addEventListener('visibilitychange', visibilityHandler)
}
onMounted(() => {
setupVisibilityListener()
})
onUnmounted(() => {
if (visibilityHandler) {
document.removeEventListener('visibilitychange', visibilityHandler)
}
})
```
**一键重试** (`UploadTaskItem.vue` 修改部分)
```vue
<template>
<!-- 在错误标签旁添加重试按钮 -->
<span v-if="vectorStatus.status === 'error'" class="uti-tag error">
{{ vectorStatus.label }}
<button class="retry-btn" @click="handleRetry" :disabled="retrying">
<LoadingOutlined v-if="retrying" spin />
<ReloadOutlined v-else />
重试
</button>
</span>
</template>
<script setup>
import { ref } from 'vue'
import { ReloadOutlined, LoadingOutlined } from '@ant-design/icons-vue'
const props = defineProps({
task: Object,
vectorStatus: Object,
examStatus: Object
})
const emit = defineEmits(['retry'])
const retrying = ref(false)
async function handleRetry() {
retrying.value = true
try {
await emit('retry', props.task.id)
} finally {
setTimeout(() => { retrying.value = false }, 1000)
}
}
</script>
```
---
### 3.3 模块三:移动端适配(优先级:🟢 中)
#### **3.3.1 适配策略**
##### **断点定义**
| 断点名称 | 宽度范围 | 目标设备 |
|---------|---------|---------|
| `sm` | ≥640px | 大屏手机横屏/小平板 |
| `md` | ≥768px | 平板竖屏 |
| `lg` | ≥1024px | 桌面端 |
##### **关键改动**
1. **UploadTaskItem 组件**
- 字体缩小12px → 11px
- 状态标签换行显示
- 错误信息默认展开(无需点击"详情"
2. **文件列表**
- 卡片布局改为单列
- 操作按钮改为底部固定栏
- 添加下拉刷新手势支持
3. **上传区域**
- 拖拽区域全屏宽度
- 点击区域增大最小44px触控目标
---
## 4. 数据流与接口变更
### 4.1 API接口调整
#### **新增接口**(可选,如后端已支持则使用):
| 接口 | 方法 | 用途 |
|------|------|------|
| `/api/file/{id}/retry` | POST | 重试失败任务 |
| `/api/file/batch-delete` | POST | 批量删除任务 |
#### **兼容性处理**
如果后端暂不支持上述接口,前端降级方案:
- **重试功能**:调用原有上传接口重新上传同一文件
- **批量删除**循环调用单个删除接口带loading状态
### 4.2 数据流图
```
用户操作
FileSelector.vue拖拽/选择文件)
fileValidator.js前端验证
├─ 通过 → 显示预览 → 开始上传
└─ 失败 → 显示友好错误提示
api/file.js uploadFile带进度回调
ProgressBar.vue实时更新进度
上传完成
useUploadTasks.js添加任务到列表
智能轮询3-30秒动态调整
UploadTaskItem.vue展示状态
├─ 处理中 → 动画图标
├─ 完成 → 成功图标 + Toast通知
└─ 失败 → 错误图标 + 重试按钮
```
---
## 5. 性能与兼容性考虑
### 5.1 性能优化措施
1. **按需加载**:新组件使用动态导入
```javascript
const DropZone = defineAsyncComponent(() => import('./components/ui/DropZone.vue'))
```
2. **防抖/节流**
- 搜索输入debounce 300ms
- 窗口resizethrottle 150ms
- 滚动事件passive listener
3. **内存管理**
- 轮询定时器在组件卸载时清除
- 大文件上传完成后释放引用
- 图片/文件预览使用URL.revokeObjectURL()
### 5.2 浏览器兼容性
| 功能 | 最低版本 | 降级方案 |
|------|---------|---------|
| Drag & Drop API | IE10+, 所有现代浏览器 | 回退到点击上传 |
| Progress Event | IE10+, 所有现代浏览器 | 不显示进度条 |
| Visibility API | IE10+, Chrome 13+ | 固定10秒轮询 |
| CSS Grid/Flexbox | IE11+ (partial), 现代浏览器全支持 | 使用float fallback |
---
## 6. 测试计划
### 6.1 单元测试
| 测试场景 | 输入 | 预期输出 | 优先级 |
|---------|------|---------|--------|
| 文件类型验证 | `test.exe` | 返回错误"不支持的格式" | P0 |
| 文件大小验证 | 60MB文件 | 返回错误"超过50MB限制" | P0 |
| 进度计算 | loaded=50, total=100 | percent=50 | P0 |
| 拖拽事件处理 | 有效文件 | 触发files-selected事件 | P1 |
| 轮询间隔调整 | 页面hidden | 间隔变为30秒 | P1 |
| 重试按钮点击 | error状态任务 | 调用retry事件 | P0 |
### 6.2 集成测试
1. **上传流程测试**
- 选择文件 → 验证 → 上传 → 进度显示 → 完成
- 拖拽文件 → 验证 → 上传 → 进度显示 → 完成
- 选择无效文件 → 显示错误 → 修正后重试
2. **任务状态测试**
- 上传后观察状态流转UPLOADED→VECTORIZING→VECTORIZED→EXAM_GENERATING→COMPLETED
- 模拟失败场景 → 点击重试 → 验证重试逻辑
- 批量选择 → 删除 → 确认删除成功
3. **边界情况测试**
- 同时上传10个文件
- 网络中断后恢复
- 页面刷新后状态保持
- 移动端触摸操作
### 6.3 兼容性测试
- ✅ Chrome 90+
- ✅ Firefox 88+
- ✅ Safari 14+
- ✅ Edge 90+
- ⚠️ iOS Safari 14+(移动端)
- ⚠️ Android Chrome 90+
---
## 7. 实施路线图
### Phase 1核心功能第1天- 6小时
**上午3小时**
- [ ] 创建 `src/utils/fileValidator.js` 文件验证工具
- [ ] 创建 `src/components/ui/ProgressBar.vue` 进度条组件
- [ ] 修改 `src/api/file.js` 添加进度回调支持
**下午3小时**
- [ ] 创建 `src/components/ui/DropZone.vue` 拖拽组件
- [ ] 修改 `src/components/FileSelector.vue` 集成新功能
- [ ] 测试上传流程(正常/异常场景)
### Phase 2任务状态优化第2天- 6小时
**上午3小时**
- [ ] 修改 `UploadTaskItem.vue` 添加重试按钮
- [ ] 修改 `useUploadTasks.js` 实现智能轮询
- [ ] 创建 `src/components/ui/ToastNotification.vue`
**下午3小时**
- [ ] 实现批量删除功能
- [ ] 创建骨架屏组件
- [ ] 测试任务状态流转
### Phase 3打磨与测试第3天- 6小时
**上午3小时**
- [ ] 移动端响应式适配
- [ ] 跨浏览器测试
- [ ] 性能 profiling
**下午3小时**
- [ ] 用户体验走查
- [ ] Bug修复
- [ ] 文档编写
---
## 8. 风险评估与缓解
### 8.1 技术风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|-------|------|---------|
| 后端不支持重试API | 中 | 中 | 前端降级为重新上传 |
| 大文件上传内存溢出 | 低 | 高 | 使用分片上传Phase 2考虑 |
| 拖拽API兼容性问题 | 低 | 低 | Feature detection + 降级方案 |
| 轮询性能开销 | 低 | 中 | 智能间隔 + 页面不可见时暂停 |
### 8.2 业务风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|-------|------|---------|
| 用户不接受新交互 | 低 | 低 | A/B测试 + 快速回滚能力 |
| 移动端体验不佳 | 中 | 中 | 充分的设备测试 |
| 与现有工作流冲突 | 低 | 中 | 保持向后兼容 + 渐进启用 |
---
## 9. 成功指标KPIs
### 9.1 定量指标
| 指标 | 当前值 | 目标值 | 测量方法 |
|------|-------|--------|---------|
| 上传操作成功率 | 未知 | ≥98% | 错误日志统计 |
| 平均上传等待感知时间 | 未知 | 减少30% | 用户调研 |
| 任务状态查询次数/会话 | 固定12次/分钟 | 动态3-30次 | 性能监控 |
| 错误重试成功率 | 0% | ≥80% | 重试按钮点击率 |
### 9.2 定性指标
- ✅ 用户能够清晰了解上传进度
- ✅ 任务失败时有明确的解决路径(重试按钮)
- ✅ 移动端操作流畅无明显卡顿
- ✅ 错误信息易于理解和行动
---
## 10. 附录
### 10.1 参考文档
- [文件管理接口文档 V2.12.0](./文件管理接口文档.md)
- Vue 3 官方文档https://vuejs.org/
- Ant Design Vue 组件库https://antdv.com/
- MDN Drag & Drop APIhttps://developer.mozilla.org/en-US/docs/Web/API/Drag_and_Drop
### 10.2 术语表
| 术语 | 定义 |
|------|------|
| **向量化(Vectorize)** | 将文档内容转换为向量表示,用于语义搜索 |
| **出题(Exam Generation)** | 基于文档内容自动生成考试题目 |
| **轮询(Polling)** | 客户端定期向服务器请求最新状态的机制 |
| **骨架屏(Skeleton)** | 内容加载时的占位动画效果 |
| **Toast通知** | 短暂出现的消息提示,通常位于屏幕角落 |
---
**文档结束**
*请审核本设计文档,确认后我们将进入实施阶段。*