From 15af16b2477d4193b0482c0fb02dac8e593b3fae Mon Sep 17 00:00:00 2001 From: dap <15891557205@163.com> Date: Wed, 4 Sep 2024 15:46:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=BE=E7=89=87=E8=A3=81=E5=89=AA?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=20&=20=E5=A4=B4=E5=83=8F=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/package.json | 1 + apps/web-antd/src/api/system/profile/index.ts | 29 +- .../src/api/system/profile/model.d.ts | 6 + apps/web-antd/src/components/cropper/index.ts | 8 + .../components/cropper/src/cropper-avatar.vue | 174 +++++++++ .../components/cropper/src/cropper-modal.vue | 353 ++++++++++++++++++ .../src/components/cropper/src/cropper.vue | 195 ++++++++++ .../src/components/cropper/src/typing.ts | 8 + apps/web-antd/src/locales/langs/en-US.json | 17 + apps/web-antd/src/locales/langs/zh-CN.json | 17 + apps/web-antd/src/utils/file/base64Conver.ts | 46 +++ apps/web-antd/src/utils/file/download.ts | 125 +++++++ .../src/views/_core/profile/index.vue | 2 +- .../src/views/_core/profile/profile-panel.vue | 26 +- pnpm-lock.yaml | 64 +--- 15 files changed, 1022 insertions(+), 49 deletions(-) create mode 100644 apps/web-antd/src/components/cropper/index.ts create mode 100644 apps/web-antd/src/components/cropper/src/cropper-avatar.vue create mode 100644 apps/web-antd/src/components/cropper/src/cropper-modal.vue create mode 100644 apps/web-antd/src/components/cropper/src/cropper.vue create mode 100644 apps/web-antd/src/components/cropper/src/typing.ts create mode 100644 apps/web-antd/src/utils/file/base64Conver.ts create mode 100644 apps/web-antd/src/utils/file/download.ts diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index 94285e31..1689f3ab 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -43,6 +43,7 @@ "@vben/utils": "workspace:*", "@vueuse/core": "^11.0.3", "ant-design-vue": "^4.2.3", + "cropperjs": "^1.6.2", "crypto-js": "^4.2.0", "dayjs": "^1.11.13", "echarts": "^5.5.1", diff --git a/apps/web-antd/src/api/system/profile/index.ts b/apps/web-antd/src/api/system/profile/index.ts index 503067a5..3b67d583 100644 --- a/apps/web-antd/src/api/system/profile/index.ts +++ b/apps/web-antd/src/api/system/profile/index.ts @@ -1,6 +1,7 @@ -import type { UpdatePasswordParam, UserProfile } from './model'; +import type { FileCallBack, UpdatePasswordParam, UserProfile } from './model'; import { requestClient } from '#/api/request'; +import { buildUUID } from '#/utils/uuid'; enum Api { root = '/system/user/profile', @@ -35,3 +36,29 @@ export function userUpdatePassword(data: UpdatePasswordParam) { encrypt: true, }); } + +/** + * 用户更新个人头像 + * @param fileCallback data + * @returns void + */ +export function userUpdateAvatar(fileCallback: FileCallBack) { + /** 直接点击头像上传 filename为空 由于后台通过拓展名判断(默认文件名blob) 会上传失败 */ + let { file } = fileCallback; + const { filename } = fileCallback; + /** + * Blob转File类型 + * 1. 在直接点击确认 filename为空 取uuid作为文件名 + * 2. 选择上传必须转为File类型 Blob类型上传后台获取文件名为空 + */ + file = filename + ? new File([file], filename) + : new File([file], `${buildUUID()}.png`); + return requestClient.post( + Api.updateAvatar, + { + avatarfile: file, + }, + { headers: { 'Content-Type': 'multipart/form-data' } }, + ); +} diff --git a/apps/web-antd/src/api/system/profile/model.d.ts b/apps/web-antd/src/api/system/profile/model.d.ts index 5d01ffab..bb1e4b38 100644 --- a/apps/web-antd/src/api/system/profile/model.d.ts +++ b/apps/web-antd/src/api/system/profile/model.d.ts @@ -67,3 +67,9 @@ export interface UpdatePasswordParam { oldPassword: string; newPassword: string; } + +interface FileCallBack { + name: string; + file: Blob; + filename: string; +} diff --git a/apps/web-antd/src/components/cropper/index.ts b/apps/web-antd/src/components/cropper/index.ts new file mode 100644 index 00000000..1d232cdb --- /dev/null +++ b/apps/web-antd/src/components/cropper/index.ts @@ -0,0 +1,8 @@ +import { withInstall } from '#/utils'; + +import cropperImage from './src/cropper.vue'; +import avatarCropper from './src/cropper-avatar.vue'; + +export type { Cropper } from './src/typing'; +export const CropperImage = withInstall(cropperImage); +export const CropperAvatar = withInstall(avatarCropper); diff --git a/apps/web-antd/src/components/cropper/src/cropper-avatar.vue b/apps/web-antd/src/components/cropper/src/cropper-avatar.vue new file mode 100644 index 00000000..5e17d2f2 --- /dev/null +++ b/apps/web-antd/src/components/cropper/src/cropper-avatar.vue @@ -0,0 +1,174 @@ + + + + diff --git a/apps/web-antd/src/components/cropper/src/cropper-modal.vue b/apps/web-antd/src/components/cropper/src/cropper-modal.vue new file mode 100644 index 00000000..e20791bf --- /dev/null +++ b/apps/web-antd/src/components/cropper/src/cropper-modal.vue @@ -0,0 +1,353 @@ + + + + diff --git a/apps/web-antd/src/components/cropper/src/cropper.vue b/apps/web-antd/src/components/cropper/src/cropper.vue new file mode 100644 index 00000000..2e1c4b2f --- /dev/null +++ b/apps/web-antd/src/components/cropper/src/cropper.vue @@ -0,0 +1,195 @@ + + + diff --git a/apps/web-antd/src/components/cropper/src/typing.ts b/apps/web-antd/src/components/cropper/src/typing.ts new file mode 100644 index 00000000..e76cc6f8 --- /dev/null +++ b/apps/web-antd/src/components/cropper/src/typing.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/web-antd/src/locales/langs/en-US.json b/apps/web-antd/src/locales/langs/en-US.json index 864c721f..a670bf42 100644 --- a/apps/web-antd/src/locales/langs/en-US.json +++ b/apps/web-antd/src/locales/langs/en-US.json @@ -4,5 +4,22 @@ "title": "Demos", "antd": "Ant Design Vue" } + }, + "component": { + "cropper": { + "selectImage": "Select Image", + "uploadSuccess": "Uploaded success!", + "imageTooBig": "Image too big", + "modalTitle": "Avatar upload", + "okText": "Confirm and upload", + "btn_reset": "Reset", + "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", + "preview": "Preview" + } } } diff --git a/apps/web-antd/src/locales/langs/zh-CN.json b/apps/web-antd/src/locales/langs/zh-CN.json index 31d3475b..4e39533b 100644 --- a/apps/web-antd/src/locales/langs/zh-CN.json +++ b/apps/web-antd/src/locales/langs/zh-CN.json @@ -4,5 +4,22 @@ "title": "演示", "antd": "Ant Design Vue" } + }, + "component": { + "cropper": { + "selectImage": "选择图片", + "uploadSuccess": "上传成功", + "imageTooBig": "图片超限", + "modalTitle": "头像上传", + "okText": "确认并上传", + "btn_reset": "重置", + "btn_rotate_left": "逆时针旋转", + "btn_rotate_right": "顺时针旋转", + "btn_scale_x": "水平翻转", + "btn_scale_y": "垂直翻转", + "btn_zoom_in": "放大", + "btn_zoom_out": "缩小", + "preview": "预览" + } } } diff --git a/apps/web-antd/src/utils/file/base64Conver.ts b/apps/web-antd/src/utils/file/base64Conver.ts new file mode 100644 index 00000000..3a487c3b --- /dev/null +++ b/apps/web-antd/src/utils/file/base64Conver.ts @@ -0,0 +1,46 @@ +/** + * @description: base64 to blob + */ +export function dataURLtoBlob(base64Buf: string): Blob { + const arr = base64Buf.split(','); + const typeItem = arr[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const mime = typeItem!.match(/:(.*?);/)![1]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const bstr = window.atob(arr[1]!); + let n = bstr.length; + const u8arr = new Uint8Array(n); + while (n--) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + 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; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const ctx = canvas!.getContext('2d'); + + const img = new Image(); + img.crossOrigin = ''; + img.addEventListener('load', () => { + if (!canvas || !ctx) { + // eslint-disable-next-line prefer-promise-reject-errors + return reject(); + } + 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/web-antd/src/utils/file/download.ts b/apps/web-antd/src/utils/file/download.ts new file mode 100644 index 00000000..667b7d9f --- /dev/null +++ b/apps/web-antd/src/utils/file/download.ts @@ -0,0 +1,125 @@ +import { openWindow } from '..'; +import { dataURLtoBlob, urlToBase64 } from './base64Conver'; + +export function downloadExcelFile( + data: BlobPart, + filename: string, + withRandomName = true, +) { + let realFileName = filename; + if (withRandomName) { + realFileName = `${filename}-${Date.now()}.xlsx`; + } + downloadByData(data, realFileName); +} + +/** + * Download online pictures + * @param url + * @param filename + * @param mime + * @param bom + */ +export function downloadByOnlineUrl( + url: string, + filename: string, + mime?: string, + bom?: BlobPart, +) { + urlToBase64(url).then((base64) => { + downloadByBase64(base64, filename, mime, bom); + }); +} + +/** + * Download pictures based on base64 + * @param buf + * @param filename + * @param mime + * @param bom + */ +export function downloadByBase64( + buf: string, + filename: string, + mime?: string, + bom?: BlobPart, +) { + const base64Buf = dataURLtoBlob(buf); + downloadByData(base64Buf, filename, mime, bom); +} + +/** + * Download according to the background interface file stream + * @param {*} data + * @param {*} filename + * @param {*} mime + * @param {*} bom + */ +export function downloadByData( + data: BlobPart, + filename: string, + mime?: string, + bom?: BlobPart, +) { + const blobData = bom === undefined ? [data] : [bom, data]; + const blob = new Blob(blobData, { type: mime || 'application/octet-stream' }); + + const blobURL = window.URL.createObjectURL(blob); + const tempLink = document.createElement('a'); + tempLink.style.display = 'none'; + tempLink.href = blobURL; + tempLink.setAttribute('download', filename); + if (tempLink.download === undefined) { + tempLink.setAttribute('target', '_blank'); + } + document.body.append(tempLink); + tempLink.click(); + tempLink.remove(); + window.URL.revokeObjectURL(blobURL); +} + +/** + * Download file according to file address + * @param {*} sUrl + */ +export function downloadByUrl({ + fileName, + target = '_blank', + url, +}: { + fileName?: string; + target?: '_blank' | '_self'; + url: string; +}): boolean { + const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome'); + const isSafari = window.navigator.userAgent.toLowerCase().includes('safari'); + + if (/iP/.test(window.navigator.userAgent)) { + console.error('Your browser does not support download!'); + return false; + } + if (isChrome || isSafari) { + const link = document.createElement('a'); + link.href = url; + link.target = target; + + if (link.download !== undefined) { + link.download = + // eslint-disable-next-line unicorn/prefer-string-slice + fileName || url.substring(url.lastIndexOf('/') + 1, url.length); + } + + if (document.createEvent) { + const e = document.createEvent('MouseEvents'); + e.initEvent('click', true, true); + link.dispatchEvent(e); + return true; + } + } + if (!url.includes('?')) { + url += '?download'; + } + + openWindow(url, { target }); + return true; +} diff --git a/apps/web-antd/src/views/_core/profile/index.vue b/apps/web-antd/src/views/_core/profile/index.vue index 28a81f28..1e47835f 100644 --- a/apps/web-antd/src/views/_core/profile/index.vue +++ b/apps/web-antd/src/views/_core/profile/index.vue @@ -23,7 +23,7 @@ onMounted(loadProfile);
- +
diff --git a/apps/web-antd/src/views/_core/profile/profile-panel.vue b/apps/web-antd/src/views/_core/profile/profile-panel.vue index 18ec2edf..f49e7eb2 100644 --- a/apps/web-antd/src/views/_core/profile/profile-panel.vue +++ b/apps/web-antd/src/views/_core/profile/profile-panel.vue @@ -1,22 +1,42 @@