diff --git a/locales/en.yaml b/locales/en.yaml index 7a000ec..40afb26 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -1,124 +1,142 @@ -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 +buttons:Search: Search +buttons:Reset: Reset +buttons:Add: Add +buttons:Update: Update +buttons:Delete: Delete +buttons:Export: Export +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..528057c 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -1,124 +1,143 @@ -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: 关 +buttons:Search: 搜索 +buttons:Reset: 重置 +buttons:Add: 添加 +buttons:Update: 修改 +buttons:Delete: 删除 +buttons:Export: 导出 +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/i18n.ts b/src/api/i18n.ts new file mode 100644 index 0000000..8128632 --- /dev/null +++ b/src/api/i18n.ts @@ -0,0 +1,201 @@ +import { http } from "@/utils/http"; +import type { LanguageInfo, TranslationInfo } from "types/system"; + +/** + * 添加语言类型参数 + */ +type AddLocaleParams = { + /**编码 */ + code: string; + /**名称 */ + name: string; +}; + +/** + * 添加语言类型 + * @param data + * @returns + */ +export const postAddLocaleAPI = (data: AddLocaleParams) => { + return http.request("post", "/api/i18n/addLocale", { + data + }); +}; + +/** + * 删除语言类型 + * @param id + * @returns + */ +export const deleteLocaleAPI = (id: string) => { + return http.request("post", `/api/i18n/deleteLocale/${id}`); +}; + +/** + * 修改语言类型 + */ + +export const putUpdateLocaleAPI = (data: AddLocaleParams, id: string) => { + return http.request("post", `/api/i18n/updateLocale/${id}`, { + data + }); +}; + +/** + * 获取语言类型信息 + */ +export const getLocaleInfoAPI = (id: string) => { + return http.request("get", `/api/i18n/locale/info/${id}`); +}; + +type GetLoacleListParams = { + /**页码 */ + page: number; + /**每页条数 */ + pageSize: number; + /**语言名称 */ + name?: string; + /**语言编码 */ + code?: string; +}; + +type GetLocaleListResult = { + /**语言列表 */ + result: LanguageInfo[]; + /**总条数 */ + total: number; + /**页码 */ + page: number; +}; +export const getLocaleListAPI = (params: GetLoacleListParams) => { + return http.request("get", "/api/i18n/locale/list", { + params + }); +}; + +/** + * 添加翻译 + */ +type AddI18nParams = { + /**键值 */ + key: string; + /**翻译内容 */ + translation: string; + /**语言ID */ + locale_id: string; +}; + +/** + * 添加翻译 + * @param data + * @returns + */ +export const postAddI18nAPI = (data: AddI18nParams) => { + return http.request("post", "/api/i18n/addI18n", { + data + }); +}; + +/** + * 获取翻译列表参数 + */ +type GetI18nListParams = { + /**页码 */ + page: number; + /**每页条数 */ + pageSize: number; + /**语言ID */ + locale_id?: string; + /**键值 */ + key?: string; + /**翻译内容 */ + translation?: string; +}; +/** + * 获取翻译列表 + */ +type GetI18nListResult = { + /**翻译列表 */ + result: TranslationInfo[]; + /**总条数 */ + total: number; + /**页码 */ + page: number; +}; + +/** + * 获取翻译列表 + * @param params + * @returns + */ +export const getI18nListAPI = (params: GetI18nListParams) => { + return http.request("get", "/api/i18n/list", { + params + }); +}; + +/** + * 获取翻译详情 + */ +export const getI18nInfoAPI = (id: string) => { + return http.request("get", `/api/i18n/info/${id}`); +}; + +/** + * 删除翻译 + * @param id + * @returns + */ +export const deleteI18nAPI = (id: string) => { + return http.request("post", `/api/i18n/deleteI18n/${id}`); +}; +/** + * 修改翻译 + * @param data + * @param id + * @returns + */ +export const putUpdateI18nAPI = (data: AddI18nParams, id: string) => { + return http.request("post", `/api/i18n/updateI18n/${id}`, { + data + }); +}; + +/** + * 获取国际化处理列表结果 + */ +type GetI18nHandleListResult = { + /** + * 翻译列表 + */ + data: object; + /** + * 名称 + */ + name: string; + /** + * 编码 + */ + locale: string; +}; + +/** + * 获取国际化处理列表 + * @param id + * @returns + */ +export const getI18nHandleListAPI = (id: string) => { + return http.request( + "get", + `/api/i18n/infoList/${id}` + ); +}; + +/** + * 获取国际化数据 + * @param locale 语言代码 + * @returns 国际化数据 + */ +export const getLocaleI18nAPI = (locale: string) => { + return http.request>("get", `/api/i18n/data/${locale}`); +}; 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/system.ts b/src/api/system.ts new file mode 100644 index 0000000..2e79317 --- /dev/null +++ b/src/api/system.ts @@ -0,0 +1,82 @@ +import { http } from "@/utils/http"; +import type { DepartmentInfo } from "types/system"; +import { filterEmptyObject } from "./utils"; + +// ---------------------------部门相关------------------------------------- + +/** + * 获取部门列表参数 + */ +type GetDepartmentListParams = { + /**当前页 */ + page: number; + /**每页数量 */ + pageSize: number; + /**部门ID */ + id?: string; + /**部门名称 */ + name?: string; + /**附属部门ID */ + parent_id?: string; + /**部门负责人 */ + principal?: string; + /**部门电话 */ + phone?: number | string; + /**部门邮件 */ + email?: string; + /**备注 */ + remark?: string; + /**排序 */ + sort?: number | string; +}; +/**获取部门列表 */ +export const getDepartmentListAPI = (params: GetDepartmentListParams) => { + return http.request>( + "get", + `/api/department/list`, + { + params: filterEmptyObject(params) + } + ); +}; + +/**更新部门数据 */ +export const putUpdateDepartmentAPI = ( + data: AddDepartmentParams, + id: string +) => { + return http.request("post", `/api/department/update/${id}`, { + data + }); +}; + +/**添加部门数据参数 */ +type AddDepartmentParams = { + /**部门名称 */ + name: string; + /**父部门ID */ + parent_id: string; + /**排序 */ + sort: number; + /**部门负责人 */ + principal: string; + /**部门电话 */ + phone: number | string; + /**部门邮件 */ + email: string; + /**备注 */ + remark: string; + /**状态 */ + status: number; +}; + +/**添加部门数据 */ +export const postAddDepartmentAPI = (data: AddDepartmentParams) => { + return http.request("post", `/api/department/add`, { + data + }); +}; +/**删除部门及其附属部门 */ +export const deleteDepartmentAPI = (id: string) => { + return http.request("post", `/api/department/delete/${id}`); +}; 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/api/utils.ts b/src/api/utils.ts new file mode 100644 index 0000000..fc8ca2b --- /dev/null +++ b/src/api/utils.ts @@ -0,0 +1,13 @@ +/**过滤字典中的空值字段 */ +export const filterEmptyObject = (data: object): object => { + // 初始化一个空对象用于存储非空值字段 + return Object.keys(data).reduce((acc, cur) => { + // 检查当前字段的值是否为空 + if (data[cur] !== null && data[cur] !== undefined && data[cur] !== "") { + // 如果不为空,则将其添加到结果对象中 + acc[cur] = data[cur]; + } + // 返回累积的结果对象 + return acc; + }, {}); +}; diff --git a/src/assets/user.jpg b/src/assets/user.jpg deleted file mode 100644 index a2973ac..0000000 Binary files a/src/assets/user.jpg and /dev/null differ diff --git a/src/assets/user.png b/src/assets/user.png new file mode 100644 index 0000000..30ba3da Binary files /dev/null and b/src/assets/user.png differ 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 () => {