diff --git a/.env.development b/.env.development index 4dd7e7873a75e13a5a0caea4aa8ad6cce4601b4f..6e1172779473285373bc0f8c926bf93d1e4ad303 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 0000000000000000000000000000000000000000..d770c642b43f2d65eaf8857982e7e3127e1355cf --- /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 0000000000000000000000000000000000000000..2871d93942f785de357189ce5d7946ed40cbf7fe --- /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 f62c3d4deb242d0c490bb260b83ce330e8042e32..14574466c84b2a993f0caa98768c9bfd596e4292 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 0000000000000000000000000000000000000000..50b6c5d914bdac54b214a05011d6af9c7c1e0bc4 --- /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 0000000000000000000000000000000000000000..38ef7c7a23076a1021f5a8cb3c78bf8378595ce3 --- /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 0000000000000000000000000000000000000000..828a2daec20b4c3491a1d4e5da5d3406b494dbe7 --- /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 0000000000000000000000000000000000000000..19bd12b8b507c91acc9295073708222a122f944a --- /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 0000000000000000000000000000000000000000..b1bb9fdc63d2a0966e3711b08611c04654177736 --- /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 0000000000000000000000000000000000000000..a201cdb8c0c505a4e431d88c1e1e4b2ae6ba1703 --- /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 0000000000000000000000000000000000000000..6bf3d908fa255a1220cd15338e1a376d7e2ff3be --- /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 0000000000000000000000000000000000000000..48c0a65831ab33f12f42981d620ba3cc33ff4956 --- /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 0000000000000000000000000000000000000000..aa2c70631b0842a6db0df2caaf8b5d0fee8e6732 --- /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 0000000000000000000000000000000000000000..3a171e8507a67d24016223c4f9b398eb2603d505 --- /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 2b5b5443b40caca435c20a0c72d824648820843f..816ab9769a42ea1baecc691ffd3d27786ab412be 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 3f8483b01ea3dd1815c280aa79ef9c0ae60c9c6d..f9f013e0a5437fd07c8c2f19b7244105f4647f1a 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 b988d1942845ce45704962d71130697403e53125..06ee31bad13f1b9d0fe4d45b8edaca810da32b10 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(); - - // Object.keys(params).forEach((key) => { - // formData.append(key, params[key as any]); - // }); - - // return this.request({ - // ...config, - // method: 'POST', - // data: formData, - // headers: { - // 'Content-type': ContentTypeEnum.FORM_DATA, - // }, - // }); - // } + /** + * @description: 文件上传 + */ + uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { + const formData = new window.FormData(); + + 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; + } + + 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 8345ca50a52c184894d6d0c897b1f441ea3b7e51..c6d1094681ee8da29351190a520ea1519300f7e0 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 0000000000000000000000000000000000000000..d15d3a23232585fe978c31864cf7f3473d0c8f11 --- /dev/null +++ b/src/views/demo/comp/upload/index.vue @@ -0,0 +1,17 @@ + +