# 异步文件上传功能实施计划 - Part 4: 前端开发

## 文档信息
- **创建日期**: 2026-03-05
- **版本**: v1.1
- **作者**: Claude
- **关联设计**: [前端设计文档](../design/2026-03-05-async-file-upload-frontend-design.md)
- **变更说明**: 移除WebSocket，改为页面轮询机制

## 任务概述

根据前端设计文档，扩展UploadData.vue组件实现异步批量上传功能。

**预计工时**: 4.5个工作日

## 任务清单

### 任务 1: API接口封装（0.5天）

**文件**: `ruoyi-ui/src/api/ccdiProjectUpload.js`

**工作内容**:
```javascript
// 批量上传文件
export function batchUploadFiles(projectId, files) {
  const formData = new FormData()
  files.forEach(file => formData.append('files', file))
  formData.append('projectId', projectId)
  
  return request({
    url: '/ccdi/file-upload/batch',
    method: 'post',
    data: formData,
    timeout: 300000
  })
}

// 查询文件上传记录列表
export function getFileUploadList(params) {
  return request({
    url: '/ccdi/file-upload/list',
    method: 'get',
    params
  })
}

// 查询文件上传统计
export function getFileUploadStatistics(projectId) {
  return request({
    url: `/ccdi/file-upload/statistics/${projectId}`,
    method: 'get'
  })
}
```

### 任务 2: 批量上传弹窗（1天）

**文件**: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`

**主要修改**:
1. 添加批量上传弹窗状态
2. 修改`handleUploadClick`方法
3. 实现文件选择和校验逻辑
4. 实现批量上传功能

**关键代码**:
```javascript
// 批量上传
async handleBatchUpload() {
  if (this.selectedFiles.length === 0) {
    this.$message.warning('请选择要上传的文件')
    return
  }
  
  this.uploadLoading = true
  
  try {
    await batchUploadFiles(
      this.projectId,
      this.selectedFiles.map(f => f.raw)
    )
    
    this.uploadLoading = false
    this.batchUploadDialogVisible = false
    
    this.$message.success('上传任务已提交，请查看处理进度')
    
    // 刷新数据并启动轮询
    await Promise.all([
      this.loadStatistics(),
      this.loadFileList()
    ])
    
    this.startPolling()
    
  } catch (error) {
    this.uploadLoading = false
    this.$message.error('上传失败：' + (error.msg || '未知错误'))
  }
}
```

### 任务 3: 统计卡片（0.5天）

**工作内容**:
1. 添加统计数据状态
2. 实现统计卡片组件
3. 实现点击筛选功能

**模板代码**:
```vue
<div class="statistics-section">
  <div class="stat-card" @click="handleStatusFilter('uploading')">
    <div class="stat-icon uploading">
      <i class="el-icon-upload"></i>
    </div>
    <div class="stat-content">
      <div class="stat-label">上传中</div>
      <div class="stat-value">{{ statistics.uploading }}</div>
    </div>
  </div>
  <!-- 其他3个统计卡片 -->
</div>
```

### 任务 4: 文件列表（1天）

**工作内容**:
1. 添加文件列表状态
2. 实现文件列表组件
3. 实现分页和筛选
4. 实现操作按钮

**关键方法**:
```javascript
// 加载文件列表
async loadFileList() {
  this.listLoading = true
  
  try {
    const res = await getFileUploadList({
      projectId: this.projectId,
      fileStatus: this.queryParams.fileStatus,
      pageNum: this.queryParams.pageNum,
      pageSize: this.queryParams.pageSize
    })
    
    this.fileList = res.rows || []
    this.total = res.total || 0
    
  } finally {
    this.listLoading = false
  }
}
```

### 任务 5: 轮询机制（0.5天）

**优先级**: P0  
**依赖**: 任务2、任务3、任务4完成

**工作内容**:

1. **添加轮询状态**:
```javascript
data() {
  return {
    // 轮询相关
    pollingTimer: null,
    pollingEnabled: false,
    pollingInterval: 5000  // 5秒轮询间隔
  }
}
```

2. **生命周期钩子**:
```javascript
mounted() {
  this.loadStatistics()
  this.loadFileList()
  
  // 检查是否需要启动轮询
  this.$nextTick(() => {
    if (this.statistics.uploading > 0 || this.statistics.parsing > 0) {
      this.startPolling()
    }
  })
},

