21 KiB
21 KiB
文件上传与任务状态管理优化设计文档
方案选择:A - 渐进式增强(用户体验优先)
版本:V1.0
日期:2026-05-30
状态:待审核
1. 设计概述
1.1 项目背景
基于《文件管理接口文档》(V2.12.0)规范,对现有文件上传功能和文件列表下的任务向量化/出题状态组件进行系统性用户体验优化。
1.2 优化目标
- 主要目标:提升用户交互体验,增强功能易用性和反馈机制
- 次要目标:改善性能表现,优化错误处理流程
- 非目标:不进行架构重构,不改变核心数据流,不引入新技术栈
1.3 设计原则
- KISS原则:保持简单,避免过度工程化
- 增量改进:每个优化点独立可回滚
- 向后兼容:不破坏现有API和组件接口
- 用户驱动:所有改进围绕实际使用场景
- 渐进增强:在现有代码基础上添加功能,不重写
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):
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):
<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 组件的错误标签旁
- 行为:
- 点击后立即调用重新处理API
- 按钮变为加载状态(转圈图标)
- 成功后自动刷新任务列表
- 失败后显示错误详情
- UI设计:
❌ 向量化失败 [🔄 重试] [📋 详情]
FR-07: 批量删除功能
- 使用场景:清理大量失败/已完成的历史任务
- 交互流程:
- 进入批量模式(勾选框出现)
- 选择多个任务(支持全选)
- 点击"删除选中项(N)"按钮
- 弹出确认对话框(显示即将删除的任务数)
- 执行删除并显示进度
- 权限控制:仅管理员可见此功能
FR-08: Toast通知系统
- 触发事件:
- ✅ 上传成功:"✅ 文件「{filename}」上传成功,正在处理中..."
- ✅ 向量化完成:"🎉 「{filename}」向量化完成,准备生成题目..."
- ✅ 出题完成:"📝 「{filename}」已生成{count}道题目"
- ❌ 处理失败:"❌ 「{filename}」处理失败:{原因}"
- 配置选项:
- 显示时长:成功=3秒,失败=5秒
- 位置:右下角
- 可关闭:是
- 堆叠方式:垂直堆叠(最多3条同时显示)
FR-09: 智能轮询策略
-
当前问题:固定5秒轮询,无论是否有活跃任务
-
优化方案:
任务状态 轮询间隔 说明 有处理中任务 3秒 快速反馈 全部完成/失败 10秒 降低频率 页面不可见 30秒 节省资源 无任何任务 停止轮询 完全停止 -
实现方式:使用
document.visibilitychange事件检测页面可见性
FR-10: 加载骨架屏
- 应用场景:
- 首次加载任务列表时
- 刷新任务状态时(>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 修改部分):
// 新增智能轮询配置
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 修改部分):
<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 | 桌面端 |
关键改动:
-
UploadTaskItem 组件:
- 字体缩小(12px → 11px)
- 状态标签换行显示
- 错误信息默认展开(无需点击"详情")
-
文件列表:
- 卡片布局改为单列
- 操作按钮改为底部固定栏
- 添加下拉刷新手势支持
-
上传区域:
- 拖拽区域全屏宽度
- 点击区域增大(最小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 性能优化措施
-
按需加载:新组件使用动态导入
const DropZone = defineAsyncComponent(() => import('./components/ui/DropZone.vue')) -
防抖/节流:
- 搜索输入:debounce 300ms
- 窗口resize:throttle 150ms
- 滚动事件:passive listener
-
内存管理:
- 轮询定时器在组件卸载时清除
- 大文件上传完成后释放引用
- 图片/文件预览使用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 集成测试
-
上传流程测试:
- 选择文件 → 验证 → 上传 → 进度显示 → 完成
- 拖拽文件 → 验证 → 上传 → 进度显示 → 完成
- 选择无效文件 → 显示错误 → 修正后重试
-
任务状态测试:
- 上传后观察状态流转(UPLOADED→VECTORIZING→VECTORIZED→EXAM_GENERATING→COMPLETED)
- 模拟失败场景 → 点击重试 → 验证重试逻辑
- 批量选择 → 删除 → 确认删除成功
-
边界情况测试:
- 同时上传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
- Vue 3 官方文档:https://vuejs.org/
- Ant Design Vue 组件库:https://antdv.com/
- MDN Drag & Drop API:https://developer.mozilla.org/en-US/docs/Web/API/Drag_and_Drop
10.2 术语表
| 术语 | 定义 |
|---|---|
| 向量化(Vectorize) | 将文档内容转换为向量表示,用于语义搜索 |
| 出题(Exam Generation) | 基于文档内容自动生成考试题目 |
| 轮询(Polling) | 客户端定期向服务器请求最新状态的机制 |
| 骨架屏(Skeleton) | 内容加载时的占位动画效果 |
| Toast通知 | 短暂出现的消息提示,通常位于屏幕角落 |
文档结束
请审核本设计文档,确认后我们将进入实施阶段。