Vue3 文件上传Hook深度解析:useUpload

578 阅读8分钟

一、功能概述

useUpload 是一个功能强大的Vue3 Composition API文件上传Hook,它提供了以下核心特性:

  1. 完整的上传生命周期管理

    • 支持上传前校验
    • 上传进度跟踪
    • 成功/失败状态处理
    • 错误重试机制
  2. 灵活的配置选项

    • 自定义上传API
    • 并发控制
    • 自动/手动上传模式
    • 自定义参数传递
  3. 强大的文件管理能力

    • 文件状态追踪
    • 重复文件检测
    • 批量操作支持
    • 文件元信息获取
  4. 与Ant Design Vue完美集成

    • 提供完整的Upload组件属性适配
    • 支持自定义预览
    • 支持文件列表展示

二、核心源码解析

1. 类型定义

// 文件对象接口
export interface UploadFile {
  uid: string;             // 客户端唯一标识
  file: File | null;       // 原始文件对象
  fileName: string;        // 本地文件名
  clientFileType: string;  // 客户端文件类型
  fileSize: number;        // 文件大小
  uploadStatus: 'waiting' | 'uploading' | 'error' | 'success';
  // ... 其他属性
}

// Hook配置选项
export interface UseUploadOptions {
  uploadApi?: (data: FormData) => Promise<any>;
  maxConcurrent?: number;
  autoUpload?: boolean;
  params?: Record<string, any>;
  // ... 其他配置项
}

2. 状态管理

// 核心状态
const fileList = reactive<UploadFile[]>([]);
const uploading = ref(false);
const currentUploading = ref(0);
const uploadQueue = reactive<string[]>([]);

3. 上传控制流程

const uploadSingleFile = async (uid: string): Promise<void> => {
  // 1. 获取并验证文件
  const file = findFile(uid);
  if (!file) return;

  // 2. 更新状态
  updateFileStatus(uid, 'uploading');
  currentUploading.value += 1;

  try {
    // 3. 构建表单数据
    const formData = new FormData();
    formData.append('file', file.file!);

    // 4. 调用上传API
    const response = await uploadApi(formData);

    // 5. 处理响应
    updateFileStatus(uid, 'success');
    updateFileServerData(uid, response);
  } catch (error) {
    // 6. 错误处理
    updateFileStatus(uid, 'error');
  }
};

三、基础用法

1. 最简单的使用方式

import { useUpload } from '@/hooks/useUpload';

export default defineComponent({
  setup() {
    const { 
      fileList, 
      uploading, 
      uploadProps 
    } = useUpload();

    return {
      uploadProps
    }
  }
});
<template>
  <a-upload v-bind="uploadProps">
    <a-button>
      <upload-outlined /> 点击上传
    </a-button>
  </a-upload>
</template>

2. 自定义配置示例

const { 
  fileList, 
  upload, 
  retry 
} = useUpload({
  // 最大并发数
  maxConcurrent: 2,
  // 自动上传
  autoUpload: true,
  // 上传前校验
  beforeUpload: (file) => {
    if (file.size > 5 * 1024 * 1024) {
      message.error('文件大小不能超过5MB');
      return false;
    }
    return true;
  },
  // 成功回调
  onSuccess: (file) => {
    message.success(`${file.fileName} 上传成功`);
  }
});

四、实际应用场景

1. 图片上传与预览场景

这是一个完整的图片上传组件示例,包含图片预览、进度显示、状态管理等功能。

<!-- ImageUploader.vue -->
<template>
  <div class="image-uploader">
    <!-- 上传区域 -->
    <a-upload
      v-bind="uploadProps"
      list-type="picture-card"
      :class="{ 'upload-list-inline': true }"
      @preview="handlePreview"
    >
      <div v-if="fileList.length < maxCount">
        <loading-outlined v-if="uploading" />
        <plus-outlined v-else />
        <div class="ant-upload-text">上传图片</div>
      </div>
    </a-upload>

    <!-- 图片预览模态框 -->
    <a-modal
      :visible="previewVisible"
      :title="previewTitle"
      :footer="null"
      @cancel="handlePreviewCancel"
    >
      <img :src="previewImage" :alt="previewTitle" style="width: 100%" />
    </a-modal>

    <!-- 上传统计信息 -->
    <div class="upload-stats" v-if="fileList.length > 0">
      <p>已上传: {{ successCount }} / {{ fileList.length }}</p>
      <a-progress 
        :percent="uploadProgress" 
        :status="uploadStatus"
        size="small"
      />
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { message } from 'ant-design-vue';
import { 
  PlusOutlined, 
  LoadingOutlined 
} from '@ant-design/icons-vue';
import { useUpload } from '@/hooks/useUpload';

