From 167d4122b704ee58f3f5b75a13dc0d2d06cd7486 Mon Sep 17 00:00:00 2001 From: valarchie <343928303@qq.com> Date: Tue, 25 Jul 2023 19:49:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- pnpm-lock.yaml | 39 ++ src/api/common.ts | 32 +- src/api/system/user.ts | 54 +++ src/components/ReCropper/index.ts | 7 + src/components/ReCropper/src/circled.css | 11 + src/components/ReCropper/src/index.tsx | 439 ++++++++++++++++++ .../ReCropper/src/svg/arrow-down.svg | 1 + src/components/ReCropper/src/svg/arrow-h.svg | 1 + .../ReCropper/src/svg/arrow-left.svg | 1 + .../ReCropper/src/svg/arrow-right.svg | 1 + src/components/ReCropper/src/svg/arrow-up.svg | 1 + src/components/ReCropper/src/svg/arrow-v.svg | 1 + src/components/ReCropper/src/svg/change.svg | 1 + src/components/ReCropper/src/svg/download.svg | 1 + src/components/ReCropper/src/svg/index.ts | 31 ++ src/components/ReCropper/src/svg/reload.svg | 1 + .../ReCropper/src/svg/rotate-left.svg | 1 + .../ReCropper/src/svg/rotate-right.svg | 1 + .../ReCropper/src/svg/search-minus.svg | 1 + .../ReCropper/src/svg/search-plus.svg | 1 + src/components/ReCropper/src/svg/upload.svg | 1 + src/layout/components/navbar.vue | 10 + src/layout/hooks/useNav.ts | 2 +- src/router/modules/global.ts | 20 + src/router/modules/remaining.ts | 16 + src/store/modules/user.ts | 4 +- src/views/system/user/hook.tsx | 2 +- src/views/system/user/profile/index.vue | 99 ++++ src/views/system/user/profile/resetPwd.vue | 89 ++++ src/views/system/user/profile/userAvatar.vue | 136 ++++++ src/views/system/user/profile/userInfo.vue | 89 ++++ 32 files changed, 1093 insertions(+), 5 deletions(-) create mode 100644 src/components/ReCropper/index.ts create mode 100644 src/components/ReCropper/src/circled.css create mode 100644 src/components/ReCropper/src/index.tsx create mode 100644 src/components/ReCropper/src/svg/arrow-down.svg create mode 100644 src/components/ReCropper/src/svg/arrow-h.svg create mode 100644 src/components/ReCropper/src/svg/arrow-left.svg create mode 100644 src/components/ReCropper/src/svg/arrow-right.svg create mode 100644 src/components/ReCropper/src/svg/arrow-up.svg create mode 100644 src/components/ReCropper/src/svg/arrow-v.svg create mode 100644 src/components/ReCropper/src/svg/change.svg create mode 100644 src/components/ReCropper/src/svg/download.svg create mode 100644 src/components/ReCropper/src/svg/index.ts create mode 100644 src/components/ReCropper/src/svg/reload.svg create mode 100644 src/components/ReCropper/src/svg/rotate-left.svg create mode 100644 src/components/ReCropper/src/svg/rotate-right.svg create mode 100644 src/components/ReCropper/src/svg/search-minus.svg create mode 100644 src/components/ReCropper/src/svg/search-plus.svg create mode 100644 src/components/ReCropper/src/svg/upload.svg create mode 100644 src/router/modules/global.ts create mode 100644 src/views/system/user/profile/index.vue create mode 100644 src/views/system/user/profile/resetPwd.vue create mode 100644 src/views/system/user/profile/userAvatar.vue create mode 100644 src/views/system/user/profile/userInfo.vue diff --git a/package.json b/package.json index 62e91d1..51d77fe 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "nprogress": "^0.2.0", "path": "^0.12.7", "pinia": "^2.1.4", - "qrcode": "^1.5.3", "pinyin-pro": "^3.15.2", + "cropperjs": "^1.5.13", + "vue-tippy": "^6.2.0", + "qrcode": "^1.5.3", "qs": "^6.11.2", "responsive-storage": "^2.2.0", "sortablejs": "^1.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbbdae6..adb4684 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,7 @@ specifiers: autoprefixer: ^10.4.14 axios: ^1.4.0 cloc: ^2.11.0 + cropperjs: ^1.5.13 crypto-js: ^4.1.1 cssnano: ^6.0.1 dayjs: ^1.11.8 @@ -86,6 +87,7 @@ specifiers: vue: ^3.3.4 vue-eslint-parser: ^9.3.1 vue-router: ^4.2.2 + vue-tippy: ^6.2.0 vue-tsc: ^1.8.1 vue-types: ^5.1.0 xlsx: ^0.18.5 @@ -98,6 +100,7 @@ dependencies: "@vueuse/motion": 2.0.0_vue@3.3.4 animate.css: 4.1.1 axios: 1.4.0 + cropperjs: 1.5.13 crypto-js: 4.1.1 dayjs: 1.11.8 echarts: 5.4.2 @@ -117,6 +120,7 @@ dependencies: typeit: 8.7.1 vue: 3.3.4 vue-router: 4.2.2_vue@3.3.4 + vue-tippy: 6.2.0_vue@3.3.4 vue-types: 5.1.0_vue@3.3.4 xlsx: 0.18.5 @@ -1447,6 +1451,13 @@ packages: dev: true optional: true + /@popperjs/core/2.11.8: + resolution: + { + integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + } + dev: false + /@pureadmin/descriptions/1.1.1_element-plus@2.3.6: resolution: { @@ -3150,6 +3161,13 @@ packages: } dev: true + /cropperjs/1.5.13: + resolution: + { + integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA== + } + dev: false + /cross-spawn/7.0.3: resolution: { @@ -8676,6 +8694,15 @@ packages: readable-stream: 3.6.2 dev: true + /tippy.js/6.3.7: + resolution: + { + integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== + } + dependencies: + "@popperjs/core": 2.11.8 + dev: false + /to-fast-properties/2.0.0: resolution: { @@ -9178,6 +9205,18 @@ packages: he: 1.2.0 dev: true + /vue-tippy/6.2.0_vue@3.3.4: + resolution: + { + integrity: sha512-UytUItp2ZDLXUwAotmioz02uLQoaAl5iVM+5yKsQWrXr29L9ivavtkL684FqbmOfbeCypBw+rVKsXhwdnCt/Cg== + } + peerDependencies: + vue: ^3.2.0 + dependencies: + tippy.js: 6.3.7 + vue: 3.3.4 + dev: false + /vue-tsc/1.8.1_typescript@5.0.4: resolution: { diff --git a/src/api/common.ts b/src/api/common.ts index 45ba6f6..b82a541 100644 --- a/src/api/common.ts +++ b/src/api/common.ts @@ -36,11 +36,41 @@ export type TokenDTO = { }; export type CurrentLoginUserDTO = { - userInfo: any; + userInfo: CurrentUserInfoDTO; roleKey: string; permissions: Set; }; +/** + * 当前User + */ +export interface CurrentUserInfoDTO { + avatar?: string; + createTime?: Date; + creatorId?: number; + creatorName?: string; + deptId?: number; + deptName?: string; + email?: string; + loginDate?: Date; + loginIp?: string; + nickName?: string; + phoneNumber?: string; + postId?: number; + postName?: string; + remark?: string; + roleId?: number; + roleName?: string; + sex?: number; + status?: number; + updaterId?: number; + updaterName?: string; + updateTime?: Date; + userId?: number; + username?: string; + userType?: number; +} + export type DictionaryData = { label: string; value: number; diff --git a/src/api/system/user.ts b/src/api/system/user.ts index 27e2550..aeaaf59 100644 --- a/src/api/system/user.ts +++ b/src/api/system/user.ts @@ -56,6 +56,26 @@ export interface UserRequest { username?: string; } +/** + * UpdateProfileCommand + */ +export interface UserProfileRequest { + email?: string; + nickName?: string; + phoneNumber?: string; + sex?: number; + userId?: number; +} + +/** + * ResetPasswordCommand + */ +export interface ResetPasswordRequest { + newPassword?: string; + oldPassword?: string; + userId?: number; +} + /** * 修改密码 */ @@ -120,3 +140,37 @@ export const exportUserExcelApi = (params: UserQuery, fileName: string) => { params }); }; + +/** 用户头像上传 */ +export const uploadUserAvatarApi = data => { + return http.request>( + "post", + "/system/user/profile/avatar", + { + data + }, + { + headers: { + "Content-Type": "multipart/form-data" + } + } + ); +}; + +/** 更改用户资料 */ +export const updateUserProfileApi = (data?: UserProfileRequest) => { + return http.request>("put", "/system/user/profile", { + data + }); +}; + +/** 更改当前用户密码 */ +export const updateCurrentUserPasswordApi = (data?: ResetPasswordRequest) => { + return http.request>( + "put", + "/system/user/profile/password", + { + data + } + ); +}; diff --git a/src/components/ReCropper/index.ts b/src/components/ReCropper/index.ts new file mode 100644 index 0000000..62e2590 --- /dev/null +++ b/src/components/ReCropper/index.ts @@ -0,0 +1,7 @@ +import reCropper from "./src"; +import { withInstall } from "@pureadmin/utils"; + +/** 图片裁剪组件 */ +export const ReCropper = withInstall(reCropper); + +export default ReCropper; diff --git a/src/components/ReCropper/src/circled.css b/src/components/ReCropper/src/circled.css new file mode 100644 index 0000000..41b0d99 --- /dev/null +++ b/src/components/ReCropper/src/circled.css @@ -0,0 +1,11 @@ +@import "cropperjs/dist/cropper.css"; +@import "tippy.js/dist/tippy.css"; +@import "tippy.js/themes/light.css"; +@import "tippy.js/animations/perspective.css"; + +.re-circled { + .cropper-view-box, + .cropper-face { + border-radius: 50%; + } +} diff --git a/src/components/ReCropper/src/index.tsx b/src/components/ReCropper/src/index.tsx new file mode 100644 index 0000000..62dd2fa --- /dev/null +++ b/src/components/ReCropper/src/index.tsx @@ -0,0 +1,439 @@ +import "./circled.css"; +import Cropper from "cropperjs"; +import { ElUpload } from "element-plus"; +import type { CSSProperties } from "vue"; +import { useResizeObserver } from "@vueuse/core"; +import { longpress } from "@/directives/longpress"; +import { useTippy, directive as tippy } from "vue-tippy"; +import { delay, debounce, isArray, downloadByBase64 } from "@pureadmin/utils"; +import { + ref, + unref, + computed, + PropType, + onMounted, + onUnmounted, + defineComponent +} from "vue"; +import { + Reload, + Upload, + ArrowH, + ArrowV, + ArrowUp, + ArrowDown, + ArrowLeft, + ChangeIcon, + ArrowRight, + RotateLeft, + SearchPlus, + RotateRight, + SearchMinus, + DownloadIcon +} from "./svg"; + +type Options = Cropper.Options; + +const defaultOptions: Options = { + aspectRatio: 1, + zoomable: true, + zoomOnTouch: true, + zoomOnWheel: true, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: true, + autoCrop: true, + background: true, + highlight: true, + center: true, + responsive: true, + restore: true, + checkCrossOrigin: true, + checkOrientation: true, + scalable: true, + modal: true, + guides: true, + movable: true, + rotatable: true +}; + +const props = { + src: { type: String, required: true }, + alt: { type: String }, + circled: { type: Boolean, default: false }, + realTimePreview: { type: Boolean, default: true }, + height: { type: [String, Number], default: "360px" }, + crossorigin: { + type: String as PropType<"" | "anonymous" | "use-credentials" | undefined>, + default: undefined + }, + imageStyle: { type: Object as PropType, default: () => ({}) }, + options: { type: Object as PropType, default: () => ({}) } +}; + +export default defineComponent({ + name: "ReCropper", + props, + setup(props, { attrs, emit }) { + const tippyElRef = ref>(); + const imgElRef = ref>(); + const cropper = ref>(); + const isReady = ref(false); + const imgBase64 = ref(); + const inCircled = ref(props.circled); + const inSrc = ref(props.src); + let scaleX = 1; + let scaleY = 1; + + const debounceRealTimeCroppered = debounce(realTimeCroppered, 80); + + const getImageStyle = computed((): CSSProperties => { + return { + height: props.height, + maxWidth: "100%", + ...props.imageStyle + }; + }); + + const getClass = computed(() => { + return [ + attrs.class, + { + ["re-circled"]: inCircled.value + } + ]; + }); + + const iconClass = computed(() => { + return [ + "p-[6px]", + "h-[30px]", + "w-[30px]", + "outline-none", + "rounded-[4px]", + "cursor-pointer", + "hover:bg-[rgba(0,0,0,0.06)]" + ]; + }); + + const getWrapperStyle = computed((): CSSProperties => { + return { height: `${props.height}`.replace(/px/, "") + "px" }; + }); + + onMounted(init); + + onUnmounted(() => { + cropper.value?.destroy(); + }); + + useResizeObserver(tippyElRef, () => { + handCropper("reset"); + }); + + async function init() { + const imgEl = unref(imgElRef); + if (!imgEl) return; + cropper.value = new Cropper(imgEl, { + ...defaultOptions, + ready: () => { + isReady.value = true; + realTimeCroppered(); + delay(400).then(() => emit("readied", cropper.value)); + }, + crop() { + debounceRealTimeCroppered(); + }, + zoom() { + debounceRealTimeCroppered(); + }, + cropmove() { + debounceRealTimeCroppered(); + }, + ...props.options + }); + } + + function realTimeCroppered() { + props.realTimePreview && croppered(); + } + + function croppered() { + if (!cropper.value) return; + const canvas = inCircled.value + ? getRoundedCanvas() + : cropper.value.getCroppedCanvas(); + // https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLCanvasElement/toBlob + canvas.toBlob(blob => { + if (!blob) return; + const fileReader: FileReader = new FileReader(); + fileReader.readAsDataURL(blob); + fileReader.onloadend = e => { + if (!e.target?.result || !blob) return; + imgBase64.value = e.target.result; + emit("cropper", { + base64: e.target.result, + blob, + info: { size: blob.size, ...cropper.value.getData() } + }); + }; + fileReader.onerror = () => { + emit("error"); + }; + }); + } + + function getRoundedCanvas() { + const sourceCanvas = cropper.value!.getCroppedCanvas(); + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d")!; + const width = sourceCanvas.width; + const height = sourceCanvas.height; + canvas.width = width; + canvas.height = height; + context.imageSmoothingEnabled = true; + context.drawImage(sourceCanvas, 0, 0, width, height); + context.globalCompositeOperation = "destination-in"; + context.beginPath(); + context.arc( + width / 2, + height / 2, + Math.min(width, height) / 2, + 0, + 2 * Math.PI, + true + ); + context.fill(); + return canvas; + } + + function handCropper(event: string, arg?: number | Array) { + if (event === "scaleX") { + scaleX = arg = scaleX === -1 ? 1 : -1; + } + if (event === "scaleY") { + scaleY = arg = scaleY === -1 ? 1 : -1; + } + arg && isArray(arg) + ? cropper.value?.[event]?.(...arg) + : cropper.value?.[event]?.(arg); + } + + function beforeUpload(file) { + const reader = new FileReader(); + reader.readAsDataURL(file); + inSrc.value = ""; + reader.onload = e => { + inSrc.value = e.target?.result as string; + }; + reader.onloadend = () => { + init(); + }; + return false; + } + + const menuContent = defineComponent({ + directives: { + tippy, + longpress + }, + setup() { + return () => ( +
+ + + + downloadByBase64(imgBase64.value, "cropping.png")} + /> + { + inCircled.value = !inCircled.value; + realTimeCroppered(); + }} + /> + handCropper("reset")} + /> + handCropper("move", [0, -10]), "0:100"]} + /> + handCropper("move", [0, 10]), "0:100"]} + /> + handCropper("move", [-10, 0]), "0:100"]} + /> + handCropper("move", [10, 0]), "0:100"]} + /> + handCropper("scaleX", -1)} + /> + handCropper("scaleY", -1)} + /> + handCropper("rotate", -45)} + /> + handCropper("rotate", 45)} + /> + handCropper("zoom", 0.1), "0:100"]} + /> + handCropper("zoom", -0.1), "0:100"]} + /> +
+ ); + } + }); + + function onContextmenu(event) { + event.preventDefault(); + + const { show, setProps } = useTippy(tippyElRef, { + content: menuContent, + arrow: false, + theme: "light", + trigger: "manual", + interactive: true, + appendTo: "parent", + // hideOnClick: false, + animation: "perspective", + placement: "bottom-start" + }); + + setProps({ + getReferenceClientRect: () => ({ + width: 0, + height: 0, + top: event.clientY, + bottom: event.clientY, + left: event.clientX, + right: event.clientX + }) + }); + + show(); + } + + return { + inSrc, + props, + imgElRef, + tippyElRef, + getClass, + getWrapperStyle, + getImageStyle, + isReady, + croppered, + onContextmenu + }; + }, + + render() { + const { + inSrc, + isReady, + getClass, + getImageStyle, + onContextmenu, + getWrapperStyle + } = this; + const { alt, crossorigin } = this.props; + + return inSrc ? ( +
onContextmenu(event)} + > + {alt} +
+ ) : null; + } +}); diff --git a/src/components/ReCropper/src/svg/arrow-down.svg b/src/components/ReCropper/src/svg/arrow-down.svg new file mode 100644 index 0000000..2839547 --- /dev/null +++ b/src/components/ReCropper/src/svg/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/arrow-h.svg b/src/components/ReCropper/src/svg/arrow-h.svg new file mode 100644 index 0000000..f955c41 --- /dev/null +++ b/src/components/ReCropper/src/svg/arrow-h.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/arrow-left.svg b/src/components/ReCropper/src/svg/arrow-left.svg new file mode 100644 index 0000000..66742bb --- /dev/null +++ b/src/components/ReCropper/src/svg/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/arrow-right.svg b/src/components/ReCropper/src/svg/arrow-right.svg new file mode 100644 index 0000000..45fbb4d --- /dev/null +++ b/src/components/ReCropper/src/svg/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/arrow-up.svg b/src/components/ReCropper/src/svg/arrow-up.svg new file mode 100644 index 0000000..7761be4 --- /dev/null +++ b/src/components/ReCropper/src/svg/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/arrow-v.svg b/src/components/ReCropper/src/svg/arrow-v.svg new file mode 100644 index 0000000..bbd0476 --- /dev/null +++ b/src/components/ReCropper/src/svg/arrow-v.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/change.svg b/src/components/ReCropper/src/svg/change.svg new file mode 100644 index 0000000..2edc209 --- /dev/null +++ b/src/components/ReCropper/src/svg/change.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/download.svg b/src/components/ReCropper/src/svg/download.svg new file mode 100644 index 0000000..f011250 --- /dev/null +++ b/src/components/ReCropper/src/svg/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/index.ts b/src/components/ReCropper/src/svg/index.ts new file mode 100644 index 0000000..1306ba7 --- /dev/null +++ b/src/components/ReCropper/src/svg/index.ts @@ -0,0 +1,31 @@ +import Reload from "./reload.svg?component"; +import Upload from "./upload.svg?component"; +import ArrowH from "./arrow-h.svg?component"; +import ArrowV from "./arrow-v.svg?component"; +import ArrowUp from "./arrow-up.svg?component"; +import ChangeIcon from "./change.svg?component"; +import ArrowDown from "./arrow-down.svg?component"; +import ArrowLeft from "./arrow-left.svg?component"; +import DownloadIcon from "./download.svg?component"; +import ArrowRight from "./arrow-right.svg?component"; +import RotateLeft from "./rotate-left.svg?component"; +import SearchPlus from "./search-plus.svg?component"; +import RotateRight from "./rotate-right.svg?component"; +import SearchMinus from "./search-minus.svg?component"; + +export { + Reload, + Upload, + ArrowH, + ArrowV, + ArrowUp, + ArrowDown, + ArrowLeft, + ChangeIcon, + ArrowRight, + RotateLeft, + SearchPlus, + RotateRight, + SearchMinus, + DownloadIcon +}; diff --git a/src/components/ReCropper/src/svg/reload.svg b/src/components/ReCropper/src/svg/reload.svg new file mode 100644 index 0000000..e8fab2c --- /dev/null +++ b/src/components/ReCropper/src/svg/reload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/rotate-left.svg b/src/components/ReCropper/src/svg/rotate-left.svg new file mode 100644 index 0000000..f702986 --- /dev/null +++ b/src/components/ReCropper/src/svg/rotate-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/rotate-right.svg b/src/components/ReCropper/src/svg/rotate-right.svg new file mode 100644 index 0000000..ffe6bc2 --- /dev/null +++ b/src/components/ReCropper/src/svg/rotate-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/search-minus.svg b/src/components/ReCropper/src/svg/search-minus.svg new file mode 100644 index 0000000..185924c --- /dev/null +++ b/src/components/ReCropper/src/svg/search-minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/search-plus.svg b/src/components/ReCropper/src/svg/search-plus.svg new file mode 100644 index 0000000..97447d2 --- /dev/null +++ b/src/components/ReCropper/src/svg/search-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropper/src/svg/upload.svg b/src/components/ReCropper/src/svg/upload.svg new file mode 100644 index 0000000..f5c9f11 --- /dev/null +++ b/src/components/ReCropper/src/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/layout/components/navbar.vue b/src/layout/components/navbar.vue index 1f774cf..557d033 100644 --- a/src/layout/components/navbar.vue +++ b/src/layout/components/navbar.vue @@ -12,6 +12,7 @@ const { layout, device, logout, + userProfile, onPanel, pureApp, username, @@ -51,6 +52,15 @@ const {

{{ username }}