liu
6 天以前 5c124cdf26e4d749eae1e7fc9df366e9a5f4d259
src/components/upload/file-upload.vue
@@ -8,9 +8,9 @@
import { computed, ref, toRefs, watch } from 'vue';
import { IconifyIcon } from '..\..\packages\icons\src';
import { $t } from '..\..\packages\locales\src';
import { checkFileType, isFunction, isObject, isString } from '..\..\packages\utils\src';
import { IconifyIcon } from '@vben/icons';
import { $t } from '@vben/locales';
import { checkFileType, isFunction, isObject, isString } from '@vben/utils';
import { Button, message, Upload } from 'ant-design-vue';
@@ -35,6 +35,8 @@
  returnText: false,
  showDescription: false,
  showDownloadIcon: true,
  valueKey: 'url',
  valuePrefix: '',
});
const emit = defineEmits([
  'change',
@@ -53,22 +55,20 @@
  maxSizeRef: maxSize,
});
/** 计算当前绑定的值,优先使用 modelValue */
const currentValue = computed(() => {
  return props.modelValue === undefined ? props.value : props.modelValue;
});
/** 判断是否使用 modelValue */
const isUsingModelValue = computed(() => {
  return props.modelValue !== undefined;
});
const fileList = ref<UploadProps['fileList']>([]);
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
const isActMsg = ref<boolean>(true); // 文件类型错误提示
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
const uploadNumber = ref<number>(0); // 上传文件计数器
const uploadList = ref<any[]>([]); // 临时上传列表
const isLtMsg = ref<boolean>(true);
const isActMsg = ref<boolean>(true);
const isFirstRender = ref<boolean>(true);
const uploadNumber = ref<number>(0);
const uploadList = ref<any[]>([]);
watch(
  currentValue,
@@ -84,17 +84,31 @@
      } else {
        value.push(v);
      }
      const existingMap = new Map<string | number, UploadFile>();
      for (const f of fileList.value ?? []) {
        const id = (f as any)?.response?.[props.valueKey] ?? (f as any)?.[props.valueKey] ?? f?.uid;
        if (id != null) existingMap.set(id, f);
      }
      fileList.value = value
        .map((item, i) => {
          if (item && isString(item)) {
            return {
            return existingMap.get(item) ?? {
              uid: `${-i}`,
              name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
              status: UploadResultStatus.DONE,
              url: item,
            };
          } else if (item && isObject(item)) {
            return item;
            const key = item[props.valueKey] ?? item.uid;
            return (key != null && existingMap.get(key)) || { ...item, url: String((item as any).url ?? '') };
          } else if (item && (typeof item === 'number' || typeof item === 'string')) {
            return existingMap.get(item) ?? {
              uid: `${-i}`,
              name: `文件 ${String(item)}`,
              status: UploadResultStatus.DONE,
              url: String(item),
              response: { [props.valueKey]: item },
            } as UploadFile;
          }
          return null;
        })
@@ -127,6 +141,12 @@
/** 处理文件预览 */
function handlePreview(file: UploadFile) {
  const url = (file as any)?.url || (file as any)?.response?.url || (file as any)?.response;
  if (!url) {
    message.warning('文件地址不存在,无法预览');
    return;
  }
  window.open(url, '_blank');
  emit('preview', file);
}
@@ -139,17 +159,10 @@
function handleUploadError(error: any) {
  console.error('上传错误:', error);
  message.error($t('ui.upload.uploadError'));
  // 上传失败时减少计数器
  uploadNumber.value = Math.max(0, uploadNumber.value - 1);
}
/**
 * 上传前校验
 * @param file 待上传的文件
 * @returns 是否允许上传
 */
async function beforeUpload(file: File) {
  // 检查文件数量限制
  if (fileList.value!.length >= props.maxNumber) {
    message.error($t('ui.upload.maxNumber', [props.maxNumber]));
    return Upload.LIST_IGNORE;
@@ -160,7 +173,6 @@
  if (!isAct) {
    message.error($t('ui.upload.acceptUpload', [accept]));
    isActMsg.value = false;
    // 防止弹出多个错误提示
    setTimeout(() => (isActMsg.value = true), 1000);
    return Upload.LIST_IGNORE;
  }
@@ -168,12 +180,10 @@
  if (isLt) {
    message.error($t('ui.upload.maxSizeMultiple', [maxSize]));
    isLtMsg.value = false;
    // 防止弹出多个错误提示
    setTimeout(() => (isLtMsg.value = true), 1000);
    return Upload.LIST_IGNORE;
  }
  // 只有在验证通过后才增加计数器
  uploadNumber.value++;
  if (props.returnText) {
    const fileContent = await file.text();
@@ -182,23 +192,18 @@
  return true;
}
/** 自定义上传请求 */
async function customRequest(info: UploadRequestOption) {
  let { api } = props;
  if (!api || !isFunction(api)) {
    api = useUpload(props.directory).httpRequest;
  }
  try {
    // 上传文件
    const progressEvent: AxiosProgressEvent = (e) => {
      const percent = Math.trunc((e.loaded / e.total!) * 100);
      info.onProgress!({ percent });
    };
    const res = await api?.(info.file as File, progressEvent);
    // 处理上传成功后的逻辑
    handleUploadSuccess(res, info.file as File);
    info.onSuccess!(res);
    message.success($t('ui.upload.uploadSuccess'));
  } catch (error: any) {
@@ -208,34 +213,27 @@
  }
}
/**
 * 处理上传成功
 * @param res 上传响应结果
 * @param file 上传的文件
 */
function handleUploadSuccess(res: any, file: File) {
  // 删除临时文件
  const index = fileList.value?.findIndex((item) => item.name === file.name);
  if (index !== -1) {
    fileList.value?.splice(index!, 1);
  }
  // 添加到临时上传列表
  const fileUrl = res?.url || res?.data || res;
  const fileUrl = res?.url || res?.data || (typeof res === 'string' ? res : '');
  uploadList.value.push({
    name: file.name,
    name: res?.name || file.name,
    url: fileUrl,
    id: res?.id,
    response: res,
    status: UploadResultStatus.DONE,
    uid: file.name + Date.now(),
  });
  // 检查是否所有文件都上传完成
  if (uploadList.value.length >= uploadNumber.value) {
    fileList.value?.push(...uploadList.value);
    uploadList.value = [];
    uploadNumber.value = 0;
    // 更新值
    const value = getValue();
    isInnerOperate.value = true;
    emit('update:value', value);
@@ -244,10 +242,6 @@
  }
}
/**
 * 获取当前文件列表的值
 * @returns 文件 URL 列表或字符串
 */
function getValue() {
  const list = (fileList.value || [])
    .filter((item) => item?.status === UploadResultStatus.DONE)
@@ -255,13 +249,17 @@
      if (item?.response && props?.resultField) {
        return item?.response;
      }
      return item?.url || item?.response?.url || item?.response;
      const val =
        item?.response?.[props.valueKey] ??
        item?.[props.valueKey] ??
        item?.url ??
        item?.response?.url ??
        item?.response;
      return props.valuePrefix ? `${props.valuePrefix}${val}` : val;
    });
  // 单个文件的情况,根据输入参数类型决定返回格式
  if (props.maxNumber === 1) {
    const singleValue = list.length > 0 ? list[0] : '';
    // 如果原始值是字符串或 modelValue 是字符串,返回字符串
    if (
      isString(props.value) ||
      (isUsingModelValue.value && isString(props.modelValue))
@@ -271,7 +269,6 @@
    return singleValue;
  }
  // 多文件情况,根据输入参数类型决定返回格式
  if (isUsingModelValue.value) {
    return Array.isArray(props.modelValue) ? list : list.join(',');
  }
@@ -295,14 +292,14 @@
      :progress="{ showInfo: true }"
      :show-upload-list="{
        showPreviewIcon: true,
        showRemoveIcon: true,
        showRemoveIcon: !disabled,
        showDownloadIcon,
      }"
      @remove="handleRemove"
      @preview="handlePreview"
      @reject="handleExceed"
    >
      <div v-if="drag" class="upload-drag-area">
      <div v-if="drag && !disabled" class="upload-drag-area">
        <p class="ant-upload-drag-icon">
          <IconifyIcon icon="lucide:cloud-upload" />
        </p>
@@ -311,7 +308,9 @@
          支持{{ accept.join('/') }}格式文件,不超过{{ maxSize }}MB
        </p>
      </div>
      <div v-else-if="fileList && fileList.length < maxNumber">
      <div
        v-else-if="!disabled && fileList && fileList.length < maxNumber"
      >
        <Button>
          <IconifyIcon icon="lucide:cloud-upload" />
          {{ $t('ui.upload.upload') }}
@@ -364,7 +363,6 @@
</style>
<style>
/* 文件上传列表显示手型光标样式失效。不知道为啥. 先这里加上 */
.ant-upload-list-text .ant-upload-list-item {
  cursor: pointer;
}