export default defineComponent({
  name: 'ImageUploader',
  components: {
    PlusOutlined,
    LoadingOutlined
  },
  props: {
    maxCount: {
      type: Number,
      default: 5
    },
    maxSize: {
      type: Number,
      default: 5 * 1024 * 1024 // 5MB
    }
  },
  emits: ['upload-complete'],
  setup(props, { emit }) {
    // 预览相关的状态
    const previewVisible = ref(false);
    const previewImage = ref('');
    const previewTitle = ref('');

    // 使用upload hook
    const { 
      fileList, 
      uploading,
      uploadProps,
      getSuccessFiles,
      getErrorFiles 
    } = useUpload({
      maxConcurrent: 2,
      autoUpload: true,
      beforeUpload: async (file) => {
        // 验证文件类型
        if (!file.type.startsWith('image/')) {
          message.error('只能上传图片文件!');
          return false;
        }

        // 验证文件大小
        if (file.size > props.maxSize) {
          message.error(`图片大小不能超过${props.maxSize / 1024 / 1024}MB!`);
          return false;
        }

        // 验证图片尺寸
        try {
          const image = await createImageBitmap(file);
          if (image.width > 4096 || image.height > 4096) {
            message.error('图片尺寸不能超过4096x4096!');
            return false;
          }
          image.close();
        } catch (error) {
          message.error('图片格式不正确!');
          return false;
        }

        return true;
      },
      onSuccess: (file) => {
        message.success(`${file.fileName} 上传成功`);
        // 当所有文件都上传完成时,触发事件
        if (getSuccessFiles().length === fileList.length) {
          emit('upload-complete', getSuccessFiles());
        }
      },
      onError: (file, error) => {
        message.error(`${file.fileName} 上传失败: ${error.message}`);
      }
    });

    // 计算上传进度
    const uploadProgress = computed(() => {
      if (fileList.length === 0) return 0;
      return Math.round((getSuccessFiles().length / fileList.length) * 100);
    });

    // 计算上传状态
    const uploadStatus = computed(() => {
      if (getErrorFiles().length > 0) return 'exception';
      if (uploadProgress.value === 100) return 'success';
      return 'active';
    });

    // 成功上传的文件数量
    const successCount = computed(() => getSuccessFiles().length);

    // 处理图片预览
    const handlePreview = async (file: any) => {
      if (!file.url && !file.preview) {
        file.preview = await getBase64(file.originFileObj);
      }
      previewImage.value = file.url || file.preview;
      previewVisible.value = true;
      previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
    };

    // 关闭预览
    const handlePreviewCancel = () => {
      previewVisible.value = false;
    };

    // 工具函数:将文件转换为base64
    const getBase64 = (file: File): Promise<string> => {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result as string);
        reader.onerror = error => reject(error);
      });
    };

    return {
      fileList,
      uploading,
      uploadProps,
      previewVisible,
      previewImage,
      previewTitle,
      uploadProgress,
      uploadStatus,
      successCount,
      handlePreview,
      handlePreviewCancel
    };
  }
});
</script>

<style lang="less" scoped>
.image-uploader {
  .upload-list-inline {
    :deep(.ant-upload-list-item) {
      margin-right: 8px;
      margin-bottom: 8px;
    }
  }

  .ant-upload-text {
    margin-top: 8px;
    color: #666;
  }

  .upload-stats {
    margin-top: 16px;
    padding: 8px;
    background: #f5f5f5;
    border-radius: 4px;

    p {
      margin-bottom: 8px;
      color: #666;
    }
  }
}
</style>

使用示例:

<!-- 在父组件中使用 -->
<template>
  <div class="page-container">
    <h2>产品图片上传</h2>
    
    <image-uploader
      :max-count="5"
      :max-size="5 * 1024 * 1024"
      @upload-complete="handleUploadComplete"
    />
    
    <!-- 已上传图片展示 -->
    <div class="image-preview" v-if="uploadedImages.length > 0">
      <h3>已上传图片</h3>
      <div class="image-grid">
        <div 
          v-for="image in uploadedImages" 
          :key="image.serverId" 
          class="image-item"
        >
          <img :src="image.fileUrl" :alt="image.fileName" />
          <div class="image-info">
            <p>{{ image.fileName }}</p>
            <p>{{ formatFileSize(image.fileSize) }}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import ImageUploader from '@/components/ImageUploader.vue';
import type { UploadFile } from '@/hooks/useUpload';

export default defineComponent({
  name: 'ProductImageUpload',
  components: {
    ImageUploader
  },
  setup() {
    const uploadedImages = ref<UploadFile[]>([]);

    const handleUploadComplete = (files: UploadFile[]) => {
      uploadedImages.value = files;
      console.log('所有图片上传完成:', files);
    };

    const formatFileSize = (size: number): string => {
      if (size < 1024) return size + ' B';
      if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
      return (size / 1024 / 1024).toFixed(2) + ' MB';
    };

    return {
      uploadedImages,
      handleUploadComplete,
      formatFileSize
    };
  }
});
</script>

