Browse Source

perf(upload): improve upload component

vben 4 years ago
parent
commit
661db0c767
39 changed files with 482 additions and 234 deletions
  1. 5 0
      CHANGELOG.zh_CN.md
  2. 1 1
      README.en-US.md
  3. 1 1
      README.md
  4. 6 6
      package.json
  5. 0 0
      src/api/sys/model/uploadModel.ts
  6. 0 0
      src/api/sys/upload.ts
  7. 1 1
      src/components/Drawer/src/props.ts
  8. 0 1
      src/components/Form/src/BasicForm.vue
  9. 1 1
      src/components/Icon/index.tsx
  10. 4 3
      src/components/Modal/src/props.ts
  11. 1 1
      src/components/Qrcode/src/index.vue
  12. 1 0
      src/components/Table/src/BasicTable.vue
  13. 1 1
      src/components/Table/src/hooks/useDataSource.ts
  14. 4 1
      src/components/Table/src/props.ts
  15. 6 0
      src/components/Table/src/style/index.less
  16. 2 0
      src/components/Table/src/types/table.ts
  17. 1 1
      src/components/Upload/index.ts
  18. 99 0
      src/components/Upload/src/BasicUpload.vue
  19. 4 7
      src/components/Upload/src/ThumnUrl.vue
  20. 0 62
      src/components/Upload/src/UploadContainer.vue
  21. 117 62
      src/components/Upload/src/UploadModal.vue
  22. 13 7
      src/components/Upload/src/UploadPreviewModal.vue
  23. 12 15
      src/components/Upload/src/data.tsx
  24. 21 4
      src/components/Upload/src/props.ts
  25. 1 1
      src/components/Upload/src/types.ts
  26. 3 3
      src/components/Upload/src/useUpload.ts
  27. 5 6
      src/components/Upload/src/utils.ts
  28. 5 0
      src/design/ant/btn.less
  29. 1 1
      src/design/var/index.less
  30. 10 4
      src/router/menus/modules/demo/comp.ts
  31. 6 2
      src/types/global.d.ts
  32. 21 1
      src/utils/file/download.ts
  33. 26 0
      src/utils/file/stream.ts
  34. 0 3
      src/utils/http/axios/Axios.ts
  35. 1 3
      src/utils/http/axios/index.ts
  36. 1 0
      src/utils/http/axios/types.ts
  37. 48 5
      src/views/demo/comp/upload/index.vue
  38. 24 1
      src/views/demo/feat/download/index.vue
  39. 29 29
      yarn.lock

+ 5 - 0
CHANGELOG.zh_CN.md

@@ -3,6 +3,11 @@
 ### ✨ Features
 
 - 新增 base64 文件流下载
+- 优化上传组件及示例
+
+### 🎫 Chores
+
+- 更新 antdv 到`2.0.0-rc.1`
 
 ## 2.0.0-rc.10 (2020-11-13)
 

+ 1 - 1
README.en-US.md

@@ -226,10 +226,10 @@ yarn clean:lib # Delete node_modules, supported window
 - [x] Data import and export
 - [x] Global error handling
 - [x] Rich text component
+- [x] Upload component
 
 ## Developing features
 
-- [ ] Upload component
 - [ ] Theme configuration
 - [ ] Dark theme
 - [ ] Build CDN

+ 1 - 1
README.md

@@ -228,10 +228,10 @@ yarn clean:lib # 删除node_modules,兼容window系统
 - [x] 系统性能优化
 - [x] 全局错误处理
 - [x] 富文本组件
+- [x] 上传组件
 
 ## 正在开发的功能
 
-- [ ] 上传组件
 - [ ] 主题配置
 - [ ] 黑暗主题
 - [ ] 打包 CDN

+ 6 - 6
package.json