beforeDestroy() {
  this.stopPolling()
}
```

3. **轮询方法**:
```javascript
methods: {
  /**
   * 启动轮询
   */
  startPolling() {
    if (this.pollingEnabled) {
      return  // 已经在轮询中
    }
    
    this.pollingEnabled = true
    console.log('启动轮询')
    
    const poll = () => {
      if (!this.pollingEnabled) {
        return
      }
      
      // 刷新统计数据和列表
      Promise.all([
        this.loadStatistics(),
        this.loadFileList()
      ]).then(() => {
        // 检查是否需要继续轮询
        if (this.statistics.uploading === 0 && 
            this.statistics.parsing === 0) {
          this.stopPolling()
          console.log('所有任务已完成，停止轮询')
          return
        }
        
        // 继续下一次轮询
        this.pollingTimer = setTimeout(poll, this.pollingInterval)
      }).catch(error => {
        console.error('轮询失败:', error)
        // 发生错误时继续轮询
        this.pollingTimer = setTimeout(poll, this.pollingInterval)
      })
    }
    
    // 立即执行一次
    poll()
  },
  
  /**
   * 停止轮询
   */
  stopPolling() {
    this.pollingEnabled = false
    
    if (this.pollingTimer) {
      clearTimeout(this.pollingTimer)
      this.pollingTimer = null
    }
    
    console.log('停止轮询')
  },
  
  /**
   * 手动刷新
   */
  async handleManualRefresh() {
    await Promise.all([
      this.loadStatistics(),
      this.loadFileList()
    ])
    
    this.$message.success('刷新成功')
    
    // 如果有进行中的任务，启动轮询
    if (this.statistics.uploading > 0 || this.statistics.parsing > 0) {
      this.startPolling()
    }
  },
  
  /**
   * 状态筛选
   */
  handleStatusFilter(status) {
    this.queryParams.fileStatus = status
    this.queryParams.pageNum = 1
    this.loadFileList()
  }
}
```

4. **在模板中添加刷新按钮**:
```vue
<el-button 
  icon="el-icon-refresh" 
  @click="handleManualRefresh"
>
  刷新
</el-button>
```

#### 5.2 验证方式

1. **启动轮询测试**：
   - 上传文件后，检查控制台输出"启动轮询"
   - 观察5秒后数据是否自动刷新

2. **停止轮询测试**：
   - 等待所有文件处理完成
   - 检查控制台输出"停止轮询"

3. **手动刷新测试**：
   - 点击刷新按钮
   - 验证数据立即更新
   - 验证提示消息显示

4. **页面销毁测试**：
   - 切换到其他页面
   - 检查控制台输出"停止轮询"
   - 确认定时器被清除

### 任务 6: 联调测试（1天）

**测试项**:
1. 批量上传功能
2. 统计卡片展示和筛选
3. 文件列表展示和分页
4. 轮询机制（启动、停止、手动刷新）
5. 操作按钮（查看流水、查看错误）

## 验收标准

- [ ] 所有API接口正常调用
- [ ] 批量上传弹窗正常工作
- [ ] 统计卡片正常显示和筛选
- [ ] 文件列表正常展示和操作
- [ ] 轮询机制正常（自动启动/停止/手动刷新）
- [ ] 所有测试项通过

## 轮询优化建议（可选）

**智能轮询间隔**：
```javascript
// 根据活跃任务数动态调整轮询间隔
getPollingInterval() {
  const { uploading, parsing } = this.statistics
  const activeCount = uploading + parsing
  
  if (activeCount > 50) {
    return 3000  // 大量任务时，3秒轮询
  } else if (activeCount > 10) {
    return 5000  // 正常情况，5秒轮询
  } else {
    return 10000  // 少量任务时，10秒轮询
  }
}
```

**用户体验优化**：
- 在页面顶部显示"自动刷新中..."状态提示
- 支持用户手动开关轮询开关

---

**文档结束**
