Ver Fonte

wip: add upload component

jq há 4 anos atrás
pai
commit
746d4a745d

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ VITE_USE_MOCK = true
 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

+ 5 - 0
src/api/demo/model/uploadModel.ts

@@ -0,0 +1,5 @@
+export interface UploadApiResult {
+  message: string;
+  code: number;
+  url: string;
+}

+ 23 - 0
src/api/demo/upload.ts

@@ -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
+  );
+}

+ 1 - 0
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';

+ 2 - 0
src/components/Upload/index.ts

@@ -0,0 +1,2 @@
+export { default as UploadContainer } from './src/UploadContainer.vue';
+// export * from './src/types';

+ 29 - 0
src/components/Upload/src/ThumnUrl.vue

@@ -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>

+ 62 - 0
src/components/Upload/src/UploadContainer.vue

@@ -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>

+ 244 - 0
src/components/Upload/src/UploadModal.vue

@@ -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>

+ 93 - 0
src/components/Upload/src/UploadPreviewModal.vue

@@ -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>

+ 159 - 0
src/components/Upload/src/data.tsx

@@ -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} />;
+    },
+  };
+}

+ 42 - 0
src/components/Upload/src/props.ts

@@ -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: () => [],
+  },
+};

+ 25 - 0
src/components/Upload/src/types.ts

@@ -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;
+}

+ 55 - 0
src/components/Upload/src/useUpload.ts

@@ -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 };
+}

+ 28 - 0
src/components/Upload/src/utils.ts

@@ -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);
+  });
+}

+ 4 - 0
src/router/menus/modules/demo/comp.ts

@@ -39,6 +39,10 @@ const menu: MenuModule = {
         name: '密码强度组件',
       },
       {
+        path: 'upload',
+        name: '上传组件',
+      },
+      {
         path: 'scroll',
         name: '滚动组件',
         children: [

+ 8 - 0
src/router/routes/modules/demo/comp.ts

@@ -170,5 +170,13 @@ export default {
         title: '密码强度组件',
       },
     },
+    {
+      path: '/upload',
+      name: 'UploadDemo',
+      component: () => import('/@/views/demo/comp/upload/index.vue'),
+      meta: {
+        title: '上传组件',
+      },
+    },
   ],
 } as AppRouteModule;

+ 38 - 20
src/utils/http/axios/Axios.ts

@@ -5,9 +5,10 @@ import { AxiosCanceler } from './axiosCancel';
 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 @@ export class VAxios {
       this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
   }
 
-  // /**
-  //  * @description:  文件上传
-  //  */
-  // uploadFiles(config: AxiosRequestConfig, params: File[]) {
-  //   const formData = new FormData();
-
-  //   Object.keys(params).forEach((key) => {
-  //     formData.append(key, params[key as any]);
-  //   });
-
-  //   return this.request({
-  //     ...config,
-  //     method: 'POST',
-  //     data: formData,
-  //     headers: {
-  //       'Content-type': ContentTypeEnum.FORM_DATA,
-  //     },
-  //   });
-  // }
+  /**
+   * @description:  文件上传
+   */
+  uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
+    const formData = new window.FormData();
+
+    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;
+        }
+
+        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:   请求方法

+ 11 - 0
src/utils/http/axios/types.ts

@@ -28,3 +28,14 @@ export interface Result<T = any> {
   message: string;
   result: T;
 }
+// multipart/form-data:上传文件
+export interface UploadFileParams {
+  // 其他参数
+  data?: { [key: string]: any };
+  // 文件参数的接口字段名
+  name?: string;
+  // 文件
+  file: File | Blob;
+  // 文件名
+  filename?: string;
+}

+ 17 - 0
src/views/demo/comp/upload/index.vue

@@ -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>