jq
2020-11-12 746d4a745d06ff1f0eb42a2c2d09c539202bc91e
wip: add upload component
13个文件已添加
6个文件已修改
864 ■■■■■ 已修改文件
.env.development 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/model/uploadModel.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/upload.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/src/types/tableAction.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/ThumnUrl.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/UploadContainer.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/UploadModal.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/UploadPreviewModal.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/data.tsx 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/props.ts 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/types.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/useUpload.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Upload/src/utils.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/menus/modules/demo/comp.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/demo/comp.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/http/axios/Axios.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/http/axios/types.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/comp/upload/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -5,7 +5,7 @@
VITE_PUBLIC_PATH = /
# Cross-domain proxy, you can configure multiple
VITE_PROXY=[["/api","http://localhost:3000"]]
VITE_PROXY=[["/api","http://localhost:3000"],["/upload","http://localhost:3001/upload"]]
# VITE_PROXY=[["/api","https://vvbin.cn/test"]]
# Delete console
src/api/demo/model/uploadModel.ts
New file
@@ -0,0 +1,5 @@
export interface UploadApiResult {
  message: string;
  code: number;
  url: string;
}
src/api/demo/upload.ts
New file
@@ -0,0 +1,23 @@
import { UploadApiResult } from './model/uploadModel';
import { defHttp } from '/@/utils/http/axios';
import { UploadFileParams } from '/@/utils/http/axios/types';
enum Api {
  UPLOAD_URL = '/upload',
}
/**
 * @description: 上传接口
 */
