Sanakey
2024-08-02 1ba88a00d3c7bb0f16a73feaa5ffcf7c1635e0ea
提交 | 用户 | age
746d4a 1 <template>
J 2   <BasicModal
661db0 3     width="800px"
962f90 4     :title="t('component.upload.upload')"
V 5     :okText="t('component.upload.save')"
746d4a 6     v-bind="$attrs"
J 7     @register="register"
8     @ok="handleOk"
9     :closeFunc="handleCloseFunc"
10     :maskClosable="false"
661db0 11     :keyboard="false"
3fcfac 12     class="upload-modal"
661db0 13     :okButtonProps="getOkButtonProps"
746d4a 14     :cancelButtonProps="{ disabled: isUploadingRef }"
J 15   >
18ad1b 16     <template #centerFooter>
661db0 17       <a-button
V 18         @click="handleStartUpload"
19         color="success"
20         :disabled="!getIsSelectFile"
21         :loading="isUploadingRef"
22       >
23         {{ getUploadBtnText }}
746d4a 24       </a-button>
J 25     </template>
bd6b20 26
815250 27     <div class="upload-modal-toolbar">
bd6b20 28       <Alert :message="getHelpText" type="info" banner class="upload-modal-toolbar__text" />
V 29
815250 30       <Upload
J 31         :accept="getStringAccept"
32         :multiple="multiple"
33         :before-upload="beforeUpload"
a462be 34         :show-upload-list="false"
815250 35         class="upload-modal-toolbar__btn"
J 36       >
9edc28 37         <a-button type="primary">
V 38           {{ t('component.upload.choose') }}
39         </a-button>
815250 40       </Upload>
J 41     </div>
beed7f 42     <FileList
B 43       v-model:dataSource="fileListRef"
44       :columns="columns"
45       :actionColumn="actionColumn"
46       :openDrag="fileListOpenDrag"
47       :dragOptions="fileListDragOptions"
48     />
746d4a 49   </BasicModal>
J 50 </template>
bab28a 51 <script lang="ts" setup>
X 52   import { ref, toRefs, unref, computed, PropType } from 'vue';
661db0 53   import { Upload, Alert } from 'ant-design-vue';
bab28a 54   import { BasicModal, useModalInner } from '@/components/Modal';
746d4a 55   // hooks
dccc8f 56   import { useUploadType } from '../hooks/useUpload';
bab28a 57   import { useMessage } from '@/hooks/web/useMessage';
746d4a 58   //   types
dccc8f 59   import { FileItem, UploadResultStatus } from '../types/typing';
371c97 60   import { handleFnKey, basicProps } from '../props';
746d4a 61   import { createTableColumns, createActionColumn } from './data';
J 62   // utils
dccc8f 63   import { checkImgType, getBase64WithFile } from '../helper';
bab28a 64   import { buildUUID } from '@/utils/uuid';
X 65   import { isFunction } from '@/utils/is';
66   import { warn } from '@/utils/log';
3f6920 67   import FileList from './FileList.vue';
bab28a 68   import { useI18n } from '@/hooks/web/useI18n';
b28a46 69   import { get } from 'lodash-es';
3f6920 70
bab28a 71   const props = defineProps({
X 72     ...basicProps,
73     previewFileList: {
d9cdf3 74       type: Array as PropType<string[] | any[]>,
bab28a 75       default: () => [],
746d4a 76     },
J 77   });
bab28a 78
X 79   const emit = defineEmits(['change', 'register', 'delete']);
80
81   const columns = createTableColumns();
82   const actionColumn = createActionColumn(handleRemove);
83
84   // 是否正在上传
85   const isUploadingRef = ref(false);
86   const fileListRef = ref<FileItem[]>([]);
87   const { accept, helpText, maxNumber, maxSize } = toRefs(props);
88
89   const { t } = useI18n();
90   const [register, { closeModal }] = useModalInner();
91
92   const { getStringAccept, getHelpText } = useUploadType({
93     acceptRef: accept,
94     helpTextRef: helpText,
95     maxNumberRef: maxNumber,
96     maxSizeRef: maxSize,
97   });
98
99   const { createMessage } = useMessage();
100
101   const getIsSelectFile = computed(() => {
102     return (
103       fileListRef.value.length > 0 &&
104       !fileListRef.value.every((item) => item.status === UploadResultStatus.SUCCESS)
105     );
106   });
107
108   const getOkButtonProps = computed(() => {
109     const someSuccess = fileListRef.value.some(
110       (item) => item.status === UploadResultStatus.SUCCESS,
111     );
112     return {
113       disabled: isUploadingRef.value || fileListRef.value.length === 0 || !someSuccess,
114     };
115   });
116
117   const getUploadBtnText = computed(() => {
118     const someError = fileListRef.value.some((item) => item.status === UploadResultStatus.ERROR);
119     return isUploadingRef.value
120       ? t('component.upload.uploading')
121       : someError
626c54 122         ? t('component.upload.reUploadFailed')
X 123         : t('component.upload.startUpload');
bab28a 124   });
X 125
126   // 上传前校验
127   function beforeUpload(file: File) {
128     const { size, name } = file;
129     const { maxSize } = props;
130     // 设置最大值,则判断
131     if (maxSize && file.size / 1024 / 1024 >= maxSize) {
132       createMessage.error(t('component.upload.maxSizeMultiple', [maxSize]));
133       return false;
134     }
135
136     const commonItem = {
137       uuid: buildUUID(),
138       file,
139       size,
140       name,
141       percent: 0,
142       type: name.split('.').pop(),
143     };
144     // 生成图片缩略图
145     if (checkImgType(file)) {
146       // beforeUpload,如果异步会调用自带上传方法
147       // file.thumbUrl = await getBase64(file);
148       getBase64WithFile(file).then(({ result: thumbUrl }) => {
149         fileListRef.value = [
150           ...unref(fileListRef),
151           {
152             thumbUrl,
153             ...commonItem,
154           },
155         ];
156       });
157     } else {
158       fileListRef.value = [...unref(fileListRef), commonItem];
159     }
160     return false;
161   }
162
163   // 删除
371c97 164   function handleRemove(obj: Record<handleFnKey, any>) {
WZ 165     let { record = {}, uidKey = 'uid' } = obj;
166     const index = fileListRef.value.findIndex((item) => item[uidKey] === record[uidKey]);
167     if (index !== -1) {
168       const removed = fileListRef.value.splice(index, 1);
169       emit('delete', removed[0][uidKey]);
170     }
bab28a 171   }
X 172
173   async function uploadApiByItem(item: FileItem) {
174     const { api } = props;
175     if (!api || !isFunction(api)) {
176       return warn('upload api must exist and be a function');
177     }
178     try {
179       item.status = UploadResultStatus.UPLOADING;
180       const ret = await props.api?.(
181         {
182           data: {
183             ...(props.uploadParams || {}),
184           },
185           file: item.file,
186           name: props.name,
187           filename: props.filename,
188         },
189         function onUploadProgress(progressEvent: ProgressEvent) {
190           const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
191           item.percent = complete;
192         },
193       );
194       const { data } = ret;
195       item.status = UploadResultStatus.SUCCESS;
196       item.response = data;
6528dc 197       if (props.resultField) {
b28a46 198         // 适配预览组件而进行封装
E 199         item.response = {
6528dc 200           code: 0,
L 201           message: 'upload Success!',
202           url: get(ret, props.resultField),
203         };
b28a46 204       }
bab28a 205       return {
X 206         success: true,
207         error: null,
208       };
209     } catch (e) {
210       console.log(e);
211       item.status = UploadResultStatus.ERROR;
212       return {
213         success: false,
214         error: e,
215       };
216     }
217   }
218
219   // 点击开始上传
220   async function handleStartUpload() {
221     const { maxNumber } = props;
6528dc 222     if (fileListRef.value.length + props.previewFileList.length > maxNumber) {
bab28a 223       return createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
X 224     }
225     try {
226       isUploadingRef.value = true;
227       // 只上传不是成功状态的
228       const uploadFileList =
229         fileListRef.value.filter((item) => item.status !== UploadResultStatus.SUCCESS) || [];
230       const data = await Promise.all(
231         uploadFileList.map((item) => {
232           return uploadApiByItem(item);
233         }),
234       );
235       isUploadingRef.value = false;
236       // 生产环境:抛出错误
237       const errorList = data.filter((item: any) => !item.success);
238       if (errorList.length > 0) throw errorList;
239     } catch (e) {
240       isUploadingRef.value = false;
241       throw e;
242     }
243   }
244
245   //   点击保存
246   function handleOk() {
247     const { maxNumber } = props;
248
249     if (fileListRef.value.length > maxNumber) {
250       return createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
251     }
252     if (isUploadingRef.value) {
253       return createMessage.warning(t('component.upload.saveWarn'));
254     }
255     const fileList: string[] = [];
6528dc 256
bab28a 257     for (const item of fileListRef.value) {
X 258       const { status, response } = item;
259       if (status === UploadResultStatus.SUCCESS && response) {
260         fileList.push(response.url);
261       }
262     }
263     // 存在一个上传成功的即可保存
264     if (fileList.length <= 0) {
265       return createMessage.warning(t('component.upload.saveError'));
266     }
267     fileListRef.value = [];
268     closeModal();
269     emit('change', fileList);
270   }
271
272   // 点击关闭:则所有操作不保存,包括上传的
273   async function handleCloseFunc() {
274     if (!isUploadingRef.value) {
275       fileListRef.value = [];
276       return true;
277     } else {
278       createMessage.warning(t('component.upload.uploadWait'));
279       return false;
280     }
281   }
746d4a 282 </script>
J 283 <style lang="less">
284   .upload-modal {
285     .ant-upload-list {
286       display: none;
287     }
288
289     .ant-table-wrapper .ant-spin-nested-loading {
290       padding: 0;
291     }
815250 292
J 293     &-toolbar {
294       display: flex;
295       align-items: center;
296       margin-bottom: 8px;
297
298       &__btn {
ba2415 299         flex: 1;
815250 300         margin-left: 8px;
J 301         text-align: right;
302       }
303     }
746d4a 304   }
J 305 </style>