From e5fe678eb6c84a2370d67ece40857190b817102f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9A=93=E6=9C=88=E5=BD=92=E5=B0=98?= Date: Tue, 11 Feb 2025 03:02:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=99=BB=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E6=B3=A8=E5=86=8C=EF=BC=8C=E5=BF=98=E8=AE=B0=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.yaml | 260 +++++----- locales/zh-CN.yaml | 261 +++++----- package.json | 2 + pnpm-lock.yaml | 34 +- src/api/login.ts | 154 ++++++ src/api/user.ts | 94 +++- src/assets/user.jpg | Bin 3694 -> 0 bytes src/assets/user.png | Bin 0 -> 4805 bytes src/components/ReCropper/index.ts | 7 + src/components/ReCropper/src/circled.css | 8 + src/components/ReCropper/src/index.tsx | 457 ++++++++++++++++++ .../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/components/ReCropperPreview/index.ts | 7 + src/components/ReCropperPreview/src/index.vue | 76 +++ src/components/RePerms/index.ts | 5 - src/components/RePerms/src/perms.tsx | 20 - src/directives/index.ts | 1 - src/directives/perms/index.ts | 15 - src/layout/components/lay-content/index.vue | 2 +- src/layout/components/lay-footer/index.vue | 2 +- src/layout/components/lay-navbar/index.vue | 12 +- .../lay-notice/components/NoticeItem.vue | 2 +- src/layout/components/lay-notice/data.ts | 12 +- src/layout/components/lay-notice/index.vue | 2 +- src/layout/components/lay-panel/index.vue | 8 +- .../lay-search/components/SearchFooter.vue | 8 +- .../lay-search/components/SearchHistory.vue | 4 +- .../lay-search/components/SearchModal.vue | 6 +- src/layout/components/lay-setting/index.vue | 84 ++-- .../components/lay-sidebar/NavHorizontal.vue | 12 +- src/layout/components/lay-sidebar/NavMix.vue | 12 +- .../components/SidebarCenterCollapse.vue | 4 +- .../components/SidebarLeftCollapse.vue | 4 +- .../components/SidebarTopCollapse.vue | 4 +- src/layout/components/lay-tag/index.vue | 5 +- src/layout/frame.vue | 2 +- src/layout/hooks/useDataThemeChange.ts | 57 ++- src/layout/hooks/useNav.ts | 43 +- src/layout/hooks/useTag.ts | 14 +- src/layout/index.vue | 2 +- src/layout/types.ts | 2 +- src/main.ts | 2 - src/router/index.ts | 66 ++- src/router/modules/home.ts | 4 +- src/router/modules/remaining.ts | 69 ++- src/router/utils.ts | 30 +- src/store/modules/user.ts | 141 ++++-- src/store/types.ts | 51 +- src/utils/auth.ts | 106 ++-- .../components/AccountSafe.vue | 67 +++ .../account-settings/components/Profile.vue | 163 +++++++ src/views/account-settings/index.vue | 176 +++++++ src/views/account-settings/utils/hooks.tsx | 349 +++++++++++++ src/views/login/components/LoginPhone.vue | 14 +- src/views/login/components/LoginQrCode.vue | 6 +- src/views/login/components/LoginRegist.vue | 300 ++++++++++-- src/views/login/components/LoginUpdate.vue | 186 +++++-- src/views/login/index.vue | 39 +- src/views/login/utils/enums.ts | 14 +- src/views/login/utils/rule.ts | 112 ++++- src/views/permission/button/index.vue | 99 ---- src/views/permission/button/perms.vue | 109 ----- src/views/permission/page/index.vue | 66 --- src/views/welcome/index.vue | 2 +- types/file.d.ts | 29 ++ types/global.d.ts | 4 + types/router.d.ts | 4 +- types/user.d.ts | 27 ++ vite.config.ts | 2 +- 83 files changed, 3007 insertions(+), 979 deletions(-) create mode 100644 src/api/login.ts delete mode 100644 src/assets/user.jpg create mode 100644 src/assets/user.png 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/components/ReCropperPreview/index.ts create mode 100644 src/components/ReCropperPreview/src/index.vue delete mode 100644 src/components/RePerms/index.ts delete mode 100644 src/components/RePerms/src/perms.tsx delete mode 100644 src/directives/perms/index.ts create mode 100644 src/views/account-settings/components/AccountSafe.vue create mode 100644 src/views/account-settings/components/Profile.vue create mode 100644 src/views/account-settings/index.vue create mode 100644 src/views/account-settings/utils/hooks.tsx delete mode 100644 src/views/permission/button/index.vue delete mode 100644 src/views/permission/button/perms.vue delete mode 100644 src/views/permission/page/index.vue create mode 100644 types/file.d.ts create mode 100644 types/user.d.ts diff --git a/locales/en.yaml b/locales/en.yaml index 7a000ec..809e1d3 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -1,124 +1,136 @@ -buttons: - pureLoginOut: LoginOut - pureLogin: Login - pureOpenSystemSet: Open System Configs - pureReload: Reload - pureCloseCurrentTab: Close CurrentTab - pureCloseLeftTabs: Close LeftTabs - pureCloseRightTabs: Close RightTabs - pureCloseOtherTabs: Close OtherTabs - pureCloseAllTabs: Close AllTabs - pureContentFullScreen: Content FullScreen - pureContentExitFullScreen: Content ExitFullScreen - pureClickCollapse: Collapse - pureClickExpand: Expand - pureConfirm: Confirm - pureSwitch: Switch - pureClose: Close - pureBackTop: BackTop - pureOpenText: Open - pureCloseText: Close -search: - pureTotal: Total - pureHistory: History - pureCollect: Collect - pureDragSort: (Drag Sort) - pureEmpty: Empty - purePlaceholder: Search Menu -panel: - pureSystemSet: System Configs - pureCloseSystemSet: Close System Configs - pureClearCacheAndToLogin: Clear cache and return to login page - pureClearCache: Clear Cache - pureOverallStyle: Overall Style - pureOverallStyleLight: Light - pureOverallStyleLightTip: Set sail freshly and light up the comfortable work interface - pureOverallStyleDark: Dark - pureOverallStyleDarkTip: Moonlight Overture, indulge in the tranquility and elegance of the night - pureOverallStyleSystem: Auto - pureOverallStyleSystemTip: Synchronize time, the interface naturally responds to morning and dusk - pureThemeColor: Theme Color - pureLayoutModel: Layout Model - pureVerticalTip: The menu on the left is familiar and friendly - pureHorizontalTip: Top menu, concise overview - pureMixTip: Mixed menu, flexible - pureStretch: Stretch Page - pureStretchFixed: Fixed - pureStretchFixedTip: Compact pages make it easy to find the information you need - pureStretchCustom: Custom - pureStretchCustomTip: Minimum 1280, maximum 1600 - pureTagsStyle: Tags Style - pureTagsStyleSmart: Smart - pureTagsStyleSmartTip: Smart tags add fun and brilliance - pureTagsStyleCard: Card - pureTagsStyleCardTip: Card tags for efficient browsing - pureTagsStyleChrome: Chrome - pureTagsStyleChromeTip: Chrome style is classic and elegant - pureInterfaceDisplay: Interface Display - pureGreyModel: Grey Model - pureWeakModel: Weak Model - pureHiddenTags: Hidden Tags - pureHiddenFooter: Hidden Footer - pureMultiTagsCache: MultiTags Cache -menus: - pureHome: Home - pureLogin: Login - pureAbnormal: Abnormal Page - pureFourZeroFour: "404" - pureFourZeroOne: "403" - pureFive: "500" - purePermission: Permission Manage - purePermissionPage: Page Permission - purePermissionButton: Button Permission - purePermissionButtonRouter: Route return button permission - purePermissionButtonLogin: Login interface return button permission -status: - pureLoad: Loading... - pureMessage: Message - pureNotify: Notify - pureTodo: Todo - pureNoMessage: No Message - pureNoNotify: No Notify - pureNoTodo: No Todo -login: - pureUsername: Username - purePassword: Password - pureVerifyCode: VerifyCode - pureRemember: days no need to login - pureRememberInfo: After checking and logging in, will automatically log in to the system without entering your username and password within the specified number of days. - pureSure: Sure Password - pureForget: Forget Password? - pureLogin: Login - pureThirdLogin: Third Login - purePhoneLogin: Phone Login - pureQRCodeLogin: QRCode Login - pureRegister: Register - pureWeChatLogin: WeChat Login - pureAlipayLogin: Alipay Login - pureQQLogin: QQ Login - pureWeiBoLogin: Weibo Login - purePhone: Phone - pureSmsVerifyCode: SMS VerifyCode - pureBack: Back - pureTest: Mock Test - pureTip: After scanning the code, click "Confirm" to complete the login - pureDefinite: Definite - pureLoginSuccess: Login Success - pureLoginFail: Login Fail - pureRegisterSuccess: Regist Success - pureTickPrivacy: Please tick Privacy Policy - pureReadAccept: I have read it carefully and accept - purePrivacyPolicy: Privacy Policy - pureGetVerifyCode: Get VerifyCode - pureInfo: Seconds - pureUsernameReg: Please enter username - purePassWordReg: Please enter password - pureVerifyCodeReg: Please enter verify code - pureVerifyCodeCorrectReg: Please enter correct verify code - pureVerifyCodeSixReg: Please enter a 6-digit verify code - purePhoneReg: Please enter the phone - purePhoneCorrectReg: Please enter the correct phone number format - purePassWordRuleReg: The password format should be any combination of 8-18 digits - purePassWordSureReg: Please enter confirm password - purePassWordDifferentReg: The two passwords do not match! - purePassWordUpdateReg: Password has been updated +buttons:AccountSettings: Account +buttons:LoginOut: LoginOut +buttons:Login: Login +buttons:OpenSystemSet: Open System Configs +buttons:Reload: Reload +buttons:CloseCurrentTab: Close CurrentTab +buttons:CloseLeftTabs: Close LeftTabs +buttons:CloseRightTabs: Close RightTabs +buttons:CloseOtherTabs: Close OtherTabs +buttons:CloseAllTabs: Close AllTabs +buttons:ContentFullScreen: Content FullScreen +buttons:ContentExitFullScreen: Content ExitFullScreen +buttons:ClickCollapse: Collapse +buttons:ClickExpand: Expand +buttons:Confirm: Confirm +buttons:Cancel: Cancel +buttons:Switch: Switch +buttons:Close: Close +buttons:BackTop: BackTop +buttons:OpenText: Open +buttons:CloseText: Close +search:Total: Total +search:History: History +search:Collect: Collect +search:DragSort: (Drag Sort) +search:Empty: Empty +search:Placeholder: Search Menu +panel:SystemSet: System Configs +panel:CloseSystemSet: Close System Configs +panel:ClearCacheAndToLogin: Clear cache and return to login page +panel:ClearCache: Clear Cache +panel:OverallStyle: Overall Style +panel:OverallStyleLight: Light +panel:OverallStyleLightTip: Set sail freshly and light up the comfortable work interface +panel:OverallStyleDark: Dark +panel:OverallStyleDarkTip: Moonlight Overture, indulge in the tranquility and elegance of the night +panel:OverallStyleSystem: Auto +panel:OverallStyleSystemTip: Synchronize time, the interface naturally responds to morning and dusk +panel:ThemeColor: Theme Color +panel:LayoutModel: Layout Model +panel:VerticalTip: The menu on the left is familiar and friendly +panel:HorizontalTip: Top menu, concise overview +panel:MixTip: Mixed menu, flexible +panel:Stretch: Stretch Page +panel:StretchFixed: Fixed +panel:StretchFixedTip: Compact pages make it easy to find the information you need +panel:StretchCustom: Custom +panel:StretchCustomTip: Minimum 1280, maximum 1600 +panel:TagsStyle: Tags Style +panel:TagsStyleSmart: Smart +panel:TagsStyleSmartTip: Smart tags add fun and brilliance +panel:TagsStyleCard: Card +panel:TagsStyleCardTip: Card tags for efficient browsing +panel:TagsStyleChrome: Chrome +panel:TagsStyleChromeTip: Chrome style is classic and elegant +panel:InterfaceDisplay: Interface Display +panel:GreyModel: Grey Model +panel:WeakModel: Weak Model +panel:HiddenTags: Hidden Tags +panel:HiddenFooter: Hidden Footer +panel:MultiTagsCache: MultiTags Cache +menus:Home: Home +menus:Login: Login +menus:Empty: Empty Page +menus:SysManagement: System Manage +menus:User: User Manage +menus:Role: Role Manage +menus:SystemMenu: Menu Manage +menus:Dept: Dept Manage +menus:SysMonitor: System Monitor +menus:OnlineUser: Online User +menus:LoginLog: Login Log +menus:OperationLog: Operation Log +menus:Abnormal: Abnormal Page +menus:FourZeroFour: "404" +menus:FourZeroOne: "403" +menus:Five: "500" +status:Load: Loading... +status:Message: Message +status:Notify: Notify +status:Todo: Todo +status:NoMessage: No Message +status:NoNotify: No Notify +status:NoTodo: No Todo +login:Username: Username +login:Nickname: Nickname +login:Password: Password +login:VerifyCode: VerifyCode +login:Remember: days no need to login +login:RememberInfo: After checking and logging in, will automatically log in to the system without entering your username and password within the specified number of days. +login:Sure: Sure Password +login:Forget: Forget Password? +login:Login: Login +login:Nextstep: Nextstep +login:Laststep: Laststep +login:ThirdLogin: Third Login +login:PhoneLogin: Phone Login +login:QRCodeLogin: QRCode Login +login:Register: Register +login:WeChatLogin: WeChat Login +login:AlipayLogin: Alipay Login +login:QQLogin: QQ Login +login:WeiBoLogin: Weibo Login +login:Phone: Phone +login:Email: Email +login:SmsVerifyCode: SMS VerifyCode +login:EmailVerifyCode: Email VerifyCode +login:Back: Back +login:Test: Mock Test +login:Tip: After scanning the code, click "Confirm" to complete the login +login:Definite: Definite +login:LoginSuccess: Login Success +login:LoginFail: Login Fail +login:RegisterSuccess: Regist Success +login:TickPrivacy: Please tick Privacy Policy +login:ReadAccept: I have read it carefully and accept +login:PrivacyPolicy: Privacy Policy +login:GetVerifyCode: Get VerifyCode +login:Info: Seconds +login:UsernameReg: Please enter username +login:NicknameReg: Please enter nickname +login:PassWordReg: Please enter password +login:VerifyCodeReg: Please enter verify code +login:EamilReg: Please enter email +login:VerifyCodeCorrectReg: Please enter correct verify code +login:VerifyCodeSixReg: Please enter a 6-digit verify code +login:PhoneReg: Please enter the phone +login:PhoneCorrectReg: Please enter the correct phone number format +login:PassWordRuleReg: The password format should be any combination of 8-18 digits +login:PassWordSureReg: Please enter confirm password +login:PassWordDifferentReg: The two passwords do not match! +login:PassWordUpdateReg: Password has been updated +logout:message: Whether to exit the system? +logout:success: Logout Success +logout:fail: Logout Fail +logout:cancel: Logout Cancel diff --git a/locales/zh-CN.yaml b/locales/zh-CN.yaml index a1e0c60..2d38f59 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -1,124 +1,137 @@ -buttons: - pureLoginOut: 退出系统 - pureLogin: 登录 - pureOpenSystemSet: 打开系统配置 - pureReload: 重新加载 - pureCloseCurrentTab: 关闭当前标签页 - pureCloseLeftTabs: 关闭左侧标签页 - pureCloseRightTabs: 关闭右侧标签页 - pureCloseOtherTabs: 关闭其他标签页 - pureCloseAllTabs: 关闭全部标签页 - pureContentFullScreen: 内容区全屏 - pureContentExitFullScreen: 内容区退出全屏 - pureClickCollapse: 点击折叠 - pureClickExpand: 点击展开 - pureConfirm: 确认 - pureSwitch: 切换 - pureClose: 关闭 - pureBackTop: 回到顶部 - pureOpenText: 开 - pureCloseText: 关 -search: - pureTotal: 共 - pureHistory: 搜索历史 - pureCollect: 收藏 - pureDragSort: (可拖拽排序) - pureEmpty: 暂无搜索结果 - purePlaceholder: 搜索菜单(支持拼音搜索) -panel: - pureSystemSet: 系统配置 - pureCloseSystemSet: 关闭配置 - pureClearCacheAndToLogin: 清空缓存并返回登录页 - pureClearCache: 清空缓存 - pureOverallStyle: 整体风格 - pureOverallStyleLight: 浅色 - pureOverallStyleLightTip: 清新启航,点亮舒适的工作界面 - pureOverallStyleDark: 深色 - pureOverallStyleDarkTip: 月光序曲,沉醉于夜的静谧雅致 - pureOverallStyleSystem: 自动 - pureOverallStyleSystemTip: 同步时光,界面随晨昏自然呼应 - pureThemeColor: 主题色 - pureLayoutModel: 导航模式 - pureVerticalTip: 左侧菜单,亲切熟悉 - pureHorizontalTip: 顶部菜单,简洁概览 - pureMixTip: 混合菜单,灵活多变 - pureStretch: 页宽 - pureStretchFixed: 固定 - pureStretchFixedTip: 紧凑页面,轻松找到所需信息 - pureStretchCustom: 自定义 - pureStretchCustomTip: 最小1280、最大1600 - pureTagsStyle: 页签风格 - pureTagsStyleSmart: 灵动 - pureTagsStyleSmartTip: 灵动标签,添趣生辉 - pureTagsStyleCard: 卡片 - pureTagsStyleCardTip: 卡片标签,高效浏览 - pureTagsStyleChrome: 谷歌 - pureTagsStyleChromeTip: 谷歌风格,经典美观 - pureInterfaceDisplay: 界面显示 - pureGreyModel: 灰色模式 - pureWeakModel: 色弱模式 - pureHiddenTags: 隐藏标签页 - pureHiddenFooter: 隐藏页脚 - pureMultiTagsCache: 页签持久化 -menus: - pureHome: 首页 - pureLogin: 登录 - pureAbnormal: 异常页面 - pureFourZeroFour: "404" - pureFourZeroOne: "403" - pureFive: "500" - purePermission: 权限管理 - purePermissionPage: 页面权限 - purePermissionButton: 按钮权限 - purePermissionButtonRouter: 路由返回按钮权限 - purePermissionButtonLogin: 登录接口返回按钮权限 -status: - pureLoad: 加载中... - pureMessage: 消息 - pureNotify: 通知 - pureTodo: 待办 - pureNoMessage: 暂无消息 - pureNoNotify: 暂无通知 - pureNoTodo: 暂无待办 -login: - pureUsername: 账号 - purePassword: 密码 - pureVerifyCode: 验证码 - pureRemember: 天内免登录 - pureRememberInfo: 勾选并登录后,规定天数内无需输入用户名和密码会自动登入系统 - pureSure: 确认密码 - pureForget: 忘记密码? - pureLogin: 登录 - pureThirdLogin: 第三方登录 - purePhoneLogin: 手机登录 - pureQRCodeLogin: 二维码登录 - pureRegister: 注册 - pureWeChatLogin: 微信登录 - pureAlipayLogin: 支付宝登录 - pureQQLogin: QQ登录 - pureWeiBoLogin: 微博登录 - purePhone: 手机号码 - pureSmsVerifyCode: 短信验证码 - pureBack: 返回 - pureTest: 模拟测试 - pureTip: 扫码后点击"确认",即可完成登录 - pureDefinite: 确定 - pureLoginSuccess: 登录成功 - pureLoginFail: 登录失败 - pureRegisterSuccess: 注册成功 - pureTickPrivacy: 请勾选隐私政策 - pureReadAccept: 我已仔细阅读并接受 - purePrivacyPolicy: 《隐私政策》 - pureGetVerifyCode: 获取验证码 - pureInfo: 秒后重新获取 - pureUsernameReg: 请输入账号 - purePassWordReg: 请输入密码 - pureVerifyCodeReg: 请输入验证码 - pureVerifyCodeCorrectReg: 请输入正确的验证码 - pureVerifyCodeSixReg: 请输入6位数字验证码 - purePhoneReg: 请输入手机号码 - purePhoneCorrectReg: 请输入正确的手机号码格式 - purePassWordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合 - purePassWordSureReg: 请输入确认密码 - purePassWordDifferentReg: 两次密码不一致! - purePassWordUpdateReg: 修改密码成功 +buttons:AccountSettings: 账户设置 +buttons:LoginOut: 退出系统 +buttons:Login: 登录 +buttons:OpenSystemSet: 打开系统配置 +buttons:Reload: 重新加载 +buttons:CloseCurrentTab: 关闭当前标签页 +buttons:CloseLeftTabs: 关闭左侧标签页 +buttons:CloseRightTabs: 关闭右侧标签页 +buttons:CloseOtherTabs: 关闭其他标签页 +buttons:CloseAllTabs: 关闭全部标签页 +buttons:ContentFullScreen: 内容区全屏 +buttons:ContentExitFullScreen: 内容区退出全屏 +buttons:ClickCollapse: 点击折叠 +buttons:ClickExpand: 点击展开 +buttons:Confirm: 确认 +buttons:Switch: 切换 +buttons:Close: 关闭 +buttons:Cancel: 取消 +buttons:BackTop: 回到顶部 +buttons:OpenText: 开 +buttons:CloseText: 关 +search:Total: 共 +search:History: 搜索历史 +search:Collect: 收藏 +search:DragSort: (可拖拽排序) +search:Empty: 暂无搜索结果 +search:Placeholder: 搜索菜单(支持拼音搜索) +panel:SystemSet: 系统配置 +panel:CloseSystemSet: 关闭配置 +panel:ClearCacheAndToLogin: 清空缓存并返回登录页 +panel:ClearCache: 清空缓存 +panel:OverallStyle: 整体风格 +panel:OverallStyleLight: 浅色 +panel:OverallStyleLightTip: 清新启航,点亮舒适的工作界面 +panel:OverallStyleDark: 深色 +panel:OverallStyleDarkTip: 月光序曲,沉醉于夜的静谧雅致 +panel:OverallStyleSystem: 自动 +panel:OverallStyleSystemTip: 同步时光,界面随晨昏自然呼应 +panel:ThemeColor: 主题色 +panel:LayoutModel: 导航模式 +panel:VerticalTip: 左侧菜单,亲切熟悉 +panel:HorizontalTip: 顶部菜单,简洁概览 +panel:MixTip: 混合菜单,灵活多变 +panel:Stretch: 页宽 +panel:StretchFixed: 固定 +panel:StretchFixedTip: 紧凑页面,轻松找到所需信息 +panel:StretchCustom: 自定义 +panel:StretchCustomTip: 最小1280、最大1600 +panel:TagsStyle: 页签风格 +panel:TagsStyleSmart: 灵动 +panel:TagsStyleSmartTip: 灵动标签,添趣生辉 +panel:TagsStyleCard: 卡片 +panel:TagsStyleCardTip: 卡片标签,高效浏览 +panel:TagsStyleChrome: 谷歌 +panel:TagsStyleChromeTip: 谷歌风格,经典美观 +panel:InterfaceDisplay: 界面显示 +panel:GreyModel: 灰色模式 +panel:WeakModel: 色弱模式 +panel:HiddenTags: 隐藏标签页 +panel:HiddenFooter: 隐藏页脚 +panel:MultiTagsCache: 页签持久化 +menus:Home: 首页 +menus:Login: 登录 +menus:Empty: 无Layout页 +menus:SysManagement: 系统管理 +menus:User: 用户管理 +menus:Role: 角色管理 +menus:SystemMenu: 菜单管理 +menus:Dept: 部门管理 +menus:SysMonitor: 系统监控 +menus:OnlineUser: 在线用户 +menus:LoginLog: 登录日志 +menus:OperationLog: 操作日志 +menus:Abnormal: 异常页面 +menus:FourZeroFour: "404" +menus:FourZeroOne: "403" +menus:Five: "500" +status:Load: 加载中... +status:Message: 消息 +status:Notify: 通知 +status:Todo: 待办 +status:NoMessage: 暂无消息 +status:NoNotify: 暂无通知 +status:NoTodo: 暂无待办 +login:Username: 账号 +login:Nickname: 昵称 +login:Password: 密码 +login:VerifyCode: 验证码 +login:Remember: 天内免登录 +login:RememberInfo: 勾选并登录后,规定天数内无需输入用户名和密码会自动登入系统 +login:Sure: 确认密码 +login:Forget: 忘记密码? +login:Login: 登录 +login:Nextstep: 下一步 +login:Laststep: 上一步 +login:ThirdLogin: 第三方登录 +login:PhoneLogin: 手机登录 +login:QRCodeLogin: 二维码登录 +login:Register: 注册 +login:WeChatLogin: 微信登录 +login:AlipayLogin: 支付宝登录 +login:QQLogin: QQ登录 +login:WeiBoLogin: 微博登录 +login:Phone: 手机号码 +login:Email: 邮箱 +login:SmsVerifyCode: 短信验证码 +login:EmailVerifyCode: 邮箱验证码 +login:Back: 返回 +login:Test: 模拟测试 +login:Tip: 扫码后点击"确认",即可完成登录 +login:Definite: 确定 +login:LoginSuccess: 登录成功 +login:LoginFail: 登录失败 +login:RegisterSuccess: 注册成功 +login:TickPrivacy: 请勾选隐私政策 +login:ReadAccept: 我已仔细阅读并接受 +login:PrivacyPolicy: 《隐私政策》 +login:GetVerifyCode: 获取验证码 +login:Info: 秒后重新获取 +login:UsernameReg: 请输入账号 +login:NicknameReg: 请输入昵称 +login:PassWordReg: 请输入密码 +login:EmailReg: 请输入邮箱 +login:VerifyCodeReg: 请输入验证码 +login:VerifyCodeCorrectReg: 请输入正确的验证码 +login:VerifyCodeSixReg: 请输入6位数字验证码 +login:PhoneReg: 请输入手机号码 +login:PhoneCorrectReg: 请输入正确的手机号码格式 +login:PassWordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合 +login:PassWordSureReg: 请输入确认密码 +login:PassWordDifferentReg: 两次密码不一致! +login:PassWordUpdateReg: 修改密码成功 +logout:message: 是否退出当前系统? +logout:success: 退出成功 +logout:fail: 退出失败 +logout:cancel: 退出取消 + diff --git a/package.json b/package.json index a0e7840..ca2e1c3 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,11 @@ "@pureadmin/utils": "^2.5.0", "@vueuse/core": "^12.0.0", "@vueuse/motion": "^2.2.6", + "@zxcvbn-ts/core": "^3.0.4", "animate.css": "^4.1.1", "axios": "^1.7.9", "dayjs": "^1.11.13", + "cropperjs": "^1.6.2", "echarts": "^5.5.1", "element-plus": "^2.9.0", "js-cookie": "^3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 188b17b..50cab2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,12 +23,18 @@ importers: '@vueuse/motion': specifier: ^2.2.6 version: 2.2.6(rollup@4.28.1)(vue@3.5.13(typescript@5.6.3)) + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 animate.css: specifier: ^4.1.1 version: 4.1.1 axios: specifier: ^1.7.9 version: 1.7.9 + cropperjs: + specifier: ^1.6.2 + version: 1.6.2 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -747,8 +753,8 @@ packages: resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==, tarball: https://registry.npmmirror.com/@intlify/shared/-/shared-11.0.0-rc.1.tgz} engines: {node: '>= 16'} - '@intlify/shared@11.0.1': - resolution: {integrity: sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==, tarball: https://registry.npmmirror.com/@intlify/shared/-/shared-11.0.1.tgz} + '@intlify/shared@11.1.1': + resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==, tarball: https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.1.tgz} engines: {node: '>= 16'} '@intlify/unplugin-vue-i18n@6.0.1': @@ -1275,6 +1281,9 @@ packages: '@vueuse/shared@9.13.0': resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==, tarball: https://registry.npmmirror.com/@zxcvbn-ts/core/-/core-3.0.4.tgz} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1570,6 +1579,9 @@ packages: typescript: optional: true + cropperjs@1.6.2: + resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==, tarball: https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1923,7 +1935,7 @@ packages: resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} fastest-levenshtein@1.0.16: - resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==, tarball: https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz} engines: {node: '>= 4.9.1'} fastq@1.17.1: @@ -4220,14 +4232,14 @@ snapshots: '@intlify/shared@11.0.0-rc.1': {} - '@intlify/shared@11.0.1': {} + '@intlify/shared@11.1.1': {} '@intlify/unplugin-vue-i18n@6.0.1(@vue/compiler-dom@3.5.13)(eslint@9.16.0(jiti@2.4.1))(rollup@4.28.1)(typescript@5.6.3)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@2.4.1)) '@intlify/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3))) - '@intlify/shared': 11.0.1 - '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) + '@intlify/shared': 11.1.1 + '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3)) '@rollup/pluginutils': 5.1.3(rollup@4.28.1) '@typescript-eslint/scope-manager': 8.18.0 '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.6.3) @@ -4249,11 +4261,11 @@ snapshots: - supports-color - typescript - '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': + '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3)))(vue@3.5.13(typescript@5.6.3))': dependencies: '@babel/parser': 7.26.3 optionalDependencies: - '@intlify/shared': 11.0.1 + '@intlify/shared': 11.1.1 '@vue/compiler-dom': 3.5.13 vue: 3.5.13(typescript@5.6.3) vue-i18n: 10.0.5(vue@3.5.13(typescript@5.6.3)) @@ -4823,6 +4835,10 @@ snapshots: - '@vue/composition-api' - vue + '@zxcvbn-ts/core@3.0.4': + dependencies: + fastest-levenshtein: 1.0.16 + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -5150,6 +5166,8 @@ snapshots: optionalDependencies: typescript: 5.6.3 + cropperjs@1.6.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..36e1def --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,154 @@ +import type { UserInfo } from "@/utils/auth"; +import { http } from "@/utils/http"; + +export type LoginResult = { + /** `token` */ + accessToken: string; + /** 用于调用刷新`accessToken`的接口时所需的`token` */ + refreshToken: string; + /** `accessToken`的过期时间戳(毫秒) */ + expiresTime: number; +}; + +/** + * 登录 + * @param data + * @returns + */ +export const getLogin = (data?: object) => { + return http.request("post", "/api/login", { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + data + }); +}; +/** 刷新token */ +export const refreshTokenApi = (data: { refreshToken: string }) => { + return http.request("post", "/api/refreshToken", { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + data + }); +}; + +export type CaptchaResponse = { + /**验证码ID */ + uuid: string; + /**验证码 */ + captcha: string; +}; + +/** 获取验证码 */ +export const GetCaptchaAPI = () => { + return http.request("get", "/api/captcha"); +}; + +/** + * 获取用户动态路由 + * @returns + */ +export const getUserRoutesAPI = () => { + return http.request("GET", "/api/getRoutes"); +}; + +/** + * 获取用户信息 + */ +export const getUserInfoAPI = () => { + return http.request("get", `/api/info`); +}; + +/** + * 退出登录 + */ +export const logoutAPI = () => { + return http.request("post", `/api/logout`); +}; + +/**获取验证码参数 */ +type GetCodeParams = { + /**用户账号 */ + username: string; + /**验证码类型 */ + title: string; + /**收件邮箱 */ + mail: string; +}; + +/** + * 获取验证码 + * @param data + * @returns + */ +export const postGetCodeAPI = (data: GetCodeParams) => { + return http.request("post", `/api/code`, { + data + }); +}; + +/** + * 注册参数 + */ +type RegisterParams = { + /**用户名 */ + username: string; + /**密码 */ + password: string; + /**邮箱 */ + email: string; + /**验证码 */ + code: string; + /**性别 */ + gender: number; + /**昵称 */ + nickname: string; + /**手机号 */ + phone: string; + /**部门ID */ + department_id: string; +}; + +/** + * 用户注册 + * @param data + * @returns + */ +export const postRegisterAPI = (data: RegisterParams) => { + return http.request("post", `/api/register`, { + data + }); +}; + +/** + * 重置密码 + */ +type ResetPasswordParams = { + /** + * 用户账号 + */ + username: string; + /** + * 邮箱 + */ + mail: string; + /** + * 验证码 + */ + code: string; + /** + * 密码 + */ + password: string; +}; +/** + * 重置密码 + * @param data + * @returns + */ +export const postResetPasswordAPI = (data: ResetPasswordParams) => { + return http.request("post", `/api/resetPassword`, { + data + }); +}; diff --git a/src/api/user.ts b/src/api/user.ts index bee3eb5..a946bb0 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,4 +1,5 @@ import { http } from "@/utils/http"; +import type { FileInfo } from "types/file"; /** * 登录结果 @@ -35,19 +36,90 @@ export const getLogin = (data?: object) => { }); }; -export type CaptchaResponse = { - /**验证码 */ - image: string; - /**验证码ID */ - captchaId: string; +/** + * 更新邮箱 + * @param data + * @returns + */ +export const putUpdateEmailAPI = (data: { + /**密码 */ + password: string; + /**邮箱 */ + email: string; +}) => { + return http.request("put", `/api/user/updateEmail`, { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + data + }); }; -/** 获取验证码 */ -export const GetCaptchaAPI = () => { - return http.request("get", "/api/captcha"); +/** + * 更新密码 + * @param data + * @returns + */ +export const putUpdatePasswordAPI = (data: { + /**旧密码 */ + oldPassword: string; + /**新密码 */ + newPassword: string; +}) => { + return http.request("put", `/api/user/updatePassword`, { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + data + }); +}; +/** + * 更新手机号 + * @param data + * @returns + */ +export const putUpdatePhoneAPI = (data: { + /**密码 */ + password: string; + /**手机号 */ + phone: string; +}) => { + return http.request("put", `/api/user/updatePhone`, { + headers: { + "content-type": "application/x-www-form-urlencoded" + }, + data + }); }; -export type UserInfo = { - permissions: string[]; - roles: string[]; +/**更新用户基础信息参数 */ +type UpdateBaseUserInfoParams = { + /**姓名 */ + name: string; + /**性别 */ + gender: number; +}; + +/** + * 更新用户基础信息 + * @param data + * @returns + */ +export const putUpdateBaseUserInfoAPI = (data: UpdateBaseUserInfoParams) => { + return http.request("PUT", "/api/user/updateBaseUserInfo", { data }); +}; + +/** + * 更新头像 + * @param id 用户ID + * @param data 图片数据 + * @returns + */ +export const postUploadAvatarAPI = (id: string, data: { file: Blob }) => { + return http.request("post", `/api/user/avatar/${id}`, { + headers: { + "content-type": "multipart/form-data" + }, + data + }); }; diff --git a/src/assets/user.jpg b/src/assets/user.jpg deleted file mode 100644 index a2973ace3367cf7181b470e2814db5a9c06a4533..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3694 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 literal 0 HcmV?d00001 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..54c77d2 --- /dev/null +++ b/src/components/ReCropper/src/circled.css @@ -0,0 +1,8 @@ +@import "cropperjs/dist/cropper.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..826ffd0 --- /dev/null +++ b/src/components/ReCropper/src/index.tsx @@ -0,0 +1,457 @@ +import "./circled.css"; +import Cropper from "cropperjs"; +import { ElUpload } from "element-plus"; +import type { CSSProperties } from "vue"; +import { useEventListener } from "@vueuse/core"; +import { longpress } from "@/directives/longpress"; +import { useTippy, directive as tippy } from "vue-tippy"; +import { + type PropType, + ref, + unref, + computed, + onMounted, + onUnmounted, + defineComponent +} from "vue"; +import { + delay, + debounce, + isArray, + downloadByBase64, + useResizeObserver +} from "@pureadmin/utils"; +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 }, + /** 是否可以通过点击裁剪区域关闭右键弹出的功能菜单,默认 `true` */ + isClose: { type: Boolean, default: true }, + 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 inCircled = ref(props.circled); + const isInClose = ref(props.isClose); + const inSrc = ref(props.src); + const isReady = ref(false); + const imgBase64 = ref(); + + 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(); + isReady.value = false; + cropper.value = null; + imgBase64.value = ""; + scaleX = 1; + scaleY = 1; + }); + + 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, destroy, state } = useTippy(tippyElRef, { + content: menuContent, + arrow: false, + theme: "light", + trigger: "manual", + interactive: true, + appendTo: "parent", + // hideOnClick: false, + placement: "bottom-end" + }); + + setProps({ + getReferenceClientRect: () => ({ + width: 0, + height: 0, + top: event.clientY, + bottom: event.clientY, + left: event.clientX, + right: event.clientX + }) + }); + + show(); + + if (isInClose.value) { + if (!state.value.isShown && !state.value.isVisible) return; + useEventListener(tippyElRef, "click", destroy); + } + } + + 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..36558e8 --- /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..5f1c01e --- /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..1a0fe00 --- /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..942f926 --- /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..ec3f02b --- /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..854b2c9 --- /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..9f9615a --- /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..bea3fc0 --- /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..67ecdc6 --- /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..7372706 --- /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..5fa8ae9 --- /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..a008019 --- /dev/null +++ b/src/components/ReCropper/src/svg/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ReCropperPreview/index.ts b/src/components/ReCropperPreview/index.ts new file mode 100644 index 0000000..e7949fe --- /dev/null +++ b/src/components/ReCropperPreview/index.ts @@ -0,0 +1,7 @@ +import reCropperPreview from "./src/index.vue"; +import { withInstall } from "@pureadmin/utils"; + +/** 图片裁剪预览组件 */ +export const ReCropperPreview = withInstall(reCropperPreview); + +export default ReCropperPreview; diff --git a/src/components/ReCropperPreview/src/index.vue b/src/components/ReCropperPreview/src/index.vue new file mode 100644 index 0000000..c34cc94 --- /dev/null +++ b/src/components/ReCropperPreview/src/index.vue @@ -0,0 +1,76 @@ + + + diff --git a/src/components/RePerms/index.ts b/src/components/RePerms/index.ts deleted file mode 100644 index 3701c3c..0000000 --- a/src/components/RePerms/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import perms from "./src/perms"; - -const Perms = perms; - -export { Perms }; diff --git a/src/components/RePerms/src/perms.tsx b/src/components/RePerms/src/perms.tsx deleted file mode 100644 index da01bc1..0000000 --- a/src/components/RePerms/src/perms.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { defineComponent, Fragment } from "vue"; -import { hasPerms } from "@/utils/auth"; - -export default defineComponent({ - name: "Perms", - props: { - value: { - type: undefined, - default: [] - } - }, - setup(props, { slots }) { - return () => { - if (!slots) return null; - return hasPerms(props.value) ? ( - {slots.default?.()} - ) : null; - }; - } -}); diff --git a/src/directives/index.ts b/src/directives/index.ts index d01fe71..3be2c5c 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -2,5 +2,4 @@ export * from "./auth"; export * from "./copy"; export * from "./longpress"; export * from "./optimize"; -export * from "./perms"; export * from "./ripple"; diff --git a/src/directives/perms/index.ts b/src/directives/perms/index.ts deleted file mode 100644 index 073c918..0000000 --- a/src/directives/perms/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { hasPerms } from "@/utils/auth"; -import type { Directive, DirectiveBinding } from "vue"; - -export const perms: Directive = { - mounted(el: HTMLElement, binding: DirectiveBinding>) { - const { value } = binding; - if (value) { - !hasPerms(value) && el.parentNode?.removeChild(el); - } else { - throw new Error( - "[Directive: perms]: need perms! Like v-perms=\"['btn.add','btn.edit']\"" - ); - } - } -}; diff --git a/src/layout/components/lay-content/index.vue b/src/layout/components/lay-content/index.vue index 5810d66..b3f6b1e 100644 --- a/src/layout/components/lay-content/index.vue +++ b/src/layout/components/lay-content/index.vue @@ -133,7 +133,7 @@ const transitionMain = defineComponent({ }" > diff --git a/src/layout/components/lay-footer/index.vue b/src/layout/components/lay-footer/index.vue index 7763134..b265daf 100644 --- a/src/layout/components/lay-footer/index.vue +++ b/src/layout/components/lay-footer/index.vue @@ -1,4 +1,4 @@ - + + + + diff --git a/src/views/account-settings/components/Profile.vue b/src/views/account-settings/components/Profile.vue new file mode 100644 index 0000000..3f173b5 --- /dev/null +++ b/src/views/account-settings/components/Profile.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/views/account-settings/index.vue b/src/views/account-settings/index.vue new file mode 100644 index 0000000..98e9947 --- /dev/null +++ b/src/views/account-settings/index.vue @@ -0,0 +1,176 @@ + + + + + + + diff --git a/src/views/account-settings/utils/hooks.tsx b/src/views/account-settings/utils/hooks.tsx new file mode 100644 index 0000000..46e0c6a --- /dev/null +++ b/src/views/account-settings/utils/hooks.tsx @@ -0,0 +1,349 @@ +import { message } from "@/utils/message"; +import { addDialog } from "@/components/ReDialog"; +import { reactive, ref, onMounted, watch } from "vue"; +import { ElForm, ElFormItem, ElInput, ElProgress } from "element-plus"; +import type { UserInfo } from "types/user"; +import { getUserInfoAPI } from "@/api/login"; +import { + putUpdateEmailAPI, + putUpdatePasswordAPI, + putUpdatePhoneAPI +} from "@/api/user"; +import { isAllEmpty, isEmail, isPhone, storageLocal } from "@pureadmin/utils"; +import { zxcvbn } from "@zxcvbn-ts/core"; +import { setUserInfo, userInfoKey } from "@/utils/auth"; + +export const useUserInfo = () => { + /** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */ + const REGEXP_PWD = + /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; + + const pwdProgress = [ + { color: "#e74242", text: "非常弱" }, + { color: "#EFBD47", text: "弱" }, + { color: "#ffa500", text: "一般" }, + { color: "#1bbf1b", text: "强" }, + { color: "#008000", text: "非常强" } + ]; + // 当前密码强度(0-4) + const curScore = ref(); + const ruleFormRef = ref(); + /**密码表单 */ + const passwordForm = reactive({ + oldPassword: "", + newPassword: "" + }); + /**联系方式表单 */ + const phoneForm = reactive({ + password: "", + phone: "" + }); + /**邮箱*/ + const emailForm = reactive({ + password: "", + email: "" + }); + const userInfo = reactive({ + id: "", + username: "", + gender: 1, + avatar: "", + email: "", + nickname: "", + phone: "", + status: 0, + create_time: "", + update_time: "", + roles: [], + permissions: [] + }); + /**获取个人信息 */ + const getUserInfo = async () => { + const res = await getUserInfoAPI(); + if (res.success) { + Object.assign(userInfo, res.data); + const user = storageLocal().getItem(userInfoKey); + storageLocal().setItem(userInfoKey, { + ...user, + avatar: res.data.avatar, + nickname: res.data.nickname + }); + setUserInfo(res.data); + } + }; + + /**组件挂载执行 */ + onMounted(async () => { + await getUserInfo(); + }); + watch( + passwordForm, + ({ newPassword }) => + (curScore.value = isAllEmpty(newPassword) + ? -1 + : zxcvbn(newPassword).score) + ); + /**密码校验规则 */ + const passwordRule = [ + { + validator: (rule, value, callback) => { + if (value === "") { + callback(new Error("请输入新密码~")); + } else if (!REGEXP_PWD.test(value)) { + callback( + new Error("密码格式应为8-18位数字、字母、符号的任意两种组合~") + ); + } else { + callback(); + } + }, + trigger: "blur" + } + ]; + /**手机好校验规则 */ + const phoneRule = [ + { + validator: (rule, value, callback) => { + if (value === "") { + callback(new Error("请输入手机号码~")); + } else if (!isPhone(value)) { + callback(new Error("请输入正确的手机号码格式~")); + } else { + callback(); + } + }, + trigger: "blur" + } + ]; + /**邮箱校验规则 */ + const emailRule = [ + { + validator: (rule, value, callback) => { + if (value === "") { + callback(new Error("请输入邮箱~")); + } else if (!isEmail(value)) { + callback(new Error("请输入正确的邮箱格式~")); + } else { + callback(); + } + }, + trigger: "blur" + } + ]; + /** 重置密码 */ + const handleReset = (row: UserInfo) => { + addDialog({ + title: `重置 ${row.username}--${row.nickname} 的密码`, + width: "30%", + draggable: true, + closeOnClickModal: false, + contentRenderer: () => ( +
+ + + + + + + + +
+ {pwdProgress.map(({ color, text }, idx) => ( +
+ = idx ? 100 : 0} + color={color} + stroke-width={10} + show-text={false} + /> +

+ {text} +

+
+ ))} +
+
+ ), + closeCallBack: () => passwordForm, + beforeSure: done => { + ruleFormRef.value.validate(async (valid: any) => { + if (valid) { + // 表单规则校验通过 + const res = await putUpdatePasswordAPI(passwordForm); + if (res.code === 200) { + done(); + message(res.msg, { + type: "success" + }); + } else { + message(res.msg, { + type: "error" + }); + } + } + }); + } + }); + }; + + /**更新手机号 */ + const handlePhone = (row: UserInfo) => { + phoneForm.phone = row.phone; + addDialog({ + title: `更新 ${row.username}--${row.nickname} 的联系号码`, + width: "30%", + draggable: true, + closeOnClickModal: false, + contentRenderer: () => ( +
+ + + + + + + + +
+ ), + closeCallBack: () => phoneForm, + beforeSure: done => { + ruleFormRef.value.validate(async (valid: any) => { + if (valid) { + // 表单规则校验通过 + const res = await putUpdatePhoneAPI(phoneForm); + if (res.code === 200) { + done(); + message(res.msg, { + type: "success" + }); + } else { + message(res.msg, { + type: "error" + }); + } + } + }); + } + }); + }; + /**更新邮箱 */ + const handleEmail = (row: UserInfo) => { + emailForm.email = row.email; + addDialog({ + title: `更新 ${row.username}--${row.nickname} 的联系邮箱`, + width: "30%", + draggable: true, + closeOnClickModal: false, + contentRenderer: () => ( +
+ + + + + + + + +
+ ), + closeCallBack: () => emailForm, + beforeSure: done => { + ruleFormRef.value.validate(async (valid: any) => { + if (valid) { + // 表单规则校验通过 + const res = await putUpdateEmailAPI(emailForm); + if (res.code === 200) { + done(); + message(res.msg, { + type: "success" + }); + } else { + message(res.msg, { + type: "error" + }); + } + } + }); + } + }); + }; + + return { + userInfo, + getUserInfo, + handleReset, + handlePhone, + handleEmail + }; +}; diff --git a/src/views/login/components/LoginPhone.vue b/src/views/login/components/LoginPhone.vue index 0a94370..46f0143 100644 --- a/src/views/login/components/LoginPhone.vue +++ b/src/views/login/components/LoginPhone.vue @@ -27,7 +27,7 @@ const onLogin = async (formEl: FormInstance | undefined) => { if (valid) { // 模拟登录请求,需根据实际开发进行修改 setTimeout(() => { - message(transformI18n($t("login.pureLoginSuccess")), { + message(transformI18n($t("login:LoginSuccess")), { type: "success" }); loading.value = false; @@ -51,7 +51,7 @@ function onBack() { @@ -63,7 +63,7 @@ function onBack() { {{ text.length > 0 - ? text + t("login.pureInfo") - : t("login.pureGetVerifyCode") + ? text + t("login:Info") + : t("login:GetVerifyCode") }} @@ -90,7 +90,7 @@ function onBack() { :loading="loading" @click="onLogin(ruleFormRef)" > - {{ t("login.pureLogin") }} + {{ t("login:Login") }} @@ -98,7 +98,7 @@ function onBack() { - {{ t("login.pureBack") }} + {{ t("login:Back") }} diff --git a/src/views/login/components/LoginQrCode.vue b/src/views/login/components/LoginQrCode.vue index 3963f84..5c4d3cd 100644 --- a/src/views/login/components/LoginQrCode.vue +++ b/src/views/login/components/LoginQrCode.vue @@ -9,11 +9,11 @@ const { t } = useI18n(); diff --git a/src/views/login/components/LoginRegist.vue b/src/views/login/components/LoginRegist.vue index 4437995..78fbfe7 100644 --- a/src/views/login/components/LoginRegist.vue +++ b/src/views/login/components/LoginRegist.vue @@ -3,37 +3,70 @@ import { useI18n } from "vue-i18n"; import { ref, reactive } from "vue"; import Motion from "../utils/motion"; import { message } from "@/utils/message"; -import { updateRules } from "../utils/rule"; -import type { FormInstance } from "element-plus"; +import { registerRules } from "../utils/rule"; +import type { FormInstance, FormItemProp } from "element-plus"; import { useVerifyCode } from "../utils/verifyCode"; +import { clone } from "@pureadmin/utils"; import { $t, transformI18n } from "@/plugins/i18n"; import { useUserStoreHook } from "@/store/modules/user"; import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import Lock from "@iconify-icons/ri/lock-fill"; import Iphone from "@iconify-icons/ep/iphone"; import User from "@iconify-icons/ri/user-3-fill"; +import UserName from "@iconify-icons/ri/user-4-line"; +import Mail from "@iconify-icons/ri/mail-open-line"; +import { postGetCodeAPI, postRegisterAPI } from "@/api/login"; const { t } = useI18n(); const checked = ref(false); const loading = ref(false); +const timer = ref(null); const ruleForm = reactive({ username: "", phone: "", - verifyCode: "", + gender: 1, + email: "", + nickname: "", + department_id: "", + code: "", password: "", repeatPassword: "" }); +/**当前选项 */ +const currentOption = ref(1); +/** + * 点击返回 + */ +const clickBack = () => { + if (currentOption.value >= 1) { + currentOption.value--; + } else { + currentOption.value = 1; + } +}; +/** + * 点击下一步 + */ +const clickNext = async () => { + await ruleFormRef.value.validate(async (valid, fields) => { + if (valid) { + if (currentOption.value <= 2) { + currentOption.value++; + } else { + currentOption.value = 2; + } + } + }); +}; const ruleFormRef = ref(); const { isDisabled, text } = useVerifyCode(); const repeatPasswordRule = [ { validator: (rule, value, callback) => { if (value === "") { - callback(new Error(transformI18n($t("login.purePassWordSureReg")))); + callback(new Error(transformI18n($t("login:PassWordSureReg")))); } else if (ruleForm.password !== value) { - callback( - new Error(transformI18n($t("login.purePassWordDifferentReg"))) - ); + callback(new Error(transformI18n($t("login:PassWordDifferentReg")))); } else { callback(); } @@ -45,19 +78,33 @@ const repeatPasswordRule = [ const onUpdate = async (formEl: FormInstance | undefined) => { loading.value = true; if (!formEl) return; - await formEl.validate(valid => { + await formEl.validate(async (valid, fields) => { if (valid) { if (checked.value) { - // 模拟请求,需根据实际开发进行修改 - setTimeout(() => { - message(transformI18n($t("login.pureRegisterSuccess")), { + const res = await postRegisterAPI({ + username: ruleForm.username, + password: ruleForm.password, + gender: ruleForm.gender, + email: ruleForm.email, + nickname: ruleForm.nickname, + phone: ruleForm.phone, + code: ruleForm.code, + department_id: ruleForm.department_id + }); + if (res.code === 200) { + message(transformI18n($t("login:RegisterSuccess")), { type: "success" }); loading.value = false; - }, 2000); + useUserStoreHook().SET_CURRENTPAGE(0); + } else { + message(res.msg, { + type: "error" + }); + } } else { loading.value = false; - message(transformI18n($t("login.pureTickPrivacy")), { + message(transformI18n($t("login:TickPrivacy")), { type: "warning" }); } @@ -66,9 +113,42 @@ const onUpdate = async (formEl: FormInstance | undefined) => { } }); }; - +const start = async ( + formEl: FormInstance | undefined, + props: FormItemProp, + time = 120 +) => { + if (!formEl) return; + const initTime = clone(time, true); + await formEl.validateField(props, async isValid => { + if (isValid) { + const res = await postGetCodeAPI({ + username: ruleForm.username, + title: "注册", + mail: ruleForm.email + }); + if (res.code === 200) { + clearInterval(timer.value); + isDisabled.value = true; + text.value = `${time}`; + timer.value = setInterval(() => { + if (time > 0) { + time -= 1; + text.value = `${time}`; + } else { + text.value = ""; + isDisabled.value = false; + clearInterval(timer.value); + time = initTime; + } + }, 1000); + } else { + message(res.msg, { type: "error" }); + } + } + }); +}; function onBack() { - useVerifyCode().end(); useUserStoreHook().SET_CURRENTPAGE(0); } @@ -77,15 +157,15 @@ function onBack() { - + + + + + + - + + + + + + - - + +
{{ text.length > 0 - ? text + t("login.pureInfo") - : t("login.pureGetVerifyCode") + ? text + t("login:Info") + : t("login:GetVerifyCode") }}
- + - + + + + + + + + + - {{ t("login.pureReadAccept") }} + {{ t("login:ReadAccept") }} - {{ t("login.purePrivacyPolicy") }} + {{ t("login:PrivacyPolicy") }} - - + - - {{ t("login.pureDefinite") }} - +
+ + {{ t("login:Laststep") }} + +
+
+
+ + +
+ + {{ t("login:Nextstep") }} + +
+
+
+ + +
+ + {{ t("login:Register") }} + +
- - {{ t("login.pureBack") }} - +
+ + {{ t("login:Back") }} + +
diff --git a/src/views/login/components/LoginUpdate.vue b/src/views/login/components/LoginUpdate.vue index 469a455..22c7b31 100644 --- a/src/views/login/components/LoginUpdate.vue +++ b/src/views/login/components/LoginUpdate.vue @@ -4,19 +4,24 @@ import { ref, reactive } from "vue"; import Motion from "../utils/motion"; import { message } from "@/utils/message"; import { updateRules } from "../utils/rule"; -import type { FormInstance } from "element-plus"; +import type { FormInstance, FormItemProp } from "element-plus"; import { useVerifyCode } from "../utils/verifyCode"; import { $t, transformI18n } from "@/plugins/i18n"; +import { clone } from "@pureadmin/utils"; import { useUserStoreHook } from "@/store/modules/user"; import { useRenderIcon } from "@/components/ReIcon/src/hooks"; +import { postGetCodeAPI, postResetPasswordAPI } from "@/api/login"; import Lock from "@iconify-icons/ri/lock-fill"; -import Iphone from "@iconify-icons/ep/iphone"; +import UserName from "@iconify-icons/ri/user-4-line"; +import Mail from "@iconify-icons/ri/mail-open-line"; const { t } = useI18n(); const loading = ref(false); +const timer = ref(null); const ruleForm = reactive({ - phone: "", - verifyCode: "", + username: "", + email: "", + code: "", password: "", repeatPassword: "" }); @@ -26,11 +31,9 @@ const repeatPasswordRule = [ { validator: (rule, value, callback) => { if (value === "") { - callback(new Error(transformI18n($t("login.purePassWordSureReg")))); + callback(new Error(transformI18n($t("login:PassWordSureReg")))); } else if (ruleForm.password !== value) { - callback( - new Error(transformI18n($t("login.purePassWordDifferentReg"))) - ); + callback(new Error(transformI18n($t("login:PassWordDifferentReg")))); } else { callback(); } @@ -42,23 +45,67 @@ const repeatPasswordRule = [ const onUpdate = async (formEl: FormInstance | undefined) => { loading.value = true; if (!formEl) return; - await formEl.validate(valid => { + await formEl.validate(async (valid, fields) => { if (valid) { - // 模拟请求,需根据实际开发进行修改 - setTimeout(() => { - message(transformI18n($t("login.purePassWordUpdateReg")), { + const res = await postResetPasswordAPI({ + username: ruleForm.username, + password: ruleForm.password, + code: ruleForm.code, + mail: ruleForm.email + }); + if (res.success) { + message(res.msg, { type: "success" }); loading.value = false; - }, 2000); + useUserStoreHook().SET_CURRENTPAGE(0); + } else { + message(res.msg, { + type: "error" + }); + loading.value = false; + } } else { loading.value = false; } }); }; - +const start = async ( + formEl: FormInstance | undefined, + props: FormItemProp, + time = 120 +) => { + if (!formEl) return; + const initTime = clone(time, true); + await formEl.validateField(props, async isValid => { + if (isValid) { + const res = await postGetCodeAPI({ + username: ruleForm.username, + title: "重置", + mail: ruleForm.email + }); + if (res.code === 200) { + clearInterval(timer.value); + isDisabled.value = true; + text.value = `${time}`; + timer.value = setInterval(() => { + if (time > 0) { + time -= 1; + text.value = `${time}`; + } else { + text.value = ""; + isDisabled.value = false; + clearInterval(timer.value); + time = initTime; + } + }, 1000); + } else { + message(res.msg, { type: "error" }); + } + } + }); +}; function onBack() { - useVerifyCode().end(); useUserStoreHook().SET_CURRENTPAGE(0); } @@ -71,34 +118,70 @@ function onBack() { size="large" > - + - - + + + + + +
{{ text.length > 0 - ? text + t("login.pureInfo") - : t("login.pureGetVerifyCode") + ? text + t("login:Info") + : t("login:GetVerifyCode") }}
@@ -111,8 +194,14 @@ function onBack() { v-model="ruleForm.password" clearable show-password - :placeholder="t('login.purePassword')" - :prefix-icon="useRenderIcon(Lock)" + :placeholder="t('login:Password')" + :prefix-icon=" + useRenderIcon(Lock, { + color: '#4380FF', + width: 32, + height: 32 + }) + " />
@@ -123,31 +212,42 @@ function onBack() { v-model="ruleForm.repeatPassword" clearable show-password - :placeholder="t('login.pureSure')" - :prefix-icon="useRenderIcon(Lock)" + :placeholder="t('login:Sure')" + :prefix-icon=" + useRenderIcon(Lock, { + color: '#4380FF', + width: 32, + height: 32 + }) + " /> - - {{ t("login.pureDefinite") }} - +
+ + {{ t("login:Definite") }} + +
- - {{ t("login.pureBack") }} - +
+ + {{ t("login:Back") }} + +
diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 81ff367..52b09dd 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -31,7 +31,9 @@ import Lock from "@iconify-icons/ri/lock-fill"; import Check from "@iconify-icons/ep/check"; import User from "@iconify-icons/ri/user-3-fill"; import Info from "@iconify-icons/ri/information-line"; -import { GetCaptchaAPI } from "@/api/user"; +import { GetCaptchaAPI } from "@/api/login"; +import { getUserInfoAPI } from "@/api/login"; +import { setUserInfo } from "@/utils/auth"; defineOptions({ name: "Login" @@ -79,17 +81,21 @@ const onLogin = async (formEl: FormInstance | undefined) => { if (res.code === 200) { useUserStoreHook().SET_ACCESSTOKEN(res.data.accessToken); // 获取后端路由 - return initRouter().then(() => { + return initRouter().then(async () => { disabled.value = true; + const res = await getUserInfoAPI(); + if (res.success) { + setUserInfo(res.data); + } router .push(getTopMenu(true).path) .then(() => { - message(t("login.pureLoginSuccess"), { type: "success" }); + message(t("login:LoginSuccess"), { type: "success" }); }) .finally(() => (disabled.value = false)); }); } else { - message(t("login.pureLoginFail"), { type: "error" }); + message(t("login:LoginFail"), { type: "error" }); await getImgCode(); } }) @@ -107,9 +113,9 @@ const immediateDebounce: any = debounce( /**获取验证码 */ const getImgCode = async () => { const res = await GetCaptchaAPI(); - if (res.code === 200) { - imgCode.value = res.data.image; - imgId.value = res.data.captchaId; + if (res.success) { + imgCode.value = res.data.captcha; + imgId.value = res.data.uuid; } }; @@ -122,9 +128,6 @@ useEventListener(document, "keydown", ({ code }) => { immediateDebounce(ruleFormRef.value); }); -// watch(imgCode, value => { -// useUserStoreHook().SET_VERIFYCODE(value); -// }); watch(checked, bool => { useUserStoreHook().SET_ISREMEMBERED(bool); }); @@ -208,7 +211,7 @@ onMounted(async () => { :rules="[ { required: true, - message: transformI18n($t('login.pureUsernameReg')), + message: transformI18n($t('login:UsernameReg')), trigger: 'blur' } ]" @@ -217,7 +220,7 @@ onMounted(async () => { @@ -229,7 +232,7 @@ onMounted(async () => { v-model="ruleForm.password" clearable show-password - :placeholder="t('login.purePassword')" + :placeholder="t('login:Password')" :prefix-icon="useRenderIcon(Lock)" /> @@ -240,7 +243,7 @@ onMounted(async () => {