<style lang="less" scoped>
.page-container {
  padding: 24px;

  h2 {
    margin-bottom: 24px;
    color: #333;
  }

  .image-preview {
    margin-top: 32px;

    h3 {
      margin-bottom: 16px;
      color: #333;
    }

    .image-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
      gap: 16px;

      .image-item {
        border: 1px solid #eee;
        border-radius: 4px;
        overflow: hidden;
        transition: all 0.3s;

        &:hover {
          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
        }

        img {
          width: 100%;
          height: 200px;
          object-fit: cover;
        }

        .image-info {
          padding: 8px;
          background: #fafafa;

          p {
            margin: 0;
            font-size: 12px;
            color: #666;
            
            &:first-child {
              margin-bottom: 4px;
              color: #333;
              font-weight: 500;
              white-space: nowrap;
              overflow: hidden;
              text-overflow: ellipsis;
            }
          }
        }
      }
    }
  }
}
</style>

这个示例展示了如何将useUpload hook集成到实际的业务组件中,并提供了完整的用户界面和交互体验。组件可以直接使用,也可以根据具体需求进行定制化开发。

2、实际应用场景:九宫格图片上传组件

这是一个实际项目中的九宫格图片上传组件,展示了useUpload在真实业务场景中的应用。

1. 组件功能特点

  • 支持多图片上传
  • 显示上传进度和状态
  • 错误处理和重试机制
  • 图片预览功能
  • 文件大小和尺寸显示
  • 使用Tailwind CSS实现的响应式布局

2. 完整代码实现

<!-- CutNinePanel.vue -->
<template>
  <div class="material-section mb-16px">
    <!-- 头部操作区 -->
    <div class="flex items-center pb-3">
      <span class="min-w-[100px] required-field">原始图片</span>
      <div class="material-actions">
        <a-button class="mr-8px" @click="openMaterialManage">素材管理</a-button>
        <!-- 上传按钮 -->
        <a-upload
          v-bind="uploadProps"
          accept="image/*"
          :multiple="true"
          :show-upload-list="false"
        >
          <a-button :loading="uploading">本地素材</a-button>
        </a-upload>
        
        <!-- 上传状态显示 -->
        <span v-if="displayList.length > 0" class="ml-16px text-gray-500">
          已上传{{ successCount }}个素材
          <span v-if="errorCount > 0" class="text-red-500 ml-8px">
            ({{ errorCount }}个失败)
            <a-button
              v-if="errorCount > 0"
              type="link"
              class="text-primary p-0 ml-4px"
              @click="retryAllFailedItems"
            >
              重试全部
            </a-button>
          </span>
        </span>
      </div>
    </div>

    <!-- 图片预览网格 -->
    <div v-if="displayList.length" 
         class="material-preview-container relative pl-[92px] max-h-[280px] overflow-y-auto pt-[12px]">
      <div class="grid grid-cols-5 gap-4">
        <!-- 图片项循环 -->
        <div v-for="(item, index) in displayList" 
             :key="index"
             class="material-preview-box relative mr-8px inline-block border rounded p-8px"
             :class="{'bg-red-50': isErrorItem(item), 'border-gray-300': !isErrorItem(item)}">
          
          <!-- 错误状态图标 -->
          <div v-if="isErrorItem(item)" 
               class="absolute z-2 text-xl text-[#ff4d4f] top-1/3 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
            <exclamation-circle-filled class="text-red-500" />
          </div>

          <!-- 图片预览区域 -->
          <img v-if="!isErrorItem(item)" 
               :src="item.path" 
               class="h-full w-full object-scale-down" />
          <img v-else-if="(item as ErrorItem).fileUid && getLocalPreviewUrl((item as ErrorItem).fileUid)"
               :src="getLocalPreviewUrl((item as ErrorItem).fileUid)"
               class="h-full w-full object-scale-down opacity-50"
               @load="addPreviewUrlToCache(getLocalPreviewUrl((item as ErrorItem).fileUid))" />
          <div v-else class="h-[160px] w-full flex items-center justify-center bg-gray-100">
            <file-exclamation-outlined class="text-red-500 text-3xl" />
          </div>

          <!-- 文件信息显示 -->
          <div class="line-clamp-1 mt-4px text-xs break-all mb-2" :title="item.name">
            {{ item.name }}
          </div>
          <div class="text-xs text-gray-500 break-all">
            <span>{{ item.size }}</span>
            <span v-if="!isErrorItem(item)" class="ml-4px">
              ({{ item.width }}x{{ item.height }})
            </span>
          </div>

          <!-- 错误信息提示 -->
          <a-tooltip v-if="isErrorItem(item)" :title="(item as ErrorItem).errorMsg">
            <div class="text-xs text-red-500 truncate">
              {{ (item as ErrorItem).errorMsg }}
            </div>
          </a-tooltip>

          <!-- 删除按钮 -->
          <a-button
            type="link"
            class="absolute z-1 right-4px top-4px h-20px w-20px flex items-center justify-center rounded-full bg-gray-700 p-0 opacity-80"
            @click="removeMaterial(index)"
          >
            <close-outlined class="h-[14px] w-[16px] text-xs text-white" />
          </a-button>

          <!-- 重试按钮 -->
          <a-button
            v-if="isErrorItem(item)"
            type="primary"
            size="small"
            class="retry-btn absolute bottom-6px right-6px"
            @click="retryUpload(index)"
          >
            重试
          </a-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue';
