From 1a6eb9cdbc22a15c7375756f76226d21cda1106b Mon Sep 17 00:00:00 2001 From: colin Date: Wed, 8 Oct 2025 11:31:07 +0800 Subject: [PATCH] feat(vben5): Avatar upload supports cropping - Add `cropper.js ` component integration. - Add Component locales. - Reconstruct personal Settings for uploading avatars and support avatar cropping. --- apps/vben5/apps/app-antd/package.json | 1 + apps/vben5/apps/app-antd/src/locales/index.ts | 10 +- apps/vben5/packages/@abp/account/package.json | 1 + .../components/components/BasicSettings.vue | 55 ++-- .../packages/@abp/components/package.json | 9 + .../@abp/components/src/cropper/Cropper.vue | 200 ++++++++++++ .../components/src/cropper/CropperAvatar.vue | 159 +++++++++ .../components/src/cropper/CropperModal.vue | 309 ++++++++++++++++++ .../@abp/components/src/cropper/index.ts | 2 + .../@abp/components/src/cropper/types.ts | 8 + .../@abp/components/src/locales/index.ts | 20 ++ .../src/locales/langs/en-US/cropper.json | 14 + .../src/locales/langs/zh-CN/cropper.json | 14 + .../packages/@abp/core/src/utils/file.ts | 42 +++ .../packages/@abp/core/src/utils/index.ts | 1 + apps/vben5/pnpm-workspace.yaml | 1 + 16 files changed, 814 insertions(+), 32 deletions(-) create mode 100644 apps/vben5/packages/@abp/components/src/cropper/Cropper.vue create mode 100644 apps/vben5/packages/@abp/components/src/cropper/CropperAvatar.vue create mode 100644 apps/vben5/packages/@abp/components/src/cropper/CropperModal.vue create mode 100644 apps/vben5/packages/@abp/components/src/cropper/index.ts create mode 100644 apps/vben5/packages/@abp/components/src/cropper/types.ts create mode 100644 apps/vben5/packages/@abp/components/src/locales/index.ts create mode 100644 apps/vben5/packages/@abp/components/src/locales/langs/en-US/cropper.json create mode 100644 apps/vben5/packages/@abp/components/src/locales/langs/zh-CN/cropper.json create mode 100644 apps/vben5/packages/@abp/core/src/utils/file.ts diff --git a/apps/vben5/apps/app-antd/package.json b/apps/vben5/apps/app-antd/package.json index efbe91733..6b1852596 100644 --- a/apps/vben5/apps/app-antd/package.json +++ b/apps/vben5/apps/app-antd/package.json @@ -28,6 +28,7 @@ "dependencies": { "@abp/account": "workspace:*", "@abp/auditing": "workspace:*", + "@abp/components": "workspace:*", "@abp/core": "workspace:*", "@abp/data-protection": "workspace:*", "@abp/demo": "workspace:*", diff --git a/apps/vben5/apps/app-antd/src/locales/index.ts b/apps/vben5/apps/app-antd/src/locales/index.ts index 7fe299ca0..434984cfd 100644 --- a/apps/vben5/apps/app-antd/src/locales/index.ts +++ b/apps/vben5/apps/app-antd/src/locales/index.ts @@ -13,6 +13,7 @@ import { } from '@vben/locales'; import { preferences } from '@vben/preferences'; +import { loadComponentMessages } from '@abp/components/locales'; import { useAbpStore } from '@abp/core'; import { useLocalizationsApi } from '@abp/localization'; import { loadPaltformMessages } from '@abp/platform'; @@ -35,16 +36,17 @@ const localesMap = loadLocalesMapFromDir( * @param lang */ async function loadMessages(lang: SupportedLanguagesType) { - const [appLocaleMessages, platformLocales, _, abpLocales] = await Promise.all( - [ + const [appLocaleMessages, compLocales, platformLocales, _, abpLocales] = + await Promise.all([ localesMap[lang]?.(), + loadComponentMessages(lang), loadPaltformMessages(lang), loadThirdPartyMessage(lang), loadAbpLocale(lang), - ], - ); + ]); return { ...appLocaleMessages?.default, + ...compLocales?.default, ...platformLocales?.default, ...abpLocales, }; diff --git a/apps/vben5/packages/@abp/account/package.json b/apps/vben5/packages/@abp/account/package.json index 0b92c7213..c742f9666 100644 --- a/apps/vben5/packages/@abp/account/package.json +++ b/apps/vben5/packages/@abp/account/package.json @@ -20,6 +20,7 @@ } }, "dependencies": { + "@abp/components": "workspace:*", "@abp/core": "workspace:*", "@abp/gdpr": "workspace:*", "@abp/identity": "workspace:*", diff --git a/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue b/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue index bacd00aea..9f80295fa 100644 --- a/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue +++ b/apps/vben5/packages/@abp/account/src/components/components/BasicSettings.vue @@ -1,7 +1,4 @@ + + diff --git a/apps/vben5/packages/@abp/components/src/cropper/CropperAvatar.vue b/apps/vben5/packages/@abp/components/src/cropper/CropperAvatar.vue new file mode 100644 index 000000000..c420f8d9b --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/cropper/CropperAvatar.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/apps/vben5/packages/@abp/components/src/cropper/CropperModal.vue b/apps/vben5/packages/@abp/components/src/cropper/CropperModal.vue new file mode 100644 index 000000000..f8e263e56 --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/cropper/CropperModal.vue @@ -0,0 +1,309 @@ + + + + + diff --git a/apps/vben5/packages/@abp/components/src/cropper/index.ts b/apps/vben5/packages/@abp/components/src/cropper/index.ts new file mode 100644 index 000000000..8708c5109 --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/cropper/index.ts @@ -0,0 +1,2 @@ +export { default as CropperAvatar } from './CropperAvatar.vue'; +export { default as CropperModal } from './CropperModal.vue'; diff --git a/apps/vben5/packages/@abp/components/src/cropper/types.ts b/apps/vben5/packages/@abp/components/src/cropper/types.ts new file mode 100644 index 000000000..e76cc6f8e --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/cropper/types.ts @@ -0,0 +1,8 @@ +import type Cropper from 'cropperjs'; + +export interface CropendResult { + imgBase64: string; + imgInfo: Cropper.Data; +} + +export type { Cropper }; diff --git a/apps/vben5/packages/@abp/components/src/locales/index.ts b/apps/vben5/packages/@abp/components/src/locales/index.ts new file mode 100644 index 000000000..de3bd1b24 --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/locales/index.ts @@ -0,0 +1,20 @@ +import type { SupportedLanguagesType } from '@vben/locales'; + +import { loadLocalesMapFromDir } from '@vben/locales'; + +const modules = import.meta.glob('./langs/**/*.json'); + +const localesMap = loadLocalesMapFromDir( + /\.\/langs\/([^/]+)\/(.*)\.json$/, + modules, +); + +/** + * 加载自定义组件本地化资源 + * @param lang 当前语言 + * @returns 资源集合 + */ +export async function loadComponentMessages(lang: SupportedLanguagesType) { + const locales = localesMap[lang]?.(); + return locales; +} diff --git a/apps/vben5/packages/@abp/components/src/locales/langs/en-US/cropper.json b/apps/vben5/packages/@abp/components/src/locales/langs/en-US/cropper.json new file mode 100644 index 000000000..5ad017f87 --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/locales/langs/en-US/cropper.json @@ -0,0 +1,14 @@ +{ + "confirmText": "Confirm and upload", + "title": "Avatar upload", + "selectImage": "Select Image", + "btn_rotate_left": "Counterclockwise rotation", + "btn_rotate_right": "Clockwise rotation", + "btn_scale_x": "Flip horizontal", + "btn_scale_y": "Flip vertical", + "btn_zoom_in": "Zoom in", + "btn_zoom_out": "Zoom out", + "btn_reset": "Reset", + "preview": "Preivew", + "uploadSuccess": "Uploaded success!" +} diff --git a/apps/vben5/packages/@abp/components/src/locales/langs/zh-CN/cropper.json b/apps/vben5/packages/@abp/components/src/locales/langs/zh-CN/cropper.json new file mode 100644 index 000000000..f3bba22eb --- /dev/null +++ b/apps/vben5/packages/@abp/components/src/locales/langs/zh-CN/cropper.json @@ -0,0 +1,14 @@ +{ + "confirmText": "确认并上传", + "title": "头像上传", + "selectImage": "选择图片", + "btn_rotate_left": "逆时针旋转", + "btn_rotate_right": "顺时针旋转", + "btn_scale_x": "水平翻转", + "btn_scale_y": "垂直翻转", + "btn_zoom_in": "放大", + "btn_zoom_out": "缩小", + "btn_reset": "重置", + "preview": "预览", + "uploadSuccess": "上传成功!" +} diff --git a/apps/vben5/packages/@abp/core/src/utils/file.ts b/apps/vben5/packages/@abp/core/src/utils/file.ts new file mode 100644 index 000000000..559c36b7b --- /dev/null +++ b/apps/vben5/packages/@abp/core/src/utils/file.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/** + * @description: base64 to blob + */ +export function dataURLtoBlob(base64Buf: string): Blob { + const arr = base64Buf.split(','); + const typeItem = arr[0]; + const mime = typeItem?.match(/:(.*?);/)?.[1]; + const bstr = window.atob(arr[1]!); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while (n--) { + u8arr[n] = bstr.codePointAt(n)!; + } + return new Blob([u8arr], { type: mime }); +} + +/** + * img url to base64 + * @param url + */ +export function urlToBase64(url: string, mineType?: string): Promise { + return new Promise((resolve, reject) => { + let canvas = document.createElement('CANVAS') as HTMLCanvasElement | null; + const ctx = canvas!.getContext('2d'); + + const img = new Image(); + img.crossOrigin = ''; + img.addEventListener('load', () => { + if (!canvas || !ctx) { + return reject(new Error('canvas or ctx is null!')); + } + canvas.height = img.height; + canvas.width = img.width; + ctx.drawImage(img, 0, 0); + const dataURL = canvas.toDataURL(mineType || 'image/png'); + canvas = null; + resolve(dataURL); + }); + img.src = url; + }); +} diff --git a/apps/vben5/packages/@abp/core/src/utils/index.ts b/apps/vben5/packages/@abp/core/src/utils/index.ts index 382c90803..b00fa27c5 100644 --- a/apps/vben5/packages/@abp/core/src/utils/index.ts +++ b/apps/vben5/packages/@abp/core/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './array'; export * from './date'; +export * from './file'; export * from './is'; export * from './mitt'; export * from './regex'; diff --git a/apps/vben5/pnpm-workspace.yaml b/apps/vben5/pnpm-workspace.yaml index 42210ddfc..5bd673e69 100644 --- a/apps/vben5/pnpm-workspace.yaml +++ b/apps/vben5/pnpm-workspace.yaml @@ -90,6 +90,7 @@ catalog: codemirror: ^5.65.3 commitlint-plugin-function-rules: ^4.0.1 consola: ^3.4.2 + cropperjs: ^1.5.12 cross-env: ^7.0.3 cspell: ^8.19.3 cssnano: ^7.0.6