需求分析与设计挑战

在开始编码前,我们需要明确核心需求:

  1. 多维度数据展示:区分上传/下载记录,区分进行中/已完成状态
  2. 高性能数据处理:支持数千条记录的快速筛选、排序
  3. 实时状态更新:上传/下载进度实时显示
  4. 复杂操作支持:全选、批量删除、搜索、排序
  5. 特殊场景处理:压缩包下载的特殊状态管理
  6. 用户体验优化:平滑过渡、加载状态、空数据提示
  7. 跨平台兼容:支持桌面和移动设备

这些需求带来了几大技术挑战:

  • 如何高效管理大量动态数据
  • 如何实现上传/下载队列与UI的同步
  • 如何处理特殊场景(如压缩包下载)的不同状态
  • 如何确保大量数据下的渲染性能

二、架构设计:模块化与状态管理

1. 核心数据结构设计

interface TransferItem {
  id: string;
  fileName: string;
  fileSize: string;
  uploadTime?: string;
  downloadTime?: string;
  progress: number;
  status: 'uploading' | 'downloading' | 'completed' | 'failed';
  uploadedFileId?: string; // 上传成功后的文件ID
  flId?: number; // API记录ID,用于删除操作
  flFolderId?: number; // 文件所属文件夹ID
  // 压缩包相关字段
  fctIsComplete?: number; // 压缩包是否完成,0未完成,1已完成,-1失败
  flFileId?: string; // 文件存储编号
  fctCompressName?: string; // 压缩包名称
  flDownloadUrl?: string; // 下载URL
}

2. 状态管理分层设计

组件采用了分层状态管理策略:

// 本地队列数据(实时性高)
uploadingList: TransferItem[] = []; // 上传中
uploadCompletedList: TransferItem[] = []; // 已完成(缓存)
downloadingList: TransferItem[] = []; // 下载中
downloadCompletedList: TransferItem[] = []; // 已完成(缓存)

// API数据(持久化)
apiUploadCompletedList: TransferItem[] = []; // API返回的已完成上传
apiDownloadCompletedList: TransferItem[] = []; // API返回的已完成下载
apiUploadTotal = 0; // 上传总数
apiDownloadTotal = 0; // 下载总数

这种设计解决了数据一致性实时性的矛盾:本地队列保证操作实时响应,API数据确保记录持久化。

三、核心功能实现:从队列管理到UI同步

1. 队列管理器集成

组件使用了一个自定义的uploadQueueManager处理文件传输队列:

// 初始化队列监听
mounted() {
  // 监听队列更新
  this.queueUpdateListener = () => {
    this.updateDataFromQueue();
    // 当有任务完成时,重新加载API数据
    this.checkAndReloadApiData();
  };
  uploadQueueManager.on('queueUpdate', this.queueUpdateListener);
  // 初始化数据
  this.updateDataFromQueue();
  // 首次加载API数据
  this.loadApiData();
}

队列更新时的处理逻辑非常关键,确保UI与数据同步:

updateDataFromQueue() {
  const uploadingTasks = uploadQueueManager.getUploadingTasks();
  const completedUploadTasks = uploadQueueManager.getCompletedUploadTasks();
  const downloadingTasks = uploadQueueManager.getDownloadingTasks();
  const completedDownloadTasks = uploadQueueManager.getCompletedDownloadTasks();

  // 转换上传中任务
  this.uploadingList = uploadingTasks.map(task => 
    this.convertUploadTaskToTransferItem(task)
  );
  
  // 转换已完成上传任务
  this.uploadCompletedList = completedUploadTasks.map(task => 
    this.convertUploadTaskToTransferItem(task)
  );

  // 下载任务处理
  this.downloadingList = downloadingTasks.map(task => 
    this.convertDownloadTaskToTransferItem(task)
  );
  
  // 检查是否需要启动下载刷新定时器
  const currentDownloadingCount = uploadQueueManager.getDownloadingTasks().length;
  if (currentDownloadingCount > 0 && !this.downloadRefreshTimer) {
    this.startDownloadRefreshTimer();
  }
}

2. 平滑数据更新:避免UI闪烁