export function uploadApi(
  params: UploadFileParams,
  onUploadProgress: (progressEvent: ProgressEvent) => void
) {
  return defHttp.uploadFile<UploadApiResult>(
    {
      url: Api.UPLOAD_URL,
      onUploadProgress,
    },
    params
  );
}
src/components/Table/src/types/tableAction.ts
@@ -1,5 +1,6 @@
export interface ActionItem {
  on?: any;
  onClick?: any;
  label: string;
  disabled?: boolean;
  color?: 'success' | 'error' | 'warning';
src/components/Upload/index.ts
New file
@@ -0,0 +1,2 @@
export { default as UploadContainer } from './src/UploadContainer.vue';
// export * from './src/types';
src/components/Upload/src/ThumnUrl.vue
New file
@@ -0,0 +1,29 @@
<template>
  <span>
    <img v-if="fileUrl" :src="fileUrl" />
    <span v-else>{{ fileType }}</span>
  </span>
</template>
<script lang="ts">
  import { defineComponent } from 'vue';
  export default defineComponent({
    props: {
      fileUrl: {
        type: String,
        default: '',
      },
      fileType: {
        type: String,
        default: '',
      },
      fileName: {
        type: String,
        default: '',
      },
    },
    setup() {
      return {};
    },
  });
</script>
src/components/Upload/src/UploadContainer.vue
New file
@@ -0,0 +1,62 @@
<template>
  <div>
    <a-button-group>
      <a-button type="primary" @click="openUploadModal">上传</a-button>
      <a-button @click="openPreviewModal">
        <Icon icon="ant-design:eye-outlined" />
      </a-button>
    </a-button-group>
    <UploadModal v-bind="$props" @register="registerUploadModal" @change="handleChange" />
    <UploadPreviewModal
      :value="fileListRef"
      @register="registerPreviewModal"
      @change="handlePreviewChange"
    />
  </div>
</template>
<script lang="ts">
  import { defineComponent, ref, watch, unref } from 'vue';
  import { useModal } from '/@/components/Modal';
  import UploadModal from './UploadModal.vue';
  import { uploadContainerProps } from './props';
  import UploadPreviewModal from './UploadPreviewModal.vue';
  import Icon from '/@/components/Icon/index';
  export default defineComponent({
    components: { UploadModal, UploadPreviewModal, Icon },
    props: uploadContainerProps,
    setup(props, { emit }) {
      // 上传modal
      const [registerUploadModal, { openModal: openUploadModal }] = useModal();
      //   预览modal
      const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
      const fileListRef = ref<string[]>([]);
      watch(
        () => props.value,
        (value) => {
          fileListRef.value = [...(value || [])];
        },
        { immediate: true }
      );
      // 上传modal保存操作
      function handleChange(urls: string[]) {
        fileListRef.value = [...unref(fileListRef), ...(urls || [])];
        emit('change', fileListRef.value);
      }
      // 预览modal保存操作
      function handlePreviewChange(urls: string[]) {
        fileListRef.value = [...(urls || [])];
        emit('change', fileListRef.value);
      }
      return {
        registerUploadModal,
        openUploadModal,
        handleChange,
        handlePreviewChange,
        registerPreviewModal,
        openPreviewModal,
        fileListRef,
      };
    },
  });
</script>
src/components/Upload/src/UploadModal.vue
New file
@@ -0,0 +1,244 @@
<template>
  <BasicModal
    v-bind="$attrs"
    @register="register"
    @ok="handleOk"
    :closeFunc="handleCloseFunc"
    :maskClosable="false"
    width="800px"
    title="上传组件"
    wrapClassName="upload-modal"
    :okButtonProps="{ disabled: isUploadingRef }"
    :cancelButtonProps="{ disabled: isUploadingRef }"
  >
    <template #centerdFooter>
      <a-button @click="handleStartUpload" color="success" :loading="isUploadingRef">
        {{ isUploadingRef ? '上传中' : '开始上传' }}
      </a-button>
    </template>
    <Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">
      <a-button type="primary"> 选择文件 </a-button>
      <span class="px-2">{{ getHelpText }}</span>
    </Upload>
    <BasicTable @register="registerTable" :dataSource="fileListRef" />
  </BasicModal>
</template>
<script lang="ts">
  import { defineComponent, reactive, ref, toRef, unref } from 'vue';
  import { Upload } from 'ant-design-vue';
  import { BasicModal, useModalInner } from '/@/components/Modal';
  import { BasicTable, useTable } from '/@/components/Table';
  // hooks
  import { useUploadType } from './useUpload';
  import { useMessage } from '/@/hooks/web/useMessage';
  //   types
  import { FileItem, UploadResultStatus } from './types';
  import { basicProps } from './props';
  import { createTableColumns, createActionColumn } from './data';
  // utils
  import { checkFileType, checkImgType, getBase64WithFile } from './utils';
  import { buildUUID } from '/@/utils/uuid';
  import { createImgPreview } from '/@/components/Preview/index';
  import { uploadApi } from '/@/api/demo/upload';
  export default defineComponent({
    components: { BasicModal, Upload, BasicTable },
    props: basicProps,
    setup(props, { emit }) {
      const [register, { closeModal }] = useModalInner();
      const { getAccept, getStringAccept, getHelpText } = useUploadType({
        acceptRef: toRef(props, 'accept'),
        helpTextRef: toRef(props, 'helpText'),
        maxNumberRef: toRef(props, 'maxNumber'),
        maxSizeRef: toRef(props, 'maxSize'),
      });
      const fileListRef = ref<FileItem[]>([]);
      const state = reactive<{ fileList: FileItem[] }>({ fileList: [] });
      const { createMessage } = useMessage();
      // 上传前校验
      function beforeUpload(file: File) {
        const { size, name } = file;
        const { maxSize } = props;
        const accept = unref(getAccept);
        // 设置最大值,则判断
        if (maxSize && file.size / 1024 / 1024 >= maxSize) {
          createMessage.error(`只能上传不超过${maxSize}MB的文件!`);
          return false;
        }
        // 设置类型,则判断
        if (accept.length > 0 && !checkFileType(file, accept)) {
          createMessage.error!(`只能上传${accept.join(',')}格式文件`);
          return false;
        }
        // 生成图片缩略图
        if (checkImgType(file)) {
          // beforeUpload,如果异步会调用自带上传方法
          // file.thumbUrl = await getBase64(file);
          getBase64WithFile(file).then(({ result: thumbUrl }) => {
            fileListRef.value = [
              ...unref(fileListRef),
              {
                uuid: buildUUID(),
                file,
                thumbUrl,
                size,
                name,
                percent: 0,
                type: name.split('.').pop(),
              },
            ];
          });
        } else {
          fileListRef.value = [
            ...unref(fileListRef),
            {
              uuid: buildUUID(),
              file,
              size,
              name,
              percent: 0,
              type: name.split('.').pop(),
            },
          ];
        }
        return false;
      }
      // 删除
      function handleRemove(record: FileItem) {
        const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
        index !== -1 && fileListRef.value.splice(index, 1);
      }
      // 预览
      function handlePreview(record: FileItem) {
        const { thumbUrl = '' } = record;
        createImgPreview({
          imageList: [thumbUrl],
        });
      }
      const [registerTable] = useTable({
        columns: createTableColumns(),
        actionColumn: createActionColumn(handleRemove, handlePreview),
        pagination: false,
      });
      //   是否正在上传
      const isUploadingRef = ref(false);
      async function uploadApiByItem(item: FileItem) {
        try {
          item.status = UploadResultStatus.UPLOADING;
          const { data } = await uploadApi(
            {
              file: item.file,
            },
            function onUploadProgress(progressEvent: ProgressEvent) {
              const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
              item.percent = complete;
            }
          );
          item.status = UploadResultStatus.SUCCESS;
          item.responseData = data;
          return {
            success: true,
            error: null,
          };
        } catch (e) {
          console.log(e);
          item.status = UploadResultStatus.ERROR;
          return {
            success: false,
            error: e,
          };
        }
      }
      // 点击开始上传
      async function handleStartUpload() {
        try {
          isUploadingRef.value = true;
          const data = await Promise.all(
            unref(fileListRef).map((item) => {
              return uploadApiByItem(item);
            })
          );
          isUploadingRef.value = false;
          // 生产环境:抛出错误
          const errorList = data.filter((item) => !item.success);
          if (errorList.length > 0) {
            throw errorList;
          }
        } catch (e) {
          isUploadingRef.value = false;
          throw e;
        }
      }
      //   点击保存
      function handleOk() {
        // TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }}
        if (isUploadingRef.value) {
          createMessage.warning('请等待文件上传后,保存');
          return;
        }
        const fileList: string[] = [];
        for (const item of fileListRef.value) {
          const { status, responseData } = item;
          if (status === UploadResultStatus.SUCCESS && responseData) {
            fileList.push(responseData.url);
          }
        }
        // 存在一个上传成功的即可保存
        if (fileList.length <= 0) {
          createMessage.warning('没有上传成功的文件,无法保存');
          return;
        }
        console.log(fileList);
        emit('change', fileList);
        fileListRef.value = [];
        closeModal();
      }
      // 点击关闭:则所有操作不保存,包括上传的
      function handleCloseFunc() {
        if (!isUploadingRef.value) {
          fileListRef.value = [];
          return true;
        } else {
          createMessage.warning('请等待文件上传结束后操作');
          return false;
        }
      }
      return {
        register,
        closeModal,
        getHelpText,
        getStringAccept,
        beforeUpload,
        registerTable,
        fileListRef,
        state,
        isUploadingRef,
        handleStartUpload,
        handleOk,
        handleCloseFunc,
      };
    },
  });
</script>
<style lang="less">
  //   /deep/ .ant-upload-list {
  //     display: none;
  //   }
  .upload-modal {
    .ant-upload-list {
      display: none;
    }
    .ant-table-wrapper .ant-spin-nested-loading {
      padding: 0;
    }
  }
</style>
src/components/Upload/src/UploadPreviewModal.vue
New file
@@ -0,0 +1,93 @@
<template>
  <BasicModal
    wrapClassName="upload-preview-modal"
    v-bind="$attrs"
    width="800px"
    @register="register"
    title="预览"
    :showOkBtn="false"
  >
    <BasicTable @register="registerTable" :dataSource="fileListRef" />
  </BasicModal>
</template>
<script lang="ts">
  import { defineComponent, watch, ref, unref } from 'vue';
  import { BasicTable, useTable } from '/@/components/Table';
  import { createPreviewColumns, createPreviewActionColumn } from './data';
  import { BasicModal, useModalInner } from '/@/components/Modal';
  import { priviewProps } from './props';
  import { PreviewFileItem } from './types';
  import { createImgPreview } from '/@/components/Preview/index';
  import { downloadByUrl } from '/@/utils/file/FileDownload';
  export default defineComponent({
    components: { BasicModal, BasicTable },
    props: priviewProps,
    setup(props, { emit }) {
      const [register, { closeModal }] = useModalInner();
      const fileListRef = ref<PreviewFileItem[]>([]);
      watch(
        () => props.value,
        (value) => {
          fileListRef.value = [];
          value.forEach((item) => {
            fileListRef.value = [
              ...unref(fileListRef),
              {
                url: item,
                type: item.split('.').pop() || '',
                name: item.split('/').pop() || '',
              },
            ];
          });
        },
        { immediate: true }
      );
      // 删除
      function handleRemove(record: PreviewFileItem) {
        const index = fileListRef.value.findIndex((item) => item.url === record.url);
        if (index !== -1) {
          fileListRef.value.splice(index, 1);
          emit(
            'change',
            fileListRef.value.map((item) => item.url)
          );
        }
      }
      // 预览
      function handlePreview(record: PreviewFileItem) {
        const { url = '' } = record;
        createImgPreview({
          imageList: [url],
        });
      }
      // 下载
      function handleDownload(record: PreviewFileItem) {
        const { url = '' } = record;
        downloadByUrl({ url });
      }
      const [registerTable] = useTable({
        columns: createPreviewColumns(),
        pagination: false,
        actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }),
      });
      return {
        register,
        closeModal,
        fileListRef,
        registerTable,
      };
    },
  });
