index.vue 3.45 KB
<template>
  <Upload
    v-bind="$attrs"
    :on-before-upload="onBeforeUpload"
    :custom-request="onUpload"
    :file-list="[]"
    :show-file-list="false"
    style="width: 100%"
  >
    <template #upload-button>
      <Input :model-value="modelValue" :readonly="true" :placeholder="placeholder">
        <template #suffix>
          <Progress v-if="loading" :percent="percent" size="mini" />
          <IconUpload v-else />
        </template>
      </Input>
    </template>
  </Upload>
</template>

<script lang="ts" setup>
  import { ref } from 'vue';
  import { IconUpload } from '@arco-design/web-vue/es/icon';
  import useLoading from '@/hooks/loading';
  import useOss from '@/hooks/oss';
  import { Input, Message, Progress, Upload, UploadRequest, useFormItem } from '@arco-design/web-vue';
  import { startsWith } from 'lodash';

  type FileType = { name: string; url: string; size: number; type: string; width?: number; height?: number; duration?: number };

  const props = defineProps({
    modelValue: { type: String, default: '' },
    prefix: { type: String, default: 'file' },
    limit: { type: Number, default: 0 },
    placeholder: { type: String, default: '请选择' },
  });

  const emits = defineEmits<{
    (e: 'update:modelValue', value: string): void;
    (e: 'success', value: FileType): void;
    (e: 'choose-file', value: File): void;
  }>();
  const { eventHandlers } = useFormItem();

  const { loading, setLoading } = useLoading(false);
  const { upload } = useOss();
  const percent = ref<number>(0);

  const onBeforeUpload = (file: File & { width?: number; height?: number; duration?: number }) => {
    if (props.limit !== 0 && file.size > props.limit * 1024 * 1024) {
      Message.warning(`${file.name} 文件超过${props.limit}MB,无法上传`);
      return Promise.resolve(false);
    }

    if (startsWith(file.type, 'image/')) {
      const imgObj = new Image();
      imgObj.src = URL.createObjectURL(file);
      imgObj.onload = () => {
        file.width = imgObj.width;
        file.height = imgObj.height;
      };
    }

    if (startsWith(file.type, 'audio/') || startsWith(file.type, 'video/')) {
      const audioElement = new Audio(URL.createObjectURL(file));
      audioElement.addEventListener('loadedmetadata', () => {
        file.duration = audioElement.duration * 1000;
      });
    }

    return Promise.resolve(file);
  };

  const onProgress = (p: number) => {
    percent.value = p;
  };

  const onUpload = (option: any): UploadRequest => {
    const { fileItem } = option;

    if (fileItem.file) {
      setLoading(true);
      // eslint-disable-next-line vue/custom-event-name-casing
      emits('choose-file', fileItem.file as File);
      upload(fileItem.file, props.prefix, onProgress)
        .then((res) => {
          emits('update:modelValue', res?.url || '');
          eventHandlers.value?.onChange?.();
          fileItem.percent = 100;
          fileItem.url = res?.url || '';
          fileItem.status = 'done';
          emits('success', {
            name: fileItem.name,
            url: fileItem.url,
            size: fileItem.file.size,
            type: fileItem.file.type,
            width: fileItem.file.width || 0,
            height: fileItem.file.height || 0,
            duration: fileItem.file.duration || 0,
          });
        })
        .finally(() => {
          setLoading(false);
        });
    }

    return {};
  };
</script>

<style lang="less" scoped>
  ::v-deep(.arco-input-append) {
    padding: 0;
  }
</style>