index.vue 3.29 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>