</script>
<style lang="less">
  .upload-preview-modal {
    .ant-upload-list {
      display: none;
    }
    .ant-table-wrapper .ant-spin-nested-loading {
      padding: 0;
    }
  }
</style>
src/components/Upload/src/data.tsx
New file
@@ -0,0 +1,159 @@
// import { BasicColumn, TableAction, ActionItem } from '@/components/table';
import { checkImgType, isImgTypeByName } from './utils';
// import ThumnUrl from './ThumbUrl.vue';
import { Progress } from 'ant-design-vue';
import { FileItem, PreviewFileItem, UploadResultStatus } from './types';
// import { ElecArchivesSaveResult } from '@/api/biz/file/model/fileModel';
// import { quryFile } from '@/api/biz/file/file';
import { BasicColumn, ActionItem, TableAction } from '/@/components/Table/index';
// 文件上传列表
export function createTableColumns(): BasicColumn[] {
  return [
    {
      dataIndex: 'thumbUrl',
      title: '图例',
      width: 100,
      customRender: ({ record }) => {
        const { thumbUrl, type } = (record as FileItem) || {};
        return <span>{thumbUrl ? <img src={thumbUrl} style={{ width: '50px' }} /> : type}</span>;
        // return <ThumnUrl fileUrl={thumbUrl} fileType={type} fileName={type} />;
      },
    },
    {
      dataIndex: 'name',
      title: '文件名',
      align: 'left',
      customRender: ({ text, record }) => {
        const { percent, status: uploadStatus } = (record as FileItem) || {};
        let status = 'normal';
        if (uploadStatus === UploadResultStatus.ERROR) {
          status = 'exception';
        } else if (uploadStatus === UploadResultStatus.UPLOADING) {
          status = 'active';
        } else if (uploadStatus === UploadResultStatus.SUCCESS) {
          status = 'success';
        }
        return (
          <span>
            <p class="ellipsis mb-1" title={text}>
              {text}
            </p>
            <Progress percent={percent} size="small" status={status} />
          </span>
        );
      },
    },
    {
      dataIndex: 'size',
      title: '文件大小',
      width: 100,
      customRender: ({ text = 0 }) => {
        return text && (text / 1024).toFixed(2) + 'KB';
      },
    },
    // {
    //   dataIndex: 'type',
    //   title: '文件类型',
    //   width: 100,
    // },
    {
      dataIndex: 'status',
      title: '状态',
      width: 100,
      customRender: ({ text }) => {
        if (text === UploadResultStatus.SUCCESS) {
          return '上传成功';
        } else if (text === UploadResultStatus.ERROR) {
          return '上传失败';
        } else if (text === UploadResultStatus.UPLOADING) {
          return '上传中';
        }
        return text;
      },
    },
  ];
}
export function createActionColumn(handleRemove: Function, handlePreview: Function): BasicColumn {
  return {
    width: 120,
    title: '操作',
    dataIndex: 'action',
    fixed: false,
    customRender: ({ record }) => {
      const actions: ActionItem[] = [
        {
          label: '删除',
          onClick: handleRemove.bind(null, record),
        },
      ];
      if (checkImgType(record)) {
        actions.unshift({
          label: '预览',
          onClick: handlePreview.bind(null, record),
        });
      }
      return <TableAction actions={actions} />;
    },
  };
}
// 文件预览列表
export function createPreviewColumns(): BasicColumn[] {
  return [
    {
      dataIndex: 'url',
      title: '图例',
      width: 100,
      customRender: ({ record }) => {
        const { url, type } = (record as PreviewFileItem) || {};
        return (
          <span>{isImgTypeByName(url) ? <img src={url} style={{ width: '50px' }} /> : type}</span>
        );
      },
    },
    {
      dataIndex: 'name',
      title: '文件名',
      align: 'left',
    },
  ];
}
export function createPreviewActionColumn({
  handleRemove,
  handlePreview,
  handleDownload,
}: {
  handleRemove: Function;
  handlePreview: Function;
  handleDownload: Function;
}): BasicColumn {
  return {
    width: 160,
    title: '操作',
    dataIndex: 'action',
    fixed: false,
    customRender: ({ record }) => {
      const { url } = (record as PreviewFileItem) || {};
      const actions: ActionItem[] = [
        {
          label: '删除',
          onClick: handleRemove.bind(null, record),
        },
        {
          label: '下载',
          onClick: handleDownload.bind(null, record),
        },
      ];
      if (isImgTypeByName(url)) {
        actions.unshift({
          label: '预览',
          onClick: handlePreview.bind(null, record),
        });
      }
      return <TableAction actions={actions} />;
    },
  };
}
src/components/Upload/src/props.ts
New file
@@ -0,0 +1,42 @@
import type { PropType } from 'vue';
export const basicProps = {
  helpText: {
    type: String as PropType<string>,
    default: '',
  },
  // 文件最大多少MB
  maxSize: {
    type: Number as PropType<number>,
    default: 2,
  },
  // 最大数量的文件,0不限制
  maxNumber: {
    type: Number as PropType<number>,
    default: 0,
  },
  // 根据后缀,或者其他
  accept: {
    type: Array as PropType<string[]>,
    default: () => [],
  },
  multiple: {
    type: Boolean,
    default: true,
  },
};
export const uploadContainerProps = {
  value: {
    type: Array as PropType<string[]>,
    default: () => [],
  },
  ...basicProps,
};
export const priviewProps = {
  value: {
    type: Array as PropType<string[]>,
    default: () => [],
  },
};
src/components/Upload/src/types.ts
New file
@@ -0,0 +1,25 @@
import { UploadApiResult } from '/@/api/demo/model/uploadModel';
export enum UploadResultStatus {
  SUCCESS = 'success',
  ERROR = 'error',
  UPLOADING = 'uploading',
}
export interface FileItem {
  thumbUrl?: string;
  name: string;
  size: string | number;
  type?: string;
  percent: number;
  file: File;
  status?: UploadResultStatus;
  responseData?: UploadApiResult;
  uuid: string;
}
export interface PreviewFileItem {
  url: string;
  name: string;
  type: string;
}
src/components/Upload/src/useUpload.ts
New file
@@ -0,0 +1,55 @@
import { Ref, unref, computed } from 'vue';
export function useUploadType({
  acceptRef,
  //   uploadTypeRef,
  helpTextRef,
  maxNumberRef,
  maxSizeRef,
}: {
  acceptRef: Ref<string[]>;
  //   uploadTypeRef: Ref<UploadTypeEnum>;
  helpTextRef: Ref<string>;
  maxNumberRef: Ref<number>;
  maxSizeRef: Ref<number>;
}) {
  // 文件类型限制
  const getAccept = computed(() => {
    // const uploadType = unref(uploadTypeRef);
    const accept = unref(acceptRef);
    if (accept && accept.length > 0) {
      return accept;
    }
    return [];
  });
  const getStringAccept = computed(() => {
    return unref(getAccept)
      .map((item) => `.${item}`)
      .join(',');
  });
  // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。
  const getHelpText = computed(() => {
    const helpText = unref(helpTextRef);
    if (helpText) {
      return helpText;
    }
    const helpTexts: string[] = [];
    const accept = unref(acceptRef);
    if (accept.length > 0) {
      helpTexts.push(`支持${accept.join(',')}格式`);
    }
    const maxSize = unref(maxSizeRef);
    if (maxSize) {
      helpTexts.push(`不超过${maxSize}MB`);
    }
    const maxNumber = unref(maxNumberRef);
    if (maxNumber) {
      helpTexts.push(`最多可选择${maxNumber}个文件`);
    }
    return helpTexts.join(',');
  });
  return { getAccept, getStringAccept, getHelpText };
}
src/components/Upload/src/utils.ts
New file
@@ -0,0 +1,28 @@
export function checkFileType(file: File, accepts: string[]) {
  const newTypes = accepts.join('|');
  // const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
  const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
  if (!reg.test(file.name)) {
    return false;
  } else {
    return true;
  }
}
export function checkImgType(file: File) {
  return /\.(jpg|jpeg|png|gif)$/i.test(file.name);
}
export function isImgTypeByName(name: string) {
  return /\.(jpg|jpeg|png|gif)$/i.test(name);
}
export function getBase64WithFile(file: File) {
  return new Promise<{
    result: string;
    file: File;
  }>((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve({ result: reader.result as string, file });
    reader.onerror = (error) => reject(error);
  });
}
src/router/menus/modules/demo/comp.ts
@@ -39,6 +39,10 @@
        name: '密码强度组件',
      },
      {
        path: 'upload',
        name: '上传组件',
      },
      {
        path: 'scroll',
        name: '滚动组件',
        children: [
src/router/routes/modules/demo/comp.ts
@@ -170,5 +170,13 @@
        title: '密码强度组件',
      },
    },
    {
      path: '/upload',
      name: 'UploadDemo',
      component: () => import('/@/views/demo/comp/upload/index.vue'),
      meta: {
        title: '上传组件',
      },
    },
  ],
} as AppRouteModule;
src/utils/http/axios/Axios.ts
@@ -5,9 +5,10 @@
import { isFunction } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import type { RequestOptions, CreateAxiosOptions, Result } from './types';
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
// import { ContentTypeEnum } from '/@/enums/httpEnum';
import { errorResult } from './const';
import { ContentTypeEnum } from '/@/enums/httpEnum';
export * from './axiosTransform';
@@ -107,25 +108,42 @@
      this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
  }
  // /**
  //  * @description:  文件上传
  //  */
  // uploadFiles(config: AxiosRequestConfig, params: File[]) {
  //   const formData = new FormData();
  /**
   * @description:  文件上传
   */
  uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
    const formData = new window.FormData();
  //   Object.keys(params).forEach((key) => {
  //     formData.append(key, params[key as any]);
  //   });
    if (params.data) {
      Object.keys(params.data).forEach((key) => {
        if (!params.data) return;
        const value = params.data[key];
        // support key-value array data
        if (Array.isArray(value)) {
          value.forEach((item) => {
            // { list: [ 11, 22 ] }
            // formData.append('list[]', 11);
            formData.append(`${key}[]`, item);
          });
          return;
        }
  //   return this.request({
  //     ...config,
  //     method: 'POST',
  //     data: formData,
  //     headers: {
  //       'Content-type': ContentTypeEnum.FORM_DATA,
  //     },
  //   });
  // }
        formData.append(key, params.data[key]);
      });
    }
    formData.append(params.name || 'file', params.file, params.filename);
    return this.axiosInstance.request<T>({
      ...config,
      method: 'POST',
      data: formData,
      headers: {
        'Content-type': ContentTypeEnum.FORM_DATA,
        ignoreCancelToken: true,
      },
    });
  }
  /**
   * @description:   请求方法
src/utils/http/axios/types.ts
@@ -28,3 +28,14 @@
  message: string;
  result: T;
}
// multipart/form-data:上传文件
export interface UploadFileParams {
  // 其他参数
  data?: { [key: string]: any };
  // 文件参数的接口字段名
  name?: string;
  // 文件
  file: File | Blob;
  // 文件名
  filename?: string;
}
src/views/demo/comp/upload/index.vue
New file
@@ -0,0 +1,17 @@
<template>
  <div class="p-4">
    <UploadContainer :maxSize="5" />
  </div>
</template>
<script lang="ts">
  import { defineComponent } from 'vue';
  import { UploadContainer } from '/@/components/Upload/index';
  //   import { Alert } from 'ant-design-vue';
  export default defineComponent({
    components: { UploadContainer },
    setup() {
      return {};
    },
  });
</script>