当从API获取数据时,组件采用平滑更新策略避免UI闪烁:

/** 
 * 平滑更新下载中列表,避免闪烁 
 * 优化:结合本地队列和API数据,严格去重确保计数一致
 */
updateDownloadingListSmooth(apiList: TransferItem[]) {
  const previousCount = this.downloadingList.length;
  
  // 第一步:对API数据进行去重
  const deduplicatedApiList = this.deduplicateApiList(apiList);
  
  // 第二步:创建映射表
  const apiMap = new Map<number, TransferItem>();
  const apiByFileIdMap = new Map<string, TransferItem>();
  deduplicatedApiList.forEach(apiItem => {
    if (apiItem.flId) apiMap.set(apiItem.flId, apiItem);
    if (apiItem.flFileId) apiByFileIdMap.set(apiItem.flFileId, apiItem);
  });

  // 第三步:合并更新,保持进度连续性
  const updatedList: TransferItem[] = [];
  const existingMap = new Map(this.downloadingList.map(item => {
    const key = item.flId ? `flId_${item.flId}` : item.id;
    return [key, item];
  }));

  // 合并API数据和现有数据
  deduplicatedApiList.forEach(apiItem => {
    const uniqueKey = apiItem.flId ? `flId_${apiItem.flId}` : apiItem.id;
    const existingItem = existingMap.get(uniqueKey);
    if (existingItem) {
      // 更新现有项,保持进度平滑
      updatedList.push({
        ...existingItem,
        ...apiItem,
        progress: Math.max(existingItem.progress || 0, apiItem.progress || 0)
      });
    } else {
      // 新增项
      updatedList.push(apiItem);
    }
  });

  this.downloadingList = updatedList;
}

3. 压缩包下载状态处理

压缩包下载是复杂场景,需要特殊处理:

<div v-if="downloadStatus === 'downloading'" class="status-content">
  <div v-if="item.status === 'failed'" class="failed-status">
    <a-icon type="exclamation-circle" class="error-icon" />
    <span class="error-text">下载失败,请重试</span>
  </div>
  <div v-else class="progress-status">
    <!-- 压缩包下载状态处理 -->
    <div v-if="isCompressedDownload(item)" class="compress-download-status">
      <div v-if="item.fctIsComplete === 1" class="compress-ready">
        <a-button type="primary" size="small" @click.stop="downloadCompressedFile(item)">
          <a-icon type="download" /> 下载
        </a-button>
        <span class="compress-ready-text">压缩包已准备完成</span>
      </div>
      <div v-else-if="item.fctIsComplete === -1" class="compress-failed">
        <a-icon type="exclamation-circle" class="error-icon" />
        <span class="error-text">压缩失败</span>
      </div>
      <div v-else class="compress-waiting">
        <a-spin size="small" class="loading-spin" />
        <span class="progress-text">正在压缩中,请稍候...</span>
      </div>
    </div>
    <!-- 普通文件下载状态 -->
    <div v-else class="normal-download-status">
      <div class="progress-info">
        <a-spin size="small" class="loading-spin" />
        <span class="progress-text">下载中 {{ item.progress }}%</span>
      </div>
      <a-progress :percent="item.progress" :showInfo="false" size="small" />
    </div>
  </div>
</div>

对应的处理逻辑:

/** 
 * 处理压缩包下载
 */
async downloadCompressedFile(item: TransferItem): Promise<void> {
  if (item.fctIsComplete !== 1) {
    this.$message.warning('压缩包还未准备完成,请稍候');
    return;
  }
  
  try {
    // 构造下载URL
    const downloadUrl = item.flDownloadUrl ? `files${item.flDownloadUrl}` : '';
    if (!downloadUrl) {
      this.$message.warning('下载地址不存在,无法下载');
      return;
    }

    // 使用a标签触发下载
    const link = document.createElement('a');
    link.href = downloadUrl;
    link.download = item.fileName || '';
    link.style.display = 'none';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    
    this.$message.success(`开始下载: ${item.fileName}`);
    
    // 调用下载任务完成记录API
    if (item.flId) {
      const formData = new FormData();
      formData.append('flId', item.flId.toString());
      await DownloadTaskRecordCompleted(formData);
    }
  } catch (error) {
    console.error(`下载压缩包失败: ${item.fileName}`, error);
    this.$message.error('下载失败,请稍后重试');
  }
}

