From 746d4a745d06ff1f0eb42a2c2d09c539202bc91e Mon Sep 17 00:00:00 2001 From: jq <1151176506@qq.com> Date: Thu, 12 Nov 2020 00:06:12 +0800 Subject: [PATCH] wip: add upload component --- .env.development | 2 +- src/api/demo/model/uploadModel.ts | 5 + src/api/demo/upload.ts | 23 ++ src/components/Table/src/types/tableAction.ts | 1 + src/components/Upload/index.ts | 2 + src/components/Upload/src/ThumnUrl.vue | 29 +++ src/components/Upload/src/UploadContainer.vue | 62 +++++ src/components/Upload/src/UploadModal.vue | 244 ++++++++++++++++++ .../Upload/src/UploadPreviewModal.vue | 93 +++++++ src/components/Upload/src/data.tsx | 159 ++++++++++++ src/components/Upload/src/props.ts | 42 +++ src/components/Upload/src/types.ts | 25 ++ src/components/Upload/src/useUpload.ts | 55 ++++ src/components/Upload/src/utils.ts | 28 ++ src/router/menus/modules/demo/comp.ts | 4 + src/router/routes/modules/demo/comp.ts | 8 + src/utils/http/axios/Axios.ts | 54 ++-- src/utils/http/axios/types.ts | 11 + src/views/demo/comp/upload/index.vue | 17 ++ 19 files changed, 845 insertions(+), 19 deletions(-) create mode 100644 src/api/demo/model/uploadModel.ts create mode 100644 src/api/demo/upload.ts create mode 100644 src/components/Upload/index.ts create mode 100644 src/components/Upload/src/ThumnUrl.vue create mode 100644 src/components/Upload/src/UploadContainer.vue create mode 100644 src/components/Upload/src/UploadModal.vue create mode 100644 src/components/Upload/src/UploadPreviewModal.vue create mode 100644 src/components/Upload/src/data.tsx create mode 100644 src/components/Upload/src/props.ts create mode 100644 src/components/Upload/src/types.ts create mode 100644 src/components/Upload/src/useUpload.ts create mode 100644 src/components/Upload/src/utils.ts create mode 100644 src/views/demo/comp/upload/index.vue diff --git a/.env.development b/.env.development index 4dd7e787..6e117277 100644 --- a/.env.development +++ b/.env.development @@ -5,7 +5,7 @@ VITE_USE_MOCK = true VITE_PUBLIC_PATH = / # Cross-domain proxy, you can configure multiple -VITE_PROXY=[["/api","http://localhost:3000"]] +VITE_PROXY=[["/api","http://localhost:3000"],["/upload","http://localhost:3001/upload"]] # VITE_PROXY=[["/api","https://vvbin.cn/test"]] # Delete console diff --git a/src/api/demo/model/uploadModel.ts b/src/api/demo/model/uploadModel.ts new file mode 100644 index 00000000..d770c642 --- /dev/null +++ b/src/api/demo/model/uploadModel.ts @@ -0,0 +1,5 @@ +export interface UploadApiResult { + message: string; + code: number; + url: string; +} diff --git a/src/api/demo/upload.ts b/src/api/demo/upload.ts new file mode 100644 index 00000000..2871d939 --- /dev/null +++ b/src/api/demo/upload.ts @@ -0,0 +1,23 @@ +import { UploadApiResult } from './model/uploadModel'; +import { defHttp } from '/@/utils/http/axios'; +import { UploadFileParams } from '/@/utils/http/axios/types'; + +enum Api { + UPLOAD_URL = '/upload', +} + +/** + * @description: 上传接口 + */ +export function uploadApi( + params: UploadFileParams, + onUploadProgress: (progressEvent: ProgressEvent) => void +) { + return defHttp.uploadFile( + { + url: Api.UPLOAD_URL, + onUploadProgress, + }, + params + ); +} diff --git a/src/components/Table/src/types/tableAction.ts b/src/components/Table/src/types/tableAction.ts index f62c3d4d..14574466 100644 --- a/src/components/Table/src/types/tableAction.ts +++ b/src/components/Table/src/types/tableAction.ts @@ -1,5 +1,6 @@ export interface ActionItem { on?: any; + onClick?: any; label: string; disabled?: boolean; color?: 'success' | 'error' | 'warning'; diff --git a/src/components/Upload/index.ts b/src/components/Upload/index.ts new file mode 100644 index 00000000..50b6c5d9 --- /dev/null +++ b/src/components/Upload/index.ts @@ -0,0 +1,2 @@ +export { default as UploadContainer } from './src/UploadContainer.vue'; +// export * from './src/types'; diff --git a/src/components/Upload/src/ThumnUrl.vue b/src/components/Upload/src/ThumnUrl.vue new file mode 100644 index 00000000..38ef7c7a --- /dev/null +++ b/src/components/Upload/src/ThumnUrl.vue @@ -0,0 +1,29 @@ + + diff --git a/src/components/Upload/src/UploadContainer.vue b/src/components/Upload/src/UploadContainer.vue new file mode 100644 index 00000000..828a2dae --- /dev/null +++ b/src/components/Upload/src/UploadContainer.vue @@ -0,0 +1,62 @@ + + diff --git a/src/components/Upload/src/UploadModal.vue b/src/components/Upload/src/UploadModal.vue new file mode 100644 index 00000000..19bd12b8 --- /dev/null +++ b/src/components/Upload/src/UploadModal.vue @@ -0,0 +1,244 @@ + + + diff --git a/src/components/Upload/src/UploadPreviewModal.vue b/src/components/Upload/src/UploadPreviewModal.vue new file mode 100644 index 00000000..b1bb9fdc --- /dev/null +++ b/src/components/Upload/src/UploadPreviewModal.vue @@ -0,0 +1,93 @@ + + + diff --git a/src/components/Upload/src/data.tsx b/src/components/Upload/src/data.tsx new file mode 100644 index 00000000..a201cdb8 --- /dev/null +++ b/src/components/Upload/src/data.tsx @@ -0,0 +1,159 @@ +// import { BasicColumn, TableAction, ActionItem } from '@/components/table'; +import { checkImgType, isImgTypeByName } from './utils'; +// import ThumnUrl from './ThumbUrl.vue'; +import { Progress } from 'ant-design-vue'; +import { FileItem, PreviewFileItem, UploadResultStatus } from './types'; +// import { ElecArchivesSaveResult } from '@/api/biz/file/model/fileModel'; +// import { quryFile } from '@/api/biz/file/file'; +import { BasicColumn, ActionItem, TableAction } from '/@/components/Table/index'; + +// 文件上传列表 +export function createTableColumns(): BasicColumn[] { + return [ + { + dataIndex: 'thumbUrl', + title: '图例', + width: 100, + customRender: ({ record }) => { + const { thumbUrl, type } = (record as FileItem) || {}; + return {thumbUrl ? : type}; + // return ; + }, + }, + { + dataIndex: 'name', + title: '文件名', + align: 'left', + customRender: ({ text, record }) => { + const { percent, status: uploadStatus } = (record as FileItem) || {}; + let status = 'normal'; + if (uploadStatus === UploadResultStatus.ERROR) { + status = 'exception'; + } else if (uploadStatus === UploadResultStatus.UPLOADING) { + status = 'active'; + } else if (uploadStatus === UploadResultStatus.SUCCESS) { + status = 'success'; + } + return ( + +

