Commit 746d4a745d06ff1f0eb42a2c2d09c539202bc91e
1 parent
2b95be80
wip: add upload component
Showing
19 changed files
with
847 additions
and
21 deletions
.env.development
... | ... | @@ -5,7 +5,7 @@ VITE_USE_MOCK = true |
5 | 5 | VITE_PUBLIC_PATH = / |
6 | 6 | |
7 | 7 | # Cross-domain proxy, you can configure multiple |
8 | -VITE_PROXY=[["/api","http://localhost:3000"]] | |
8 | +VITE_PROXY=[["/api","http://localhost:3000"],["/upload","http://localhost:3001/upload"]] | |
9 | 9 | # VITE_PROXY=[["/api","https://vvbin.cn/test"]] |
10 | 10 | |
11 | 11 | # Delete console | ... | ... |
src/api/demo/model/uploadModel.ts
0 → 100644
src/api/demo/upload.ts
0 → 100644
1 | +import { UploadApiResult } from './model/uploadModel'; | |
2 | +import { defHttp } from '/@/utils/http/axios'; | |
3 | +import { UploadFileParams } from '/@/utils/http/axios/types'; | |
4 | + | |
5 | +enum Api { | |
6 | + UPLOAD_URL = '/upload', | |
7 | +} | |
8 | + | |
9 | +/** | |
10 | + * @description: 上传接口 | |
11 | + */ | |
12 | +export function uploadApi( | |
13 | + params: UploadFileParams, | |
14 | + onUploadProgress: (progressEvent: ProgressEvent) => void | |
15 | +) { | |
16 | + return defHttp.uploadFile<UploadApiResult>( | |
17 | + { | |
18 | + url: Api.UPLOAD_URL, | |
19 | + onUploadProgress, | |
20 | + }, | |
21 | + params | |
22 | + ); | |
23 | +} | ... | ... |
src/components/Table/src/types/tableAction.ts
src/components/Upload/index.ts
0 → 100644
src/components/Upload/src/ThumnUrl.vue
0 → 100644
1 | +<template> | |
2 | + <span> | |
3 | + <img v-if="fileUrl" :src="fileUrl" /> | |
4 | + <span v-else>{{ fileType }}</span> | |
5 | + </span> | |
6 | +</template> | |
7 | +<script lang="ts"> | |
8 | + import { defineComponent } from 'vue'; | |
9 | + | |
10 | + export default defineComponent({ | |
11 | + props: { | |
12 | + fileUrl: { | |
13 | + type: String, | |
14 | + default: '', | |
15 | + }, | |
16 | + fileType: { | |
17 | + type: String, | |
18 | + default: '', | |
19 | + }, | |
20 | + fileName: { | |
21 | + type: String, | |
22 | + default: '', | |
23 | + }, | |
24 | + }, | |
25 | + setup() { | |
26 | + return {}; | |
27 | + }, | |
28 | + }); | |
29 | +</script> | ... | ... |
src/components/Upload/src/UploadContainer.vue
0 → 100644
1 | +<template> | |
2 | + <div> | |
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" /> | |
7 | + </a-button> | |
8 | + </a-button-group> | |
9 | + <UploadModal v-bind="$props" @register="registerUploadModal" @change="handleChange" /> | |
10 | + <UploadPreviewModal | |
11 | + :value="fileListRef" | |
12 | + @register="registerPreviewModal" | |
13 | + @change="handlePreviewChange" | |
14 | + /> | |
15 | + </div> | |
16 | +</template> | |
17 | +<script lang="ts"> | |
18 | + import { defineComponent, ref, watch, unref } from 'vue'; | |
19 | + import { useModal } from '/@/components/Modal'; | |
20 | + import UploadModal from './UploadModal.vue'; | |
21 | + import { uploadContainerProps } from './props'; | |
22 | + import UploadPreviewModal from './UploadPreviewModal.vue'; | |
23 | + import Icon from '/@/components/Icon/index'; | |
24 | + export default defineComponent({ | |
25 | + components: { UploadModal, UploadPreviewModal, Icon }, | |
26 | + props: uploadContainerProps, | |
27 | + setup(props, { emit }) { | |
28 | + // 上传modal | |
29 | + const [registerUploadModal, { openModal: openUploadModal }] = useModal(); | |
30 | + // 预览modal | |
31 | + const [registerPreviewModal, { openModal: openPreviewModal }] = useModal(); | |
32 | + | |
33 | + const fileListRef = ref<string[]>([]); | |
34 | + watch( | |
35 | + () => props.value, | |
36 | + (value) => { | |
37 | + fileListRef.value = [...(value || [])]; | |
38 | + }, | |
39 | + { immediate: true } | |
40 | + ); | |
41 | + // 上传modal保存操作 | |
42 | + function handleChange(urls: string[]) { | |
43 | + fileListRef.value = [...unref(fileListRef), ...(urls || [])]; | |
44 | + emit('change', fileListRef.value); | |
45 | + } | |
46 | + // 预览modal保存操作 | |
47 | + function handlePreviewChange(urls: string[]) { | |
48 | + fileListRef.value = [...(urls || [])]; | |
49 | + emit('change', fileListRef.value); | |
50 | + } | |
51 | + return { | |
52 | + registerUploadModal, | |
53 | + openUploadModal, | |
54 | + handleChange, | |
55 | + handlePreviewChange, | |
56 | + registerPreviewModal, | |
57 | + openPreviewModal, | |
58 | + fileListRef, | |
59 | + }; | |
60 | + }, | |
61 | + }); | |
62 | +</script> | ... | ... |
src/components/Upload/src/UploadModal.vue
0 → 100644
1 | +<template> | |
2 | + <BasicModal | |
3 | + v-bind="$attrs" | |
4 | + @register="register" | |
5 | + @ok="handleOk" | |
6 | + :closeFunc="handleCloseFunc" | |
7 | + :maskClosable="false" | |
8 | + width="800px" | |
9 | + title="上传组件" | |
10 | + wrapClassName="upload-modal" | |
11 | + :okButtonProps="{ disabled: isUploadingRef }" | |
12 | + :cancelButtonProps="{ disabled: isUploadingRef }" | |
13 | + > | |
14 | + <template #centerdFooter> | |
15 | + <a-button @click="handleStartUpload" color="success" :loading="isUploadingRef"> | |
16 | + {{ isUploadingRef ? '上传中' : '开始上传' }} | |
17 | + </a-button> | |
18 | + </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" /> | |
24 | + </BasicModal> | |
25 | +</template> | |
26 | +<script lang="ts"> | |
27 | + import { defineComponent, reactive, ref, toRef, unref } from 'vue'; | |
28 | + import { Upload } from 'ant-design-vue'; | |
29 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
30 | + import { BasicTable, useTable } from '/@/components/Table'; | |
31 | + // hooks | |
32 | + import { useUploadType } from './useUpload'; | |
33 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
34 | + // types | |
35 | + import { FileItem, UploadResultStatus } from './types'; | |
36 | + import { basicProps } from './props'; | |
37 | + import { createTableColumns, createActionColumn } from './data'; | |
38 | + // utils | |
39 | + import { checkFileType, checkImgType, getBase64WithFile } from './utils'; | |
40 | + import { buildUUID } from '/@/utils/uuid'; | |
41 | + import { createImgPreview } from '/@/components/Preview/index'; | |
42 | + import { uploadApi } from '/@/api/demo/upload'; | |
43 | + | |
44 | + export default defineComponent({ | |
45 | + components: { BasicModal, Upload, BasicTable }, | |
46 | + props: basicProps, | |
47 | + setup(props, { emit }) { | |
48 | + const [register, { closeModal }] = useModalInner(); | |
49 | + 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'), | |
54 | + }); | |
55 | + | |
56 | + const fileListRef = ref<FileItem[]>([]); | |
57 | + const state = reactive<{ fileList: FileItem[] }>({ fileList: [] }); | |
58 | + const { createMessage } = useMessage(); | |
59 | + // 上传前校验 | |
60 | + function beforeUpload(file: File) { | |
61 | + const { size, name } = file; | |
62 | + const { maxSize } = props; | |
63 | + const accept = unref(getAccept); | |
64 | + | |
65 | + // 设置最大值,则判断 | |
66 | + if (maxSize && file.size / 1024 / 1024 >= maxSize) { | |
67 | + createMessage.error(`只能上传不超过${maxSize}MB的文件!`); | |
68 | + return false; | |
69 | + } | |
70 | + | |
71 | + // 设置类型,则判断 | |
72 | + if (accept.length > 0 && !checkFileType(file, accept)) { | |
73 | + createMessage.error!(`只能上传${accept.join(',')}格式文件`); | |
74 | + return false; | |
75 | + } | |
76 | + // 生成图片缩略图 | |
77 | + if (checkImgType(file)) { | |
78 | + // beforeUpload,如果异步会调用自带上传方法 | |
79 | + // file.thumbUrl = await getBase64(file); | |
80 | + getBase64WithFile(file).then(({ result: thumbUrl }) => { | |
81 | + fileListRef.value = [ | |
82 | + ...unref(fileListRef), | |
83 | + { | |
84 | + uuid: buildUUID(), | |
85 | + file, | |
86 | + thumbUrl, | |
87 | + size, | |
88 | + name, | |
89 | + percent: 0, | |
90 | + type: name.split('.').pop(), | |
91 | + }, | |
92 | + ]; | |
93 | + }); | |
94 | + } 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 | + ]; | |
107 | + } | |
108 | + return false; | |
109 | + } | |
110 | + // 删除 | |
111 | + function handleRemove(record: FileItem) { | |
112 | + const index = fileListRef.value.findIndex((item) => item.uuid === record.uuid); | |
113 | + index !== -1 && fileListRef.value.splice(index, 1); | |
114 | + } | |
115 | + // 预览 | |
116 | + function handlePreview(record: FileItem) { | |
117 | + const { thumbUrl = '' } = record; | |
118 | + createImgPreview({ | |
119 | + imageList: [thumbUrl], | |
120 | + }); | |
121 | + } | |
122 | + const [registerTable] = useTable({ | |
123 | + columns: createTableColumns(), | |
124 | + actionColumn: createActionColumn(handleRemove, handlePreview), | |
125 | + pagination: false, | |
126 | + }); | |
127 | + // 是否正在上传 | |
128 | + const isUploadingRef = ref(false); | |
129 | + async function uploadApiByItem(item: FileItem) { | |
130 | + try { | |
131 | + item.status = UploadResultStatus.UPLOADING; | |
132 | + | |
133 | + const { data } = await uploadApi( | |
134 | + { | |
135 | + file: item.file, | |
136 | + }, | |
137 | + function onUploadProgress(progressEvent: ProgressEvent) { | |
138 | + const complete = ((progressEvent.loaded / progressEvent.total) * 100) | 0; | |
139 | + item.percent = complete; | |
140 | + } | |
141 | + ); | |
142 | + item.status = UploadResultStatus.SUCCESS; | |
143 | + item.responseData = data; | |
144 | + return { | |
145 | + success: true, | |
146 | + error: null, | |
147 | + }; | |
148 | + } catch (e) { | |
149 | + console.log(e); | |
150 | + item.status = UploadResultStatus.ERROR; | |
151 | + return { | |
152 | + success: false, | |
153 | + error: e, | |
154 | + }; | |
155 | + } | |
156 | + } | |
157 | + // 点击开始上传 | |
158 | + async function handleStartUpload() { | |
159 | + try { | |
160 | + isUploadingRef.value = true; | |
161 | + const data = await Promise.all( | |
162 | + unref(fileListRef).map((item) => { | |
163 | + return uploadApiByItem(item); | |
164 | + }) | |
165 | + ); | |
166 | + isUploadingRef.value = false; | |
167 | + // 生产环境:抛出错误 | |
168 | + const errorList = data.filter((item) => !item.success); | |
169 | + if (errorList.length > 0) { | |
170 | + throw errorList; | |
171 | + } | |
172 | + } catch (e) { | |
173 | + isUploadingRef.value = false; | |
174 | + throw e; | |
175 | + } | |
176 | + } | |
177 | + // 点击保存 | |
178 | + function handleOk() { | |
179 | + // TODO: 没起作用:okButtonProps={{ disabled: state.isUploading }} | |
180 | + if (isUploadingRef.value) { | |
181 | + createMessage.warning('请等待文件上传后,保存'); | |
182 | + return; | |
183 | + } | |
184 | + const fileList: string[] = []; | |
185 | + | |
186 | + for (const item of fileListRef.value) { | |
187 | + const { status, responseData } = item; | |
188 | + if (status === UploadResultStatus.SUCCESS && responseData) { | |
189 | + fileList.push(responseData.url); | |
190 | + } | |
191 | + } | |
192 | + | |
193 | + // 存在一个上传成功的即可保存 | |
194 | + | |
195 | + if (fileList.length <= 0) { | |
196 | + createMessage.warning('没有上传成功的文件,无法保存'); | |
197 | + return; | |
198 | + } | |
199 | + console.log(fileList); | |
200 | + emit('change', fileList); | |
201 | + fileListRef.value = []; | |
202 | + closeModal(); | |
203 | + } | |
204 | + // 点击关闭:则所有操作不保存,包括上传的 | |
205 | + function handleCloseFunc() { | |
206 | + if (!isUploadingRef.value) { | |
207 | + fileListRef.value = []; | |
208 | + return true; | |
209 | + } else { | |
210 | + createMessage.warning('请等待文件上传结束后操作'); | |
211 | + return false; | |
212 | + } | |
213 | + } | |
214 | + return { | |
215 | + register, | |
216 | + closeModal, | |
217 | + getHelpText, | |
218 | + getStringAccept, | |
219 | + beforeUpload, | |
220 | + registerTable, | |
221 | + fileListRef, | |
222 | + state, | |
223 | + isUploadingRef, | |
224 | + handleStartUpload, | |
225 | + handleOk, | |
226 | + handleCloseFunc, | |
227 | + }; | |
228 | + }, | |
229 | + }); | |
230 | +</script> | |
231 | +<style lang="less"> | |
232 | + // /deep/ .ant-upload-list { | |
233 | + // display: none; | |
234 | + // } | |
235 | + .upload-modal { | |
236 | + .ant-upload-list { | |
237 | + display: none; | |
238 | + } | |
239 | + | |
240 | + .ant-table-wrapper .ant-spin-nested-loading { | |
241 | + padding: 0; | |
242 | + } | |
243 | + } | |
244 | +</style> | ... | ... |
src/components/Upload/src/UploadPreviewModal.vue
0 → 100644
1 | +<template> | |
2 | + <BasicModal | |
3 | + wrapClassName="upload-preview-modal" | |
4 | + v-bind="$attrs" | |
5 | + width="800px" | |
6 | + @register="register" | |
7 | + title="预览" | |
8 | + :showOkBtn="false" | |
9 | + > | |
10 | + <BasicTable @register="registerTable" :dataSource="fileListRef" /> | |
11 | + </BasicModal> | |
12 | +</template> | |
13 | +<script lang="ts"> | |
14 | + import { defineComponent, watch, ref, unref } from 'vue'; | |
15 | + import { BasicTable, useTable } from '/@/components/Table'; | |
16 | + import { createPreviewColumns, createPreviewActionColumn } from './data'; | |
17 | + import { BasicModal, useModalInner } from '/@/components/Modal'; | |
18 | + import { priviewProps } from './props'; | |
19 | + import { PreviewFileItem } from './types'; | |
20 | + import { createImgPreview } from '/@/components/Preview/index'; | |
21 | + import { downloadByUrl } from '/@/utils/file/FileDownload'; | |
22 | + | |
23 | + export default defineComponent({ | |
24 | + components: { BasicModal, BasicTable }, | |
25 | + props: priviewProps, | |
26 | + setup(props, { emit }) { | |
27 | + const [register, { closeModal }] = useModalInner(); | |
28 | + const fileListRef = ref<PreviewFileItem[]>([]); | |
29 | + watch( | |
30 | + () => props.value, | |
31 | + (value) => { | |
32 | + fileListRef.value = []; | |
33 | + value.forEach((item) => { | |
34 | + fileListRef.value = [ | |
35 | + ...unref(fileListRef), | |
36 | + { | |
37 | + url: item, | |
38 | + type: item.split('.').pop() || '', | |
39 | + name: item.split('/').pop() || '', | |
40 | + }, | |
41 | + ]; | |
42 | + }); | |
43 | + }, | |
44 | + { immediate: true } | |
45 | + ); | |
46 | + // 删除 | |
47 | + function handleRemove(record: PreviewFileItem) { | |
48 | + const index = fileListRef.value.findIndex((item) => item.url === record.url); | |
49 | + if (index !== -1) { | |
50 | + fileListRef.value.splice(index, 1); | |
51 | + emit( | |
52 | + 'change', | |
53 | + fileListRef.value.map((item) => item.url) | |
54 | + ); | |
55 | + } | |
56 | + } | |
57 | + // 预览 | |
58 | + function handlePreview(record: PreviewFileItem) { | |
59 | + const { url = '' } = record; | |
60 | + createImgPreview({ | |
61 | + imageList: [url], | |
62 | + }); | |
63 | + } | |
64 | + // 下载 | |
65 | + function handleDownload(record: PreviewFileItem) { | |
66 | + const { url = '' } = record; | |
67 | + downloadByUrl({ url }); | |
68 | + } | |
69 | + const [registerTable] = useTable({ | |
70 | + columns: createPreviewColumns(), | |
71 | + pagination: false, | |
72 | + actionColumn: createPreviewActionColumn({ handleRemove, handlePreview, handleDownload }), | |
73 | + }); | |
74 | + return { | |
75 | + register, | |
76 | + closeModal, | |
77 | + fileListRef, | |
78 | + registerTable, | |
79 | + }; | |
80 | + }, | |
81 | + }); | |
82 | +</script> | |
83 | +<style lang="less"> | |
84 | + .upload-preview-modal { | |
85 | + .ant-upload-list { | |
86 | + display: none; | |
87 | + } | |
88 | + | |
89 | + .ant-table-wrapper .ant-spin-nested-loading { | |
90 | + padding: 0; | |
91 | + } | |
92 | + } | |
93 | +</style> | ... | ... |
src/components/Upload/src/data.tsx
0 → 100644
1 | +// import { BasicColumn, TableAction, ActionItem } from '@/components/table'; | |
2 | +import { checkImgType, isImgTypeByName } from './utils'; | |
3 | +// import ThumnUrl from './ThumbUrl.vue'; | |
4 | +import { Progress } from 'ant-design-vue'; | |
5 | +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'; | |
9 | + | |
10 | +// 文件上传列表 | |
11 | +export function createTableColumns(): BasicColumn[] { | |
12 | + return [ | |
13 | + { | |
14 | + dataIndex: 'thumbUrl', | |
15 | + title: '图例', | |
16 | + width: 100, | |
17 | + customRender: ({ record }) => { | |
18 | + 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} />; | |
21 | + }, | |
22 | + }, | |
23 | + { | |
24 | + dataIndex: 'name', | |
25 | + title: '文件名', | |
26 | + align: 'left', | |
27 | + customRender: ({ text, record }) => { | |
28 | + const { percent, status: uploadStatus } = (record as FileItem) || {}; | |
29 | + let status = 'normal'; | |
30 | + if (uploadStatus === UploadResultStatus.ERROR) { | |
31 | + status = 'exception'; | |
32 | + } else if (uploadStatus === UploadResultStatus.UPLOADING) { | |
33 | + status = 'active'; | |
34 | + } else if (uploadStatus === UploadResultStatus.SUCCESS) { | |
35 | + status = 'success'; | |
36 | + } | |
37 | + return ( | |
38 | + <span> | |
39 | + <p class="ellipsis mb-1" title={text}> | |
40 | + {text} | |
41 | + </p> | |
42 | + <Progress percent={percent} size="small" status={status} /> | |
43 | + </span> | |
44 | + ); | |
45 | + }, | |
46 | + }, | |
47 | + { | |
48 | + dataIndex: 'size', | |
49 | + title: '文件大小', | |
50 | + width: 100, | |
51 | + customRender: ({ text = 0 }) => { | |
52 | + return text && (text / 1024).toFixed(2) + 'KB'; | |
53 | + }, | |
54 | + }, | |
55 | + // { | |
56 | + // dataIndex: 'type', | |
57 | + // title: '文件类型', | |
58 | + // width: 100, | |
59 | + // }, | |
60 | + { | |
61 | + dataIndex: 'status', | |
62 | + title: '状态', | |
63 | + width: 100, | |
64 | + customRender: ({ text }) => { | |
65 | + if (text === UploadResultStatus.SUCCESS) { | |
66 | + return '上传成功'; | |
67 | + } else if (text === UploadResultStatus.ERROR) { | |
68 | + return '上传失败'; | |
69 | + } else if (text === UploadResultStatus.UPLOADING) { | |
70 | + return '上传中'; | |
71 | + } | |
72 | + | |
73 | + return text; | |
74 | + }, | |
75 | + }, | |
76 | + ]; | |
77 | +} | |
78 | +export function createActionColumn(handleRemove: Function, handlePreview: Function): BasicColumn { | |
79 | + return { | |
80 | + width: 120, | |
81 | + title: '操作', | |
82 | + dataIndex: 'action', | |
83 | + fixed: false, | |
84 | + customRender: ({ record }) => { | |
85 | + const actions: ActionItem[] = [ | |
86 | + { | |
87 | + label: '删除', | |
88 | + onClick: handleRemove.bind(null, record), | |
89 | + }, | |
90 | + ]; | |
91 | + if (checkImgType(record)) { | |
92 | + actions.unshift({ | |
93 | + label: '预览', | |
94 | + onClick: handlePreview.bind(null, record), | |
95 | + }); | |
96 | + } | |
97 | + return <TableAction actions={actions} />; | |
98 | + }, | |
99 | + }; | |
100 | +} | |
101 | +// 文件预览列表 | |
102 | +export function createPreviewColumns(): BasicColumn[] { | |
103 | + return [ | |
104 | + { | |
105 | + dataIndex: 'url', | |
106 | + title: '图例', | |
107 | + width: 100, | |
108 | + customRender: ({ record }) => { | |
109 | + const { url, type } = (record as PreviewFileItem) || {}; | |
110 | + return ( | |
111 | + <span>{isImgTypeByName(url) ? <img src={url} style={{ width: '50px' }} /> : type}</span> | |
112 | + ); | |
113 | + }, | |
114 | + }, | |
115 | + { | |
116 | + dataIndex: 'name', | |
117 | + title: '文件名', | |
118 | + align: 'left', | |
119 | + }, | |
120 | + ]; | |
121 | +} | |
122 | + | |
123 | +export function createPreviewActionColumn({ | |
124 | + handleRemove, | |
125 | + handlePreview, | |
126 | + handleDownload, | |
127 | +}: { | |
128 | + handleRemove: Function; | |
129 | + handlePreview: Function; | |
130 | + handleDownload: Function; | |
131 | +}): BasicColumn { | |
132 | + return { | |
133 | + width: 160, | |
134 | + title: '操作', | |
135 | + dataIndex: 'action', | |
136 | + fixed: false, | |
137 | + customRender: ({ record }) => { | |
138 | + const { url } = (record as PreviewFileItem) || {}; | |
139 | + | |
140 | + const actions: ActionItem[] = [ | |
141 | + { | |
142 | + label: '删除', | |
143 | + onClick: handleRemove.bind(null, record), | |
144 | + }, | |
145 | + { | |
146 | + label: '下载', | |
147 | + onClick: handleDownload.bind(null, record), | |
148 | + }, | |
149 | + ]; | |
150 | + if (isImgTypeByName(url)) { | |
151 | + actions.unshift({ | |
152 | + label: '预览', | |
153 | + onClick: handlePreview.bind(null, record), | |
154 | + }); | |
155 | + } | |
156 | + return <TableAction actions={actions} />; | |
157 | + }, | |
158 | + }; | |
159 | +} | ... | ... |
src/components/Upload/src/props.ts
0 → 100644
1 | +import type { PropType } from 'vue'; | |
2 | + | |
3 | +export const basicProps = { | |
4 | + helpText: { | |
5 | + type: String as PropType<string>, | |
6 | + default: '', | |
7 | + }, | |
8 | + // 文件最大多少MB | |
9 | + maxSize: { | |
10 | + type: Number as PropType<number>, | |
11 | + default: 2, | |
12 | + }, | |
13 | + // 最大数量的文件,0不限制 | |
14 | + maxNumber: { | |
15 | + type: Number as PropType<number>, | |
16 | + default: 0, | |
17 | + }, | |
18 | + // 根据后缀,或者其他 | |
19 | + accept: { | |
20 | + type: Array as PropType<string[]>, | |
21 | + default: () => [], | |
22 | + }, | |
23 | + multiple: { | |
24 | + type: Boolean, | |
25 | + default: true, | |
26 | + }, | |
27 | +}; | |
28 | + | |
29 | +export const uploadContainerProps = { | |
30 | + value: { | |
31 | + type: Array as PropType<string[]>, | |
32 | + default: () => [], | |
33 | + }, | |
34 | + ...basicProps, | |
35 | +}; | |
36 | + | |
37 | +export const priviewProps = { | |
38 | + value: { | |
39 | + type: Array as PropType<string[]>, | |
40 | + default: () => [], | |
41 | + }, | |
42 | +}; | ... | ... |
src/components/Upload/src/types.ts
0 → 100644
1 | +import { UploadApiResult } from '/@/api/demo/model/uploadModel'; | |
2 | + | |
3 | +export enum UploadResultStatus { | |
4 | + SUCCESS = 'success', | |
5 | + ERROR = 'error', | |
6 | + UPLOADING = 'uploading', | |
7 | +} | |
8 | + | |
9 | +export interface FileItem { | |
10 | + thumbUrl?: string; | |
11 | + name: string; | |
12 | + size: string | number; | |
13 | + type?: string; | |
14 | + percent: number; | |
15 | + file: File; | |
16 | + status?: UploadResultStatus; | |
17 | + responseData?: UploadApiResult; | |
18 | + uuid: string; | |
19 | +} | |
20 | + | |
21 | +export interface PreviewFileItem { | |
22 | + url: string; | |
23 | + name: string; | |
24 | + type: string; | |
25 | +} | ... | ... |
src/components/Upload/src/useUpload.ts
0 → 100644
1 | +import { Ref, unref, computed } from 'vue'; | |
2 | + | |
3 | +export function useUploadType({ | |
4 | + acceptRef, | |
5 | + // uploadTypeRef, | |
6 | + helpTextRef, | |
7 | + maxNumberRef, | |
8 | + maxSizeRef, | |
9 | +}: { | |
10 | + acceptRef: Ref<string[]>; | |
11 | + // uploadTypeRef: Ref<UploadTypeEnum>; | |
12 | + helpTextRef: Ref<string>; | |
13 | + maxNumberRef: Ref<number>; | |
14 | + maxSizeRef: Ref<number>; | |
15 | +}) { | |
16 | + // 文件类型限制 | |
17 | + const getAccept = computed(() => { | |
18 | + // const uploadType = unref(uploadTypeRef); | |
19 | + const accept = unref(acceptRef); | |
20 | + if (accept && accept.length > 0) { | |
21 | + return accept; | |
22 | + } | |
23 | + return []; | |
24 | + }); | |
25 | + const getStringAccept = computed(() => { | |
26 | + return unref(getAccept) | |
27 | + .map((item) => `.${item}`) | |
28 | + .join(','); | |
29 | + }); | |
30 | + // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。 | |
31 | + const getHelpText = computed(() => { | |
32 | + const helpText = unref(helpTextRef); | |
33 | + if (helpText) { | |
34 | + return helpText; | |
35 | + } | |
36 | + const helpTexts: string[] = []; | |
37 | + | |
38 | + const accept = unref(acceptRef); | |
39 | + if (accept.length > 0) { | |
40 | + helpTexts.push(`支持${accept.join(',')}格式`); | |
41 | + } | |
42 | + | |
43 | + const maxSize = unref(maxSizeRef); | |
44 | + if (maxSize) { | |
45 | + helpTexts.push(`不超过${maxSize}MB`); | |
46 | + } | |
47 | + | |
48 | + const maxNumber = unref(maxNumberRef); | |
49 | + if (maxNumber) { | |
50 | + helpTexts.push(`最多可选择${maxNumber}个文件`); | |
51 | + } | |
52 | + return helpTexts.join(','); | |
53 | + }); | |
54 | + return { getAccept, getStringAccept, getHelpText }; | |
55 | +} | ... | ... |
src/components/Upload/src/utils.ts
0 → 100644
1 | +export function checkFileType(file: File, accepts: string[]) { | |
2 | + const newTypes = accepts.join('|'); | |
3 | + // const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i; | |
4 | + const reg = new RegExp('\\.(' + newTypes + ')$', 'i'); | |
5 | + | |
6 | + if (!reg.test(file.name)) { | |
7 | + return false; | |
8 | + } else { | |
9 | + return true; | |
10 | + } | |
11 | +} | |
12 | +export function checkImgType(file: File) { | |
13 | + return /\.(jpg|jpeg|png|gif)$/i.test(file.name); | |
14 | +} | |
15 | +export function isImgTypeByName(name: string) { | |
16 | + return /\.(jpg|jpeg|png|gif)$/i.test(name); | |
17 | +} | |
18 | +export function getBase64WithFile(file: File) { | |
19 | + return new Promise<{ | |
20 | + result: string; | |
21 | + file: File; | |
22 | + }>((resolve, reject) => { | |
23 | + const reader = new FileReader(); | |
24 | + reader.readAsDataURL(file); | |
25 | + reader.onload = () => resolve({ result: reader.result as string, file }); | |
26 | + reader.onerror = (error) => reject(error); | |
27 | + }); | |
28 | +} | ... | ... |
src/router/menus/modules/demo/comp.ts
src/router/routes/modules/demo/comp.ts
... | ... | @@ -170,5 +170,13 @@ export default { |
170 | 170 | title: '密码强度组件', |
171 | 171 | }, |
172 | 172 | }, |
173 | + { | |
174 | + path: '/upload', | |
175 | + name: 'UploadDemo', | |
176 | + component: () => import('/@/views/demo/comp/upload/index.vue'), | |
177 | + meta: { | |
178 | + title: '上传组件', | |
179 | + }, | |
180 | + }, | |
173 | 181 | ], |
174 | 182 | } as AppRouteModule; | ... | ... |
src/utils/http/axios/Axios.ts
... | ... | @@ -5,9 +5,10 @@ import { AxiosCanceler } from './axiosCancel'; |
5 | 5 | import { isFunction } from '/@/utils/is'; |
6 | 6 | import { cloneDeep } from 'lodash-es'; |
7 | 7 | |
8 | -import type { RequestOptions, CreateAxiosOptions, Result } from './types'; | |
8 | +import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types'; | |
9 | 9 | // import { ContentTypeEnum } from '/@/enums/httpEnum'; |
10 | 10 | import { errorResult } from './const'; |
11 | +import { ContentTypeEnum } from '/@/enums/httpEnum'; | |
11 | 12 | |
12 | 13 | export * from './axiosTransform'; |
13 | 14 | |
... | ... | @@ -107,25 +108,42 @@ export class VAxios { |
107 | 108 | this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch); |
108 | 109 | } |
109 | 110 | |
110 | - // /** | |
111 | - // * @description: 文件上传 | |
112 | - // */ | |
113 | - // uploadFiles(config: AxiosRequestConfig, params: File[]) { | |
114 | - // const formData = new FormData(); | |
115 | - | |
116 | - // Object.keys(params).forEach((key) => { | |
117 | - // formData.append(key, params[key as any]); | |
118 | - // }); | |
119 | - | |
120 | - // return this.request({ | |
121 | - // ...config, | |
122 | - // method: 'POST', | |
123 | - // data: formData, | |
124 | - // headers: { | |
125 | - // 'Content-type': ContentTypeEnum.FORM_DATA, | |
126 | - // }, | |
127 | - // }); | |
128 | - // } | |
111 | + /** | |
112 | + * @description: 文件上传 | |
113 | + */ | |
114 | + uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) { | |
115 | + const formData = new window.FormData(); | |
116 | + | |
117 | + if (params.data) { | |
118 | + Object.keys(params.data).forEach((key) => { | |
119 | + if (!params.data) return; | |
120 | + const value = params.data[key]; | |
121 | + // support key-value array data | |
122 | + if (Array.isArray(value)) { | |
123 | + value.forEach((item) => { | |
124 | + // { list: [ 11, 22 ] } | |
125 | + // formData.append('list[]', 11); | |
126 | + formData.append(`${key}[]`, item); | |
127 | + }); | |
128 | + return; | |
129 | + } | |
130 | + | |
131 | + formData.append(key, params.data[key]); | |
132 | + }); | |
133 | + } | |
134 | + | |
135 | + formData.append(params.name || 'file', params.file, params.filename); | |
136 | + | |
137 | + return this.axiosInstance.request<T>({ | |
138 | + ...config, | |
139 | + method: 'POST', | |
140 | + data: formData, | |
141 | + headers: { | |
142 | + 'Content-type': ContentTypeEnum.FORM_DATA, | |
143 | + ignoreCancelToken: true, | |
144 | + }, | |
145 | + }); | |
146 | + } | |
129 | 147 | |
130 | 148 | /** |
131 | 149 | * @description: 请求方法 | ... | ... |
src/utils/http/axios/types.ts
... | ... | @@ -28,3 +28,14 @@ export interface Result<T = any> { |
28 | 28 | message: string; |
29 | 29 | result: T; |
30 | 30 | } |
31 | +// multipart/form-data:上传文件 | |
32 | +export interface UploadFileParams { | |
33 | + // 其他参数 | |
34 | + data?: { [key: string]: any }; | |
35 | + // 文件参数的接口字段名 | |
36 | + name?: string; | |
37 | + // 文件 | |
38 | + file: File | Blob; | |
39 | + // 文件名 | |
40 | + filename?: string; | |
41 | +} | ... | ... |
src/views/demo/comp/upload/index.vue
0 → 100644
1 | +<template> | |
2 | + <div class="p-4"> | |
3 | + <UploadContainer :maxSize="5" /> | |
4 | + </div> | |
5 | +</template> | |
6 | +<script lang="ts"> | |
7 | + import { defineComponent } from 'vue'; | |
8 | + import { UploadContainer } from '/@/components/Upload/index'; | |
9 | + | |
10 | + // import { Alert } from 'ant-design-vue'; | |
11 | + export default defineComponent({ | |
12 | + components: { UploadContainer }, | |
13 | + setup() { | |
14 | + return {}; | |
15 | + }, | |
16 | + }); | |
17 | +</script> | ... | ... |