From 41fda26248ba44e5cdba00971146dfcb09e3a881 Mon Sep 17 00:00:00 2001 From: dap <15891557205@163.com> Date: Sun, 6 Oct 2024 12:19:24 +0800 Subject: [PATCH] feat: image upload component --- apps/web-antd/package.json | 1 + apps/web-antd/src/adapter/form.ts | 3 + apps/web-antd/src/api/core/upload.ts | 8 +- apps/web-antd/src/components/upload/index.ts | 1 + .../src/components/upload/src/helper.ts | 32 +++ .../components/upload/src/image-upload.vue | 261 ++++++++++++++++++ .../src/components/upload/src/typing.ts | 37 +++ .../src/components/upload/src/use-upload.ts | 60 ++++ apps/web-antd/src/locales/langs/en-US.json | 29 ++ apps/web-antd/src/locales/langs/zh-CN.json | 29 ++ .../views/演示使用自行删除/upload/index.vue | 30 ++ pnpm-lock.yaml | 14 +- 12 files changed, 495 insertions(+), 10 deletions(-) create mode 100644 apps/web-antd/src/components/upload/index.ts create mode 100644 apps/web-antd/src/components/upload/src/helper.ts create mode 100644 apps/web-antd/src/components/upload/src/image-upload.vue create mode 100644 apps/web-antd/src/components/upload/src/typing.ts create mode 100644 apps/web-antd/src/components/upload/src/use-upload.ts create mode 100644 apps/web-antd/src/views/演示使用自行删除/upload/index.vue diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 48a8d727..2d22f369 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -26,6 +26,7 @@ "#/*": "./src/*" }, "dependencies": { + "@ant-design/icons-vue": "^7.0.1", "@tinymce/tinymce-vue": "^6.0.1", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", diff --git a/apps/web-antd/src/adapter/form.ts b/apps/web-antd/src/adapter/form.ts index 3a9352fb..2bae74ac 100644 --- a/apps/web-antd/src/adapter/form.ts +++ b/apps/web-antd/src/adapter/form.ts @@ -36,6 +36,7 @@ import { import { isArray } from 'lodash-es'; import { Tinymce as RichTextarea } from '#/components/tinymce'; +import { ImageUpload } from '#/components/upload'; // 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明 export type FormComponentType = @@ -44,6 +45,7 @@ export type FormComponentType = | 'CheckboxGroup' | 'DatePicker' | 'Divider' + | 'ImageUpload' | 'Input' | 'InputNumber' | 'InputPassword' @@ -104,6 +106,7 @@ setupVbenForm({ TimePicker, TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'), Upload, + ImageUpload, }, config: { // ant design vue组件库默认都是 v-model:value diff --git a/apps/web-antd/src/api/core/upload.ts b/apps/web-antd/src/api/core/upload.ts index aeb0328b..a7b7f91b 100644 --- a/apps/web-antd/src/api/core/upload.ts +++ b/apps/web-antd/src/api/core/upload.ts @@ -1,7 +1,13 @@ import { requestClient } from '#/api/request'; +/** + * 通过单文件上传接口 + * @param file 上传的文件 + * @returns 上传结果 + */ export function uploadApi(file: Blob | File) { - return requestClient.upload('/resource/oss/upload', file); + console.log('uploadApi', file); + return requestClient.upload('/resource/oss/upload', { file }); } /** * 默认上传结果 diff --git a/apps/web-antd/src/components/upload/index.ts b/apps/web-antd/src/components/upload/index.ts new file mode 100644 index 00000000..89fac34a --- /dev/null +++ b/apps/web-antd/src/components/upload/index.ts @@ -0,0 +1 @@ +export { default as ImageUpload } from './src/image-upload.vue'; diff --git a/apps/web-antd/src/components/upload/src/helper.ts b/apps/web-antd/src/components/upload/src/helper.ts new file mode 100644 index 00000000..7c7c0078 --- /dev/null +++ b/apps/web-antd/src/components/upload/src/helper.ts @@ -0,0 +1,32 @@ +export function checkFileType(file: File, accepts: string[]) { + let reg; + if (!accepts || accepts.length === 0) { + reg = /.(?:jpg|jpeg|png|gif|webp)$/i; + } else { + const newTypes = accepts.join('|'); + reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i'); + } + return reg.test(file.name); +} + +export function checkImgType(file: File) { + return isImgTypeByName(file.name); +} + +export function isImgTypeByName(name: string) { + return /\.(?:jpg|jpeg|png|gif|webp)$/i.test(name); +} + +export function getBase64WithFile(file: File) { + return new Promise<{ + file: File; + result: string; + }>((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.addEventListener('load', () => + resolve({ result: reader.result as string, file }), + ); + reader.addEventListener('error', (error) => reject(error)); + }); +} diff --git a/apps/web-antd/src/components/upload/src/image-upload.vue b/apps/web-antd/src/components/upload/src/image-upload.vue new file mode 100644 index 00000000..aae5c37c --- /dev/null +++ b/apps/web-antd/src/components/upload/src/image-upload.vue @@ -0,0 +1,261 @@ + + + + + diff --git a/apps/web-antd/src/components/upload/src/typing.ts b/apps/web-antd/src/components/upload/src/typing.ts new file mode 100644 index 00000000..8f87fe28 --- /dev/null +++ b/apps/web-antd/src/components/upload/src/typing.ts @@ -0,0 +1,37 @@ +import type { Recordable } from '@vben/types'; + +export enum UploadResultStatus { + DONE = 'done', + ERROR = 'error', + SUCCESS = 'success', + UPLOADING = 'uploading', +} + +export interface FileItem { + thumbUrl?: string; + name: string; + size: number | string; + type?: string; + percent: number; + file: File; + status?: UploadResultStatus; + response?: { fileName: string; ossId: string; url: string } | Recordable; + uuid: string; +} + +export interface Wrapper { + record: FileItem; + uidKey: string; + valueKey: string; +} + +export interface BaseFileItem { + uid: number | string; + url: string; + name?: string; +} +export interface PreviewFileItem { + url: string; + name: string; + type: string; +} diff --git a/apps/web-antd/src/components/upload/src/use-upload.ts b/apps/web-antd/src/components/upload/src/use-upload.ts new file mode 100644 index 00000000..b8710f14 --- /dev/null +++ b/apps/web-antd/src/components/upload/src/use-upload.ts @@ -0,0 +1,60 @@ +import { computed, unref } from 'vue'; +import type { Ref } from 'vue'; + +import { $t } from '@vben/locales'; + +export function useUploadType({ + acceptRef, + helpTextRef, + maxNumberRef, + maxSizeRef, +}: { + acceptRef: Ref; + helpTextRef: Ref; + maxNumberRef: Ref; + maxSizeRef: Ref; +}) { + // 文件类型限制 + const getAccept = computed(() => { + const accept = unref(acceptRef); + if (accept && accept.length > 0) { + return accept; + } + return []; + }); + const getStringAccept = computed(() => { + return unref(getAccept) + .map((item) => { + return item.indexOf('/') > 0 || item.startsWith('.') + ? 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($t('component.upload.accept', [accept.join(',')])); + } + + const maxSize = unref(maxSizeRef); + if (maxSize) { + helpTexts.push($t('component.upload.maxSize', [maxSize])); + } + + const maxNumber = unref(maxNumberRef); + if (maxNumber && maxNumber !== Infinity) { + helpTexts.push($t('component.upload.maxNumber', [maxNumber])); + } + return helpTexts.join(','); + }); + return { getAccept, getStringAccept, getHelpText }; +} diff --git a/apps/web-antd/src/locales/langs/en-US.json b/apps/web-antd/src/locales/langs/en-US.json index 7049c320..45590224 100644 --- a/apps/web-antd/src/locales/langs/en-US.json +++ b/apps/web-antd/src/locales/langs/en-US.json @@ -29,6 +29,35 @@ "notice": { "title": "Notice", "received": "You have received a new message" + }, + "upload": { + "save": "Save", + "upload": "Upload", + "imgUpload": "ImageUpload", + "uploaded": "Uploaded", + "operating": "Operating", + "del": "Delete", + "download": "download", + "saveWarn": "Please wait for the file to upload and save!", + "saveError": "There is no file successfully uploaded and cannot be saved!", + "preview": "Preview", + "choose": "Select the file", + "accept": "Support {0} format", + "acceptUpload": "Only upload files in {0} format", + "maxSize": "A single file does not exceed {0}MB ", + "maxSizeMultiple": "Only upload files up to {0}MB!", + "maxNumber": "Only upload up to {0} files", + "legend": "Legend", + "fileName": "File name", + "fileSize": "File size", + "fileStatue": "File status", + "pending": "Pending", + "startUpload": "Start upload", + "uploadSuccess": "Upload successfully", + "uploadError": "Upload failed", + "uploading": "Uploading", + "uploadWait": "Please wait for the file upload to finish", + "reUploadFailed": "Re-upload failed files" } }, "pages": { diff --git a/apps/web-antd/src/locales/langs/zh-CN.json b/apps/web-antd/src/locales/langs/zh-CN.json index 5c6e4986..c0df070b 100644 --- a/apps/web-antd/src/locales/langs/zh-CN.json +++ b/apps/web-antd/src/locales/langs/zh-CN.json @@ -29,6 +29,35 @@ "notice": { "title": "消息", "received": "收到新消息" + }, + "upload": { + "save": "保存", + "upload": "上传", + "imgUpload": "图片上传", + "uploaded": "已上传", + "operating": "操作", + "del": "删除", + "download": "下载", + "saveWarn": "请等待文件上传后,保存!", + "saveError": "没有上传成功的文件,无法保存!", + "preview": "预览", + "choose": "选择文件", + "accept": "支持{0}格式", + "acceptUpload": "只能上传{0}格式文件", + "maxSize": "单个文件不超过{0}MB", + "maxSizeMultiple": "只能上传不超过{0}MB的文件!", + "maxNumber": "最多只能上传{0}个文件", + "legend": "略缩图", + "fileName": "文件名", + "fileSize": "文件大小", + "fileStatue": "状态", + "pending": "待上传", + "startUpload": "开始上传", + "uploadSuccess": "上传成功", + "uploadError": "上传失败", + "uploading": "上传中", + "uploadWait": "请等待文件上传结束后操作", + "reUploadFailed": "重新上传失败文件" } }, "pages": { diff --git a/apps/web-antd/src/views/演示使用自行删除/upload/index.vue b/apps/web-antd/src/views/演示使用自行删除/upload/index.vue new file mode 100644 index 00000000..0b3f8851 --- /dev/null +++ b/apps/web-antd/src/views/演示使用自行删除/upload/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea3723f4..8f9c2abf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -621,6 +621,9 @@ importers: apps/web-antd: dependencies: + '@ant-design/icons-vue': + specifier: ^7.0.1 + version: 7.0.1(vue@3.5.11(typescript@5.6.2)) '@tinymce/tinymce-vue': specifier: ^6.0.1 version: 6.0.1(vue@3.5.11(typescript@5.6.2)) @@ -5926,9 +5929,6 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} - core-js-compat@3.38.0: - resolution: {integrity: sha512-75LAicdLa4OJVwFxFbQR3NdnZjNgX6ILpVcVzcC4T2smerB5lELMrJQQQoWV6TiuC/vlaFqgU2tKQx9w5s0e0A==} - core-js-compat@3.38.1: resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} @@ -12029,7 +12029,7 @@ snapshots: babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.25.2) babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.25.2) babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.25.2) - core-js-compat: 3.38.0 + core-js-compat: 3.38.1 semver: 6.3.1 transitivePeerDependencies: - supports-color @@ -15176,7 +15176,7 @@ snapshots: dependencies: '@babel/core': 7.25.2 '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.25.2) - core-js-compat: 3.38.0 + core-js-compat: 3.38.1 transitivePeerDependencies: - supports-color @@ -15645,10 +15645,6 @@ snapshots: dependencies: is-what: 4.1.16 - core-js-compat@3.38.0: - dependencies: - browserslist: 4.23.3 - core-js-compat@3.38.1: dependencies: browserslist: 4.23.3