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,7 +5,7 @@ VITE_USE_MOCK = true | ||
5 | VITE_PUBLIC_PATH = / | 5 | VITE_PUBLIC_PATH = / |
6 | 6 | ||
7 | # Cross-domain proxy, you can configure multiple | 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 | # VITE_PROXY=[["/api","https://vvbin.cn/test"]] | 9 | # VITE_PROXY=[["/api","https://vvbin.cn/test"]] |
10 | 10 | ||
11 | # Delete console | 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
@@ -39,6 +39,10 @@ const menu: MenuModule = { | @@ -39,6 +39,10 @@ const menu: MenuModule = { | ||
39 | name: '密码强度组件', | 39 | name: '密码强度组件', |
40 | }, | 40 | }, |
41 | { | 41 | { |
42 | + path: 'upload', | ||
43 | + name: '上传组件', | ||
44 | + }, | ||
45 | + { | ||
42 | path: 'scroll', | 46 | path: 'scroll', |
43 | name: '滚动组件', | 47 | name: '滚动组件', |
44 | children: [ | 48 | children: [ |
src/router/routes/modules/demo/comp.ts
@@ -170,5 +170,13 @@ export default { | @@ -170,5 +170,13 @@ export default { | ||
170 | title: '密码强度组件', | 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 | } as AppRouteModule; | 182 | } as AppRouteModule; |
src/utils/http/axios/Axios.ts
@@ -5,9 +5,10 @@ import { AxiosCanceler } from './axiosCancel'; | @@ -5,9 +5,10 @@ import { AxiosCanceler } from './axiosCancel'; | ||
5 | import { isFunction } from '/@/utils/is'; | 5 | import { isFunction } from '/@/utils/is'; |
6 | import { cloneDeep } from 'lodash-es'; | 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 | // import { ContentTypeEnum } from '/@/enums/httpEnum'; | 9 | // import { ContentTypeEnum } from '/@/enums/httpEnum'; |
10 | import { errorResult } from './const'; | 10 | import { errorResult } from './const'; |
11 | +import { ContentTypeEnum } from '/@/enums/httpEnum'; | ||
11 | 12 | ||
12 | export * from './axiosTransform'; | 13 | export * from './axiosTransform'; |
13 | 14 | ||
@@ -107,25 +108,42 @@ export class VAxios { | @@ -107,25 +108,42 @@ export class VAxios { | ||
107 | this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch); | 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 | * @description: 请求方法 | 149 | * @description: 请求方法 |
src/utils/http/axios/types.ts
@@ -28,3 +28,14 @@ export interface Result<T = any> { | @@ -28,3 +28,14 @@ export interface Result<T = any> { | ||
28 | message: string; | 28 | message: string; |
29 | result: T; | 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> |