import { useUpload } from '@/hooks/useUpload';
import { 
  ExclamationCircleFilled, 
  FileExclamationOutlined,
  CloseOutlined 
} from '@ant-design/icons-vue';

// 常量定义
const MAX_MATERIAL_COUNT = 9;

// 类型定义
interface MaterialItem {
  id: number;
  name: string;
  size: string;
  path: string;
  uuid: string;
  width: number;
  height: number;
  md5: string;
  isLocal: boolean;
}

interface ErrorItem {
  name: string;
  size: string;
  fileUid: string;
  errorMsg: string;
}

// 状态管理
const materialList = ref<MaterialItem[]>([]);
const errorList = ref<ErrorItem[]>([]);

// 使用useUpload钩子
const {
  fileList,
  uploading,
  uploadProps,
  retry,
  remove: removeFile
} = useUpload({
  params: {
    bizType: '1' // 业务类型参数
  },
  beforeUpload: () => {
    if (materialList.value.length + errorList.value.length >= MAX_MATERIAL_COUNT) {
      message.warning(`最多只能添加${MAX_MATERIAL_COUNT}个素材`);
      return false;
    }
    return true;
  },
  onSuccess: (file) => {
    if (materialList.value.length + errorList.value.length >= MAX_MATERIAL_COUNT) {
      message.warning(`最多只能添加${MAX_MATERIAL_COUNT}个素材`);
      return;
    }

    const fileData = file.response.data || {};
    materialList.value.push({
      id: fileData.id,
      name: file.fileName,
      size: formatFileSize(file.fileSize),
      path: fileData.fileUrl || fileData.url,
      uuid: fileData.uuid,
      width: file.width,
      height: file.height,
      md5: fileData.md5,
      isLocal: true,
    });
  },
  onError: (file) => {
    errorList.value.push({
      name: file.fileName,
      size: formatFileSize(file.fileSize),
      fileUid: file.uid,
      errorMsg: file.error?.message || '上传失败'
    });
  }
});

// 计算属性
const displayList = computed(() => [...materialList.value, ...errorList.value]);
const successCount = computed(() => materialList.value.length);
const errorCount = computed(() => errorList.value.length);