@@ -22,8 +22,8 @@
   },
   "dependencies": {
     "@iconify/iconify": "^2.0.0-rc.2",
-    "@vueuse/core": "^4.0.0-beta.40",
-    "ant-design-vue": "^2.0.0-beta.15",
+    "@vueuse/core": "^4.0.0-beta.41",
+    "ant-design-vue": "^2.0.0-rc.1",
     "apexcharts": "3.22.0",
     "axios": "^0.21.0",
     "echarts": "^4.9.0",
@@ -33,10 +33,10 @@
     "nprogress": "^0.2.0",
     "path-to-regexp": "^6.2.0",
     "qrcode": "^1.4.4",
-    "vditor": "^3.6.0",
+    "vditor": "^3.6.2",
     "vue": "^3.0.2",
     "vue-i18n": "^9.0.0-beta.6",
-    "vue-router": "^4.0.0-rc.2",
+    "vue-router": "^4.0.0-rc.3",
     "vuex": "^4.0.0-rc.1",
     "vuex-module-decorators": "^1.0.1",
     "xlsx": "^0.16.8",
@@ -45,11 +45,11 @@
   "devDependencies": {
     "@commitlint/cli": "^11.0.0",
     "@commitlint/config-conventional": "^11.0.0",
-    "@iconify/json": "^1.1.254",
+    "@iconify/json": "^1.1.258",
     "@ls-lint/ls-lint": "^1.9.2",
     "@purge-icons/generated": "^0.4.1",
     "@types/echarts": "^4.9.0",
-    "@types/fs-extra": "^9.0.2",
+    "@types/fs-extra": "^9.0.4",
     "@types/koa-static": "^4.0.1",
     "@types/lodash-es": "^4.17.3",
     "@types/mockjs": "^1.0.3",

+ 0 - 0
src/api/demo/model/uploadModel.ts → src/api/sys/model/uploadModel.ts


+ 0 - 0
src/api/demo/upload.ts → src/api/sys/upload.ts


+ 1 - 1
src/components/Drawer/src/props.ts

@@ -24,7 +24,7 @@ export const footerProps = {
   okButtonProps: Object as PropType<any>,
   okText: {
     type: String as PropType<string>,
-    default: '保存',
+    default: '确认',
   },
   okType: {
     type: String as PropType<string>,

+ 0 - 1
src/components/Form/src/BasicForm.vue

@@ -44,7 +44,6 @@
   import { useFormValues } from './hooks/useFormValues';
   import useAdvanced from './hooks/useAdvanced';
   import { useFormAction } from './hooks/useFormAction';
-
   export default defineComponent({
     name: 'BasicForm',
     components: { FormItem, Form, Row, FormAction },

+ 1 - 1
src/components/Icon/index.tsx

@@ -18,7 +18,7 @@ export default defineComponent({
     // icon size
     size: {
       type: [String, Number] as PropType<string | number>,
-      default: 14,
+      default: 16,
     },
     prefix: {
       type: String as PropType<string>,

+ 4 - 3
src/components/Modal/src/props.ts

@@ -1,4 +1,5 @@
 import type { PropType } from 'vue';
+import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
 export const modalProps = {
   visible: Boolean as PropType<boolean>,
   // open drag
@@ -16,7 +17,7 @@ export const modalProps = {
   },
   okText: {
     type: String as PropType<string>,
-    default: '保存',
+    default: '确认',
   },
   closeFunc: Function as PropType<() => Promise<boolean>>,
 };
@@ -100,9 +101,9 @@ export const basicProps = Object.assign({}, modalProps, {
     default: 'primary',
   },
 
-  okButtonProps: Object as PropType<any>,
+  okButtonProps: Object as PropType<ButtonProps>,
 
-  cancelButtonProps: Object as PropType<any>,
+  cancelButtonProps: Object as PropType<ButtonProps>,
 
   title: {
     type: String as PropType<string>,

+ 1 - 1
src/components/Qrcode/src/index.vue

@@ -7,7 +7,7 @@
   import { defineComponent, watchEffect, PropType, ref, unref } from 'vue';
   import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus';
   import { toDataURL } from 'qrcode';
-  import { downloadByUrl } from '/@/utils/file/FileDownload';
+  import { downloadByUrl } from '/@/utils/file/download';
 
   export default defineComponent({
     name: 'QrCode',

+ 1 - 0
src/components/Table/src/BasicTable.vue

@@ -4,6 +4,7 @@
     class="basic-table"
     :class="{
       'table-form-container': getBindValues.useSearchForm,
+      inset: getBindValues.inset,
     }"
   >
     <BasicForm

+ 1 - 1
src/components/Table/src/hooks/useDataSource.ts

@@ -84,7 +84,7 @@ export function useDataSource(
     const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref(
       propsRef
     );
-    if (!api && !isFunction(api)) return;
+    if (!api || !isFunction(api)) return;
     try {
       loadingRef.value = true;
       const { pageField, sizeField, listField, totalField } = fetchSetting || FETCH_SETTING;

+ 4 - 1
src/components/Table/src/props.ts

@@ -16,7 +16,10 @@ export const basicProps = {
   tableSetting: {
     type: Object as PropType<TableSetting>,
   },
-
+  inset: {
+    type: Boolean as PropType<boolean>,
+    default: false,
+  },
   sortFn: {
     type: Function as PropType<(sortInfo: SorterResult) => any>,
     default: DEFAULT_SORT_FN,

+ 6 - 0
src/components/Table/src/style/index.less

@@ -49,6 +49,12 @@
     }
   }
 
+  &.inset {
+    .ant-table-wrapper {
+      padding: 0;
+    }
+  }
+
   //
   .ant-table {
     border: none;

+ 2 - 0
src/components/Table/src/types/table.ts

@@ -126,6 +126,8 @@ export interface TableSetting {
 export interface BasicTableProps<T = any> {
   // 自定义排序方法
   sortFn?: (sortInfo: SorterResult) => any;
+  // 取消表格的默认padding
+  inset?: boolean;
   // 显示表格设置
   showTableSetting?: boolean;
   tableSetting?: TableSetting;

+ 1 - 1
src/components/Upload/index.ts

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

+ 99 - 0
src/components/Upload/src/BasicUpload.vue

@@ -0,0 +1,99 @@
+<template>
+  <div>
+    <a-button-group>
+      <a-button type="primary" @click="openUploadModal" preIcon="ant-design:cloud-upload-outlined">
+        上传
+      </a-button>
+      <Tooltip placement="bottom" v-if="showPreview">
+        <template #title>
+          已上传
+          <template v-if="fileListRef.length">{{ fileListRef.length }}</template>
+        </template>
+        <a-button @click="openPreviewModal">
+          <Icon icon="ant-design:eye-outlined" />
+          <template v-if="fileListRef.length && showPreviewNumber">
+            {{ fileListRef.length }}
+          </template>
+        </a-button>
+      </Tooltip>
+    </a-button-group>
+
+    <UploadModal v-bind="bindValue" @register="registerUploadModal" @change="handleChange" />
+
+    <UploadPreviewModal
+      :value="fileListRef"
+      @register="registerPreviewModal"
+      @list-change="handlePreviewChange"
+    />
+  </div>
+</template>
+<script lang="ts">
+  import { defineComponent, ref, watch, unref, computed } from 'vue';
+
+  import UploadModal from './UploadModal.vue';
+  import UploadPreviewModal from './UploadPreviewModal.vue';
+  import Icon from '/@/components/Icon';
+  import { Tooltip } from 'ant-design-vue';
+
+  import { useModal } from '/@/components/Modal';
+
+  import { uploadContainerProps } from './props';
+  import { omit } from 'lodash-es';
+
+  export default defineComponent({
+    components: { UploadModal, UploadPreviewModal, Icon, Tooltip },
+    props: uploadContainerProps,
+    setup(props, { emit, attrs }) {
+      // 上传modal
+      const [registerUploadModal, { openModal: openUploadModal }] = useModal();
+
+      //   预览modal
+      const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
+
+      const fileListRef = ref<string[]>([]);
+
+      const showPreview = computed(() => {
+        const { emptyHidePreview } = props;
+        if (!emptyHidePreview) return true;
+        return emptyHidePreview ? fileListRef.value.length > 0 : true;
+      });
+
+      const bindValue = computed(() => {
+        const value = { ...attrs, ...props };
+        return omit(value, 'onChange');
+      });
+
+      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,
+        showPreview,
+        bindValue,
+      };
+    },
+  });
+</script>

+ 4 - 7
src/components/Upload/src/ThumnUrl.vue

@@ -5,25 +5,22 @@
   </span>
 </template>
 <script lang="ts">
-  import { defineComponent } from 'vue';
+  import { defineComponent, PropType } from 'vue';
 
   export default defineComponent({
     props: {
       fileUrl: {
-        type: String,
+        type: String as PropType<string>,
         default: '',
       },
       fileType: {
-        type: String,
+        type: String as PropType<string>,
         default: '',
       },
       fileName: {
-        type: String,
+        type: String as PropType<string>,
         default: '',
       },
     },
-    setup() {
-      return {};
-    },
   });
 </script>

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

@@ -1,62 +0,0 @@
-<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>

+ 117 - 62
src/components/Upload/src/UploadModal.vue

@@ -1,31 +1,44 @@
 <template>
   <BasicModal
+    width="800px"
+    title="上传"
+    okText="保存"
     v-bind="$attrs"
     @register="register"
     @ok="handleOk"
     :closeFunc="handleCloseFunc"
     :maskClosable="false"
-    width="800px"
-    title="上传组件"
+    :keyboard="false"
     wrapClassName="upload-modal"
-    :okButtonProps="{ disabled: isUploadingRef }"
+    :okButtonProps="getOkButtonProps"
     :cancelButtonProps="{ disabled: isUploadingRef }"
   >
     <template #centerdFooter>
-      <a-button @click="handleStartUpload" color="success" :loading="isUploadingRef">
-        {{ isUploadingRef ? '上传中' : '开始上传' }}
+      <a-button
+        @click="handleStartUpload"
+        color="success"
+        :disabled="!getIsSelectFile"
+        :loading="isUploadingRef"
+      >
+        {{ getUploadBtnText }}
       </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" />
+
+    <BasicTable @register="registerTable" :dataSource="fileListRef">
+      <template #toolbar>
+        <Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">
+          <a-button type="primary"> 选择文件 </a-button>
+        </Upload>
+      </template>
+      <template #tableTitle>
+        <Alert :message="getHelpText" type="info" banner></Alert>
+      </template>
+    </BasicTable>
   </BasicModal>
 </template>
 <script lang="ts">
-  import { defineComponent, reactive, ref, toRef, unref } from 'vue';
-  import { Upload } from 'ant-design-vue';
+  import { defineComponent, reactive, ref, toRefs, unref, computed } from 'vue';
+  import { Upload, Alert } from 'ant-design-vue';
   import { BasicModal, useModalInner } from '/@/components/Modal';
   import { BasicTable, useTable } from '/@/components/Table';
   // hooks
@@ -39,23 +52,56 @@
   import { checkFileType, checkImgType, getBase64WithFile } from './utils';
   import { buildUUID } from '/@/utils/uuid';
   import { createImgPreview } from '/@/components/Preview/index';
-  import { uploadApi } from '/@/api/demo/upload';
+  import { uploadApi } from '/@/api/sys/upload';
+  import { isFunction } from '/@/utils/is';
+  import { warn } from '/@/utils/log';
 
   export default defineComponent({
-    components: { BasicModal, Upload, BasicTable },
+    components: { BasicModal, Upload, BasicTable, Alert },
     props: basicProps,
     setup(props, { emit }) {
+      //   是否正在上传
+      const isUploadingRef = ref(false);
+      const fileListRef = ref<FileItem[]>([]);
+      const state = reactive<{ fileList: FileItem[] }>({
+        fileList: [],
+      });
+
       const [register, { closeModal }] = useModalInner();
+
+      const { accept, helpText, maxNumber, maxSize } = toRefs(props);
       const { getAccept, getStringAccept, getHelpText } = useUploadType({
-        acceptRef: toRef(props, 'accept'),
-        helpTextRef: toRef(props, 'helpText'),
-        maxNumberRef: toRef(props, 'maxNumber'),
-        maxSizeRef: toRef(props, 'maxSize'),
+        acceptRef: accept,
+        helpTextRef: helpText,
+        maxNumberRef: maxNumber,
+        maxSizeRef: maxSize,
       });
 
-      const fileListRef = ref<FileItem[]>([]);
-      const state = reactive<{ fileList: FileItem[] }>({ fileList: [] });
       const { createMessage } = useMessage();
+
+      const getIsSelectFile = computed(() => {
+        return (
+          fileListRef.value.length > 0 &&
+          !fileListRef.value.every((item) => item.status === UploadResultStatus.SUCCESS)
+        );
+      });
+
+      const getOkButtonProps = computed(() => {
+        const someSuccess = fileListRef.value.some(
+          (item) => item.status === UploadResultStatus.SUCCESS
+        );
+        return {
+          disabled: isUploadingRef.value || fileListRef.value.length === 0 || !someSuccess,
+        };
+      });
+
+      const getUploadBtnText = computed(() => {
+        const someError = fileListRef.value.some(
+          (item) => item.status === UploadResultStatus.ERROR
+        );
+        return isUploadingRef.value ? '上传中' : someError ? '重新上传失败文件' : '开始上传';
+      });
+
       // 上传前校验
       function beforeUpload(file: File) {
         const { size, name } = file;
@@ -73,6 +119,14 @@
           createMessage.error!(`只能上传${accept.join(',')}格式文件`);
           return false;
         }
+        const commonItem = {
+          uuid: buildUUID(),
+          file,
+          size,
+          name,
+          percent: 0,
+          type: name.split('.').pop(),
+        };
         // 生成图片缩略图
         if (checkImgType(file)) {
           // beforeUpload,如果异步会调用自带上传方法
@@ -81,29 +135,13 @@
             fileListRef.value = [
               ...unref(fileListRef),
               {
-                uuid: buildUUID(),
-                file,
                 thumbUrl,
-                size,
-                name,
-                percent: 0,
-                type: name.split('.').pop(),
+                ...commonItem,
               },
             ];
           });
         } else {
-          fileListRef.value = [
-            ...unref(fileListRef),
-            {
-              uuid: buildUUID(),
-
-              file,
-              size,
-              name,
-              percent: 0,
-              type: name.split('.').pop(),
-            },
-          ];
+          fileListRef.value = [...unref(fileListRef), commonItem];
         }
         return false;
       }
@@ -112,6 +150,7 @@
         const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
         index !== -1 && fileListRef.value.splice(index, 1);
       }
+
       // 预览
       function handlePreview(record: FileItem) {
         const { thumbUrl = '' } = record;
@@ -119,19 +158,18 @@
           imageList: [thumbUrl],
         });
       }
-      const [registerTable] = useTable({
-        columns: createTableColumns(),
-        actionColumn: createActionColumn(handleRemove, handlePreview),
-        pagination: false,
-      });
-      //   是否正在上传
-      const isUploadingRef = ref(false);
+
       async function uploadApiByItem(item: FileItem) {
+        const { api } = props;
+        if (!api || !isFunction(api)) {
+          return warn('upload api must exist and be a function');
+        }
         try {
           item.status = UploadResultStatus.UPLOADING;
 
           const { data } = await uploadApi(
             {
+              ...(props.uploadParams || {}),
               file: item.file,
             },
             function onUploadProgress(progressEvent: ProgressEvent) {
@@ -154,32 +192,42 @@
           };
         }
       }
+
       // 点击开始上传
       async function handleStartUpload() {
+        const { maxNumber } = props;
+        if (fileListRef.value.length > maxNumber) {
+          return createMessage.warning(`最多只能上传${maxNumber}个文件`);
+        }
         try {
           isUploadingRef.value = true;
+          // 只上传不是成功状态的
+          const uploadFileList =
+            fileListRef.value.filter((item) => item.status !== UploadResultStatus.SUCCESS) || [];
           const data = await Promise.all(
-            unref(fileListRef).map((item) => {
+            uploadFileList.map((item) => {
               return uploadApiByItem(item);
             })
           );
           isUploadingRef.value = false;
           // 生产环境:抛出错误
-          const errorList = data.filter((item) => !item.success);
-          if (errorList.length > 0) {
-            throw errorList;
-          }
+          const errorList = data.filter((item: any) => !item.success);
+          if (errorList.length > 0) throw errorList;
         } catch (e) {
           isUploadingRef.value = false;
           throw e;
         }
       }
+
       //   点击保存
       function handleOk() {
-        // TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }}
+        const { maxNumber } = props;
+
+        if (fileListRef.value.length > maxNumber) {
+          return createMessage.warning(`最多只能上传${maxNumber}个文件`);
+        }
         if (isUploadingRef.value) {
-          createMessage.warning('请等待文件上传后,保存');
-          return;
+          return createMessage.warning('请等待文件上传后,保存');
         }
         const fileList: string[] = [];
 
@@ -189,18 +237,15 @@
             fileList.push(responseData.url);
           }
         }
-
         // 存在一个上传成功的即可保存
-
         if (fileList.length <= 0) {
-          createMessage.warning('没有上传成功的文件,无法保存');
-          return;
+          return createMessage.warning('没有上传成功的文件,无法保存');
         }
-        console.log(fileList);
-        emit('change', fileList);
         fileListRef.value = [];
         closeModal();
+        emit('change', fileList);
       }
+
       // 点击关闭:则所有操作不保存,包括上传的
       function handleCloseFunc() {
         if (!isUploadingRef.value) {
@@ -211,11 +256,22 @@
           return false;
         }
       }
+
+      const [registerTable] = useTable({
+        columns: createTableColumns(),
+        actionColumn: createActionColumn(handleRemove, handlePreview),
+        pagination: false,
+        inset: true,
+        scroll: {
+          y: 3000,
+        },
+      });
       return {
         register,
         closeModal,
         getHelpText,
         getStringAccept,
+        getOkButtonProps,
         beforeUpload,
         registerTable,
         fileListRef,
@@ -224,14 +280,13 @@
         handleStartUpload,
         handleOk,
         handleCloseFunc,
+        getIsSelectFile,
+        getUploadBtnText,
       };
     },
   });
 </script>
 <style lang="less">
-  //   /deep/ .ant-upload-list {
-  //     display: none;
-  //   }
   .upload-modal {
     .ant-upload-list {
       display: none;

+ 13 - 7
src/components/Upload/src/UploadPreviewModal.vue

@@ -1,10 +1,10 @@
 <template>
   <BasicModal
+    width="800px"
+    title="预览"
     wrapClassName="upload-preview-modal"
     v-bind="$attrs"
-    width="800px"
     @register="register"
-    title="预览"
     :showOkBtn="false"
   >
     <BasicTable @register="registerTable" :dataSource="fileListRef" />
@@ -12,17 +12,18 @@
 </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 { previewProps } from './props';
   import { PreviewFileItem } from './types';
   import { createImgPreview } from '/@/components/Preview/index';
-  import { downloadByUrl } from '/@/utils/file/FileDownload';
+  import { downloadByUrl } from '/@/utils/file/download';
 
+  import { createPreviewColumns, createPreviewActionColumn } from './data';
   export default defineComponent({
     components: { BasicModal, BasicTable },
-    props: priviewProps,
+    props: previewProps,
     setup(props, { emit }) {
       const [register, { closeModal }] = useModalInner();
       const fileListRef = ref<PreviewFileItem[]>([]);
@@ -43,17 +44,19 @@
         },
         { 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',
+            'list-change',
             fileListRef.value.map((item) => item.url)
           );
         }
       }
+
       // 预览
       function handlePreview(record: PreviewFileItem) {
         const { url = '' } = record;
@@ -61,16 +64,19 @@
           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,

+ 12 - 15
src/components/Upload/src/data.tsx

@@ -1,10 +1,6 @@
-// import { BasicColumn, TableAction, ActionItem } from '@/components/table';
 import { checkImgType, isImgTypeByName } from './utils';
-// import ThumnUrl from './ThumbUrl.vue';
-import { Progress } from 'ant-design-vue';
+import { Progress, Tag } 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';
 
 // 文件上传列表
@@ -16,8 +12,7 @@ export function createTableColumns(): BasicColumn[] {
       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} />;
+        return <span>{thumbUrl ? <img style={{ maxWidth: '60px' }} src={thumbUrl} /> : type}</span>;
       },
     },
     {
@@ -26,7 +21,7 @@ export function createTableColumns(): BasicColumn[] {
       align: 'left',
       customRender: ({ text, record }) => {
         const { percent, status: uploadStatus } = (record as FileItem) || {};
-        let status = 'normal';
+        let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
         if (uploadStatus === UploadResultStatus.ERROR) {
           status = 'exception';
         } else if (uploadStatus === UploadResultStatus.UPLOADING) {
@@ -63,11 +58,11 @@ export function createTableColumns(): BasicColumn[] {
       width: 100,
       customRender: ({ text }) => {
         if (text === UploadResultStatus.SUCCESS) {
-          return '上传成功';
+          return <Tag color="green">{() => '上传成功'}</Tag>;
         } else if (text === UploadResultStatus.ERROR) {
-          return '上传失败';
+          return <Tag color="red">{() => '上传失败'}</Tag>;
         } else if (text === UploadResultStatus.UPLOADING) {
-          return '上传中';
+          return <Tag color="blue">{() => '上传中'}</Tag>;
         }
 
         return text;
@@ -85,6 +80,7 @@ export function createActionColumn(handleRemove: Function, handlePreview: Functi
       const actions: ActionItem[] = [
         {
           label: '删除',
+          color: 'error',
           onClick: handleRemove.bind(null, record),
         },
       ];
@@ -125,9 +121,9 @@ export function createPreviewActionColumn({
   handlePreview,
   handleDownload,
 }: {
-  handleRemove: Function;
-  handlePreview: Function;
-  handleDownload: Function;
+  handleRemove: Fn;
+  handlePreview: Fn;
+  handleDownload: Fn;
 }): BasicColumn {
   return {
     width: 160,
@@ -135,11 +131,12 @@ export function createPreviewActionColumn({
     dataIndex: 'action',
     fixed: false,
     customRender: ({ record }) => {
-      const { url } = (record as PreviewFileItem) || {};
+      const { url } = (record || {}) as PreviewFileItem;
 
       const actions: ActionItem[] = [
         {
           label: '删除',
+          color: 'error',
           onClick: handleRemove.bind(null, record),
         },
         {

+ 21 - 4
src/components/Upload/src/props.ts

@@ -10,10 +10,10 @@ export const basicProps = {
     type: Number as PropType<number>,
     default: 2,
   },
-  // 最大数量的文件,0不限制
+  // 最大数量的文件,Infinity不限制
   maxNumber: {
     type: Number as PropType<number>,
-    default: 0,
+    default: Infinity,
   },
   // 根据后缀,或者其他
   accept: {
@@ -21,9 +21,18 @@ export const basicProps = {
     default: () => [],
   },
   multiple: {
-    type: Boolean,
+    type: Boolean as PropType<boolean>,
     default: true,
   },
+  uploadParams: {
+    type: Object as PropType<any>,
+    default: {},
+  },
+  api: {
+    type: Function as PropType<PromiseFn>,
+    default: null,
+    required: true,
+  },
 };
 
 export const uploadContainerProps = {
@@ -32,9 +41,17 @@ export const uploadContainerProps = {
     default: () => [],
   },
   ...basicProps,
+  showPreviewNumber: {
+    type: Boolean as PropType<boolean>,
+    default: true,
+  },
+  emptyHidePreview: {
+    type: Boolean as PropType<boolean>,
+    default: false,
+  },
 };
 
-export const priviewProps = {
+export const previewProps = {
   value: {
     type: Array as PropType<string[]>,
     default: () => [],

+ 1 - 1
src/components/Upload/src/types.ts

@@ -1,4 +1,4 @@
-import { UploadApiResult } from '/@/api/demo/model/uploadModel';
+import { UploadApiResult } from '/@/api/sys/model/uploadModel';
 
 export enum UploadResultStatus {
   SUCCESS = 'success',

+ 3 - 3
src/components/Upload/src/useUpload.ts

@@ -42,12 +42,12 @@ export function useUploadType({
 
     const maxSize = unref(maxSizeRef);
     if (maxSize) {
-      helpTexts.push(`不超过${maxSize}MB`);
+      helpTexts.push(`单个文件不超过${maxSize}MB`);
     }
 
     const maxNumber = unref(maxNumberRef);
-    if (maxNumber) {
-      helpTexts.push(`最多可选择${maxNumber}个文件`);
+    if (maxNumber && maxNumber !== Infinity) {
+      helpTexts.push(`最多只能上传${maxNumber}个文件`);
     }
     return helpTexts.join(',');
   });

+ 5 - 6
src/components/Upload/src/utils.ts

@@ -3,18 +3,17 @@ export function checkFileType(file: File, accepts: string[]) {
   // 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;
-  }
+  return reg.test(file.name);
 }
+
 export function checkImgType(file: File) {
-  return /\.(jpg|jpeg|png|gif)$/i.test(file.name);
+  return isImgTypeByName(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;

+ 5 - 0
src/design/ant/btn.less

@@ -6,6 +6,11 @@
   // &.ant-btn-primary:not(.ant-btn-link) {
   //   box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08) !important;
   // }
+  // &-group {
+  //   .ant-btn:not(:first-child) {
+  //     bottom: 1px;
+  //   }
+  // }
 
   &-primary {
     color: @white;

+ 1 - 1
src/design/var/index.less

@@ -16,4 +16,4 @@
 @page-loading-z-index: 10000;
 
 // left-menu
-@app-menu-item-height: 46px;
+@app-menu-item-height: 44px;

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

@@ -4,6 +4,9 @@ const menu: MenuModule = {
   menu: {
     name: '组件',
     path: '/comp',
+    tag: {
+      dot: true,
+    },
     children: [
       {
         path: 'basic',
@@ -38,10 +41,13 @@ const menu: MenuModule = {
         path: 'strength-meter',
         name: '密码强度组件',
       },
-      // {
-      //   path: 'upload',
-      //   name: '上传组件',
-      // },
+      {
+        path: 'upload',
+        name: '上传组件',
+        tag: {
+          content: 'new',
+        },
+      },
       {
         path: 'scroll',
         name: '滚动组件',

+ 6 - 2
src/types/global.d.ts

@@ -1,5 +1,9 @@
-declare interface Fn<T = any> {
-  (...arg: T[]): T;
+declare interface Fn<T = any, R = T> {
+  (...arg: T[]): R;
+}
+
+declare interface PromiseFn<T = any, R = T> {
+  (...arg: T[]): Promise<R>;
 }
 
 // 任意对象

+ 21 - 1
src/utils/file/FileDownload.ts → src/utils/file/download.ts

@@ -1,5 +1,25 @@
-import { dataURLtoBlob } from './stream';
+import { dataURLtoBlob, urlToBase64 } from './stream';
 
+/**
+ * Download online pictures
+ * @param url
+ * @param filename
+ * @param mime
+ * @param bom
+ */
+export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) {
+  urlToBase64(url).then((base64) => {
+    downloadByBase64(base64, filename, mime, bom);
+  });
+}
+
+/**
+ * Download pictures based on base64
+ * @param buf
+ * @param filename
+ * @param mime
+ * @param bom
+ */
 export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) {
   const base64Buf = dataURLtoBlob(buf);
   downloadByData(base64Buf, filename, mime, bom);

+ 26 - 0
src/utils/file/stream.ts

@@ -13,3 +13,29 @@ export function dataURLtoBlob(base64Buf: string): Blob {
   }
   return new Blob([u8arr], { type: mime });
 }
+
+/**
+ * img url to base64
+ * @param url
+ */
+export function urlToBase64(url: string, mineType?: string): Promise<string> {
+  return new Promise((resolve, reject) => {
+    let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>;
+    const ctx = canvas!.getContext('2d');
+
+    const img = new Image();
+    img.crossOrigin = '';
+    img.onload = function () {
+      if (!canvas || !ctx) {
+        return reject();
+      }
+      canvas.height = img.height;
+      canvas.width = img.width;
+      ctx.drawImage(img, 0, 0);
+      const dataURL = canvas.toDataURL(mineType || 'image/png');
+      canvas = null;
+      resolve(dataURL);
+    };
+    img.src = url;
+  });
+}

+ 0 - 3
src/utils/http/axios/Axios.ts

@@ -118,11 +118,8 @@ export class VAxios {
       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;

+ 1 - 3
src/utils/http/axios/index.ts

@@ -160,20 +160,18 @@ const transform: AxiosTransform = {
     try {
       if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
         createMessage.error('接口请求超时,请刷新页面重试!');
-        return;
       }
       if (err && err.includes('Network Error')) {
         createErrorModal({
           title: '网络异常',
           content: '请检查您的网络连接是否正常!',
         });
-        return;
       }
     } catch (error) {
       throw new Error(error);
     }
     checkStatus(error.response && error.response.status, msg);
-    return error;
+    return Promise.reject(error);
   },
 };
 

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

@@ -38,4 +38,5 @@ export interface UploadFileParams {
   file: File | Blob;
   // 文件名
   filename?: string;
+  [key: string]: any;
 }

+ 48 - 5
src/views/demo/comp/upload/index.vue

@@ -1,17 +1,60 @@
 <template>
   <div class="p-4">
-    <UploadContainer :maxSize="5" />
+    <a-alert message="基础示例" class="my-5"></a-alert>
+    <BasicUpload :maxSize="20" :maxNumber="10" @change="handleChange" :api="uploadApi" />
+
+    <a-alert message="嵌入表单,加入表单校验" class="my-5"></a-alert>
+
+    <BasicForm @register="register" />
   </div>
 </template>
 <script lang="ts">
-  import { defineComponent } from 'vue';
-  import { UploadContainer } from '/@/components/Upload/index';
+  import { defineComponent, h } from 'vue';
+  import { BasicUpload } from '/@/components/Upload';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
 
+  import { uploadApi } from '/@/api/sys/upload';
   //   import { Alert } from 'ant-design-vue';
+
+  const schemas: FormSchema[] = [
+    {
+      field: 'field1',
+      component: 'Input',
+      label: '字段1',
+      colProps: {
+        span: 8,
+      },
+      rules: [{ required: true, type: 'array', message: '请选择上传文件' }],
+      render: ({ model, field }) => {
+        return h(BasicUpload, {
+          value: model[field],
+          api: uploadApi,
+          onChange: (val: string[]) => {
+            model[field] = val;
+          },
+        });
+      },
+    },
+  ];
   export default defineComponent({
-    components: { UploadContainer },
+    components: { BasicUpload, BasicForm },
     setup() {
-      return {};
+      const { createMessage } = useMessage();
+      const [register] = useForm({
+        labelWidth: 120,
+        schemas,
+        actionColOptions: {
+          span: 16,
+        },
+      });
+      return {
+        handleChange: (list: string[]) => {
+          createMessage.info(`已上传文件${JSON.stringify(list)}`);
+        },
+        uploadApi,
+        register,
+      };
     },
   });
 </script>

+ 24 - 1
src/views/demo/feat/download/index.vue

@@ -8,11 +8,21 @@
 
     <a-alert message="base64流下载" />
     <a-button type="primary" class="my-4" @click="handleDownloadByBase64"> base64流下载 </a-button>
+
+    <a-alert message="图片Url下载,如果有跨域问题,需要处理图片跨域" />
+    <a-button type="primary" class="my-4" @click="handleDownloadByOnlineUrl">
+      图片Url下载
+    </a-button>
   </div>
 </template>
 <script lang="ts">
   import { defineComponent } from 'vue';
-  import { downloadByUrl, downloadByData, downloadByBase64 } from '/@/utils/file/FileDownload';
+  import {
+    downloadByUrl,
+    downloadByData,
+    downloadByBase64,
+    downloadByOnlineUrl,
+  } from '/@/utils/file/download';
   import imgBase64 from './imgBase64';
   export default defineComponent({
     setup() {
@@ -24,15 +34,28 @@
           url: 'https://codeload.github.com/anncwb/vue-vben-admin-doc/zip/master',
           target: '_self',
         });
+
+        downloadByUrl({
+          url: 'https://vebn.oss-cn-beijing.aliyuncs.com/vben/logo.png',
+          target: '_self',
+        });
       }
 
       function handleDownloadByBase64() {
         downloadByBase64(imgBase64, 'logo.png');
       }
+
+      function handleDownloadByOnlineUrl() {
+        downloadByOnlineUrl(
+          'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5944817f47b8408e9f1442ece49d68ca~tplv-k3u1fbpfcp-watermark.image',
+          'logo.png'
+        );
+      }
       return {
         handleDownloadByUrl,
         handleDownByData,
         handleDownloadByBase64,
+        handleDownloadByOnlineUrl,
       };
     },
   });

+ 29 - 29
yarn.lock

@@ -1050,10 +1050,10 @@
   resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.2.tgz#c4a95ddc06ca9b9496df03604e66fdefb39f4c4b"
   integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw==
 
-"@iconify/json@^1.1.254":
-  version "1.1.256"
-  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.256.tgz#0f138d421ab12faca2fdd49aaf4fbc0122db08e3"
-  integrity sha512-CeLKbKL3lvq8afhR3LEyaBqXZDC52fgU0Ij3LbTRCwPUsumLNzhXA7MzN/f0JDYfXm9LShkfpgMcm00wQaANgg==
+"@iconify/json@^1.1.258":
+  version "1.1.258"
+  resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.258.tgz#392064ae8fd4c6d542c21bb4d0d57d5860f38abb"
+  integrity sha512-x5DKhRrg8v1NWmClWa8zA80gWQ9xevivsUAF4s8CyAl/ZplBsEE1funKuuVcIKjexyE1UXb7uFWrUKt1fB5n1A==
 
 "@koa/cors@^3.1.0":
   version "3.1.0"
@@ -1316,10 +1316,10 @@
     "@types/qs" "*"
     "@types/serve-static" "*"
 
-"@types/fs-extra@^9.0.2":
-  version "9.0.3"
-  resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.3.tgz#9996e5cce993508c32325380b429f04a1327523e"
-  integrity sha512-NKdGoXLTFTRED3ENcfCsH8+ekV4gbsysanx2OPbstXVV6fZMgUCqTxubs6I9r7pbOJbFgVq1rpFtLURjKCZWUw==
+"@types/fs-extra@^9.0.4":
+  version "9.0.4"
+  resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa"
+  integrity sha512-50GO5ez44lxK5MDH90DYHFFfqxH7+fTqEEnvguQRzJ/tY9qFrMSHLiYHite+F3SNmf7+LHC1eMXojuD+E3Qcyg==
   dependencies:
     "@types/node" "*"
 
@@ -1725,18 +1725,18 @@
     vscode-languageserver-textdocument "^1.0.1"
     vscode-uri "^2.1.2"
 
-"@vueuse/core@^4.0.0-beta.40":
-  version "4.0.0-beta.40"
-  resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.40.tgz#7efdc15c1b994647dff7ae65c0ca573d96ce9b28"
-  integrity sha512-FOTOUrXAAp0NOmy8hMlP1HpUhnB8LeRJZDOEUl/A9gKMDwWvPTEvxKsDAIzSa4s7I0MapVzfeP3soNCNfl9+vQ==
+"@vueuse/core@^4.0.0-beta.41":
+  version "4.0.0-beta.41"
+  resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.41.tgz#0058aed5ade75ae2866283498009ad5172cbae84"
+  integrity sha512-CgUih65PzYScorm1S4F93e6XXm+qxA8GrRLOSB1kXaqtP6vXedwkBxKkNEYNACx4reL4VEHqM/BrM6FajXkQUg==
   dependencies:
-    "@vueuse/shared" "4.0.0-beta.40"
+    "@vueuse/shared" "4.0.0-beta.41"
     vue-demi latest
 
-"@vueuse/shared@4.0.0-beta.40":
-  version "4.0.0-beta.40"
-  resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.40.tgz#76e9b52228159e7ec88df2c8f4eea8fce1a42ec3"
-  integrity sha512-Ay71viUTXs0XX2hQ04kEExhpsCrw3KankBMP7euorsPjuQmIZjUA4NNOb45UAudg+uF5HXLpgWLvwb4cMOLHnQ==
+"@vueuse/shared@4.0.0-beta.41":
+  version "4.0.0-beta.41"
+  resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.41.tgz#395782ea2e580f1fc9488d25c89bd09f70170b25"
+  integrity sha512-dqnuEPPC3OUJ6L6rhMiOCuPWIR698DtdwOydwCZBISsG2V6gZ2QFND6xtRwLib6/lhUMYVYPwIz3hPjlx7BIzw==
   dependencies:
     vue-demi latest
 
@@ -1850,10 +1850,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   dependencies:
     color-convert "^2.0.1"
 
-ant-design-vue@^2.0.0-beta.15:
-  version "2.0.0-beta.15"
-  resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-beta.15.tgz#3c787dabb70a33885d0e751e58f9a5610ed06134"
-  integrity sha512-OxZy+ZYU3LauIL4Rhqwy441K/iD++Cit6upnQy5+LVUrX0PSObPqPqMWVpncbAmJJYTEz88gkvgGeYqBdzouWA==
+ant-design-vue@^2.0.0-rc.1:
+  version "2.0.0-rc.1"
+  resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-rc.1.tgz#2ef02475f3aa4c1474f2fe3cf44a52c34787be02"
+  integrity sha512-iKXkFtTHarvLHV7LWmYh6g/Cmkv+xK+vS621A1Qvg37Z6lCGg3K9BGAizmklAYzOTiPz0Ltt63eSiNqYMGh52g==
   dependencies:
     "@ant-design-vue/use" "^0.0.1-0"
     "@ant-design/icons-vue" "^5.1.5"
@@ -8109,10 +8109,10 @@ vary@^1.1.2:
   resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
 
-vditor@^3.6.0:
-  version "3.6.1"
-  resolved "https://registry.npmjs.org/vditor/-/vditor-3.6.1.tgz#b0b510f23d0cf0e5d8b3d36924e40400de96f692"
-  integrity sha512-83GdGLIWrV1x04aK8DO9aZidqQfmuGXXUbxSCuQxRla+T9KfnFRmJwfqIxXQm8h+4jUBCFL38e8PqLa3fBOf9w==
+vditor@^3.6.2:
+  version "3.6.2"
+  resolved "https://registry.npmjs.org/vditor/-/vditor-3.6.2.tgz#ee6011efa3ec563c6356ed82efbf2e00ba2e35c6"
+  integrity sha512-HPHHun5+IXmYGMKDWcUD83VfP1Qfncz7DmaIKoWpluJgE8ve7s+4RbFBcaEpYPXYzIuL2UTHoMnIjmTPbenOCA==
   dependencies:
     diff-match-patch "^1.0.5"
 
@@ -8272,10 +8272,10 @@ vue-i18n@^9.0.0-beta.6:
   dependencies:
     source-map "^0.6.1"
 
-vue-router@^4.0.0-rc.2:
-  version "4.0.0-rc.2"
-  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.2.tgz#8545cab76a05ca4f6dffbe6c6a671a4dbf585ab2"
-  integrity sha512-51mBp39rzBFpk1nyU9SkhPcwR67gBzWIH8p3pyeDmtNYgWzGF3q8MneD/xbMwsfTQkw2H1qBk6uwRaVy3M8Nxw==
+vue-router@^4.0.0-rc.3:
+  version "4.0.0-rc.3"
+  resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.3.tgz#70d18e90030bc6a25e81a30401d673223998ec6b"
+  integrity sha512-NnPqWIfanEhJC4wu8BEFBmnEDIrx9ST0/HtmBiE+oV2MQlhyRk1TmdttWwVqx6Sh7kONsrI10GQV9l3YEkcWXg==
 
 vue-types@^3.0.0:
   version "3.0.1"