4. 定时刷新机制

为了保持下载状态实时更新,组件实现了智能定时器:

/** 
 * 启动下载状态刷新定时器 
 */
startDownloadRefreshTimer(): void {
  // 先清除现有定时器
  this.stopDownloadRefreshTimer();
  
  console.log('[传输记录] 启动下载状态刷新定时器');
  
  this.downloadRefreshTimer = setInterval(async () => {
    try {
      await this.refreshDownloadingStatus();
    } catch (error) {
      console.error('[传输记录] 刷新下载状态失败:', error);
    }
  }, this.REFRESH_INTERVAL); // 3秒刷新一次
  
  // 立即执行一次
  this.refreshDownloadingStatus();
}

/** 
 * 停止下载状态刷新定时器 
 */
stopDownloadRefreshTimer(): void {
  if (this.downloadRefreshTimer) {
    clearInterval(this.downloadRefreshTimer);
    this.downloadRefreshTimer = null;
  }
}

/** 
 * 刷新下载中的任务状态 
 */
async refreshDownloadingStatus(): Promise<void> {
  try {
    // 重新加载下载中的数据
    await this.loadDownloadInProgressData(false);
    
    // 检查缓存中是否有下载中任务
    const cacheDownloadingTasks = uploadQueueManager.getDownloadingTasks();
    const hasApiTasks = this.downloadingList.length > 0;
    const hasCacheTasks = cacheDownloadingTasks.length > 0;
    
    // 只有当API和缓存都没有下载中的任务时,才停止定时器
    if (!hasApiTasks && !hasCacheTasks) {
      this.stopDownloadRefreshTimer();
      return;
    }
    
    // 检查是否有满足下载条件的任务(压缩完成的文件夹)
    const downloadingTasks = this.downloadingList.filter(item => 
      this.isCompressedDownload(item) && item.fctIsComplete === 1
    );
    
    // 处理满足条件的下载任务
    for (const task of downloadingTasks) {
      await this.processDownloadingTask(task as any);
    }
  } catch (error) {
    console.error('[传输记录] 刷新下载状态失败:', error);
  }
}

四、性能优化:大规模数据处理

1. 虚拟滚动与分页

组件实现了智能分页,只渲染可见数据:

// 显示的列表(用于加载更多功能)
get displayUploadList(): TransferItem[] {
  let filteredList = this.currentUploadList;
  // 如果是已完成状态且有搜索关键词,进行过滤
  if (this.uploadStatus === 'completed' && this.uploadSearchKeyword.trim()) {
    filteredList = this.currentUploadList.filter(item => 
      item.fileName.toLowerCase().includes(this.uploadSearchKeyword.toLowerCase().trim())
    );
  }
  // 只返回当前页需要显示的数据
  return filteredList?.slice(0, this.uploadCurrentPage * this.uploadPageSize);
}

2. 计算属性优化

使用计算属性替代方法调用,提高渲染性能:

// 当前已完成上传任务的实际显示数量
get currentUploadCompletedCount(): number {
  // 上传已完成状态的计数:
  // - 优先使用缓存数据(实时)
  // - 无缓存时使用API数据(稳定)
  const cacheCompletedCount = this.uploadCompletedList?.length || 0;
  const apiCompletedCount = this.apiUploadTotal || 0;
  // 缓存数据优先:优先使用缓存,无缓存时使用API数据
  return cacheCompletedCount > 0 ? cacheCompletedCount : apiCompletedCount;
}

3. 防抖与节流

对频繁触发的操作(如搜索)使用防抖:

/** 
 * 加载下载记录已完成数据(带防抖) 
 */
private loadDownloadCompletedDataTimer: any = null;
async loadDownloadCompletedData(immediate = false) {
  // 如果不是立即执行,使用防抖
  if (!immediate && this.loadDownloadCompletedDataTimer) {
    return;

更多推荐