feat: 添加部门管理
266
locales/en.yaml
@@ -1,124 +1,142 @@
|
|||||||
buttons:
|
buttons:AccountSettings: Account
|
||||||
pureLoginOut: LoginOut
|
buttons:LoginOut: LoginOut
|
||||||
pureLogin: Login
|
buttons:Login: Login
|
||||||
pureOpenSystemSet: Open System Configs
|
buttons:OpenSystemSet: Open System Configs
|
||||||
pureReload: Reload
|
buttons:Reload: Reload
|
||||||
pureCloseCurrentTab: Close CurrentTab
|
buttons:CloseCurrentTab: Close CurrentTab
|
||||||
pureCloseLeftTabs: Close LeftTabs
|
buttons:CloseLeftTabs: Close LeftTabs
|
||||||
pureCloseRightTabs: Close RightTabs
|
buttons:CloseRightTabs: Close RightTabs
|
||||||
pureCloseOtherTabs: Close OtherTabs
|
buttons:CloseOtherTabs: Close OtherTabs
|
||||||
pureCloseAllTabs: Close AllTabs
|
buttons:CloseAllTabs: Close AllTabs
|
||||||
pureContentFullScreen: Content FullScreen
|
buttons:ContentFullScreen: Content FullScreen
|
||||||
pureContentExitFullScreen: Content ExitFullScreen
|
buttons:ContentExitFullScreen: Content ExitFullScreen
|
||||||
pureClickCollapse: Collapse
|
buttons:ClickCollapse: Collapse
|
||||||
pureClickExpand: Expand
|
buttons:ClickExpand: Expand
|
||||||
pureConfirm: Confirm
|
buttons:Confirm: Confirm
|
||||||
pureSwitch: Switch
|
buttons:Cancel: Cancel
|
||||||
pureClose: Close
|
buttons:Switch: Switch
|
||||||
pureBackTop: BackTop
|
buttons:Close: Close
|
||||||
pureOpenText: Open
|
buttons:BackTop: BackTop
|
||||||
pureCloseText: Close
|
buttons:OpenText: Open
|
||||||
search:
|
buttons:CloseText: Close
|
||||||
pureTotal: Total
|
buttons:Search: Search
|
||||||
pureHistory: History
|
buttons:Reset: Reset
|
||||||
pureCollect: Collect
|
buttons:Add: Add
|
||||||
pureDragSort: (Drag Sort)
|
buttons:Update: Update
|
||||||
pureEmpty: Empty
|
buttons:Delete: Delete
|
||||||
purePlaceholder: Search Menu
|
buttons:Export: Export
|
||||||
panel:
|
search:Total: Total
|
||||||
pureSystemSet: System Configs
|
search:History: History
|
||||||
pureCloseSystemSet: Close System Configs
|
search:Collect: Collect
|
||||||
pureClearCacheAndToLogin: Clear cache and return to login page
|
search:DragSort: (Drag Sort)
|
||||||
pureClearCache: Clear Cache
|
search:Empty: Empty
|
||||||
pureOverallStyle: Overall Style
|
search:Placeholder: Search Menu
|
||||||
pureOverallStyleLight: Light
|
panel:SystemSet: System Configs
|
||||||
pureOverallStyleLightTip: Set sail freshly and light up the comfortable work interface
|
panel:CloseSystemSet: Close System Configs
|
||||||
pureOverallStyleDark: Dark
|
panel:ClearCacheAndToLogin: Clear cache and return to login page
|
||||||
pureOverallStyleDarkTip: Moonlight Overture, indulge in the tranquility and elegance of the night
|
panel:ClearCache: Clear Cache
|
||||||
pureOverallStyleSystem: Auto
|
panel:OverallStyle: Overall Style
|
||||||
pureOverallStyleSystemTip: Synchronize time, the interface naturally responds to morning and dusk
|
panel:OverallStyleLight: Light
|
||||||
pureThemeColor: Theme Color
|
panel:OverallStyleLightTip: Set sail freshly and light up the comfortable work interface
|
||||||
pureLayoutModel: Layout Model
|
panel:OverallStyleDark: Dark
|
||||||
pureVerticalTip: The menu on the left is familiar and friendly
|
panel:OverallStyleDarkTip: Moonlight Overture, indulge in the tranquility and elegance of the night
|
||||||
pureHorizontalTip: Top menu, concise overview
|
panel:OverallStyleSystem: Auto
|
||||||
pureMixTip: Mixed menu, flexible
|
panel:OverallStyleSystemTip: Synchronize time, the interface naturally responds to morning and dusk
|
||||||
pureStretch: Stretch Page
|
panel:ThemeColor: Theme Color
|
||||||
pureStretchFixed: Fixed
|
panel:LayoutModel: Layout Model
|
||||||
pureStretchFixedTip: Compact pages make it easy to find the information you need
|
panel:VerticalTip: The menu on the left is familiar and friendly
|
||||||
pureStretchCustom: Custom
|
panel:HorizontalTip: Top menu, concise overview
|
||||||
pureStretchCustomTip: Minimum 1280, maximum 1600
|
panel:MixTip: Mixed menu, flexible
|
||||||
pureTagsStyle: Tags Style
|
panel:Stretch: Stretch Page
|
||||||
pureTagsStyleSmart: Smart
|
panel:StretchFixed: Fixed
|
||||||
pureTagsStyleSmartTip: Smart tags add fun and brilliance
|
panel:StretchFixedTip: Compact pages make it easy to find the information you need
|
||||||
pureTagsStyleCard: Card
|
panel:StretchCustom: Custom
|
||||||
pureTagsStyleCardTip: Card tags for efficient browsing
|
panel:StretchCustomTip: Minimum 1280, maximum 1600
|
||||||
pureTagsStyleChrome: Chrome
|
panel:TagsStyle: Tags Style
|
||||||
pureTagsStyleChromeTip: Chrome style is classic and elegant
|
panel:TagsStyleSmart: Smart
|
||||||
pureInterfaceDisplay: Interface Display
|
panel:TagsStyleSmartTip: Smart tags add fun and brilliance
|
||||||
pureGreyModel: Grey Model
|
panel:TagsStyleCard: Card
|
||||||
pureWeakModel: Weak Model
|
panel:TagsStyleCardTip: Card tags for efficient browsing
|
||||||
pureHiddenTags: Hidden Tags
|
panel:TagsStyleChrome: Chrome
|
||||||
pureHiddenFooter: Hidden Footer
|
panel:TagsStyleChromeTip: Chrome style is classic and elegant
|
||||||
pureMultiTagsCache: MultiTags Cache
|
panel:InterfaceDisplay: Interface Display
|
||||||
menus:
|
panel:GreyModel: Grey Model
|
||||||
pureHome: Home
|
panel:WeakModel: Weak Model
|
||||||
pureLogin: Login
|
panel:HiddenTags: Hidden Tags
|
||||||
pureAbnormal: Abnormal Page
|
panel:HiddenFooter: Hidden Footer
|
||||||
pureFourZeroFour: "404"
|
panel:MultiTagsCache: MultiTags Cache
|
||||||
pureFourZeroOne: "403"
|
menus:Home: Home
|
||||||
pureFive: "500"
|
menus:Login: Login
|
||||||
purePermission: Permission Manage
|
menus:Empty: Empty Page
|
||||||
purePermissionPage: Page Permission
|
menus:SysManagement: System Manage
|
||||||
purePermissionButton: Button Permission
|
menus:User: User Manage
|
||||||
purePermissionButtonRouter: Route return button permission
|
menus:Role: Role Manage
|
||||||
purePermissionButtonLogin: Login interface return button permission
|
menus:SystemMenu: Menu Manage
|
||||||
status:
|
menus:Dept: Dept Manage
|
||||||
pureLoad: Loading...
|
menus:SysMonitor: System Monitor
|
||||||
pureMessage: Message
|
menus:OnlineUser: Online User
|
||||||
pureNotify: Notify
|
menus:LoginLog: Login Log
|
||||||
pureTodo: Todo
|
menus:OperationLog: Operation Log
|
||||||
pureNoMessage: No Message
|
menus:Abnormal: Abnormal Page
|
||||||
pureNoNotify: No Notify
|
menus:FourZeroFour: "404"
|
||||||
pureNoTodo: No Todo
|
menus:FourZeroOne: "403"
|
||||||
login:
|
menus:Five: "500"
|
||||||
pureUsername: Username
|
status:Load: Loading...
|
||||||
purePassword: Password
|
status:Message: Message
|
||||||
pureVerifyCode: VerifyCode
|
status:Notify: Notify
|
||||||
pureRemember: days no need to login
|
status:Todo: Todo
|
||||||
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.
|
status:NoMessage: No Message
|
||||||
pureSure: Sure Password
|
status:NoNotify: No Notify
|
||||||
pureForget: Forget Password?
|
status:NoTodo: No Todo
|
||||||
pureLogin: Login
|
login:Username: Username
|
||||||
pureThirdLogin: Third Login
|
login:Nickname: Nickname
|
||||||
purePhoneLogin: Phone Login
|
login:Password: Password
|
||||||
pureQRCodeLogin: QRCode Login
|
login:VerifyCode: VerifyCode
|
||||||
pureRegister: Register
|
login:Remember: days no need to login
|
||||||
pureWeChatLogin: WeChat 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.
|
||||||
pureAlipayLogin: Alipay Login
|
login:Sure: Sure Password
|
||||||
pureQQLogin: QQ Login
|
login:Forget: Forget Password?
|
||||||
pureWeiBoLogin: Weibo Login
|
login:Login: Login
|
||||||
purePhone: Phone
|
login:Nextstep: Nextstep
|
||||||
pureSmsVerifyCode: SMS VerifyCode
|
login:Laststep: Laststep
|
||||||
pureBack: Back
|
login:ThirdLogin: Third Login
|
||||||
pureTest: Mock Test
|
login:PhoneLogin: Phone Login
|
||||||
pureTip: After scanning the code, click "Confirm" to complete the login
|
login:QRCodeLogin: QRCode Login
|
||||||
pureDefinite: Definite
|
login:Register: Register
|
||||||
pureLoginSuccess: Login Success
|
login:WeChatLogin: WeChat Login
|
||||||
pureLoginFail: Login Fail
|
login:AlipayLogin: Alipay Login
|
||||||
pureRegisterSuccess: Regist Success
|
login:QQLogin: QQ Login
|
||||||
pureTickPrivacy: Please tick Privacy Policy
|
login:WeiBoLogin: Weibo Login
|
||||||
pureReadAccept: I have read it carefully and accept
|
login:Phone: Phone
|
||||||
purePrivacyPolicy: Privacy Policy
|
login:Email: Email
|
||||||
pureGetVerifyCode: Get VerifyCode
|
login:SmsVerifyCode: SMS VerifyCode
|
||||||
pureInfo: Seconds
|
login:EmailVerifyCode: Email VerifyCode
|
||||||
pureUsernameReg: Please enter username
|
login:Back: Back
|
||||||
purePassWordReg: Please enter password
|
login:Test: Mock Test
|
||||||
pureVerifyCodeReg: Please enter verify code
|
login:Tip: After scanning the code, click "Confirm" to complete the login
|
||||||
pureVerifyCodeCorrectReg: Please enter correct verify code
|
login:Definite: Definite
|
||||||
pureVerifyCodeSixReg: Please enter a 6-digit verify code
|
login:LoginSuccess: Login Success
|
||||||
purePhoneReg: Please enter the phone
|
login:LoginFail: Login Fail
|
||||||
purePhoneCorrectReg: Please enter the correct phone number format
|
login:RegisterSuccess: Regist Success
|
||||||
purePassWordRuleReg: The password format should be any combination of 8-18 digits
|
login:TickPrivacy: Please tick Privacy Policy
|
||||||
purePassWordSureReg: Please enter confirm password
|
login:ReadAccept: I have read it carefully and accept
|
||||||
purePassWordDifferentReg: The two passwords do not match!
|
login:PrivacyPolicy: Privacy Policy
|
||||||
purePassWordUpdateReg: Password has been updated
|
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
|
||||||
|
|||||||
@@ -1,124 +1,143 @@
|
|||||||
buttons:
|
buttons:AccountSettings: 账户设置
|
||||||
pureLoginOut: 退出系统
|
buttons:LoginOut: 退出系统
|
||||||
pureLogin: 登录
|
buttons:Login: 登录
|
||||||
pureOpenSystemSet: 打开系统配置
|
buttons:OpenSystemSet: 打开系统配置
|
||||||
pureReload: 重新加载
|
buttons:Reload: 重新加载
|
||||||
pureCloseCurrentTab: 关闭当前标签页
|
buttons:CloseCurrentTab: 关闭当前标签页
|
||||||
pureCloseLeftTabs: 关闭左侧标签页
|
buttons:CloseLeftTabs: 关闭左侧标签页
|
||||||
pureCloseRightTabs: 关闭右侧标签页
|
buttons:CloseRightTabs: 关闭右侧标签页
|
||||||
pureCloseOtherTabs: 关闭其他标签页
|
buttons:CloseOtherTabs: 关闭其他标签页
|
||||||
pureCloseAllTabs: 关闭全部标签页
|
buttons:CloseAllTabs: 关闭全部标签页
|
||||||
pureContentFullScreen: 内容区全屏
|
buttons:ContentFullScreen: 内容区全屏
|
||||||
pureContentExitFullScreen: 内容区退出全屏
|
buttons:ContentExitFullScreen: 内容区退出全屏
|
||||||
pureClickCollapse: 点击折叠
|
buttons:ClickCollapse: 点击折叠
|
||||||
pureClickExpand: 点击展开
|
buttons:ClickExpand: 点击展开
|
||||||
pureConfirm: 确认
|
buttons:Confirm: 确认
|
||||||
pureSwitch: 切换
|
buttons:Switch: 切换
|
||||||
pureClose: 关闭
|
buttons:Close: 关闭
|
||||||
pureBackTop: 回到顶部
|
buttons:Cancel: 取消
|
||||||
pureOpenText: 开
|
buttons:BackTop: 回到顶部
|
||||||
pureCloseText: 关
|
buttons:OpenText: 开
|
||||||
search:
|
buttons:CloseText: 关
|
||||||
pureTotal: 共
|
buttons:Search: 搜索
|
||||||
pureHistory: 搜索历史
|
buttons:Reset: 重置
|
||||||
pureCollect: 收藏
|
buttons:Add: 添加
|
||||||
pureDragSort: (可拖拽排序)
|
buttons:Update: 修改
|
||||||
pureEmpty: 暂无搜索结果
|
buttons:Delete: 删除
|
||||||
purePlaceholder: 搜索菜单(支持拼音搜索)
|
buttons:Export: 导出
|
||||||
panel:
|
search:Total: 共
|
||||||
pureSystemSet: 系统配置
|
search:History: 搜索历史
|
||||||
pureCloseSystemSet: 关闭配置
|
search:Collect: 收藏
|
||||||
pureClearCacheAndToLogin: 清空缓存并返回登录页
|
search:DragSort: (可拖拽排序)
|
||||||
pureClearCache: 清空缓存
|
search:Empty: 暂无搜索结果
|
||||||
pureOverallStyle: 整体风格
|
search:Placeholder: 搜索菜单(支持拼音搜索)
|
||||||
pureOverallStyleLight: 浅色
|
panel:SystemSet: 系统配置
|
||||||
pureOverallStyleLightTip: 清新启航,点亮舒适的工作界面
|
panel:CloseSystemSet: 关闭配置
|
||||||
pureOverallStyleDark: 深色
|
panel:ClearCacheAndToLogin: 清空缓存并返回登录页
|
||||||
pureOverallStyleDarkTip: 月光序曲,沉醉于夜的静谧雅致
|
panel:ClearCache: 清空缓存
|
||||||
pureOverallStyleSystem: 自动
|
panel:OverallStyle: 整体风格
|
||||||
pureOverallStyleSystemTip: 同步时光,界面随晨昏自然呼应
|
panel:OverallStyleLight: 浅色
|
||||||
pureThemeColor: 主题色
|
panel:OverallStyleLightTip: 清新启航,点亮舒适的工作界面
|
||||||
pureLayoutModel: 导航模式
|
panel:OverallStyleDark: 深色
|
||||||
pureVerticalTip: 左侧菜单,亲切熟悉
|
panel:OverallStyleDarkTip: 月光序曲,沉醉于夜的静谧雅致
|
||||||
pureHorizontalTip: 顶部菜单,简洁概览
|
panel:OverallStyleSystem: 自动
|
||||||
pureMixTip: 混合菜单,灵活多变
|
panel:OverallStyleSystemTip: 同步时光,界面随晨昏自然呼应
|
||||||
pureStretch: 页宽
|
panel:ThemeColor: 主题色
|
||||||
pureStretchFixed: 固定
|
panel:LayoutModel: 导航模式
|
||||||
pureStretchFixedTip: 紧凑页面,轻松找到所需信息
|
panel:VerticalTip: 左侧菜单,亲切熟悉
|
||||||
pureStretchCustom: 自定义
|
panel:HorizontalTip: 顶部菜单,简洁概览
|
||||||
pureStretchCustomTip: 最小1280、最大1600
|
panel:MixTip: 混合菜单,灵活多变
|
||||||
pureTagsStyle: 页签风格
|
panel:Stretch: 页宽
|
||||||
pureTagsStyleSmart: 灵动
|
panel:StretchFixed: 固定
|
||||||
pureTagsStyleSmartTip: 灵动标签,添趣生辉
|
panel:StretchFixedTip: 紧凑页面,轻松找到所需信息
|
||||||
pureTagsStyleCard: 卡片
|
panel:StretchCustom: 自定义
|
||||||
pureTagsStyleCardTip: 卡片标签,高效浏览
|
panel:StretchCustomTip: 最小1280、最大1600
|
||||||
pureTagsStyleChrome: 谷歌
|
panel:TagsStyle: 页签风格
|
||||||
pureTagsStyleChromeTip: 谷歌风格,经典美观
|
panel:TagsStyleSmart: 灵动
|
||||||
pureInterfaceDisplay: 界面显示
|
panel:TagsStyleSmartTip: 灵动标签,添趣生辉
|
||||||
pureGreyModel: 灰色模式
|
panel:TagsStyleCard: 卡片
|
||||||
pureWeakModel: 色弱模式
|
panel:TagsStyleCardTip: 卡片标签,高效浏览
|
||||||
pureHiddenTags: 隐藏标签页
|
panel:TagsStyleChrome: 谷歌
|
||||||
pureHiddenFooter: 隐藏页脚
|
panel:TagsStyleChromeTip: 谷歌风格,经典美观
|
||||||
pureMultiTagsCache: 页签持久化
|
panel:InterfaceDisplay: 界面显示
|
||||||
menus:
|
panel:GreyModel: 灰色模式
|
||||||
pureHome: 首页
|
panel:WeakModel: 色弱模式
|
||||||
pureLogin: 登录
|
panel:HiddenTags: 隐藏标签页
|
||||||
pureAbnormal: 异常页面
|
panel:HiddenFooter: 隐藏页脚
|
||||||
pureFourZeroFour: "404"
|
panel:MultiTagsCache: 页签持久化
|
||||||
pureFourZeroOne: "403"
|
menus:Home: 首页
|
||||||
pureFive: "500"
|
menus:Login: 登录
|
||||||
purePermission: 权限管理
|
menus:Empty: 无Layout页
|
||||||
purePermissionPage: 页面权限
|
menus:SysManagement: 系统管理
|
||||||
purePermissionButton: 按钮权限
|
menus:User: 用户管理
|
||||||
purePermissionButtonRouter: 路由返回按钮权限
|
menus:Role: 角色管理
|
||||||
purePermissionButtonLogin: 登录接口返回按钮权限
|
menus:SystemMenu: 菜单管理
|
||||||
status:
|
menus:Dept: 部门管理
|
||||||
pureLoad: 加载中...
|
menus:SysMonitor: 系统监控
|
||||||
pureMessage: 消息
|
menus:OnlineUser: 在线用户
|
||||||
pureNotify: 通知
|
menus:LoginLog: 登录日志
|
||||||
pureTodo: 待办
|
menus:OperationLog: 操作日志
|
||||||
pureNoMessage: 暂无消息
|
menus:Abnormal: 异常页面
|
||||||
pureNoNotify: 暂无通知
|
menus:FourZeroFour: "404"
|
||||||
pureNoTodo: 暂无待办
|
menus:FourZeroOne: "403"
|
||||||
login:
|
menus:Five: "500"
|
||||||
pureUsername: 账号
|
status:Load: 加载中...
|
||||||
purePassword: 密码
|
status:Message: 消息
|
||||||
pureVerifyCode: 验证码
|
status:Notify: 通知
|
||||||
pureRemember: 天内免登录
|
status:Todo: 待办
|
||||||
pureRememberInfo: 勾选并登录后,规定天数内无需输入用户名和密码会自动登入系统
|
status:NoMessage: 暂无消息
|
||||||
pureSure: 确认密码
|
status:NoNotify: 暂无通知
|
||||||
pureForget: 忘记密码?
|
status:NoTodo: 暂无待办
|
||||||
pureLogin: 登录
|
login:Username: 账号
|
||||||
pureThirdLogin: 第三方登录
|
login:Nickname: 昵称
|
||||||
purePhoneLogin: 手机登录
|
login:Password: 密码
|
||||||
pureQRCodeLogin: 二维码登录
|
login:VerifyCode: 验证码
|
||||||
pureRegister: 注册
|
login:Remember: 天内免登录
|
||||||
pureWeChatLogin: 微信登录
|
login:RememberInfo: 勾选并登录后,规定天数内无需输入用户名和密码会自动登入系统
|
||||||
pureAlipayLogin: 支付宝登录
|
login:Sure: 确认密码
|
||||||
pureQQLogin: QQ登录
|
login:Forget: 忘记密码?
|
||||||
pureWeiBoLogin: 微博登录
|
login:Login: 登录
|
||||||
purePhone: 手机号码
|
login:Nextstep: 下一步
|
||||||
pureSmsVerifyCode: 短信验证码
|
login:Laststep: 上一步
|
||||||
pureBack: 返回
|
login:ThirdLogin: 第三方登录
|
||||||
pureTest: 模拟测试
|
login:PhoneLogin: 手机登录
|
||||||
pureTip: 扫码后点击"确认",即可完成登录
|
login:QRCodeLogin: 二维码登录
|
||||||
pureDefinite: 确定
|
login:Register: 注册
|
||||||
pureLoginSuccess: 登录成功
|
login:WeChatLogin: 微信登录
|
||||||
pureLoginFail: 登录失败
|
login:AlipayLogin: 支付宝登录
|
||||||
pureRegisterSuccess: 注册成功
|
login:QQLogin: QQ登录
|
||||||
pureTickPrivacy: 请勾选隐私政策
|
login:WeiBoLogin: 微博登录
|
||||||
pureReadAccept: 我已仔细阅读并接受
|
login:Phone: 手机号码
|
||||||
purePrivacyPolicy: 《隐私政策》
|
login:Email: 邮箱
|
||||||
pureGetVerifyCode: 获取验证码
|
login:SmsVerifyCode: 短信验证码
|
||||||
pureInfo: 秒后重新获取
|
login:EmailVerifyCode: 邮箱验证码
|
||||||
pureUsernameReg: 请输入账号
|
login:Back: 返回
|
||||||
purePassWordReg: 请输入密码
|
login:Test: 模拟测试
|
||||||
pureVerifyCodeReg: 请输入验证码
|
login:Tip: 扫码后点击"确认",即可完成登录
|
||||||
pureVerifyCodeCorrectReg: 请输入正确的验证码
|
login:Definite: 确定
|
||||||
pureVerifyCodeSixReg: 请输入6位数字验证码
|
login:LoginSuccess: 登录成功
|
||||||
purePhoneReg: 请输入手机号码
|
login:LoginFail: 登录失败
|
||||||
purePhoneCorrectReg: 请输入正确的手机号码格式
|
login:RegisterSuccess: 注册成功
|
||||||
purePassWordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合
|
login:TickPrivacy: 请勾选隐私政策
|
||||||
purePassWordSureReg: 请输入确认密码
|
login:ReadAccept: 我已仔细阅读并接受
|
||||||
purePassWordDifferentReg: 两次密码不一致!
|
login:PrivacyPolicy: 《隐私政策》
|
||||||
purePassWordUpdateReg: 修改密码成功
|
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: 退出取消
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,11 @@
|
|||||||
"@pureadmin/utils": "^2.5.0",
|
"@pureadmin/utils": "^2.5.0",
|
||||||
"@vueuse/core": "^12.0.0",
|
"@vueuse/core": "^12.0.0",
|
||||||
"@vueuse/motion": "^2.2.6",
|
"@vueuse/motion": "^2.2.6",
|
||||||
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"cropperjs": "^1.6.2",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"element-plus": "^2.9.0",
|
"element-plus": "^2.9.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
@@ -23,12 +23,18 @@ importers:
|
|||||||
'@vueuse/motion':
|
'@vueuse/motion':
|
||||||
specifier: ^2.2.6
|
specifier: ^2.2.6
|
||||||
version: 2.2.6(rollup@4.28.1)(vue@3.5.13(typescript@5.6.3))
|
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:
|
animate.css:
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.9
|
specifier: ^1.7.9
|
||||||
version: 1.7.9
|
version: 1.7.9
|
||||||
|
cropperjs:
|
||||||
|
specifier: ^1.6.2
|
||||||
|
version: 1.6.2
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.13
|
specifier: ^1.11.13
|
||||||
version: 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}
|
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==, tarball: https://registry.npmmirror.com/@intlify/shared/-/shared-11.0.0-rc.1.tgz}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
'@intlify/shared@11.0.1':
|
'@intlify/shared@11.1.1':
|
||||||
resolution: {integrity: sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==, tarball: https://registry.npmmirror.com/@intlify/shared/-/shared-11.0.1.tgz}
|
resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==, tarball: https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.1.tgz}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
|
|
||||||
'@intlify/unplugin-vue-i18n@6.0.1':
|
'@intlify/unplugin-vue-i18n@6.0.1':
|
||||||
@@ -1275,6 +1281,9 @@ packages:
|
|||||||
'@vueuse/shared@9.13.0':
|
'@vueuse/shared@9.13.0':
|
||||||
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
|
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:
|
JSONStream@1.3.5:
|
||||||
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1570,6 +1579,9 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
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:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -1923,7 +1935,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==}
|
resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==}
|
||||||
|
|
||||||
fastest-levenshtein@1.0.16:
|
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'}
|
engines: {node: '>= 4.9.1'}
|
||||||
|
|
||||||
fastq@1.17.1:
|
fastq@1.17.1:
|
||||||
@@ -4220,14 +4232,14 @@ snapshots:
|
|||||||
|
|
||||||
'@intlify/shared@11.0.0-rc.1': {}
|
'@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))':
|
'@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:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0(jiti@2.4.1))
|
'@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/bundle-utils': 10.0.0(vue-i18n@10.0.5(vue@3.5.13(typescript@5.6.3)))
|
||||||
'@intlify/shared': 11.0.1
|
'@intlify/shared': 11.1.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/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)
|
'@rollup/pluginutils': 5.1.3(rollup@4.28.1)
|
||||||
'@typescript-eslint/scope-manager': 8.18.0
|
'@typescript-eslint/scope-manager': 8.18.0
|
||||||
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.6.3)
|
'@typescript-eslint/typescript-estree': 8.18.0(typescript@5.6.3)
|
||||||
@@ -4249,11 +4261,11 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- 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:
|
dependencies:
|
||||||
'@babel/parser': 7.26.3
|
'@babel/parser': 7.26.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@intlify/shared': 11.0.1
|
'@intlify/shared': 11.1.1
|
||||||
'@vue/compiler-dom': 3.5.13
|
'@vue/compiler-dom': 3.5.13
|
||||||
vue: 3.5.13(typescript@5.6.3)
|
vue: 3.5.13(typescript@5.6.3)
|
||||||
vue-i18n: 10.0.5(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/composition-api'
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@zxcvbn-ts/core@3.0.4':
|
||||||
|
dependencies:
|
||||||
|
fastest-levenshtein: 1.0.16
|
||||||
|
|
||||||
JSONStream@1.3.5:
|
JSONStream@1.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
jsonparse: 1.3.1
|
jsonparse: 1.3.1
|
||||||
@@ -5150,6 +5166,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.3
|
typescript: 5.6.3
|
||||||
|
|
||||||
|
cropperjs@1.6.2: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|||||||
201
src/api/i18n.ts
Normal file
@@ -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<null>("post", "/api/i18n/addLocale", {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除语言类型
|
||||||
|
* @param id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteLocaleAPI = (id: string) => {
|
||||||
|
return http.request<null>("post", `/api/i18n/deleteLocale/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改语言类型
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const putUpdateLocaleAPI = (data: AddLocaleParams, id: string) => {
|
||||||
|
return http.request<null>("post", `/api/i18n/updateLocale/${id}`, {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言类型信息
|
||||||
|
*/
|
||||||
|
export const getLocaleInfoAPI = (id: string) => {
|
||||||
|
return http.request<LanguageInfo>("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<GetLocaleListResult>("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<null>("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<GetI18nListResult>("get", "/api/i18n/list", {
|
||||||
|
params
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取翻译详情
|
||||||
|
*/
|
||||||
|
export const getI18nInfoAPI = (id: string) => {
|
||||||
|
return http.request<TranslationInfo>("get", `/api/i18n/info/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除翻译
|
||||||
|
* @param id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteI18nAPI = (id: string) => {
|
||||||
|
return http.request<null>("post", `/api/i18n/deleteI18n/${id}`);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 修改翻译
|
||||||
|
* @param data
|
||||||
|
* @param id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const putUpdateI18nAPI = (data: AddI18nParams, id: string) => {
|
||||||
|
return http.request<null>("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<GetI18nHandleListResult>(
|
||||||
|
"get",
|
||||||
|
`/api/i18n/infoList/${id}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取国际化数据
|
||||||
|
* @param locale 语言代码
|
||||||
|
* @returns 国际化数据
|
||||||
|
*/
|
||||||
|
export const getLocaleI18nAPI = (locale: string) => {
|
||||||
|
return http.request<Record<string, any>>("get", `/api/i18n/data/${locale}`);
|
||||||
|
};
|
||||||
154
src/api/login.ts
Normal file
@@ -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<LoginResult>("post", "/api/login", {
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 刷新token */
|
||||||
|
export const refreshTokenApi = (data: { refreshToken: string }) => {
|
||||||
|
return http.request<LoginResult>("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<CaptchaResponse>("get", "/api/captcha");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户动态路由
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getUserRoutesAPI = () => {
|
||||||
|
return http.request<any[]>("GET", "/api/getRoutes");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
*/
|
||||||
|
export const getUserInfoAPI = () => {
|
||||||
|
return http.request<UserInfo>("get", `/api/info`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
export const logoutAPI = () => {
|
||||||
|
return http.request<null>("post", `/api/logout`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**获取验证码参数 */
|
||||||
|
type GetCodeParams = {
|
||||||
|
/**用户账号 */
|
||||||
|
username: string;
|
||||||
|
/**验证码类型 */
|
||||||
|
title: string;
|
||||||
|
/**收件邮箱 */
|
||||||
|
mail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const postGetCodeAPI = (data: GetCodeParams) => {
|
||||||
|
return http.request<null>("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<null>("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<null>("post", `/api/resetPassword`, {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
82
src/api/system.ts
Normal file
@@ -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<QueryListResult<DepartmentInfo[]>>(
|
||||||
|
"get",
|
||||||
|
`/api/department/list`,
|
||||||
|
{
|
||||||
|
params: filterEmptyObject(params)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**更新部门数据 */
|
||||||
|
export const putUpdateDepartmentAPI = (
|
||||||
|
data: AddDepartmentParams,
|
||||||
|
id: string
|
||||||
|
) => {
|
||||||
|
return http.request<null>("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<null>("post", `/api/department/add`, {
|
||||||
|
data
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**删除部门及其附属部门 */
|
||||||
|
export const deleteDepartmentAPI = (id: string) => {
|
||||||
|
return http.request<null>("post", `/api/department/delete/${id}`);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { http } from "@/utils/http";
|
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;
|
* @param data
|
||||||
/**验证码ID */
|
* @returns
|
||||||
captchaId: string;
|
*/
|
||||||
|
export const putUpdateEmailAPI = (data: {
|
||||||
|
/**密码 */
|
||||||
|
password: string;
|
||||||
|
/**邮箱 */
|
||||||
|
email: string;
|
||||||
|
}) => {
|
||||||
|
return http.request<null>("put", `/api/user/updateEmail`, {
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
data
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 获取验证码 */
|
/**
|
||||||
export const GetCaptchaAPI = () => {
|
* 更新密码
|
||||||
return http.request<CaptchaResponse>("get", "/api/captcha");
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const putUpdatePasswordAPI = (data: {
|
||||||
|
/**旧密码 */
|
||||||
|
oldPassword: string;
|
||||||
|
/**新密码 */
|
||||||
|
newPassword: string;
|
||||||
|
}) => {
|
||||||
|
return http.request<null>("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<null>("put", `/api/user/updatePhone`, {
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
data
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserInfo = {
|
/**更新用户基础信息参数 */
|
||||||
permissions: string[];
|
type UpdateBaseUserInfoParams = {
|
||||||
roles: string[];
|
/**姓名 */
|
||||||
|
name: string;
|
||||||
|
/**性别 */
|
||||||
|
gender: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户基础信息
|
||||||
|
* @param data
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const putUpdateBaseUserInfoAPI = (data: UpdateBaseUserInfoParams) => {
|
||||||
|
return http.request<null>("PUT", "/api/user/updateBaseUserInfo", { data });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新头像
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param data 图片数据
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const postUploadAvatarAPI = (id: string, data: { file: Blob }) => {
|
||||||
|
return http.request<FileInfo>("post", `/api/user/avatar/${id}`, {
|
||||||
|
headers: {
|
||||||
|
"content-type": "multipart/form-data"
|
||||||
|
},
|
||||||
|
data
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
13
src/api/utils.ts
Normal file
@@ -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;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB |
BIN
src/assets/user.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
7
src/components/ReCropper/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import reCropper from "./src";
|
||||||
|
import { withInstall } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 图片裁剪组件 */
|
||||||
|
export const ReCropper = withInstall(reCropper);
|
||||||
|
|
||||||
|
export default ReCropper;
|
||||||
8
src/components/ReCropper/src/circled.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@import "cropperjs/dist/cropper.css";
|
||||||
|
|
||||||
|
.re-circled {
|
||||||
|
.cropper-view-box,
|
||||||
|
.cropper-face {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
457
src/components/ReCropper/src/index.tsx
Normal file
@@ -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<CSSProperties>, default: () => ({}) },
|
||||||
|
options: { type: Object as PropType<Options>, default: () => ({}) }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ReCropper",
|
||||||
|
props,
|
||||||
|
setup(props, { attrs, emit }) {
|
||||||
|
const tippyElRef = ref<ElRef<HTMLImageElement>>();
|
||||||
|
const imgElRef = ref<ElRef<HTMLImageElement>>();
|
||||||
|
const cropper = ref<Nullable<Cropper>>();
|
||||||
|
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<number>) {
|
||||||
|
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 () => (
|
||||||
|
<div class="flex flex-wrap w-[60px] justify-between">
|
||||||
|
<ElUpload
|
||||||
|
accept="image/*"
|
||||||
|
show-file-list={false}
|
||||||
|
before-upload={beforeUpload}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "上传",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ElUpload>
|
||||||
|
<DownloadIcon
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "下载",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => downloadByBase64(imgBase64.value, "cropping.png")}
|
||||||
|
/>
|
||||||
|
<ChangeIcon
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "圆形、矩形裁剪",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
inCircled.value = !inCircled.value;
|
||||||
|
realTimeCroppered();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Reload
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "重置",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("reset")}
|
||||||
|
/>
|
||||||
|
<ArrowUp
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "上移(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [0, -10]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowDown
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "下移(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [0, 10]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowLeft
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "左移(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [-10, 0]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowRight
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "右移(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("move", [10, 0]), "0:100"]}
|
||||||
|
/>
|
||||||
|
<ArrowH
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "水平翻转",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("scaleX", -1)}
|
||||||
|
/>
|
||||||
|
<ArrowV
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "垂直翻转",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("scaleY", -1)}
|
||||||
|
/>
|
||||||
|
<RotateLeft
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "逆时针旋转",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("rotate", -45)}
|
||||||
|
/>
|
||||||
|
<RotateRight
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "顺时针旋转",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
onClick={() => handCropper("rotate", 45)}
|
||||||
|
/>
|
||||||
|
<SearchPlus
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "放大(可长按)",
|
||||||
|
placement: "left-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("zoom", 0.1), "0:100"]}
|
||||||
|
/>
|
||||||
|
<SearchMinus
|
||||||
|
class={iconClass.value}
|
||||||
|
v-tippy={{
|
||||||
|
content: "缩小(可长按)",
|
||||||
|
placement: "right-start"
|
||||||
|
}}
|
||||||
|
v-longpress={[() => handCropper("zoom", -0.1), "0:100"]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 ? (
|
||||||
|
<div
|
||||||
|
ref="tippyElRef"
|
||||||
|
class={getClass}
|
||||||
|
style={getWrapperStyle}
|
||||||
|
onContextmenu={event => onContextmenu(event)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-show={isReady}
|
||||||
|
ref="imgElRef"
|
||||||
|
style={getImageStyle}
|
||||||
|
src={inSrc}
|
||||||
|
alt={alt}
|
||||||
|
crossorigin={crossorigin}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
});
|
||||||
1
src/components/ReCropper/src/svg/arrow-down.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0 0 48.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2"/></svg>
|
||||||
|
After Width: | Height: | Size: 346 B |
1
src/components/ReCropper/src/svg/arrow-h.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m296.992 216.992-272 272L3.008 512l21.984 23.008 272 272 46.016-46.016L126.016 544h772L680.992 760.992l46.016 46.016 272-272L1020.992 512l-21.984-23.008-272-272-46.048 46.048L898.016 480h-772l216.96-216.992z"/></svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
1
src/components/ReCropper/src/svg/arrow-left.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M872 474H286.9l350.2-304c5.6-4.9 2.2-14-5.2-14h-88.5c-3.9 0-7.6 1.4-10.5 3.9L155 487.8a31.96 31.96 0 0 0 0 48.3L535.1 866c1.5 1.3 3.3 2 5.2 2h91.5c7.4 0 10.8-9.2 5.2-14L286.9 550H872c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8"/></svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
1
src/components/ReCropper/src/svg/arrow-right.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M869 487.8 491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-.7 5.2-2L869 536.2a32.07 32.07 0 0 0 0-48.4"/></svg>
|
||||||
|
After Width: | Height: | Size: 350 B |
1
src/components/ReCropper/src/svg/arrow-up.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M868 545.5 536.1 163a31.96 31.96 0 0 0-48.3 0L156 545.5a7.97 7.97 0 0 0 6 13.2h81c4.6 0 9-2 12.1-5.5L474 300.9V864c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V300.9l218.9 252.3c3 3.5 7.4 5.5 12.1 5.5h81c6.8 0 10.5-8 6-13.2"/></svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
1
src/components/ReCropper/src/svg/arrow-v.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="m512 67.008-23.008 21.984-256 256 46.048 46.048L480 190.016v644L279.008 632.96l-46.048 46.08 256 256 23.008 21.984 23.008-21.984 256-256-46.016-46.016L544 834.016v-644l200.992 200.96 46.016-45.984-256-256z"/></svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
1
src/components/ReCropper/src/svg/change.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path d="M956.8 988.8H585.6c-16 0-25.6-9.6-25.6-28.8V576c0-16 9.6-28.8 25.6-28.8h371.2c16 0 25.6 9.6 25.6 28.8v384c0 16-9.6 28.8-25.6 28.8M608 937.6h326.4V598.4H608zm-121.6 44.8C262.4 982.4 144 848 144 595.2c0-19.2 9.6-28.8 25.6-28.8s25.6 12.8 25.6 28.8c0 220.8 96 326.4 288 326.4 16 0 25.6 12.8 25.6 28.8s-6.4 32-22.4 32"/><path d="M262.4 694.4c-6.4 0-9.6-3.2-16-6.4L160 601.6c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8-3.2 3.2-6.4 6.4-12.8 6.4"/><path d="M86.4 694.4c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0 9.6 9.6 9.6 22.4 0 28.8L99.2 688c-3.2 3.2-6.4 6.4-12.8 6.4m790.4-249.6c-16 0-28.8-12.8-28.8-32 0-224-99.2-336-300.8-336-16 0-28.8-12.8-28.8-32s9.6-32 28.8-32c233.6 0 355.2 137.6 355.2 396.8 0 22.4-9.6 35.2-25.6 35.2"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4l-86.4-86.4c-9.6-9.6-9.6-22.4 0-28.8s22.4-9.6 28.8 0l86.4 86.4c9.6 9.6 9.6 22.4 0 28.8 0 3.2-6.4 6.4-12.8 6.4"/><path d="M876.8 448c-6.4 0-9.6-3.2-16-6.4-9.6-9.6-9.6-22.4 0-28.8l86.4-86.4c9.6-9.6 22.4-9.6 28.8 0s9.6 22.4 0 28.8l-86.4 86.4c-3.2 3.2-6.4 6.4-12.8 6.4M288 524.8C156.8 524.8 48 416 48 278.4S156.8 35.2 288 35.2 528 144 528 281.6 419.2 524.8 288 524.8m-3.2-432c-99.2 0-179.2 83.2-179.2 185.6S185.6 464 284.8 464 464 380.8 464 278.4 384 92.8 284.8 92.8"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
1
src/components/ReCropper/src/svg/download.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>
|
||||||
|
After Width: | Height: | Size: 417 B |
31
src/components/ReCropper/src/svg/index.ts
Normal file
@@ -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
|
||||||
|
};
|
||||||
1
src/components/ReCropper/src/svg/reload.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 0 1 755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 0 0 3 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8m756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 0 1 512.1 856a342.24 342.24 0 0 1-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 0 0-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 0 0-8-8.2"/></svg>
|
||||||
|
After Width: | Height: | Size: 863 B |
1
src/components/ReCropper/src/svg/rotate-left.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M672 418H144c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H188V494h440z"/><path fill="currentColor" d="M819.3 328.5c-78.8-100.7-196-153.6-314.6-154.2l-.2-64c0-6.5-7.6-10.1-12.6-6.1l-128 101c-4 3.1-3.9 9.1 0 12.3L492 318.6c5.1 4 12.7.4 12.6-6.1v-63.9c12.9.1 25.9.9 38.8 2.5 42.1 5.2 82.1 18.2 119 38.7 38.1 21.2 71.2 49.7 98.4 84.3 27.1 34.7 46.7 73.7 58.1 115.8 11 40.7 14 82.7 8.9 124.8-.7 5.4-1.4 10.8-2.4 16.1h74.9c14.8-103.6-11.3-213-81-302.3"/></svg>
|
||||||
|
After Width: | Height: | Size: 630 B |
1
src/components/ReCropper/src/svg/rotate-right.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M480.5 251.2c13-1.6 25.9-2.4 38.8-2.5v63.9c0 6.5 7.5 10.1 12.6 6.1L660 217.6c4-3.2 4-9.2 0-12.3l-128-101c-5.1-4-12.6-.4-12.6 6.1l-.2 64c-118.6.5-235.8 53.4-314.6 154.2-69.6 89.2-95.7 198.6-81.1 302.4h74.9c-.9-5.3-1.7-10.7-2.4-16.1-5.1-42.1-2.1-84.1 8.9-124.8 11.4-42.2 31-81.1 58.1-115.8 27.2-34.7 60.3-63.2 98.4-84.3 37-20.6 76.9-33.6 119.1-38.8"/><path fill="currentColor" d="M880 418H352c-17.7 0-32 14.3-32 32v414c0 17.7 14.3 32 32 32h528c17.7 0 32-14.3 32-32V450c0-17.7-14.3-32-32-32m-44 402H396V494h440z"/></svg>
|
||||||
|
After Width: | Height: | Size: 633 B |
1
src/components/ReCropper/src/svg/search-minus.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>
|
||||||
|
After Width: | Height: | Size: 532 B |
1
src/components/ReCropper/src/svg/search-plus.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8m284 424L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11M696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430"/></svg>
|
||||||
|
After Width: | Height: | Size: 628 B |
1
src/components/ReCropper/src/svg/upload.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M400 317.7h73.9V656c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V317.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 163a8 8 0 0 0-12.6 0l-112 141.7c-4.1 5.3-.4 13 6.3 13M878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8"/></svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
7
src/components/ReCropperPreview/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import reCropperPreview from "./src/index.vue";
|
||||||
|
import { withInstall } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
/** 图片裁剪预览组件 */
|
||||||
|
export const ReCropperPreview = withInstall(reCropperPreview);
|
||||||
|
|
||||||
|
export default ReCropperPreview;
|
||||||
76
src/components/ReCropperPreview/src/index.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCropper from "@/components/ReCropper";
|
||||||
|
import { formatBytes } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "ReCropperPreview"
|
||||||
|
});
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
imgSrc: String
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["cropper"]);
|
||||||
|
|
||||||
|
const infos = ref();
|
||||||
|
const popoverRef = ref();
|
||||||
|
const refCropper = ref();
|
||||||
|
const showPopover = ref(false);
|
||||||
|
const cropperImg = ref<string>("");
|
||||||
|
|
||||||
|
function onCropper({ base64, blob, info }) {
|
||||||
|
infos.value = info;
|
||||||
|
cropperImg.value = base64;
|
||||||
|
emit("cropper", { base64, blob, info });
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePopover() {
|
||||||
|
popoverRef.value.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ hidePopover });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-loading="!showPopover" element-loading-background="transparent">
|
||||||
|
<el-popover
|
||||||
|
ref="popoverRef"
|
||||||
|
:visible="showPopover"
|
||||||
|
placement="right"
|
||||||
|
width="18vw"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div class="w-[18vw]">
|
||||||
|
<ReCropper
|
||||||
|
ref="refCropper"
|
||||||
|
:src="imgSrc"
|
||||||
|
circled
|
||||||
|
@cropper="onCropper"
|
||||||
|
@readied="showPopover = true"
|
||||||
|
/>
|
||||||
|
<p v-show="showPopover" class="mt-1 text-center">
|
||||||
|
温馨提示:右键上方裁剪区可开启功能菜单
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-wrap justify-center items-center text-center">
|
||||||
|
<el-image
|
||||||
|
v-if="cropperImg"
|
||||||
|
:src="cropperImg"
|
||||||
|
:preview-src-list="Array.of(cropperImg)"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<div v-if="infos" class="mt-1">
|
||||||
|
<p>
|
||||||
|
图像大小:{{ parseInt(infos.width) }} ×
|
||||||
|
{{ parseInt(infos.height) }}像素
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
文件大小:{{ formatBytes(infos.size) }}({{ infos.size }} 字节)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import perms from "./src/perms";
|
|
||||||
|
|
||||||
const Perms = perms;
|
|
||||||
|
|
||||||
export { Perms };
|
|
||||||
@@ -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) ? (
|
|
||||||
<Fragment>{slots.default?.()}</Fragment>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -2,5 +2,4 @@ export * from "./auth";
|
|||||||
export * from "./copy";
|
export * from "./copy";
|
||||||
export * from "./longpress";
|
export * from "./longpress";
|
||||||
export * from "./optimize";
|
export * from "./optimize";
|
||||||
export * from "./perms";
|
|
||||||
export * from "./ripple";
|
export * from "./ripple";
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { hasPerms } from "@/utils/auth";
|
|
||||||
import type { Directive, DirectiveBinding } from "vue";
|
|
||||||
|
|
||||||
export const perms: Directive = {
|
|
||||||
mounted(el: HTMLElement, binding: DirectiveBinding<string | Array<string>>) {
|
|
||||||
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']\""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -133,7 +133,7 @@ const transitionMain = defineComponent({
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<el-backtop
|
<el-backtop
|
||||||
:title="t('buttons.pureBackTop')"
|
:title="t('buttons:BackTop')"
|
||||||
target=".app-main .el-scrollbar__wrap"
|
target=".app-main .el-scrollbar__wrap"
|
||||||
>
|
>
|
||||||
<BackTopIcon />
|
<BackTopIcon />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { getConfig } from "@/config";
|
import { getConfig } from "@/config";
|
||||||
|
|
||||||
const TITLE = getConfig("Title");
|
const TITLE = getConfig("Title");
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const {
|
|||||||
userAvatar,
|
userAvatar,
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
toggleSideBar,
|
toggleSideBar,
|
||||||
|
toAccountSettings,
|
||||||
getDropdownItemStyle,
|
getDropdownItemStyle,
|
||||||
getDropdownItemClass
|
getDropdownItemClass
|
||||||
} = useNav();
|
} = useNav();
|
||||||
@@ -94,19 +95,26 @@ const { t, locale, translationCh, translationEn } = useTranslationLang();
|
|||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu class="logout">
|
<el-dropdown-menu class="logout">
|
||||||
|
<el-dropdown-item @click="toAccountSettings">
|
||||||
|
<IconifyIconOffline
|
||||||
|
:icon="AccountSettingsIcon"
|
||||||
|
style="margin: 5px"
|
||||||
|
/>
|
||||||
|
{{ t("buttons:AccountSettings") }}
|
||||||
|
</el-dropdown-item>
|
||||||
<el-dropdown-item @click="logout">
|
<el-dropdown-item @click="logout">
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
:icon="LogoutCircleRLine"
|
:icon="LogoutCircleRLine"
|
||||||
style="margin: 5px"
|
style="margin: 5px"
|
||||||
/>
|
/>
|
||||||
{{ t("buttons.pureLoginOut") }}
|
{{ t("buttons:LoginOut") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<span
|
<span
|
||||||
class="set-icon navbar-bg-hover"
|
class="set-icon navbar-bg-hover"
|
||||||
:title="t('buttons.pureOpenSystemSet')"
|
:title="t('buttons:OpenSystemSet')"
|
||||||
@click="onPanel"
|
@click="onPanel"
|
||||||
>
|
>
|
||||||
<IconifyIconOffline :icon="Setting" />
|
<IconifyIconOffline :icon="Setting" />
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ function hoverDescription(event, description) {
|
|||||||
max-width: 238px;
|
max-width: 238px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
.notice-container {
|
.notice-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ export interface TabItem {
|
|||||||
export const noticesData: TabItem[] = [
|
export const noticesData: TabItem[] = [
|
||||||
{
|
{
|
||||||
key: "1",
|
key: "1",
|
||||||
name: $t("status.pureNotify"),
|
name: $t("status:Notify"),
|
||||||
list: [],
|
list: [],
|
||||||
emptyText: $t("status.pureNoNotify")
|
emptyText: $t("status:NoNotify")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "2",
|
key: "2",
|
||||||
name: $t("status.pureMessage"),
|
name: $t("status:Message"),
|
||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile1.svg",
|
avatar: "https://xiaoxian521.github.io/hyperlink/svg/smile1.svg",
|
||||||
@@ -51,11 +51,11 @@ export const noticesData: TabItem[] = [
|
|||||||
type: "2"
|
type: "2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
emptyText: $t("status.pureNoMessage")
|
emptyText: $t("status:NoMessage")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "3",
|
key: "3",
|
||||||
name: $t("status.pureTodo"),
|
name: $t("status:Todo"),
|
||||||
list: [
|
list: [
|
||||||
{
|
{
|
||||||
avatar: "",
|
avatar: "",
|
||||||
@@ -94,6 +94,6 @@ export const noticesData: TabItem[] = [
|
|||||||
type: "3"
|
type: "3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
emptyText: $t("status.pureNoTodo")
|
emptyText: $t("status:NoTodo")
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const getLabel = computed(
|
|||||||
>
|
>
|
||||||
<el-empty
|
<el-empty
|
||||||
v-if="notices.length === 0"
|
v-if="notices.length === 0"
|
||||||
:description="t('status.pureNoMessage')"
|
:description="t('status:NoMessage')"
|
||||||
:image-size="60"
|
:image-size="60"
|
||||||
/>
|
/>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ onBeforeUnmount(() => {
|
|||||||
class="project-configuration border-b-[1px] border-solid border-[var(--pure-border-color)]"
|
class="project-configuration border-b-[1px] border-solid border-[var(--pure-border-color)]"
|
||||||
>
|
>
|
||||||
<h4 class="dark:text-white">
|
<h4 class="dark:text-white">
|
||||||
{{ t("panel.pureSystemSet") }}
|
{{ t("panel:SystemSet") }}
|
||||||
</h4>
|
</h4>
|
||||||
<span
|
<span
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: t('panel.pureCloseSystemSet'),
|
content: t('panel:CloseSystemSet'),
|
||||||
placement: 'bottom-start',
|
placement: 'bottom-start',
|
||||||
zIndex: 41000
|
zIndex: 41000
|
||||||
}"
|
}"
|
||||||
@@ -82,7 +82,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: t('panel.pureClearCacheAndToLogin'),
|
content: t('panel:ClearCacheAndToLogin'),
|
||||||
placement: 'left-start',
|
placement: 'left-start',
|
||||||
zIndex: 41000
|
zIndex: 41000
|
||||||
}"
|
}"
|
||||||
@@ -91,7 +91,7 @@ onBeforeUnmount(() => {
|
|||||||
bg
|
bg
|
||||||
@click="onReset"
|
@click="onReset"
|
||||||
>
|
>
|
||||||
{{ t("panel.pureClearCache") }}
|
{{ t("panel:ClearCache") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ const { device } = useNav();
|
|||||||
<div class="search-footer text-[#333] dark:text-white">
|
<div class="search-footer text-[#333] dark:text-white">
|
||||||
<span class="search-footer-item">
|
<span class="search-footer-item">
|
||||||
<EnterOutlined class="icon" />
|
<EnterOutlined class="icon" />
|
||||||
{{ t("buttons.pureConfirm") }}
|
{{ t("buttons:Confirm") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="search-footer-item">
|
<span class="search-footer-item">
|
||||||
<IconifyIconOffline :icon="ArrowUpLine" class="icon" />
|
<IconifyIconOffline :icon="ArrowUpLine" class="icon" />
|
||||||
<IconifyIconOffline :icon="ArrowDownLine" class="icon" />
|
<IconifyIconOffline :icon="ArrowDownLine" class="icon" />
|
||||||
{{ t("buttons.pureSwitch") }}
|
{{ t("buttons:Switch") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="search-footer-item">
|
<span class="search-footer-item">
|
||||||
<MdiKeyboardEsc class="icon" />
|
<MdiKeyboardEsc class="icon" />
|
||||||
{{ t("buttons.pureClose") }}
|
{{ t("buttons:Close") }}
|
||||||
</span>
|
</span>
|
||||||
<p v-if="device !== 'mobile' && total > 0" class="search-footer-total">
|
<p v-if="device !== 'mobile' && total > 0" class="search-footer-total">
|
||||||
{{ `${t("search.pureTotal")} ${total}` }}
|
{{ `${t("search:Total")} ${total}` }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ defineExpose({ handleScroll });
|
|||||||
<div ref="historyRef" class="history">
|
<div ref="historyRef" class="history">
|
||||||
<template v-if="historyList.length">
|
<template v-if="historyList.length">
|
||||||
<div :style="titleStyle">
|
<div :style="titleStyle">
|
||||||
{{ t("search.pureHistory") }}
|
{{ t("search:History") }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in historyList"
|
v-for="(item, index) in historyList"
|
||||||
@@ -165,7 +165,7 @@ defineExpose({ handleScroll });
|
|||||||
<template v-if="collectList.length">
|
<template v-if="collectList.length">
|
||||||
<div :style="titleStyle">
|
<div :style="titleStyle">
|
||||||
{{
|
{{
|
||||||
`${t("search.pureCollect")}${collectList.length > 1 ? t("search.pureDragSort") : ""}`
|
`${t("search:Collect")}${collectList.length > 1 ? t("search:DragSort") : ""}`
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="collect-container">
|
<div class="collect-container">
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useRouter } from "vue-router";
|
|||||||
import SearchResult from "./SearchResult.vue";
|
import SearchResult from "./SearchResult.vue";
|
||||||
import SearchFooter from "./SearchFooter.vue";
|
import SearchFooter from "./SearchFooter.vue";
|
||||||
import { useNav } from "@/layout/hooks/useNav";
|
import { useNav } from "@/layout/hooks/useNav";
|
||||||
import { transformI18n } from "@/plugins/i18n";
|
|
||||||
import SearchHistory from "./SearchHistory.vue";
|
import SearchHistory from "./SearchHistory.vue";
|
||||||
|
import { transformI18n, $t } from "@/plugins/i18n";
|
||||||
import type { optionsItem, dragItem } from "../types";
|
import type { optionsItem, dragItem } from "../types";
|
||||||
import { ref, computed, shallowRef, watch } from "vue";
|
import { ref, computed, shallowRef, watch } from "vue";
|
||||||
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
|
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
|
||||||
@@ -293,7 +293,7 @@ onKeyStroke("ArrowDown", handleDown);
|
|||||||
v-model="keyword"
|
v-model="keyword"
|
||||||
size="large"
|
size="large"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('search.purePlaceholder')"
|
:placeholder="t('search:Placeholder')"
|
||||||
@input="handleSearch"
|
@input="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -305,7 +305,7 @@ onKeyStroke("ArrowDown", handleDown);
|
|||||||
</el-input>
|
</el-input>
|
||||||
<div class="search-content">
|
<div class="search-content">
|
||||||
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
|
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
|
||||||
<el-empty v-if="showEmpty" :description="t('search.pureEmpty')" />
|
<el-empty v-if="showEmpty" :description="t('search:Empty')" />
|
||||||
<SearchHistory
|
<SearchHistory
|
||||||
v-if="showSearchHistory"
|
v-if="showSearchHistory"
|
||||||
ref="historyRef"
|
ref="historyRef"
|
||||||
|
|||||||
@@ -147,13 +147,13 @@ function setFalse(Doms): any {
|
|||||||
const stretchTypeOptions = computed<Array<OptionsType>>(() => {
|
const stretchTypeOptions = computed<Array<OptionsType>>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: t("panel.pureStretchFixed"),
|
label: t("panel:StretchFixed"),
|
||||||
tip: t("panel.pureStretchFixedTip"),
|
tip: t("panel:StretchFixedTip"),
|
||||||
value: "fixed"
|
value: "fixed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("panel.pureStretchCustom"),
|
label: t("panel:StretchCustom"),
|
||||||
tip: t("panel.pureStretchCustomTip"),
|
tip: t("panel:StretchCustomTip"),
|
||||||
value: "custom"
|
value: "custom"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -195,24 +195,24 @@ const pClass = computed(() => {
|
|||||||
const themeOptions = computed<Array<OptionsType>>(() => {
|
const themeOptions = computed<Array<OptionsType>>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: t("panel.pureOverallStyleLight"),
|
label: t("panel:OverallStyleLight"),
|
||||||
icon: DayIcon,
|
icon: DayIcon,
|
||||||
theme: "light",
|
theme: "light",
|
||||||
tip: t("panel.pureOverallStyleLightTip"),
|
tip: t("panel:OverallStyleLightTip"),
|
||||||
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
|
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("panel.pureOverallStyleDark"),
|
label: t("panel:OverallStyleDark"),
|
||||||
icon: DarkIcon,
|
icon: DarkIcon,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
tip: t("panel.pureOverallStyleDarkTip"),
|
tip: t("panel:OverallStyleDarkTip"),
|
||||||
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
|
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("panel.pureOverallStyleSystem"),
|
label: t("panel:OverallStyleSystem"),
|
||||||
icon: SystemIcon,
|
icon: SystemIcon,
|
||||||
theme: "system",
|
theme: "system",
|
||||||
tip: t("panel.pureOverallStyleSystemTip"),
|
tip: t("panel:OverallStyleSystemTip"),
|
||||||
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
|
iconAttrs: { fill: isDark.value ? "#fff" : "#000" }
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -221,18 +221,18 @@ const themeOptions = computed<Array<OptionsType>>(() => {
|
|||||||
const markOptions = computed<Array<OptionsType>>(() => {
|
const markOptions = computed<Array<OptionsType>>(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: t("panel.pureTagsStyleSmart"),
|
label: t("panel:TagsStyleSmart"),
|
||||||
tip: t("panel.pureTagsStyleSmartTip"),
|
tip: t("panel:TagsStyleSmartTip"),
|
||||||
value: "smart"
|
value: "smart"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("panel.pureTagsStyleCard"),
|
label: t("panel:TagsStyleCard"),
|
||||||
tip: t("panel.pureTagsStyleCardTip"),
|
tip: t("panel:TagsStyleCardTip"),
|
||||||
value: "card"
|
value: "card"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("panel.pureTagsStyleChrome"),
|
label: t("panel:TagsStyleChrome"),
|
||||||
tip: t("panel.pureTagsStyleChromeTip"),
|
tip: t("panel:TagsStyleChromeTip"),
|
||||||
value: "chrome"
|
value: "chrome"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -317,7 +317,7 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
<template>
|
<template>
|
||||||
<LayPanel>
|
<LayPanel>
|
||||||
<div class="p-5">
|
<div class="p-5">
|
||||||
<p :class="pClass">{{ t("panel.pureOverallStyle") }}</p>
|
<p :class="pClass">{{ t("panel:OverallStyle") }}</p>
|
||||||
<Segmented
|
<Segmented
|
||||||
resize
|
resize
|
||||||
class="select-none"
|
class="select-none"
|
||||||
@@ -335,7 +335,7 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p :class="['mt-5', pClass]">{{ t("panel.pureThemeColor") }}</p>
|
<p :class="['mt-5', pClass]">{{ t("panel:ThemeColor") }}</p>
|
||||||
<ul class="theme-color">
|
<ul class="theme-color">
|
||||||
<li
|
<li
|
||||||
v-for="(item, index) in themeColors"
|
v-for="(item, index) in themeColors"
|
||||||
@@ -354,12 +354,12 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p :class="['mt-5', pClass]">{{ t("panel.pureLayoutModel") }}</p>
|
<p :class="['mt-5', pClass]">{{ t("panel:LayoutModel") }}</p>
|
||||||
<ul class="pure-theme">
|
<ul class="pure-theme">
|
||||||
<li
|
<li
|
||||||
ref="verticalRef"
|
ref="verticalRef"
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: t('panel.pureVerticalTip'),
|
content: t('panel:VerticalTip'),
|
||||||
zIndex: 41000
|
zIndex: 41000
|
||||||
}"
|
}"
|
||||||
:class="layoutTheme.layout === 'vertical' ? 'is-select' : ''"
|
:class="layoutTheme.layout === 'vertical' ? 'is-select' : ''"
|
||||||
@@ -372,7 +372,7 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
v-if="device !== 'mobile'"
|
v-if="device !== 'mobile'"
|
||||||
ref="horizontalRef"
|
ref="horizontalRef"
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: t('panel.pureHorizontalTip'),
|
content: t('panel:HorizontalTip'),
|
||||||
zIndex: 41000
|
zIndex: 41000
|
||||||
}"
|
}"
|
||||||
:class="layoutTheme.layout === 'horizontal' ? 'is-select' : ''"
|
:class="layoutTheme.layout === 'horizontal' ? 'is-select' : ''"
|
||||||
@@ -385,7 +385,7 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
v-if="device !== 'mobile'"
|
v-if="device !== 'mobile'"
|
||||||
ref="mixRef"
|
ref="mixRef"
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: t('panel.pureMixTip'),
|
content: t('panel:MixTip'),
|
||||||
zIndex: 41000
|
zIndex: 41000
|
||||||
}"
|
}"
|
||||||
:class="layoutTheme.layout === 'mix' ? 'is-select' : ''"
|
:class="layoutTheme.layout === 'mix' ? 'is-select' : ''"
|
||||||
@@ -397,7 +397,7 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<span v-if="useAppStoreHook().getViewportWidth > 1280">
|
<span v-if="useAppStoreHook().getViewportWidth > 1280">
|
||||||
<p :class="['mt-5', pClass]">{{ t("panel.pureStretch") }}</p>
|
<p :class="['mt-5', pClass]">{{ t("panel:Stretch") }}</p>
|
||||||
<Segmented
|
<Segmented
|
||||||
resize
|
resize
|
||||||
class="mb-2 select-none"
|
class="mb-2 select-none"
|
||||||
@@ -440,7 +440,7 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<p :class="['mt-4', pClass]">{{ t("panel.pureTagsStyle") }}</p>
|
<p :class="['mt-4', pClass]">{{ t("panel:TagsStyle") }}</p>
|
||||||
<Segmented
|
<Segmented
|
||||||
resize
|
resize
|
||||||
class="select-none"
|
class="select-none"
|
||||||
@@ -450,46 +450,46 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<p class="mt-5 font-medium text-sm dark:text-white">
|
<p class="mt-5 font-medium text-sm dark:text-white">
|
||||||
{{ t("panel.pureInterfaceDisplay") }}
|
{{ t("panel:InterfaceDisplay") }}
|
||||||
</p>
|
</p>
|
||||||
<ul class="setting">
|
<ul class="setting">
|
||||||
<li>
|
<li>
|
||||||
<span class="dark:text-white">{{ t("panel.pureGreyModel") }}</span>
|
<span class="dark:text-white">{{ t("panel:GreyModel") }}</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="settings.greyVal"
|
v-model="settings.greyVal"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
:active-text="t('buttons.pureOpenText')"
|
:active-text="t('buttons:OpenText')"
|
||||||
:inactive-text="t('buttons.pureCloseText')"
|
:inactive-text="t('buttons:CloseText')"
|
||||||
@change="greyChange"
|
@change="greyChange"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="dark:text-white">{{ t("panel.pureWeakModel") }}</span>
|
<span class="dark:text-white">{{ t("panel:WeakModel") }}</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="settings.weakVal"
|
v-model="settings.weakVal"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
:active-text="t('buttons.pureOpenText')"
|
:active-text="t('buttons:OpenText')"
|
||||||
:inactive-text="t('buttons.pureCloseText')"
|
:inactive-text="t('buttons:CloseText')"
|
||||||
@change="weekChange"
|
@change="weekChange"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="dark:text-white">{{ t("panel.pureHiddenTags") }}</span>
|
<span class="dark:text-white">{{ t("panel:HiddenTags") }}</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="settings.tabsVal"
|
v-model="settings.tabsVal"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
:active-text="t('buttons.pureOpenText')"
|
:active-text="t('buttons:OpenText')"
|
||||||
:inactive-text="t('buttons.pureCloseText')"
|
:inactive-text="t('buttons:CloseText')"
|
||||||
@change="tagsChange"
|
@change="tagsChange"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="dark:text-white">{{ t("panel.pureHiddenFooter") }}</span>
|
<span class="dark:text-white">{{ t("panel:HiddenFooter") }}</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="settings.hideFooter"
|
v-model="settings.hideFooter"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
:active-text="t('buttons.pureOpenText')"
|
:active-text="t('buttons:OpenText')"
|
||||||
:inactive-text="t('buttons.pureCloseText')"
|
:inactive-text="t('buttons:CloseText')"
|
||||||
@change="hideFooterChange"
|
@change="hideFooterChange"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
@@ -500,20 +500,20 @@ onUnmounted(() => removeMatchMedia);
|
|||||||
inline-prompt
|
inline-prompt
|
||||||
:active-value="true"
|
:active-value="true"
|
||||||
:inactive-value="false"
|
:inactive-value="false"
|
||||||
:active-text="t('buttons.pureOpenText')"
|
:active-text="t('buttons:OpenText')"
|
||||||
:inactive-text="t('buttons.pureCloseText')"
|
:inactive-text="t('buttons:CloseText')"
|
||||||
@change="logoChange"
|
@change="logoChange"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="dark:text-white">
|
<span class="dark:text-white">
|
||||||
{{ t("panel.pureMultiTagsCache") }}
|
{{ t("panel:MultiTagsCache") }}
|
||||||
</span>
|
</span>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="settings.multiTagsCache"
|
v-model="settings.multiTagsCache"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
:active-text="t('buttons.pureOpenText')"
|
:active-text="t('buttons:OpenText')"
|
||||||
:inactive-text="t('buttons.pureCloseText')"
|
:inactive-text="t('buttons:CloseText')"
|
||||||
@change="multiTagsCacheChange"
|
@change="multiTagsCacheChange"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const {
|
|||||||
userAvatar,
|
userAvatar,
|
||||||
backTopMenu,
|
backTopMenu,
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
|
toAccountSettings,
|
||||||
getDropdownItemStyle,
|
getDropdownItemStyle,
|
||||||
getDropdownItemClass
|
getDropdownItemClass
|
||||||
} = useNav();
|
} = useNav();
|
||||||
@@ -121,20 +122,27 @@ onMounted(() => {
|
|||||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
|
<el-dropdown-item @click="toAccountSettings">
|
||||||
|
<IconifyIconOffline
|
||||||
|
:icon="AccountSettingsIcon"
|
||||||
|
style="margin: 5px"
|
||||||
|
/>
|
||||||
|
{{ t("buttons:AccountSettings") }}
|
||||||
|
</el-dropdown-item>
|
||||||
<el-dropdown-menu class="logout">
|
<el-dropdown-menu class="logout">
|
||||||
<el-dropdown-item @click="logout">
|
<el-dropdown-item @click="logout">
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
:icon="LogoutCircleRLine"
|
:icon="LogoutCircleRLine"
|
||||||
style="margin: 5px"
|
style="margin: 5px"
|
||||||
/>
|
/>
|
||||||
{{ t("buttons.pureLoginOut") }}
|
{{ t("buttons:LoginOut") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<span
|
<span
|
||||||
class="set-icon navbar-bg-hover"
|
class="set-icon navbar-bg-hover"
|
||||||
:title="t('buttons.pureOpenSystemSet')"
|
:title="t('buttons:OpenSystemSet')"
|
||||||
@click="onPanel"
|
@click="onPanel"
|
||||||
>
|
>
|
||||||
<IconifyIconOffline :icon="Setting" />
|
<IconifyIconOffline :icon="Setting" />
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const {
|
|||||||
userAvatar,
|
userAvatar,
|
||||||
getDivStyle,
|
getDivStyle,
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
|
toAccountSettings,
|
||||||
getDropdownItemStyle,
|
getDropdownItemStyle,
|
||||||
getDropdownItemClass
|
getDropdownItemClass
|
||||||
} = useNav();
|
} = useNav();
|
||||||
@@ -142,20 +143,27 @@ watch(
|
|||||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
|
<el-dropdown-item @click="toAccountSettings">
|
||||||
|
<IconifyIconOffline
|
||||||
|
:icon="AccountSettingsIcon"
|
||||||
|
style="margin: 5px"
|
||||||
|
/>
|
||||||
|
{{ t("buttons:AccountSettings") }}
|
||||||
|
</el-dropdown-item>
|
||||||
<el-dropdown-menu class="logout">
|
<el-dropdown-menu class="logout">
|
||||||
<el-dropdown-item @click="logout">
|
<el-dropdown-item @click="logout">
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
:icon="LogoutCircleRLine"
|
:icon="LogoutCircleRLine"
|
||||||
style="margin: 5px"
|
style="margin: 5px"
|
||||||
/>
|
/>
|
||||||
{{ t("buttons.pureLoginOut") }}
|
{{ t("buttons:LoginOut") }}
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<span
|
<span
|
||||||
class="set-icon navbar-bg-hover"
|
class="set-icon navbar-bg-hover"
|
||||||
:title="t('buttons.pureOpenSystemSet')"
|
:title="t('buttons:OpenSystemSet')"
|
||||||
@click="onPanel"
|
@click="onPanel"
|
||||||
>
|
>
|
||||||
<IconifyIconOffline :icon="Setting" />
|
<IconifyIconOffline :icon="Setting" />
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ const toggleClick = () => {
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: isActive
|
content: isActive ? t('buttons:ClickCollapse') : t('buttons:ClickExpand'),
|
||||||
? t('buttons.pureClickCollapse')
|
|
||||||
: t('buttons.pureClickExpand'),
|
|
||||||
theme: tooltipEffect,
|
theme: tooltipEffect,
|
||||||
hideOnClick: 'toggle',
|
hideOnClick: 'toggle',
|
||||||
placement: 'right'
|
placement: 'right'
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ const toggleClick = () => {
|
|||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: isActive
|
content: isActive
|
||||||
? t('buttons.pureClickCollapse')
|
? t('buttons:ClickCollapse')
|
||||||
: t('buttons.pureClickExpand'),
|
: t('buttons:ClickExpand'),
|
||||||
theme: tooltipEffect,
|
theme: tooltipEffect,
|
||||||
hideOnClick: 'toggle',
|
hideOnClick: 'toggle',
|
||||||
placement: 'right'
|
placement: 'right'
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ const toggleClick = () => {
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="px-3 mr-1 navbar-bg-hover"
|
class="px-3 mr-1 navbar-bg-hover"
|
||||||
:title="
|
:title="isActive ? t('buttons:ClickCollapse') : t('buttons:ClickExpand')"
|
||||||
isActive ? t('buttons.pureClickCollapse') : t('buttons.pureClickExpand')
|
|
||||||
"
|
|
||||||
@click="toggleClick"
|
@click="toggleClick"
|
||||||
>
|
>
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
|
|||||||
@@ -344,10 +344,10 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (pureSetting.hiddenSideBar) {
|
if (pureSetting.hiddenSideBar) {
|
||||||
tagsViews[6].icon = ExitFullscreen;
|
tagsViews[6].icon = ExitFullscreen;
|
||||||
tagsViews[6].text = $t("buttons.pureContentExitFullScreen");
|
tagsViews[6].text = $t("buttons:ContentExitFullScreen");
|
||||||
} else {
|
} else {
|
||||||
tagsViews[6].icon = Fullscreen;
|
tagsViews[6].icon = Fullscreen;
|
||||||
tagsViews[6].text = $t("buttons.pureContentFullScreen");
|
tagsViews[6].text = $t("buttons:ContentFullScreen");
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
break;
|
break;
|
||||||
@@ -511,6 +511,7 @@ function tagOnClick(item) {
|
|||||||
} else {
|
} else {
|
||||||
router.push({ path });
|
router.push({ path });
|
||||||
}
|
}
|
||||||
|
emitter.emit("tagOnClick", item);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickOutside(contextmenuRef, closeMenu, {
|
onClickOutside(contextmenuRef, closeMenu, {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ onMounted(() => {
|
|||||||
<div
|
<div
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
class="frame"
|
class="frame"
|
||||||
:element-loading-text="t('status.pureLoad')"
|
:element-loading-text="t('status:Load')"
|
||||||
>
|
>
|
||||||
<iframe ref="frameRef" :src="frameSrc" class="frame-iframe" />
|
<iframe ref="frameRef" :src="frameSrc" class="frame-iframe" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { useAppStoreHook } from "@/store/modules/app";
|
|||||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||||
import { darken, lighten, useGlobal, storageLocal } from "@pureadmin/utils";
|
import { darken, lighten, useGlobal, storageLocal } from "@pureadmin/utils";
|
||||||
|
import { ElMessageBox } from "element-plus";
|
||||||
|
import { transformI18n } from "@/plugins/i18n";
|
||||||
|
import { logoutAPI } from "@/api/login";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
|
||||||
export function useDataThemeChange() {
|
export function useDataThemeChange() {
|
||||||
const { layoutTheme, layout } = useLayout();
|
const { layoutTheme, layout } = useLayout();
|
||||||
@@ -110,9 +114,23 @@ export function useDataThemeChange() {
|
|||||||
|
|
||||||
/** 清空缓存并返回登录页 */
|
/** 清空缓存并返回登录页 */
|
||||||
function onReset() {
|
function onReset() {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
transformI18n("logout:message"),
|
||||||
|
transformI18n("buttons:LoginOut"),
|
||||||
|
{
|
||||||
|
confirmButtonText: transformI18n("buttons:Confirm"),
|
||||||
|
cancelButtonText: transformI18n("buttons:Cancel"),
|
||||||
|
type: "warning",
|
||||||
|
center: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
const res = await logoutAPI();
|
||||||
|
if (!res.success) {
|
||||||
removeToken();
|
removeToken();
|
||||||
storageLocal().clear();
|
storageLocal().clear();
|
||||||
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
|
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } =
|
||||||
|
getConfig();
|
||||||
useAppStoreHook().setLayout(Layout);
|
useAppStoreHook().setLayout(Layout);
|
||||||
setEpThemeColor(EpThemeColor);
|
setEpThemeColor(EpThemeColor);
|
||||||
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
|
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
|
||||||
@@ -121,6 +139,23 @@ export function useDataThemeChange() {
|
|||||||
router.push("/login");
|
router.push("/login");
|
||||||
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
|
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
|
||||||
resetRouter();
|
resetRouter();
|
||||||
|
message(transformI18n("logout:success"), {
|
||||||
|
type: "success",
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message(transformI18n("logout:fail"), {
|
||||||
|
type: "error",
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message(transformI18n("logout:cancel"), {
|
||||||
|
type: "info",
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { storeToRefs } from "pinia";
|
|||||||
import { getConfig } from "@/config";
|
import { getConfig } from "@/config";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { emitter } from "@/utils/mitt";
|
import { emitter } from "@/utils/mitt";
|
||||||
import Avatar from "@/assets/user.jpg";
|
import Avatar from "@/assets/user.png";
|
||||||
import { getTopMenu } from "@/router/utils";
|
import { getTopMenu } from "@/router/utils";
|
||||||
import { useFullscreen } from "@vueuse/core";
|
import { useFullscreen } from "@vueuse/core";
|
||||||
import type { routeMetaType } from "../types";
|
import type { routeMetaType } from "../types";
|
||||||
@@ -14,9 +14,11 @@ import { useUserStoreHook } from "@/store/modules/user";
|
|||||||
import { useGlobal, isAllEmpty } from "@pureadmin/utils";
|
import { useGlobal, isAllEmpty } from "@pureadmin/utils";
|
||||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { logoutAPI } from "@/api/login";
|
||||||
|
import { ElMessageBox } from "element-plus";
|
||||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
||||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
||||||
|
|
||||||
const errorInfo =
|
const errorInfo =
|
||||||
"The current routing configuration is incorrect, please check the configuration";
|
"The current routing configuration is incorrect, please check the configuration";
|
||||||
|
|
||||||
@@ -98,7 +100,37 @@ export function useNav() {
|
|||||||
|
|
||||||
/** 退出登录 */
|
/** 退出登录 */
|
||||||
function logout() {
|
function logout() {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
transformI18n("logout:message"),
|
||||||
|
transformI18n("buttons:LoginOut"),
|
||||||
|
{
|
||||||
|
confirmButtonText: transformI18n("buttons:Confirm"),
|
||||||
|
cancelButtonText: transformI18n("buttons:Cancel"),
|
||||||
|
type: "warning",
|
||||||
|
center: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
const res = await logoutAPI();
|
||||||
|
if (!res.success) {
|
||||||
useUserStoreHook().logOut();
|
useUserStoreHook().logOut();
|
||||||
|
message(transformI18n("logout:success"), {
|
||||||
|
type: "success",
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message(transformI18n("logout:fail"), {
|
||||||
|
type: "error",
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
message(transformI18n("logout:cancel"), {
|
||||||
|
type: "info",
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function backTopMenu() {
|
function backTopMenu() {
|
||||||
@@ -109,6 +141,10 @@ export function useNav() {
|
|||||||
emitter.emit("openPanel");
|
emitter.emit("openPanel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toAccountSettings() {
|
||||||
|
router.push({ name: "AccountSettings" });
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSideBar() {
|
function toggleSideBar() {
|
||||||
pureApp.toggleSideBar();
|
pureApp.toggleSideBar();
|
||||||
}
|
}
|
||||||
@@ -169,6 +205,7 @@ export function useNav() {
|
|||||||
userAvatar,
|
userAvatar,
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
tooltipEffect,
|
tooltipEffect,
|
||||||
|
toAccountSettings,
|
||||||
getDropdownItemStyle,
|
getDropdownItemStyle,
|
||||||
getDropdownItemClass
|
getDropdownItemClass
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,49 +64,49 @@ export function useTags() {
|
|||||||
const tagsViews = reactive<Array<tagsViewsType>>([
|
const tagsViews = reactive<Array<tagsViewsType>>([
|
||||||
{
|
{
|
||||||
icon: RefreshRight,
|
icon: RefreshRight,
|
||||||
text: $t("buttons.pureReload"),
|
text: $t("buttons:Reload"),
|
||||||
divided: false,
|
divided: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Close,
|
icon: Close,
|
||||||
text: $t("buttons.pureCloseCurrentTab"),
|
text: $t("buttons:CloseCurrentTab"),
|
||||||
divided: false,
|
divided: false,
|
||||||
disabled: multiTags.value.length > 1 ? false : true,
|
disabled: multiTags.value.length > 1 ? false : true,
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: CloseLeftTags,
|
icon: CloseLeftTags,
|
||||||
text: $t("buttons.pureCloseLeftTabs"),
|
text: $t("buttons:CloseLeftTabs"),
|
||||||
divided: true,
|
divided: true,
|
||||||
disabled: multiTags.value.length > 1 ? false : true,
|
disabled: multiTags.value.length > 1 ? false : true,
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: CloseRightTags,
|
icon: CloseRightTags,
|
||||||
text: $t("buttons.pureCloseRightTabs"),
|
text: $t("buttons:CloseRightTabs"),
|
||||||
divided: false,
|
divided: false,
|
||||||
disabled: multiTags.value.length > 1 ? false : true,
|
disabled: multiTags.value.length > 1 ? false : true,
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: CloseOtherTags,
|
icon: CloseOtherTags,
|
||||||
text: $t("buttons.pureCloseOtherTabs"),
|
text: $t("buttons:CloseOtherTabs"),
|
||||||
divided: true,
|
divided: true,
|
||||||
disabled: multiTags.value.length > 2 ? false : true,
|
disabled: multiTags.value.length > 2 ? false : true,
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: CloseAllTags,
|
icon: CloseAllTags,
|
||||||
text: $t("buttons.pureCloseAllTabs"),
|
text: $t("buttons:CloseAllTabs"),
|
||||||
divided: false,
|
divided: false,
|
||||||
disabled: multiTags.value.length > 1 ? false : true,
|
disabled: multiTags.value.length > 1 ? false : true,
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Fullscreen,
|
icon: Fullscreen,
|
||||||
text: $t("buttons.pureContentFullScreen"),
|
text: $t("buttons:ContentFullScreen"),
|
||||||
divided: true,
|
divided: true,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
show: true
|
show: true
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const LayHeader = defineComponent({
|
|||||||
</div>
|
</div>
|
||||||
<el-scrollbar v-else>
|
<el-scrollbar v-else>
|
||||||
<el-backtop
|
<el-backtop
|
||||||
:title="t('buttons.pureBackTop')"
|
:title="t('buttons:BackTop')"
|
||||||
target=".main-container .el-scrollbar__wrap"
|
target=".main-container .el-scrollbar__wrap"
|
||||||
>
|
>
|
||||||
<BackTopIcon />
|
<BackTopIcon />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const routerArrays: Array<RouteConfigs> =
|
|||||||
{
|
{
|
||||||
path: "/welcome",
|
path: "/welcome",
|
||||||
meta: {
|
meta: {
|
||||||
title: "menus.pureHome",
|
title: "menus:Home",
|
||||||
icon: "ep:home-filled"
|
icon: "ep:home-filled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,7 @@ app.component("FontIcon", FontIcon);
|
|||||||
|
|
||||||
// 全局注册按钮级别权限组件
|
// 全局注册按钮级别权限组件
|
||||||
import { Auth } from "@/components/ReAuth";
|
import { Auth } from "@/components/ReAuth";
|
||||||
import { Perms } from "@/components/RePerms";
|
|
||||||
app.component("Auth", Auth);
|
app.component("Auth", Auth);
|
||||||
app.component("Perms", Perms);
|
|
||||||
|
|
||||||
// 全局注册vue-tippy
|
// 全局注册vue-tippy
|
||||||
import "tippy.js/dist/tippy.css";
|
import "tippy.js/dist/tippy.css";
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import { buildHierarchyTree } from "@/utils/tree";
|
|||||||
import remainingRouter from "./modules/remaining";
|
import remainingRouter from "./modules/remaining";
|
||||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||||
import { isUrl, openLink, storageLocal } from "@pureadmin/utils";
|
import { isAllEmpty, isUrl, openLink, storageLocal } from "@pureadmin/utils";
|
||||||
import {
|
import {
|
||||||
ascending,
|
ascending,
|
||||||
isOneOfArray,
|
isOneOfArray,
|
||||||
getHistoryMode,
|
getHistoryMode,
|
||||||
|
initRouter,
|
||||||
findRouteByPath,
|
findRouteByPath,
|
||||||
handleAliveRoute,
|
handleAliveRoute,
|
||||||
formatTwoStageRoutes,
|
formatTwoStageRoutes,
|
||||||
formatFlatteningRoutes,
|
formatFlatteningRoutes,
|
||||||
addPathMatch
|
getTopMenu
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import {
|
import {
|
||||||
type Router,
|
type Router,
|
||||||
@@ -24,8 +25,8 @@ import {
|
|||||||
type RouteComponent
|
type RouteComponent
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import {
|
import {
|
||||||
type DataInfo,
|
type UserInfo,
|
||||||
userKey,
|
userInfoKey,
|
||||||
removeToken,
|
removeToken,
|
||||||
getTokenInfo
|
getTokenInfo
|
||||||
} from "@/utils/auth";
|
} from "@/utils/auth";
|
||||||
@@ -112,16 +113,16 @@ router.beforeEach((to: ToRouteType, _from, next) => {
|
|||||||
handleAliveRoute(to);
|
handleAliveRoute(to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userInfo = storageLocal().getItem<DataInfo>(userKey);
|
const userInfo = storageLocal().getItem<UserInfo>(userInfoKey);
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
const externalLink = isUrl(to?.name as string);
|
const externalLink = isUrl(to?.name as string);
|
||||||
if (!externalLink) {
|
if (!externalLink) {
|
||||||
to.matched.some(item => {
|
to.matched.some(item => {
|
||||||
if (!item.meta.title) return "";
|
if (!item.meta.title) return "";
|
||||||
const Title = getConfig().Title;
|
const Title = getConfig().Title;
|
||||||
if (Title)
|
if (Title) {
|
||||||
document.title = `${transformI18n(item.meta.title)} | ${Title}`;
|
document.title = `${transformI18n(item.meta.title)} | ${Title}`;
|
||||||
else document.title = transformI18n(item.meta.title);
|
} else document.title = transformI18n(item.meta.title);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */
|
/** 如果已经登录并存在登录信息后不能跳转到路由白名单,而是继续保持在当前页面 */
|
||||||
@@ -129,11 +130,11 @@ router.beforeEach((to: ToRouteType, _from, next) => {
|
|||||||
whiteList.includes(to.fullPath) ? next(_from.fullPath) : next();
|
whiteList.includes(to.fullPath) ? next(_from.fullPath) : next();
|
||||||
}
|
}
|
||||||
const data = getTokenInfo();
|
const data = getTokenInfo();
|
||||||
if (!data.isExpire && userInfo) {
|
if (!data.isExpire) {
|
||||||
// 无权限跳转403页面
|
// 无权限跳转403页面
|
||||||
if (
|
if (
|
||||||
to.meta?.auths &&
|
to.meta?.permissions &&
|
||||||
!isOneOfArray(to.meta?.auths, userInfo?.permissions)
|
!isOneOfArray(to.meta?.permissions, userInfo?.permissions)
|
||||||
) {
|
) {
|
||||||
next({ path: "/403" });
|
next({ path: "/403" });
|
||||||
}
|
}
|
||||||
@@ -155,23 +156,38 @@ router.beforeEach((to: ToRouteType, _from, next) => {
|
|||||||
usePermissionStoreHook().wholeMenus.length === 0 &&
|
usePermissionStoreHook().wholeMenus.length === 0 &&
|
||||||
to.path !== "/login"
|
to.path !== "/login"
|
||||||
) {
|
) {
|
||||||
usePermissionStoreHook().handleWholeMenus([]);
|
initRouter().then((router: Router) => {
|
||||||
addPathMatch();
|
|
||||||
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||||
const { path } = to;
|
const { path } = to;
|
||||||
const route = findRouteByPath(
|
const route = findRouteByPath(
|
||||||
path,
|
path,
|
||||||
router.options.routes[0].children
|
router.options.routes[0].children
|
||||||
);
|
);
|
||||||
|
getTopMenu(true);
|
||||||
|
// query、params模式路由传参数的标签页不在此处处理
|
||||||
if (route && route.meta?.title) {
|
if (route && route.meta?.title) {
|
||||||
|
if (isAllEmpty(route.parentId) && route.meta?.backstage) {
|
||||||
|
// 此处为动态顶级路由(目录)
|
||||||
|
const { path, name, meta } = route.children[0];
|
||||||
useMultiTagsStoreHook().handleTags("push", {
|
useMultiTagsStoreHook().handleTags("push", {
|
||||||
path: route.path,
|
path,
|
||||||
name: route.name,
|
name,
|
||||||
meta: route.meta
|
meta
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { path, name, meta } = route;
|
||||||
|
useMultiTagsStoreHook().handleTags("push", {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
meta
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次)
|
||||||
|
if (isAllEmpty(to.name)) router.push(to.fullPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
toCorrectRoute();
|
toCorrectRoute();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default {
|
|||||||
redirect: "/welcome",
|
redirect: "/welcome",
|
||||||
meta: {
|
meta: {
|
||||||
icon: "ep:home-filled",
|
icon: "ep:home-filled",
|
||||||
title: $t("menus.pureHome"),
|
title: $t("menus:Home"),
|
||||||
rank: 0
|
rank: 0
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@@ -18,7 +18,7 @@ export default {
|
|||||||
name: "Welcome",
|
name: "Welcome",
|
||||||
component: () => import("@/views/welcome/index.vue"),
|
component: () => import("@/views/welcome/index.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: $t("menus.pureHome"),
|
title: $t("menus:Home"),
|
||||||
showLink: VITE_HIDE_HOME === "true" ? false : true
|
showLink: VITE_HIDE_HOME === "true" ? false : true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,40 +7,16 @@ export default [
|
|||||||
name: "Login",
|
name: "Login",
|
||||||
component: () => import("@/views/login/index.vue"),
|
component: () => import("@/views/login/index.vue"),
|
||||||
meta: {
|
meta: {
|
||||||
title: $t("menus.pureLogin"),
|
title: $t("menus:Login"),
|
||||||
showLink: false,
|
showLink: false,
|
||||||
rank: 101
|
rank: 101
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/403",
|
|
||||||
name: "403",
|
|
||||||
component: () => import("@/views/error/403.vue"),
|
|
||||||
meta: {
|
|
||||||
title: $t("menus.pureFourZeroOne")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/404",
|
|
||||||
name: "404",
|
|
||||||
component: () => import("@/views/error/404.vue"),
|
|
||||||
meta: {
|
|
||||||
title: $t("menus.pureFourZeroFour")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/500",
|
|
||||||
name: "500",
|
|
||||||
component: () => import("@/views/error/500.vue"),
|
|
||||||
meta: {
|
|
||||||
title: $t("menus.pureFive")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/redirect",
|
path: "/redirect",
|
||||||
component: Layout,
|
component: Layout,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t("status.pureLoad"),
|
title: $t("status:Load"),
|
||||||
showLink: false,
|
showLink: false,
|
||||||
rank: 102
|
rank: 102
|
||||||
},
|
},
|
||||||
@@ -51,5 +27,46 @@ export default [
|
|||||||
component: () => import("@/layout/redirect.vue")
|
component: () => import("@/layout/redirect.vue")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: "/403",
|
||||||
|
name: "403",
|
||||||
|
component: () => import("@/views/error/403.vue"),
|
||||||
|
meta: {
|
||||||
|
title: $t("menus:FourZeroOne"),
|
||||||
|
showLink: false,
|
||||||
|
rank: 104
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/404",
|
||||||
|
name: "404",
|
||||||
|
component: () => import("@/views/error/404.vue"),
|
||||||
|
meta: {
|
||||||
|
title: $t("menus:FourZeroFour"),
|
||||||
|
showLink: false,
|
||||||
|
rank: 105
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/500",
|
||||||
|
name: "500",
|
||||||
|
component: () => import("@/views/error/500.vue"),
|
||||||
|
meta: {
|
||||||
|
title: $t("menus:Five"),
|
||||||
|
showLink: false,
|
||||||
|
rank: 106
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/account-settings",
|
||||||
|
name: "AccountSettings",
|
||||||
|
component: () => import("@/views/account-settings/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: $t("buttons:AccountSettings"),
|
||||||
|
showLink: false,
|
||||||
|
rank: 107
|
||||||
|
}
|
||||||
}
|
}
|
||||||
] satisfies Array<RouteConfigsTable>;
|
] satisfies Array<RouteConfigsTable>;
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ import {
|
|||||||
isIncludeAllChildren
|
isIncludeAllChildren
|
||||||
} from "@pureadmin/utils";
|
} from "@pureadmin/utils";
|
||||||
import { getConfig } from "@/config";
|
import { getConfig } from "@/config";
|
||||||
import type { menuType } from "@/layout/types";
|
import { type menuType, routerArrays } from "@/layout/types";
|
||||||
import { buildHierarchyTree } from "@/utils/tree";
|
import { buildHierarchyTree } from "@/utils/tree";
|
||||||
import { userKey, type DataInfo } from "@/utils/auth";
|
import { userInfoKey, type UserInfo } from "@/utils/auth";
|
||||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
|
|
||||||
|
// 动态路由
|
||||||
|
import { getUserRoutesAPI } from "@/api/login";
|
||||||
const IFrame = () => import("@/layout/frame.vue");
|
const IFrame = () => import("@/layout/frame.vue");
|
||||||
// https://cn.vitejs.dev/guide/features.html#glob-import
|
// https://cn.vitejs.dev/guide/features.html#glob-import
|
||||||
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
|
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
|
||||||
@@ -82,7 +85,7 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
|
|||||||
/** 从localStorage里取出当前登陆用户的角色permissions,过滤无权限的菜单 */
|
/** 从localStorage里取出当前登陆用户的角色permissions,过滤无权限的菜单 */
|
||||||
function filterNoPermissionTree(data: RouteComponent[]) {
|
function filterNoPermissionTree(data: RouteComponent[]) {
|
||||||
const currentRoles =
|
const currentRoles =
|
||||||
storageLocal().getItem<DataInfo>(userKey)?.permissions ?? [];
|
storageLocal().getItem<UserInfo>(userInfoKey)?.permissions ?? [];
|
||||||
const newTree = cloneDeep(data).filter((v: any) =>
|
const newTree = cloneDeep(data).filter((v: any) =>
|
||||||
isOneOfArray(v.meta?.permissions, currentRoles)
|
isOneOfArray(v.meta?.permissions, currentRoles)
|
||||||
);
|
);
|
||||||
@@ -176,6 +179,14 @@ function handleAsyncRoutes(routeList) {
|
|||||||
);
|
);
|
||||||
usePermissionStoreHook().handleWholeMenus(routeList);
|
usePermissionStoreHook().handleWholeMenus(routeList);
|
||||||
}
|
}
|
||||||
|
if (!useMultiTagsStoreHook().getMultiTagsCache) {
|
||||||
|
useMultiTagsStoreHook().handleTags("equal", [
|
||||||
|
...routerArrays,
|
||||||
|
...usePermissionStoreHook().flatteningRoutes.filter(
|
||||||
|
v => v?.meta?.fixedTag
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
addPathMatch();
|
addPathMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,14 +203,19 @@ function initRouter() {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
handleAsyncRoutes(cloneDeep([]));
|
getUserRoutesAPI().then(({ data }) => {
|
||||||
|
handleAsyncRoutes(cloneDeep(data));
|
||||||
|
storageLocal().setItem(key, data);
|
||||||
resolve(router);
|
resolve(router);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
handleAsyncRoutes(cloneDeep([]));
|
getUserRoutesAPI().then(({ data }) => {
|
||||||
resolve([]);
|
handleAsyncRoutes(cloneDeep(data));
|
||||||
|
resolve(router);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,35 +299,83 @@ function handleAliveRoute({ name }: ToRouteType, mode?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 过滤后端传来的动态路由 重新生成规范路由 */
|
/**
|
||||||
|
* 过滤后端传来的动态路由,重新生成规范路由
|
||||||
|
*/
|
||||||
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
|
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
|
||||||
if (!arrRoutes || !arrRoutes.length) return;
|
if (!arrRoutes || !arrRoutes.length) return;
|
||||||
|
|
||||||
const modulesRoutesKeys = Object.keys(modulesRoutes);
|
const modulesRoutesKeys = Object.keys(modulesRoutes);
|
||||||
|
|
||||||
arrRoutes.forEach((v: RouteRecordRaw) => {
|
arrRoutes.forEach((v: RouteRecordRaw) => {
|
||||||
// 将backstage属性加入meta,标识此路由为后端返回路由
|
// 将 backstage 属性加入 meta,标识此路由为后端返回路由
|
||||||
v.meta.backstage = true;
|
v.meta.backstage = true;
|
||||||
// 父级的redirect属性取值:如果子级存在且父级的redirect属性不存在,默认取第一个子级的path;如果子级存在且父级的redirect属性存在,取存在的redirect属性,会覆盖默认值
|
// v.meta.title = transformI18n(v.meta.title);
|
||||||
if (v?.children && v.children.length && !v.redirect)
|
|
||||||
|
// 处理父级路由的 redirect 属性
|
||||||
|
if (v?.children && v.children.length && !v.redirect) {
|
||||||
v.redirect = v.children[0].path;
|
v.redirect = v.children[0].path;
|
||||||
// 父级的name属性取值:如果子级存在且父级的name属性不存在,默认取第一个子级的name;如果子级存在且父级的name属性存在,取存在的name属性,会覆盖默认值(注意:测试中发现父级的name不能和子级name重复,如果重复会造成重定向无效(跳转404),所以这里给父级的name起名的时候后面会自动加上`Parent`,避免重复)
|
}
|
||||||
if (v?.children && v.children.length && !v.name)
|
|
||||||
|
// 处理父级路由的 name 属性
|
||||||
|
if (v?.children && v.children.length && !v.name) {
|
||||||
v.name = (v.children[0].name as string) + "Parent";
|
v.name = (v.children[0].name as string) + "Parent";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 iframe 路由
|
||||||
if (v.meta?.frameSrc) {
|
if (v.meta?.frameSrc) {
|
||||||
v.component = IFrame;
|
v.component = IFrame;
|
||||||
} else {
|
} else if (v.component) {
|
||||||
// 对后端传component组件路径和不传做兼容(如果后端传component组件路径,那么path可以随便写,如果不传,component组件路径会跟path保持一致)
|
// 如果路由有 component 参数,直接加载对应的组件
|
||||||
const index = v?.component
|
const index = modulesRoutesKeys.findIndex(ev =>
|
||||||
? modulesRoutesKeys.findIndex(ev => ev.includes(v.component as any))
|
ev.includes(v.component as any)
|
||||||
: modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
|
);
|
||||||
|
if (index !== -1) {
|
||||||
v.component = modulesRoutes[modulesRoutesKeys[index]];
|
v.component = modulesRoutes[modulesRoutesKeys[index]];
|
||||||
|
} else {
|
||||||
|
console.warn(`未找到 ${v.component} 对应的组件文件`);
|
||||||
|
return; // 跳过无效路由
|
||||||
}
|
}
|
||||||
|
} else if (v?.children && v.children.length) {
|
||||||
|
// 如果是一级菜单(没有 component),跳过组件加载逻辑
|
||||||
|
console.log(`一级菜单 ${v.path} 不需要组件`);
|
||||||
|
} else {
|
||||||
|
// 如果路由没有 component 参数,尝试加载文件夹下的第一个 .vue 文件
|
||||||
|
const componentPath = findFirstVueFile(v.path);
|
||||||
|
if (componentPath) {
|
||||||
|
v.component = modulesRoutes[componentPath];
|
||||||
|
} else {
|
||||||
|
console.warn(`未找到 ${v.path} 对应的组件文件`);
|
||||||
|
return; // 跳过无效路由
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理子路由
|
||||||
if (v?.children && v.children.length) {
|
if (v?.children && v.children.length) {
|
||||||
addAsyncRoutes(v.children);
|
addAsyncRoutes(v.children);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return arrRoutes;
|
return arrRoutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件夹中查找第一个 .vue 文件
|
||||||
|
*/
|
||||||
|
function findFirstVueFile(folderPath: string): string | null {
|
||||||
|
const files = import.meta.glob("/src/views/**/*.vue"); // 匹配文件夹下的 .vue 文件
|
||||||
|
const filePaths = Object.keys(files);
|
||||||
|
|
||||||
|
// 查找与 folderPath 匹配的第一个 .vue 文件
|
||||||
|
for (const filePath of filePaths) {
|
||||||
|
if (filePath.includes(`/src/views/${folderPath}/`)) {
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // 未找到 .vue 文件
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
|
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
|
||||||
function getHistoryMode(routerHistory): RouterHistory {
|
function getHistoryMode(routerHistory): RouterHistory {
|
||||||
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
|
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
|
||||||
|
|||||||
@@ -1,41 +1,74 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
import { store } from "@/store";
|
||||||
|
import type { userType } from "../types";
|
||||||
|
import { routerArrays } from "@/layout/types";
|
||||||
|
import { router, resetRouter } from "@/router";
|
||||||
|
import { storageLocal } from "@pureadmin/utils";
|
||||||
|
import { getLogin, refreshTokenApi } from "@/api/login";
|
||||||
|
import type { LoginResult } from "@/api/login";
|
||||||
|
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||||
import {
|
import {
|
||||||
type userType,
|
type UserInfo,
|
||||||
store,
|
type UserToken,
|
||||||
router,
|
setToken,
|
||||||
resetRouter,
|
removeToken,
|
||||||
routerArrays,
|
userInfoKey,
|
||||||
storageLocal
|
userTokenKey
|
||||||
} from "../utils";
|
} from "@/utils/auth";
|
||||||
import { type UserResult, getLogin } from "@/api/user";
|
|
||||||
import { useMultiTagsStoreHook } from "./multiTags";
|
|
||||||
import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth";
|
|
||||||
|
|
||||||
export const useUserStore = defineStore({
|
export const useUserStore = defineStore({
|
||||||
id: "user",
|
id: "user",
|
||||||
state: (): userType => ({
|
state: (): userType => ({
|
||||||
// 用户名
|
// 用户名
|
||||||
username: storageLocal().getItem<DataInfo>(userKey)?.username ?? "",
|
username: storageLocal().getItem<UserInfo>(userInfoKey)?.username ?? "",
|
||||||
// 页面级别权限
|
// 页面级别权限
|
||||||
permissions: storageLocal().getItem<DataInfo>(userKey)?.permissions ?? [],
|
permissions:
|
||||||
|
storageLocal().getItem<UserInfo>(userInfoKey)?.permissions ?? [],
|
||||||
// 前端生成的验证码(按实际需求替换)
|
// 前端生成的验证码(按实际需求替换)
|
||||||
verifyCode: "",
|
verifyCode: "",
|
||||||
// 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码,1,2预留暂不开发)
|
// 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码,1,2预留暂不开发)
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
/**用户Id */
|
|
||||||
userId: storageLocal().getItem<DataInfo>(userKey)?.userId ?? "",
|
|
||||||
/**用户头像 */
|
/**用户头像 */
|
||||||
avatar: storageLocal().getItem<DataInfo>(userKey)?.avatar ?? "",
|
avatar: storageLocal().getItem<UserInfo>(userInfoKey)?.avatar ?? "",
|
||||||
/**用户数据库ID */
|
/**用户数据库ID */
|
||||||
roles: storageLocal().getItem<DataInfo>(userKey)?.roles ?? [],
|
id: storageLocal().getItem<UserInfo>(userInfoKey)?.id ?? "",
|
||||||
/**用户Token */
|
/**用户Token */
|
||||||
accessToken: storageLocal().getItem<DataInfo>(userKey)?.accessToken ?? "",
|
accessToken:
|
||||||
|
storageLocal().getItem<UserToken>(userTokenKey)?.accessToken ?? "",
|
||||||
|
/**用户刷新Token*/
|
||||||
|
refreshToken:
|
||||||
|
storageLocal().getItem<UserToken>(userTokenKey)?.refreshToken ?? "",
|
||||||
/**过期时间 */
|
/**过期时间 */
|
||||||
expires: storageLocal().getItem<DataInfo>(userKey)?.expires ?? 0,
|
expires: storageLocal().getItem<UserToken>(userTokenKey)?.expiresTime ?? 0,
|
||||||
// 是否勾选了登录页的免登录
|
// 是否勾选了登录页的免登录
|
||||||
isRemembered: false,
|
isRemembered: false,
|
||||||
// 登录页的免登录存储几天,默认2天
|
// 登录页的免登录存储几天,默认2天
|
||||||
loginDay: 2
|
loginDay: 2,
|
||||||
|
/***
|
||||||
|
* 用户手机号
|
||||||
|
*/
|
||||||
|
phone: storageLocal().getItem<UserInfo>(userInfoKey)?.phone ?? "",
|
||||||
|
/***
|
||||||
|
* 用户邮箱
|
||||||
|
*/
|
||||||
|
email: storageLocal().getItem<UserInfo>(userInfoKey)?.email ?? "",
|
||||||
|
/***
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
|
nickname: storageLocal().getItem<UserInfo>(userInfoKey)?.nickname ?? "",
|
||||||
|
/***
|
||||||
|
* 用户性别
|
||||||
|
*/
|
||||||
|
gender: storageLocal().getItem<UserInfo>(userInfoKey)?.gender ?? 0,
|
||||||
|
/***
|
||||||
|
* 用户角色
|
||||||
|
*/
|
||||||
|
roles: storageLocal().getItem<UserInfo>(userInfoKey)?.roles ?? [],
|
||||||
|
/***
|
||||||
|
* 部门ID
|
||||||
|
*/
|
||||||
|
department_id:
|
||||||
|
storageLocal().getItem<UserInfo>(userInfoKey)?.department_id ?? ""
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
/** 存储用户名 */
|
/** 存储用户名 */
|
||||||
@@ -54,22 +87,22 @@ export const useUserStore = defineStore({
|
|||||||
SET_CURRENTPAGE(value: number) {
|
SET_CURRENTPAGE(value: number) {
|
||||||
this.currentPage = value;
|
this.currentPage = value;
|
||||||
},
|
},
|
||||||
/**存储用户ID */
|
|
||||||
SET_USETID(value: string) {
|
|
||||||
this.userId = value;
|
|
||||||
},
|
|
||||||
/**存储用户头像 */
|
/**存储用户头像 */
|
||||||
SET_AVATAR(value: string) {
|
SET_AVATAR(value: string) {
|
||||||
this.avatar = value;
|
this.avatar = value;
|
||||||
},
|
},
|
||||||
/**存储用户数据库ID */
|
/**存储用户数据库ID */
|
||||||
SET_ROLES(value: string[]) {
|
SET_ID(value: string) {
|
||||||
this.roles = value;
|
this.id = value;
|
||||||
},
|
},
|
||||||
/**存储用户Token */
|
/**存储用户Token */
|
||||||
SET_ACCESSTOKEN(value: string) {
|
SET_ACCESSTOKEN(value: string) {
|
||||||
this.accessToken = value;
|
this.accessToken = value;
|
||||||
},
|
},
|
||||||
|
/**存储用户刷新Token */
|
||||||
|
SET_REFRESHTOKEN(value: string) {
|
||||||
|
this.refreshToken = value;
|
||||||
|
},
|
||||||
/**存储用户Token过期时间戳 */
|
/**存储用户Token过期时间戳 */
|
||||||
SET_EXPIRES(value: number) {
|
SET_EXPIRES(value: number) {
|
||||||
this.expires = value;
|
this.expires = value;
|
||||||
@@ -82,15 +115,37 @@ export const useUserStore = defineStore({
|
|||||||
SET_LOGINDAY(value: number) {
|
SET_LOGINDAY(value: number) {
|
||||||
this.loginDay = Number(value);
|
this.loginDay = Number(value);
|
||||||
},
|
},
|
||||||
|
/** 存储用户手机号 */
|
||||||
|
SET_PHONE(value: string) {
|
||||||
|
this.phone = value;
|
||||||
|
},
|
||||||
|
/** 存储用户邮箱 */
|
||||||
|
SET_EMAIL(value: string) {
|
||||||
|
this.email = value;
|
||||||
|
},
|
||||||
|
/** 存储用户昵称 */
|
||||||
|
SET_NICKNAME(value: string) {
|
||||||
|
this.nickname = value;
|
||||||
|
},
|
||||||
|
/** 存储用户性别 */
|
||||||
|
SET_GENDER(value: number) {
|
||||||
|
this.gender = value;
|
||||||
|
},
|
||||||
|
/** 存储用户角色 */
|
||||||
|
SET_ROLES(value: Array<string>) {
|
||||||
|
this.roles = value;
|
||||||
|
},
|
||||||
|
/** 存储用户部门ID */
|
||||||
|
SET_DEPARTMENT_ID(value: string) {
|
||||||
|
this.department_id = value;
|
||||||
|
},
|
||||||
/** 登入 */
|
/** 登入 */
|
||||||
async loginByUsername(data: {
|
async loginByUsername(data: {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
loginDay?: number;
|
loginDay?: number;
|
||||||
uuid: string;
|
|
||||||
code: string;
|
|
||||||
}) {
|
}) {
|
||||||
return new Promise<ResponseResult<UserResult>>((resolve, reject) => {
|
return new Promise<ResponseResult<LoginResult>>((resolve, reject) => {
|
||||||
getLogin(data)
|
getLogin(data)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
@@ -107,10 +162,40 @@ export const useUserStore = defineStore({
|
|||||||
logOut() {
|
logOut() {
|
||||||
this.username = "";
|
this.username = "";
|
||||||
this.permissions = [];
|
this.permissions = [];
|
||||||
|
this.verifyCode = "";
|
||||||
|
this.currentPage = 0;
|
||||||
|
this.avatar = "";
|
||||||
|
this.id = "";
|
||||||
|
this.accessToken = "";
|
||||||
|
this.refreshToken = "";
|
||||||
|
this.expires = 0;
|
||||||
|
this.isRemembered = false;
|
||||||
|
this.loginDay = 2;
|
||||||
|
this.phone = "";
|
||||||
|
this.email = "";
|
||||||
|
this.nickname = "";
|
||||||
|
this.gender = 0;
|
||||||
|
this.roles = [];
|
||||||
|
this.department_id = "";
|
||||||
removeToken();
|
removeToken();
|
||||||
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
|
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
|
||||||
resetRouter();
|
resetRouter();
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
|
},
|
||||||
|
/** 刷新`token` */
|
||||||
|
async handRefreshToken(data: { refreshToken: string }) {
|
||||||
|
return new Promise<ResponseResult<LoginResult>>((resolve, reject) => {
|
||||||
|
refreshTokenApi(data)
|
||||||
|
.then(data => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
setToken(data.data);
|
||||||
|
}
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,15 +37,52 @@ export type setType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type userType = {
|
export type userType = {
|
||||||
username?: string;
|
/**
|
||||||
permissions?: Array<string>;
|
* 验证码
|
||||||
|
*/
|
||||||
verifyCode?: string;
|
verifyCode?: string;
|
||||||
|
/**
|
||||||
|
* 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码,1,2预留暂不开发)
|
||||||
|
*/
|
||||||
currentPage?: number;
|
currentPage?: number;
|
||||||
userId?: string;
|
/**
|
||||||
avatar?: string;
|
* 是否记住密码
|
||||||
roles?: Array<string>;
|
*/
|
||||||
accessToken?: string;
|
|
||||||
expires?: number;
|
|
||||||
isRemembered?: boolean;
|
isRemembered?: boolean;
|
||||||
|
/**
|
||||||
|
* 登录天数
|
||||||
|
*/
|
||||||
loginDay?: number;
|
loginDay?: number;
|
||||||
|
/** 用户名 */
|
||||||
|
username: string;
|
||||||
|
/** 用户性别 */
|
||||||
|
gender: number;
|
||||||
|
/** 用户昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
/**用户邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/**用户手机号 */
|
||||||
|
phone?: string;
|
||||||
|
/**用户角色 */
|
||||||
|
roles?: Array<string>;
|
||||||
|
/**用户权限 */
|
||||||
|
permissions?: Array<string>;
|
||||||
|
/**用户部门ID */
|
||||||
|
department_id?: string;
|
||||||
|
/**用户头像 */
|
||||||
|
avatar?: string;
|
||||||
|
/**用户数据库ID */
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* 用户Token
|
||||||
|
*/
|
||||||
|
accessToken: string;
|
||||||
|
/**
|
||||||
|
* 过期时间(时间戳)
|
||||||
|
*/
|
||||||
|
expires: number;
|
||||||
|
/**
|
||||||
|
* 用于调用刷新accessToken的接口时所需的token
|
||||||
|
*/
|
||||||
|
refreshToken: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,40 +1,74 @@
|
|||||||
|
import { storageLocal } from "@pureadmin/utils";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { storageLocal, isString, isIncludeAllChildren } from "@pureadmin/utils";
|
|
||||||
|
|
||||||
export interface DataInfo {
|
export interface UserInfo {
|
||||||
/** token */
|
|
||||||
accessToken: string;
|
|
||||||
/** `accessToken`的过期时间(时间戳) */
|
|
||||||
expires: number;
|
|
||||||
/** 用户名 */
|
/** 用户名 */
|
||||||
username?: string;
|
username: string;
|
||||||
/** 当前登陆用户的角色 */
|
/** 用户性别 */
|
||||||
|
gender: number;
|
||||||
|
/** 用户昵称 */
|
||||||
|
nickname?: string;
|
||||||
|
/**用户邮箱 */
|
||||||
|
email?: string;
|
||||||
|
/**用户手机号 */
|
||||||
|
phone?: string;
|
||||||
|
/**用户角色 */
|
||||||
|
roles?: Array<string>;
|
||||||
|
/**用户权限 */
|
||||||
permissions?: Array<string>;
|
permissions?: Array<string>;
|
||||||
/**用户ID */
|
/**用户部门ID */
|
||||||
userId?: string;
|
department_id?: string;
|
||||||
/**用户头像 */
|
/**用户头像 */
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
/**用户权限列表 */
|
/**用户数据库ID */
|
||||||
roles?: Array<string>;
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const userKey = "user-info";
|
export const userTokenKey = "user-token";
|
||||||
|
|
||||||
/**存储用户基础信息 */
|
export const userInfoKey = "user-info";
|
||||||
export function setToken(data: DataInfo) {
|
|
||||||
useUserStoreHook().SET_USERNAME(data.username);
|
export interface UserToken {
|
||||||
useUserStoreHook().SET_PERMISSIONS(data.permissions);
|
/**
|
||||||
useUserStoreHook().SET_USETID(data.userId);
|
* 用户Token
|
||||||
useUserStoreHook().SET_ROLES(data.roles);
|
*/
|
||||||
useUserStoreHook().SET_AVATAR(data.avatar);
|
accessToken: string;
|
||||||
|
/**
|
||||||
|
* 过期时间(时间戳)
|
||||||
|
*/
|
||||||
|
expiresTime: number;
|
||||||
|
/**
|
||||||
|
* 用于调用刷新accessToken的接口时所需的token
|
||||||
|
*/
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**存储用户Token信息 */
|
||||||
|
export function setToken(data: UserToken) {
|
||||||
useUserStoreHook().SET_ACCESSTOKEN(data.accessToken);
|
useUserStoreHook().SET_ACCESSTOKEN(data.accessToken);
|
||||||
useUserStoreHook().SET_EXPIRES(data.expires);
|
useUserStoreHook().SET_REFRESHTOKEN(data.refreshToken);
|
||||||
storageLocal().setItem(userKey, data);
|
useUserStoreHook().SET_EXPIRES(data.expiresTime);
|
||||||
|
storageLocal().setItem(userTokenKey, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**存储用户信息 */
|
||||||
|
export function setUserInfo(data: UserInfo) {
|
||||||
|
useUserStoreHook().SET_AVATAR(data.avatar);
|
||||||
|
useUserStoreHook().SET_USERNAME(data.username);
|
||||||
|
useUserStoreHook().SET_NICKNAME(data.nickname);
|
||||||
|
useUserStoreHook().SET_EMAIL(data.email);
|
||||||
|
useUserStoreHook().SET_PHONE(data.phone);
|
||||||
|
useUserStoreHook().SET_DEPARTMENT_ID(data.department_id);
|
||||||
|
useUserStoreHook().SET_ROLES(data.roles);
|
||||||
|
useUserStoreHook().SET_PERMISSIONS(data.permissions);
|
||||||
|
useUserStoreHook().SET_ID(data.id);
|
||||||
|
useUserStoreHook().SET_GENDER(data.gender);
|
||||||
|
storageLocal().setItem(userInfoKey, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
/** 删除`token`以及key值为`user-info`的localStorage信息 */
|
||||||
export function removeToken() {
|
export function removeToken() {
|
||||||
storageLocal().removeItem(userKey);
|
storageLocal().removeItem(userTokenKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 格式化token(jwt格式) */
|
/** 格式化token(jwt格式) */
|
||||||
@@ -47,37 +81,31 @@ export const formatToken = (token: string): string => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getTokenInfo(): {
|
export function getTokenInfo(): {
|
||||||
|
isRefresh: boolean;
|
||||||
isExpire: boolean;
|
isExpire: boolean;
|
||||||
accesstoken: string;
|
accesstoken: string;
|
||||||
|
refreshToken: string;
|
||||||
} {
|
} {
|
||||||
const userStore = useUserStoreHook();
|
const userStore = useUserStoreHook();
|
||||||
const expires = userStore?.expires ?? 0;
|
const expires = userStore?.expires ?? 0;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const diff = expires - now;
|
const diff = expires - now / 1000;
|
||||||
const accessToken = userStore?.accessToken ?? "";
|
const accessToken = userStore?.accessToken ?? "";
|
||||||
|
const refreshToken = userStore?.refreshToken ?? "";
|
||||||
|
|
||||||
if (diff > 0) {
|
if (diff > 0) {
|
||||||
return {
|
return {
|
||||||
|
isRefresh: diff <= 1800000,
|
||||||
isExpire: false,
|
isExpire: false,
|
||||||
accesstoken: accessToken
|
accesstoken: accessToken,
|
||||||
|
refreshToken: refreshToken
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
isRefresh: true,
|
||||||
isExpire: true,
|
isExpire: true,
|
||||||
accesstoken: accessToken
|
accesstoken: accessToken,
|
||||||
|
refreshToken: refreshToken
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否有按钮级别的权限(根据登录接口返回的`permissions`字段进行判断)*/
|
|
||||||
export const hasPerms = (value: string | Array<string>): boolean => {
|
|
||||||
if (!value) return false;
|
|
||||||
const allPerms = "*:*:*";
|
|
||||||
const { permissions } = useUserStoreHook();
|
|
||||||
if (!permissions) return false;
|
|
||||||
if (permissions.length === 1 && permissions[0] === allPerms) return true;
|
|
||||||
const isAuths = isString(value)
|
|
||||||
? permissions.includes(value)
|
|
||||||
: isIncludeAllChildren(value, permissions);
|
|
||||||
return isAuths ? true : false;
|
|
||||||
};
|
|
||||||
|
|||||||
67
src/views/account-settings/components/AccountSafe.vue
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { deviceDetection, hideTextAtIndex } from "@pureadmin/utils";
|
||||||
|
import { useUserInfo } from "../utils/hooks";
|
||||||
|
defineOptions({
|
||||||
|
name: "AccountSafe"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleReset, handlePhone, handleEmail, userInfo } = useUserInfo();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'min-w-[180px]',
|
||||||
|
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<h3 class="my-8">账户安全</h3>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p>账号密码</p>
|
||||||
|
<el-text class="mx-1" type="info" />
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" text @click="handleReset(userInfo)"
|
||||||
|
>修改</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p>绑定号码</p>
|
||||||
|
<el-text class="mx-1" type="info">
|
||||||
|
已经绑定手机:{{
|
||||||
|
hideTextAtIndex(userInfo.phone, { start: 3, end: 6 })
|
||||||
|
}}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" text @click="handlePhone(userInfo)"
|
||||||
|
>修改</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p>绑定邮箱</p>
|
||||||
|
<el-text class="mx-1" type="info"
|
||||||
|
>已绑定邮箱:{{
|
||||||
|
hideTextAtIndex(userInfo.email, {
|
||||||
|
start: 3,
|
||||||
|
end: userInfo.email.indexOf("@") - 1
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</el-text>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" text @click="handleEmail(userInfo)"
|
||||||
|
>修改</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<el-divider />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.el-divider--horizontal {
|
||||||
|
border-top: 0.1px var(--el-border-color) var(--el-border-style);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
163
src/views/account-settings/components/Profile.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import type { FormInstance } from "element-plus";
|
||||||
|
import ReCropperPreview from "@/components/ReCropperPreview";
|
||||||
|
import { deviceDetection } from "@pureadmin/utils";
|
||||||
|
import uploadLine from "@iconify-icons/ri/upload-line";
|
||||||
|
import { postUploadAvatarAPI, putUpdateBaseUserInfoAPI } from "@/api/user";
|
||||||
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
|
import { userInfoKey } from "@/utils/auth";
|
||||||
|
import { storageLocal } from "@pureadmin/utils";
|
||||||
|
import { useUserInfo } from "../utils/hooks";
|
||||||
|
defineOptions({
|
||||||
|
name: "Profile"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { userInfo, getUserInfo } = useUserInfo();
|
||||||
|
const imgSrc = ref("");
|
||||||
|
const cropperBlob = ref();
|
||||||
|
const cropRef = ref();
|
||||||
|
const uploadRef = ref();
|
||||||
|
const isShow = ref(false);
|
||||||
|
const userInfoFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const disabledDate = (time: Date) => {
|
||||||
|
return time.getTime() > Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = uploadFile => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
imgSrc.value = e.target.result as string;
|
||||||
|
isShow.value = true;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(uploadFile.raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
cropRef.value.hidePopover();
|
||||||
|
uploadRef.value.clearFiles();
|
||||||
|
isShow.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCropper = ({ blob }) => (cropperBlob.value = blob);
|
||||||
|
|
||||||
|
const handleSubmitImage = async () => {
|
||||||
|
const res = await postUploadAvatarAPI(userInfo.id, {
|
||||||
|
file: cropperBlob.value
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
message("更新头像成功", { type: "success" });
|
||||||
|
userInfo.avatar = `/file/${res.data.id}`;
|
||||||
|
const user = storageLocal().getItem<object>(userInfoKey);
|
||||||
|
storageLocal().setItem(userInfoKey, {
|
||||||
|
...user,
|
||||||
|
avatar: `/file/${res.data.id}`
|
||||||
|
});
|
||||||
|
useUserStoreHook().SET_AVATAR(`/file/${res.data.id}`);
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
message("更新头像失败", { type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新信息
|
||||||
|
const onSubmit = async (formEl: FormInstance) => {
|
||||||
|
await formEl.validate(async (valid, fields) => {
|
||||||
|
if (valid) {
|
||||||
|
let updateForm = {
|
||||||
|
name: userInfo.nickname,
|
||||||
|
gender: userInfo.gender
|
||||||
|
};
|
||||||
|
const res = await putUpdateBaseUserInfoAPI(updateForm);
|
||||||
|
if (res.code === 200) {
|
||||||
|
message(res.msg, { type: "success" });
|
||||||
|
await getUserInfo();
|
||||||
|
} else {
|
||||||
|
message(res.msg, { type: "error" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("error submit!", fields);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'min-w-[180px]',
|
||||||
|
deviceDetection() ? 'max-w-[100%]' : 'max-w-[70%]'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<h3 class="my-8">个人信息</h3>
|
||||||
|
<el-form ref="userInfoFormRef" label-position="top" :model="userInfo">
|
||||||
|
<el-form-item label="头像">
|
||||||
|
<el-avatar :size="80" :src="userInfo.avatar" />
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
accept="image/*"
|
||||||
|
action="#"
|
||||||
|
:limit="1"
|
||||||
|
:auto-upload="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-change="onChange"
|
||||||
|
>
|
||||||
|
<el-button plain class="ml-4">
|
||||||
|
<IconifyIconOffline :icon="uploadLine" />
|
||||||
|
<span class="ml-2">更新头像</span>
|
||||||
|
</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="账号" prop="userId">
|
||||||
|
<el-input
|
||||||
|
v-model="userInfo.username"
|
||||||
|
placeholder="请输入账号~"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
label="姓名"
|
||||||
|
prop="name"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请输入用户姓名~',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="userInfo.nickname" placeholder="请输入姓名~" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="性别" prop="gender">
|
||||||
|
<el-radio-group v-model="userInfo.gender" class="ml-4">
|
||||||
|
<el-radio :value="1" size="default">男</el-radio>
|
||||||
|
<el-radio :value="0" size="default">女</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-button type="primary" @click="onSubmit(userInfoFormRef)">
|
||||||
|
更新信息
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
<el-dialog
|
||||||
|
v-model="isShow"
|
||||||
|
width="40%"
|
||||||
|
title="编辑头像"
|
||||||
|
destroy-on-close
|
||||||
|
:closeOnClickModal="false"
|
||||||
|
:before-close="handleClose"
|
||||||
|
:fullscreen="deviceDetection()"
|
||||||
|
>
|
||||||
|
<ReCropperPreview ref="cropRef" :imgSrc="imgSrc" @cropper="onCropper" />
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button bg text @click="handleClose">取消</el-button>
|
||||||
|
<el-button bg text type="primary" @click="handleSubmitImage">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
176
src/views/account-settings/index.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ref, onBeforeMount, computed } from "vue";
|
||||||
|
import { ReText } from "@/components/ReText";
|
||||||
|
import Avatar from "@/assets/user.png";
|
||||||
|
import Profile from "./components/Profile.vue";
|
||||||
|
import AccountSafe from "./components/AccountSafe.vue";
|
||||||
|
import { useGlobal, deviceDetection } from "@pureadmin/utils";
|
||||||
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
|
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
|
||||||
|
import LaySidebarTopCollapse from "@/layout/components/lay-sidebar/components/SidebarTopCollapse.vue";
|
||||||
|
|
||||||
|
import leftLine from "@iconify-icons/ri/arrow-left-s-line";
|
||||||
|
import ProfileIcon from "@iconify-icons/ri/user-3-line";
|
||||||
|
import AccountSafeIcon from "@iconify-icons/ri/profile-line";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "AccountSettings"
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const isOpen = ref(deviceDetection() ? false : true);
|
||||||
|
const { $storage } = useGlobal<GlobalPropertiesApi>();
|
||||||
|
onBeforeMount(() => {
|
||||||
|
useDataThemeChange().dataThemeChange($storage.layout?.overallStyle);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 用户名 */
|
||||||
|
const username = computed(() => {
|
||||||
|
return useUserStoreHook()?.username;
|
||||||
|
});
|
||||||
|
/** 用户头像 */
|
||||||
|
const userAvatar = computed(() => {
|
||||||
|
console.log(useUserStoreHook()?.avatar);
|
||||||
|
if (useUserStoreHook()?.avatar) {
|
||||||
|
return useUserStoreHook()?.avatar;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
return Avatar;
|
||||||
|
});
|
||||||
|
/**用户账号 */
|
||||||
|
const nickname = computed(() => {
|
||||||
|
return useUserStoreHook()?.nickname;
|
||||||
|
});
|
||||||
|
const panes = [
|
||||||
|
{
|
||||||
|
key: "profile",
|
||||||
|
label: "个人信息",
|
||||||
|
icon: ProfileIcon,
|
||||||
|
component: Profile
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "accountSafe",
|
||||||
|
label: "账户安全",
|
||||||
|
icon: AccountSafeIcon,
|
||||||
|
component: AccountSafe
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const witchPane = ref("profile");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-container class="h-full">
|
||||||
|
<el-aside
|
||||||
|
v-if="isOpen"
|
||||||
|
class="pure-account-settings overflow-hidden px-2 dark:!bg-[var(--el-bg-color)] border-r-[1px] border-[var(--pure-border-color)]"
|
||||||
|
:width="deviceDetection() ? '180px' : '240px'"
|
||||||
|
>
|
||||||
|
<el-menu :default-active="witchPane" class="pure-account-settings-menu">
|
||||||
|
<el-menu-item
|
||||||
|
class="hover:!transition-all hover:!duration-200 hover:!text-base !h-[50px]"
|
||||||
|
@click="router.go(-1)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<IconifyIconOffline :icon="leftLine" />
|
||||||
|
<span class="ml-2">返回</span>
|
||||||
|
</div>
|
||||||
|
</el-menu-item>
|
||||||
|
<div class="flex items-center ml-8 mt-4 mb-4">
|
||||||
|
<el-avatar :size="48" :src="userAvatar" />
|
||||||
|
<div class="ml-4 flex flex-col max-w-[130px]">
|
||||||
|
<ReText class="font-bold !self-baseline">
|
||||||
|
{{ username }}
|
||||||
|
</ReText>
|
||||||
|
<ReText class="!self-baseline" type="info">
|
||||||
|
{{ nickname }}
|
||||||
|
</ReText>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="item in panes"
|
||||||
|
:key="item.key"
|
||||||
|
:index="item.key"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
witchPane = item.key;
|
||||||
|
if (deviceDetection()) {
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="flex items-center z-10">
|
||||||
|
<el-icon><IconifyIconOffline :icon="item.icon" /></el-icon>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-main>
|
||||||
|
<LaySidebarTopCollapse
|
||||||
|
v-if="deviceDetection()"
|
||||||
|
class="px-0"
|
||||||
|
:is-active="isOpen"
|
||||||
|
@toggleClick="isOpen = !isOpen"
|
||||||
|
/>
|
||||||
|
<component
|
||||||
|
:is="panes.find(item => item.key === witchPane).component"
|
||||||
|
:class="[!deviceDetection() && 'ml-[120px]']"
|
||||||
|
/>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.pure-account-settings {
|
||||||
|
background: var(--pure-theme-menu-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-account-settings-menu {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.el-menu-item {
|
||||||
|
height: 48px !important;
|
||||||
|
color: var(--pure-theme-menu-text);
|
||||||
|
background-color: transparent !important;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--pure-theme-menu-title-hover) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
color: #fff !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 8px;
|
||||||
|
margin: 4px 0;
|
||||||
|
clear: both;
|
||||||
|
content: "";
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
body[layout] {
|
||||||
|
.el-menu--vertical .is-active {
|
||||||
|
color: #fff !important;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
349
src/views/account-settings/utils/hooks.tsx
Normal file
@@ -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<UserInfo>({
|
||||||
|
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<object>(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: () => (
|
||||||
|
<div>
|
||||||
|
<ElForm ref={ruleFormRef} model={passwordForm}>
|
||||||
|
<ElFormItem
|
||||||
|
prop="oldPassword"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入旧密码~",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
type="password"
|
||||||
|
v-model={passwordForm.oldPassword}
|
||||||
|
placeholder="请输入旧密码~"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="newPassword" rules={passwordRule}>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
type="password"
|
||||||
|
v-model={passwordForm.newPassword}
|
||||||
|
placeholder="请输入新密码~"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
<div class="mt-4 flex">
|
||||||
|
{pwdProgress.map(({ color, text }, idx) => (
|
||||||
|
<div
|
||||||
|
class="w-[19vw]"
|
||||||
|
style={{ marginLeft: idx !== 0 ? "4px" : 0 }}
|
||||||
|
>
|
||||||
|
<ElProgress
|
||||||
|
striped
|
||||||
|
striped-flow
|
||||||
|
duration={curScore.value === idx ? 6 : 0}
|
||||||
|
percentage={curScore.value >= idx ? 100 : 0}
|
||||||
|
color={color}
|
||||||
|
stroke-width={10}
|
||||||
|
show-text={false}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="text-center"
|
||||||
|
style={{ color: curScore.value === idx ? color : "" }}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: () => (
|
||||||
|
<div>
|
||||||
|
<ElForm ref={ruleFormRef} model={phoneForm}>
|
||||||
|
<ElFormItem
|
||||||
|
prop="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入密码~",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
type="password"
|
||||||
|
v-model={phoneForm.password}
|
||||||
|
placeholder="请输入密码~"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="phone" rules={phoneRule}>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
type="text"
|
||||||
|
v-model={phoneForm.phone}
|
||||||
|
placeholder="请输入手机号~"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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: () => (
|
||||||
|
<div>
|
||||||
|
<ElForm ref={ruleFormRef} model={emailForm}>
|
||||||
|
<ElFormItem
|
||||||
|
prop="password"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请输入密码~",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
type="password"
|
||||||
|
v-model={emailForm.password}
|
||||||
|
placeholder="请输入密码~"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="email" rules={emailRule}>
|
||||||
|
<ElInput
|
||||||
|
clearable
|
||||||
|
type="text"
|
||||||
|
v-model={emailForm.email}
|
||||||
|
placeholder="请输入联系邮箱~"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</ElForm>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -27,7 +27,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
|||||||
if (valid) {
|
if (valid) {
|
||||||
// 模拟登录请求,需根据实际开发进行修改
|
// 模拟登录请求,需根据实际开发进行修改
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
message(transformI18n($t("login.pureLoginSuccess")), {
|
message(transformI18n($t("login:LoginSuccess")), {
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -51,7 +51,7 @@ function onBack() {
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.phone"
|
v-model="ruleForm.phone"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.purePhone')"
|
:placeholder="t('login:Phone')"
|
||||||
:prefix-icon="useRenderIcon(Iphone)"
|
:prefix-icon="useRenderIcon(Iphone)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -63,7 +63,7 @@ function onBack() {
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.verifyCode"
|
v-model="ruleForm.verifyCode"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.pureSmsVerifyCode')"
|
:placeholder="t('login:SmsVerifyCode')"
|
||||||
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -73,8 +73,8 @@ function onBack() {
|
|||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
text.length > 0
|
text.length > 0
|
||||||
? text + t("login.pureInfo")
|
? text + t("login:Info")
|
||||||
: t("login.pureGetVerifyCode")
|
: t("login:GetVerifyCode")
|
||||||
}}
|
}}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +90,7 @@ function onBack() {
|
|||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="onLogin(ruleFormRef)"
|
@click="onLogin(ruleFormRef)"
|
||||||
>
|
>
|
||||||
{{ t("login.pureLogin") }}
|
{{ t("login:Login") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
@@ -98,7 +98,7 @@ function onBack() {
|
|||||||
<Motion :delay="200">
|
<Motion :delay="200">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button class="w-full" size="default" @click="onBack">
|
<el-button class="w-full" size="default" @click="onBack">
|
||||||
{{ t("login.pureBack") }}
|
{{ t("login:Back") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ const { t } = useI18n();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Motion class="-mt-2 -mb-2">
|
<Motion class="-mt-2 -mb-2">
|
||||||
<ReQrcode :text="t('login.pureTest')" />
|
<ReQrcode :text="t('login:Test')" />
|
||||||
</Motion>
|
</Motion>
|
||||||
<Motion :delay="100">
|
<Motion :delay="100">
|
||||||
<el-divider>
|
<el-divider>
|
||||||
<p class="text-gray-500 text-xs">{{ t("login.pureTip") }}</p>
|
<p class="text-gray-500 text-xs">{{ t("login:Tip") }}</p>
|
||||||
</el-divider>
|
</el-divider>
|
||||||
</Motion>
|
</Motion>
|
||||||
<Motion :delay="150">
|
<Motion :delay="150">
|
||||||
@@ -21,7 +21,7 @@ const { t } = useI18n();
|
|||||||
class="w-full mt-4"
|
class="w-full mt-4"
|
||||||
@click="useUserStoreHook().SET_CURRENTPAGE(0)"
|
@click="useUserStoreHook().SET_CURRENTPAGE(0)"
|
||||||
>
|
>
|
||||||
{{ t("login.pureBack") }}
|
{{ t("login:Back") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</Motion>
|
</Motion>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,37 +3,70 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { ref, reactive } from "vue";
|
import { ref, reactive } from "vue";
|
||||||
import Motion from "../utils/motion";
|
import Motion from "../utils/motion";
|
||||||
import { message } from "@/utils/message";
|
import { message } from "@/utils/message";
|
||||||
import { updateRules } from "../utils/rule";
|
import { registerRules } from "../utils/rule";
|
||||||
import type { FormInstance } from "element-plus";
|
import type { FormInstance, FormItemProp } from "element-plus";
|
||||||
import { useVerifyCode } from "../utils/verifyCode";
|
import { useVerifyCode } from "../utils/verifyCode";
|
||||||
|
import { clone } from "@pureadmin/utils";
|
||||||
import { $t, transformI18n } from "@/plugins/i18n";
|
import { $t, transformI18n } from "@/plugins/i18n";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
import Lock from "@iconify-icons/ri/lock-fill";
|
import Lock from "@iconify-icons/ri/lock-fill";
|
||||||
import Iphone from "@iconify-icons/ep/iphone";
|
import Iphone from "@iconify-icons/ep/iphone";
|
||||||
import User from "@iconify-icons/ri/user-3-fill";
|
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 { t } = useI18n();
|
||||||
const checked = ref(false);
|
const checked = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const timer = ref(null);
|
||||||
const ruleForm = reactive({
|
const ruleForm = reactive({
|
||||||
username: "",
|
username: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
verifyCode: "",
|
gender: 1,
|
||||||
|
email: "",
|
||||||
|
nickname: "",
|
||||||
|
department_id: "",
|
||||||
|
code: "",
|
||||||
password: "",
|
password: "",
|
||||||
repeatPassword: ""
|
repeatPassword: ""
|
||||||
});
|
});
|
||||||
|
/**当前选项 */
|
||||||
|
const currentOption = ref<number>(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<FormInstance>();
|
const ruleFormRef = ref<FormInstance>();
|
||||||
const { isDisabled, text } = useVerifyCode();
|
const { isDisabled, text } = useVerifyCode();
|
||||||
const repeatPasswordRule = [
|
const repeatPasswordRule = [
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.purePassWordSureReg"))));
|
callback(new Error(transformI18n($t("login:PassWordSureReg"))));
|
||||||
} else if (ruleForm.password !== value) {
|
} else if (ruleForm.password !== value) {
|
||||||
callback(
|
callback(new Error(transformI18n($t("login:PassWordDifferentReg"))));
|
||||||
new Error(transformI18n($t("login.purePassWordDifferentReg")))
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -45,19 +78,33 @@ const repeatPasswordRule = [
|
|||||||
const onUpdate = async (formEl: FormInstance | undefined) => {
|
const onUpdate = async (formEl: FormInstance | undefined) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (!formEl) return;
|
if (!formEl) return;
|
||||||
await formEl.validate(valid => {
|
await formEl.validate(async (valid, fields) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (checked.value) {
|
if (checked.value) {
|
||||||
// 模拟请求,需根据实际开发进行修改
|
const res = await postRegisterAPI({
|
||||||
setTimeout(() => {
|
username: ruleForm.username,
|
||||||
message(transformI18n($t("login.pureRegisterSuccess")), {
|
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"
|
type: "success"
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}, 2000);
|
useUserStoreHook().SET_CURRENTPAGE(0);
|
||||||
|
} else {
|
||||||
|
message(res.msg, {
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
message(transformI18n($t("login.pureTickPrivacy")), {
|
message(transformI18n($t("login:TickPrivacy")), {
|
||||||
type: "warning"
|
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() {
|
function onBack() {
|
||||||
useVerifyCode().end();
|
|
||||||
useUserStoreHook().SET_CURRENTPAGE(0);
|
useUserStoreHook().SET_CURRENTPAGE(0);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -77,15 +157,15 @@ function onBack() {
|
|||||||
<el-form
|
<el-form
|
||||||
ref="ruleFormRef"
|
ref="ruleFormRef"
|
||||||
:model="ruleForm"
|
:model="ruleForm"
|
||||||
:rules="updateRules"
|
:rules="registerRules"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<Motion>
|
<Motion v-if="currentOption === 1">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:rules="[
|
:rules="[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: transformI18n($t('login.pureUsernameReg')),
|
message: transformI18n($t('login:UsernameReg')),
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
@@ -94,101 +174,215 @@ function onBack() {
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.username"
|
v-model="ruleForm.username"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.pureUsername')"
|
:placeholder="t('login:Username')"
|
||||||
:prefix-icon="useRenderIcon(User)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(UserName, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</Motion>
|
||||||
|
<Motion v-if="currentOption === 1">
|
||||||
|
<el-form-item
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: transformI18n($t('login:NicknameReg')),
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
prop="nickname"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.nickname"
|
||||||
|
clearable
|
||||||
|
:placeholder="t('login:Nickname')"
|
||||||
|
:prefix-icon="
|
||||||
|
useRenderIcon(User, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="100">
|
<Motion v-if="currentOption === 1" :delay="100">
|
||||||
<el-form-item prop="phone">
|
<el-form-item prop="phone">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.phone"
|
v-model="ruleForm.phone"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.purePhone')"
|
:placeholder="t('login:Phone')"
|
||||||
:prefix-icon="useRenderIcon(Iphone)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(Iphone, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</Motion>
|
||||||
|
<Motion v-if="currentOption === 2" :delay="100">
|
||||||
|
<el-form-item prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.email"
|
||||||
|
clearable
|
||||||
|
:placeholder="t('login:Email')"
|
||||||
|
:prefix-icon="
|
||||||
|
useRenderIcon(Mail, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="150">
|
<Motion v-if="currentOption === 2" :delay="150">
|
||||||
<el-form-item prop="verifyCode">
|
<el-form-item prop="code">
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.verifyCode"
|
v-model="ruleForm.code"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.pureSmsVerifyCode')"
|
:placeholder="t('login:EmailVerifyCode')"
|
||||||
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
:prefix-icon="
|
||||||
|
useRenderIcon('ri:shield-keyhole-line', {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
@click="useVerifyCode().start(ruleFormRef, 'phone')"
|
@click="start(ruleFormRef, 'email')"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
text.length > 0
|
text.length > 0
|
||||||
? text + t("login.pureInfo")
|
? text + t("login:Info")
|
||||||
: t("login.pureGetVerifyCode")
|
: t("login:GetVerifyCode")
|
||||||
}}
|
}}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="200">
|
<Motion v-if="currentOption === 2" :delay="200">
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.password"
|
v-model="ruleForm.password"
|
||||||
clearable
|
clearable
|
||||||
show-password
|
show-password
|
||||||
:placeholder="t('login.purePassword')"
|
:placeholder="t('login:Password')"
|
||||||
:prefix-icon="useRenderIcon(Lock)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(Lock, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="250">
|
<Motion v-if="currentOption === 2" :delay="250">
|
||||||
<el-form-item :rules="repeatPasswordRule" prop="repeatPassword">
|
<el-form-item :rules="repeatPasswordRule" prop="repeatPassword">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.repeatPassword"
|
v-model="ruleForm.repeatPassword"
|
||||||
clearable
|
clearable
|
||||||
show-password
|
show-password
|
||||||
:placeholder="t('login.pureSure')"
|
:placeholder="t('login:Sure')"
|
||||||
:prefix-icon="useRenderIcon(Lock)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(Lock, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
<Motion v-if="currentOption === 1" :delay="100">
|
||||||
|
<el-form-item label="性别" prop="gender">
|
||||||
|
<el-radio-group v-model="ruleForm.gender" class="ml-4">
|
||||||
|
<el-radio :value="1" size="default">男</el-radio>
|
||||||
|
<el-radio :value="0" size="default">女</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="300">
|
<Motion :delay="300">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-checkbox v-model="checked">
|
<el-checkbox v-model="checked">
|
||||||
{{ t("login.pureReadAccept") }}
|
{{ t("login:ReadAccept") }}
|
||||||
</el-checkbox>
|
</el-checkbox>
|
||||||
<el-button link type="primary">
|
<el-button link type="primary">
|
||||||
{{ t("login.purePrivacyPolicy") }}
|
{{ t("login:PrivacyPolicy") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
<Motion v-if="currentOption !== 1" :delay="350">
|
||||||
<Motion :delay="350">
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
<div class="w-full flex justify-center items-center">
|
||||||
<el-button
|
<el-button
|
||||||
class="w-full"
|
class="w-[75%]"
|
||||||
|
size="default"
|
||||||
|
round
|
||||||
|
:loading="loading"
|
||||||
|
@click="clickBack"
|
||||||
|
>
|
||||||
|
{{ t("login:Laststep") }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</Motion>
|
||||||
|
<Motion v-if="currentOption !== 2" :delay="350">
|
||||||
|
<el-form-item>
|
||||||
|
<div class="w-full flex justify-center items-center">
|
||||||
|
<el-button
|
||||||
|
class="w-[75%]"
|
||||||
size="default"
|
size="default"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
round
|
||||||
|
:loading="loading"
|
||||||
|
@click="clickNext"
|
||||||
|
>
|
||||||
|
{{ t("login:Nextstep") }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</Motion>
|
||||||
|
<Motion v-if="currentOption === 2" :delay="350">
|
||||||
|
<el-form-item>
|
||||||
|
<div class="w-full flex justify-center items-center">
|
||||||
|
<el-button
|
||||||
|
class="w-[75%]"
|
||||||
|
size="default"
|
||||||
|
type="primary"
|
||||||
|
round
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="onUpdate(ruleFormRef)"
|
@click="onUpdate(ruleFormRef)"
|
||||||
>
|
>
|
||||||
{{ t("login.pureDefinite") }}
|
{{ t("login:Register") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="400">
|
<Motion :delay="400">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button class="w-full" size="default" @click="onBack">
|
<div class="w-full flex justify-center items-center">
|
||||||
{{ t("login.pureBack") }}
|
<el-button class="w-[75%]" round size="default" @click="onBack">
|
||||||
|
{{ t("login:Back") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@@ -4,19 +4,24 @@ import { ref, reactive } from "vue";
|
|||||||
import Motion from "../utils/motion";
|
import Motion from "../utils/motion";
|
||||||
import { message } from "@/utils/message";
|
import { message } from "@/utils/message";
|
||||||
import { updateRules } from "../utils/rule";
|
import { updateRules } from "../utils/rule";
|
||||||
import type { FormInstance } from "element-plus";
|
import type { FormInstance, FormItemProp } from "element-plus";
|
||||||
import { useVerifyCode } from "../utils/verifyCode";
|
import { useVerifyCode } from "../utils/verifyCode";
|
||||||
import { $t, transformI18n } from "@/plugins/i18n";
|
import { $t, transformI18n } from "@/plugins/i18n";
|
||||||
|
import { clone } from "@pureadmin/utils";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import { postGetCodeAPI, postResetPasswordAPI } from "@/api/login";
|
||||||
import Lock from "@iconify-icons/ri/lock-fill";
|
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 { t } = useI18n();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const timer = ref(null);
|
||||||
const ruleForm = reactive({
|
const ruleForm = reactive({
|
||||||
phone: "",
|
username: "",
|
||||||
verifyCode: "",
|
email: "",
|
||||||
|
code: "",
|
||||||
password: "",
|
password: "",
|
||||||
repeatPassword: ""
|
repeatPassword: ""
|
||||||
});
|
});
|
||||||
@@ -26,11 +31,9 @@ const repeatPasswordRule = [
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.purePassWordSureReg"))));
|
callback(new Error(transformI18n($t("login:PassWordSureReg"))));
|
||||||
} else if (ruleForm.password !== value) {
|
} else if (ruleForm.password !== value) {
|
||||||
callback(
|
callback(new Error(transformI18n($t("login:PassWordDifferentReg"))));
|
||||||
new Error(transformI18n($t("login.purePassWordDifferentReg")))
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -42,23 +45,67 @@ const repeatPasswordRule = [
|
|||||||
const onUpdate = async (formEl: FormInstance | undefined) => {
|
const onUpdate = async (formEl: FormInstance | undefined) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (!formEl) return;
|
if (!formEl) return;
|
||||||
await formEl.validate(valid => {
|
await formEl.validate(async (valid, fields) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
// 模拟请求,需根据实际开发进行修改
|
const res = await postResetPasswordAPI({
|
||||||
setTimeout(() => {
|
username: ruleForm.username,
|
||||||
message(transformI18n($t("login.purePassWordUpdateReg")), {
|
password: ruleForm.password,
|
||||||
|
code: ruleForm.code,
|
||||||
|
mail: ruleForm.email
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
message(res.msg, {
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}, 2000);
|
useUserStoreHook().SET_CURRENTPAGE(0);
|
||||||
|
} else {
|
||||||
|
message(res.msg, {
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
loading.value = false;
|
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() {
|
function onBack() {
|
||||||
useVerifyCode().end();
|
|
||||||
useUserStoreHook().SET_CURRENTPAGE(0);
|
useUserStoreHook().SET_CURRENTPAGE(0);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -71,34 +118,70 @@ function onBack() {
|
|||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<Motion>
|
<Motion>
|
||||||
<el-form-item prop="phone">
|
<el-form-item
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: transformI18n($t('login:UsernameReg')),
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
prop="username"
|
||||||
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.phone"
|
v-model="ruleForm.username"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.purePhone')"
|
:placeholder="t('login:Username')"
|
||||||
:prefix-icon="useRenderIcon(Iphone)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(UserName, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="100">
|
<Motion :delay="100">
|
||||||
<el-form-item prop="verifyCode">
|
<el-form-item prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="ruleForm.email"
|
||||||
|
clearable
|
||||||
|
:placeholder="t('login:Email')"
|
||||||
|
:prefix-icon="
|
||||||
|
useRenderIcon(Mail, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</Motion>
|
||||||
|
<Motion :delay="100">
|
||||||
|
<el-form-item prop="code">
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.verifyCode"
|
v-model="ruleForm.code"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.pureSmsVerifyCode')"
|
:placeholder="t('login:EmailVerifyCode')"
|
||||||
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
:prefix-icon="
|
||||||
|
useRenderIcon('ri:shield-keyhole-line', {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
class="ml-2"
|
class="ml-2"
|
||||||
@click="useVerifyCode().start(ruleFormRef, 'phone')"
|
@click="start(ruleFormRef, 'email')"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
text.length > 0
|
text.length > 0
|
||||||
? text + t("login.pureInfo")
|
? text + t("login:Info")
|
||||||
: t("login.pureGetVerifyCode")
|
: t("login:GetVerifyCode")
|
||||||
}}
|
}}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,8 +194,14 @@ function onBack() {
|
|||||||
v-model="ruleForm.password"
|
v-model="ruleForm.password"
|
||||||
clearable
|
clearable
|
||||||
show-password
|
show-password
|
||||||
:placeholder="t('login.purePassword')"
|
:placeholder="t('login:Password')"
|
||||||
:prefix-icon="useRenderIcon(Lock)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(Lock, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
@@ -123,31 +212,42 @@ function onBack() {
|
|||||||
v-model="ruleForm.repeatPassword"
|
v-model="ruleForm.repeatPassword"
|
||||||
clearable
|
clearable
|
||||||
show-password
|
show-password
|
||||||
:placeholder="t('login.pureSure')"
|
:placeholder="t('login:Sure')"
|
||||||
:prefix-icon="useRenderIcon(Lock)"
|
:prefix-icon="
|
||||||
|
useRenderIcon(Lock, {
|
||||||
|
color: '#4380FF',
|
||||||
|
width: 32,
|
||||||
|
height: 32
|
||||||
|
})
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="250">
|
<Motion :delay="250">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
|
<div class="w-full flex justify-center items-center">
|
||||||
<el-button
|
<el-button
|
||||||
class="w-full"
|
class="w-[75%]"
|
||||||
size="default"
|
size="default"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
round
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="onUpdate(ruleFormRef)"
|
@click="onUpdate(ruleFormRef)"
|
||||||
>
|
>
|
||||||
{{ t("login.pureDefinite") }}
|
{{ t("login:Definite") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|
||||||
<Motion :delay="300">
|
<Motion :delay="300">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button class="w-full" size="default" @click="onBack">
|
<div class="w-full flex justify-center items-center">
|
||||||
{{ t("login.pureBack") }}
|
<el-button class="w-[75%]" size="default" round @click="onBack">
|
||||||
|
{{ t("login:Back") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ import Lock from "@iconify-icons/ri/lock-fill";
|
|||||||
import Check from "@iconify-icons/ep/check";
|
import Check from "@iconify-icons/ep/check";
|
||||||
import User from "@iconify-icons/ri/user-3-fill";
|
import User from "@iconify-icons/ri/user-3-fill";
|
||||||
import Info from "@iconify-icons/ri/information-line";
|
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({
|
defineOptions({
|
||||||
name: "Login"
|
name: "Login"
|
||||||
@@ -79,17 +81,21 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
|||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
useUserStoreHook().SET_ACCESSTOKEN(res.data.accessToken);
|
useUserStoreHook().SET_ACCESSTOKEN(res.data.accessToken);
|
||||||
// 获取后端路由
|
// 获取后端路由
|
||||||
return initRouter().then(() => {
|
return initRouter().then(async () => {
|
||||||
disabled.value = true;
|
disabled.value = true;
|
||||||
|
const res = await getUserInfoAPI();
|
||||||
|
if (res.success) {
|
||||||
|
setUserInfo(res.data);
|
||||||
|
}
|
||||||
router
|
router
|
||||||
.push(getTopMenu(true).path)
|
.push(getTopMenu(true).path)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
message(t("login.pureLoginSuccess"), { type: "success" });
|
message(t("login:LoginSuccess"), { type: "success" });
|
||||||
})
|
})
|
||||||
.finally(() => (disabled.value = false));
|
.finally(() => (disabled.value = false));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message(t("login.pureLoginFail"), { type: "error" });
|
message(t("login:LoginFail"), { type: "error" });
|
||||||
await getImgCode();
|
await getImgCode();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -107,9 +113,9 @@ const immediateDebounce: any = debounce(
|
|||||||
/**获取验证码 */
|
/**获取验证码 */
|
||||||
const getImgCode = async () => {
|
const getImgCode = async () => {
|
||||||
const res = await GetCaptchaAPI();
|
const res = await GetCaptchaAPI();
|
||||||
if (res.code === 200) {
|
if (res.success) {
|
||||||
imgCode.value = res.data.image;
|
imgCode.value = res.data.captcha;
|
||||||
imgId.value = res.data.captchaId;
|
imgId.value = res.data.uuid;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,9 +128,6 @@ useEventListener(document, "keydown", ({ code }) => {
|
|||||||
immediateDebounce(ruleFormRef.value);
|
immediateDebounce(ruleFormRef.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// watch(imgCode, value => {
|
|
||||||
// useUserStoreHook().SET_VERIFYCODE(value);
|
|
||||||
// });
|
|
||||||
watch(checked, bool => {
|
watch(checked, bool => {
|
||||||
useUserStoreHook().SET_ISREMEMBERED(bool);
|
useUserStoreHook().SET_ISREMEMBERED(bool);
|
||||||
});
|
});
|
||||||
@@ -208,7 +211,7 @@ onMounted(async () => {
|
|||||||
:rules="[
|
:rules="[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: transformI18n($t('login.pureUsernameReg')),
|
message: transformI18n($t('login:UsernameReg')),
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
@@ -217,7 +220,7 @@ onMounted(async () => {
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.username"
|
v-model="ruleForm.username"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.pureUsername')"
|
:placeholder="t('login:Username')"
|
||||||
:prefix-icon="useRenderIcon(User)"
|
:prefix-icon="useRenderIcon(User)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -229,7 +232,7 @@ onMounted(async () => {
|
|||||||
v-model="ruleForm.password"
|
v-model="ruleForm.password"
|
||||||
clearable
|
clearable
|
||||||
show-password
|
show-password
|
||||||
:placeholder="t('login.purePassword')"
|
:placeholder="t('login:Password')"
|
||||||
:prefix-icon="useRenderIcon(Lock)"
|
:prefix-icon="useRenderIcon(Lock)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -240,7 +243,7 @@ onMounted(async () => {
|
|||||||
<el-input
|
<el-input
|
||||||
v-model="ruleForm.verifyCode"
|
v-model="ruleForm.verifyCode"
|
||||||
clearable
|
clearable
|
||||||
:placeholder="t('login.pureVerifyCode')"
|
:placeholder="t('login:VerifyCode')"
|
||||||
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
@@ -273,10 +276,10 @@ onMounted(async () => {
|
|||||||
<option value="7">7</option>
|
<option value="7">7</option>
|
||||||
<option value="30">30</option>
|
<option value="30">30</option>
|
||||||
</select>
|
</select>
|
||||||
{{ t("login.pureRemember") }}
|
{{ t("login:Remember") }}
|
||||||
<IconifyIconOffline
|
<IconifyIconOffline
|
||||||
v-tippy="{
|
v-tippy="{
|
||||||
content: t('login.pureRememberInfo'),
|
content: t('login:RememberInfo'),
|
||||||
placement: 'top'
|
placement: 'top'
|
||||||
}"
|
}"
|
||||||
:icon="Info"
|
:icon="Info"
|
||||||
@@ -289,7 +292,7 @@ onMounted(async () => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
@click="useUserStoreHook().SET_CURRENTPAGE(4)"
|
@click="useUserStoreHook().SET_CURRENTPAGE(4)"
|
||||||
>
|
>
|
||||||
{{ t("login.pureForget") }}
|
{{ t("login:Forget") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -300,7 +303,7 @@ onMounted(async () => {
|
|||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click="onLogin(ruleFormRef)"
|
@click="onLogin(ruleFormRef)"
|
||||||
>
|
>
|
||||||
{{ t("login.pureLogin") }}
|
{{ t("login:Login") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|||||||
@@ -2,31 +2,31 @@ import { $t } from "@/plugins/i18n";
|
|||||||
|
|
||||||
const operates = [
|
const operates = [
|
||||||
{
|
{
|
||||||
title: $t("login.purePhoneLogin")
|
title: $t("login:PhoneLogin")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t("login.pureQRCodeLogin")
|
title: $t("login:QRCodeLogin")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t("login.pureRegister")
|
title: $t("login:Register")
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const thirdParty = [
|
const thirdParty = [
|
||||||
{
|
{
|
||||||
title: $t("login.pureWeChatLogin"),
|
title: $t("login:WeChatLogin"),
|
||||||
icon: "wechat"
|
icon: "wechat"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t("login.pureAlipayLogin"),
|
title: $t("login:AlipayLogin"),
|
||||||
icon: "alipay"
|
icon: "alipay"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t("login.pureQQLogin"),
|
title: $t("login:QQLogin"),
|
||||||
icon: "qq"
|
icon: "qq"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t("login.pureWeiBoLogin"),
|
title: $t("login:WeiBoLogin"),
|
||||||
icon: "weibo"
|
icon: "weibo"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ const loginRules = reactive<FormRules>({
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.purePassWordReg"))));
|
callback(new Error(transformI18n($t("login:PassWordReg"))));
|
||||||
} else if (!REGEXP_PWD.test(value)) {
|
} else if (!REGEXP_PWD.test(value)) {
|
||||||
callback(new Error(transformI18n($t("login.purePassWordRuleReg"))));
|
callback(new Error(transformI18n($t("login:PassWordRuleReg"))));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ const loginRules = reactive<FormRules>({
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
|
callback(new Error(transformI18n($t("login:VerifyCodeReg"))));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -46,9 +46,9 @@ const phoneRules = reactive<FormRules>({
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.purePhoneReg"))));
|
callback(new Error(transformI18n($t("login:PhoneReg"))));
|
||||||
} else if (!isPhone(value)) {
|
} else if (!isPhone(value)) {
|
||||||
callback(new Error(transformI18n($t("login.purePhoneCorrectReg"))));
|
callback(new Error(transformI18n($t("login:PhoneCorrectReg"))));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -60,9 +60,9 @@ const phoneRules = reactive<FormRules>({
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
|
callback(new Error(transformI18n($t("login:VerifyCodeReg"))));
|
||||||
} else if (!REGEXP_SIX.test(value)) {
|
} else if (!REGEXP_SIX.test(value)) {
|
||||||
callback(new Error(transformI18n($t("login.pureVerifyCodeSixReg"))));
|
callback(new Error(transformI18n($t("login:VerifyCodeSixReg"))));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -74,27 +74,22 @@ const phoneRules = reactive<FormRules>({
|
|||||||
|
|
||||||
/** 忘记密码校验 */
|
/** 忘记密码校验 */
|
||||||
const updateRules = reactive<FormRules>({
|
const updateRules = reactive<FormRules>({
|
||||||
phone: [
|
email: [
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.purePhoneReg"))));
|
callback(new Error(transformI18n($t("login:EmailReg"))));
|
||||||
} else if (!isPhone(value)) {
|
|
||||||
callback(new Error(transformI18n($t("login.purePhoneCorrectReg"))));
|
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
trigger: "blur"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
verifyCode: [
|
code: [
|
||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.pureVerifyCodeReg"))));
|
callback(new Error(transformI18n($t("login:VerifyCodeReg"))));
|
||||||
} else if (!REGEXP_SIX.test(value)) {
|
|
||||||
callback(new Error(transformI18n($t("login.pureVerifyCodeSixReg"))));
|
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -106,9 +101,9 @@ const updateRules = reactive<FormRules>({
|
|||||||
{
|
{
|
||||||
validator: (rule, value, callback) => {
|
validator: (rule, value, callback) => {
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
callback(new Error(transformI18n($t("login.purePassWordReg"))));
|
callback(new Error(transformI18n($t("login:PassWordReg"))));
|
||||||
} else if (!REGEXP_PWD.test(value)) {
|
} else if (!REGEXP_PWD.test(value)) {
|
||||||
callback(new Error(transformI18n($t("login.purePassWordRuleReg"))));
|
callback(new Error(transformI18n($t("login:PassWordRuleReg"))));
|
||||||
} else {
|
} else {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
@@ -118,4 +113,81 @@ const updateRules = reactive<FormRules>({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
export { loginRules, phoneRules, updateRules };
|
/** 注册账号校验 */
|
||||||
|
const registerRules = reactive<FormRules>({
|
||||||
|
phone: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error(transformI18n($t("login:PhoneReg"))));
|
||||||
|
} else if (!isPhone(value)) {
|
||||||
|
callback(new Error(transformI18n($t("login:PhoneCorrectReg"))));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error(transformI18n($t("login:VerifyCodeReg"))));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error(transformI18n($t("login:PassWordReg"))));
|
||||||
|
} else if (!REGEXP_PWD.test(value)) {
|
||||||
|
callback(new Error(transformI18n($t("login:PassWordRuleReg"))));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
username: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error(transformI18n($t("login:UsernameReg"))));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
nickname: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error(transformI18n($t("login:NicknameReg"))));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback(new Error(transformI18n($t("login:EmailReg"))));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export { loginRules, phoneRules, updateRules, registerRules };
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { hasAuth, getAuths } from "@/router/utils";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PermissionButtonRouter"
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<p class="mb-2">当前拥有的code列表:{{ getAuths() }}</p>
|
|
||||||
|
|
||||||
<el-card shadow="never" class="mb-2">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">组件方式判断权限</div>
|
|
||||||
</template>
|
|
||||||
<el-space wrap>
|
|
||||||
<Auth value="permission:btn:add">
|
|
||||||
<el-button plain type="warning">
|
|
||||||
拥有code:'permission:btn:add' 权限可见
|
|
||||||
</el-button>
|
|
||||||
</Auth>
|
|
||||||
<Auth :value="['permission:btn:edit']">
|
|
||||||
<el-button plain type="primary">
|
|
||||||
拥有code:['permission:btn:edit'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</Auth>
|
|
||||||
<Auth
|
|
||||||
:value="[
|
|
||||||
'permission:btn:add',
|
|
||||||
'permission:btn:edit',
|
|
||||||
'permission:btn:delete'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<el-button plain type="danger">
|
|
||||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
|
||||||
'permission:btn:delete'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</Auth>
|
|
||||||
</el-space>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card shadow="never" class="mb-2">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">函数方式判断权限</div>
|
|
||||||
</template>
|
|
||||||
<el-space wrap>
|
|
||||||
<el-button v-if="hasAuth('permission:btn:add')" plain type="warning">
|
|
||||||
拥有code:'permission:btn:add' 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button v-if="hasAuth(['permission:btn:edit'])" plain type="primary">
|
|
||||||
拥有code:['permission:btn:edit'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="
|
|
||||||
hasAuth([
|
|
||||||
'permission:btn:add',
|
|
||||||
'permission:btn:edit',
|
|
||||||
'permission:btn:delete'
|
|
||||||
])
|
|
||||||
"
|
|
||||||
plain
|
|
||||||
type="danger"
|
|
||||||
>
|
|
||||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
|
||||||
'permission:btn:delete'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</el-space>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
指令方式判断权限(该方式不能动态修改权限)
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-space wrap>
|
|
||||||
<el-button v-auth="'permission:btn:add'" plain type="warning">
|
|
||||||
拥有code:'permission:btn:add' 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button v-auth="['permission:btn:edit']" plain type="primary">
|
|
||||||
拥有code:['permission:btn:edit'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-auth="[
|
|
||||||
'permission:btn:add',
|
|
||||||
'permission:btn:edit',
|
|
||||||
'permission:btn:delete'
|
|
||||||
]"
|
|
||||||
plain
|
|
||||||
type="danger"
|
|
||||||
>
|
|
||||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
|
||||||
'permission:btn:delete'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</el-space>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { hasPerms } from "@/utils/auth";
|
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
|
||||||
|
|
||||||
const { permissions } = useUserStoreHook();
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PermissionButtonLogin"
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<p class="mb-2">当前拥有的code列表:{{ permissions }}</p>
|
|
||||||
<p v-show="permissions?.[0] === '*:*:*'" class="mb-2">
|
|
||||||
*:*:* 代表拥有全部按钮级别权限
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<el-card shadow="never" class="mb-2">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">组件方式判断权限</div>
|
|
||||||
</template>
|
|
||||||
<el-space wrap>
|
|
||||||
<Perms value="permission:btn:add">
|
|
||||||
<el-button plain type="warning">
|
|
||||||
拥有code:'permission:btn:add' 权限可见
|
|
||||||
</el-button>
|
|
||||||
</Perms>
|
|
||||||
<Perms :value="['permission:btn:edit']">
|
|
||||||
<el-button plain type="primary">
|
|
||||||
拥有code:['permission:btn:edit'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</Perms>
|
|
||||||
<Perms
|
|
||||||
:value="[
|
|
||||||
'permission:btn:add',
|
|
||||||
'permission:btn:edit',
|
|
||||||
'permission:btn:delete'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<el-button plain type="danger">
|
|
||||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
|
||||||
'permission:btn:delete'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</Perms>
|
|
||||||
</el-space>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card shadow="never" class="mb-2">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">函数方式判断权限</div>
|
|
||||||
</template>
|
|
||||||
<el-space wrap>
|
|
||||||
<el-button v-if="hasPerms('permission:btn:add')" plain type="warning">
|
|
||||||
拥有code:'permission:btn:add' 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="hasPerms(['permission:btn:edit'])"
|
|
||||||
plain
|
|
||||||
type="primary"
|
|
||||||
>
|
|
||||||
拥有code:['permission:btn:edit'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-if="
|
|
||||||
hasPerms([
|
|
||||||
'permission:btn:add',
|
|
||||||
'permission:btn:edit',
|
|
||||||
'permission:btn:delete'
|
|
||||||
])
|
|
||||||
"
|
|
||||||
plain
|
|
||||||
type="danger"
|
|
||||||
>
|
|
||||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
|
||||||
'permission:btn:delete'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</el-space>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card shadow="never">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
指令方式判断权限(该方式不能动态修改权限)
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-space wrap>
|
|
||||||
<el-button v-perms="'permission:btn:add'" plain type="warning">
|
|
||||||
拥有code:'permission:btn:add' 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button v-perms="['permission:btn:edit']" plain type="primary">
|
|
||||||
拥有code:['permission:btn:edit'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
v-perms="[
|
|
||||||
'permission:btn:add',
|
|
||||||
'permission:btn:edit',
|
|
||||||
'permission:btn:delete'
|
|
||||||
]"
|
|
||||||
plain
|
|
||||||
type="danger"
|
|
||||||
>
|
|
||||||
拥有code:['permission:btn:add', 'permission:btn:edit',
|
|
||||||
'permission:btn:delete'] 权限可见
|
|
||||||
</el-button>
|
|
||||||
</el-space>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { initRouter } from "@/router/utils";
|
|
||||||
import { storageLocal } from "@pureadmin/utils";
|
|
||||||
import { type CSSProperties, ref, computed } from "vue";
|
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
|
||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: "PermissionPage"
|
|
||||||
});
|
|
||||||
|
|
||||||
const elStyle = computed((): CSSProperties => {
|
|
||||||
return {
|
|
||||||
width: "85vw",
|
|
||||||
justifyContent: "start"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const username = ref(useUserStoreHook()?.username);
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
value: "admin",
|
|
||||||
label: "管理员角色"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "common",
|
|
||||||
label: "普通角色"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function onChange() {
|
|
||||||
useUserStoreHook()
|
|
||||||
.loginByUsername({ username: username.value, password: "admin123" })
|
|
||||||
.then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
storageLocal().removeItem("async-routes");
|
|
||||||
usePermissionStoreHook().clearAllCachePage();
|
|
||||||
initRouter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<p class="mb-2">
|
|
||||||
模拟后台根据不同角色返回对应路由,观察左侧菜单变化(管理员角色可查看系统管理菜单、普通角色不可查看系统管理菜单)
|
|
||||||
</p>
|
|
||||||
<el-card shadow="never" :style="elStyle">
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>当前角色:{{ username }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<el-select v-model="username" class="!w-[160px]" @change="onChange">
|
|
||||||
<el-option
|
|
||||||
v-for="item in options"
|
|
||||||
:key="item.value"
|
|
||||||
:label="item.label"
|
|
||||||
:value="item.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
193
src/views/system/department/components/form.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
import { usePublicHooks } from "../../hooks";
|
||||||
|
import { FormRules } from "element-plus";
|
||||||
|
import { isEmail, isPhone } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemDepartmentForm"
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 表单
|
||||||
|
*/
|
||||||
|
interface FormItemProps {
|
||||||
|
higherDeptOptions: Record<string, unknown>[];
|
||||||
|
id: string;
|
||||||
|
parent_id: string;
|
||||||
|
name: string;
|
||||||
|
principal: string;
|
||||||
|
phone: string | number;
|
||||||
|
email: string;
|
||||||
|
sort: number;
|
||||||
|
status: number;
|
||||||
|
remark: string;
|
||||||
|
}
|
||||||
|
interface FormProps {
|
||||||
|
formInline: FormItemProps;
|
||||||
|
}
|
||||||
|
const formRules = reactive<FormRules>({
|
||||||
|
name: [{ required: true, message: "部门名称为必填项", trigger: "blur" }],
|
||||||
|
phone: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback();
|
||||||
|
} else if (!isPhone(value)) {
|
||||||
|
callback(new Error("请输入正确的手机号码格式"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
// trigger: "click" // 如果想在点击确定按钮时触发这个校验,trigger 设置成 click 即可
|
||||||
|
}
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (value === "") {
|
||||||
|
callback();
|
||||||
|
} else if (!isEmail(value)) {
|
||||||
|
callback(new Error("请输入正确的邮箱格式"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const props = withDefaults(defineProps<FormProps>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
id: "",
|
||||||
|
higherDeptOptions: [],
|
||||||
|
parent_id: "",
|
||||||
|
name: "",
|
||||||
|
principal: "",
|
||||||
|
phone: "",
|
||||||
|
email: "",
|
||||||
|
sort: 0,
|
||||||
|
status: 1,
|
||||||
|
remark: ""
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
const { switchStyle } = usePublicHooks();
|
||||||
|
const newFormInline = ref(props.formInline);
|
||||||
|
|
||||||
|
function getRef() {
|
||||||
|
return ruleFormRef.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ getRef });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-form
|
||||||
|
ref="ruleFormRef"
|
||||||
|
:model="newFormInline"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="82px"
|
||||||
|
>
|
||||||
|
<el-row :gutter="30">
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="上级部门">
|
||||||
|
<el-cascader
|
||||||
|
v-model="newFormInline.parent_id"
|
||||||
|
class="w-full"
|
||||||
|
:options="newFormInline.higherDeptOptions"
|
||||||
|
:props="{
|
||||||
|
value: 'id',
|
||||||
|
label: 'name',
|
||||||
|
emitPath: false,
|
||||||
|
checkStrictly: true
|
||||||
|
}"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请选择上级部门"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span>{{ data.name }}</span>
|
||||||
|
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="部门名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.name"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入部门名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="部门负责人">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.principal"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入部门负责人"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.phone"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.email"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-input-number
|
||||||
|
v-model="newFormInline.sort"
|
||||||
|
class="!w-full"
|
||||||
|
:min="0"
|
||||||
|
:max="9999"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="12" :xs="24" :sm="24">
|
||||||
|
<el-form-item label="部门状态">
|
||||||
|
<el-switch
|
||||||
|
v-model="newFormInline.status"
|
||||||
|
inline-prompt
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="停用"
|
||||||
|
:style="switchStyle"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
|
||||||
|
<re-col>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.remark"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
type="textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
172
src/views/system/department/index.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useDepartment } from "./utils/hook";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import EditPen from "@iconify-icons/ep/edit-pen";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
import { onBeforeRouteUpdate } from "vue-router";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "SystemDepartment"
|
||||||
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
const formRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
columns,
|
||||||
|
dataList,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
openDialog,
|
||||||
|
handleDelete
|
||||||
|
} = useDepartment();
|
||||||
|
onBeforeRouteUpdate((to, from, next) => {
|
||||||
|
onSearch();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="部门名称:" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="请输入部门名称"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态:" prop="status">
|
||||||
|
<el-select
|
||||||
|
v-model="form.status"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
>
|
||||||
|
<el-option label="启用" :value="1" />
|
||||||
|
<el-option label="停用" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
{{ t("buttons:Search") }}
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
{{ t("buttons:Reset") }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<PureTableBar
|
||||||
|
title="部门管理"
|
||||||
|
:columns="columns"
|
||||||
|
:tableRef="tableRef?.getTableRef()"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog()"
|
||||||
|
>
|
||||||
|
{{ t("buttons:Add") }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
adaptive
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
:adaptiveConfig="{ offsetBottom: 45 }"
|
||||||
|
align-whole="center"
|
||||||
|
row-key="id"
|
||||||
|
showOverflowTooltip
|
||||||
|
table-layout="auto"
|
||||||
|
default-expand-all
|
||||||
|
:default-sort="{ prop: 'sort', order: 'ascending' }"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', row)"
|
||||||
|
>
|
||||||
|
{{ t("buttons:Update") }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog('新增', { parentId: row.id } as any)"
|
||||||
|
>
|
||||||
|
{{ t("buttons:Add") }}
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认删除部门名称为${row.name}的这条数据`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
{{ t("buttons:Delete") }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.el-table__inner-wrapper::before) {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
239
src/views/system/department/utils/hook.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import editForm from "../components/form.vue";
|
||||||
|
import { handleTree } from "@/utils/tree";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
import { reactive, ref, onMounted, h } from "vue";
|
||||||
|
import type { FormItemProps } from "../utils/types";
|
||||||
|
import { cloneDeep } from "@pureadmin/utils";
|
||||||
|
import type { DepartmentInfo } from "types/system";
|
||||||
|
import { usePublicHooks } from "../../hooks";
|
||||||
|
import {
|
||||||
|
deleteDepartmentAPI,
|
||||||
|
getDepartmentListAPI,
|
||||||
|
postAddDepartmentAPI,
|
||||||
|
putUpdateDepartmentAPI
|
||||||
|
} from "@/api/system";
|
||||||
|
|
||||||
|
export const useDepartment = () => {
|
||||||
|
const form = reactive({
|
||||||
|
/**部门ID */
|
||||||
|
id: "",
|
||||||
|
/**部门名称 */
|
||||||
|
name: "",
|
||||||
|
/**附属部门ID */
|
||||||
|
parent_id: "",
|
||||||
|
/**部门负责人 */
|
||||||
|
principal: "",
|
||||||
|
/**部门电话 */
|
||||||
|
phone: "",
|
||||||
|
/**部门邮件 */
|
||||||
|
email: "",
|
||||||
|
/**备注 */
|
||||||
|
remark: "",
|
||||||
|
/**排序 */
|
||||||
|
sort: 0,
|
||||||
|
/**状态 */
|
||||||
|
status: ""
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 表单Ref
|
||||||
|
*/
|
||||||
|
const formRef = ref();
|
||||||
|
/**
|
||||||
|
* 数据列表
|
||||||
|
*/
|
||||||
|
const dataList = ref<DepartmentInfo[]>([]);
|
||||||
|
/**
|
||||||
|
* 加载状态
|
||||||
|
*/
|
||||||
|
const loading = ref(true);
|
||||||
|
/**
|
||||||
|
* 标签样式
|
||||||
|
*/
|
||||||
|
const { tagStyle, formatHigherDeptOptions } = usePublicHooks();
|
||||||
|
/**
|
||||||
|
* 表格列设置
|
||||||
|
*/
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "部门名称",
|
||||||
|
prop: "name",
|
||||||
|
width: 180,
|
||||||
|
align: "left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "排序",
|
||||||
|
prop: "sort",
|
||||||
|
minWidth: 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "状态",
|
||||||
|
prop: "status",
|
||||||
|
minWidth: 100,
|
||||||
|
cellRenderer: ({ row, props }) => (
|
||||||
|
<el-tag size={props.size} style={tagStyle.value(row.status)}>
|
||||||
|
{row.status === 1 ? "启用" : "停用"}
|
||||||
|
</el-tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "创建时间",
|
||||||
|
minWidth: 200,
|
||||||
|
prop: "create_time",
|
||||||
|
formatter: ({ create_time }) =>
|
||||||
|
dayjs(create_time).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "修改时间",
|
||||||
|
minWidth: 200,
|
||||||
|
prop: "update_time",
|
||||||
|
formatter: ({ update_time }) =>
|
||||||
|
dayjs(update_time).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "备注",
|
||||||
|
prop: "remark",
|
||||||
|
minWidth: 320
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 230,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* 初次查询
|
||||||
|
*/
|
||||||
|
const onSearch = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getDepartmentListAPI({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 9999,
|
||||||
|
...form
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
dataList.value = handleTree(res.data.result, "id", "parent_id"); // 处理成树结构
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = formEl => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDialog = (title = "新增", row?: FormItemProps) => {
|
||||||
|
addDialog({
|
||||||
|
title: `${title}部门`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
higherDeptOptions: formatHigherDeptOptions(cloneDeep(dataList.value)),
|
||||||
|
id: row?.id ?? "",
|
||||||
|
parent_id: row?.parent_id ?? "",
|
||||||
|
name: row?.name ?? "",
|
||||||
|
principal: row?.principal ?? "",
|
||||||
|
phone: row?.phone ?? "",
|
||||||
|
email: row?.email ?? "",
|
||||||
|
sort: row?.sort ?? 0,
|
||||||
|
status: row?.status ?? 1,
|
||||||
|
remark: row?.remark ?? ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: "40%",
|
||||||
|
draggable: true,
|
||||||
|
fullscreenIcon: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () =>
|
||||||
|
h(editForm, {
|
||||||
|
formInline: {
|
||||||
|
higherDeptOptions: formatHigherDeptOptions(
|
||||||
|
cloneDeep(dataList.value)
|
||||||
|
),
|
||||||
|
id: row?.id ?? "",
|
||||||
|
parent_id: row?.parent_id ?? "",
|
||||||
|
name: row?.name ?? "",
|
||||||
|
principal: row?.principal ?? "",
|
||||||
|
phone: row?.phone ?? "",
|
||||||
|
email: row?.email ?? "",
|
||||||
|
sort: row?.sort ?? 0,
|
||||||
|
status: row?.status ?? 1,
|
||||||
|
remark: row?.remark ?? ""
|
||||||
|
},
|
||||||
|
ref: formRef
|
||||||
|
}),
|
||||||
|
beforeSure: (done, { options }) => {
|
||||||
|
const FormRef = formRef.value.getRef();
|
||||||
|
const curData = options.props.formInline as FormItemProps;
|
||||||
|
function chores() {
|
||||||
|
message(`您${title}了部门名称为${curData.name}的这条数据`, {
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
done(); // 关闭弹框
|
||||||
|
onSearch(); // 刷新表格数据
|
||||||
|
}
|
||||||
|
FormRef.validate(async (valid: any) => {
|
||||||
|
if (valid) {
|
||||||
|
// 表单规则校验通过
|
||||||
|
if (title === "新增") {
|
||||||
|
// 实际开发先调用新增接口,再进行下面操作
|
||||||
|
const res = await postAddDepartmentAPI(curData);
|
||||||
|
if (res.success) {
|
||||||
|
chores();
|
||||||
|
} else {
|
||||||
|
message(`添加失败!`, {
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 实际开发先调用修改接口,再进行下面操作
|
||||||
|
const res = await putUpdateDepartmentAPI(curData, row.id);
|
||||||
|
if (res.success) {
|
||||||
|
chores();
|
||||||
|
} else {
|
||||||
|
message(`修改失败!`, {
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 删除部门
|
||||||
|
*/
|
||||||
|
const handleDelete = async (row: DepartmentInfo) => {
|
||||||
|
const res = await deleteDepartmentAPI(row.id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
message(`您删除了部门:${row.name}及其附属部门`, { type: "success" });
|
||||||
|
onSearch();
|
||||||
|
} else {
|
||||||
|
message(`删除失败!`, {
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
loading,
|
||||||
|
dataList,
|
||||||
|
columns,
|
||||||
|
onSearch,
|
||||||
|
resetForm,
|
||||||
|
openDialog,
|
||||||
|
handleDelete
|
||||||
|
};
|
||||||
|
};
|
||||||
53
src/views/system/hooks.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// 抽离可公用的工具函数等用于系统管理页面逻辑
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useDark } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
export function usePublicHooks() {
|
||||||
|
const { isDark } = useDark();
|
||||||
|
|
||||||
|
const switchStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
"--el-switch-on-color": "#6abe39",
|
||||||
|
"--el-switch-off-color": "#e84749"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const tagStyle = computed(() => {
|
||||||
|
return (status: number) => {
|
||||||
|
return status === 1
|
||||||
|
? {
|
||||||
|
"--el-tag-text-color": isDark.value ? "#6abe39" : "#389e0d",
|
||||||
|
"--el-tag-bg-color": isDark.value ? "#172412" : "#f6ffed",
|
||||||
|
"--el-tag-border-color": isDark.value ? "#274a17" : "#b7eb8f"
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
"--el-tag-text-color": isDark.value ? "#e84749" : "#cf1322",
|
||||||
|
"--el-tag-bg-color": isDark.value ? "#2b1316" : "#fff1f0",
|
||||||
|
"--el-tag-border-color": isDark.value ? "#58191c" : "#ffa39e"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatHigherDeptOptions = treeList => {
|
||||||
|
// 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
|
||||||
|
if (!treeList || !treeList.length) return;
|
||||||
|
const newTreeList = [];
|
||||||
|
for (let i = 0; i < treeList.length; i++) {
|
||||||
|
treeList[i].disabled = treeList[i].status === 0 ? true : false;
|
||||||
|
formatHigherDeptOptions(treeList[i].children);
|
||||||
|
newTreeList.push(treeList[i]);
|
||||||
|
}
|
||||||
|
return newTreeList;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
/** 当前网页是否为`dark`模式 */
|
||||||
|
isDark,
|
||||||
|
/** 表现更鲜明的`el-switch`组件 */
|
||||||
|
switchStyle,
|
||||||
|
/** 表现更鲜明的`el-tag`组件 */
|
||||||
|
tagStyle,
|
||||||
|
/** 获取上级部门级联选择器的数据结构*/
|
||||||
|
formatHigherDeptOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
93
src/views/system/i18n/components/form.vue
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<el-form ref="ruleFormRef" :model="newFormInline" label-width="82px">
|
||||||
|
<el-row :gutter="30">
|
||||||
|
<re-col :value="24" :xm="24" :sm="24">
|
||||||
|
<el-form-item label="国际化key" prop="key">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.key"
|
||||||
|
placeholder="请输入国际化关键词~"
|
||||||
|
clearable
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="24" :xm="24" :sm="24">
|
||||||
|
<el-form-item label="语言类型" prop="locale_id">
|
||||||
|
<el-select
|
||||||
|
v-model="newFormInline.locale_id"
|
||||||
|
placeholder="请选择语言类型~"
|
||||||
|
clearable
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in localeList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="24" :xm="24" :sm="24">
|
||||||
|
<el-form-item label="国际化值" prop="translation">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.translation"
|
||||||
|
placeholder="请输入国际化值~"
|
||||||
|
clearable
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
import { LanguageInfo } from "types/system";
|
||||||
|
import { getLocaleListAPI } from "@/api/i18n";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
interface PropsInfo {
|
||||||
|
title: string;
|
||||||
|
key: string;
|
||||||
|
locale_id: string;
|
||||||
|
translation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProsData = {
|
||||||
|
formInline: PropsInfo;
|
||||||
|
};
|
||||||
|
const props = withDefaults(defineProps<ProsData>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
title: "新增",
|
||||||
|
key: "",
|
||||||
|
locale_id: "",
|
||||||
|
translation: ""
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const newFormInline = ref<PropsInfo>(props.formInline);
|
||||||
|
defineExpose({ newFormInline });
|
||||||
|
/**语言类型 */
|
||||||
|
const localeList = ref<LanguageInfo[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言类型
|
||||||
|
*/
|
||||||
|
const getLocaleList = async () => {
|
||||||
|
const res = await getLocaleListAPI({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 9999
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
localeList.value = res.data.result;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
onMounted(async () => {
|
||||||
|
await getLocaleList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
287
src/views/system/i18n/hook.tsx
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import editForm from "./components/form.vue";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { type Ref, ref, reactive, onMounted, h } from "vue";
|
||||||
|
import type { LanguageInfo, TranslationInfo } from "types/system";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
import {
|
||||||
|
deleteI18nAPI,
|
||||||
|
getI18nListAPI,
|
||||||
|
getLocaleListAPI,
|
||||||
|
postAddI18nAPI,
|
||||||
|
putUpdateI18nAPI
|
||||||
|
} from "@/api/i18n";
|
||||||
|
|
||||||
|
export const useI18n = (tableRef: Ref) => {
|
||||||
|
/**
|
||||||
|
* 查询表单
|
||||||
|
*/
|
||||||
|
const form = reactive({
|
||||||
|
key: "",
|
||||||
|
locale_id: "",
|
||||||
|
translation: ""
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 表单Ref
|
||||||
|
*/
|
||||||
|
const formRef = ref(null);
|
||||||
|
/**
|
||||||
|
* 数据列表
|
||||||
|
*/
|
||||||
|
const dataList = ref<TranslationInfo[]>([]);
|
||||||
|
/**
|
||||||
|
* 加载状态
|
||||||
|
*/
|
||||||
|
const loading = ref(true);
|
||||||
|
/**
|
||||||
|
* 已选数量
|
||||||
|
*/
|
||||||
|
const selectedNum = ref<number>(0);
|
||||||
|
/**
|
||||||
|
* 分页参数
|
||||||
|
*/
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 表格列设置
|
||||||
|
*/
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||||
|
type: "selection",
|
||||||
|
fixed: "left",
|
||||||
|
reserveSelection: true // 数据刷新后保留选项
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "国际化key",
|
||||||
|
prop: "key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "国际化值",
|
||||||
|
prop: "translation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "语言编码",
|
||||||
|
prop: "locale_code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "语言名称",
|
||||||
|
prop: "locale_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "创建时间",
|
||||||
|
prop: "create_time",
|
||||||
|
formatter: ({ create_time }) =>
|
||||||
|
dayjs(create_time).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 220,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初次查询
|
||||||
|
*/
|
||||||
|
const onSearch = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getI18nListAPI({
|
||||||
|
page: pagination.currentPage,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
key: form.key,
|
||||||
|
locale_id: form.locale_id,
|
||||||
|
translation: form.translation
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
dataList.value = res.data.result;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
pagination.currentPage = res.data.page;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 重置表单
|
||||||
|
* @param formEl 表单ref
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const resetForm = (formEl: any) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 处理删除
|
||||||
|
* @param row
|
||||||
|
*/
|
||||||
|
const handleDelete = async (row: TranslationInfo) => {
|
||||||
|
const res = await deleteI18nAPI(row.id);
|
||||||
|
if (res.success) {
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 处理每页数量变化
|
||||||
|
*/
|
||||||
|
const handleSizeChange = async (val: number) => {
|
||||||
|
const res = await getI18nListAPI({
|
||||||
|
page: pagination.currentPage,
|
||||||
|
pageSize: val,
|
||||||
|
key: form.key,
|
||||||
|
locale_id: form.locale_id,
|
||||||
|
translation: form.translation
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
dataList.value = res.data.result;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
pagination.currentPage = res.data.page;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理页码变化
|
||||||
|
* @param val
|
||||||
|
*/
|
||||||
|
const handleCurrentChange = async (val: number) => {
|
||||||
|
const res = await getI18nListAPI({
|
||||||
|
page: val,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
key: form.key,
|
||||||
|
locale_id: form.locale_id,
|
||||||
|
translation: form.translation
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
dataList.value = res.data.result;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
pagination.currentPage = res.data.page;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||||
|
const handleSelectionChange = async (val: any) => {
|
||||||
|
selectedNum.value = val.length;
|
||||||
|
// 重置表格高度
|
||||||
|
tableRef.value.setAdaptive();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消选择 */
|
||||||
|
const onSelectionCancel = async () => {
|
||||||
|
selectedNum.value = 0;
|
||||||
|
// 用于多选表格,清空用户的选择
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDialog = async (title = "新增", row?: TranslationInfo) => {
|
||||||
|
addDialog({
|
||||||
|
title: `${title}国际化项`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
title: title,
|
||||||
|
key: row?.key ?? "",
|
||||||
|
locale_id: row?.locale_id ?? "",
|
||||||
|
translation: row?.translation ?? ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: "45%",
|
||||||
|
draggable: true,
|
||||||
|
fullscreenIcon: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () =>
|
||||||
|
h(editForm, {
|
||||||
|
formInline: {
|
||||||
|
title: title,
|
||||||
|
key: row?.key ?? "",
|
||||||
|
locale_id: row?.locale_id ?? "",
|
||||||
|
translation: row?.translation ?? ""
|
||||||
|
},
|
||||||
|
ref: formRef
|
||||||
|
}),
|
||||||
|
beforeSure: async (done, {}) => {
|
||||||
|
const FormData = formRef.value.newFormInline;
|
||||||
|
let form = {
|
||||||
|
key: FormData.key ?? "",
|
||||||
|
locale_id: FormData.locale_id ?? "",
|
||||||
|
translation: FormData.translation ?? ""
|
||||||
|
};
|
||||||
|
if (title === "新增") {
|
||||||
|
const res = await postAddI18nAPI(form);
|
||||||
|
if (res.success) {
|
||||||
|
done();
|
||||||
|
await onSearch();
|
||||||
|
}
|
||||||
|
message(res.msg, { type: res.success ? "success" : "error" });
|
||||||
|
} else {
|
||||||
|
const res = await putUpdateI18nAPI(form, row.id);
|
||||||
|
if (res.success) {
|
||||||
|
done();
|
||||||
|
await onSearch();
|
||||||
|
}
|
||||||
|
message(res.msg, { type: res.success ? "success" : "error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**语言类型 */
|
||||||
|
const localeList = ref<LanguageInfo[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言类型
|
||||||
|
*/
|
||||||
|
const getLocaleList = async () => {
|
||||||
|
const res = await getLocaleListAPI({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 9999
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
localeList.value = res.data.result;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 页面加载执行
|
||||||
|
*/
|
||||||
|
onMounted(async () => {
|
||||||
|
await onSearch();
|
||||||
|
await getLocaleList();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
formRef,
|
||||||
|
dataList,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
columns,
|
||||||
|
selectedNum,
|
||||||
|
localeList,
|
||||||
|
onSearch,
|
||||||
|
openDialog,
|
||||||
|
resetForm,
|
||||||
|
handleDelete,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange,
|
||||||
|
onSelectionCancel,
|
||||||
|
getLocaleList
|
||||||
|
};
|
||||||
|
};
|
||||||
198
src/views/system/i18n/index.vue
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="国际化key" prop="key">
|
||||||
|
<el-input
|
||||||
|
v-model="form.key"
|
||||||
|
placeholder="请输入国际化关键词~"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言类型" prop="locale_id">
|
||||||
|
<el-select
|
||||||
|
v-model="form.locale_id"
|
||||||
|
placeholder="请选择语言类型~"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in localeList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="国际化值" prop="translation">
|
||||||
|
<el-input
|
||||||
|
v-model="form.translation"
|
||||||
|
placeholder="请输入国际化值~"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<PureTableBar title="国际化管理" :columns="columns" @refresh="onSearch">
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog('新增')"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<div
|
||||||
|
v-if="selectedNum > 0"
|
||||||
|
v-motion-fade
|
||||||
|
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<span
|
||||||
|
style="font-size: var(--el-font-size-base)"
|
||||||
|
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||||
|
>
|
||||||
|
已选 {{ selectedNum }} 项
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" text @click="onSelectionCancel">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-popconfirm title="是否确认删除?">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" text class="mr-1"> 批量删除 </el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
adaptive
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
:adaptiveConfig="{ offsetBottom: 45 }"
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', row)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认删除国际化key为 ${row.key} 的这条数据`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: "I18nIndex"
|
||||||
|
});
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useI18n } from "./hook";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import EditPen from "@iconify-icons/ep/edit-pen";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
/**
|
||||||
|
* 表格Ref
|
||||||
|
*/
|
||||||
|
const tableRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formRef,
|
||||||
|
dataList,
|
||||||
|
localeList,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
columns,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
openDialog,
|
||||||
|
resetForm,
|
||||||
|
handleDelete,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange,
|
||||||
|
onSelectionCancel
|
||||||
|
} = useI18n(tableRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/views/system/language/components/form.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<el-form ref="ruleFormRef" :model="newFormInline" label-width="82px">
|
||||||
|
<el-row :gutter="30">
|
||||||
|
<re-col :value="24" :xm="24" :sm="24">
|
||||||
|
<el-form-item label="语言编码" prop="code">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.code"
|
||||||
|
placeholder="请输入语言编码~"
|
||||||
|
clearable
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
<re-col :value="24" :xm="24" :sm="24">
|
||||||
|
<el-form-item label="语言名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="newFormInline.name"
|
||||||
|
placeholder="请输入语言名称~"
|
||||||
|
clearable
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</re-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import ReCol from "@/components/ReCol";
|
||||||
|
const ruleFormRef = ref();
|
||||||
|
interface PropsInfo {
|
||||||
|
title: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProsData = {
|
||||||
|
formInline: PropsInfo;
|
||||||
|
};
|
||||||
|
const props = withDefaults(defineProps<ProsData>(), {
|
||||||
|
formInline: () => ({
|
||||||
|
title: "新增",
|
||||||
|
code: "",
|
||||||
|
name: ""
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const newFormInline = ref<PropsInfo>(props.formInline);
|
||||||
|
defineExpose({ newFormInline });
|
||||||
|
</script>
|
||||||
281
src/views/system/language/hook.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
|
import editForm from "./components/form.vue";
|
||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { type Ref, ref, reactive, onMounted, h } from "vue";
|
||||||
|
import type { LanguageInfo, TranslationInfo } from "types/system";
|
||||||
|
import type { PaginationProps } from "@pureadmin/table";
|
||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
import {
|
||||||
|
getLocaleListAPI,
|
||||||
|
deleteLocaleAPI,
|
||||||
|
postAddLocaleAPI,
|
||||||
|
putUpdateLocaleAPI,
|
||||||
|
getI18nHandleListAPI
|
||||||
|
} from "@/api/i18n";
|
||||||
|
|
||||||
|
import jsyaml from "js-yaml";
|
||||||
|
|
||||||
|
export const useLocale = (tableRef: Ref) => {
|
||||||
|
/**
|
||||||
|
* 查询表单
|
||||||
|
*/
|
||||||
|
const form = reactive({
|
||||||
|
name: "",
|
||||||
|
code: ""
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 表单Ref
|
||||||
|
*/
|
||||||
|
const formRef = ref(null);
|
||||||
|
/**
|
||||||
|
* 数据列表
|
||||||
|
*/
|
||||||
|
const dataList = ref<LanguageInfo[]>([]);
|
||||||
|
/**
|
||||||
|
* 加载状态
|
||||||
|
*/
|
||||||
|
const loading = ref(true);
|
||||||
|
/**
|
||||||
|
* 已选数量
|
||||||
|
*/
|
||||||
|
const selectedNum = ref<number>(0);
|
||||||
|
/**
|
||||||
|
* 分页参数
|
||||||
|
*/
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: 0,
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* 表格列设置
|
||||||
|
*/
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "勾选列", // 如果需要表格多选,此处label必须设置
|
||||||
|
type: "selection",
|
||||||
|
fixed: "left",
|
||||||
|
reserveSelection: true // 数据刷新后保留选项
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "语言编码",
|
||||||
|
prop: "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "语言名称",
|
||||||
|
prop: "name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "创建时间",
|
||||||
|
prop: "create_time",
|
||||||
|
formatter: ({ create_time }) =>
|
||||||
|
dayjs(create_time).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "操作",
|
||||||
|
fixed: "right",
|
||||||
|
width: 200,
|
||||||
|
slot: "operation"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初次查询
|
||||||
|
*/
|
||||||
|
const onSearch = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getLocaleListAPI({
|
||||||
|
page: pagination.currentPage,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
name: form.name,
|
||||||
|
code: form.code
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
dataList.value = res.data.result;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
pagination.currentPage = res.data.page;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 重置表单
|
||||||
|
* @param formEl 表单ref
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const resetForm = (formEl: any) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.resetFields();
|
||||||
|
onSearch();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 处理删除
|
||||||
|
* @param row
|
||||||
|
*/
|
||||||
|
const handleDelete = async (row: TranslationInfo) => {
|
||||||
|
const res = await deleteLocaleAPI(row.id);
|
||||||
|
if (res.success) {
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 处理每页数量变化
|
||||||
|
*/
|
||||||
|
const handleSizeChange = async (val: number) => {
|
||||||
|
const res = await getLocaleListAPI({
|
||||||
|
page: pagination.currentPage,
|
||||||
|
pageSize: val,
|
||||||
|
name: form.name,
|
||||||
|
code: form.code
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
dataList.value = res.data.result;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
pagination.currentPage = res.data.page;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理页码变化
|
||||||
|
* @param val
|
||||||
|
*/
|
||||||
|
const handleCurrentChange = async (val: number) => {
|
||||||
|
const res = await getLocaleListAPI({
|
||||||
|
page: val,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
name: form.name,
|
||||||
|
code: form.code
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
dataList.value = res.data.result;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
pagination.currentPage = res.data.page;
|
||||||
|
}
|
||||||
|
message(res.msg, {
|
||||||
|
type: res.success ? "success" : "error"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||||
|
const handleSelectionChange = async (val: any) => {
|
||||||
|
selectedNum.value = val.length;
|
||||||
|
// 重置表格高度
|
||||||
|
tableRef.value.setAdaptive();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消选择 */
|
||||||
|
const onSelectionCancel = async () => {
|
||||||
|
selectedNum.value = 0;
|
||||||
|
// 用于多选表格,清空用户的选择
|
||||||
|
tableRef.value.getTableRef().clearSelection();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDialog = async (title = "新增", row?: LanguageInfo) => {
|
||||||
|
addDialog({
|
||||||
|
title: `${title}国际化项`,
|
||||||
|
props: {
|
||||||
|
formInline: {
|
||||||
|
title: title,
|
||||||
|
name: row?.name ?? "",
|
||||||
|
code: row?.code ?? ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: "45%",
|
||||||
|
draggable: true,
|
||||||
|
fullscreenIcon: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () =>
|
||||||
|
h(editForm, {
|
||||||
|
formInline: {
|
||||||
|
title: title,
|
||||||
|
name: row?.name ?? "",
|
||||||
|
code: row?.code ?? ""
|
||||||
|
},
|
||||||
|
ref: formRef
|
||||||
|
}),
|
||||||
|
beforeSure: async (done, {}) => {
|
||||||
|
const FormData = formRef.value.newFormInline;
|
||||||
|
let form = {
|
||||||
|
name: FormData.name ?? "",
|
||||||
|
code: FormData.code ?? ""
|
||||||
|
};
|
||||||
|
if (title === "新增") {
|
||||||
|
const res = await postAddLocaleAPI(form);
|
||||||
|
if (res.success) {
|
||||||
|
done();
|
||||||
|
await onSearch();
|
||||||
|
}
|
||||||
|
message(res.msg, { type: res.success ? "success" : "error" });
|
||||||
|
} else {
|
||||||
|
const res = await putUpdateLocaleAPI(form, row.id);
|
||||||
|
if (res.success) {
|
||||||
|
done();
|
||||||
|
await onSearch();
|
||||||
|
}
|
||||||
|
message(res.msg, { type: res.success ? "success" : "error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出 YAML 文件
|
||||||
|
*/
|
||||||
|
const export_to_yaml = async (row: LanguageInfo) => {
|
||||||
|
const res = await getI18nHandleListAPI(row.id); // 调用 API 获取数据
|
||||||
|
if (res.success) {
|
||||||
|
// 将 JSON 转换为 YAML
|
||||||
|
const yamlString = jsyaml.dump(res.data.data);
|
||||||
|
|
||||||
|
// 创建 Blob 对象
|
||||||
|
const blob = new Blob([yamlString], { type: "text/yaml" });
|
||||||
|
|
||||||
|
// 生成下载链接
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// 创建 <a> 元素并触发下载
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = `${row.code}.yaml`; // 设置下载文件名
|
||||||
|
document.body.appendChild(link); // 将 <a> 元素添加到 DOM 中
|
||||||
|
link.click(); // 模拟点击下载
|
||||||
|
|
||||||
|
// 清理 URL 对象
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(link); // 移除 <a> 元素
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 页面加载执行
|
||||||
|
*/
|
||||||
|
onMounted(async () => {
|
||||||
|
await onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
formRef,
|
||||||
|
dataList,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
columns,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
openDialog,
|
||||||
|
resetForm,
|
||||||
|
export_to_yaml,
|
||||||
|
handleDelete,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange,
|
||||||
|
onSelectionCancel
|
||||||
|
};
|
||||||
|
};
|
||||||
200
src/views/system/language/index.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:inline="true"
|
||||||
|
:model="form"
|
||||||
|
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
|
||||||
|
>
|
||||||
|
<el-form-item label="语言编码" prop="code">
|
||||||
|
<el-input
|
||||||
|
v-model="form.code"
|
||||||
|
placeholder="请输入语言编码~"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
placeholder="请输入语言名称~"
|
||||||
|
clearable
|
||||||
|
class="!w-[180px]"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon('ri:search-line')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="onSearch"
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<PureTableBar title="语言类型管理" :columns="columns" @refresh="onSearch">
|
||||||
|
<template #buttons>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="useRenderIcon(AddFill)"
|
||||||
|
@click="openDialog('新增')"
|
||||||
|
>
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<div
|
||||||
|
v-if="selectedNum > 0"
|
||||||
|
v-motion-fade
|
||||||
|
class="bg-[var(--el-fill-color-light)] w-full h-[46px] mb-2 pl-4 flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-auto">
|
||||||
|
<span
|
||||||
|
style="font-size: var(--el-font-size-base)"
|
||||||
|
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||||
|
>
|
||||||
|
已选 {{ selectedNum }} 项
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" text @click="onSelectionCancel">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-popconfirm title="是否确认删除?">
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="danger" text class="mr-1"> 批量删除 </el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</div>
|
||||||
|
<pure-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
adaptive
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
:adaptiveConfig="{ offsetBottom: 45 }"
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="dataList"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-fill-color-light)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #operation="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(EditPen)"
|
||||||
|
@click="openDialog('修改', row)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认导出语言名称为 ${row.name} 的这条数据为yaml文件`"
|
||||||
|
@confirm="export_to_yaml(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Download)"
|
||||||
|
>
|
||||||
|
导出
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
<el-popconfirm
|
||||||
|
:title="`是否确认删除语言名称为 ${row.name} 的这条数据`"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
:size="size"
|
||||||
|
:icon="useRenderIcon(Delete)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: "LocaleIndex"
|
||||||
|
});
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useLocale } from "./hook";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import Delete from "@iconify-icons/ep/delete";
|
||||||
|
import EditPen from "@iconify-icons/ep/edit-pen";
|
||||||
|
import Refresh from "@iconify-icons/ep/refresh";
|
||||||
|
import AddFill from "@iconify-icons/ri/add-circle-line";
|
||||||
|
import Download from "@iconify-icons/ri/file-download-line";
|
||||||
|
/**
|
||||||
|
* 表格Ref
|
||||||
|
*/
|
||||||
|
const tableRef = ref();
|
||||||
|
const {
|
||||||
|
form,
|
||||||
|
formRef,
|
||||||
|
dataList,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
columns,
|
||||||
|
selectedNum,
|
||||||
|
onSearch,
|
||||||
|
openDialog,
|
||||||
|
resetForm,
|
||||||
|
export_to_yaml,
|
||||||
|
handleDelete,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
handleSelectionChange,
|
||||||
|
onSelectionCancel
|
||||||
|
} = useLocale(tableRef);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.el-dropdown-menu__item i) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 24px 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,5 +5,5 @@ defineOptions({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Pure-Admin-Thin(国际化版本)</h1>
|
<h1>首页</h1>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
29
types/file.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/** 文件信息类型 */
|
||||||
|
export type FileInfo = {
|
||||||
|
/** 文件ID */
|
||||||
|
id: string;
|
||||||
|
/** 文件名称 */
|
||||||
|
name: string;
|
||||||
|
/** 文件大小(单位:字节) */
|
||||||
|
size: number;
|
||||||
|
/** 文件类型(MIME 类型) */
|
||||||
|
file_type: string;
|
||||||
|
/** 文件绝对路径 */
|
||||||
|
absolute_path: string;
|
||||||
|
/** 文件相对路径 */
|
||||||
|
relative_path: string;
|
||||||
|
/** 上传者ID */
|
||||||
|
uploader_id: string;
|
||||||
|
/** 上传者用户名 */
|
||||||
|
uploader_username: string;
|
||||||
|
/** 上传者昵称 */
|
||||||
|
uploader_nickname: string;
|
||||||
|
/** 上传者部门ID */
|
||||||
|
uploader_department_id: string;
|
||||||
|
/** 上传者部门名称 */
|
||||||
|
uploader_department_name: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
update_time: string;
|
||||||
|
/** 创建时间 */
|
||||||
|
create_time: string;
|
||||||
|
};
|
||||||
17
types/global.d.ts
vendored
@@ -197,7 +197,24 @@ declare global {
|
|||||||
code: number;
|
code: number;
|
||||||
/**操作信息 */
|
/**操作信息 */
|
||||||
msg: string;
|
msg: string;
|
||||||
|
/**执行时间 */
|
||||||
|
time: string;
|
||||||
|
/**执行状态 */
|
||||||
|
success: boolean;
|
||||||
/**响应结果 */
|
/**响应结果 */
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 查询列表响应
|
||||||
|
*/
|
||||||
|
interface QueryListResult<T> {
|
||||||
|
/**当前页 */
|
||||||
|
page: number;
|
||||||
|
/**每页数量 */
|
||||||
|
pageSize: number;
|
||||||
|
/**总页数 */
|
||||||
|
total: number;
|
||||||
|
/**结果 */
|
||||||
|
result: Array<T>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
types/router.d.ts
vendored
@@ -22,10 +22,8 @@ declare global {
|
|||||||
showLink?: boolean;
|
showLink?: boolean;
|
||||||
/** 是否显示父级菜单 `可选` */
|
/** 是否显示父级菜单 `可选` */
|
||||||
showParent?: boolean;
|
showParent?: boolean;
|
||||||
/** 页面级别权限设置 `可选` */
|
|
||||||
roles?: Array<string>;
|
|
||||||
/** 按钮级别权限设置 `可选` */
|
/** 按钮级别权限设置 `可选` */
|
||||||
auths?: Array<string>;
|
permissions?: Array<string>;
|
||||||
/** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
|
/** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
|
||||||
keepAlive?: boolean;
|
keepAlive?: boolean;
|
||||||
/** 内嵌的`iframe`链接 `可选` */
|
/** 内嵌的`iframe`链接 `可选` */
|
||||||
|
|||||||
65
types/system.d.ts
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/** 语言信息类型 */
|
||||||
|
export type LanguageInfo = {
|
||||||
|
/** 语言ID */
|
||||||
|
id: string;
|
||||||
|
/** 语言代码 */
|
||||||
|
code: string;
|
||||||
|
/** 语言名称 */
|
||||||
|
name: string;
|
||||||
|
/** 创建时间 */
|
||||||
|
create_time: string;
|
||||||
|
/** 更新时间 */
|
||||||
|
update_time: string;
|
||||||
|
/** 创建人 */
|
||||||
|
create_by: string;
|
||||||
|
/** 更新人 */
|
||||||
|
update_by: string;
|
||||||
|
};
|
||||||
|
/** 翻译信息类型 */
|
||||||
|
export type TranslationInfo = {
|
||||||
|
/** 翻译记录ID */
|
||||||
|
id: string;
|
||||||
|
/** 键值 */
|
||||||
|
key: string;
|
||||||
|
/** 翻译内容 */
|
||||||
|
translation: string;
|
||||||
|
/** 语言ID */
|
||||||
|
locale_id: string;
|
||||||
|
/** 语言代码 */
|
||||||
|
locale_code: string;
|
||||||
|
/** 语言名称 */
|
||||||
|
locale_name: string;
|
||||||
|
/** 创建时间 */
|
||||||
|
create_time: string;
|
||||||
|
/** 修改时间 */
|
||||||
|
update_time: string;
|
||||||
|
/** 创建人 */
|
||||||
|
create_by: string;
|
||||||
|
/** 修改人 */
|
||||||
|
update_by: string;
|
||||||
|
};
|
||||||
|
/**部门信息类型 */
|
||||||
|
export type DepartmentInfo = {
|
||||||
|
/**部门ID */
|
||||||
|
id: string;
|
||||||
|
/**状态 */
|
||||||
|
status: number;
|
||||||
|
/**创建时间 */
|
||||||
|
create_time: string;
|
||||||
|
/**修改时间 */
|
||||||
|
update_time: string;
|
||||||
|
/**部门名称 */
|
||||||
|
name: string;
|
||||||
|
/**父部门ID */
|
||||||
|
parentId: string;
|
||||||
|
/**排序 */
|
||||||
|
sort: number;
|
||||||
|
/**部门电话 */
|
||||||
|
phone: string;
|
||||||
|
/**负责人 */
|
||||||
|
principal: string;
|
||||||
|
/**部门邮件 */
|
||||||
|
email: string;
|
||||||
|
/**备注信息 */
|
||||||
|
remark: string;
|
||||||
|
};
|
||||||
27
types/user.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**用户信息类型 */
|
||||||
|
export type UserInfo = {
|
||||||
|
/**用户名 */
|
||||||
|
username: string;
|
||||||
|
/**用户昵称 */
|
||||||
|
nickname: string;
|
||||||
|
/**用户状态 */
|
||||||
|
status: number;
|
||||||
|
/**用户性别 */
|
||||||
|
gender: number;
|
||||||
|
/**用户头像 */
|
||||||
|
avatar: string;
|
||||||
|
/**用户数据库ID */
|
||||||
|
id: string;
|
||||||
|
/**用户邮箱 */
|
||||||
|
email: string;
|
||||||
|
/**用户手机号 */
|
||||||
|
phone: string;
|
||||||
|
/**创建时间 */
|
||||||
|
create_time: string;
|
||||||
|
/**修改时间 */
|
||||||
|
update_time: string;
|
||||||
|
/** 角色列表 */
|
||||||
|
roles: string[];
|
||||||
|
/** 权限列表 */
|
||||||
|
permissions: string[];
|
||||||
|
};
|
||||||
@@ -27,7 +27,7 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
|
|||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
// 这里填写后端地址
|
// 这里填写后端地址
|
||||||
target: "http://localhost:8080",
|
target: "http://localhost:9090",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: path => path.replace(/^\/api/, "")
|
rewrite: path => path.replace(/^\/api/, "")
|
||||||
}
|
}
|
||||||
|
|||||||