+ {text} +

+ +
+ ); + }, + }, + { + dataIndex: 'size', + title: '文件大小', + width: 100, + customRender: ({ text = 0 }) => { + return text && (text / 1024).toFixed(2) + 'KB'; + }, + }, + // { + // dataIndex: 'type', + // title: '文件类型', + // width: 100, + // }, + { + dataIndex: 'status', + title: '状态', + width: 100, + customRender: ({ text }) => { + if (text === UploadResultStatus.SUCCESS) { + return '上传成功'; + } else if (text === UploadResultStatus.ERROR) { + return '上传失败'; + } else if (text === UploadResultStatus.UPLOADING) { + return '上传中'; + } + + return text; + }, + }, + ]; +} +export function createActionColumn(handleRemove: Function, handlePreview: Function): BasicColumn { + return { + width: 120, + title: '操作', + dataIndex: 'action', + fixed: false, + customRender: ({ record }) => { + const actions: ActionItem[] = [ + { + label: '删除', + onClick: handleRemove.bind(null, record), + }, + ]; + if (checkImgType(record)) { + actions.unshift({ + label: '预览', + onClick: handlePreview.bind(null, record), + }); + } + return ; + }, + }; +} +// 文件预览列表 +export function createPreviewColumns(): BasicColumn[] { + return [ + { + dataIndex: 'url', + title: '图例', + width: 100, + customRender: ({ record }) => { + const { url, type } = (record as PreviewFileItem) || {}; + return ( + {isImgTypeByName(url) ? : type} + ); + }, + }, + { + dataIndex: 'name', + title: '文件名', + align: 'left', + }, + ]; +} + +export function createPreviewActionColumn({ + handleRemove, + handlePreview, + handleDownload, +}: { + handleRemove: Function; + handlePreview: Function; + handleDownload: Function; +}): BasicColumn { + return { + width: 160, + title: '操作', + dataIndex: 'action', + fixed: false, + customRender: ({ record }) => { + const { url } = (record as PreviewFileItem) || {}; + + const actions: ActionItem[] = [ + { + label: '删除', + onClick: handleRemove.bind(null, record), + }, + { + label: '下载', + onClick: handleDownload.bind(null, record), + }, + ]; + if (isImgTypeByName(url)) { + actions.unshift({ + label: '预览', + onClick: handlePreview.bind(null, record), + }); + } + return ; + }, + }; +} diff --git a/src/components/Upload/src/props.ts b/src/components/Upload/src/props.ts new file mode 100644 index 00000000..6bf3d908 --- /dev/null +++ b/src/components/Upload/src/props.ts @@ -0,0 +1,42 @@ +import type { PropType } from 'vue'; + +export const basicProps = { + helpText: { + type: String as PropType, + default: '', + }, + // 文件最大多少MB + maxSize: { + type: Number as PropType, + default: 2, + }, + // 最大数量的文件,0不限制 + maxNumber: { + type: Number as PropType, + default: 0, + }, + // 根据后缀,或者其他 + accept: { + type: Array as PropType, + default: () => [], + }, + multiple: { + type: Boolean, + default: true, + }, +}; + +export const uploadContainerProps = { + value: { + type: Array as PropType, + default: () => [], + }, + ...basicProps, +}; + +export const priviewProps = { + value: { + type: Array as PropType, + default: () => [], + }, +}; diff --git a/src/components/Upload/src/types.ts b/src/components/Upload/src/types.ts new file mode 100644 index 00000000..48c0a658 --- /dev/null +++ b/src/components/Upload/src/types.ts @@ -0,0 +1,25 @@ +import { UploadApiResult } from '/@/api/demo/model/uploadModel'; + +export enum UploadResultStatus { + SUCCESS = 'success', + ERROR = 'error', + UPLOADING = 'uploading', +} + +export interface FileItem { + thumbUrl?: string; + name: string; + size: string | number; + type?: string; + percent: number; + file: File; + status?: UploadResultStatus; + responseData?: UploadApiResult; + uuid: string; +} + +export interface PreviewFileItem { + url: string; + name: string; + type: string; +} diff --git a/src/components/Upload/src/useUpload.ts b/src/components/Upload/src/useUpload.ts new file mode 100644 index 00000000..aa2c7063 --- /dev/null +++ b/src/components/Upload/src/useUpload.ts @@ -0,0 +1,55 @@ +import { Ref, unref, computed } from 'vue'; + +export function useUploadType({ + acceptRef, + // uploadTypeRef, + helpTextRef, + maxNumberRef, + maxSizeRef, +}: { + acceptRef: Ref; + // uploadTypeRef: Ref; + helpTextRef: Ref; + maxNumberRef: Ref; + maxSizeRef: Ref; +}) { + // 文件类型限制 + const getAccept = computed(() => { + // const uploadType = unref(uploadTypeRef); + const accept = unref(acceptRef); + if (accept && accept.length > 0) { + return accept; + } + return []; + }); + const getStringAccept = computed(() => { + return unref(getAccept) + .map((item) => `.${item}`) + .join(','); + }); + // 支持jpg、jpeg、png格式,不超过2M,最多可选择10张图片,。 + const getHelpText = computed(() => { + const helpText = unref(helpTextRef); + if (helpText) { + return helpText; + } + const helpTexts: string[] = []; + + const accept = unref(acceptRef); + if (accept.length > 0) { + helpTexts.push(`支持${accept.join(',')}格式`); + } + + const maxSize = unref(maxSizeRef); + if (maxSize) { + helpTexts.push(`不超过${maxSize}MB`); + } + + const maxNumber = unref(maxNumberRef); + if (maxNumber) { + helpTexts.push(`最多可选择${maxNumber}个文件`); + } + return helpTexts.join(','); + }); + return { getAccept, getStringAccept, getHelpText }; +} diff --git a/src/components/Upload/src/utils.ts b/src/components/Upload/src/utils.ts new file mode 100644 index 00000000..3a171e85 --- /dev/null +++ b/src/components/Upload/src/utils.ts @@ -0,0 +1,28 @@ +export function checkFileType(file: File, accepts: string[]) { + const newTypes = accepts.join('|'); + // const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i; + const reg = new RegExp('\\.(' + newTypes + ')$', 'i'); + + if (!reg.test(file.name)) { + return false; + } else { + return true; + } +} +export function checkImgType(file: File) { + return /\.(jpg|jpeg|png|gif)$/i.test(file.name); +} +export function isImgTypeByName(name: string) { + return /\.(jpg|jpeg|png|gif)$/i.test(name); +} +export function getBase64WithFile(file: File) { + return new Promise<{ + result: string; + file: File; + }>((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve({ result: reader.result as string, file }); + reader.onerror = (error) => reject(error); + }); +} diff --git a/src/router/menus/modules/demo/comp.ts b/src/router/menus/modules/demo/comp.ts index 2b5b5443..816ab976 100644 --- a/src/router/menus/modules/demo/comp.ts +++ b/src/router/menus/modules/demo/comp.ts @@ -38,6 +38,10 @@ const menu: MenuModule = { path: 'strength-meter', name: '密码强度组件', }, + { + path: 'upload', + name: '上传组件', + }, { path: 'scroll', name: '滚动组件', diff --git a/src/router/routes/modules/demo/comp.ts b/src/router/routes/modules/demo/comp.ts index 3f8483b0..f9f013e0 100644 --- a/src/router/routes/modules/demo/comp.ts +++ b/src/router/routes/modules/demo/comp.ts @@ -170,5 +170,13 @@ export default { title: '密码强度组件', }, }, + { + path: '/upload', + name: 'UploadDemo', + component: () => import('/@/views/demo/comp/upload/index.vue'), + meta: { + title: '上传组件', + }, + }, ], } as AppRouteModule; diff --git a/src/utils/http/axios/Axios.ts b/src/utils/http/axios/Axios.ts index b988d194..06ee31ba 100644 --- a/src/utils/http/axios/Axios.ts +++ b/src/utils/http/axios/Axios.ts @@ -5,9 +5,10 @@ import { AxiosCanceler } from './axiosCancel'; import { isFunction } from '/@/utils/is'; import { cloneDeep } from 'lodash-es'; -import type { RequestOptions, CreateAxiosOptions, Result } from './types'; +import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types'; // import { ContentTypeEnum } from '/@/enums/httpEnum'; import { errorResult } from './const'; +import { ContentTypeEnum } from '/@/enums/httpEnum'; export * from './axiosTransform'; @@ -107,25 +108,42 @@ export class VAxios { this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch); } - // /** - // * @description: 文件上传 - // */ - // uploadFiles(config: AxiosRequestConfig, params: File[]) { - // const formData = new FormData(); + /** + * @description: 文件上传 + */ + uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { + const formData = new window.FormData(); - // Object.keys(params).forEach((key) => { - // formData.append(key, params[key as any]); - // }); + if (params.data) { + Object.keys(params.data).forEach((key) => { + if (!params.data) return; + const value = params.data[key]; + // support key-value array data + if (Array.isArray(value)) { + value.forEach((item) => { + // { list: [ 11, 22 ] } + // formData.append('list[]', 11); + formData.append(`${key}[]`, item); + }); + return; + } - // return this.request({ - // ...config, - // method: 'POST', - // data: formData, - // headers: { - // 'Content-type': ContentTypeEnum.FORM_DATA, - // }, - // }); - // } + formData.append(key, params.data[key]); + }); + } + + formData.append(params.name || 'file', params.file, params.filename); + + return this.axiosInstance.request({ + ...config, + method: 'POST', + data: formData, + headers: { + 'Content-type': ContentTypeEnum.FORM_DATA, + ignoreCancelToken: true, + }, + }); + } /** * @description: 请求方法 diff --git a/src/utils/http/axios/types.ts b/src/utils/http/axios/types.ts index 8345ca50..c6d10946 100644 --- a/src/utils/http/axios/types.ts +++ b/src/utils/http/axios/types.ts @@ -28,3 +28,14 @@ export interface Result { message: string; result: T; } +// multipart/form-data:上传文件 +export interface UploadFileParams { + // 其他参数 + data?: { [key: string]: any }; + // 文件参数的接口字段名 + name?: string; + // 文件 + file: File | Blob; + // 文件名 + filename?: string; +} diff --git a/src/views/demo/comp/upload/index.vue b/src/views/demo/comp/upload/index.vue new file mode 100644 index 00000000..d15d3a23 --- /dev/null +++ b/src/views/demo/comp/upload/index.vue @@ -0,0 +1,17 @@ + +