Commit 661db0c767772bb7a30da9d3eeaf2b47858ccf0b

Authored by vben
1 parent a161bfa8

perf(upload): improve upload component

CHANGELOG.zh_CN.md
@@ -3,6 +3,11 @@ @@ -3,6 +3,11 @@
3 ### ✨ Features 3 ### ✨ Features
4 4
5 - 新增 base64 文件流下载 5 - 新增 base64 文件流下载
  6 +- 优化上传组件及示例
  7 +
  8 +### 🎫 Chores
  9 +
  10 +- 更新 antdv 到`2.0.0-rc.1`
6 11
7 ## 2.0.0-rc.10 (2020-11-13) 12 ## 2.0.0-rc.10 (2020-11-13)
8 13
README.en-US.md
@@ -226,10 +226,10 @@ yarn clean:lib # Delete node_modules, supported window @@ -226,10 +226,10 @@ yarn clean:lib # Delete node_modules, supported window
226 - [x] Data import and export 226 - [x] Data import and export
227 - [x] Global error handling 227 - [x] Global error handling
228 - [x] Rich text component 228 - [x] Rich text component
  229 +- [x] Upload component
229 230
230 ## Developing features 231 ## Developing features
231 232
232 -- [ ] Upload component  
233 - [ ] Theme configuration 233 - [ ] Theme configuration
234 - [ ] Dark theme 234 - [ ] Dark theme
235 - [ ] Build CDN 235 - [ ] Build CDN
README.md
@@ -228,10 +228,10 @@ yarn clean:lib # 删除node_modules,兼容window系统 @@ -228,10 +228,10 @@ yarn clean:lib # 删除node_modules,兼容window系统
228 - [x] 系统性能优化 228 - [x] 系统性能优化
229 - [x] 全局错误处理 229 - [x] 全局错误处理
230 - [x] 富文本组件 230 - [x] 富文本组件
  231 +- [x] 上传组件