// 工具函数
const formatFileSize = (size: number): string => {
  if (size < 1024) return `${size}B`;
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`;
  return `${(size / 1024 / 1024).toFixed(1)}MB`;
};

const isErrorItem = (item: MaterialItem | ErrorItem): item is ErrorItem => {
  return 'errorMsg' in item;
};

// 事件处理函数
const retryUpload = (index: number) => {
  const item = displayList.value[index];
  if (isErrorItem(item)) {
    retry(item.fileUid);
  }
};

const removeMaterial = (index: number) => {
  const item = displayList.value[index];
  if (isErrorItem(item)) {
    const errorIndex = errorList.value.findIndex(e => e.fileUid === item.fileUid);
    if (errorIndex > -1) {
      errorList.value.splice(errorIndex, 1);
      removeFile(item.fileUid);
    }
  } else {
    materialList.value.splice(index, 1);
  }
};

const retryAllFailedItems = () => {
  errorList.value.forEach(item => {
    retry(item.fileUid);
  });
};
</script>

<style lang="less" scoped>
.material-section {
  .material-preview-box {
    height: 220px;
    transition: all 0.3s;

    &:hover {
      .retry-btn {
        opacity: 1;
      }
    }
  }

  .retry-btn {
    opacity: 0;
    transition: opacity 0.3s;
  }

  .required-field {
    &::before {
      content: '*';
      color: #ff4d4f;
      margin-right: 4px;
    }
  }
}
</style>

3. 组件特色功能

  1. 智能状态管理

    • 分别维护成功和失败的上传列表
    • 实时显示上传状态和进度
    • 支持失败重试和批量重试
  2. 优雅的错误处理

    • 显示错误状态和提示信息
    • 支持单个文件重试
    • 支持全部失败文件重试
  3. 完善的文件信息展示

    • 显示文件名称和大小
    • 显示图片尺寸信息
    • 支持图片预览
  4. 良好的用户体验

    • 使用Grid布局实现响应式
    • 优雅的hover效果
    • 清晰的状态反馈

4. 使用方式

<template>
  <cut-nine-panel @upload-complete="handleUploadComplete" />
</template>

<script lang="ts" setup>
import CutNinePanel from './components/CutNinePanel.vue';
import type { MaterialItem } from './types';

const handleUploadComplete = (materials: MaterialItem[]) => {
  console.log('上传完成的素材:', materials);
};
</script>

六、总结

useUpload Hook提供了一个完整的文件上传解决方案,它不仅封装了基础的上传功能,还提供了丰富的扩展性。通过合理的配置和扩展,可以满足各种复杂的业务场景需求。

建议在实际使用中根据具体需求选择合适的配置项,并结合业务场景进行必要的扩展。例如:

  • 图片上传时添加压缩、裁剪功能
  • 视频上传时自动生成封面图
  • 大文件上传时启用分片上传
  • 特殊文件类型的预览处理

通过这些扩展,可以打造出更加强大和易用的文件上传功能。

完整代码

import { ref, reactive, computed, watch, nextTick, Ref } from 'vue';
import { message } from 'ant-design-vue';
import { v4 as uuidv4 } from 'uuid';
import type { UploadProps } from 'ant-design-vue/es/upload';
import { uploadFiles } from '@/api/common';

// 文件对象接口定义
export interface UploadFile {
  // 客户端属性(上传前)
  uid: string;             // 客户端唯一标识
  file: File | null;       // 原始文件对象,上传成功后可释放
  fileName: string;        // 本地文件名
  clientFileType: string;  // 客户端文件类型
  fileSize: number;        // 文件大小(字节)
  width?: number;          // 宽度(可选)
  height?: number;         // 高度(可选)
  abortCheckpoint?: number; // 保留字段(可选)
  uploadStatus: 'waiting' | 'uploading' | 'error' | 'success'; // 上传状态

  // 服务端属性(上传后更新)
  serverId?: number;       // 服务端文件ID
  serverUuid?: string;     // 服务端UUID
  fileUrl?: string;        // 文件URL
  coverUrl?: string | null; // 封面URL
  serverFileName?: string; // 服务端文件名
  serverFileType?: string; // 服务端文件类型
  md5?: string;            // 文件MD5
  mediaEncrypted?: boolean; // 是否加密
  warnMsg?: string | null; // 警告信息

  // 控制属性
  response?: any;          // 原始上传响应
  error?: Error;           // 错误信息
  percent?: number;        // 上传进度百分比
}

// Hooks配置项接口
export interface UseUploadOptions {
  // 上传接口函数,默认使用 uploadNewFiles
  uploadApi?: (data: FormData) => Promise<any>;
  // 最大并发数
  maxConcurrent?: number;
  // 自动上传
  autoUpload?: boolean;
  // 自定义参数
  params?: Record<string, any>;
  // 上传前的处理函数
  beforeUpload?: (file: File) => boolean | Promise<boolean | File>;
  // 成功回调
  onSuccess?: (file: UploadFile) => void;
  // 失败回调
  onError?: (file: UploadFile, error: Error) => void;
  // 进度回调
  onProgress?: (file: UploadFile, percent: number) => void;
  // 其他 a-upload 组件支持的配置
  uploadProps?: Partial<UploadProps>;
}

// Hooks返回值接口
export interface UseUploadReturn {
  // 文件列表 - 包含完整文件信息
  fileList: UploadFile[];
  // 上传状态
  uploading: Ref<boolean>;
  // 上传文件方法
  upload: (files: File[]) => void;
  // 重试上传方法 - 接收文件对象或UID
  retry: (fileOrUid: UploadFile | string) => void;
  // 重试所有失败文件
  retryAll: () => number;
  // 删除文件
  remove: (uid: string) => void;
  // 清空文件列表
  clear: () => void;
  // 获取上传成功的文件
  getSuccessFiles: () => UploadFile[];
  // 获取上传失败的文件
  getErrorFiles: () => UploadFile[];
  // 获取当前正在上传的文件数量
  currentUploading: Ref<number>;
  // 获取等待上传的文件队列
  uploadQueue: string[];
  // a-upload 所需的属性和方法
  uploadProps: Partial<UploadProps>;
}

/**
 * 文件上传Hooks
 * @param options 上传配置选项
 * @returns UseUploadReturn
 */
export default function useUpload(options: UseUploadOptions = {}): UseUploadReturn {
  // 解构配置选项,设置默认值
  const {
    uploadApi = uploadNewFiles,
    maxConcurrent = 3,
    autoUpload = true,
    params = {},
    beforeUpload,
    onSuccess,
    onError,
    onProgress,
    uploadProps = {}
  } = options;

  // ============= 状态管理模块 =============
  // 文件列表状态
  const fileList = reactive<UploadFile[]>([]);
  // 上传中状态
  const uploading = ref(false);
  // 当前正在上传的文件数量
  const currentUploading = ref(0);
  // 等待上传的文件队列
  const uploadQueue = reactive<string[]>([]);

  /**
   * 创建文件对象
   * @param file 原始File对象
   * @returns UploadFile对象
   */
  const createFileObject = async (file: File): Promise<UploadFile> => {
    const fileObj: UploadFile = {
      uid: uuidv4(),
      file,
      fileName: file.name,
      clientFileType: file.type,
      fileSize: file.size,
      uploadStatus: 'waiting',
      percent: 0
    };

    // 如果是图片文件,使用createImageBitmap获取尺寸
    if (file.type.startsWith('image/')) {
      try {
        const imageBitmap = await createImageBitmap(file);
        fileObj.width = imageBitmap.width;
        fileObj.height = imageBitmap.height;
        imageBitmap.close(); // 释放资源
      } catch (error) {
        console.warn(`无法获取图片 ${file.name} 的尺寸信息:`, error);
      }
    }

    return fileObj;
  };

  /**
   * 获取成功上传的文件
   * @returns 成功上传的文件列表
   */
  const getSuccessFiles = (): UploadFile[] => {
    return fileList.filter(file => file.uploadStatus === 'success');
  };

  /**
   * 获取上传失败的文件
   * @returns 上传失败的文件列表
   */
  const getErrorFiles = (): UploadFile[] => {
    return fileList.filter(file => file.uploadStatus === 'error');
  };

  /**
   * 根据uid查找文件
   * @param uid 文件唯一标识
   * @returns 文件对象或undefined
   */
  const findFile = (uid: string): UploadFile | undefined => {
    return fileList.find(file => file.uid === uid);
  };

  /**
   * 更新文件状态
   * @param uid 文件唯一标识
   * @param status 新状态
   */
  const updateFileStatus = (uid: string, status: UploadFile['uploadStatus']): void => {
    const file = findFile(uid);
    if (file) {
      file.uploadStatus = status;
    }
  };

  /**
   * 更新文件上传进度
   * @param uid 文件唯一标识
   * @param percent 进度百分比
   */
  const updateFileProgress = (uid: string, percent: number): void => {
    const file = findFile(uid);
    if (file) {
      file.percent = percent;
      // 调用进度回调
      onProgress?.(file, percent);
    }
  };

  /**
   * 更新文件服务端信息
   * @param uid 文件唯一标识
   * @param serverData 服务端返回的数据
   */
  const updateFileServerData = (uid: string, serverData: any): void => {
    const file = findFile(uid);
    if (!file || !serverData) return;

    // 获取真正的数据对象,处理嵌套的响应结构
    const data = serverData.data || serverData;

    // 更新服务端属性
    file.serverId = data.id;
    file.serverUuid = data.uuid;
    file.fileUrl = data.url || data.fileUrl;
    file.coverUrl = data.coverUrl;
    file.serverFileName = data.fileName || data.name;
    file.serverFileType = data.fileType || data.type;
    file.md5 = data.md5;
    file.mediaEncrypted = data.mediaEncrypted;
    file.warnMsg = data.warnMsg;
    file.response = serverData; // 保留整个响应对象

    // 只有当上传成功时才清除原始文件引用,错误状态下保留原始文件以支持重试
    if (file.uploadStatus === 'success') {
      file.file = null as any;
    }
  };

  /**
   * 添加文件到列表
   * @param files 文件数组
   * @returns 添加的文件对象数组
   */
  const addFiles = async (files: File[]): Promise<UploadFile[]> => {
    const newFiles: UploadFile[] = [];

    for (const file of files) {
      // 检查是否已存在相同文件名的文件(可选的重复检查)
      const isDuplicate = fileList.some(existingFile =>
        existingFile.fileName === file.name &&
        existingFile.fileSize === file.size
      );

      if (isDuplicate) {
        message.warning(`文件 ${file.name} 已存在`);
        continue;
      }

      const fileObj = await createFileObject(file);
      fileList.push(fileObj);
      newFiles.push(fileObj);

      // 如果是自动上传,将文件加入上传队列
      if (autoUpload) {
        uploadQueue.push(fileObj.uid);
      }
    }

    return newFiles;
  };

  /**
   * 移除文件
   * @param uid 文件唯一标识
   */
  const remove = (uid: string): void => {
    const index = fileList.findIndex(file => file.uid === uid);
    if (index !== -1) {
      // 清除原始File对象引用
      fileList[index].file = null;
      fileList.splice(index, 1);
    }

    // 如果在上传队列中,也一并移除
    const queueIndex = uploadQueue.indexOf(uid);
    if (queueIndex !== -1) {
      uploadQueue.splice(queueIndex, 1);
    }
  };

  /**
   * 清空文件列表
   */
  const clear = (): void => {
    // 清除所有文件的原始File对象引用
    fileList.forEach(file => {
      file.file = null;
    });
    fileList.splice(0, fileList.length);
    uploadQueue.splice(0, uploadQueue.length);
    currentUploading.value = 0;
    uploading.value = false;
  };

  // ============= 上传控制模块 =============

  /**
   * 上传单个文件
   * @param uid 文件唯一标识
   */
  const uploadSingleFile = async (uid: string): Promise<void> => {
    const file = findFile(uid);
    if (!file) {
      console.warn(`找不到UID为${uid}的文件,可能已被删除`);
      checkQueue();
      return;
    }

    // 如果文件已上传成功或已被取消,跳过处理
    if (file.uploadStatus === 'success') {
      checkQueue();
      return;
    }

    // 保存原始文件引用,以备错误处理时使用
    const originalFile = file.file;

    // 更新文件状态为上传中
    updateFileStatus(uid, 'uploading');
    currentUploading.value += 1;
    uploading.value = true;

    try {
      // 创建FormData对象
      const formData = new FormData();
      // 确保file不为null
      if (!file.file) {
        throw new Error('文件对象丢失');
      }
      formData.append('file', file.file);

      // 添加自定义参数
      Object.keys(params).forEach(key => {
        formData.append(key, params[key]);
      });

      // 调用上传API
      const response = await uploadApi(formData);

      // 检查业务逻辑状态码,如果不是'0000'则表示业务逻辑错误
      if (response.code !== '0000') {
        throw new Error(response.message || response.msg || '业务处理失败');
      }

      // 上传完成后,再次检查文件是否存在(可能在上传过程中被删除)
      const updatedFile = findFile(uid);
      if (!updatedFile) {
        console.warn(`文件${uid}已在上传过程中被删除`);
        return;
      }

      // 更新文件状态和服务端数据
      updateFileStatus(uid, 'success');
      updateFileProgress(uid, 100);
      updateFileServerData(uid, response);

      // 调用成功回调
      if (onSuccess && updatedFile) {
        onSuccess(updatedFile);
      }

    } catch (error) {
      // 再次检查文件是否存在(上传过程中可能被删除)
      const errorFile = findFile(uid);
      if (!errorFile) {
        console.warn(`文件${uid}已在上传过程中被删除`);
        return;
      }

      // 确保错误项保留原始文件引用(用于重试和预览)
      if (!errorFile.file && originalFile) {
        errorFile.file = originalFile;
      }

      updateFileStatus(uid, 'error');
      errorFile.error = error as Error;
      const errorMsg = (error as Error).message || '未知错误';

      // 调用错误回调
      if (onError) {
        onError(errorFile, error as Error);
      }

      message.error(`${errorFile.fileName} 上传失败: ${errorMsg}`);
    } finally {
      currentUploading.value = Math.max(0, currentUploading.value - 1);

      // 如果没有正在上传的文件且队列为空,设置uploading为false
      if (currentUploading.value === 0 && uploadQueue.length === 0) {
        uploading.value = false;
      }

      // 检查队列,继续上传
      checkQueue();
    }
  };

  /**
   * 检查队列,开始上传下一个文件
   */
  const checkQueue = (): void => {
    // 如果没有等待上传的文件,或者当前上传数量已达到最大并发数,不处理
    if (uploadQueue.length === 0 || currentUploading.value >= maxConcurrent) {
      return;
    }

    // 计算可以同时开始的上传数量
    const availableSlots = Math.max(0, maxConcurrent - currentUploading.value);
    const filesToUpload = Math.min(availableSlots, uploadQueue.length);

    // 获取要上传的文件UID
    const uidsToUpload = uploadQueue.splice(0, filesToUpload);

    // 对每个文件启动上传
    uidsToUpload.forEach(uid => {
      // 使用nextTick避免可能的递归调用堆栈溢出
      nextTick(() => uploadSingleFile(uid));
    });
  };

  /**
   * 启动上传队列处理
   */
  const startUpload = (): void => {
    // 如果没有等待上传的文件,不处理
    if (uploadQueue.length === 0) {
      return;
    }

    // 计算可以同时上传的文件数量
    const availableSlots = Math.max(0, maxConcurrent - currentUploading.value);

    // 只启动可用的并发槽数量的上传
    for (let i = 0; i < availableSlots && uploadQueue.length > 0; i++) {
      const uid = uploadQueue[0];
      uploadQueue.shift(); // 从队列中移除
      uploadSingleFile(uid);
    }
  };

  // 监听上传队列变化,自动开始上传 - 优化监听逻辑避免递归触发
  watch(uploadQueue, (newQueue) => {
    // 只有当有文件在队列中且当前没有达到最大并发数时才启动上传
    if (newQueue.length > 0 && currentUploading.value < maxConcurrent) {
      // 使用 nextTick 延迟执行,避免递归触发
      nextTick(startUpload);
    }
  });

  // ============= 错误处理与重试模块 =============

  /**
   * 重试上传单个文件
   * @param fileOrUid 文件对象或UID
   */
  const retry = (fileOrUid: UploadFile | string): void => {
    const uid = typeof fileOrUid === 'string' ? fileOrUid : fileOrUid.uid;
    const file = findFile(uid);

    if (!file) {
      console.error(`找不到UID为${uid}的文件`);
      return;
    }

    // 只允许重试错误状态的文件
    if (file.uploadStatus !== 'error') {
      console.warn(`文件${file.fileName}当前不是错误状态,无法重试`);
      return;
    }

    // 重置文件状态
    file.error = undefined;
    file.percent = 0;
    file.uploadStatus = 'waiting';

    // 确保文件引用有效
    if (!file.file) {
      message.error(`文件${file.fileName}的原始数据已释放,无法重试上传`);
      file.uploadStatus = 'error';
      file.error = new Error('文件原始数据已释放');
      return;
    }

    // 加入上传队列
    if (!uploadQueue.includes(uid)) {
      uploadQueue.push(uid);
      // 如果当前没有正在上传的文件,启动上传
      if (currentUploading.value === 0) {
        nextTick(checkQueue);
      }
    }
  };

  /**
   * 重试所有失败的文件
   * @returns 重试的文件数量
   */
  const retryAll = (): number => {
    const errorFiles = getErrorFiles();

    // 过滤出可以重试的文件(文件引用未被释放)
    const retriableFiles = errorFiles.filter(file => !!file.file);

    if (retriableFiles.length === 0) {
      if (errorFiles.length > 0) {
        message.warning('所有失败文件的原始数据已释放,无法重试');
      }
      return 0;
    }

    // 重试每个文件
    retriableFiles.forEach(file => {
      retry(file);
    });

    return retriableFiles.length;
  };

  /**
   * 上传文件
   * @param files 文件数组
   */
  const upload = async (files: File[]): Promise<void> => {
    if (!files || files.length === 0) {
      return;
    }

    // 添加文件到列表
    const newFiles = await addFiles(files);

    // 如果不是自动上传,需要手动触发
    if (!autoUpload && newFiles.length > 0) {
      // 使用 nextTick 延迟处理,避免频繁触发监听器
      nextTick(() => {
        // 将新添加的文件加入上传队列
        newFiles.forEach(file => {
          if (!uploadQueue.includes(file.uid)) {
            uploadQueue.push(file.uid);
          }
        });
      });
    }
  };

  // ============= 与Ant Design Vue的a-upload组件集成 =============

  /**
   * 处理a-upload的beforeUpload事件
   */
  const handleBeforeUpload = async (file: File): Promise<boolean> => {
    // 如果设置了自定义的beforeUpload钩子,先执行验证
    if (beforeUpload) {
      try {
        const result = await beforeUpload(file);

        // 如果返回false,表示验证失败,不添加文件
        if (result === false) {
          return false;
        }

        // 如果返回新的File对象,使用新文件
        if (result instanceof File) {
          upload([result]);
          return false; // 阻止默认上传行为
        }
      } catch (error) {
        message.error(`文件校验失败: ${(error as Error).message}`);
        return false;
      }
    }

    // 验证通过,添加文件
    upload([file]);

    // 返回false,阻止组件默认上传行为,由我们接管上传过程
    return false;
  };

  /**
   * 处理a-upload的remove事件
   */
  const handleRemove = (file: any): void => {
    // ant-design-vue的Upload组件传入的file对象结构与我们的不同
    // 需要通过uid找到对应的文件
    const uid = file.uid;
    remove(uid);
  };

  /**
   * 为a-upload组件准备的配置项
   */
  const antUploadProps = computed<Partial<UploadProps>>(() => {
    return {
      ...uploadProps,
      fileList: fileList.map(file => {
        // 转换上传状态为ant design格式
        let status: 'error' | 'success' | 'uploading' | 'done' | undefined;

        switch (file.uploadStatus) {
          case 'error':
            status = 'error';
            break;
          case 'success':
            status = 'done';
            break;
          case 'uploading':
          case 'waiting':
            status = 'uploading';
            break;
        }

        return {
          uid: file.uid,
          name: file.fileName,
          status,
          percent: file.percent,
          url: file.fileUrl || undefined,
          thumbUrl: file.coverUrl || undefined,
          response: file.response,
          // 增加类型和大小信息,有助于预览
          type: file.clientFileType,
          size: file.fileSize,
        };
      }),
      beforeUpload: handleBeforeUpload,
      customRequest: () => {}, // 使用自定义上传,不执行默认的上传行为
      onRemove: handleRemove,
      // 处理预览事件
      onPreview: uploadProps.onPreview,
    };
  });

  return {
    fileList,
    uploading,
    upload,
    retry,
    retryAll,
    remove,
    clear,
    getSuccessFiles,
    getErrorFiles,
    currentUploading,
    uploadQueue,
    uploadProps: antUploadProps.value
  };
}