231 232
232 ## 正在开发的功能 233 ## 正在开发的功能
233 234
234 -- [ ] 上传组件  
235 - [ ] 主题配置 235 - [ ] 主题配置
236 - [ ] 黑暗主题 236 - [ ] 黑暗主题
237 - [ ] 打包 CDN 237 - [ ] 打包 CDN
package.json
@@ -22,8 +22,8 @@ @@ -22,8 +22,8 @@
22 }, 22 },
23 "dependencies": { 23 "dependencies": {
24 "@iconify/iconify": "^2.0.0-rc.2", 24 "@iconify/iconify": "^2.0.0-rc.2",
25 - "@vueuse/core": "^4.0.0-beta.40",  
26 - "ant-design-vue": "^2.0.0-beta.15", 25 + "@vueuse/core": "^4.0.0-beta.41",
  26 + "ant-design-vue": "^2.0.0-rc.1",
27 "apexcharts": "3.22.0", 27 "apexcharts": "3.22.0",
28 "axios": "^0.21.0", 28 "axios": "^0.21.0",
29 "echarts": "^4.9.0", 29 "echarts": "^4.9.0",
@@ -33,10 +33,10 @@ @@ -33,10 +33,10 @@
33 "nprogress": "^0.2.0", 33 "nprogress": "^0.2.0",
34 "path-to-regexp": "^6.2.0", 34 "path-to-regexp": "^6.2.0",
35 "qrcode": "^1.4.4", 35 "qrcode": "^1.4.4",
36 - "vditor": "^3.6.0", 36 + "vditor": "^3.6.2",
37 "vue": "^3.0.2", 37 "vue": "^3.0.2",
38 "vue-i18n": "^9.0.0-beta.6", 38 "vue-i18n": "^9.0.0-beta.6",
39 - "vue-router": "^4.0.0-rc.2", 39 + "vue-router": "^4.0.0-rc.3",
40 "vuex": "^4.0.0-rc.1", 40 "vuex": "^4.0.0-rc.1",
41 "vuex-module-decorators": "^1.0.1", 41 "vuex-module-decorators": "^1.0.1",
42 "xlsx": "^0.16.8", 42 "xlsx": "^0.16.8",
@@ -45,11 +45,11 @@ @@ -45,11 +45,11 @@
45 "devDependencies": { 45 "devDependencies": {
46 "@commitlint/cli": "^11.0.0", 46 "@commitlint/cli": "^11.0.0",
47 "@commitlint/config-conventional": "^11.0.0", 47 "@commitlint/config-conventional": "^11.0.0",
48 - "@iconify/json": "^1.1.254", 48 + "@iconify/json": "^1.1.258",
49 "@ls-lint/ls-lint": "^1.9.2", 49 "@ls-lint/ls-lint": "^1.9.2",
50 "@purge-icons/generated": "^0.4.1", 50 "@purge-icons/generated": "^0.4.1",
51 "@types/echarts": "^4.9.0", 51 "@types/echarts": "^4.9.0",
52 - "@types/fs-extra": "^9.0.2", 52 + "@types/fs-extra": "^9.0.4",
53 "@types/koa-static": "^4.0.1", 53 "@types/koa-static": "^4.0.1",
54 "@types/lodash-es": "^4.17.3", 54 "@types/lodash-es": "^4.17.3",
55 "@types/mockjs": "^1.0.3", 55 "@types/mockjs": "^1.0.3",
src/api/demo/model/uploadModel.ts renamed to src/api/sys/model/uploadModel.ts
src/api/demo/upload.ts renamed to src/api/sys/upload.ts
src/components/Drawer/src/props.ts
@@ -24,7 +24,7 @@ export const footerProps = { @@ -24,7 +24,7 @@ export const footerProps = {
24 okButtonProps: Object as PropType<any>, 24 okButtonProps: Object as PropType<any>,
25 okText: { 25 okText: {
26 type: String as PropType<string>, 26 type: String as PropType<string>,
27 - default: '保存', 27 + default: '确认',
28 }, 28 },
29 okType: { 29 okType: {
30 type: String as PropType<string>, 30 type: String as PropType<string>,
src/components/Form/src/BasicForm.vue
@@ -44,7 +44,6 @@ @@ -44,7 +44,6 @@
44 import { useFormValues } from './hooks/useFormValues'; 44 import { useFormValues } from './hooks/useFormValues';
45 import useAdvanced from './hooks/useAdvanced'; 45 import useAdvanced from './hooks/useAdvanced';
46 import { useFormAction } from './hooks/useFormAction'; 46 import { useFormAction } from './hooks/useFormAction';
47 -  
48 export default defineComponent({ 47 export default defineComponent({
49 name: 'BasicForm', 48 name: 'BasicForm',
50 components: { FormItem, Form, Row, FormAction }, 49 components: { FormItem, Form, Row, FormAction },
src/components/Icon/index.tsx
@@ -18,7 +18,7 @@ export default defineComponent({ @@ -18,7 +18,7 @@ export default defineComponent({
18 // icon size 18 // icon size
19 size: { 19 size: {
20 type: [String, Number] as PropType<string | number>, 20 type: [String, Number] as PropType<string | number>,
21 - default: 14, 21 + default: 16,
22 }, 22 },
23 prefix: { 23 prefix: {
24 type: String as PropType<string>, 24 type: String as PropType<string>,
src/components/Modal/src/props.ts
1 import type { PropType } from 'vue'; 1 import type { PropType } from 'vue';
  2 +import { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
2 export const modalProps = { 3 export const modalProps = {
3 visible: Boolean as PropType<boolean>, 4 visible: Boolean as PropType<boolean>,
4 // open drag 5 // open drag
@@ -16,7 +17,7 @@ export const modalProps = { @@ -16,7 +17,7 @@ export const modalProps = {
16 }, 17 },
17 okText: { 18 okText: {
18 type: String as PropType<string>, 19 type: String as PropType<string>,
19 - default: '保存', 20 + default: '确认',
20 }, 21 },
21 closeFunc: Function as PropType<() => Promise<boolean>>, 22 closeFunc: Function as PropType<() => Promise<boolean>>,
22 }; 23 };
@@ -100,9 +101,9 @@ export const basicProps = Object.assign({}, modalProps, { @@ -100,9 +101,9 @@ export const basicProps = Object.assign({}, modalProps, {
100 default: 'primary', 101 default: 'primary',
101 }, 102 },
102 103
103 - okButtonProps: Object as PropType<any>, 104 + okButtonProps: Object as PropType<ButtonProps>,
104 105
105 - cancelButtonProps: Object as PropType<any>, 106 + cancelButtonProps: Object as PropType<ButtonProps>,
106 107
107 title: { 108 title: {
108 type: String as PropType<string>, 109 type: String as PropType<string>,
src/components/Qrcode/src/index.vue
@@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
7 import { defineComponent, watchEffect, PropType, ref, unref } from 'vue'; 7 import { defineComponent, watchEffect, PropType, ref, unref } from 'vue';
8 import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus'; 8 import { toCanvas, QRCodeRenderersOptions, LogoType } from './qrcodePlus';
9 import { toDataURL } from 'qrcode'; 9 import { toDataURL } from 'qrcode';
10 - import { downloadByUrl } from '/@/utils/file/FileDownload'; 10 + import { downloadByUrl } from '/@/utils/file/download';
11 11
12 export default defineComponent({ 12 export default defineComponent({
13 name: 'QrCode', 13 name: 'QrCode',
src/components/Table/src/BasicTable.vue
@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 class="basic-table" 4 class="basic-table"
5 :class="{ 5 :class="{
6 'table-form-container': getBindValues.useSearchForm, 6 'table-form-container': getBindValues.useSearchForm,
  7 + inset: getBindValues.inset,
7 }" 8 }"
8 > 9 >
9 <BasicForm 10 <BasicForm
src/components/Table/src/hooks/useDataSource.ts
@@ -84,7 +84,7 @@ export function useDataSource( @@ -84,7 +84,7 @@ export function useDataSource(
84 const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref( 84 const { api, searchInfo, fetchSetting, beforeFetch, afterFetch, useSearchForm } = unref(
85 propsRef 85 propsRef
86 ); 86 );
87 - if (!api && !isFunction(api)) return; 87 + if (!api || !isFunction(api)) return;
88 try { 88 try {
89 loadingRef.value = true; 89 loadingRef.value = true;
90 const { pageField, sizeField, listField, totalField } = fetchSetting || FETCH_SETTING; 90 const { pageField, sizeField, listField, totalField } = fetchSetting || FETCH_SETTING;
src/components/Table/src/props.ts
@@ -16,7 +16,10 @@ export const basicProps = { @@ -16,7 +16,10 @@ export const basicProps = {
16 tableSetting: { 16 tableSetting: {
17 type: Object as PropType<TableSetting>, 17 type: Object as PropType<TableSetting>,
18 }, 18 },
19 - 19 + inset: {
  20 + type: Boolean as PropType<boolean>,
  21 + default: false,
  22 + },
20 sortFn: { 23 sortFn: {
21 type: Function as PropType<(sortInfo: SorterResult) => any>, 24 type: Function as PropType<(sortInfo: SorterResult) => any>,
22 default: DEFAULT_SORT_FN, 25 default: DEFAULT_SORT_FN,
src/components/Table/src/style/index.less
@@ -49,6 +49,12 @@ @@ -49,6 +49,12 @@
49 } 49 }
50 } 50 }
51 51
  52 + &.inset {
  53 + .ant-table-wrapper {
  54 + padding: 0;
  55 + }
  56 + }
  57 +
52 // 58 //
53 .ant-table { 59 .ant-table {
54 border: none; 60 border: none;
src/components/Table/src/types/table.ts
@@ -126,6 +126,8 @@ export interface TableSetting { @@ -126,6 +126,8 @@ export interface TableSetting {
126 export interface BasicTableProps<T = any> { 126 export interface BasicTableProps<T = any> {
127 // 自定义排序方法 127 // 自定义排序方法
128 sortFn?: (sortInfo: SorterResult) => any; 128 sortFn?: (sortInfo: SorterResult) => any;
  129 + // 取消表格的默认padding
  130 + inset?: boolean;
129 // 显示表格设置 131 // 显示表格设置
130 showTableSetting?: boolean; 132 showTableSetting?: boolean;
131 tableSetting?: TableSetting; 133 tableSetting?: TableSetting;
src/components/Upload/index.ts
1 -export { default as UploadContainer } from './src/UploadContainer.vue'; 1 +export { default as BasicUpload } from './src/BasicUpload.vue';
2 // export * from './src/types'; 2 // export * from './src/types';
src/components/Upload/src/UploadContainer.vue renamed to src/components/Upload/src/BasicUpload.vue
1 <template> 1 <template>
2 <div> 2 <div>
3 <a-button-group> 3 <a-button-group>
4 - <a-button type="primary" @click="openUploadModal">上传</a-button>  
5 - <a-button @click="openPreviewModal">  
6 - <Icon icon="ant-design:eye-outlined" /> 4 + <a-button type="primary" @click="openUploadModal" preIcon="ant-design:cloud-upload-outlined">
  5 + 上传
7 </a-button> 6 </a-button>
  7 + <Tooltip placement="bottom" v-if="showPreview">
  8 + <template #title>
  9 + 已上传
  10 + <template v-if="fileListRef.length">{{ fileListRef.length }}</template>
  11 + </template>
  12 + <a-button @click="openPreviewModal">
  13 + <Icon icon="ant-design:eye-outlined" />
  14 + <template v-if="fileListRef.length && showPreviewNumber">
  15 + {{ fileListRef.length }}
  16 + </template>
  17 + </a-button>
  18 + </Tooltip>
8 </a-button-group> 19 </a-button-group>
9 - <UploadModal v-bind="$props" @register="registerUploadModal" @change="handleChange" /> 20 +
  21 + <UploadModal v-bind="bindValue" @register="registerUploadModal" @change="handleChange" />
  22 +
10 <UploadPreviewModal 23 <UploadPreviewModal
11 :value="fileListRef" 24 :value="fileListRef"
12 @register="registerPreviewModal" 25 @register="registerPreviewModal"
13 - @change="handlePreviewChange" 26 + @list-change="handlePreviewChange"
14 /> 27 />
15 </div> 28 </div>
16 </template> 29 </template>
17 <script lang="ts"> 30 <script lang="ts">
18 - import { defineComponent, ref, watch, unref } from 'vue';  
19 - import { useModal } from '/@/components/Modal'; 31 + import { defineComponent, ref, watch, unref, computed } from 'vue';
  32 +
20 import UploadModal from './UploadModal.vue'; 33 import UploadModal from './UploadModal.vue';
21 - import { uploadContainerProps } from './props';  
22 import UploadPreviewModal from './UploadPreviewModal.vue'; 34 import UploadPreviewModal from './UploadPreviewModal.vue';
23 - import Icon from '/@/components/Icon/index'; 35 + import Icon from '/@/components/Icon';
  36 + import { Tooltip } from 'ant-design-vue';
  37 +
  38 + import { useModal } from '/@/components/Modal';
  39 +
  40 + import { uploadContainerProps } from './props';
  41 + import { omit } from 'lodash-es';
  42 +
24 export default defineComponent({ 43 export default defineComponent({
25 - components: { UploadModal, UploadPreviewModal, Icon }, 44 + components: { UploadModal, UploadPreviewModal, Icon, Tooltip },
26 props: uploadContainerProps, 45 props: uploadContainerProps,
27 - setup(props, { emit }) { 46 + setup(props, { emit, attrs }) {
28 // 上传modal 47 // 上传modal
29 const [registerUploadModal, { openModal: openUploadModal }] = useModal(); 48 const [registerUploadModal, { openModal: openUploadModal }] = useModal();
  49 +
30 // 预览modal 50 // 预览modal
31 const [registerPreviewModal, { openModal: openPreviewModal }] = useModal(); 51 const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
32 52
33 const fileListRef = ref<string[]>([]); 53 const fileListRef = ref<string[]>([]);
  54 +
  55 + const showPreview = computed(() => {
  56 + const { emptyHidePreview } = props;
  57 + if (!emptyHidePreview) return true;
  58 + return emptyHidePreview ? fileListRef.value.length > 0 : true;
  59 + });
  60 +
  61 + const bindValue = computed(() => {
  62 + const value = { ...attrs, ...props };
  63 + return omit(value, 'onChange');
  64 + });
  65 +
34 watch( 66 watch(
35 () => props.value, 67 () => props.value,
36 - (value) => {  
37 - fileListRef.value = [...(value || [])]; 68 + (value = []) => {
  69 + fileListRef.value = value;
38 }, 70 },
39 { immediate: true } 71 { immediate: true }
40 ); 72 );
  73 +
41 // 上传modal保存操作 74 // 上传modal保存操作
42 function handleChange(urls: string[]) { 75 function handleChange(urls: string[]) {
43 fileListRef.value = [...unref(fileListRef), ...(urls || [])]; 76 fileListRef.value = [...unref(fileListRef), ...(urls || [])];
44 emit('change', fileListRef.value); 77 emit('change', fileListRef.value);
45 } 78 }
  79 +
46 // 预览modal保存操作 80 // 预览modal保存操作
47 function handlePreviewChange(urls: string[]) { 81 function handlePreviewChange(urls: string[]) {
48 fileListRef.value = [...(urls || [])]; 82 fileListRef.value = [...(urls || [])];
49 emit('change', fileListRef.value); 83 emit('change', fileListRef.value);
50 } 84 }
  85 +
51 return { 86 return {
52 registerUploadModal, 87 registerUploadModal,
53 openUploadModal, 88 openUploadModal,
@@ -56,6 +91,8 @@ @@ -56,6 +91,8 @@
56 registerPreviewModal, 91 registerPreviewModal,
57 openPreviewModal, 92 openPreviewModal,
58 fileListRef, 93 fileListRef,
  94 + showPreview,
  95 + bindValue,
59 }; 96 };
60 }, 97 },
61 }); 98 });
src/components/Upload/src/ThumnUrl.vue
@@ -5,25 +5,22 @@ @@ -5,25 +5,22 @@
5 </span> 5 </span>
6 </template> 6 </template>
7 <script lang="ts"> 7 <script lang="ts">
8 - import { defineComponent } from 'vue'; 8 + import { defineComponent, PropType } from 'vue';
9 9
10 export default defineComponent({ 10 export default defineComponent({
11 props: { 11 props: {
12 fileUrl: { 12 fileUrl: {
13 - type: String, 13 + type: String as PropType<string>,
14 default: '', 14 default: '',
15 }, 15 },
16 fileType: { 16 fileType: {
17 - type: String, 17 + type: String as PropType<string>,
18 default: '', 18 default: '',
19 }, 19 },
20 fileName: { 20 fileName: {
21 - type: String, 21 + type: String as PropType<string>,
22 default: '', 22 default: '',
23 }, 23 },
24 }, 24 },
25 - setup() {  
26 - return {};  
27 - },  
28 }); 25 });
29 </script> 26 </script>
src/components/Upload/src/UploadModal.vue
1 <template> 1 <template>
2 <BasicModal 2 <BasicModal
  3 + width="800px"
  4 + title="上传"
  5 + okText="保存"
3 v-bind="$attrs" 6 v-bind="$attrs"
4 @register="register" 7 @register="register"
5 @ok="handleOk" 8 @ok="handleOk"
6 :closeFunc="handleCloseFunc" 9 :closeFunc="handleCloseFunc"
7 :maskClosable="false" 10 :maskClosable="false"
8 - width="800px"  
9 - title="上传组件" 11 + :keyboard="false"
10 wrapClassName="upload-modal" 12 wrapClassName="upload-modal"
11 - :okButtonProps="{ disabled: isUploadingRef }" 13 + :okButtonProps="getOkButtonProps"
12 :cancelButtonProps="{ disabled: isUploadingRef }" 14 :cancelButtonProps="{ disabled: isUploadingRef }"
13 > 15 >
14 <template #centerdFooter> 16 <template #centerdFooter>
15 - <a-button @click="handleStartUpload" color="success" :loading="isUploadingRef">  
16 - {{ isUploadingRef ? '上传中' : '开始上传' }} 17 + <a-button
  18 + @click="handleStartUpload"
  19 + color="success"
  20 + :disabled="!getIsSelectFile"
  21 + :loading="isUploadingRef"
  22 + >
  23 + {{ getUploadBtnText }}
17 </a-button> 24 </a-button>
18 </template> 25 </template>
19 - <Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">  
20 - <a-button type="primary"> 选择文件 </a-button>  
21 - <span class="px-2">{{ getHelpText }}</span>  
22 - </Upload>  
23 - <BasicTable @register="registerTable" :dataSource="fileListRef" /> 26 +
  27 + <BasicTable @register="registerTable" :dataSource="fileListRef">
  28 + <template #toolbar>
  29 + <Upload :accept="getStringAccept" :multiple="multiple" :before-upload="beforeUpload">
  30 + <a-button type="primary"> 选择文件 </a-button>
  31 + </Upload>
  32 + </template>
  33 + <template #tableTitle>
  34 + <Alert :message="getHelpText" type="info" banner></Alert>
  35 + </template>
  36 + </BasicTable>
24 </BasicModal> 37 </BasicModal>
25 </template> 38 </template>
26 <script lang="ts"> 39 <script lang="ts">
27 - import { defineComponent, reactive, ref, toRef, unref } from 'vue';  
28 - import { Upload } from 'ant-design-vue'; 40 + import { defineComponent, reactive, ref, toRefs, unref, computed } from 'vue';
  41 + import { Upload, Alert } from 'ant-design-vue';
29 import { BasicModal, useModalInner } from '/@/components/Modal'; 42 import { BasicModal, useModalInner } from '/@/components/Modal';
30 import { BasicTable, useTable } from '/@/components/Table'; 43 import { BasicTable, useTable } from '/@/components/Table';
31 // hooks 44 // hooks
@@ -39,23 +52,56 @@ @@ -39,23 +52,56 @@
39 import { checkFileType, checkImgType, getBase64WithFile } from './utils'; 52 import { checkFileType, checkImgType, getBase64WithFile } from './utils';
40 import { buildUUID } from '/@/utils/uuid'; 53 import { buildUUID } from '/@/utils/uuid';
41 import { createImgPreview } from '/@/components/Preview/index'; 54 import { createImgPreview } from '/@/components/Preview/index';
42 - import { uploadApi } from '/@/api/demo/upload'; 55 + import { uploadApi } from '/@/api/sys/upload';
  56 + import { isFunction } from '/@/utils/is';
  57 + import { warn } from '/@/utils/log';
43 58
44 export default defineComponent({ 59 export default defineComponent({
45 - components: { BasicModal, Upload, BasicTable }, 60 + components: { BasicModal, Upload, BasicTable, Alert },
46 props: basicProps, 61 props: basicProps,
47 setup(props, { emit }) { 62 setup(props, { emit }) {
  63 + // 是否正在上传
  64 + const isUploadingRef = ref(false);
  65 + const fileListRef = ref<FileItem[]>([]);
  66 + const state = reactive<{ fileList: FileItem[] }>({
  67 + fileList: [],
  68 + });
  69 +
48 const [register, { closeModal }] = useModalInner(); 70 const [register, { closeModal }] = useModalInner();
  71 +
  72 + const { accept, helpText, maxNumber, maxSize } = toRefs(props);
49 const { getAccept, getStringAccept, getHelpText } = useUploadType({ 73 const { getAccept, getStringAccept, getHelpText } = useUploadType({
50 - acceptRef: toRef(props, 'accept'),  
51 - helpTextRef: toRef(props, 'helpText'),  
52 - maxNumberRef: toRef(props, 'maxNumber'),  
53 - maxSizeRef: toRef(props, 'maxSize'), 74 + acceptRef: accept,
  75 + helpTextRef: helpText,
  76 + maxNumberRef: maxNumber,
  77 + maxSizeRef: maxSize,
54 }); 78 });
55 79
56 - const fileListRef = ref<FileItem[]>([]);  
57 - const state = reactive<{ fileList: FileItem[] }>({ fileList: [] });  
58 const { createMessage } = useMessage(); 80 const { createMessage } = useMessage();
  81 +
  82 + const getIsSelectFile = computed(() => {
  83 + return (
  84 + fileListRef.value.length > 0 &&
  85 + !fileListRef.value.every((item) => item.status === UploadResultStatus.SUCCESS)
  86 + );
  87 + });
  88 +
  89 + const getOkButtonProps = computed(() => {
  90 + const someSuccess = fileListRef.value.some(
  91 + (item) => item.status === UploadResultStatus.SUCCESS
  92 + );
  93 + return {
  94 + disabled: isUploadingRef.value || fileListRef.value.length === 0 || !someSuccess,
  95 + };
  96 + });
  97 +
  98 + const getUploadBtnText = computed(() => {
  99 + const someError = fileListRef.value.some(
  100 + (item) => item.status === UploadResultStatus.ERROR
  101 + );
  102 + return isUploadingRef.value ? '上传中' : someError ? '重新上传失败文件' : '开始上传';
  103 + });
  104 +
59 // 上传前校验 105 // 上传前校验
60 function beforeUpload(file: File) { 106 function beforeUpload(file: File) {
61 const { size, name } = file; 107 const { size, name } = file;
@@ -73,6 +119,14 @@ @@ -73,6 +119,14 @@
73 createMessage.error!(`只能上传${accept.join(',')}格式文件`); 119 createMessage.error!(`只能上传${accept.join(',')}格式文件`);
74 return false; 120 return false;
75 } 121 }
  122 + const commonItem = {
  123 + uuid: buildUUID(),
  124 + file,
  125 + size,
  126 + name,
  127 + percent: 0,
  128 + type: name.split('.').pop(),
  129 + };
76 // 生成图片缩略图 130 // 生成图片缩略图
77 if (checkImgType(file)) { 131 if (checkImgType(file)) {
78 // beforeUpload,如果异步会调用自带上传方法 132 // beforeUpload,如果异步会调用自带上传方法
@@ -81,29 +135,13 @@ @@ -81,29 +135,13 @@
81 fileListRef.value = [ 135 fileListRef.value = [
82 ...unref(fileListRef), 136 ...unref(fileListRef),
83 { 137 {
84 - uuid: buildUUID(),  
85 - file,  
86 thumbUrl, 138 thumbUrl,
87 - size,  
88 - name,  
89 - percent: 0,  
90 - type: name.split('.').pop(), 139 + ...commonItem,
91 }, 140 },
92 ]; 141 ];
93 }); 142 });
94 } else { 143 } else {
95 - fileListRef.value = [  
96 - ...unref(fileListRef),  
97 - {  
98 - uuid: buildUUID(),  
99 -  
100 - file,  
101 - size,  
102 - name,  
103 - percent: 0,  
104 - type: name.split('.').pop(),  
105 - },  
106 - ]; 144 + fileListRef.value = [...unref(fileListRef), commonItem];
107 } 145 }
108 return false; 146 return false;
109 } 147 }
@@ -112,6 +150,7 @@ @@ -112,6 +150,7 @@
112 const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid); 150 const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid);
113 index !== -1 && fileListRef.value.splice(index, 1); 151 index !== -1 && fileListRef.value.splice(index, 1);
114 } 152 }
  153 +
115 // 预览 154 // 预览
116 function handlePreview(record: FileItem) { 155 function handlePreview(record: FileItem) {
117 const { thumbUrl = '' } = record; 156 const { thumbUrl = '' } = record;
@@ -119,19 +158,18 @@ @@ -119,19 +158,18 @@
119 imageList: [thumbUrl], 158 imageList: [thumbUrl],
120 }); 159 });
121 } 160 }
122 - const [registerTable] = useTable({  
123 - columns: createTableColumns(),  
124 - actionColumn: createActionColumn(handleRemove, handlePreview),  
125 - pagination: false,  
126 - });  
127 - // 是否正在上传  
128 - const isUploadingRef = ref(false); 161 +
129 async function uploadApiByItem(item: FileItem) { 162 async function uploadApiByItem(item: FileItem) {
  163 + const { api } = props;
  164 + if (!api || !isFunction(api)) {
  165 + return warn('upload api must exist and be a function');
  166 + }
130 try { 167 try {
131 item.status = UploadResultStatus.UPLOADING; 168 item.status = UploadResultStatus.UPLOADING;
132 169
133 const { data } = await uploadApi( 170 const { data } = await uploadApi(
134 { 171 {
  172 + ...(props.uploadParams || {}),
135 file: item.file, 173 file: item.file,
136 }, 174 },
137 function onUploadProgress(progressEvent: ProgressEvent) { 175 function onUploadProgress(progressEvent: ProgressEvent) {
@@ -154,32 +192,42 @@ @@ -154,32 +192,42 @@
154 }; 192 };
155 } 193 }
156 } 194 }
  195 +
157 // 点击开始上传 196 // 点击开始上传
158 async function handleStartUpload() { 197 async function handleStartUpload() {
  198 + const { maxNumber } = props;
  199 + if (fileListRef.value.length > maxNumber) {
  200 + return createMessage.warning(`最多只能上传${maxNumber}个文件`);
  201 + }
159 try { 202 try {
160 isUploadingRef.value = true; 203 isUploadingRef.value = true;
  204 + // 只上传不是成功状态的
  205 + const uploadFileList =
  206 + fileListRef.value.filter((item) => item.status !== UploadResultStatus.SUCCESS) || [];
161 const data = await Promise.all( 207 const data = await Promise.all(
162 - unref(fileListRef).map((item) => { 208 + uploadFileList.map((item) => {
163 return uploadApiByItem(item); 209 return uploadApiByItem(item);
164 }) 210 })
165 ); 211 );
166 isUploadingRef.value = false; 212 isUploadingRef.value = false;
167 // 生产环境:抛出错误 213 // 生产环境:抛出错误
168 - const errorList = data.filter((item) => !item.success);  
169 - if (errorList.length > 0) {  
170 - throw errorList;  
171 - } 214 + const errorList = data.filter((item: any) => !item.success);
  215 + if (errorList.length > 0) throw errorList;
172 } catch (e) { 216 } catch (e) {
173 isUploadingRef.value = false; 217 isUploadingRef.value = false;
174 throw e; 218 throw e;
175 } 219 }
176 } 220 }
  221 +
177 // 点击保存 222 // 点击保存
178 function handleOk() { 223 function handleOk() {
179 - // TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }} 224 + const { maxNumber } = props;
  225 +
  226 + if (fileListRef.value.length > maxNumber) {
  227 + return createMessage.warning(`最多只能上传${maxNumber}个文件`);
  228 + }
180 if (isUploadingRef.value) { 229 if (isUploadingRef.value) {
181 - createMessage.warning('请等待文件上传后,保存');  
182 - return; 230 + return createMessage.warning('请等待文件上传后,保存');
183 } 231 }
184 const fileList: string[] = []; 232 const fileList: string[] = [];
185 233
@@ -189,18 +237,15 @@ @@ -189,18 +237,15 @@
189 fileList.push(responseData.url); 237 fileList.push(responseData.url);
190 } 238 }
191 } 239 }
192 -  
193 // 存在一个上传成功的即可保存 240 // 存在一个上传成功的即可保存
194 -  
195 if (fileList.length <= 0) { 241 if (fileList.length <= 0) {
196 - createMessage.warning('没有上传成功的文件,无法保存');  
197 - return; 242 + return createMessage.warning('没有上传成功的文件,无法保存');
198 } 243 }
199 - console.log(fileList);  
200 - emit('change', fileList);  
201 fileListRef.value = []; 244 fileListRef.value = [];
202 closeModal(); 245 closeModal();
  246 + emit('change', fileList);
203 } 247 }
  248 +
204 // 点击关闭:则所有操作不保存,包括上传的 249 // 点击关闭:则所有操作不保存,包括上传的
205 function handleCloseFunc() { 250 function handleCloseFunc() {
206 if (!isUploadingRef.value) { 251 if (!isUploadingRef.value) {
@@ -211,11 +256,22 @@ @@ -211,11 +256,22 @@
211 return false; 256 return false;
212 } 257 }
213 } 258 }
  259 +
  260 + const [registerTable] = useTable({
  261 + columns: createTableColumns(),
  262 + actionColumn: createActionColumn(handleRemove, handlePreview),
  263 + pagination: false,
  264 + inset: true,
  265 + scroll: {
  266 + y: 3000,
  267 + },
  268 + });
214 return { 269 return {
215 register, 270 register,
216 closeModal, 271 closeModal,
217 getHelpText, 272 getHelpText,
218 getStringAccept, 273 getStringAccept,
  274 + getOkButtonProps,
219 beforeUpload, 275 beforeUpload,
220 registerTable, 276 registerTable,
221 fileListRef, 277 fileListRef,
@@ -224,14 +280,13 @@ @@ -224,14 +280,13 @@
224 handleStartUpload, 280 handleStartUpload,
225 handleOk, 281 handleOk,
226 handleCloseFunc, 282 handleCloseFunc,
  283 + getIsSelectFile,
  284 + getUploadBtnText,
227 }; 285 };
228 }, 286 },
229 }); 287 });
230 </script> 288 </script>
231 <style lang="less"> 289 <style lang="less">
232 - // /deep/ .ant-upload-list {  
233 - // display: none;  
234 - // }  
235 .upload-modal { 290 .upload-modal {
236 .ant-upload-list { 291 .ant-upload-list {
237 display: none; 292 display: none;
src/components/Upload/src/UploadPreviewModal.vue
1 <template> 1 <template>
2 <BasicModal 2 <BasicModal
  3 + width="800px"
  4 + title="预览"
3 wrapClassName="upload-preview-modal" 5 wrapClassName="upload-preview-modal"
4 v-bind="$attrs" 6 v-bind="$attrs"
5 - width="800px"  
6 @register="register" 7 @register="register"
7 - title="预览"  
8 :showOkBtn="false" 8 :showOkBtn="false"
9 > 9 >
10 <BasicTable @register="registerTable" :dataSource="fileListRef" /> 10 <BasicTable @register="registerTable" :dataSource="fileListRef" />
@@ -12,17 +12,18 @@ @@ -12,17 +12,18 @@
12 </template> 12 </template>
13 <script lang="ts"> 13 <script lang="ts">
14 import { defineComponent, watch, ref, unref } from 'vue'; 14 import { defineComponent, watch, ref, unref } from 'vue';
  15 +
15 import { BasicTable, useTable } from '/@/components/Table'; 16 import { BasicTable, useTable } from '/@/components/Table';
16 - import { createPreviewColumns, createPreviewActionColumn } from './data';  
17 import { BasicModal, useModalInner } from '/@/components/Modal'; 17 import { BasicModal, useModalInner } from '/@/components/Modal';
18 - import { priviewProps } from './props'; 18 + import { previewProps } from './props';
19 import { PreviewFileItem } from './types'; 19 import { PreviewFileItem } from './types';
20 import { createImgPreview } from '/@/components/Preview/index'; 20 import { createImgPreview } from '/@/components/Preview/index';
21 - import { downloadByUrl } from '/@/utils/file/FileDownload'; 21 + import { downloadByUrl } from '/@/utils/file/download';
22 22
  23 + import { createPreviewColumns, createPreviewActionColumn } from './data';
23 export default defineComponent({ 24 export default defineComponent({
24 components: { BasicModal, BasicTable }, 25 components: { BasicModal, BasicTable },
25 - props: priviewProps, 26 + props: previewProps,
26 setup(props, { emit }) { 27 setup(props, { emit }) {
27 const [register, { closeModal }] = useModalInner(); 28 const [register, { closeModal }] = useModalInner();
28 const fileListRef = ref<PreviewFileItem[]>([]); 29 const fileListRef = ref<PreviewFileItem[]>([]);
@@ -43,17 +44,19 @@ @@ -43,17 +44,19 @@
43 }, 44 },
44 { immediate: true } 45 { immediate: true }
45 ); 46 );
  47 +
46 // 删除 48 // 删除
47 function handleRemove(record: PreviewFileItem) { 49 function handleRemove(record: PreviewFileItem) {
48 const index = fileListRef.value.findIndex((item) => item.url === record.url); 50 const index = fileListRef.value.findIndex((item) => item.url === record.url);
49 if (index !== -1) { 51 if (index !== -1) {
50 fileListRef.value.splice(index, 1); 52 fileListRef.value.splice(index, 1);
51 emit( 53 emit(
52 - 'change', 54 + 'list-change',
53 fileListRef.value.map((item) => item.url) 55 fileListRef.value.map((item) => item.url)
54 ); 56 );
55 } 57 }
56 } 58 }
  59 +
57 // 预览 60 // 预览
58 function handlePreview(record: PreviewFileItem) { 61 function handlePreview(record: PreviewFileItem) {
59 const { url = '' } = record; 62 const { url = '' } = record;
@@ -61,16 +64,19 @@ @@ -61,16 +64,19 @@
61 imageList: [url], 64 imageList: [url],
62 }); 65 });
63 } 66 }
  67 +
64 // 下载 68 // 下载
65 function handleDownload(record: PreviewFileItem) { 69 function handleDownload(record: PreviewFileItem) {
66 const { url = '' } = record; 70 const { url = '' } = record;
67 downloadByUrl({ url }); 71 downloadByUrl({ url });
68 } 72 }
  73 +
69 const [registerTable] = useTable({ 74 const [registerTable] = useTable({
70 columns: createPreviewColumns(), 75 columns: createPreviewColumns(),
71 pagination: false, 76 pagination: false,
72 actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }), 77 actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }),
73 }); 78 });
  79 +
74 return { 80 return {
75 register, 81 register,
76 closeModal, 82 closeModal,
src/components/Upload/src/data.tsx
1 -// import { BasicColumn, TableAction, ActionItem } from '@/components/table';  
2 import { checkImgType, isImgTypeByName } from './utils'; 1 import { checkImgType, isImgTypeByName } from './utils';
3 -// import ThumnUrl from './ThumbUrl.vue';  
4 -import { Progress } from 'ant-design-vue'; 2 +import { Progress, Tag } from 'ant-design-vue';
5 import { FileItem, PreviewFileItem, UploadResultStatus } from './types'; 3 import { FileItem, PreviewFileItem, UploadResultStatus } from './types';
6 -// import { ElecArchivesSaveResult } from '@/api/biz/file/model/fileModel';  
7 -// import { quryFile } from '@/api/biz/file/file';  
8 import { BasicColumn, ActionItem, TableAction } from '/@/components/Table/index'; 4 import { BasicColumn, ActionItem, TableAction } from '/@/components/Table/index';
9 5
10 // 文件上传列表 6 // 文件上传列表
@@ -16,8 +12,7 @@ export function createTableColumns(): BasicColumn[] { @@ -16,8 +12,7 @@ export function createTableColumns(): BasicColumn[] {
16 width: 100, 12 width: 100,
17 customRender: ({ record }) => { 13 customRender: ({ record }) => {
18 const { thumbUrl, type } = (record as FileItem) || {}; 14 const { thumbUrl, type } = (record as FileItem) || {};
19 - return <span>{thumbUrl ? <img src={thumbUrl} style={{ width: '50px' }} /> : type}</span>;  
20 - // return <ThumnUrl fileUrl={thumbUrl} fileType={type} fileName={type} />; 15 + return <span>{thumbUrl ? <img style={{ maxWidth: '60px' }} src={thumbUrl} /> : type}</span>;
21 }, 16 },
22 }, 17 },
23 { 18 {
@@ -26,7 +21,7 @@ export function createTableColumns(): BasicColumn[] { @@ -26,7 +21,7 @@ export function createTableColumns(): BasicColumn[] {
26 align: 'left', 21 align: 'left',
27 customRender: ({ text, record }) => { 22 customRender: ({ text, record }) => {
28 const { percent, status: uploadStatus } = (record as FileItem) || {}; 23 const { percent, status: uploadStatus } = (record as FileItem) || {};
29 - let status = 'normal'; 24 + let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
30 if (uploadStatus === UploadResultStatus.ERROR) { 25 if (uploadStatus === UploadResultStatus.ERROR) {
31 status = 'exception'; 26 status = 'exception';
32 } else if (uploadStatus === UploadResultStatus.UPLOADING) { 27 } else if (uploadStatus === UploadResultStatus.UPLOADING) {
@@ -63,11 +58,11 @@ export function createTableColumns(): BasicColumn[] { @@ -63,11 +58,11 @@ export function createTableColumns(): BasicColumn[] {
63 width: 100, 58 width: 100,
64 customRender: ({ text }) => { 59 customRender: ({ text }) => {
65 if (text === UploadResultStatus.SUCCESS) { 60 if (text === UploadResultStatus.SUCCESS) {
66 - return '上传成功'; 61 + return <Tag color="green">{() => '上传成功'}</Tag>;
67 } else if (text === UploadResultStatus.ERROR) { 62 } else if (text === UploadResultStatus.ERROR) {
68 - return '上传失败'; 63 + return <Tag color="red">{() => '上传失败'}</Tag>;
69 } else if (text === UploadResultStatus.UPLOADING) { 64 } else if (text === UploadResultStatus.UPLOADING) {
70 - return '上传中'; 65 + return <Tag color="blue">{() => '上传中'}</Tag>;
71 } 66 }
72 67
73 return text; 68 return text;
@@ -85,6 +80,7 @@ export function createActionColumn(handleRemove: Function, handlePreview: Functi @@ -85,6 +80,7 @@ export function createActionColumn(handleRemove: Function, handlePreview: Functi
85 const actions: ActionItem[] = [ 80 const actions: ActionItem[] = [
86 { 81 {
87 label: '删除', 82 label: '删除',
  83 + color: 'error',
88 onClick: handleRemove.bind(null, record), 84 onClick: handleRemove.bind(null, record),
89 }, 85 },
90 ]; 86 ];
@@ -125,9 +121,9 @@ export function createPreviewActionColumn({ @@ -125,9 +121,9 @@ export function createPreviewActionColumn({
125 handlePreview, 121 handlePreview,
126 handleDownload, 122 handleDownload,
127 }: { 123 }: {
128 - handleRemove: Function;  
129 - handlePreview: Function;  
130 - handleDownload: Function; 124 + handleRemove: Fn;
  125 + handlePreview: Fn;
  126 + handleDownload: Fn;
131 }): BasicColumn { 127 }): BasicColumn {
132 return { 128 return {
133 width: 160, 129 width: 160,
@@ -135,11 +131,12 @@ export function createPreviewActionColumn({ @@ -135,11 +131,12 @@ export function createPreviewActionColumn({
135 dataIndex: 'action', 131 dataIndex: 'action',
136 fixed: false, 132 fixed: false,
137 customRender: ({ record }) => { 133 customRender: ({ record }) => {
138 - const { url } = (record as PreviewFileItem) || {}; 134 + const { url } = (record || {}) as PreviewFileItem;
139 135
140 const actions: ActionItem[] = [ 136 const actions: ActionItem[] = [
141 { 137 {
142 label: '删除', 138 label: '删除',
  139 + color: 'error',
143 onClick: handleRemove.bind(null, record), 140 onClick: handleRemove.bind(null, record),
144 }, 141 },
145 { 142 {
src/components/Upload/src/props.ts
@@ -10,10 +10,10 @@ export const basicProps = { @@ -10,10 +10,10 @@ export const basicProps = {
10 type: Number as PropType<number>, 10 type: Number as PropType<number>,
11 default: 2, 11 default: 2,
12 }, 12 },
13 - // 最大数量的文件,0不限制 13 + // 最大数量的文件,Infinity不限制
14 maxNumber: { 14 maxNumber: {
15 type: Number as PropType<number>, 15 type: Number as PropType<number>,
16 - default: 0, 16 + default: Infinity,
17 }, 17 },
18 // 根据后缀,或者其他 18 // 根据后缀,或者其他
19 accept: { 19 accept: {
@@ -21,9 +21,18 @@ export const basicProps = { @@ -21,9 +21,18 @@ export const basicProps = {
21 default: () => [], 21 default: () => [],
22 }, 22 },
23 multiple: { 23 multiple: {
24 - type: Boolean, 24 + type: Boolean as PropType<boolean>,
25 default: true, 25 default: true,
26 }, 26 },
  27 + uploadParams: {
  28 + type: Object as PropType<any>,
  29 + default: {},
  30 + },
  31 + api: {
  32 + type: Function as PropType<PromiseFn>,
  33 + default: null,
  34 + required: true,
  35 + },
27 }; 36 };
28 37
29 export const uploadContainerProps = { 38 export const uploadContainerProps = {
@@ -32,9 +41,17 @@ export const uploadContainerProps = { @@ -32,9 +41,17 @@ export const uploadContainerProps = {
32 default: () => [], 41 default: () => [],
33 }, 42 },
34 ...basicProps, 43 ...basicProps,
  44 + showPreviewNumber: {
  45 + type: Boolean as PropType<boolean>,
  46 + default: true,
  47 + },
  48 + emptyHidePreview: {
  49 + type: Boolean as PropType<boolean>,
  50 + default: false,
  51 + },
35 }; 52 };
36 53
37 -export const priviewProps = { 54 +export const previewProps = {
38 value: { 55 value: {
39 type: Array as PropType<string[]>, 56 type: Array as PropType<string[]>,
40 default: () => [], 57 default: () => [],
src/components/Upload/src/types.ts
1 -import { UploadApiResult } from '/@/api/demo/model/uploadModel'; 1 +import { UploadApiResult } from '/@/api/sys/model/uploadModel';
2 2
3 export enum UploadResultStatus { 3 export enum UploadResultStatus {
4 SUCCESS = 'success', 4 SUCCESS = 'success',
src/components/Upload/src/useUpload.ts
@@ -42,12 +42,12 @@ export function useUploadType({ @@ -42,12 +42,12 @@ export function useUploadType({
42 42
43 const maxSize = unref(maxSizeRef); 43 const maxSize = unref(maxSizeRef);
44 if (maxSize) { 44 if (maxSize) {
45 - helpTexts.push(`不超过${maxSize}MB`); 45 + helpTexts.push(`单个文件不超过${maxSize}MB`);
46 } 46 }
47 47
48 const maxNumber = unref(maxNumberRef); 48 const maxNumber = unref(maxNumberRef);
49 - if (maxNumber) {  
50 - helpTexts.push(`最多可选择${maxNumber}个文件`); 49 + if (maxNumber && maxNumber !== Infinity) {
  50 + helpTexts.push(`最多只能上传${maxNumber}个文件`);
51 } 51 }
52 return helpTexts.join(','); 52 return helpTexts.join(',');
53 }); 53 });
src/components/Upload/src/utils.ts
@@ -3,18 +3,17 @@ export function checkFileType(file: File, accepts: string[]) { @@ -3,18 +3,17 @@ export function checkFileType(file: File, accepts: string[]) {
3 // const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i; 3 // const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
4 const reg = new RegExp('\\.(' + newTypes + ')$', 'i'); 4 const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
5 5
6 - if (!reg.test(file.name)) {  
7 - return false;  
8 - } else {  
9 - return true;  
10 - } 6 + return reg.test(file.name);
11 } 7 }
  8 +
12 export function checkImgType(file: File) { 9 export function checkImgType(file: File) {
13 - return /\.(jpg|jpeg|png|gif)$/i.test(file.name); 10 + return isImgTypeByName(file.name);
14 } 11 }
  12 +
15 export function isImgTypeByName(name: string) { 13 export function isImgTypeByName(name: string) {
16 return /\.(jpg|jpeg|png|gif)$/i.test(name); 14 return /\.(jpg|jpeg|png|gif)$/i.test(name);
17 } 15 }
  16 +
18 export function getBase64WithFile(file: File) { 17 export function getBase64WithFile(file: File) {
19 return new Promise<{ 18 return new Promise<{
20 result: string; 19 result: string;
src/design/ant/btn.less
@@ -6,6 +6,11 @@ @@ -6,6 +6,11 @@
6 // &.ant-btn-primary:not(.ant-btn-link) { 6 // &.ant-btn-primary:not(.ant-btn-link) {
7 // box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08) !important; 7 // box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08) !important;
8 // } 8 // }
  9 + // &-group {
  10 + // .ant-btn:not(:first-child) {
  11 + // bottom: 1px;
  12 + // }
  13 + // }
9 14
10 &-primary { 15 &-primary {
11 color: @white; 16 color: @white;
src/design/var/index.less
@@ -16,4 +16,4 @@ @@ -16,4 +16,4 @@
16 @page-loading-z-index: 10000; 16 @page-loading-z-index: 10000;
17 17
18 // left-menu 18 // left-menu
19 -@app-menu-item-height: 46px; 19 +@app-menu-item-height: 44px;
src/router/menus/modules/demo/comp.ts
@@ -4,6 +4,9 @@ const menu: MenuModule = { @@ -4,6 +4,9 @@ const menu: MenuModule = {
4 menu: { 4 menu: {
5 name: '组件', 5 name: '组件',
6 path: '/comp', 6 path: '/comp',
  7 + tag: {
  8 + dot: true,
  9 + },
7 children: [ 10 children: [
8 { 11 {
9 path: 'basic', 12 path: 'basic',
@@ -38,10 +41,13 @@ const menu: MenuModule = { @@ -38,10 +41,13 @@ const menu: MenuModule = {
38 path: 'strength-meter', 41 path: 'strength-meter',
39 name: '密码强度组件', 42 name: '密码强度组件',
40 }, 43 },
41 - // {  
42 - // path: 'upload',  
43 - // name: '上传组件',  
44 - // }, 44 + {
  45 + path: 'upload',
  46 + name: '上传组件',
  47 + tag: {
  48 + content: 'new',
  49 + },
  50 + },
45 { 51 {
46 path: 'scroll', 52 path: 'scroll',
47 name: '滚动组件', 53 name: '滚动组件',
src/types/global.d.ts
1 -declare interface Fn<T = any> {  
2 - (...arg: T[]): T; 1 +declare interface Fn<T = any, R = T> {
  2 + (...arg: T[]): R;
  3 +}
  4 +
  5 +declare interface PromiseFn<T = any, R = T> {
  6 + (...arg: T[]): Promise<R>;
3 } 7 }
4 8
5 // 任意对象 9 // 任意对象
src/utils/file/FileDownload.ts renamed to src/utils/file/download.ts
1 -import { dataURLtoBlob } from './stream'; 1 +import { dataURLtoBlob, urlToBase64 } from './stream';
2 2
  3 +/**
  4 + * Download online pictures
  5 + * @param url
  6 + * @param filename
  7 + * @param mime
  8 + * @param bom
  9 + */
  10 +export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) {
  11 + urlToBase64(url).then((base64) => {
  12 + downloadByBase64(base64, filename, mime, bom);
  13 + });
  14 +}
  15 +
  16 +/**
  17 + * Download pictures based on base64
  18 + * @param buf
  19 + * @param filename
  20 + * @param mime
  21 + * @param bom
  22 + */
3 export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) { 23 export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) {
4 const base64Buf = dataURLtoBlob(buf); 24 const base64Buf = dataURLtoBlob(buf);
5 downloadByData(base64Buf, filename, mime, bom); 25 downloadByData(base64Buf, filename, mime, bom);
src/utils/file/stream.ts
@@ -13,3 +13,29 @@ export function dataURLtoBlob(base64Buf: string): Blob { @@ -13,3 +13,29 @@ export function dataURLtoBlob(base64Buf: string): Blob {
13 } 13 }
14 return new Blob([u8arr], { type: mime }); 14 return new Blob([u8arr], { type: mime });
15 } 15 }
  16 +
  17 +/**
  18 + * img url to base64
  19 + * @param url
  20 + */
  21 +export function urlToBase64(url: string, mineType?: string): Promise<string> {
  22 + return new Promise((resolve, reject) => {
  23 + let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>;
  24 + const ctx = canvas!.getContext('2d');
  25 +
  26 + const img = new Image();
  27 + img.crossOrigin = '';
  28 + img.onload = function () {
  29 + if (!canvas || !ctx) {
  30 + return reject();
  31 + }
  32 + canvas.height = img.height;
  33 + canvas.width = img.width;
  34 + ctx.drawImage(img, 0, 0);
  35 + const dataURL = canvas.toDataURL(mineType || 'image/png');
  36 + canvas = null;
  37 + resolve(dataURL);
  38 + };
  39 + img.src = url;
  40 + });
  41 +}
src/utils/http/axios/Axios.ts
@@ -118,11 +118,8 @@ export class VAxios { @@ -118,11 +118,8 @@ export class VAxios {
118 Object.keys(params.data).forEach((key) => { 118 Object.keys(params.data).forEach((key) => {
119 if (!params.data) return; 119 if (!params.data) return;
120 const value = params.data[key]; 120 const value = params.data[key];
121 - // support key-value array data  
122 if (Array.isArray(value)) { 121 if (Array.isArray(value)) {
123 value.forEach((item) => { 122 value.forEach((item) => {
124 - // { list: [ 11, 22 ] }  
125 - // formData.append('list[]', 11);  
126 formData.append(`${key}[]`, item); 123 formData.append(`${key}[]`, item);
127 }); 124 });
128 return; 125 return;
src/utils/http/axios/index.ts
@@ -160,20 +160,18 @@ const transform: AxiosTransform = { @@ -160,20 +160,18 @@ const transform: AxiosTransform = {
160 try { 160 try {
161 if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { 161 if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
162 createMessage.error('接口请求超时,请刷新页面重试!'); 162 createMessage.error('接口请求超时,请刷新页面重试!');
163 - return;  
164 } 163 }
165 if (err && err.includes('Network Error')) { 164 if (err && err.includes('Network Error')) {
166 createErrorModal({ 165 createErrorModal({
167 title: '网络异常', 166 title: '网络异常',
168 content: '请检查您的网络连接是否正常!', 167 content: '请检查您的网络连接是否正常!',
169 }); 168 });
170 - return;  
171 } 169 }
172 } catch (error) { 170 } catch (error) {
173 throw new Error(error); 171 throw new Error(error);
174 } 172 }
175 checkStatus(error.response && error.response.status, msg); 173 checkStatus(error.response && error.response.status, msg);
176 - return error; 174 + return Promise.reject(error);
177 }, 175 },
178 }; 176 };
179 177
src/utils/http/axios/types.ts
@@ -38,4 +38,5 @@ export interface UploadFileParams { @@ -38,4 +38,5 @@ export interface UploadFileParams {
38 file: File | Blob; 38 file: File | Blob;
39 // 文件名 39 // 文件名
40 filename?: string; 40 filename?: string;
  41 + [key: string]: any;
41 } 42 }
src/views/demo/comp/upload/index.vue
1 <template> 1 <template>
2 <div class="p-4"> 2 <div class="p-4">
3 - <UploadContainer :maxSize="5" /> 3 + <a-alert message="基础示例" class="my-5"></a-alert>
  4 + <BasicUpload :maxSize="20" :maxNumber="10" @change="handleChange" :api="uploadApi" />
  5 +
  6 + <a-alert message="嵌入表单,加入表单校验" class="my-5"></a-alert>
  7 +
  8 + <BasicForm @register="register" />
4 </div> 9 </div>
5 </template> 10 </template>
6 <script lang="ts"> 11 <script lang="ts">
7 - import { defineComponent } from 'vue';  
8 - import { UploadContainer } from '/@/components/Upload/index'; 12 + import { defineComponent, h } from 'vue';
  13 + import { BasicUpload } from '/@/components/Upload';
  14 + import { useMessage } from '/@/hooks/web/useMessage';
  15 + import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
9 16
  17 + import { uploadApi } from '/@/api/sys/upload';
10 // import { Alert } from 'ant-design-vue'; 18 // import { Alert } from 'ant-design-vue';
  19 +
  20 + const schemas: FormSchema[] = [
  21 + {
  22 + field: 'field1',
  23 + component: 'Input',
  24 + label: '字段1',
  25 + colProps: {
  26 + span: 8,
  27 + },
  28 + rules: [{ required: true, type: 'array', message: '请选择上传文件' }],
  29 + render: ({ model, field }) => {
  30 + return h(BasicUpload, {
  31 + value: model[field],
  32 + api: uploadApi,
  33 + onChange: (val: string[]) => {
  34 + model[field] = val;
  35 + },
  36 + });
  37 + },
  38 + },
  39 + ];
11 export default defineComponent({ 40 export default defineComponent({
12 - components: { UploadContainer }, 41 + components: { BasicUpload, BasicForm },
13 setup() { 42 setup() {
14 - return {}; 43 + const { createMessage } = useMessage();
  44 + const [register] = useForm({
  45 + labelWidth: 120,
  46 + schemas,
  47 + actionColOptions: {
  48 + span: 16,
  49 + },
  50 + });
  51 + return {
  52 + handleChange: (list: string[]) => {
  53 + createMessage.info(`已上传文件${JSON.stringify(list)}`);
  54 + },
  55 + uploadApi,
  56 + register,
  57 + };
15 }, 58 },
16 }); 59 });
17 </script> 60 </script>
src/views/demo/feat/download/index.vue
@@ -8,11 +8,21 @@ @@ -8,11 +8,21 @@
8 8
9 <a-alert message="base64流下载" /> 9 <a-alert message="base64流下载" />
10 <a-button type="primary" class="my-4" @click="handleDownloadByBase64"> base64流下载 </a-button> 10 <a-button type="primary" class="my-4" @click="handleDownloadByBase64"> base64流下载 </a-button>
  11 +
  12 + <a-alert message="图片Url下载,如果有跨域问题,需要处理图片跨域" />
  13 + <a-button type="primary" class="my-4" @click="handleDownloadByOnlineUrl">
  14 + 图片Url下载
  15 + </a-button>
11 </div> 16 </div>
12 </template> 17 </template>
13 <script lang="ts"> 18 <script lang="ts">
14 import { defineComponent } from 'vue'; 19 import { defineComponent } from 'vue';
15 - import { downloadByUrl, downloadByData, downloadByBase64 } from '/@/utils/file/FileDownload'; 20 + import {
  21 + downloadByUrl,
  22 + downloadByData,
  23 + downloadByBase64,
  24 + downloadByOnlineUrl,
  25 + } from '/@/utils/file/download';
16 import imgBase64 from './imgBase64'; 26 import imgBase64 from './imgBase64';
17 export default defineComponent({ 27 export default defineComponent({
18 setup() { 28 setup() {
@@ -24,15 +34,28 @@ @@ -24,15 +34,28 @@
24 url: 'https://codeload.github.com/anncwb/vue-vben-admin-doc/zip/master', 34 url: 'https://codeload.github.com/anncwb/vue-vben-admin-doc/zip/master',
25 target: '_self', 35 target: '_self',
26 }); 36 });
  37 +
  38 + downloadByUrl({
  39 + url: 'https://vebn.oss-cn-beijing.aliyuncs.com/vben/logo.png',
  40 + target: '_self',
  41 + });
27 } 42 }
28 43
29 function handleDownloadByBase64() { 44 function handleDownloadByBase64() {
30 downloadByBase64(imgBase64, 'logo.png'); 45 downloadByBase64(imgBase64, 'logo.png');
31 } 46 }
  47 +
  48 + function handleDownloadByOnlineUrl() {
  49 + downloadByOnlineUrl(
  50 + 'https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5944817f47b8408e9f1442ece49d68ca~tplv-k3u1fbpfcp-watermark.image',
  51 + 'logo.png'
  52 + );
  53 + }
32 return { 54 return {
33 handleDownloadByUrl, 55 handleDownloadByUrl,
34 handleDownByData, 56 handleDownByData,
35 handleDownloadByBase64, 57 handleDownloadByBase64,
  58 + handleDownloadByOnlineUrl,
36 }; 59 };
37 }, 60 },
38 }); 61 });
yarn.lock
@@ -1050,10 +1050,10 @@ @@ -1050,10 +1050,10 @@
1050 resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.2.tgz#c4a95ddc06ca9b9496df03604e66fdefb39f4c4b" 1050 resolved "https://registry.npmjs.org/@iconify/iconify/-/iconify-2.0.0-rc.2.tgz#c4a95ddc06ca9b9496df03604e66fdefb39f4c4b"
1051 integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw== 1051 integrity sha512-BybEHU5/I9EQ0CcwKAqmreZ2bMnAXrqLCTptAc6vPetHMbrXdZfejP5mt57e/8PNSt/qE7BHniU5PCYA+PGIHw==
1052 1052
1053 -"@iconify/json@^1.1.254":  
1054 - version "1.1.256"  
1055 - resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.256.tgz#0f138d421ab12faca2fdd49aaf4fbc0122db08e3"  
1056 - integrity sha512-CeLKbKL3lvq8afhR3LEyaBqXZDC52fgU0Ij3LbTRCwPUsumLNzhXA7MzN/f0JDYfXm9LShkfpgMcm00wQaANgg== 1053 +"@iconify/json@^1.1.258":
  1054 + version "1.1.258"
  1055 + resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.258.tgz#392064ae8fd4c6d542c21bb4d0d57d5860f38abb"
  1056 + integrity sha512-x5DKhRrg8v1NWmClWa8zA80gWQ9xevivsUAF4s8CyAl/ZplBsEE1funKuuVcIKjexyE1UXb7uFWrUKt1fB5n1A==
1057 1057
1058 "@koa/cors@^3.1.0": 1058 "@koa/cors@^3.1.0":
1059 version "3.1.0" 1059 version "3.1.0"
@@ -1316,10 +1316,10 @@ @@ -1316,10 +1316,10 @@
1316 "@types/qs" "*" 1316 "@types/qs" "*"
1317 "@types/serve-static" "*" 1317 "@types/serve-static" "*"
1318 1318
1319 -"@types/fs-extra@^9.0.2":  
1320 - version "9.0.3"  
1321 - resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.3.tgz#9996e5cce993508c32325380b429f04a1327523e"  
1322 - integrity sha512-NKdGoXLTFTRED3ENcfCsH8+ekV4gbsysanx2OPbstXVV6fZMgUCqTxubs6I9r7pbOJbFgVq1rpFtLURjKCZWUw== 1319 +"@types/fs-extra@^9.0.4":
  1320 + version "9.0.4"
  1321 + resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.4.tgz#12553138cf0438db9a31cdc8b0a3aa9332eb67aa"
  1322 + integrity sha512-50GO5ez44lxK5MDH90DYHFFfqxH7+fTqEEnvguQRzJ/tY9qFrMSHLiYHite+F3SNmf7+LHC1eMXojuD+E3Qcyg==
1323 dependencies: 1323 dependencies:
1324 "@types/node" "*" 1324 "@types/node" "*"
1325 1325
@@ -1725,18 +1725,18 @@ @@ -1725,18 +1725,18 @@
1725 vscode-languageserver-textdocument "^1.0.1" 1725 vscode-languageserver-textdocument "^1.0.1"
1726 vscode-uri "^2.1.2" 1726 vscode-uri "^2.1.2"
1727 1727
1728 -"@vueuse/core@^4.0.0-beta.40":  
1729 - version "4.0.0-beta.40"  
1730 - resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.40.tgz#7efdc15c1b994647dff7ae65c0ca573d96ce9b28"  
1731 - integrity sha512-FOTOUrXAAp0NOmy8hMlP1HpUhnB8LeRJZDOEUl/A9gKMDwWvPTEvxKsDAIzSa4s7I0MapVzfeP3soNCNfl9+vQ== 1728 +"@vueuse/core@^4.0.0-beta.41":
  1729 + version "4.0.0-beta.41"
  1730 + resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.0-beta.41.tgz#0058aed5ade75ae2866283498009ad5172cbae84"
  1731 + integrity sha512-CgUih65PzYScorm1S4F93e6XXm+qxA8GrRLOSB1kXaqtP6vXedwkBxKkNEYNACx4reL4VEHqM/BrM6FajXkQUg==
1732 dependencies: 1732 dependencies:
1733 - "@vueuse/shared" "4.0.0-beta.40" 1733 + "@vueuse/shared" "4.0.0-beta.41"
1734 vue-demi latest 1734 vue-demi latest
1735 1735
1736 -"@vueuse/shared@4.0.0-beta.40":  
1737 - version "4.0.0-beta.40"  
1738 - resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.40.tgz#76e9b52228159e7ec88df2c8f4eea8fce1a42ec3"  
1739 - integrity sha512-Ay71viUTXs0XX2hQ04kEExhpsCrw3KankBMP7euorsPjuQmIZjUA4NNOb45UAudg+uF5HXLpgWLvwb4cMOLHnQ== 1736 +"@vueuse/shared@4.0.0-beta.41":
  1737 + version "4.0.0-beta.41"
  1738 + resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.0-beta.41.tgz#395782ea2e580f1fc9488d25c89bd09f70170b25"
  1739 + integrity sha512-dqnuEPPC3OUJ6L6rhMiOCuPWIR698DtdwOydwCZBISsG2V6gZ2QFND6xtRwLib6/lhUMYVYPwIz3hPjlx7BIzw==
1740 dependencies: 1740 dependencies:
1741 vue-demi latest 1741 vue-demi latest
1742 1742
@@ -1850,10 +1850,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: @@ -1850,10 +1850,10 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
1850 dependencies: 1850 dependencies:
1851 color-convert "^2.0.1" 1851 color-convert "^2.0.1"
1852 1852
1853 -ant-design-vue@^2.0.0-beta.15:  
1854 - version "2.0.0-beta.15"  
1855 - resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-beta.15.tgz#3c787dabb70a33885d0e751e58f9a5610ed06134"  
1856 - integrity sha512-OxZy+ZYU3LauIL4Rhqwy441K/iD++Cit6upnQy5+LVUrX0PSObPqPqMWVpncbAmJJYTEz88gkvgGeYqBdzouWA== 1853 +ant-design-vue@^2.0.0-rc.1:
  1854 + version "2.0.0-rc.1"
  1855 + resolved "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-2.0.0-rc.1.tgz#2ef02475f3aa4c1474f2fe3cf44a52c34787be02"
  1856 + integrity sha512-iKXkFtTHarvLHV7LWmYh6g/Cmkv+xK+vS621A1Qvg37Z6lCGg3K9BGAizmklAYzOTiPz0Ltt63eSiNqYMGh52g==
1857 dependencies: 1857 dependencies:
1858 "@ant-design-vue/use" "^0.0.1-0" 1858 "@ant-design-vue/use" "^0.0.1-0"
1859 "@ant-design/icons-vue" "^5.1.5" 1859 "@ant-design/icons-vue" "^5.1.5"
@@ -8109,10 +8109,10 @@ vary@^1.1.2: @@ -8109,10 +8109,10 @@ vary@^1.1.2:
8109 resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 8109 resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
8110 integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 8110 integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
8111 8111
8112 -vditor@^3.6.0:  
8113 - version "3.6.1"  
8114 - resolved "https://registry.npmjs.org/vditor/-/vditor-3.6.1.tgz#b0b510f23d0cf0e5d8b3d36924e40400de96f692"  
8115 - integrity sha512-83GdGLIWrV1x04aK8DO9aZidqQfmuGXXUbxSCuQxRla+T9KfnFRmJwfqIxXQm8h+4jUBCFL38e8PqLa3fBOf9w== 8112 +vditor@^3.6.2:
  8113 + version "3.6.2"
  8114 + resolved "https://registry.npmjs.org/vditor/-/vditor-3.6.2.tgz#ee6011efa3ec563c6356ed82efbf2e00ba2e35c6"
  8115 + integrity sha512-HPHHun5+IXmYGMKDWcUD83VfP1Qfncz7DmaIKoWpluJgE8ve7s+4RbFBcaEpYPXYzIuL2UTHoMnIjmTPbenOCA==
8116 dependencies: 8116 dependencies:
8117 diff-match-patch "^1.0.5" 8117 diff-match-patch "^1.0.5"
8118 8118
@@ -8272,10 +8272,10 @@ vue-i18n@^9.0.0-beta.6: @@ -8272,10 +8272,10 @@ vue-i18n@^9.0.0-beta.6:
8272 dependencies: 8272 dependencies:
8273 source-map "^0.6.1" 8273 source-map "^0.6.1"
8274 8274
8275 -vue-router@^4.0.0-rc.2:  
8276 - version "4.0.0-rc.2"  
8277 - resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.2.tgz#8545cab76a05ca4f6dffbe6c6a671a4dbf585ab2"  
8278 - integrity sha512-51mBp39rzBFpk1nyU9SkhPcwR67gBzWIH8p3pyeDmtNYgWzGF3q8MneD/xbMwsfTQkw2H1qBk6uwRaVy3M8Nxw== 8275 +vue-router@^4.0.0-rc.3:
  8276 + version "4.0.0-rc.3"
  8277 + resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.0.0-rc.3.tgz#70d18e90030bc6a25e81a30401d673223998ec6b"
  8278 + integrity sha512-NnPqWIfanEhJC4wu8BEFBmnEDIrx9ST0/HtmBiE+oV2MQlhyRk1TmdttWwVqx6Sh7kONsrI10GQV9l3YEkcWXg==
8279 8279
8280 vue-types@^3.0.0: 8280 vue-types@^3.0.0:
8281 version "3.0.1" 8281 version "3.0.1"