兰宝车间质量管理系统-前端
LiuHao
2023-04-02 251d2411f235e23209d57173857e05b637729ce8
refactor ts
已添加109个文件
已重命名2个文件
已删除64个文件
已修改89个文件
28447 ■■■■ 文件已修改
.env.development 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
.eslintignore 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.eslintrc-auto-import.json 281 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.eslintrc.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.prettierignore 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.prettierrc.cjs 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md 72 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Vite/plugins/auto-import.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Vite/plugins/components.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Vite/plugins/icons.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Vite/plugins/index.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Vite/plugins/svg-icon.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Vite/plugins/unocss.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
commitlint.config.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
html/ie.html 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
index.html 380 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/animate.ts 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/demo.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/demo.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/tree.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/tree.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/demo/types.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.js 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/login.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/menu.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/menu.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/cache.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/cache/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/cache/types.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/loginInfo/index.ts 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/loginInfo/types.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/logininfor.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/online.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/online/index.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/online/types.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/operlog.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/operlog/index.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/monitor/operlog/types.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/config.js 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/config/index.ts 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/config/types.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dept.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dept/index.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dept/types.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dict/data.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dict/data/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dict/data/types.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dict/type.js 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dict/type/index.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/dict/type/types.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/menu.js 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/menu/index.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/menu/types.ts 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/notice.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/notice/index.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/notice/types.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/oss.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/oss/index.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/oss/types.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/ossConfig.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/ossConfig/index.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/ossConfig/types.ts 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post.js 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post/index.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/post/types.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/role.js 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/role/index.ts 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/role/types.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant.js 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant/index.ts 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenant/types.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenantPackage.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenantPackage/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/tenantPackage/types.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/index.ts 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user/types.ts 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tool/gen.js 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tool/gen/index.ts 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tool/gen/types.ts 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/types.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/login-background.jpg 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/images/profile.jpg 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo/logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/btn.scss 124 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/element-ui.scss 101 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 190 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/mixin.scss 92 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/ruoyi.scss 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 386 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/transition.scss 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 52 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Breadcrumb/index.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/DictTag/index.vue 65 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Editor/index.vue 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FileUpload/index.vue 155 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Hamburger/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/HeaderSearch/index.vue 122 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/IconSelect/index.vue 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/IconSelect/requireIcons.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/IconSelect/requireIcons.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImagePreview/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImageUpload/index.vue 164 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Pagination/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ParentView/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RightToolbar/index.vue 79 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RuoYiDoc/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/RuoYiGit/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Screenfull/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SizeSelect/index.vue 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SvgIcon/index.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SvgIcon/svgicon.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/TopNav/index.vue 101 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/TreeSelect/index.vue 88 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/iFrame/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/common/copyText.js 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/common/copyText.ts 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/index.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/index.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/permission/hasPermi.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/permission/hasRole.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/directive/permission/index.ts 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/MenuTypeEnum.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/RespEnum.ts 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/SettingTypeEnum.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enums/layout/LayoutEnum.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lang/en.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lang/index.ts 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/lang/zh-cn.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/IframeToggle/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/InnerLink/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 228 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Settings/index.vue 211 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Link.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/SidebarItem.vue 81 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/ScrollPane.vue 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/index.vue 236 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/index.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.ts 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.js 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/permission.ts 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/auth.js 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/auth.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/cache.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/cache.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/download.js 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/download.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/index.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/index.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/modal.js 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/modal.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/svgicon.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/tab.js 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/tab.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.ts 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/settings.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/settings.ts 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/app.js 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/app.ts 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/dict.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/dict.ts 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.js 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.ts 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/settings.js 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/settings.ts 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/tagsView.js 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/tagsView.ts 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.js 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/auto-imports.d.ts 574 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/axios.d.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/components.d.ts 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/element.d.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/env.d.ts 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/global.d.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/module.d.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/router.d.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/types/setting.d.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/auth.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/auth.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.js 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.ts 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dynamicTitle.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dynamicTitle.ts 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/errorCode.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/errorCode.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/i18n.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/index.js 390 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/index.ts 318 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/jsencrypt.js 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/jsencrypt.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/permission.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/permission.ts 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.js 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.ts 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/ruoyi.js 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/ruoyi.ts 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/scroll-to.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/scroll-to.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/theme.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/theme.ts 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/validate.js 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/validate.ts 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/demo/index.vue 648 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/demo/tree/index.vue 410 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/error/401.vue 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/error/404.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 227 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 265 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/admin/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/cache/index.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/logininfor/index.vue 328 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/online/index.vue 174 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/operlog/index.vue 492 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/monitor/xxljob/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/redirect/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/register.vue 234 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/config/index.vue 447 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dept/index.vue 494 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dict/data.vue 564 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/dict/index.vue 455 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 824 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/notice/index.vue 461 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/oss/config.vue 492 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/oss/index.vue 597 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/post/index.vue 445 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/authUser.vue 246 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 826 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/selectUser.vue 203 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenant/index.vue 576 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/tenantPackage/index.vue 453 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/authRole.vue 179 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/index.vue 1066 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/index.vue 160 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/resetPwd.vue 71 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/userAvatar.vue 204 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/user/profile/userInfo.vue 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/build/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/basicInfoForm.vue 76 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/editTable.vue 327 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/genInfoForm.vue 501 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/importTable.vue 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/gen/index.vue 460 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tsconfig.json 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/auto-import.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/compression.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/index.js 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/setup-extend.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite/plugins/svg-icon.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development
@@ -15,3 +15,4 @@
# xxl-job æŽ§åˆ¶å°åœ°å€
VITE_APP_XXL_JOB_ADMIN = 'http://localhost:9100/xxl-job-admin'
VITE_APP_PORT = 3000
.env.production
@@ -17,4 +17,6 @@
VITE_APP_BASE_API = '/prod-api'
# æ˜¯å¦åœ¨æ‰“包时开启压缩,支持 gzip å’Œ brotli
VITE_BUILD_COMPRESS = gzip
VITE_BUILD_COMPRESS = gzip
VITE_APP_PORT = 3000
.eslintignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
.eslintrc.js
prettier.config.js
src/assets
tailwind.config.js
.eslintrc-auto-import.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,281 @@
{
    "globals": {
        "useRouter": true,
        "useRoute": true,
        "EffectScope": true,
        "ElTable": true,
        "ElSelect": true,
        "ElUpload": true,
        "ElForm": true,
        "ElTree": true,
        "ElMessage": true,
        "ElMessageBox": true,
        "asyncComputed": true,
        "autoResetRef": true,
        "computed": true,
        "computedAsync": true,
        "computedEager": true,
        "computedInject": true,
        "computedWithControl": true,
        "controlledComputed": true,
        "controlledRef": true,
        "createApp": true,
        "createEventHook": true,
        "createGlobalState": true,
        "createInjectionState": true,
        "createReactiveFn": true,
        "createSharedComposable": true,
        "createUnrefFn": true,
        "customRef": true,
        "debouncedRef": true,
        "debouncedWatch": true,
        "defineAsyncComponent": true,
        "defineComponent": true,
        "eagerComputed": true,
        "effectScope": true,
        "extendRef": true,
        "getCurrentInstance": true,
        "getCurrentScope": true,
        "h": true,
        "ignorableWatch": true,
        "inject": true,
        "isDefined": true,
        "isProxy": true,
        "isReactive": true,
        "isReadonly": true,
        "isRef": true,
        "makeDestructurable": true,
        "markRaw": true,
        "nextTick": true,
        "onActivated": true,
        "onBeforeMount": true,
        "onBeforeUnmount": true,
        "onBeforeUpdate": true,
        "onClickOutside": true,
        "onDeactivated": true,
        "onErrorCaptured": true,
        "onKeyStroke": true,
        "onLongPress": true,
        "onMounted": true,
        "onRenderTracked": true,
        "onRenderTriggered": true,
        "onScopeDispose": true,
        "onServerPrefetch": true,
        "onStartTyping": true,
        "onUnmounted": true,
        "onUpdated": true,
        "pausableWatch": true,
        "provide": true,
        "reactify": true,
        "reactifyObject": true,
        "reactive": true,
        "reactiveComputed": true,
        "reactiveOmit": true,
        "reactivePick": true,
        "readonly": true,
        "ref": true,
        "refAutoReset": true,
        "refDebounced": true,
        "refDefault": true,
        "refThrottled": true,
        "refWithControl": true,
        "resolveComponent": true,
        "resolveDirective": true,
        "resolveRef": true,
        "resolveUnref": true,
        "shallowReactive": true,
        "shallowReadonly": true,
        "shallowRef": true,
        "syncRef": true,
        "syncRefs": true,
        "templateRef": true,
        "throttledRef": true,
        "throttledWatch": true,
        "toRaw": true,
        "toReactive": true,
        "toRef": true,
        "toRefs": true,
        "triggerRef": true,
        "tryOnBeforeMount": true,
        "tryOnBeforeUnmount": true,
        "tryOnMounted": true,
        "tryOnScopeDispose": true,
        "tryOnUnmounted": true,
        "unref": true,
        "unrefElement": true,
        "until": true,
        "useActiveElement": true,
        "useArrayEvery": true,
        "useArrayFilter": true,
        "useArrayFind": true,
        "useArrayFindIndex": true,
        "useArrayFindLast": true,
        "useArrayJoin": true,
        "useArrayMap": true,
        "useArrayReduce": true,
        "useArraySome": true,
        "useArrayUnique": true,
        "useAsyncQueue": true,
        "useAsyncState": true,
        "useAttrs": true,
        "useBase64": true,
        "useBattery": true,
        "useBluetooth": true,
        "useBreakpoints": true,
        "useBroadcastChannel": true,
        "useBrowserLocation": true,
        "useCached": true,
        "useClipboard": true,
        "useCloned": true,
        "useColorMode": true,
        "useConfirmDialog": true,
        "useCounter": true,
        "useCssModule": true,
        "useCssVar": true,
        "useCssVars": true,
        "useCurrentElement": true,
        "useCycleList": true,
        "useDark": true,
        "useDateFormat": true,
        "useDebounce": true,
        "useDebounceFn": true,
        "useDebouncedRefHistory": true,
        "useDeviceMotion": true,
        "useDeviceOrientation": true,
        "useDevicePixelRatio": true,
        "useDevicesList": true,
        "useDisplayMedia": true,
        "useDocumentVisibility": true,
        "useDraggable": true,
        "useDropZone": true,
        "useElementBounding": true,
        "useElementByPoint": true,
        "useElementHover": true,
        "useElementSize": true,
        "useElementVisibility": true,
        "useEventBus": true,
        "useEventListener": true,
        "useEventSource": true,
        "useEyeDropper": true,
        "useFavicon": true,
        "useFetch": true,
        "useFileDialog": true,
        "useFileSystemAccess": true,
        "useFocus": true,
        "useFocusWithin": true,
        "useFps": true,
        "useFullscreen": true,
        "useGamepad": true,
        "useGeolocation": true,
        "useIdle": true,
        "useImage": true,
        "useInfiniteScroll": true,
        "useIntersectionObserver": true,
        "useInterval": true,
        "useIntervalFn": true,
        "useKeyModifier": true,
        "useLastChanged": true,
        "useLocalStorage": true,
        "useMagicKeys": true,
        "useManualRefHistory": true,
        "useMediaControls": true,
        "useMediaQuery": true,
        "useMemoize": true,
        "useMemory": true,
        "useMounted": true,
        "useMouse": true,
        "useMouseInElement": true,
        "useMousePressed": true,
        "useMutationObserver": true,
        "useNavigatorLanguage": true,
        "useNetwork": true,
        "useNow": true,
        "useObjectUrl": true,
        "useOffsetPagination": true,
        "useOnline": true,
        "usePageLeave": true,
        "useParallax": true,
        "usePermission": true,
        "usePointer": true,
        "usePointerLock": true,
        "usePointerSwipe": true,
        "usePreferredColorScheme": true,
        "usePreferredContrast": true,
        "usePreferredDark": true,
        "usePreferredLanguages": true,
        "usePreferredReducedMotion": true,
        "usePrevious": true,
        "useRafFn": true,
        "useRefHistory": true,
        "useResizeObserver": true,
        "useScreenOrientation": true,
        "useScreenSafeArea": true,
        "useScriptTag": true,
        "useScroll": true,
        "useScrollLock": true,
        "useSessionStorage": true,
        "useShare": true,
        "useSlots": true,
        "useSorted": true,
        "useSpeechRecognition": true,
        "useSpeechSynthesis": true,
        "useStepper": true,
        "useStorage": true,
        "useStorageAsync": true,
        "useStyleTag": true,
        "useSupported": true,
        "useSwipe": true,
        "useTemplateRefsList": true,
        "useTextDirection": true,
        "useTextSelection": true,
        "useTextareaAutosize": true,
        "useThrottle": true,
        "useThrottleFn": true,
        "useThrottledRefHistory": true,
        "useTimeAgo": true,
        "useTimeout": true,
        "useTimeoutFn": true,
        "useTimeoutPoll": true,
        "useTimestamp": true,
        "useTitle": true,
        "useToNumber": true,
        "useToString": true,
        "useToggle": true,
        "useTransition": true,
        "useUrlSearchParams": true,
        "useUserMedia": true,
        "useVModel": true,
        "useVModels": true,
        "useVibrate": true,
        "useVirtualList": true,
        "useWakeLock": true,
        "useWebNotification": true,
        "useWebSocket": true,
        "useWebWorker": true,
        "useWebWorkerFn": true,
        "useWindowFocus": true,
        "useWindowScroll": true,
        "useWindowSize": true,
        "watch": true,
        "watchArray": true,
        "watchAtMost": true,
        "watchDebounced": true,
        "watchEffect": true,
        "watchIgnorable": true,
        "watchOnce": true,
        "watchPausable": true,
        "watchPostEffect": true,
        "watchSyncEffect": true,
        "watchThrottled": true,
        "watchTriggerable": true,
        "watchWithFilter": true,
        "whenever": true,
        "ImportOption": true,
        "TreeType": true,
        "FieldOption": true,
        "PageData": true,
        "storeToRefs": true,
        "DictDataOption": true,
        "UploadOption": true
    }
}
.eslintrc.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
module.exports = {
    env: {
        browser: true,
        es2021: true,
        node: true
    },
    parser: 'vue-eslint-parser',
    extends: [
        'eslint:recommended',
        'plugin:vue/vue3-essential',
        'plugin:@typescript-eslint/recommended',
        './.eslintrc-auto-import.json',
        'plugin:prettier/recommended'
    ],
    parserOptions: {
        ecmaVersion: '2020',
        sourceType: 'module',
        parser: '@typescript-eslint/parser'
    },
    plugins: ['vue', '@typescript-eslint'],
    rules: {
        'vue/multi-word-component-names': 'off',
        '@typescript-eslint/no-empty-function': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
        'vue/no-v-model-argument': 'off',
        '@typescript-eslint/ban-types': [
            'error',
            {
                // å…³é—­ç©ºç±»åž‹æ£€æŸ¥ {}
                extendDefaults: true,
                types: {
                    '{}': false
                }
            }
        ]
    },
    globals: {
        DialogOption: 'readonly',
        OptionType: 'readonly'
    }
};
.prettierignore
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
.prettierrc.cjs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
/**
 * ä»£ç æ ¼å¼åŒ–配置
 */
module.exports = {
    // ä¸€è¡Œæœ€å¤šå¤šå°‘个字符
    printWidth: 150,
    // æŒ‡å®šæ¯ä¸ªç¼©è¿›çº§åˆ«çš„空格数
    tabWidth: 2,
    // ä½¿ç”¨åˆ¶è¡¨ç¬¦è€Œä¸æ˜¯ç©ºæ ¼ç¼©è¿›è¡Œ
    useTabs: true,
    // åœ¨è¯­å¥æœ«å°¾æ˜¯å¦éœ€è¦åˆ†å·
    semi: true,
    // æ˜¯å¦ä½¿ç”¨å•引号
    singleQuote: true,
    // æ›´æ”¹å¼•用对象属性的时间 å¯é€‰å€¼"<as-needed|consistent|preserve>"
    quoteProps: 'as-needed',
    // åœ¨JSX中使用单引号而不是双引号
    jsxSingleQuote: false,
    // å¤šè¡Œæ—¶å°½å¯èƒ½æ‰“印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) å¯é€‰å€¼"<none|es5|all>",默认none
    trailingComma: 'none',
    // åœ¨å¯¹è±¡æ–‡å­—中的括号之间打印空格
    bracketSpacing: true,
    // jsx æ ‡ç­¾çš„反尖括号需要换行
    jsxBracketSameLine: false,
    embeddedLanguageFormatting: 'off',
    // åœ¨å•独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
    arrowParens: 'always',
    // è¿™ä¸¤ä¸ªé€‰é¡¹å¯ç”¨äºŽæ ¼å¼åŒ–以给定字符偏移量(分别包括和不包括)开始和结束的代码
    rangeStart: 0,
    rangeEnd: Infinity,
    // æŒ‡å®šè¦ä½¿ç”¨çš„解析器,不需要写文件开头的 @prettier
    requirePragma: false,
    // ä¸éœ€è¦è‡ªåŠ¨åœ¨æ–‡ä»¶å¼€å¤´æ’å…¥ @prettier
    insertPragma: false,
    // ä½¿ç”¨é»˜è®¤çš„æŠ˜è¡Œæ ‡å‡† always\never\preserve
    proseWrap: 'preserve',
    // æŒ‡å®šHTML文件的全局空格敏感度 css\strict\ignore
    htmlWhitespaceSensitivity: 'css',
    // Vue文件脚本和样式标签缩进
    vueIndentScriptAndStyle: false,
    //在 windows æ“ä½œç³»ç»Ÿä¸­æ¢è¡Œç¬¦é€šå¸¸æ˜¯å›žè½¦ (CR) åŠ æ¢è¡Œåˆ†éš”ç¬¦ (LF),也就是回车换行(CRLF),
    //然而在 Linux å’Œ Unix ä¸­åªä½¿ç”¨ç®€å•的换行分隔符 (LF)。
    //对应的控制字符为 "\n" (LF) å’Œ "\r\n"(CRLF)。auto意为保持现有的行尾
    // æ¢è¡Œç¬¦ä½¿ç”¨ lf ç»“尾是 å¯é€‰å€¼"<auto|lf|crlf|cr>"
    endOfLine: 'auto'
};
README.md
@@ -1,9 +1,62 @@
## å¹³å°ç®€ä»‹
* æœ¬ä»“库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) ç‰ˆæœ¬ã€‚
* é…å¥—后端代码仓库地址
* [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
* [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
## å¹³å°ç®€ä»‹
- æœ¬ä»“库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) ç‰ˆæœ¬ã€‚
- é…å¥—后端代码仓库地址
- [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
- [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
## å‰ç«¯è¿è¡Œ
```bash
# å…‹éš†é¡¹ç›®
git clone https://gitee.com/JavaLionLi/plus-ui.git
# å®‰è£…依赖
npm install --registry=https://registry.npmmirror.com
# å¯åŠ¨æœåŠ¡
npm run dev
# æž„建测试环境 yarn build:stage
# æž„建生产环境 yarn build:prod
# å‰ç«¯è®¿é—®åœ°å€ http://localhost:80
```
## æœ¬æ¡†æž¶ä¸Ž RuoYi çš„业务差异
| ä¸šåŠ¡         | åŠŸèƒ½è¯´æ˜Ž                                                        | æœ¬æ¡†æž¶ | RuoYi                          |
| ------------ | --------------------------------------------------------------- | ------ | ------------------------------ |
| ç§Ÿæˆ·ç®¡ç†     | ç³»ç»Ÿå†…租户的管理 å¦‚:租户套餐、过期时间、用户数量、企业信息等    | æ”¯æŒ   | æ—                              |
| ç§Ÿæˆ·å¥—餐管理 | ç³»ç»Ÿå†…租户所能使用的套餐管理 å¦‚:套餐内所包含的菜单等            | æ”¯æŒ   | æ—                              |
| ç”¨æˆ·ç®¡ç†     | ç”¨æˆ·çš„管理配置 å¦‚:新增用户、分配用户所属部门、角色、岗位等      | æ”¯æŒ   | æ”¯æŒ                           |
| éƒ¨é—¨ç®¡ç†     | é…ç½®ç³»ç»Ÿç»„织机构(公司、部门、小组) æ ‘结构展现支持数据权限     | æ”¯æŒ   | æ”¯æŒ                           |
| å²—位管理     | é…ç½®ç³»ç»Ÿç”¨æˆ·æ‰€å±žæ‹…任职务                                        | æ”¯æŒ   | æ”¯æŒ                           |
| èœå•管理     | é…ç½®ç³»ç»Ÿèœå•、操作权限、按钮权限标识等                          | æ”¯æŒ   | æ”¯æŒ                           |
| è§’色管理     | è§’色菜单权限分配、设置角色按机构进行数据范围权限划分            | æ”¯æŒ   | æ”¯æŒ                           |
| å­—典管理     | å¯¹ç³»ç»Ÿä¸­ç»å¸¸ä½¿ç”¨çš„一些较为固定的数据进行维护                    | æ”¯æŒ   | æ”¯æŒ                           |
| å‚数管理     | å¯¹ç³»ç»ŸåŠ¨æ€é…ç½®å¸¸ç”¨å‚æ•°                                          | æ”¯æŒ   | æ”¯æŒ                           |
| é€šçŸ¥å…¬å‘Š     | ç³»ç»Ÿé€šçŸ¥å…¬å‘Šä¿¡æ¯å‘布维护                                        | æ”¯æŒ   | æ”¯æŒ                           |
| æ“ä½œæ—¥å¿—     | ç³»ç»Ÿæ­£å¸¸æ“ä½œæ—¥å¿—记录和查询 ç³»ç»Ÿå¼‚常信息日志记录和查询           | æ”¯æŒ   | æ”¯æŒ                           |
| ç™»å½•日志     | ç³»ç»Ÿç™»å½•日志记录查询包含登录异常                                | æ”¯æŒ   | æ”¯æŒ                           |
| æ–‡ä»¶ç®¡ç†     | ç³»ç»Ÿæ–‡ä»¶å±•示、上传、下载、删除等管理                            | æ”¯æŒ   | æ—                              |
| æ–‡ä»¶é…ç½®ç®¡ç† | ç³»ç»Ÿæ–‡ä»¶ä¸Šä¼ ã€ä¸‹è½½æ‰€éœ€è¦çš„配置信息动态添加、修改、删除等管理    | æ”¯æŒ   | æ—                              |
| åœ¨çº¿ç”¨æˆ·ç®¡ç† | å·²ç™»å½•系统的在线用户信息监控与强制踢出操作                      | æ”¯æŒ   | æ”¯æŒ                           |
| å®šæ—¶ä»»åŠ¡     | è¿è¡ŒæŠ¥è¡¨ã€ä»»åŠ¡ç®¡ç†(添加、修改、删除)、日志管理、执行器管理等    | æ”¯æŒ   | ä»…支持任务与日志管理           |
| ä»£ç ç”Ÿæˆ     | å¤šæ•°æ®æºå‰åŽç«¯ä»£ç çš„生成(java、html、xml、sql)支持 CRUD ä¸‹è½½  | æ”¯æŒ   | ä»…支持单数据源                 |
| ç³»ç»ŸæŽ¥å£     | æ ¹æ®ä¸šåŠ¡ä»£ç è‡ªåŠ¨ç”Ÿæˆç›¸å…³çš„ api æŽ¥å£æ–‡æ¡£                         | æ”¯æŒ   | æ”¯æŒ                           |
| æœåŠ¡ç›‘æŽ§     | ç›‘视集群系统 CPU、内存、磁盘、堆栈、在线日志、Spring ç›¸å…³é…ç½®ç­‰ | æ”¯æŒ   | ä»…支持单机 CPU、内存、磁盘监控 |
| ç¼“存监控     | å¯¹ç³»ç»Ÿçš„缓存信息查询,命令统计等。                              | æ”¯æŒ   | æ”¯æŒ                           |
| åœ¨çº¿æž„建器   | æ‹–动表单元素生成相应的 HTML ä»£ç ã€‚                              | æ”¯æŒ   | æ”¯æŒ                           |
| ä½¿ç”¨æ¡ˆä¾‹     | ç³»ç»Ÿçš„一些功能案例                                              | æ”¯æŒ   | ä¸æ”¯æŒ                         |
## æ¼”示图
- æœ¬ä»“库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) ç‰ˆæœ¬ã€‚
- é…å¥—后端代码仓库地址
- [RuoYi-Vue-Plus 5.X(注意版本号)](https://gitee.com/dromara/RuoYi-Vue-Plus)
- [RuoYi-Cloud-Plus 2.X(注意版本号)](https://gitee.com/dromara/RuoYi-Cloud-Plus)
## å‰ç«¯è¿è¡Œ
@@ -23,6 +76,7 @@
```
## åŽç«¯æ”¹é€ 
参考后端代码内 `ruoyi-gen/resources/vm/vue/v3/readme.txt` è¯´æ˜Ž
## å†…置功能
@@ -40,11 +94,11 @@
11. ç™»å½•日志:系统登录日志记录查询包含登录异常。
12. åœ¨çº¿ç”¨æˆ·ï¼šå½“前系统中活跃用户状态监控。
13. å®šæ—¶ä»»åŠ¡ï¼šåœ¨çº¿ï¼ˆæ·»åŠ ã€ä¿®æ”¹ã€åˆ é™¤)任务调度包含执行结果日志。
14. ä»£ç ç”Ÿæˆï¼šå‰åŽç«¯ä»£ç çš„生成(java、html、xml、sql)支持CRUD下载 ã€‚
15. ç³»ç»ŸæŽ¥å£ï¼šæ ¹æ®ä¸šåŠ¡ä»£ç è‡ªåŠ¨ç”Ÿæˆç›¸å…³çš„api接口文档。
16. æœåŠ¡ç›‘æŽ§ï¼šç›‘è§†å½“å‰ç³»ç»ŸCPU、内存、磁盘、堆栈等相关信息。
14. ä»£ç ç”Ÿæˆï¼šå‰åŽç«¯ä»£ç çš„生成(java、html、xml、sql)支持 CRUD ä¸‹è½½ ã€‚
15. ç³»ç»ŸæŽ¥å£ï¼šæ ¹æ®ä¸šåŠ¡ä»£ç è‡ªåŠ¨ç”Ÿæˆç›¸å…³çš„ api æŽ¥å£æ–‡æ¡£ã€‚
16. æœåŠ¡ç›‘æŽ§ï¼šç›‘è§†å½“å‰ç³»ç»Ÿ CPU、内存、磁盘、堆栈等相关信息。
17. ç¼“存监控:对系统的缓存信息查询,命令统计等。
18. åœ¨çº¿æž„建器:拖动表单元素生成相应的HTML代码。
18. åœ¨çº¿æž„建器:拖动表单元素生成相应的 HTML ä»£ç ã€‚
## æ¼”示图
@@ -81,4 +135,4 @@
        <td><img src="https://oscimg.oschina.net/oscnet/b6115bc8c31de52951982e509930b20684a.jpg"/></td>
        <td><img src="https://oscimg.oschina.net/oscnet/up-5e4daac0bb59612c5038448acbcef235e3a.png"/></td>
    </tr>
</table>
</table>
Vite/plugins/auto-import.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
import AutoImport from 'unplugin-auto-import/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import IconsResolver from 'unplugin-icons/resolver';
export default (path: any) => {
    return AutoImport({
        // è‡ªåЍ坼入 Vue ç›¸å…³å‡½æ•°
        imports: ['vue', 'vue-router', '@vueuse/core', 'pinia'],
        eslintrc: {
            enabled: false,
            filepath: './.eslintrc-auto-import.json',
            globalsPropValue: true
        },
        resolvers: [
            // è‡ªåЍ坼入 Element Plus ç›¸å…³å‡½æ•°ElMessage, ElMessageBox... (带样式)
            ElementPlusResolver(),
            IconsResolver({
                prefix: 'Icon'
            })
        ],
        vueTemplate: true, // æ˜¯å¦åœ¨ vue æ¨¡æ¿ä¸­è‡ªåЍ坼入
        dts: path.resolve(path.resolve(__dirname, '../../src'), 'types', 'auto-imports.d.ts')
    });
};
Vite/plugins/components.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
import IconsResolver from 'unplugin-icons/resolver';
export default (path: any) => {
    return Components({
        resolvers: [
            // è‡ªåЍ坼入 Element Plus ç»„ä»¶
            ElementPlusResolver(),
            // è‡ªåŠ¨æ³¨å†Œå›¾æ ‡ç»„ä»¶
            IconsResolver({
                enabledCollections: ['ep']
            })
        ],
        dts: path.resolve(path.resolve(__dirname, '../../src'), 'types', 'components.d.ts')
    });
};
Vite/plugins/icons.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
import Icons from 'unplugin-icons/vite';
export default () => {
    return Icons({
        // è‡ªåŠ¨å®‰è£…å›¾æ ‡åº“
        autoInstall: true
    });
};
Vite/plugins/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import vue from '@vitejs/plugin-vue';
import createUnoCss from './unocss';
import createAutoImport from './auto-import';
import createComponents from './components';
import createIcons from './icons';
import createSvgIconsPlugin from './svg-icon';
import path from 'path';
export default (viteEnv, isBuild = false): [] => {
    const vitePlusgins: any = [];
    vitePlusgins.push(vue());
    vitePlusgins.push(createUnoCss());
    vitePlusgins.push(createAutoImport(path));
    vitePlusgins.push(createComponents(path));
    vitePlusgins.push(createIcons());
    vitePlusgins.push(createSvgIconsPlugin(path));
    return vitePlusgins;
};
Vite/plugins/svg-icon.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
export default (path: any) => {
    return createSvgIconsPlugin({
        // æŒ‡å®šéœ€è¦ç¼“存的图标文件夹
        iconDirs: [path.resolve(path.resolve(__dirname, '../../src'), 'assets/icons/svg')],
        // æŒ‡å®šsymbolId格式
        symbolId: 'icon-[dir]-[name]'
    });
};
Vite/plugins/unocss.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
import UnoCss from 'unocss/vite';
import { presetUno, presetAttributify, presetIcons } from 'unocss';
export default () => {
    return UnoCss({
        presets: [presetUno(), presetAttributify(), presetIcons()],
        // rules: [['search', {}]],
        shortcuts: {
            'panel-title':
                'pb-[5px] font-sans leading-[1.1] font-medium text-base text-[#6379bb] border-b border-b-solid border-[var(--el-border-color-light)] mb-5 mt-0'
        }
    });
};
commitlint.config.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
module.exports = {
    extends: ['@commitlint/config-conventional'],
    rules: {
        'type-enum': [
            2,
            'always',
            [
                'feat', // æ–°åŠŸèƒ½ feature
                'fix', // ä¿®å¤ bug
                'docs', // æ–‡æ¡£æ³¨é‡Š
                'style', // ä»£ç æ ¼å¼
                'refactor', // é‡æž„
                'perf', // æ€§èƒ½ä¼˜åŒ–
                'test', // å¢žåŠ æµ‹è¯•
                'chore', // æž„建过程或辅助工具的变动
                'revert', // å›žé€€
                'build' // æ‰“包
            ]
        ],
        'subject-case': [0]
    }
};
html/ie.html
@@ -1,46 +1,60 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <title>请升级您的浏览器</title>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" >
    <meta name="renderer" content="webkit">
    <base target="_blank" />
    <style type="text/css">
        html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
        a{text-decoration:none;color:#0072c6;}a:hover{text-decoration:none;color:#004d8c;}
        body{width:960px;margin:0 auto;padding:10px;font-size:14px;line-height:24px;color:#454545;font-family:'Microsoft YaHei UI','Microsoft YaHei',DengXian,SimSun,'Segoe UI',Tahoma,Helvetica,sans-serif;overflow-y:scroll}
        h1{font-size:40px;line-height:80px;font-weight:100;margin-bottom:10px;}
        h2{font-size:20px;line-height:25px;font-weight:100;margin:10px 0;}
        em{color:red}
        p{margin-bottom:10px;}
        hr{margin:20px 0;border:0;border-top:1px solid #dadada}
        span{display:block;font-size:12px;line-height:12px;}
        .clean{clear:both;}
        .browser{padding:10px 10px;}
        .browser li{width:auto;padding:0 80px;margin-top:30px;height:34px;line-height:22px;float:left;list-style:none;background:url(data:image/png;base64,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) no-repeat;padding-left:40px}
        .browser .browser-firefox{background-position:0 -34px}
        .browser .browser-ie{background-position:0 -68px;margin-left:0px}
        .browser .browser-360{background-position:0 -170px;margin-left: -27px}
    </style>
</head>
<body style="margin-top:50px">
<h1>请升级您的浏览器,以便我们更好的为您提供服务!</h1>
<p>您正在使用 Internet Explorer çš„æ—©æœŸç‰ˆæœ¬ï¼ˆIE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。</p>
<hr>
<h2>请注意:微软公司对Windows XP åŠ Internet Explorer æ—©æœŸç‰ˆæœ¬çš„æ”¯æŒå·²ç»ç»“束</h2>
<p>自 2016 å¹´ 1 æœˆ 12 æ—¥èµ·ï¼ŒMicrosoft ä¸å†ä¸º IE 11 ä»¥ä¸‹ç‰ˆæœ¬æä¾›ç›¸åº”支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 <a href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support">微软对 Internet Explorer æ—©æœŸç‰ˆæœ¬çš„æ”¯æŒå°†äºŽ 2016 å¹´ 1 æœˆ 12 æ—¥ç»“束的说明</a> ã€‚</p>
<hr>
<h2>您可以选择更先进的浏览器</h2>
<p>推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。</p>
<ul class="browser">
    <li class="browser-chrome"><a href="https://www.google.cn/chrome/browser/desktop/index.html?hl=zh-CN&standalone=1"> è°·æ­Œæµè§ˆå™¨<span>Google Chrome</span></a></li>
    <li class="browser-firefox"><a href="https://www.mozilla.org/zh-CN/firefox/new/"> ç«ç‹æµè§ˆå™¨<span>Mozilla Firefox</span></a></li>
    <li class="browser-ie"><a href="https://windows.microsoft.com/zh-cn/internet-explorer/download-ie"> IE 11 æµè§ˆå™¨<span>Internet Explorer</span></a></li>
    <li class="browser-360"><a href="http://se.360.cn/"> 360安全浏览器<span>360 Chrome</span></a></li>
    <div class="clean"></div>
</ul>
<hr>
</body>
</html>
    <head>
        <meta charset="UTF-8" />
        <title>请升级您的浏览器</title>
        <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />
        <meta name="renderer" content="webkit" />
        <base target="_blank" />
        <style type="text/css">
            html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
            a{text-decoration:none;color:#0072c6;}a:hover{text-decoration:none;color:#004d8c;}
            body{width:960px;margin:0 auto;padding:10px;font-size:14px;line-height:24px;color:#454545;font-family:'Microsoft YaHei UI','Microsoft YaHei',DengXian,SimSun,'Segoe UI',Tahoma,Helvetica,sans-serif;overflow-y:scroll}
            h1{font-size:40px;line-height:80px;font-weight:100;margin-bottom:10px;}
            h2{font-size:20px;line-height:25px;font-weight:100;margin:10px 0;}
            em{color:red}
            p{margin-bottom:10px;}
            hr{margin:20px 0;border:0;border-top:1px solid #dadada}
            span{display:block;font-size:12px;line-height:12px;}
            .clean{clear:both;}
            .browser{padding:10px 10px;}
            .browser li{width:auto;padding:0 80px;margin-top:30px;height:34px;line-height:22px;float:left;list-style:none;background:url(data:image/png;base64,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) no-repeat;padding-left:40px}
            .browser .browser-firefox{background-position:0 -34px}
            .browser .browser-ie{background-position:0 -68px;margin-left:0px}
            .browser .browser-360{background-position:0 -170px;margin-left: -27px}
        </style>
    </head>
    <body style="margin-top:50px">
        <h1>请升级您的浏览器,以便我们更好的为您提供服务!</h1>
        <p>您正在使用 Internet Explorer çš„æ—©æœŸç‰ˆæœ¬ï¼ˆIE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。</p>
        <hr />
        <h2>请注意:微软公司对Windows XP åŠ Internet Explorer æ—©æœŸç‰ˆæœ¬çš„æ”¯æŒå·²ç»ç»“束</h2>
        <p>
            è‡ª 2016 å¹´ 1 æœˆ 12 æ—¥èµ·ï¼ŒMicrosoft ä¸å†ä¸º IE 11
            ä»¥ä¸‹ç‰ˆæœ¬æä¾›ç›¸åº”支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅
            <a href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support"
                >微软对 Internet Explorer æ—©æœŸç‰ˆæœ¬çš„æ”¯æŒå°†äºŽ 2016 å¹´ 1 æœˆ 12 æ—¥ç»“束的说明</a
            >
            ã€‚
        </p>
        <hr />
        <h2>您可以选择更先进的浏览器</h2>
        <p>推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。</p>
        <ul class="browser">
            <li class="browser-chrome">
                <a href="https://www.google.cn/chrome/browser/desktop/index.html?hl=zh-CN&standalone=1"> è°·æ­Œæµè§ˆå™¨<span>Google Chrome</span></a>
            </li>
            <li class="browser-firefox">
                <a href="https://www.mozilla.org/zh-CN/firefox/new/"> ç«ç‹æµè§ˆå™¨<span>Mozilla Firefox</span></a>
            </li>
            <li class="browser-ie">
                <a href="https://windows.microsoft.com/zh-cn/internet-explorer/download-ie"> IE 11 æµè§ˆå™¨<span>Internet Explorer</span></a>
            </li>
            <li class="browser-360">
                <a href="http://se.360.cn/"> 360安全浏览器<span>360 Chrome</span></a>
            </li>
            <div class="clean"></div>
        </ul>
        <hr />
    </body>
</html>
index.html
@@ -1,215 +1,217 @@
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta name="renderer" content="webkit" />
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
        <link rel="icon" href="/favicon.ico" />
        <title>RuoYi-Vue-Plus多租户管理系统</title>
        <!--[if lt IE 11
            ]><script>
                window.location.href='/html/ie.html';
            </script><!
        [endif]-->
        <style>
            html,
            body,
            #app {
              height: 100%;
              margin: 0px;
              padding: 0px;
            }
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="renderer" content="webkit">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/favicon.ico">
  <title>RuoYi-Vue-Plus多租户管理系统</title>
  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
  <style>
    html,
    body,
    #app {
      height: 100%;
      margin: 0px;
      padding: 0px;
    }
            .chromeframe {
              margin: 0.2em 0;
              background: #ccc;
              color: #000;
              padding: 0.2em 0;
            }
    .chromeframe {
      margin: 0.2em 0;
      background: #ccc;
      color: #000;
      padding: 0.2em 0;
    }
            #loader-wrapper {
              position: fixed;
              top: 0;
              left: 0;
              width: 100%;
              height: 100%;
              z-index: 999999;
            }
    #loader-wrapper {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 999999;
    }
            #loader {
              display: block;
              position: relative;
              left: 50%;
              top: 50%;
              width: 150px;
              height: 150px;
              margin: -75px 0 0 -75px;
              border-radius: 50%;
              border: 3px solid transparent;
              border-top-color: #FFF;
              -webkit-animation: spin 2s linear infinite;
              -ms-animation: spin 2s linear infinite;
              -moz-animation: spin 2s linear infinite;
              -o-animation: spin 2s linear infinite;
              animation: spin 2s linear infinite;
              z-index: 1001;
            }
    #loader {
      display: block;
      position: relative;
      left: 50%;
      top: 50%;
      width: 150px;
      height: 150px;
      margin: -75px 0 0 -75px;
      border-radius: 50%;
      border: 3px solid transparent;
      border-top-color: #FFF;
      -webkit-animation: spin 2s linear infinite;
      -ms-animation: spin 2s linear infinite;
      -moz-animation: spin 2s linear infinite;
      -o-animation: spin 2s linear infinite;
      animation: spin 2s linear infinite;
      z-index: 1001;
    }
            #loader:before {
              content: "";
              position: absolute;
              top: 5px;
              left: 5px;
              right: 5px;
              bottom: 5px;
              border-radius: 50%;
              border: 3px solid transparent;
              border-top-color: #FFF;
              -webkit-animation: spin 3s linear infinite;
              -moz-animation: spin 3s linear infinite;
              -o-animation: spin 3s linear infinite;
              -ms-animation: spin 3s linear infinite;
              animation: spin 3s linear infinite;
            }
    #loader:before {
      content: "";
      position: absolute;
      top: 5px;
      left: 5px;
      right: 5px;
      bottom: 5px;
      border-radius: 50%;
      border: 3px solid transparent;
      border-top-color: #FFF;
      -webkit-animation: spin 3s linear infinite;
      -moz-animation: spin 3s linear infinite;
      -o-animation: spin 3s linear infinite;
      -ms-animation: spin 3s linear infinite;
      animation: spin 3s linear infinite;
    }
    #loader:after {
      content: "";
      position: absolute;
      top: 15px;
      left: 15px;
      right: 15px;
      bottom: 15px;
      border-radius: 50%;
      border: 3px solid transparent;
      border-top-color: #FFF;
      -moz-animation: spin 1.5s linear infinite;
      -o-animation: spin 1.5s linear infinite;
      -ms-animation: spin 1.5s linear infinite;
      -webkit-animation: spin 1.5s linear infinite;
      animation: spin 1.5s linear infinite;
    }
            #loader:after {
              content: "";
              position: absolute;
              top: 15px;
              left: 15px;
              right: 15px;
              bottom: 15px;
              border-radius: 50%;
              border: 3px solid transparent;
              border-top-color: #FFF;
              -moz-animation: spin 1.5s linear infinite;
              -o-animation: spin 1.5s linear infinite;
              -ms-animation: spin 1.5s linear infinite;
              -webkit-animation: spin 1.5s linear infinite;
              animation: spin 1.5s linear infinite;
            }
    @-webkit-keyframes spin {
      0% {
        -webkit-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        transform: rotate(0deg);
      }
            @-webkit-keyframes spin {
              0% {
                -webkit-transform: rotate(0deg);
                -ms-transform: rotate(0deg);
                transform: rotate(0deg);
              }
      100% {
        -webkit-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }
              100% {
                -webkit-transform: rotate(360deg);
                -ms-transform: rotate(360deg);
                transform: rotate(360deg);
              }
            }
    @keyframes spin {
      0% {
        -webkit-transform: rotate(0deg);
        -ms-transform: rotate(0deg);
        transform: rotate(0deg);
      }
            @keyframes spin {
              0% {
                -webkit-transform: rotate(0deg);
                -ms-transform: rotate(0deg);
                transform: rotate(0deg);
              }
      100% {
        -webkit-transform: rotate(360deg);
        -ms-transform: rotate(360deg);
        transform: rotate(360deg);
      }
    }
              100% {
                -webkit-transform: rotate(360deg);
                -ms-transform: rotate(360deg);
                transform: rotate(360deg);
              }
            }
    #loader-wrapper .loader-section {
      position: fixed;
      top: 0;
      width: 51%;
      height: 100%;
      background: #7171C6;
      z-index: 1000;
      -webkit-transform: translateX(0);
      -ms-transform: translateX(0);
      transform: translateX(0);
    }
            #loader-wrapper .loader-section {
              position: fixed;
              top: 0;
              width: 51%;
              height: 100%;
              background: #7171C6;
              z-index: 1000;
              -webkit-transform: translateX(0);
              -ms-transform: translateX(0);
              transform: translateX(0);
            }
    #loader-wrapper .loader-section.section-left {
      left: 0;
    }
            #loader-wrapper .loader-section.section-left {
              left: 0;
            }
    #loader-wrapper .loader-section.section-right {
      right: 0;
    }
            #loader-wrapper .loader-section.section-right {
              right: 0;
            }
    .loaded #loader-wrapper .loader-section.section-left {
      -webkit-transform: translateX(-100%);
      -ms-transform: translateX(-100%);
      transform: translateX(-100%);
      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
    }
            .loaded #loader-wrapper .loader-section.section-left {
              -webkit-transform: translateX(-100%);
              -ms-transform: translateX(-100%);
              transform: translateX(-100%);
              -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
              transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
            }
    .loaded #loader-wrapper .loader-section.section-right {
      -webkit-transform: translateX(100%);
      -ms-transform: translateX(100%);
      transform: translateX(100%);
      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
    }
            .loaded #loader-wrapper .loader-section.section-right {
              -webkit-transform: translateX(100%);
              -ms-transform: translateX(100%);
              transform: translateX(100%);
              -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
              transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
            }
    .loaded #loader {
      opacity: 0;
      -webkit-transition: all 0.3s ease-out;
      transition: all 0.3s ease-out;
    }
            .loaded #loader {
              opacity: 0;
              -webkit-transition: all 0.3s ease-out;
              transition: all 0.3s ease-out;
            }
    .loaded #loader-wrapper {
      visibility: hidden;
      -webkit-transform: translateY(-100%);
      -ms-transform: translateY(-100%);
      transform: translateY(-100%);
      -webkit-transition: all 0.3s 1s ease-out;
      transition: all 0.3s 1s ease-out;
    }
            .loaded #loader-wrapper {
              visibility: hidden;
              -webkit-transform: translateY(-100%);
              -ms-transform: translateY(-100%);
              transform: translateY(-100%);
              -webkit-transition: all 0.3s 1s ease-out;
              transition: all 0.3s 1s ease-out;
            }
    .no-js #loader-wrapper {
      display: none;
    }
            .no-js #loader-wrapper {
              display: none;
            }
    .no-js h1 {
      color: #222222;
    }
            .no-js h1 {
              color: #222222;
            }
    #loader-wrapper .load_title {
      font-family: 'Open Sans';
      color: #FFF;
      font-size: 19px;
      width: 100%;
      text-align: center;
      z-index: 9999999999999;
      position: absolute;
      top: 60%;
      opacity: 1;
      line-height: 30px;
    }
            #loader-wrapper .load_title {
              font-family: 'Open Sans';
              color: #FFF;
              font-size: 19px;
              width: 100%;
              text-align: center;
              z-index: 9999999999999;
              position: absolute;
              top: 60%;
              opacity: 1;
              line-height: 30px;
            }
    #loader-wrapper .load_title span {
      font-weight: normal;
      font-style: italic;
      font-size: 13px;
      color: #FFF;
      opacity: 0.5;
    }
  </style>
</head>
            #loader-wrapper .load_title span {
              font-weight: normal;
              font-style: italic;
              font-size: 13px;
              color: #FFF;
              opacity: 0.5;
            }
        </style>
    </head>
<body>
  <div id="app">
    <div id="loader-wrapper">
      <div id="loader"></div>
      <div class="loader-section section-left"></div>
      <div class="loader-section section-right"></div>
      <div class="load_title">正在加载系统资源,请耐心等待</div>
    </div>
  </div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
    <body>
        <div id="app">
            <div id="loader-wrapper">
                <div id="loader"></div>
                <div class="loader-section section-left"></div>
                <div class="loader-section section-right"></div>
                <div class="load_title">正在加载系统资源,请耐心等待</div>
            </div>
        </div>
        <script type="module" src="/src/main.ts"></script>
    </body>
</html>
package.json
@@ -1,43 +1,80 @@
{
  "name": "ruoyi-vue-plus",
  "version": "5.0.0-SNAPSHOT",
  "description": "RuoYi-Vue-Plus多租户管理系统",
  "author": "LionLi",
  "license": "MIT",
  "scripts": {
    "dev": "vite",
    "build:prod": "vite build",
    "preview": "vite preview"
  },
  "repository": {
    "type": "git",
    "url": "https://gitee.com/JavaLionLi/plus-ui.git"
  },
  "dependencies": {
    "@element-plus/icons-vue": "2.0.10",
    "@vueup/vue-quill": "1.1.0",
    "@vueuse/core": "9.5.0",
    "axios": "0.27.2",
    "echarts": "5.4.0",
    "element-plus": "2.2.27",
    "file-saver": "2.0.5",
    "fuse.js": "6.6.2",
    "js-cookie": "3.0.1",
    "jsencrypt": "3.3.1",
    "nprogress": "0.2.0",
    "pinia": "2.0.22",
    "vue": "3.2.45",
    "vue-cropper": "1.0.3",
    "vue-router": "4.1.4"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "3.1.0",
    "@vue/compiler-sfc": "3.2.45",
    "sass": "1.56.1",
    "unplugin-auto-import": "0.11.4",
    "vite": "3.2.3",
    "vite-plugin-compression": "0.5.1",
    "vite-plugin-svg-icons": "2.0.1",
    "vite-plugin-vue-setup-extend": "0.4.0"
  }
    "name": "ruoyi-vue-plus",
    "version": "5.0.0-SNAPSHOT",
    "description": "RuoYi-Vue-Plus多租户管理系统",
    "author": "LionLi",
    "license": "MIT",
    "scripts": {
        "dev": "vite serve --mode development",
        "build:prod": "vite build --mode production &&vue-tsc --noEmit",
        "preview": "vite preview",
        "lint": "eslint src/**/*.{ts,js,vue} --fix",
        "prepare": "husky install",
        "prettier": "prettier --write ."
    },
    "repository": {
        "type": "git",
        "url": "https://gitee.com/JavaLionLi/plus-ui.git"
    },
    "dependencies": {
        "@element-plus/icons-vue": "2.1.0",
        "@vueup/vue-quill": "1.1.0",
        "@vueuse/core": "9.5.0",
        "animate.css": "4.1.1",
        "await-to-js": "^3.0.0",
        "axios": "^1.3.4",
        "echarts": "5.4.0",
        "element-plus": "2.2.27",
        "file-saver": "2.0.5",
        "fuse.js": "6.6.2",
        "js-cookie": "3.0.1",
        "jsencrypt": "3.3.1",
        "nprogress": "0.2.0",
        "path-browserify": "1.0.1",
        "path-to-regexp": "6.2.0",
        "pinia": "2.0.22",
        "screenfull": "6.0.0",
        "vue": "3.2.45",
        "vue-cropper": "1.0.3",
        "vue-i18n": "9.2.2",
        "vue-router": "4.1.4"
    },
    "devDependencies": {
        "@iconify/json": "^2.2.40",
        "@intlify/unplugin-vue-i18n": "0.8.2",
        "@types/file-saver": "2.0.5",
        "@types/js-cookie": "3.0.3",
        "@types/node": "18.14.2",
        "@types/nprogress": "0.2.0",
        "@types/path-browserify": "^1.0.0",
        "@typescript-eslint/eslint-plugin": "5.56.0",
        "@typescript-eslint/parser": "5.56.0",
        "@unocss/preset-attributify": "^0.50.6",
        "@unocss/preset-icons": "^0.50.6",
        "@unocss/preset-uno": "^0.50.6",
        "@vitejs/plugin-vue": "4.0.0",
        "@vue/compiler-sfc": "3.2.45",
        "autoprefixer": "10.4.14",
        "eslint": "8.36.0",
        "eslint-config-prettier": "8.8.0",
        "eslint-plugin-prettier": "4.2.1",
        "eslint-plugin-vue": "9.9.0",
        "fast-glob": "^3.2.11",
        "husky": "7.0.4",
        "postcss": "^8.4.21",
        "prettier": "2.8.6",
        "sass": "1.56.1",
        "typescript": "4.9.5",
        "unocss": "^0.50.6",
        "unplugin-auto-import": "0.13.0",
        "unplugin-icons": "0.15.1",
        "unplugin-vue-components": "0.23.0",
        "vite": "4.1.4",
        "vite-plugin-compression": "0.5.1",
        "vite-plugin-svg-icons": "2.0.1",
        "vite-plugin-vue-setup-extend": "0.4.0",
        "vitest": "^0.29.7",
        "vue-eslint-parser": "9.1.0",
        "vue-tsc": "0.35.0"
    }
}
public/favicon.ico

src/App.vue
@@ -1,8 +1,8 @@
<template>
  <router-view />
    <router-view />
</template>
<script setup>
<script setup lang="ts">
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
src/animate.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
// å‰ç¼€
const animatePrefix = 'animate__animated ';
// å¼€å¯éšæœºåŠ¨ç”» éšæœºåŠ¨ç”»å€¼
const animateList: string[] = [
    animatePrefix + 'animate__pulse',
    animatePrefix + 'animate__rubberBand',
    animatePrefix + 'animate__bounceIn',
    animatePrefix + 'animate__bounceInLeft',
    animatePrefix + 'animate__fadeIn',
    animatePrefix + 'animate__fadeInLeft',
    animatePrefix + 'animate__fadeInDown',
    animatePrefix + 'animate__fadeInUp',
    animatePrefix + 'animate__flipInX',
    animatePrefix + 'animate__lightSpeedInLeft',
    animatePrefix + 'animate__rotateInDownLeft',
    animatePrefix + 'animate__rollIn',
    animatePrefix + 'animate__rotateInDownLeft',
    animatePrefix + 'animate__zoomIn',
    animatePrefix + 'animate__zoomInDown',
    animatePrefix + 'animate__slideInLeft',
    animatePrefix + 'animate__lightSpeedIn'
];
// å…³é—­éšæœºåŠ¨ç”»åŽçš„é»˜è®¤æ•ˆæžœ
const defaultAnimate = animatePrefix + 'animate__bounceIn';
// æœç´¢éšè—æ˜¾ç¤ºåŠ¨ç”»
const searchAnimate = {
    enter: animatePrefix + 'animate__flipInX',
    leave: animatePrefix + 'animate__bounceOut'
};
// èœå•搜索动画
const menuSearchAnimate = {
    enter: animatePrefix + 'animate__fadeInLeft',
    leave: animatePrefix + 'animate__fadeOutLeft'
};
// logo动画
const logoAnimate = {
    enter: animatePrefix + 'animate__bounceIn',
    leave: animatePrefix + 'animate__bounceOut'
};
export default {
    animateList,
    defaultAnimate,
    searchAnimate,
    menuSearchAnimate,
    logoAnimate
};
src/api/demo/demo.js
ÎļþÒÑɾ³ý
src/api/demo/demo.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
import request from '@/utils/request';
import { DemoForm, DemoQuery, DemoVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢æµ‹è¯•单表列表
export function listDemo(query: DemoQuery): AxiosPromise<DemoVO[]> {
    return request({
        url: '/demo/demo/list',
        method: 'get',
        params: query
    });
}
// è‡ªå®šä¹‰åˆ†é¡µæŽ¥å£
export function pageDemo(query: DemoQuery): AxiosPromise<DemoVO[]> {
    return request({
        url: '/demo/demo/page',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢æµ‹è¯•单表详细
export function getDemo(id: string | number): AxiosPromise<DemoVO> {
    return request({
        url: '/demo/demo/' + id,
        method: 'get'
    });
}
// æ–°å¢žæµ‹è¯•单表
export function addDemo(data: DemoForm) {
    return request({
        url: '/demo/demo',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹æµ‹è¯•单表
export function updateDemo(data: DemoForm) {
    return request({
        url: '/demo/demo',
        method: 'put',
        data: data
    });
}
// åˆ é™¤æµ‹è¯•单表
export function delDemo(id: string | number | Array<string | number>) {
    return request({
        url: '/demo/demo/' + id,
        method: 'delete'
    });
}
src/api/demo/tree.js
ÎļþÒÑɾ³ý
src/api/demo/tree.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
import { AxiosPromise } from 'axios';
import request from '@/utils/request';
import { DemoTreeForm, DemoTreeVO, DemoTreeQuery } from './types';
// æŸ¥è¯¢æµ‹è¯•树表列表
export function listTree(query?: DemoTreeQuery): AxiosPromise<DemoTreeVO[]> {
    return request({
        url: '/demo/tree/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢æµ‹è¯•树表详细
export function getTree(id: string | number): AxiosPromise<DemoTreeVO> {
    return request({
        url: '/demo/tree/' + id,
        method: 'get'
    });
}
// æ–°å¢žæµ‹è¯•树表
export function addTree(data: DemoTreeForm) {
    return request({
        url: '/demo/tree',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹æµ‹è¯•树表
export function updateTree(data: DemoTreeForm) {
    return request({
        url: '/demo/tree',
        method: 'put',
        data: data
    });
}
// åˆ é™¤æµ‹è¯•树表
export function delTree(id: string | number | Array<string | number>) {
    return request({
        url: '/demo/tree/' + id,
        method: 'delete'
    });
}
src/api/demo/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
export interface DemoVO extends BaseEntity {
    id: number | string;
    deptId: number | string;
    userId: number | string;
    orderNum: number;
    testKey: string;
    value: string;
    createByName: string;
    updateByName?: any;
}
export interface DemoQuery extends PageQuery {
    testKey: string;
    value: string;
    createTime: string;
}
export interface DemoForm {
    id: string | number | undefined;
    deptId: string | number | undefined;
    userId: string | number | undefined;
    orderNum: number;
    testKey: string;
    value: string;
    version: string;
    ossConfigId: string | number | undefined;
    createTime?: string;
}
export interface DemoTreeVO extends BaseEntity {
    id: number | string;
    parentId: number | string;
    deptId: number | string;
    userId: number | string;
    treeName: string;
    children?: DemoTreeVO[];
}
export interface DemoTreeQuery {
    treeName: string;
    createTime: string;
}
export interface DemoTreeForm {
    id: string | number | undefined;
    parentId: string | number | undefined;
    deptId: string | number | undefined;
    userId: string | number | undefined;
    treeName: string;
}
export interface DemoTreeOptionsType {
    id: string | number;
    treeName: string;
    children?: DemoTreeOptionsType[];
}
src/api/login.js
ÎļþÒÑɾ³ý
src/api/login.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { LoginData, LoginResult, VerifyCodeResult, TenantInfo } from './types';
import { UserInfo } from '@/api/system/user/types';
/**
 * @param data {LoginData}
 * @returns
 */
export function login(data: LoginData): AxiosPromise<LoginResult> {
    const params = {
        tenantId: data.tenantId,
        username: data.username.trim(),
        password: data.password,
        code: data.code,
        uuid: data.uuid
    };
    return request({
        url: '/auth/login',
        headers: {
            isToken: false
        },
        method: 'post',
        data: params
    });
}
// æ³¨å†Œæ–¹æ³•
export function register(data: any) {
    return request({
        url: '/auth/register',
        headers: {
            isToken: false
        },
        method: 'post',
        data: data
    });
}
/**
 * æ³¨é”€
 */
export function logout() {
    return request({
        url: '/auth/logout',
        method: 'post'
    });
}
/**
 * èŽ·å–éªŒè¯ç 
 */
export function getCodeImg(): AxiosPromise<VerifyCodeResult> {
    return request({
        url: '/code',
        headers: {
            isToken: false
        },
        method: 'get',
        timeout: 20000
    });
}
// èŽ·å–ç”¨æˆ·è¯¦ç»†ä¿¡æ¯
export function getInfo(): AxiosPromise<UserInfo> {
    return request({
        url: '/system/user/getInfo',
        method: 'get'
    });
}
// èŽ·å–ç§Ÿæˆ·åˆ—è¡¨
export function getTenantList(): AxiosPromise<TenantInfo> {
    return request({
        url: '/auth/tenant/list',
        headers: {
            isToken: false
        },
        method: 'get'
    });
}
src/api/menu.js
ÎļþÒÑɾ³ý
src/api/menu.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { RouteRecordRaw } from 'vue-router';
// èŽ·å–è·¯ç”±
export function getRouters(): AxiosPromise<RouteRecordRaw[]> {
    return request({
        url: '/system/menu/getRouters',
        method: 'get'
    });
}
src/api/monitor/cache.js
ÎļþÒÑɾ³ý
src/api/monitor/cache/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { CacheVO } from './types';
// æŸ¥è¯¢ç¼“存详细
export function getCache(): AxiosPromise<CacheVO> {
    return request({
        url: '/monitor/cache',
        method: 'get'
    });
}
// æŸ¥è¯¢ç¼“存名称列表
export function listCacheName() {
    return request({
        url: '/monitor/cache/getNames',
        method: 'get'
    });
}
// æŸ¥è¯¢ç¼“存键名列表
export function listCacheKey(cacheName: string) {
    return request({
        url: '/monitor/cache/getKeys/' + cacheName,
        method: 'get'
    });
}
// æŸ¥è¯¢ç¼“存内容
export function getCacheValue(cacheName: string, cacheKey: string) {
    return request({
        url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
        method: 'get'
    });
}
// æ¸…理指定名称缓存
export function clearCacheName(cacheName: string) {
    return request({
        url: '/monitor/cache/clearCacheName/' + cacheName,
        method: 'delete'
    });
}
// æ¸…理指定键名缓存
export function clearCacheKey(cacheName: string, cacheKey: string) {
    return request({
        url: '/monitor/cache/clearCacheKey/' + cacheName + '/' + cacheKey,
        method: 'delete'
    });
}
// æ¸…理全部缓存
export function clearCacheAll() {
    return request({
        url: '/monitor/cache/clearCacheAll',
        method: 'delete'
    });
}
src/api/monitor/cache/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
export interface CacheVO {
    commandStats: Array<{ name: string; value: string }>;
    dbSize: number;
    info: { [key: string]: string };
}
src/api/monitor/loginInfo/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
import request from '@/utils/request';
import { LoginInfoQuery, LoginInfoVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢ç™»å½•日志列表
export function list(query: LoginInfoQuery): AxiosPromise<LoginInfoVO[]> {
    return request({
        url: '/monitor/logininfor/list',
        method: 'get',
        params: query
    });
}
// åˆ é™¤ç™»å½•日志
export function delLoginInfo(infoId: string | number | Array<string | number>) {
    return request({
        url: '/monitor/logininfor/' + infoId,
        method: 'delete'
    });
}
// è§£é”ç”¨æˆ·ç™»å½•状态
export function unlockLoginInfo(userName: string | Array<string>) {
    return request({
        url: '/monitor/logininfor/unlock/' + userName,
        method: 'get'
    });
}
// æ¸…空登录日志
export function cleanLoginInfo() {
    return request({
        url: '/monitor/logininfor/clean',
        method: 'delete'
    });
}
src/api/monitor/loginInfo/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
export interface LoginInfoVO {
    infoId: string | number;
    tenantId: string | number;
    userName: string;
    status: string;
    ipaddr: string;
    loginLocation: string;
    browser: string;
    os: string;
    msg: string;
    loginTime: string;
}
export interface LoginInfoQuery extends PageQuery {
    ipaddr: string;
    userName: string;
    status: string;
    orderByColumn: string;
    isAsc: string;
}
src/api/monitor/logininfor.js
ÎļþÒÑɾ³ý
src/api/monitor/online.js
ÎļþÒÑɾ³ý
src/api/monitor/online/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
import request from '@/utils/request';
import { OnlineQuery, OnlineVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢åœ¨çº¿ç”¨æˆ·åˆ—表
export function list(query: OnlineQuery): AxiosPromise<OnlineVO[]> {
    return request({
        url: '/monitor/online/list',
        method: 'get',
        params: query
    });
}
// å¼ºé€€ç”¨æˆ·
export function forceLogout(tokenId: string) {
    return request({
        url: '/monitor/online/' + tokenId,
        method: 'delete'
    });
}
src/api/monitor/online/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
export interface OnlineQuery extends PageQuery {
    ipaddr: string;
    userName: string;
}
export interface OnlineVO extends BaseEntity {
    tokenId: string;
    deptName: string;
    userName: string;
    ipaddr: string;
    loginLocation: string;
    browser: string;
    os: string;
    loginTime: number;
}
src/api/monitor/operlog.js
ÎļþÒÑɾ³ý
src/api/monitor/operlog/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from '@/utils/request';
import { OperLogQuery, OperLogVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢æ“ä½œæ—¥å¿—列表
export function list(query: OperLogQuery): AxiosPromise<OperLogVO[]> {
    return request({
        url: '/monitor/operlog/list',
        method: 'get',
        params: query
    });
}
// åˆ é™¤æ“ä½œæ—¥å¿—
export function delOperlog(operId: string | number | Array<string | number>) {
    return request({
        url: '/monitor/operlog/' + operId,
        method: 'delete'
    });
}
// æ¸…空操作日志
export function cleanOperlog() {
    return request({
        url: '/monitor/operlog/clean',
        method: 'delete'
    });
}
src/api/monitor/operlog/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
export interface OperLogQuery extends PageQuery {
    title: string;
    operName: string;
    businessType: string;
    status: string;
    orderByColumn: string;
    isAsc: string;
}
export interface OperLogVO extends BaseEntity {
    operId: string | number;
    tenantId: string;
    title: string;
    businessType: number;
    businessTypes: number[] | undefined;
    method: string;
    requestMethod: string;
    operatorType: number;
    operName: string;
    deptName: string;
    operUrl: string;
    operIp: string;
    operLocation: string;
    operParam: string;
    jsonResult: string;
    status: number;
    errorMsg: string;
    operTime: string;
    costTime: number;
}
export interface OperLogForm {
    operId: number | string | undefined;
    tenantId: string | number | undefined;
    title: string;
    businessType: number;
    businessTypes: number[] | undefined;
    method: string;
    requestMethod: string;
    operatorType: number;
    operName: string;
    deptName: string;
    operUrl: string;
    operIp: string;
    operLocation: string;
    operParam: string;
    jsonResult: string;
    status: number;
    errorMsg: string;
    operTime: string;
    costTime: number;
}
src/api/system/config.js
ÎļþÒÑɾ³ý
src/api/system/config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
import request from '@/utils/request';
import { ConfigForm, ConfigQuery, ConfigVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢å‚数列表
export function listConfig(query: ConfigQuery): AxiosPromise<ConfigVO[]> {
    return request({
        url: '/system/config/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢å‚数详细
export function getConfig(configId: string | number): AxiosPromise<ConfigVO> {
    return request({
        url: '/system/config/' + configId,
        method: 'get'
    });
}
// æ ¹æ®å‚数键名查询参数值
export function getConfigKey(configKey: string): AxiosPromise<ConfigVO> {
    return request({
        url: '/system/config/configKey/' + configKey,
        method: 'get'
    });
}
// æ–°å¢žå‚数配置
export function addConfig(data: ConfigForm) {
    return request({
        url: '/system/config',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹å‚数配置
export function updateConfig(data: ConfigForm) {
    return request({
        url: '/system/config',
        method: 'put',
        data: data
    });
}
// ä¿®æ”¹å‚数配置
export function updateConfigByKey(key: string, value: any) {
    return request({
        url: '/system/config/updateByKey',
        method: 'put',
        data: {
            configKey: key,
            configValue: value
        }
    });
}
// åˆ é™¤å‚数配置
export function delConfig(configId: string | number | Array<string | number>) {
    return request({
        url: '/system/config/' + configId,
        method: 'delete'
    });
}
// åˆ·æ–°å‚数缓存
export function refreshCache() {
    return request({
        url: '/system/config/refreshCache',
        method: 'delete'
    });
}
src/api/system/config/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
export interface ConfigVO extends BaseEntity {
    configId: number | string;
    configName: string;
    configKey: string;
    configValue: string;
    configType: string;
    remark: string;
}
export interface ConfigForm {
    configId: number | string | undefined;
    configName: string;
    configKey: string;
    configValue: string;
    configType: string;
    remark: string;
}
export interface ConfigQuery extends PageQuery {
    configName: string;
    configKey: string;
    configType: string;
}
src/api/system/dept.js
ÎļþÒÑɾ³ý
src/api/system/dept/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DeptForm, DeptQuery, DeptVO } from './types';
// æŸ¥è¯¢éƒ¨é—¨åˆ—表
export const listDept = (query?: DeptQuery) => {
    return request({
        url: '/system/dept/list',
        method: 'get',
        params: query
    });
};
// æŸ¥è¯¢éƒ¨é—¨åˆ—表(排除节点)
export const listDeptExcludeChild = (deptId: string | number): AxiosPromise<DeptVO[]> => {
    return request({
        url: '/system/dept/list/exclude/' + deptId,
        method: 'get'
    });
};
// æŸ¥è¯¢éƒ¨é—¨è¯¦ç»†
export const getDept = (deptId: string | number): AxiosPromise<DeptVO> => {
    return request({
        url: '/system/dept/' + deptId,
        method: 'get'
    });
};
// æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构
export const treeselect = (): AxiosPromise<DeptVO[]> => {
    return request({
        url: '/system/dept/treeselect',
        method: 'get'
    });
};
// æ–°å¢žéƒ¨é—¨
export const addDept = (data: DeptForm) => {
    return request({
        url: '/system/dept',
        method: 'post',
        data: data
    });
};
// ä¿®æ”¹éƒ¨é—¨
export const updateDept = (data: DeptForm) => {
    return request({
        url: '/system/dept',
        method: 'put',
        data: data
    });
};
// åˆ é™¤éƒ¨é—¨
export const delDept = (deptId: number | string) => {
    return request({
        url: '/system/dept/' + deptId,
        method: 'delete'
    });
};
src/api/system/dept/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
/**
 * éƒ¨é—¨æŸ¥è¯¢å‚æ•°
 */
export interface DeptQuery extends PageQuery {
    deptName?: string;
    status?: number;
}
/**
 * éƒ¨é—¨ç±»åž‹
 */
export interface DeptVO extends BaseEntity {
    id: number | string;
    parentName: string;
    parentId: number | string;
    children: DeptVO[];
    deptId: number | string;
    deptName: string;
    orderNum: number;
    leader: string;
    phone: string;
    email: string;
    status: string;
    delFlag: string;
    ancestors: string;
    menuId: string | number;
}
/**
 * éƒ¨é—¨è¡¨å•类型
 */
export interface DeptForm {
    parentName?: string;
    parentId?: number | string;
    children?: DeptForm[];
    deptId?: number | string;
    deptName?: string;
    orderNum?: number;
    leader?: string;
    phone?: string;
    email?: string;
    status?: string;
    delFlag?: string;
    ancestors?: string;
}
src/api/system/dict/data.js
ÎļþÒÑɾ³ý
src/api/system/dict/data/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DictDataForm, DictDataQuery, DictDataVO } from './types';
// æ ¹æ®å­—典类型查询字典数据信息
export function getDicts(dictType: string): AxiosPromise<DictDataVO[]> {
    return request({
        url: '/system/dict/data/type/' + dictType,
        method: 'get'
    });
}
// æŸ¥è¯¢å­—典数据列表
export function listData(query: DictDataQuery): AxiosPromise<DictDataVO[]> {
    return request({
        url: '/system/dict/data/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢å­—典数据详细
export function getData(dictCode: string | number): AxiosPromise<DictDataVO> {
    return request({
        url: '/system/dict/data/' + dictCode,
        method: 'get'
    });
}
// æ–°å¢žå­—典数据
export function addData(data: DictDataForm) {
    return request({
        url: '/system/dict/data',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹å­—典数据
export function updateData(data: DictDataForm) {
    return request({
        url: '/system/dict/data',
        method: 'put',
        data: data
    });
}
// åˆ é™¤å­—典数据
export function delData(dictCode: string | number | Array<string | number>) {
    return request({
        url: '/system/dict/data/' + dictCode,
        method: 'delete'
    });
}
src/api/system/dict/data/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
export interface DictDataQuery extends PageQuery {
    dictName: string;
    dictType: string;
    status: string;
    dictLabel: string;
}
export interface DictDataVO extends BaseEntity {
    dictCode: string;
    dictLabel: string;
    dictValue: string;
    cssClass: string;
    listClass: ElTagType;
    dictSort: number;
    status: string;
    remark: string;
}
export interface DictDataForm {
    dictType?: string;
    dictCode: string | undefined;
    dictLabel: string;
    dictValue: string;
    cssClass: string;
    listClass: ElTagType;
    dictSort: number;
    status: string;
    remark: string;
}
src/api/system/dict/type.js
ÎļþÒÑɾ³ý
src/api/system/dict/type/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
import request from '@/utils/request';
import { DictTypeForm, DictTypeVO, DictTypeQuery } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢å­—典类型列表
export function listType(query: DictTypeQuery): AxiosPromise<DictTypeVO[]> {
    return request({
        url: '/system/dict/type/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢å­—典类型详细
export function getType(dictId: number | string): AxiosPromise<DictTypeVO> {
    return request({
        url: '/system/dict/type/' + dictId,
        method: 'get'
    });
}
// æ–°å¢žå­—典类型
export function addType(data: DictTypeForm) {
    return request({
        url: '/system/dict/type',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹å­—典类型
export function updateType(data: DictTypeForm) {
    return request({
        url: '/system/dict/type',
        method: 'put',
        data: data
    });
}
// åˆ é™¤å­—典类型
export function delType(dictId: string | number | Array<string | number>) {
    return request({
        url: '/system/dict/type/' + dictId,
        method: 'delete'
    });
}
// åˆ·æ–°å­—典缓存
export function refreshCache() {
    return request({
        url: '/system/dict/type/refreshCache',
        method: 'delete'
    });
}
// èŽ·å–å­—å…¸é€‰æ‹©æ¡†åˆ—è¡¨
export function optionselect(): AxiosPromise<DictTypeVO[]> {
    return request({
        url: '/system/dict/type/optionselect',
        method: 'get'
    });
}
src/api/system/dict/type/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
export interface DictTypeVO extends BaseEntity {
    dictId: number | string;
    dictName: string;
    dictType: string;
    status: string;
    remark: string;
}
export interface DictTypeForm {
    dictId: number | string | undefined;
    dictName: string;
    dictType: string;
    status: string;
    remark: string;
}
export interface DictTypeQuery extends PageQuery {
    dictName: string;
    dictType: string;
    status: string;
}
src/api/system/menu.js
ÎļþÒÑɾ³ý
src/api/system/menu/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { MenuQuery, MenuVO, MenuForm, MenuTreeOption, RoleMenuTree } from './types';
// æŸ¥è¯¢èœå•列表
export const listMenu = (query?: MenuQuery): AxiosPromise<MenuVO[]> => {
    return request({
        url: '/system/menu/list',
        method: 'get',
        params: query
    });
};
// æŸ¥è¯¢èœå•详细
export const getMenu = (menuId: string | number): AxiosPromise<MenuVO> => {
    return request({
        url: '/system/menu/' + menuId,
        method: 'get'
    });
};
// æŸ¥è¯¢èœå•下拉树结构
export const treeselect = (): AxiosPromise<MenuTreeOption[]> => {
    return request({
        url: '/system/menu/treeselect',
        method: 'get'
    });
};
// æ ¹æ®è§’色ID查询菜单下拉树结构
export const roleMenuTreeselect = (roleId: string | number): AxiosPromise<RoleMenuTree> => {
    return request({
        url: '/system/menu/roleMenuTreeselect/' + roleId,
        method: 'get'
    });
};
// æ ¹æ®è§’色ID查询菜单下拉树结构
export const tenantPackageMenuTreeselect = (packageId: string | number): AxiosPromise<RoleMenuTree> => {
    return request({
        url: '/system/menu/tenantPackageMenuTreeselect/' + packageId,
        method: 'get'
    });
};
// æ–°å¢žèœå•
export const addMenu = (data: MenuForm) => {
    return request({
        url: '/system/menu',
        method: 'post',
        data: data
    });
};
// ä¿®æ”¹èœå•
export const updateMenu = (data: MenuForm) => {
    return request({
        url: '/system/menu',
        method: 'put',
        data: data
    });
};
// åˆ é™¤èœå•
export const delMenu = (menuId: string | number) => {
    return request({
        url: '/system/menu/' + menuId,
        method: 'delete'
    });
};
src/api/system/menu/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
/**
 * èœå•树形结构类型
 */
export interface MenuTreeOption {
    id: string | number;
    label: string;
    parentId: string | number;
    weight: number;
    children?: MenuTreeOption[];
}
export interface RoleMenuTree {
    menus: MenuTreeOption[];
    checkedKeys: string[];
}
/**
 * èœå•查询参数类型
 */
export interface MenuQuery {
    keywords?: string;
    menuName?: string;
    status?: string;
}
/**
 * èœå•视图对象类型
 */
export interface MenuVO extends BaseEntity {
    parentName: string;
    parentId: string | number;
    children: MenuVO[];
    menuId: string | number;
    menuName: string;
    orderNum: number;
    path: string;
    component: string;
    queryParam: string;
    isFrame: string;
    isCache: string;
    menuType: MenuTypeEnum;
    visible: string;
    status: string;
    icon: string;
    remark: string;
}
export interface MenuForm {
    parentName?: string;
    parentId?: string | number;
    children?: MenuForm[];
    menuId?: string | number;
    menuName: string;
    orderNum: number;
    path: string;
    component?: string;
    queryParam?: string;
    isFrame?: string;
    isCache?: string;
    menuType?: MenuTypeEnum;
    visible?: string;
    status?: string;
    icon?: string;
    remark?: string;
    query?: string;
    perms?: string;
}
src/api/system/notice.js
ÎļþÒÑɾ³ý
src/api/system/notice/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
import request from '@/utils/request';
import { NoticeForm, NoticeQuery, NoticeVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢å…¬å‘Šåˆ—表
export function listNotice(query: NoticeQuery): AxiosPromise<NoticeVO[]> {
    return request({
        url: '/system/notice/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢å…¬å‘Šè¯¦ç»†
export function getNotice(noticeId: string | number): AxiosPromise<NoticeVO> {
    return request({
        url: '/system/notice/' + noticeId,
        method: 'get'
    });
}
// æ–°å¢žå…¬å‘Š
export function addNotice(data: NoticeForm) {
    return request({
        url: '/system/notice',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹å…¬å‘Š
export function updateNotice(data: NoticeForm) {
    return request({
        url: '/system/notice',
        method: 'put',
        data: data
    });
}
// åˆ é™¤å…¬å‘Š
export function delNotice(noticeId: string | number | Array<string | number>) {
    return request({
        url: '/system/notice/' + noticeId,
        method: 'delete'
    });
}
src/api/system/notice/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
export interface NoticeVO extends BaseEntity {
    noticeId: number;
    noticeTitle: string;
    noticeType: string;
    noticeContent: string;
    status: string;
    remark: string;
    createByName: string;
}
export interface NoticeQuery extends PageQuery {
    noticeTitle: string;
    createByName: string;
    status: string;
    noticeType: string;
}
export interface NoticeForm {
    noticeId: number | string | undefined;
    noticeTitle: string;
    noticeType: string;
    noticeContent: string;
    status: string;
    remark: string;
    createByName: string;
}
src/api/system/oss.js
ÎļþÒÑɾ³ý
src/api/system/oss/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import request from '@/utils/request';
import { OssQuery, OssVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢OSS对象存储列表
export function listOss(query: OssQuery): AxiosPromise<OssVO[]> {
    return request({
        url: '/system/oss/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢OSS对象基于id串
export function listByIds(ossId: string | number): AxiosPromise<OssVO[]> {
    return request({
        url: '/system/oss/listByIds/' + ossId,
        method: 'get'
    });
}
// åˆ é™¤OSS对象存储
export function delOss(ossId: string | number | Array<string | number>) {
    return request({
        url: '/system/oss/' + ossId,
        method: 'delete'
    });
}
src/api/system/oss/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
export interface OssVO extends BaseEntity {
    ossId: string | number;
    fileName: string;
    originalName: string;
    fileSuffix: string;
    url: string;
    createByName: string;
    service: string;
}
export interface OssQuery extends PageQuery {
    fileName: string;
    originalName: string;
    fileSuffix: string;
    createTime: string;
    service: string;
    orderByColumn: string;
    isAsc: string;
}
export interface OssForm {
    file: undefined | string;
}
src/api/system/ossConfig.js
ÎļþÒÑɾ³ý
src/api/system/ossConfig/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import request from '@/utils/request';
import { OssConfigForm, OssConfigQuery, OssConfigVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢å¯¹è±¡å­˜å‚¨é…ç½®åˆ—表
export function listOssConfig(query: OssConfigQuery): AxiosPromise<OssConfigVO[]> {
    return request({
        url: '/system/oss/config/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢å¯¹è±¡å­˜å‚¨é…ç½®è¯¦ç»†
export function getOssConfig(ossConfigId: string | number): AxiosPromise<OssConfigVO> {
    return request({
        url: '/system/oss/config/' + ossConfigId,
        method: 'get'
    });
}
// æ–°å¢žå¯¹è±¡å­˜å‚¨é…ç½®
export function addOssConfig(data: OssConfigForm) {
    return request({
        url: '/system/oss/config',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹å¯¹è±¡å­˜å‚¨é…ç½®
export function updateOssConfig(data: OssConfigForm) {
    return request({
        url: '/system/oss/config',
        method: 'put',
        data: data
    });
}
// åˆ é™¤å¯¹è±¡å­˜å‚¨é…ç½®
export function delOssConfig(ossConfigId: string | number | Array<string | number>) {
    return request({
        url: '/system/oss/config/' + ossConfigId,
        method: 'delete'
    });
}
// å¯¹è±¡å­˜å‚¨çŠ¶æ€ä¿®æ”¹
export function changeOssConfigStatus(ossConfigId: string | number, status: string, configKey: string) {
    const data = {
        ossConfigId,
        status,
        configKey
    };
    return request({
        url: '/system/oss/config/changeStatus',
        method: 'put',
        data: data
    });
}
src/api/system/ossConfig/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
export interface OssConfigVO extends BaseEntity {
    ossConfigId: number | string;
    configKey: string;
    accessKey: string;
    secretKey: string;
    bucketName: string;
    prefix: string;
    endpoint: string;
    domain: string;
    isHttps: string;
    region: string;
    status: string;
    ext1: string;
    remark: string;
    accessPolicy: string;
}
export interface OssConfigQuery extends PageQuery {
    configKey: string;
    bucketName: string;
    status: string;
}
export interface OssConfigForm {
    ossConfigId: string | number | undefined;
    configKey: string;
    accessKey: string;
    secretKey: string;
    bucketName: string;
    prefix: string;
    endpoint: string;
    domain: string;
    isHttps: string;
    accessPolicy: string;
    region: string;
    status: string;
    remark: string;
}
src/api/system/post.js
ÎļþÒÑɾ³ý
src/api/system/post/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
import request from '@/utils/request';
import { PostForm, PostQuery, PostVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢å²—位列表
export function listPost(query: PostQuery): AxiosPromise<PostVO[]> {
    return request({
        url: '/system/post/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢å²—位详细
export function getPost(postId: string | number): AxiosPromise<PostVO> {
    return request({
        url: '/system/post/' + postId,
        method: 'get'
    });
}
// æ–°å¢žå²—位
export function addPost(data: PostForm) {
    return request({
        url: '/system/post',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹å²—位
export function updatePost(data: PostForm) {
    return request({
        url: '/system/post',
        method: 'put',
        data: data
    });
}
// åˆ é™¤å²—位
export function delPost(postId: string | number | (string | number)[]) {
    return request({
        url: '/system/post/' + postId,
        method: 'delete'
    });
}
src/api/system/post/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
export interface PostVO extends BaseEntity {
    postId: number | string;
    postCode: string;
    postName: string;
    postSort: number;
    status: string;
    remark: string;
}
export interface PostForm {
    postId: number | string | undefined;
    postCode: string;
    postName: string;
    postSort: number;
    status: string;
    remark: string;
}
export interface PostQuery extends PageQuery {
    postCode: string;
    postName: string;
    status: string;
}
src/api/system/role.js
ÎļþÒÑɾ³ý
src/api/system/role/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
import { UserVO } from '@/api/system/user/types';
import { UserQuery } from '@/api/system/user/types';
import { AxiosPromise } from 'axios';
import { RoleQuery, RoleVO, RoleDeptTree } from './types';
import request from '@/utils/request';
export const listRole = (query: RoleQuery): AxiosPromise<RoleVO[]> => {
    return request({
        url: '/system/role/list',
        method: 'get',
        params: query
    });
};
/**
 * æŸ¥è¯¢è§’色详细
 */
export const getRole = (roleId: string | number): AxiosPromise<RoleVO> => {
    return request({
        url: '/system/role/' + roleId,
        method: 'get'
    });
};
/**
 * æ–°å¢žè§’色
 */
export const addRole = (data: any) => {
    return request({
        url: '/system/role',
        method: 'post',
        data: data
    });
};
/**
 * ä¿®æ”¹è§’色
 * @param data
 */
export const updateRole = (data: any) => {
    return request({
        url: '/system/role',
        method: 'put',
        data: data
    });
};
/**
 * è§’色数据权限
 */
export const dataScope = (data: any) => {
    return request({
        url: '/system/role/dataScope',
        method: 'put',
        data: data
    });
};
/**
 * è§’色状态修改
 */
export const changeRoleStatus = (roleId: string | number, status: string) => {
    const data = {
        roleId,
        status
    };
    return request({
        url: '/system/role/changeStatus',
        method: 'put',
        data: data
    });
};
/**
 * åˆ é™¤è§’色
 */
export const delRole = (roleId: Array<string | number> | string | number) => {
    return request({
        url: '/system/role/' + roleId,
        method: 'delete'
    });
};
/**
 * æŸ¥è¯¢è§’色已授权用户列表
 */
export const allocatedUserList = (query: UserQuery): AxiosPromise<UserVO[]> => {
    return request({
        url: '/system/role/authUser/allocatedList',
        method: 'get',
        params: query
    });
};
/**
 * æŸ¥è¯¢è§’色未授权用户列表
 */
export const unallocatedUserList = (query: UserQuery): AxiosPromise<UserVO[]> => {
    return request({
        url: '/system/role/authUser/unallocatedList',
        method: 'get',
        params: query
    });
};
/**
 * å–消用户授权角色
 */
export const authUserCancel = (data: any) => {
    return request({
        url: '/system/role/authUser/cancel',
        method: 'put',
        data: data
    });
};
/**
 * æ‰¹é‡å–消用户授权角色
 */
export const authUserCancelAll = (data: any) => {
    return request({
        url: '/system/role/authUser/cancelAll',
        method: 'put',
        params: data
    });
};
/**
 * æŽˆæƒç”¨æˆ·é€‰æ‹©
 */
export const authUserSelectAll = (data: any) => {
    return request({
        url: '/system/role/authUser/selectAll',
        method: 'put',
        params: data
    });
};
// æ ¹æ®è§’色ID查询部门树结构
export const deptTreeSelect = (roleId: string | number): AxiosPromise<RoleDeptTree> => {
    return request({
        url: '/system/role/deptTree/' + roleId,
        method: 'get'
    });
};
src/api/system/role/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
/**
 * èœå•树形结构类型
 */
export interface DeptTreeOption {
    id: string;
    label: string;
    parentId: string;
    weight: number;
    children?: DeptTreeOption[];
}
export interface RoleDeptTree {
    checkedKeys: string[];
    depts: DeptTreeOption[];
}
export interface RoleVO extends BaseEntity {
    roleId: string | number;
    roleName: string;
    roleKey: string;
    roleSort: number;
    dataScope: string;
    menuCheckStrictly: boolean;
    deptCheckStrictly: boolean;
    status: string;
    delFlag: string;
    remark?: any;
    flag: boolean;
    menuIds?: Array<string | number>;
    deptIds?: Array<string | number>;
    admin: boolean;
}
export interface RoleQuery extends PageQuery {
    roleName: string;
    roleKey: string;
    status: string;
}
export interface RoleForm {
    roleName: string;
    roleKey: string;
    roleSort: number;
    status: string;
    menuCheckStrictly: boolean;
    deptCheckStrictly: boolean;
    remark: string;
    dataScope?: number;
    roleId: string | undefined;
    menuIds: Array<string | number>;
    deptIds: Array<string | number>;
}
src/api/system/tenant.js
ÎļþÒÑɾ³ý
src/api/system/tenant/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
import request from '@/utils/request';
import { TenantForm, TenantQuery, TenantVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢ç§Ÿæˆ·åˆ—表
export function listTenant(query: TenantQuery): AxiosPromise<TenantVO[]> {
    return request({
        url: '/system/tenant/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢ç§Ÿæˆ·è¯¦ç»†
export function getTenant(id: string | number): AxiosPromise<TenantVO> {
    return request({
        url: '/system/tenant/' + id,
        method: 'get'
    });
}
// æ–°å¢žç§Ÿæˆ·
export function addTenant(data: TenantForm) {
    return request({
        url: '/system/tenant',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹ç§Ÿæˆ·
export function updateTenant(data: TenantForm) {
    return request({
        url: '/system/tenant',
        method: 'put',
        data: data
    });
}
// ç§Ÿæˆ·çŠ¶æ€ä¿®æ”¹
export function changeTenantStatus(id: string | number, tenantId: string | number, status: string) {
    const data = {
        id,
        tenantId,
        status
    };
    return request({
        url: '/system/tenant/changeStatus',
        method: 'put',
        data: data
    });
}
// åˆ é™¤ç§Ÿæˆ·
export function delTenant(id: string | number | Array<string | number>) {
    return request({
        url: '/system/tenant/' + id,
        method: 'delete'
    });
}
// åŠ¨æ€åˆ‡æ¢ç§Ÿæˆ·
export function dynamicTenant(tenantId: string | number) {
    return request({
        url: '/system/tenant/dynamic/' + tenantId,
        method: 'get'
    });
}
// æ¸…除动态租户
export function dynamicClear() {
    return request({
        url: '/system/tenant/dynamic/clear',
        method: 'get'
    });
}
// åŒæ­¥ç§Ÿæˆ·å¥—餐
export function syncTenantPackage(tenantId: string | number, packageId: string | number) {
    const data = {
        tenantId,
        packageId
    };
    return request({
        url: '/system/tenant/syncTenantPackage',
        method: 'get',
        params: data
    });
}
src/api/system/tenant/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
export interface TenantVO extends BaseEntity {
    id: number | string;
    tenantId: number | string;
    username: string;
    contactUserName: string;
    contactPhone: string;
    companyName: string;
    licenseNumber: string;
    address: string;
    domain: string;
    intro: string;
    remark: string;
    packageId: string | number;
    expireTime: string;
    accountCount: number;
    status: string;
}
export interface TenantQuery extends PageQuery {
    tenantId: string | number;
    contactUserName: string;
    contactPhone: string;
    companyName: string;
}
export interface TenantForm {
    id: number | string | undefined;
    tenantId: number | string | undefined;
    username: string;
    password: string;
    contactUserName: string;
    contactPhone: string;
    companyName: string;
    licenseNumber: string;
    domain: string;
    address: string;
    intro: string;
    remark: string;
    packageId: string | number;
    expireTime: string;
    accountCount: number;
    status: string;
}
src/api/system/tenantPackage.js
ÎļþÒÑɾ³ý
src/api/system/tenantPackage/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
import request from '@/utils/request';
import { TenantPkgForm, TenantPkgQuery, TenantPkgVO } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢ç§Ÿæˆ·å¥—餐列表
export function listTenantPackage(query?: TenantPkgQuery): AxiosPromise<TenantPkgVO[]> {
    return request({
        url: '/system/tenant/package/list',
        method: 'get',
        params: query
    });
}
// æŸ¥è¯¢ç§Ÿæˆ·å¥—餐详细
export function getTenantPackage(packageId: string | number): AxiosPromise<TenantPkgVO> {
    return request({
        url: '/system/tenant/package/' + packageId,
        method: 'get'
    });
}
// æ–°å¢žç§Ÿæˆ·å¥—餐
export function addTenantPackage(data: TenantPkgForm) {
    return request({
        url: '/system/tenant/package',
        method: 'post',
        data: data
    });
}
// ä¿®æ”¹ç§Ÿæˆ·å¥—餐
export function updateTenantPackage(data: TenantPkgForm) {
    return request({
        url: '/system/tenant/package',
        method: 'put',
        data: data
    });
}
// ç§Ÿæˆ·å¥—餐状态修改
export function changePackageStatus(packageId: number | string, status: string) {
    const data = {
        packageId,
        status
    };
    return request({
        url: '/system/tenant/package/changeStatus',
        method: 'put',
        data: data
    });
}
// åˆ é™¤ç§Ÿæˆ·å¥—餐
export function delTenantPackage(packageId: string | number | Array<string | number>) {
    return request({
        url: '/system/tenant/package/' + packageId,
        method: 'delete'
    });
}
src/api/system/tenantPackage/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
export interface TenantPkgVO extends BaseEntity {
    packageId: string | number;
    packageName: string;
    menuIds: string;
    remark: string;
    menuCheckStrictly: boolean;
    status: string;
}
export interface TenantPkgQuery extends PageQuery {
    packageName: string;
    status: string;
}
export interface TenantPkgForm {
    packageId: string | number | undefined;
    packageName: string;
    menuIds: string;
    remark: string;
    menuCheckStrictly: boolean;
    status: string;
}
src/api/system/user.js
ÎļþÒÑɾ³ý
src/api/system/user/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,180 @@
import { DeptVO } from './../dept/types';
import { RoleVO } from '@/api/system/role/types';
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { UserForm, UserQuery, UserVO, UserInfoVO } from './types';
import { parseStrEmpty } from '@/utils/ruoyi';
/**
 * æŸ¥è¯¢ç”¨æˆ·åˆ—表
 * @param query
 */
export function listUser(query: UserQuery): AxiosPromise<UserVO[]> {
    return request({
        url: '/system/user/list',
        method: 'get',
        params: query
    });
}
/**
 * èŽ·å–ç”¨æˆ·è¯¦æƒ…
 * @param userId
 */
export function getUser(userId?: string | number): AxiosPromise<UserInfoVO> {
    return request({
        url: '/system/user/' + parseStrEmpty(userId),
        method: 'get'
    });
}
/**
 * æ–°å¢žç”¨æˆ·
 */
export function addUser(data: UserForm) {
    return request({
        url: '/system/user',
        method: 'post',
        data: data
    });
}
/**
 * ä¿®æ”¹ç”¨æˆ·
 */
export function updateUser(data: UserForm) {
    return request({
        url: '/system/user',
        method: 'put',
        data: data
    });
}
/**
 * åˆ é™¤ç”¨æˆ·
 * @param userId ç”¨æˆ·ID
 */
export function delUser(userId: Array<string | number> | string | number) {
    return request({
        url: '/system/user/' + userId,
        method: 'delete'
    });
}
/**
 * ç”¨æˆ·å¯†ç é‡ç½®
 * @param userId ç”¨æˆ·ID
 * @param password å¯†ç 
 */
export function resetUserPwd(userId: string | number, password: string) {
    const data = {
        userId,
        password
    };
    return request({
        url: '/system/user/resetPwd',
        method: 'put',
        data: data
    });
}
/**
 * ç”¨æˆ·çŠ¶æ€ä¿®æ”¹
 * @param userId ç”¨æˆ·ID
 * @param status ç”¨æˆ·çŠ¶æ€
 */
export function changeUserStatus(userId: number | string, status: string) {
    const data = {
        userId,
        status
    };
    return request({
        url: '/system/user/changeStatus',
        method: 'put',
        data: data
    });
}
/**
 * æŸ¥è¯¢ç”¨æˆ·ä¸ªäººä¿¡æ¯
 */
export function getUserProfile(): AxiosPromise<UserInfoVO> {
    return request({
        url: '/system/user/profile',
        method: 'get'
    });
}
/**
 * ä¿®æ”¹ç”¨æˆ·ä¸ªäººä¿¡æ¯
 * @param data ç”¨æˆ·ä¿¡æ¯
 */
export function updateUserProfile(data: UserForm) {
    return request({
        url: '/system/user/profile',
        method: 'put',
        data: data
    });
}
/**
 * ç”¨æˆ·å¯†ç é‡ç½®
 * @param oldPassword æ—§å¯†ç 
 * @param newPassword æ–°å¯†ç 
 */
export function updateUserPwd(oldPassword: string, newPassword: string) {
    const data = {
        oldPassword,
        newPassword
    };
    return request({
        url: '/system/user/profile/updatePwd',
        method: 'put',
        params: data
    });
}
/**
 * ç”¨æˆ·å¤´åƒä¸Šä¼ 
 * @param data å¤´åƒæ–‡ä»¶
 */
export function uploadAvatar(data: FormData) {
    return request({
        url: '/system/user/profile/avatar',
        method: 'post',
        data: data
    });
}
/**
 * æŸ¥è¯¢æŽˆæƒè§’色
 * @param userId ç”¨æˆ·ID
 */
export function getAuthRole(userId: string | number): AxiosPromise<{ user: UserVO; roles: RoleVO[] }> {
    return request({
        url: '/system/user/authRole/' + userId,
        method: 'get'
    });
}
/**
 * ä¿å­˜æŽˆæƒè§’色
 * @param data ç”¨æˆ·ID
 */
export function updateAuthRole(data: { userId: string; roleIds: string }) {
    return request({
        url: '/system/user/authRole',
        method: 'put',
        params: data
    });
}
/**
 * æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构
 */
export function deptTreeSelect(): AxiosPromise<DeptVO[]> {
    return request({
        url: '/system/user/deptTree',
        method: 'get'
    });
}
src/api/system/user/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
import { DeptVO } from './../dept/types';
import { RoleVO } from '@/api/system/role/types';
import { PostVO } from '@/api/system/post/types';
/**
 * ç”¨æˆ·ä¿¡æ¯
 */
export interface UserInfo {
    user: UserVO;
    roles: string[];
    permissions: string[];
}
/**
 * ç”¨æˆ·æŸ¥è¯¢å¯¹è±¡ç±»åž‹
 */
export interface UserQuery extends PageQuery {
    userName?: string;
    phonenumber?: string;
    status?: string;
    deptId?: string | number;
    roleId?: string | number;
}
/**
 * ç”¨æˆ·è¿”回对象
 */
export interface UserVO extends BaseEntity {
    userId: string | number;
    deptId: number;
    userName: string;
    nickName: string;
    userType: string;
    email: string;
    phonenumber: string;
    sex: string;
    avatar: string;
    status: string;
    delFlag: string;
    loginIp: string;
    loginDate: string;
    remark: string;
    dept: DeptVO;
    roles: RoleVO[];
    roleIds: any;
    postIds: any;
    roleId: any;
    admin: boolean;
}
/**
 * ç”¨æˆ·è¡¨å•类型
 */
export interface UserForm {
    id?: string;
    userId?: string;
    deptId?: number;
    userName: string;
    nickName?: string;
    password: string;
    phonenumber?: string;
    email?: string;
    sex?: string;
    status: string;
    remark?: string;
    postIds: string[];
    roleIds: string[];
}
export interface UserInfoVO {
    user: UserVO;
    roles: RoleVO[];
    roleIds: string[];
    posts: PostVO[];
    postIds: string[];
    roleGroup: string;
    postGroup: string;
}
export interface ResetPwdForm {
    oldPassword: string;
    newPassword: string;
    confirmPassword: string;
}
src/api/tool/gen.js
ÎļþÒÑɾ³ý
src/api/tool/gen/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
import request from '@/utils/request';
import { DbTableQuery, DbTableVO, TableQuery, TableVO, GenTableVO, DbTableForm } from './types';
import { AxiosPromise } from 'axios';
// æŸ¥è¯¢ç”Ÿæˆè¡¨æ•°æ®
export const listTable = (query: TableQuery): AxiosPromise<TableVO[]> => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/list',
        method: 'get',
        params: query
    });
};
// æŸ¥è¯¢db数据库列表
export const listDbTable = (query: DbTableQuery): AxiosPromise<DbTableVO[]> => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/db/list',
        method: 'get',
        params: query
    });
};
// æŸ¥è¯¢è¡¨è¯¦ç»†ä¿¡æ¯
export const getGenTable = (tableId: string | number): AxiosPromise<GenTableVO> => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/' + tableId,
        method: 'get'
    });
};
// ä¿®æ”¹ä»£ç ç”Ÿæˆä¿¡æ¯
export const updateGenTable = (data: DbTableForm) => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen',
        method: 'put',
        data: data
    });
};
// å¯¼å…¥è¡¨
export const importTable = (data: { tables: string }) => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/importTable',
        method: 'post',
        params: data
    });
};
// é¢„览生成代码
export const previewTable = (tableId: string | number) => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/preview/' + tableId,
        method: 'get'
    });
};
// åˆ é™¤è¡¨æ•°æ®
export const delTable = (tableId: string | number | Array<string | number>) => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/' + tableId,
        method: 'delete'
    });
};
// ç”Ÿæˆä»£ç ï¼ˆè‡ªå®šä¹‰è·¯å¾„)
export const genCode = (tableName: string) => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/genCode/' + tableName,
        method: 'get'
    });
};
// åŒæ­¥æ•°æ®åº“
export const synchDb = (tableName: string) => {
    return request({
        headers: { datasource: localStorage.getItem('dataName') },
        url: '/tool/gen/synchDb/' + tableName,
        method: 'get'
    });
};
src/api/tool/gen/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,178 @@
export interface TableVO extends BaseEntity {
    createDept: number | string;
    tableId: string | number;
    tableName: string;
    tableComment: string;
    subTableName?: any;
    subTableFkName?: any;
    className: string;
    tplCategory: string;
    packageName: string;
    moduleName: string;
    businessName: string;
    functionName: string;
    functionAuthor: string;
    genType: string;
    genPath: string;
    pkColumn?: any;
    columns?: any;
    options?: any;
    remark?: any;
    treeCode?: any;
    treeParentCode?: any;
    treeName?: any;
    menuIds?: any;
    parentMenuId?: any;
    parentMenuName?: any;
    tree: boolean;
    crud: boolean;
}
export interface TableQuery extends PageQuery {
    tableName: string;
    tableComment: string;
    dataName: string;
}
export interface DbColumnVO extends BaseEntity {
    createDept?: any;
    columnId?: any;
    tableId?: any;
    columnName?: any;
    columnComment?: any;
    columnType?: any;
    javaType?: any;
    javaField?: any;
    isPk?: any;
    isIncrement?: any;
    isRequired?: any;
    isInsert?: any;
    isEdit?: any;
    isList?: any;
    isQuery?: any;
    queryType?: any;
    htmlType?: any;
    dictType?: any;
    sort?: any;
    increment: boolean;
    capJavaField?: any;
    usableColumn: boolean;
    superColumn: boolean;
    list: boolean;
    pk: boolean;
    insert: boolean;
    edit: boolean;
    query: boolean;
    required: boolean;
}
export interface DbTableVO {
    createDept?: any;
    tableId?: any;
    tableName: string;
    tableComment: string;
    subTableName?: any;
    subTableFkName?: any;
    className?: any;
    tplCategory?: any;
    packageName?: any;
    moduleName?: any;
    businessName?: any;
    functionName?: any;
    functionAuthor?: any;
    genType?: any;
    genPath?: any;
    pkColumn?: any;
    columns: DbColumnVO[];
    options?: any;
    remark?: any;
    treeCode?: any;
    treeParentCode?: any;
    treeName?: any;
    menuIds?: any;
    parentMenuId?: any;
    parentMenuName?: any;
    tree: boolean;
    crud: boolean;
}
export interface DbTableQuery extends PageQuery {
    tableName: string;
    tableComment: string;
}
export interface GenTableVO {
    info: DbTableVO;
    rows: DbColumnVO[];
    tables: DbTableVO[];
}
export interface DbColumnForm extends BaseEntity {
    createDept: number;
    columnId: string;
    tableId: string;
    columnName: string;
    columnComment: string;
    columnType: string;
    javaType: string;
    javaField: string;
    isPk: string;
    isIncrement: string;
    isRequired: string;
    isInsert?: any;
    isEdit: string;
    isList: string;
    isQuery?: any;
    queryType: string;
    htmlType: string;
    dictType: string;
    sort: number;
    increment: boolean;
    capJavaField: string;
    usableColumn: boolean;
    superColumn: boolean;
    list: boolean;
    pk: boolean;
    insert: boolean;
    edit: boolean;
    query: boolean;
    required: boolean;
}
export interface DbParamForm {
    treeCode?: any;
    treeName?: any;
    treeParentCode?: any;
    parentMenuId: string;
}
export interface DbTableForm extends BaseEntity {
    createDept?: any;
    tableId: string | string;
    tableName: string;
    tableComment: string;
    subTableName?: any;
    subTableFkName?: any;
    className: string;
    tplCategory: string;
    packageName: string;
    moduleName: string;
    businessName: string;
    functionName: string;
    functionAuthor: string;
    genType: string;
    genPath: string;
    pkColumn?: any;
    columns: DbColumnForm[];
    options: string;
    remark?: any;
    treeCode?: any;
    treeParentCode?: any;
    treeName?: any;
    menuIds?: any;
    parentMenuId: string;
    parentMenuName?: any;
    tree: boolean;
    crud: boolean;
    params: DbParamForm;
}
src/api/types.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
/**
 * æ³¨å†Œ
 */
export type RegisterForm = {
    tenantId: string;
    username: string;
    password: string;
    confirmPassword?: string;
    code?: string;
    uuid?: string;
    userType?: string;
};
/**
 * ç™»å½•请求
 */
export interface LoginData {
    tenantId: string;
    username: string;
    password: string;
    rememberMe?: boolean;
    code?: string;
    uuid?: string;
}
/**
 * ç™»å½•响应
 */
export interface LoginResult {
    token: string;
}
/**
 * éªŒè¯ç è¿”回
 */
export interface VerifyCodeResult {
    captchaEnabled: boolean;
    uuid?: string;
    img?: string;
}
/**
 * ç§Ÿæˆ·
 */
export interface TenantVO {
    companyName: string;
    domain: any;
    tenantId: string;
}
export interface TenantInfo {
    tenantEnabled: boolean;
    voList: TenantVO[];
}
src/assets/images/login-background.jpg

src/assets/images/profile.jpg

src/assets/logo/logo.png

src/assets/styles/btn.scss
@@ -1,99 +1,99 @@
@import './variables.module.scss';
@mixin colorBtn($color) {
  background: $color;
    background: $color;
  &:hover {
    color: $color;
    &:hover {
        color: $color;
    &:before,
    &:after {
      background: $color;
    }
  }
        &:before,
        &:after {
            background: $color;
        }
    }
}
.blue-btn {
  @include colorBtn($blue)
    @include colorBtn($blue);
}
.light-blue-btn {
  @include colorBtn($light-blue)
    @include colorBtn($light-blue);
}
.red-btn {
  @include colorBtn($red)
    @include colorBtn($red);
}
.pink-btn {
  @include colorBtn($pink)
    @include colorBtn($pink);
}
.green-btn {
  @include colorBtn($green)
    @include colorBtn($green);
}
.tiffany-btn {
  @include colorBtn($tiffany)
    @include colorBtn($tiffany);
}
.yellow-btn {
  @include colorBtn($yellow)
    @include colorBtn($yellow);
}
.pan-btn {
  font-size: 14px;
  color: #fff;
  padding: 14px 36px;
  border-radius: 8px;
  border: none;
  outline: none;
  transition: 600ms ease all;
  position: relative;
  display: inline-block;
    font-size: 14px;
    color: #fff;
    padding: 14px 36px;
    border-radius: 8px;
    border: none;
    outline: none;
    transition: 600ms ease all;
    position: relative;
    display: inline-block;
  &:hover {
    background: #fff;
    &:hover {
        background: #fff;
    &:before,
    &:after {
      width: 100%;
      transition: 600ms ease all;
    }
  }
        &:before,
        &:after {
            width: 100%;
            transition: 600ms ease all;
        }
    }
  &:before,
  &:after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    height: 2px;
    width: 0;
    transition: 400ms ease all;
  }
    &:before,
    &:after {
        content: '';
        position: absolute;
        top: 0;
        right: 0;
        height: 2px;
        width: 0;
        transition: 400ms ease all;
    }
  &::after {
    right: inherit;
    top: inherit;
    left: 0;
    bottom: 0;
  }
    &::after {
        right: inherit;
        top: inherit;
        left: 0;
        bottom: 0;
    }
}
.custom-button {
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  color: #fff;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  outline: 0;
  margin: 0;
  padding: 10px 15px;
  font-size: 14px;
  border-radius: 4px;
    display: inline-block;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    background: #fff;
    color: #fff;
    -webkit-appearance: none;
    text-align: center;
    box-sizing: border-box;
    outline: 0;
    margin: 0;
    padding: 10px 15px;
    font-size: 14px;
    border-radius: 4px;
}
src/assets/styles/element-ui.scss
@@ -1,96 +1,101 @@
// cover some element-ui styles
.el-divider--horizontal {
    margin-bottom: 10px;
    margin-top: 10px;
}
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
  font-weight: 400 !important;
    font-weight: 400 !important;
}
.el-upload {
  input[type="file"] {
    display: none !important;
  }
    input[type='file'] {
        display: none !important;
    }
}
.el-upload__input {
  display: none;
    display: none;
}
.cell {
  .el-tag {
    margin-right: 0px;
  }
    .el-tag {
        margin-right: 0px;
    }
}
.small-padding {
  .cell {
    padding-left: 5px;
    padding-right: 5px;
  }
    .cell {
        padding-left: 5px;
        padding-right: 5px;
    }
}
.fixed-width {
  .el-button--mini {
    padding: 7px 10px;
    width: 60px;
  }
    .el-button--mini {
        padding: 7px 10px;
        width: 60px;
    }
}
.status-col {
  .cell {
    padding: 0 10px;
    text-align: center;
    .cell {
        padding: 0 10px;
        text-align: center;
    .el-tag {
      margin-right: 0px;
    }
  }
        .el-tag {
            margin-right: 0px;
        }
    }
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
  transform: none;
  left: 0;
  position: relative;
  margin: 0 auto;
    transform: none;
    left: 0;
    position: relative;
    margin: 0 auto;
}
// refine element ui upload
.upload-container {
  .el-upload {
    width: 100%;
    .el-upload {
        width: 100%;
    .el-upload-dragger {
      width: 100%;
      height: 200px;
    }
  }
        .el-upload-dragger {
            width: 100%;
            height: 200px;
        }
    }
}
// dropdown
.el-dropdown-menu {
  a {
    display: block
  }
    a {
        display: block;
    }
}
// fix date-picker ui bug in filter-item
.el-range-editor.el-input__inner {
  display: inline-flex !important;
    display: inline-flex !important;
}
// to fix el-date-picker css style
.el-range-separator {
  box-sizing: content-box;
    box-sizing: content-box;
}
.el-menu--collapse
  > div
  > .el-submenu
  > .el-submenu__title
  .el-submenu__icon-arrow {
  display: none;
.el-menu--collapse > div > .el-submenu > .el-submenu__title .el-submenu__icon-arrow {
    display: none;
}
.el-dropdown .el-dropdown-link{
  color: var(--el-color-primary) !important;
}
.el-dropdown .el-dropdown-link {
    color: var(--el-color-primary) !important;
}
.el-dialog {
    border-radius: 3% !important;
}
src/assets/styles/index.scss
@@ -5,189 +5,203 @@
@import './sidebar.scss';
@import './btn.scss';
@import './ruoyi.scss';
@import 'animate.css';
//@import 'element-plus/dist/index.css';
body {
  height: 100%;
  margin: 0;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
    height: 100%;
    margin: 0;
    -moz-osx-font-smoothing: grayscale;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
  font-weight: 700;
    font-weight: 700;
}
html {
  height: 100%;
  box-sizing: border-box;
    height: 100%;
    box-sizing: border-box;
}
#app {
  height: 100%;
    height: 100%;
}
*,
*:before,
*:after {
  box-sizing: inherit;
    box-sizing: inherit;
}
.no-padding {
  padding: 0px !important;
    padding: 0px !important;
}
.padding-content {
  padding: 4px 0;
    padding: 4px 0;
}
a:focus,
a:active {
  outline: none;
    outline: none;
}
a,
a:focus,
a:hover {
  cursor: pointer;
  color: inherit;
  text-decoration: none;
    cursor: pointer;
    color: inherit;
    text-decoration: none;
}
div:focus {
  outline: none;
    outline: none;
}
.fr {
  float: right;
    float: right;
}
.fl {
  float: left;
    float: left;
}
.pr-5 {
  padding-right: 5px;
    padding-right: 5px;
}
.pl-5 {
  padding-left: 5px;
    padding-left: 5px;
}
.block {
  display: block;
    display: block;
}
.pointer {
  cursor: pointer;
    cursor: pointer;
}
.inlineBlock {
  display: block;
    display: block;
}
.clearfix {
  &:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0;
  }
    &:after {
        visibility: hidden;
        display: block;
        font-size: 0;
        content: ' ';
        clear: both;
        height: 0;
    }
}
aside {
  background: #eef1f6;
  padding: 8px 24px;
  margin-bottom: 20px;
  border-radius: 2px;
  display: block;
  line-height: 32px;
  font-size: 16px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
  color: #2c3e50;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
    background: #eef1f6;
    padding: 8px 24px;
    margin-bottom: 20px;
    border-radius: 2px;
    display: block;
    line-height: 32px;
    font-size: 16px;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
        sans-serif;
    color: #2c3e50;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  a {
    color: #337ab7;
    cursor: pointer;
    a {
        color: #337ab7;
        cursor: pointer;
    &:hover {
      color: rgb(32, 160, 255);
    }
  }
        &:hover {
            color: rgb(32, 160, 255);
        }
    }
}
//main-container全局样式
.app-container {
  padding: 20px;
    padding: 20px;
}
// search面板样式
.panel,
.search {
    margin-bottom: 0.75rem;
    border-radius: 0.25rem;
    border: 1px solid var(--el-border-color-light);
    background-color: var(--el-bg-color-overlay);
    padding: 0.75rem;
    --tw-shadow: var(--el-box-shadow-light);
    --tw-shadow-colored: var(--el-box-shadow-light);
    box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.components-container {
  margin: 30px 50px;
  position: relative;
    margin: 30px 50px;
    position: relative;
}
.pagination-container {
  margin-top: 30px;
    margin-top: 30px;
}
.text-center {
  text-align: center
    text-align: center;
}
.sub-navbar {
  height: 50px;
  line-height: 50px;
  position: relative;
  width: 100%;
  text-align: right;
  padding-right: 20px;
  transition: 600ms ease position;
  background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
    height: 50px;
    line-height: 50px;
    position: relative;
    width: 100%;
    text-align: right;
    padding-right: 20px;
    transition: 600ms ease position;
    background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
  .subtitle {
    font-size: 20px;
    color: #fff;
  }
    .subtitle {
        font-size: 20px;
        color: #fff;
    }
  &.draft {
    background: #d0d0d0;
  }
    &.draft {
        background: #d0d0d0;
    }
  &.deleted {
    background: #d0d0d0;
  }
    &.deleted {
        background: #d0d0d0;
    }
}
.link-type,
.link-type:focus {
  color: #337ab7;
  cursor: pointer;
    color: #337ab7;
    cursor: pointer;
  &:hover {
    color: rgb(32, 160, 255);
  }
    &:hover {
        color: rgb(32, 160, 255);
    }
}
.filter-container {
  padding-bottom: 10px;
    padding-bottom: 10px;
  .filter-item {
    display: inline-block;
    vertical-align: middle;
    margin-bottom: 10px;
  }
    .filter-item {
        display: inline-block;
        vertical-align: middle;
        margin-bottom: 10px;
    }
}
//refine vue-multiselect plugin
.multiselect {
  line-height: 16px;
    line-height: 16px;
}
.multiselect--active {
  z-index: 1000 !important;
    z-index: 1000 !important;
}
src/assets/styles/mixin.scss
@@ -1,66 +1,60 @@
@mixin clearfix {
  &:after {
    content: "";
    display: table;
    clear: both;
  }
    &:after {
        content: '';
        display: table;
        clear: both;
    }
}
@mixin scrollBar {
  &::-webkit-scrollbar-track-piece {
    background: #d3dce6;
  }
    &::-webkit-scrollbar-track-piece {
        background: #d3dce6;
    }
  &::-webkit-scrollbar {
    width: 6px;
  }
    &::-webkit-scrollbar {
        width: 6px;
    }
  &::-webkit-scrollbar-thumb {
    background: #99a9bf;
    border-radius: 20px;
  }
    &::-webkit-scrollbar-thumb {
        background: #99a9bf;
        border-radius: 20px;
    }
}
@mixin relative {
  position: relative;
  width: 100%;
  height: 100%;
    position: relative;
    width: 100%;
    height: 100%;
}
@mixin pct($pct) {
  width: #{$pct};
  position: relative;
  margin: 0 auto;
    width: #{$pct};
    position: relative;
    margin: 0 auto;
}
@mixin triangle($width, $height, $color, $direction) {
  $width: $width/2;
  $color-border-style: $height solid $color;
  $transparent-border-style: $width solid transparent;
  height: 0;
  width: 0;
    $width: $width/2;
    $color-border-style: $height solid $color;
    $transparent-border-style: $width solid transparent;
    height: 0;
    width: 0;
  @if $direction==up {
    border-bottom: $color-border-style;
    border-left: $transparent-border-style;
    border-right: $transparent-border-style;
  }
  @else if $direction==right {
    border-left: $color-border-style;
    border-top: $transparent-border-style;
    border-bottom: $transparent-border-style;
  }
  @else if $direction==down {
    border-top: $color-border-style;
    border-left: $transparent-border-style;
    border-right: $transparent-border-style;
  }
  @else if $direction==left {
    border-right: $color-border-style;
    border-top: $transparent-border-style;
    border-bottom: $transparent-border-style;
  }
    @if $direction==up {
        border-bottom: $color-border-style;
        border-left: $transparent-border-style;
        border-right: $transparent-border-style;
    } @else if $direction==right {
        border-left: $color-border-style;
        border-top: $transparent-border-style;
        border-bottom: $transparent-border-style;
    } @else if $direction==down {
        border-top: $color-border-style;
        border-left: $transparent-border-style;
        border-right: $transparent-border-style;
    } @else if $direction==left {
        border-right: $color-border-style;
        border-top: $transparent-border-style;
        border-bottom: $transparent-border-style;
    }
}
src/assets/styles/ruoyi.scss
@@ -1,9 +1,9 @@
 /**
/**
 * é€šç”¨css样式布局处理
 * Copyright (c) 2019 ruoyi
 */
 /** åŸºç¡€é€šç”¨ **/
/** åŸºç¡€é€šç”¨ **/
.pt5 {
    padding-top: 5px;
}
@@ -53,7 +53,18 @@
    margin-left: 20px;
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
.h1,
.h2,
.h3,
.h4,
.h5,
.h6,
h1,
h2,
h3,
h4,
h5,
h6 {
    font-family: inherit;
    font-weight: 500;
    line-height: 1.1;
@@ -75,7 +86,8 @@
}
.el-table {
    .el-table__header-wrapper, .el-table__fixed-header-wrapper {
    .el-table__header-wrapper,
    .el-table__fixed-header-wrapper {
        th {
            word-break: break-word;
            background-color: #f8f8f9 !important;
@@ -85,7 +97,7 @@
        }
    }
    .el-table__body-wrapper {
        .el-button [class*="el-icon-"] + span {
        .el-button [class*='el-icon-'] + span {
            margin-left: 1px;
        }
    }
@@ -93,11 +105,11 @@
/** è¡¨å•布局 **/
.form-header {
    font-size:15px;
    color:#6379bb;
    border-bottom:1px solid #ddd;
    margin:8px 10px 25px 10px;
    padding-bottom:5px
    font-size: 15px;
    color: #6379bb;
    border-bottom: 1px solid #ddd;
    margin: 8px 10px 25px 10px;
    padding-bottom: 5px;
}
/** è¡¨æ ¼å¸ƒå±€ **/
@@ -111,25 +123,25 @@
/* tree border */
.tree-border {
    margin-top: 5px;
    border: 1px solid #e5e6e7;
    background: #FFFFFF none;
    border-radius:4px;
    width: 100%;
    margin-top: 5px;
    border: 1px solid #e5e6e7;
    background: #ffffff none;
    border-radius: 4px;
    width: 100%;
}
.pagination-container .el-pagination {
    right: 0;
    position: absolute;
    //right: 0;
    //position: absolute;
}
@media ( max-width : 768px) {
  .pagination-container .el-pagination > .el-pagination__jump {
    display: none !important;
  }
  .pagination-container .el-pagination > .el-pagination__sizes {
    display: none !important;
  }
@media (max-width: 768px) {
    .pagination-container .el-pagination > .el-pagination__jump {
        display: none !important;
    }
    .pagination-container .el-pagination > .el-pagination__sizes {
        display: none !important;
    }
}
.el-table .fixed-width .el-button--small {
@@ -141,11 +153,12 @@
/** è¡¨æ ¼æ›´å¤šæ“ä½œä¸‹æ‹‰æ ·å¼ */
.el-table .el-dropdown-link {
    cursor: pointer;
    color: #409EFF;
    color: #409eff;
    margin-left: 10px;
}
.el-table .el-dropdown, .el-icon-arrow-down {
.el-table .el-dropdown,
.el-icon-arrow-down {
    font-size: 12px;
}
@@ -196,22 +209,22 @@
/* button color */
.el-button--cyan.is-active,
.el-button--cyan:active {
  background: #20B2AA;
  border-color: #20B2AA;
  color: #FFFFFF;
    background: #20b2aa;
    border-color: #20b2aa;
    color: #ffffff;
}
.el-button--cyan:focus,
.el-button--cyan:hover {
  background: #48D1CC;
  border-color: #48D1CC;
  color: #FFFFFF;
    background: #48d1cc;
    border-color: #48d1cc;
    color: #ffffff;
}
.el-button--cyan {
  background-color: #20B2AA;
  border-color: #20B2AA;
  color: #FFFFFF;
    background-color: #20b2aa;
    border-color: #20b2aa;
    color: #ffffff;
}
/* text color */
@@ -265,10 +278,10 @@
}
/* æ‹–拽列样式 */
.sortable-ghost{
    opacity: .8;
    color: #fff!important;
    background: #42b983!important;
.sortable-ghost {
    opacity: 0.8;
    color: #fff !important;
    background: #42b983 !important;
}
/* è¡¨æ ¼å³ä¾§å·¥å…·æ æ ·å¼ */
src/assets/styles/sidebar.scss
@@ -1,238 +1,236 @@
#app {
    .main-container {
        min-height: 100%;
        transition: margin-left 0.28s;
        margin-left: $base-sidebar-width;
        position: relative;
    }
  .main-container {
    min-height: 100%;
    transition: margin-left .28s;
    margin-left: $base-sidebar-width;
    position: relative;
  }
    .sidebarHide {
        margin-left: 0 !important;
    }
  .sidebarHide {
    margin-left: 0!important;
  }
    .sidebar-container {
        -webkit-transition: width 0.28s;
        transition: width 0.28s;
        width: $base-sidebar-width !important;
        background-color: $base-menu-background;
        height: 100%;
        position: fixed;
        font-size: 0px;
        top: 0;
        bottom: 0;
        left: 0;
        z-index: 1001;
        overflow: hidden;
        -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
        box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
  .sidebar-container {
    -webkit-transition: width .28s;
    transition: width 0.28s;
    width: $base-sidebar-width !important;
    background-color: $base-menu-background;
    height: 100%;
    position: fixed;
    font-size: 0px;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 1001;
    overflow: hidden;
    -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
    box-shadow: 2px 0 6px rgba(0,21,41,.35);
        // reset element-ui css
        .horizontal-collapse-transition {
            transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
        }
    // reset element-ui css
    .horizontal-collapse-transition {
      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
    }
        .scrollbar-wrapper {
            overflow-x: hidden !important;
        }
    .scrollbar-wrapper {
      overflow-x: hidden !important;
    }
        .el-scrollbar__bar.is-vertical {
            right: 0px;
        }
    .el-scrollbar__bar.is-vertical {
      right: 0px;
    }
        .el-scrollbar {
            height: 100%;
        }
    .el-scrollbar {
      height: 100%;
    }
        &.has-logo {
            .el-scrollbar {
                height: calc(100% - 50px);
            }
        }
    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 50px);
      }
    }
        .is-horizontal {
            display: none;
        }
    .is-horizontal {
      display: none;
    }
        a {
            display: inline-block;
            width: 100%;
            overflow: hidden;
        }
    a {
      display: inline-block;
      width: 100%;
      overflow: hidden;
    }
        .svg-icon {
            margin-right: 16px;
        }
    .svg-icon {
      margin-right: 16px;
    }
        .el-menu {
            border: none;
            height: 100%;
            width: 100% !important;
        }
    .el-menu {
      border: none;
      height: 100%;
      width: 100% !important;
    }
        .el-menu-item,
        .menu-title {
            overflow: hidden !important;
            text-overflow: ellipsis !important;
            white-space: nowrap !important;
        }
    .el-menu-item, .menu-title {
      overflow: hidden !important;
      text-overflow: ellipsis !important;
      white-space: nowrap !important;
    }
        .el-menu-item .el-menu-tooltip__trigger {
            display: inline-block !important;
        }
    .el-menu-item .el-menu-tooltip__trigger {
      display: inline-block !important;
    }
        // menu hover
        .sub-menu-title-noDropdown,
        .el-sub-menu__title {
            &:hover {
                background-color: rgba(0, 0, 0, 0.06) !important;
            }
        }
    // menu hover
    .sub-menu-title-noDropdown,
    .el-sub-menu__title {
      &:hover {
        background-color: rgba(0, 0, 0, 0.06) !important;
      }
    }
        & .theme-dark .is-active > .el-sub-menu__title {
            color: $base-menu-color-active !important;
        }
    & .theme-dark .is-active > .el-sub-menu__title {
      color: $base-menu-color-active !important;
    }
        & .nest-menu .el-sub-menu > .el-sub-menu__title,
        & .el-sub-menu .el-menu-item {
            min-width: $base-sidebar-width !important;
    & .nest-menu .el-sub-menu>.el-sub-menu__title,
    & .el-sub-menu .el-menu-item {
      min-width: $base-sidebar-width !important;
            &:hover {
                background-color: rgba(0, 0, 0, 0.06) !important;
            }
        }
      &:hover {
        background-color: rgba(0, 0, 0, 0.06) !important;
      }
    }
        & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
        & .theme-dark .el-sub-menu .el-menu-item {
            background-color: $base-sub-menu-background !important;
    & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
    & .theme-dark .el-sub-menu .el-menu-item {
      background-color: $base-sub-menu-background !important;
            &:hover {
                background-color: $base-sub-menu-hover !important;
            }
        }
    }
      &:hover {
        background-color: $base-sub-menu-hover !important;
      }
    }
  }
    .hideSidebar {
        .sidebar-container {
            width: 54px !important;
        }
  .hideSidebar {
    .sidebar-container {
      width: 54px !important;
    }
        .main-container {
            margin-left: 54px;
        }
    .main-container {
      margin-left: 54px;
    }
        .sub-menu-title-noDropdown {
            padding: 0 !important;
            position: relative;
    .sub-menu-title-noDropdown {
      padding: 0 !important;
      position: relative;
            .el-tooltip {
                padding: 0 !important;
      .el-tooltip {
        padding: 0 !important;
                .svg-icon {
                    margin-left: 20px;
                }
            }
        }
        .svg-icon {
          margin-left: 20px;
        }
      }
    }
        .el-sub-menu {
            overflow: hidden;
    .el-sub-menu {
      overflow: hidden;
            & > .el-sub-menu__title {
                padding: 0 !important;
      &>.el-sub-menu__title {
        padding: 0 !important;
                .svg-icon {
                    margin-left: 20px;
                }
            }
        }
        .svg-icon {
          margin-left: 20px;
        }
        .el-menu--collapse {
            .el-sub-menu {
                & > .el-sub-menu__title {
                    & > span {
                        height: 0;
                        width: 0;
                        overflow: hidden;
                        visibility: hidden;
                        display: inline-block;
                    }
                    & > i {
                        height: 0;
                        width: 0;
                        overflow: hidden;
                        visibility: hidden;
                        display: inline-block;
                    }
                }
            }
        }
    }
      }
    }
    .el-menu--collapse .el-menu .el-sub-menu {
        min-width: $base-sidebar-width !important;
    }
    .el-menu--collapse {
      .el-sub-menu {
        &>.el-sub-menu__title {
          &>span {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
          &>i {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
        }
      }
    }
  }
    // mobile responsive
    .mobile {
        .main-container {
            margin-left: 0px;
        }
  .el-menu--collapse .el-menu .el-sub-menu {
    min-width: $base-sidebar-width !important;
  }
        .sidebar-container {
            transition: transform 0.28s;
            width: $base-sidebar-width !important;
        }
  // mobile responsive
  .mobile {
    .main-container {
      margin-left: 0px;
    }
        &.hideSidebar {
            .sidebar-container {
                pointer-events: none;
                transition-duration: 0.3s;
                transform: translate3d(-$base-sidebar-width, 0, 0);
            }
        }
    }
    .sidebar-container {
      transition: transform .28s;
      width: $base-sidebar-width !important;
    }
    &.hideSidebar {
      .sidebar-container {
        pointer-events: none;
        transition-duration: 0.3s;
        transform: translate3d(-$base-sidebar-width, 0, 0);
      }
    }
  }
  .withoutAnimation {
    .main-container,
    .sidebar-container {
      transition: none;
    }
  }
    .withoutAnimation {
        .main-container,
        .sidebar-container {
            transition: none;
        }
    }
}
// when menu collapsed
.el-menu--vertical {
  &>.el-menu {
    .svg-icon {
      margin-right: 16px;
    }
  }
    & > .el-menu {
        .svg-icon {
            margin-right: 16px;
        }
    }
  .nest-menu .el-sub-menu>.el-sub-menu__title,
  .el-menu-item {
    &:hover {
      // you can use $sub-menuHover
      background-color: rgba(0, 0, 0, 0.06) !important;
    }
  }
    .nest-menu .el-sub-menu > .el-sub-menu__title,
    .el-menu-item {
        &:hover {
            // you can use $sub-menuHover
            background-color: rgba(0, 0, 0, 0.06) !important;
        }
    }
  // the scroll bar appears when the sub-menu is too long
  >.el-menu--popup {
    max-height: 100vh;
    overflow-y: auto;
    // the scroll bar appears when the sub-menu is too long
    > .el-menu--popup {
        max-height: 100vh;
        overflow-y: auto;
    &::-webkit-scrollbar-track-piece {
      background: #d3dce6;
    }
        &::-webkit-scrollbar-track-piece {
            background: #d3dce6;
        }
    &::-webkit-scrollbar {
      width: 6px;
    }
        &::-webkit-scrollbar {
            width: 6px;
        }
    &::-webkit-scrollbar-thumb {
      background: #99a9bf;
      border-radius: 20px;
    }
  }
        &::-webkit-scrollbar-thumb {
            background: #99a9bf;
            border-radius: 20px;
        }
    }
}
src/assets/styles/transition.scss
@@ -3,51 +3,51 @@
/* fade */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.28s;
    transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
  opacity: 0;
    opacity: 0;
}
/* fade-transform */
.fade-transform--move,
.fade-transform-leave-active,
.fade-transform-enter-active {
  transition: all .5s;
    transition: all 0.5s;
}
.fade-transform-leave-active {
  position: absolute;
    position: absolute;
}
.fade-transform-enter {
  opacity: 0;
  transform: translateX(-30px);
    opacity: 0;
    transform: translateX(-30px);
}
.fade-transform-leave-to {
  opacity: 0;
  transform: translateX(30px);
    opacity: 0;
    transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
  transition: all .5s;
    transition: all 0.5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
  opacity: 0;
  transform: translateX(20px);
    opacity: 0;
    transform: translateX(20px);
}
.breadcrumb-move {
  transition: all .5s;
    transition: all 0.5s;
}
.breadcrumb-leave-active {
  position: absolute;
    position: absolute;
}
src/assets/styles/variables.module.scss
@@ -1,12 +1,12 @@
// base color
$blue: #324157;
$light-blue: #3A71A8;
$red: #C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow: #FEC171;
$panGreen: #30B08F;
$light-blue: #3a71a8;
$red: #c03639;
$pink: #e65d6e;
$green: #30b08f;
$tiffany: #4ab7bd;
$yellow: #fec171;
$panGreen: #30b08f;
// é»˜è®¤èœå•主题风格
$base-menu-color: #bfcbd9;
@@ -36,10 +36,10 @@
$base-sub-menu-hover:#001528;
*/
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-primary: #409eff;
$--color-success: #67c23a;
$--color-warning: #e6a23c;
$--color-danger: #f56c6c;
$--color-info: #909399;
$base-sidebar-width: 200px;
@@ -47,19 +47,19 @@
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
  menuColor: $base-menu-color;
  menuLightColor: $base-menu-light-color;
  menuColorActive: $base-menu-color-active;
  menuBackground: $base-menu-background;
  menuLightBackground: $base-menu-light-background;
  subMenuBackground: $base-sub-menu-background;
  subMenuHover: $base-sub-menu-hover;
  sideBarWidth: $base-sidebar-width;
  logoTitleColor: $base-logo-title-color;
  logoLightTitleColor: $base-logo-light-title-color;
  primaryColor: $--color-primary;
  successColor: $--color-success;
  dangerColor: $--color-danger;
  infoColor: $--color-info;
  warningColor: $--color-warning;
    menuColor: $base-menu-color;
    menuLightColor: $base-menu-light-color;
    menuColorActive: $base-menu-color-active;
    menuBackground: $base-menu-background;
    menuLightBackground: $base-menu-light-background;
    subMenuBackground: $base-sub-menu-background;
    subMenuHover: $base-sub-menu-hover;
    sideBarWidth: $base-sidebar-width;
    logoTitleColor: $base-logo-title-color;
    logoLightTitleColor: $base-logo-light-title-color;
    primaryColor: $--color-primary;
    successColor: $--color-success;
    dangerColor: $--color-danger;
    infoColor: $--color-info;
    warningColor: $--color-warning;
}
src/components/Breadcrumb/index.vue
@@ -1,57 +1,55 @@
<template>
  <el-breadcrumb class="app-breadcrumb" separator="/">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
      </el-breadcrumb-item>
    </transition-group>
  </el-breadcrumb>
</template>
<script setup lang="ts">
import { RouteLocationMatched } from 'vue-router'
<script setup>
const route = useRoute();
const router = useRouter();
const levelList = ref([])
const levelList = ref<RouteLocationMatched[]>([])
function getBreadcrumb() {
const getBreadcrumb = () => {
  // only show routes with meta.title
  let matched = route.matched.filter(item => item.meta && item.meta.title);
  const first = matched[0]
  // åˆ¤æ–­æ˜¯å¦ä¸ºé¦–页
  if (!isDashboard(first)) {
    matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
    matched = ([{ path: '/index', meta: { title: '首页' } }] as any).concat(matched)
  }
  levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function isDashboard(route) {
  const name = route && route.name
const isDashboard = (route: RouteLocationMatched) => {
  const name = route && route.name as string
  if (!name) {
    return false
  }
  return name.trim() === 'Index'
}
function handleLink(item) {
const handleLink = (item: RouteLocationMatched) => {
  const { redirect, path } = item
  if (redirect) {
    router.push(redirect)
    return
  }
  router.push(path)
  redirect ? router.push(redirect as string) : router.push(path)
}
watchEffect(() => {
  // if you go to the redirect page, do not update the breadcrumbs
  if (route.path.startsWith('/redirect/')) {
    return
  }
  if (route.path.startsWith('/redirect/')) return
  getBreadcrumb()
})
getBreadcrumb();
onMounted(() => {
  getBreadcrumb();
})
</script>
<style lang='scss' scoped>
<template>
    <el-breadcrumb class="app-breadcrumb" separator="/">
        <transition-group name="breadcrumb">
            <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
                <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{
          item.meta?.title }}</span>
                <a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
            </el-breadcrumb-item>
        </transition-group>
    </el-breadcrumb>
</template>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
  display: inline-block;
  font-size: 14px;
@@ -63,4 +61,4 @@
    cursor: text;
  }
}
</style>
</style>
src/components/DictTag/index.vue
@@ -1,31 +1,10 @@
<template>
  <div>
    <template v-for="(item, index) in options">
      <template v-if="values.includes(item.value)">
        <span
          v-if="item.elTagType == 'default' || item.elTagType == ''"
          :key="item.value"
          :index="index"
          :class="item.elTagClass"
        >{{ item.label }}</span>
        <el-tag
          v-else
          :disable-transitions="true"
          :key="item.value + ''"
          :index="index"
          :type="item.elTagType === 'primary' ? '' : item.elTagType"
          :class="item.elTagClass"
        >{{ item.label }}</el-tag>
      </template>
    </template>
  </div>
</template>
<script setup lang="ts">
import { PropType } from 'vue';
<script setup>
const props = defineProps({
  // æ•°æ®
  options: {
    type: Array,
    type: Array as PropType<DictDataOption[]>,
    default: null,
  },
  // å½“前的值
@@ -33,17 +12,41 @@
})
const values = computed(() => {
  if (props.value !== null && typeof props.value !== 'undefined') {
    return Array.isArray(props.value) ? props.value : [String(props.value)];
  } else {
    return [];
  }
    if (props.value !== null && typeof props.value !== 'undefined') {
        return Array.isArray(props.value) ? props.value : [String(props.value)];
    } else {
        return [];
    }
})
</script>
<template>
    <div>
        <template v-for="(item, index) in options">
            <template v-if="values.includes(item.value)">
                <span
                    v-if="item.elTagType == 'default' || item.elTagType == ''"
                    :key="item.value"
                    :index="index"
                    :class="item.elTagClass"
                    >{{ item.label }}</span
                >
                <el-tag
                    v-else
                    :disable-transitions="true"
                    :key="item.value + ''"
                    :index="index"
                    :type="item.elTagType === 'primary' ? '' : item.elTagType"
                    :class="item.elTagClass"
                    >{{ item.label }}</el-tag
                >
            </template>
        </template>
    </div>
</template>
<style scoped>
.el-tag + .el-tag {
  margin-left: 10px;
}
</style>
</style>
src/components/Editor/index.vue
@@ -1,36 +1,8 @@
<template>
  <div>
    <el-upload
        :action="uploadUrl"
        :before-upload="handleBeforeUpload"
        :on-success="handleUploadSuccess"
        :on-error="handleUploadError"
        class="editor-img-uploader"
        name="file"
        :show-file-list="false"
        :headers="headers"
        style="display: none"
        ref="uploadRef"
        v-if="type == 'url'"
    >
    </el-upload>
    <div class="editor">
      <quill-editor
          ref="myQuillEditor"
          v-model:content="content"
          contentType="html"
          @textChange="(e) => $emit('update:modelValue', content)"
          :options="options"
          :style="styles"
      />
    </div>
  </div>
</template>
<script setup>
<script setup lang="ts">
import { QuillEditor, Quill } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { getToken } from "@/utils/auth";
import { ComponentInternalInstance } from "vue";
const props = defineProps({
  /* ç¼–辑器的内容 */
@@ -64,10 +36,12 @@
  }
});
const { proxy } = getCurrentInstance();
// ä¸Šä¼ çš„图片服务器地址
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/system/oss/upload");
const headers = ref({ Authorization: "Bearer " + getToken() });
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const upload = reactive<UploadOption>({
  headers: { Authorization: "Bearer " + getToken() },
  url: import.meta.env.VITE_APP_BASE_API + '/system/oss/upload'
})
const myQuillEditor = ref();
const options = ref({
@@ -90,10 +64,10 @@
        ["link", "image", "video"]                       // é“¾æŽ¥ã€å›¾ç‰‡ã€è§†é¢‘
      ],
      handlers: {
        image: function (value) {
        image: function (value: any) {
          if (value) {
            // è°ƒç”¨element图片上传
            document.querySelector(".editor-img-uploader>.el-upload").click();
            (document.querySelector(".editor-img-uploader>.el-upload") as HTMLDivElement)?.click();
          } else {
            Quill.format("image", true);
          }
@@ -106,7 +80,7 @@
});
const styles = computed(() => {
  let style = {};
  let style: any = {};
  if (props.minHeight) {
    style.minHeight = `${props.minHeight}px`;
  }
@@ -118,50 +92,78 @@
const content = ref("");
watch(() => props.modelValue, (v) => {
  if (v !== content) {
  if (v !== content.value) {
    content.value = v === undefined ? "<p></p>" : v;
  }
}, { immediate: true });
// å›¾ç‰‡ä¸Šä¼ æˆåŠŸè¿”å›žå›¾ç‰‡åœ°å€
function handleUploadSuccess(res, file) {
const handleUploadSuccess = (res: any) => {
  // èŽ·å–å¯Œæ–‡æœ¬å®žä¾‹
  let quill = toRaw(myQuillEditor.value).getQuill();
  // å¦‚果上传成功
  if (res.code == 200) {
  if (res.code === 200) {
    // èŽ·å–å…‰æ ‡ä½ç½®
    let length = quill.selection.savedRange.index;
    // æ’入图片,res为服务器返回的图片链接地址
    quill.insertEmbed(length, "image", res.data.url);
    // è°ƒæ•´å…‰æ ‡åˆ°æœ€åŽ
    quill.setSelection(length + 1);
    proxy.$modal.closeLoading();
    proxy?.$modal.closeLoading();
  } else {
    proxy.$modal.loading(res.msg);
    proxy.$modal.closeLoading();
    proxy?.$modal.loading(res.msg);
    proxy?.$modal.closeLoading();
  }
}
// å›¾ç‰‡ä¸Šä¼ å‰æ‹¦æˆª
function handleBeforeUpload(file) {
const handleBeforeUpload = (file: any) => {
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
  }
  proxy.$modal.loading("正在上传文件,请稍候...");
  proxy?.$modal.loading('正在上传文件,请稍候...');
  return true;
}
// å›¾ç‰‡å¤±è´¥æ‹¦æˆª
function handleUploadError(err) {
  proxy.$modal.msgError("上传文件失败");
const handleUploadError = (err: any) => {
  console.error(err);
  proxy?.$modal.msgError('上传文件失败');
}
</script>
<template>
    <div>
        <el-upload
            :action="upload.url"
            :before-upload="handleBeforeUpload"
            :on-success="handleUploadSuccess"
            :on-error="handleUploadError"
            class="editor-img-uploader"
            name="file"
            :show-file-list="false"
            :headers="upload.headers"
            style="display: none"
            v-if="type === 'url'"
        >
        </el-upload>
        <div class="editor">
            <quill-editor
                ref="myQuillEditor"
                v-model:content="content"
                contentType="html"
                @textChange="(e: any) => $emit('update:modelValue', content)"
                :options="options"
                :style="styles"
            />
        </div>
    </div>
</template>
<style>
.editor, .ql-toolbar {
@@ -175,9 +177,9 @@
  content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  border-right: 0px;
  border-right: 0;
  content: "保存";
  padding-right: 0px;
  padding-right: 0;
}
.ql-snow .ql-tooltip[data-mode="video"]::before {
src/components/FileUpload/index.vue
@@ -1,46 +1,8 @@
<template>
  <div class="upload-file">
    <el-upload
      multiple
      :action="uploadFileUrl"
      :before-upload="handleBeforeUpload"
      :file-list="fileList"
      :limit="limit"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      :on-success="handleUploadSuccess"
      :show-file-list="false"
      :headers="headers"
      class="upload-file-uploader"
      ref="fileUpload"
    >
      <!-- ä¸Šä¼ æŒ‰é’® -->
      <el-button type="primary">选取文件</el-button>
    </el-upload>
    <!-- ä¸Šä¼ æç¤º -->
    <div class="el-upload__tip" v-if="showTip">
      è¯·ä¸Šä¼ 
      <template v-if="fileSize"> å¤§å°ä¸è¶…过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
      <template v-if="fileType"> æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
      çš„æ–‡ä»¶
    </div>
    <!-- æ–‡ä»¶åˆ—表 -->
    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
        <el-link :href="`${file.url}`" :underline="false" target="_blank">
          <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
        </el-link>
        <div class="ele-upload-list__item-content-action">
          <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
        </div>
      </li>
    </transition-group>
  </div>
</template>
<script setup>
<script setup lang="ts">
import { getToken } from "@/utils/auth";
import { listByIds, delOss } from "@/api/system/oss";
import { ComponentInternalInstance } from "vue";
import { ElUpload, UploadFile } from "element-plus";
const props = defineProps({
  modelValue: [String, Object, Array],
@@ -66,32 +28,35 @@
  }
});
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref([]);
const uploadList = ref<any[]>([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + "/system/oss/upload"); // ä¸Šä¼ æ–‡ä»¶æœåŠ¡å™¨åœ°å€
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const fileList = ref<any[]>([]);
const showTip = computed(
  () => props.isShowTip && (props.fileType || props.fileSize)
);
const fileUploadRef = ref(ElUpload);
watch(() => props.modelValue, async val => {
  if (val) {
    let temp = 1;
    // é¦–先将值转为数组
    let list;
    let list = [];
    if (Array.isArray(val)) {
      list = val;
    } else {
      await listByIds(val).then(res => {
        list = res.data.map(oss => {
          oss = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
          return oss;
      const res =  await listByIds(val as string)
      list = res.data.map((oss) => {
          const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
          return data;
        });
      })
    }
    // ç„¶åŽå°†æ•°ç»„转为对象数组
    fileList.value = list.map(item => {
@@ -106,14 +71,14 @@
},{ deep: true, immediate: true });
// ä¸Šä¼ å‰æ ¡æ£€æ ¼å¼å’Œå¤§å°
function handleBeforeUpload(file) {
const handleBeforeUpload = (file: any) => {
  // æ ¡æ£€æ–‡ä»¶ç±»åž‹
  if (props.fileType.length) {
    const fileName = file.name.split('.');
    const fileExt = fileName[fileName.length - 1];
    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
    if (!isTypeOk) {
      proxy.$modal.msgError(`文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join("/")}格式文件!`);
      proxy?.$modal.msgError(`文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join("/")}格式文件!`);
      return false;
    }
  }
@@ -121,41 +86,41 @@
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
  }
  proxy.$modal.loading("正在上传文件,请稍候...");
  proxy?.$modal.loading("正在上传文件,请稍候...");
  number.value++;
  return true;
}
// æ–‡ä»¶ä¸ªæ•°è¶…出
function handleExceed() {
  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
const handleExceed = () => {
  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
}
// ä¸Šä¼ å¤±è´¥
function handleUploadError(err) {
  proxy.$modal.msgError("上传文件失败");
const handleUploadError = () => {
  proxy?.$modal.msgError("上传文件失败");
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file) {
const handleUploadSuccess = (res:any, file: UploadFile) => {
  if (res.code === 200) {
    uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
    uploadedSuccessfully();
  } else {
    number.value--;
    proxy.$modal.closeLoading();
    proxy.$modal.msgError(res.msg);
    proxy.$refs.fileUpload.handleRemove(file);
    proxy?.$modal.closeLoading();
    proxy?.$modal.msgError(res.msg);
    fileUploadRef.value.handleRemove(file);
    uploadedSuccessfully();
  }
}
// åˆ é™¤æ–‡ä»¶
function handleDelete(index) {
const handleDelete = (index: number) => {
  let ossId = fileList.value[index].ossId;
  delOss(ossId);
  fileList.value.splice(index, 1);
@@ -163,18 +128,18 @@
}
// ä¸Šä¼ ç»“束处理
function uploadedSuccessfully() {
const uploadedSuccessfully =() => {
  if (number.value > 0 && uploadList.value.length === number.value) {
    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
    uploadList.value = [];
    number.value = 0;
    emit("update:modelValue", listToString(fileList.value));
    proxy.$modal.closeLoading();
    proxy?.$modal.closeLoading();
  }
}
// èŽ·å–æ–‡ä»¶åç§°
function getFileName(name) {
const getFileName = (name: string) => {
  // å¦‚果是url那么取最后的名字 å¦‚果不是直接返回
  if (name.lastIndexOf("/") > -1) {
    return name.slice(name.lastIndexOf("/") + 1);
@@ -184,18 +149,62 @@
}
// å¯¹è±¡è½¬æˆæŒ‡å®šå­—符串分隔
function listToString(list, separator) {
const listToString = (list: any[], separator?: string) => {
  let strs = "";
  separator = separator || ",";
  for (let i in list) {
    if(list[i].ossId) {
      strs += list[i].ossId + separator;
  list.forEach(item => {
    if (item.ossId) {
      strs += item.ossId + separator;
    }
  }
  return strs != "" ? strs.substr(0, strs.length - 1) : "";
  })
  return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
</script>
<template>
    <div class="upload-file">
        <el-upload
            multiple
            :action="uploadFileUrl"
            :before-upload="handleBeforeUpload"
            :file-list="fileList"
            :limit="limit"
            :on-error="handleUploadError"
            :on-exceed="handleExceed"
            :on-success="handleUploadSuccess"
            :show-file-list="false"
            :headers="headers"
            class="upload-file-uploader"
            ref="fileUploadRef"
        >
            <!-- ä¸Šä¼ æŒ‰é’® -->
            <el-button type="primary">选取文件</el-button>
        </el-upload>
        <!-- ä¸Šä¼ æç¤º -->
        <div class="el-upload__tip" v-if="showTip">
            è¯·ä¸Šä¼ 
            <template v-if="fileSize">
                å¤§å°ä¸è¶…过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
            </template>
            <template v-if="fileType">
                æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
            </template>
            çš„æ–‡ä»¶
        </div>
        <!-- æ–‡ä»¶åˆ—表 -->
        <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
            <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
                <el-link :href="`${file.url}`" :underline="false" target="_blank">
                    <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
                </el-link>
                <div class="ele-upload-list__item-content-action">
                    <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
                </div>
            </li>
        </transition-group>
    </div>
</template>
<style scoped lang="scss">
.upload-file-uploader {
  margin-bottom: 5px;
src/components/Hamburger/index.vue
@@ -1,19 +1,4 @@
<template>
  <div style="padding: 0 15px;" @click="toggleClick">
    <svg
      :class="{'is-active':isActive}"
      class="hamburger"
      viewBox="0 0 1024 1024"
      xmlns="http://www.w3.org/2000/svg"
      width="64"
      height="64"
    >
      <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
    </svg>
  </div>
</template>
<script setup>
<script setup lang="ts">
defineProps({
  isActive: {
    type: Boolean,
@@ -21,12 +6,22 @@
  }
})
const emit = defineEmits()
const emit = defineEmits(['toggleClick'])
const toggleClick = () => {
  emit('toggleClick');
}
</script>
<template>
    <div style="padding: 0 15px;" @click="toggleClick">
        <svg :class="{'is-active':isActive}" class="hamburger" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="64" height="64">
            <path
                d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
            />
        </svg>
    </div>
</template>
<style scoped>
.hamburger {
  display: inline-block;
src/components/HeaderSearch/index.vue
@@ -1,49 +1,36 @@
<template>
  <div :class="{ 'show': show }" class="header-search">
    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
    <el-select
      ref="headerSearchSelectRef"
      v-model="search"
      :remote-method="querySearch"
      filterable
      default-first-option
      remote
      placeholder="Search"
      class="header-search-select"
      @change="change"
    >
      <el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
    </el-select>
  </div>
</template>
<script setup>
<script setup lang="ts">
import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate'
import usePermissionStore from '@/store/modules/permission'
import { RouteOption } from 'vue-router'
type Router = Array<{
  path: string;
  title: string[];
}>
const search = ref('');
const options = ref([]);
const searchPool = ref([]);
const options = ref<any>([]);
const searchPool = ref<Router>([]);
const show = ref(false);
const fuse = ref(undefined);
const headerSearchSelectRef = ref(null);
const fuse = ref();
const headerSearchSelectRef = ref(ElSelect);
const router = useRouter();
const routes = computed(() => usePermissionStore().routes);
function click() {
const click = () => {
  show.value = !show.value
  if (show.value) {
    headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
  }
};
function close() {
const close = () => {
  headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
  options.value = []
  show.value = false
}
function change(val) {
const change = (val: any) => {
  const path = val.path;
  if (isHttp(path)) {
    // http(s):// è·¯å¾„新窗口打开
@@ -52,14 +39,13 @@
  } else {
    router.push(path)
  }
  search.value = ''
  options.value = []
  nextTick(() => {
    show.value = false
  })
}
function initFuse(list) {
const initFuse = (list: Router) => {
  fuse.value = new Fuse(list, {
    shouldSort: true,
    threshold: 0.4,
@@ -77,39 +63,36 @@
}
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) {
  let res = []
  for (const r of routes) {
const generateRoutes = (routes: RouteOption[], basePath = '', prefixTitle: string[] = []) => {
  let res: Router = []
  routes.forEach(r => {
    // skip hidden router
    if (r.hidden) { continue }
    const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
    const data = {
      path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
      title: [...prefixTitle]
    }
    if (r.meta && r.meta.title) {
      data.title = [...data.title, r.meta.title]
      if (r.redirect !== 'noRedirect') {
        // only push the routes with title
        // special case: need to exclude parent router without redirect
        res.push(data)
    if (!r.hidden) {
      const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
      const data = {
        path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
        title: [...prefixTitle]
      }
      if (r.meta && r.meta.title) {
        data.title = [...data.title, r.meta.title];
        if (r.redirect !== 'noRedirect') {
          // only push the routes with title
          // special case: need to exclude parent router without redirect
          res.push(data);
        }
      }
      // recursive child routes
      if (r.children) {
        const tempRoutes = generateRoutes(r.children, data.path, data.title);
        if (tempRoutes.length >= 1) {
          res = [...res, ...tempRoutes];
        }
      }
    }
    // recursive child routes
    if (r.children) {
      const tempRoutes = generateRoutes(r.children, data.path, data.title)
      if (tempRoutes.length >= 1) {
        res = [...res, ...tempRoutes]
      }
    }
  }
  return res
  })
  return res;
}
function querySearch(query) {
const querySearch = (query: string) => {
  if (query !== '') {
    options.value = fuse.value.search(query)
  } else {
@@ -138,7 +121,26 @@
})
</script>
<style lang='scss' scoped>
<template>
    <div :class="{ 'show': show }" class="header-search">
        <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
        <el-select
            ref="headerSearchSelectRef"
            v-model="search"
            :remote-method="querySearch"
            filterable
            default-first-option
            remote
            placeholder="Search"
            class="header-search-select"
            @change="change"
        >
            <el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
        </el-select>
    </div>
</template>
<style lang="scss" scoped>
.header-search {
  font-size: 0 !important;
@@ -176,4 +178,4 @@
    }
  }
}
</style>
</style>
src/components/IconSelect/index.vue
@@ -1,74 +1,105 @@
<template>
  <div class="icon-body">
    <el-input
      v-model="iconName"
      style="position: relative;"
      clearable
      placeholder="请输入图标名称"
      @clear="filterIcons"
      @input="filterIcons"
    >
      <template #suffix><i class="el-icon-search el-input__icon" /></template>
    </el-input>
    <div class="icon-list">
      <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
        <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
        <span>{{ item }}</span>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import icons from '@/components/IconSelect/requireIcons';
<script setup>
import icons from './requireIcons'
const props = defineProps({
  modelValue: {
    type: String,
    require: true
  },
  width: {
    type: String,
    require: false,
    default: '400px'
  }
});
const iconName = ref('');
const iconList = ref(icons);
const emit = defineEmits(['selected']);
const emit = defineEmits(['update:modelValue']);
const visible = ref(false);
const { modelValue, width } = toRefs(props);
const iconNames = ref<string[]>(icons);
function filterIcons() {
  iconList.value = icons
  if (iconName.value) {
    iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
const filterValue = ref('');
/**
 * ç­›é€‰å›¾æ ‡
 */
const filterIcons = () => {
  if (filterValue.value) {
    iconNames.value = icons.filter(iconName =>
      iconName.includes(filterValue.value)
    );
  } else {
    iconNames.value = icons;
  }
}
function selectedIcon(name) {
  emit('selected', name)
  document.body.click()
/**
 * é€‰æ‹©å›¾æ ‡
 * @param iconName é€‰æ‹©çš„图标名称
 */
const selectedIcon = (iconName: string) => {
  emit('update:modelValue', iconName);
  visible.value = false;
}
function reset() {
  iconName.value = ''
  iconList.value = icons
}
defineExpose({
  reset
})
</script>
<style lang='scss' scoped>
.icon-body {
  width: 100%;
  padding: 10px;
  .icon-list {
    height: 200px;
    overflow-y: scroll;
    div {
      height: 30px;
      line-height: 30px;
      margin-bottom: -5px;
      cursor: pointer;
      width: 33%;
      float: left;
    }
    span {
      display: inline-block;
      vertical-align: -0.15em;
      fill: currentColor;
      overflow: hidden;
<template>
    <div class="relative" :style="{ width: width }">
        <el-input v-model="modelValue" readonly @click="visible = !visible" placeholder="点击选择图标">
            <template #prepend>
                <svg-icon :icon-class="modelValue as string"></svg-icon>
            </template>
        </el-input>
        <el-popover shadow="none" :visible="visible" placement="bottom-end" trigger="click" :width="450">
            <template #reference>
                <div @click="visible = !visible" class="cursor-pointer text-[#999] absolute right-[10px] top-0 height-[32px] leading-[32px]">
                    <i-ep-caret-top v-show="visible"></i-ep-caret-top>
                    <i-ep-caret-bottom v-show="!visible"></i-ep-caret-bottom>
                </div>
            </template>
            <el-input class="p-2" v-model="filterValue" placeholder="搜索图标" clearable @input="filterIcons" />
            <el-scrollbar height="w-[200px]">
                <ul class="icon-list">
                    <el-tooltip v-for="(iconName, index) in iconNames" :key="index" :content="iconName" placement="bottom" effect="light">
                        <li class="icon-item" @click="selectedIcon(iconName)">
                            <svg-icon color="var(--el-text-color-regular)" :icon-class="iconName" />
                        </li>
                    </el-tooltip>
                </ul>
            </el-scrollbar>
        </el-popover>
    </div>
</template>
<style scoped lang="scss">
.el-divider--horizontal {
  margin: 10px auto !important;
}
.icon-list {
  display: flex;
  flex-wrap: wrap;
  padding-left: 10px;
  margin-top: 10px;
  .icon-item {
    cursor: pointer;
    width: 10%;
    margin: 0 10px 10px 0;
    padding: 5px;
    display: flex;
    flex-direction: column;
    justify-items: center;
    align-items: center;
    border: 1px solid #ccc;
    &:hover {
      border-color: var(--el-color-primary);
      color: var(--el-color-primary);
      transition: all 0.2s;
      transform: scaleX(1.1);
    }
  }
}
</style>
</style>
src/components/IconSelect/requireIcons.js
ÎļþÒÑɾ³ý
src/components/IconSelect/requireIcons.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
const icons: string[] = [];
const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
for (const path in modules) {
    const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
    icons.push(p);
}
export default icons;
src/components/ImagePreview/index.vue
@@ -1,21 +1,4 @@
<template>
  <el-image
    :src="`${realSrc}`"
    fit="cover"
    :style="`width:${realWidth};height:${realHeight};`"
    :preview-src-list="realSrcList"
    preview-teleported
  >
    <template #error>
      <div class="image-slot">
        <el-icon><picture-filled /></el-icon>
      </div>
    </template>
  </el-image>
</template>
<script setup>
<script setup lang="ts">
const props = defineProps({
  src: {
    type: String,
@@ -44,7 +27,7 @@
    return;
  }
  let real_src_list = props.src.split(",");
  let srcList = [];
  let srcList:string[] = [];
  real_src_list.forEach(item => {
    return srcList.push(item);
  });
@@ -60,6 +43,16 @@
);
</script>
<template>
    <el-image :src="`${realSrc}`" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="realSrcList" preview-teleported>
        <template #error>
            <div class="image-slot">
                <el-icon><picture-filled /></el-icon>
            </div>
        </template>
    </el-image>
</template>
<style lang="scss" scoped>
.el-image {
  border-radius: 5px;
src/components/ImageUpload/index.vue
@@ -1,53 +1,9 @@
<template>
  <div class="component-upload-image">
    <el-upload
      multiple
      :action="uploadImgUrl"
      list-type="picture-card"
      :on-success="handleUploadSuccess"
      :before-upload="handleBeforeUpload"
      :limit="limit"
      :on-error="handleUploadError"
      :on-exceed="handleExceed"
      ref="imageUpload"
      :before-remove="handleDelete"
      :show-file-list="true"
      :headers="headers"
      :file-list="fileList"
      :on-preview="handlePictureCardPreview"
      :class="{ hide: fileList.length >= limit }"
    >
      <el-icon class="avatar-uploader-icon"><plus /></el-icon>
    </el-upload>
    <!-- ä¸Šä¼ æç¤º -->
    <div class="el-upload__tip" v-if="showTip">
      è¯·ä¸Šä¼ 
      <template v-if="fileSize">
        å¤§å°ä¸è¶…过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
      </template>
      <template v-if="fileType">
        æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
      </template>
      çš„æ–‡ä»¶
    </div>
    <el-dialog
      v-model="dialogVisible"
      title="预览"
      width="800px"
      append-to-body
    >
      <img
        :src="dialogImageUrl"
        style="display: block; max-width: 100%; margin: 0 auto"
      />
    </el-dialog>
  </div>
</template>
<script setup>
<script setup lang="ts">
import { getToken } from "@/utils/auth";
import { listByIds, delOss } from "@/api/system/oss";
import { ComponentInternalInstance, PropType } from "vue";
import { OssVO } from "@/api/system/oss/types";
import { ElUpload, UploadFile } from "element-plus";
const props = defineProps({
  modelValue: [String, Object, Array],
@@ -63,7 +19,7 @@
  },
  // æ–‡ä»¶ç±»åž‹, ä¾‹å¦‚['png', 'jpg', 'jpeg']
  fileType: {
    type: Array,
    type: Array as PropType<string[]>,
    default: () => ["png", "jpg", "jpeg"],
  },
  // æ˜¯å¦æ˜¾ç¤ºæç¤º
@@ -73,41 +29,45 @@
  },
});
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref([]);
const uploadList = ref<any[]>([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(baseUrl + "/system/oss/upload"); // ä¸Šä¼ çš„图片服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const fileList = ref<any[]>([]);
const showTip = computed(
  () => props.isShowTip && (props.fileType || props.fileSize)
);
const imageUploadRef = ref(ElUpload);
watch(() => props.modelValue, async val => {
  if (val) {
    // é¦–先将值转为数组
    let list;
    let list:OssVO[] = [];
    if (Array.isArray(val)) {
      list = val;
      list = val as OssVO[];
    } else {
      await listByIds(val).then(res => {
        list = res.data;
      })
      const res = await listByIds(val as string)
      list = res.data
    }
    // ç„¶åŽå°†æ•°ç»„转为对象数组
    fileList.value = list.map(item => {
      // å­—符串回显处理 å¦‚果此处存的是url可直接回显 å¦‚果存的是id需要调用接口查出来
      let itemData;
      if (typeof item === "string") {
        item = { name: item, url: item };
        itemData = { name: item, url: item };
      } else {
        // æ­¤å¤„name使用ossId é˜²æ­¢åˆ é™¤å‡ºçŽ°é‡å
        item = { name: item.ossId, url: item.url, ossId: item.ossId };
        itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
      }
      return item;
      return itemData;
    });
  } else {
    fileList.value = [];
@@ -115,8 +75,8 @@
  }
},{ deep: true, immediate: true });
// ä¸Šä¼ å‰loading加载
function handleBeforeUpload(file) {
/** ä¸Šä¼ å‰loading加载 */
const handleBeforeUpload = (file: any) => {
  let isImg = false;
  if (props.fileType.length) {
    let fileExtension = "";
@@ -132,7 +92,7 @@
    isImg = file.type.indexOf("image") > -1;
  }
  if (!isImg) {
    proxy.$modal.msgError(
    proxy?.$modal.msgError(
      `文件格式不正确, è¯·ä¸Šä¼ ${props.fileType.join("/")}图片格式文件!`
    );
    return false;
@@ -140,35 +100,35 @@
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
      proxy?.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
  }
  proxy.$modal.loading("正在上传图片,请稍候...");
  proxy?.$modal.loading("正在上传图片,请稍候...");
  number.value++;
}
// æ–‡ä»¶ä¸ªæ•°è¶…出
function handleExceed() {
  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
const handleExceed = () => {
  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} ä¸ª!`);
}
// ä¸Šä¼ æˆåŠŸå›žè°ƒ
function handleUploadSuccess(res, file) {
const handleUploadSuccess = (res: any, file: UploadFile) => {
  if (res.code === 200) {
    uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
    uploadedSuccessfully();
  } else {
    number.value--;
    proxy.$modal.closeLoading();
    proxy.$modal.msgError(res.msg);
    proxy.$refs.imageUpload.handleRemove(file);
    proxy?.$modal.closeLoading();
    proxy?.$modal.msgError(res.msg);
    imageUploadRef.value.handleRemove(file);
    uploadedSuccessfully();
  }
}
// åˆ é™¤å›¾ç‰‡
function handleDelete(file) {
const handleDelete = (file: UploadFile): boolean => {
  const findex = fileList.value.map(f => f.name).indexOf(file.name);
  if (findex > -1 && uploadList.value.length === number.value) {
    let ossId = fileList.value[findex].ossId;
@@ -177,33 +137,34 @@
    emit("update:modelValue", listToString(fileList.value));
    return false;
  }
  return true;
}
// ä¸Šä¼ ç»“束处理
function uploadedSuccessfully() {
const uploadedSuccessfully = () => {
  if (number.value > 0 && uploadList.value.length === number.value) {
    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
    uploadList.value = [];
    number.value = 0;
    emit("update:modelValue", listToString(fileList.value));
    proxy.$modal.closeLoading();
    proxy?.$modal.closeLoading();
  }
}
// ä¸Šä¼ å¤±è´¥
function handleUploadError(res) {
  proxy.$modal.msgError("上传图片失败");
  proxy.$modal.closeLoading();
const handleUploadError = () => {
  proxy?.$modal.msgError("上传图片失败");
  proxy?.$modal.closeLoading();
}
// é¢„览
function handlePictureCardPreview(file) {
const handlePictureCardPreview = (file: any) => {
  dialogImageUrl.value = file.url;
  dialogVisible.value = true;
}
// å¯¹è±¡è½¬æˆæŒ‡å®šå­—符串分隔
function listToString(list, separator) {
const listToString = (list: any[], separator?: string) => {
  let strs = "";
  separator = separator || ",";
  for (let i in list) {
@@ -211,13 +172,52 @@
      strs += list[i].ossId + separator;
    }
  }
  return strs != "" ? strs.substr(0, strs.length - 1) : "";
  return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
</script>
<template>
    <div class="component-upload-image">
        <el-upload
            multiple
            :action="uploadImgUrl"
            list-type="picture-card"
            :on-success="handleUploadSuccess"
            :before-upload="handleBeforeUpload"
            :limit="limit"
            :on-error="handleUploadError"
            :on-exceed="handleExceed"
            ref="imageUpload"
            :before-remove="handleDelete"
            :show-file-list="true"
            :headers="headers"
            :file-list="fileList"
            :on-preview="handlePictureCardPreview"
            :class="{ hide: fileList.length >= limit }"
        >
            <el-icon class="avatar-uploader-icon"><plus /></el-icon>
        </el-upload>
        <!-- ä¸Šä¼ æç¤º -->
        <div class="el-upload__tip" v-if="showTip">
            è¯·ä¸Šä¼ 
            <template v-if="fileSize">
                å¤§å°ä¸è¶…过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
            </template>
            <template v-if="fileType">
                æ ¼å¼ä¸º <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
            </template>
            çš„æ–‡ä»¶
        </div>
        <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
            <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
        </el-dialog>
    </div>
</template>
<style scoped lang="scss">
// .el-upload--picture-card æŽ§åˆ¶åŠ å·éƒ¨åˆ†
:deep(.hide .el-upload--picture-card) {
    display: none;
}
</style>
</style>
src/components/Pagination/index.vue
@@ -1,21 +1,12 @@
<template>
  <div :class="{ 'hidden': hidden }" class="pagination-container">
    <el-pagination
      :background="background"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :layout="layout"
      :page-sizes="pageSizes"
      :pager-count="pagerCount"
      :total="total"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>
<script lang="ts">
export default {
  name: 'Pagination'
}
</script>
<script setup>
<script setup lang="ts">
import { scrollTo } from '@/utils/scroll-to'
import { PropType } from "vue";
const props = defineProps({
  total: {
@@ -31,7 +22,7 @@
    default: 20
  },
  pageSizes: {
    type: Array,
    type: Array as PropType<number[]>,
    default() {
      return [10, 20, 30, 50]
    }
@@ -56,10 +47,14 @@
  hidden: {
    type: Boolean,
    default: false
  },
  float: {
    type: String,
    default: 'right'
  }
})
const emit = defineEmits();
const emit = defineEmits(['update:page', 'update:limit', 'pagination']);
const currentPage = computed({
  get() {
    return props.page
@@ -76,7 +71,7 @@
    emit('update:limit', val)
  }
})
function handleSizeChange(val) {
function handleSizeChange(val: number) {
  if (currentPage.value * val > props.total) {
    currentPage.value = 1
  }
@@ -85,21 +80,39 @@
    scrollTo(0, 800)
  }
}
function handleCurrentChange(val) {
function handleCurrentChange(val: number) {
  emit('pagination', { page: val, limit: pageSize.value })
  if (props.autoScroll) {
    scrollTo(0, 800)
  }
}
</script>
<style scoped>
<template>
    <div :class="{ 'hidden': hidden }" class="pagination-container">
        <el-pagination
            :background="background"
            v-model:current-page="currentPage"
            v-model:page-size="pageSize"
            :layout="layout"
            :page-sizes="pageSizes"
            :pager-count="pagerCount"
            :total="total"
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
        />
    </div>
</template>
<style lang="scss" scoped>
.pagination-container {
  background: #fff;
  padding: 32px 16px;
  .el-pagination{
    float: v-bind(float);
  }
}
.pagination-container.hidden {
  display: none;
}
</style>
</style>
src/components/ParentView/index.vue
@@ -1,3 +1,3 @@
<template >
  <router-view />
<template>
    <router-view />
</template>
src/components/RightToolbar/index.vue
@@ -1,35 +1,14 @@
<template>
  <div class="top-right-btn" :style="style">
    <el-row>
      <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
        <el-button circle icon="Search" @click="toggleSearch()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
        <el-button circle icon="Refresh" @click="refresh()" />
      </el-tooltip>
      <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
        <el-button circle icon="Menu" @click="showColumn()" />
      </el-tooltip>
    </el-row>
    <el-dialog :title="title" v-model="open" append-to-body>
      <el-transfer
        :titles="['显示', '隐藏']"
        v-model="value"
        :data="columns"
        @change="dataChange"
      ></el-transfer>
    </el-dialog>
  </div>
</template>
<script setup lang="ts">
import { TransferKey } from "element-plus";
import { PropType } from "vue";
<script setup>
const props = defineProps({
  showSearch: {
    type: Boolean,
    default: true,
  },
  columns: {
    type: Array,
    type: Array as PropType<FieldOption[]>,
  },
  search: {
    type: Boolean,
@@ -44,14 +23,14 @@
const emits = defineEmits(['update:showSearch', 'queryTable']);
// æ˜¾é𐿕°æ®
const value = ref([]);
const value = ref<Array<number>>([]);
// å¼¹å‡ºå±‚标题
const title = ref("显示/隐藏");
// æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚
const open = ref(false);
const style = computed(() => {
  const ret = {};
  const ret: any = {};
  if (props.gutter) {
    ret.marginRight = `${props.gutter / 2}px`;
  }
@@ -69,27 +48,47 @@
}
// å³ä¾§åˆ—表元素变化
function dataChange(data) {
  for (let item in props.columns) {
    const key = props.columns[item].key;
    props.columns[item].visible = !data.includes(key);
  }
function dataChange(data: TransferKey[]) {
  props.columns?.forEach((item) => {
    item.visible = !data.includes(item.key);
  })
}
// æ‰“开显隐列dialog
function showColumn() {
const showColumn = () => {
  open.value = true;
}
// æ˜¾éšåˆ—初始默认隐藏列
for (let item in props.columns) {
  if (props.columns[item].visible === false) {
    value.value.push(parseInt(item));
  }
}
onMounted(() => {
  props.columns?.forEach((item) => {
    if (!item.visible) {
      value.value.push(item.key);
    }
  })
})
</script>
<style lang='scss' scoped>
<template>
    <div class="top-right-btn" :style="style">
        <el-row>
            <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
                <el-button circle icon="Search" @click="toggleSearch()" />
            </el-tooltip>
            <el-tooltip class="item" effect="dark" content="刷新" placement="top">
                <el-button circle icon="Refresh" @click="refresh()" />
            </el-tooltip>
            <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
                <el-button circle icon="Menu" @click="showColumn()" />
            </el-tooltip>
        </el-row>
        <el-dialog :title="title" v-model="open" append-to-body>
            <el-transfer :titles="['显示', '隐藏']" v-model="value" :data="columns" @change="dataChange"></el-transfer>
        </el-dialog>
    </div>
</template>
<style lang="scss" scoped>
:deep(.el-transfer__button) {
  border-radius: 50%;
  display: block;
@@ -102,4 +101,4 @@
.my-el-transfer {
  text-align: center;
}
</style>
</style>
src/components/RuoYiDoc/index.vue
ÎļþÃû´Ó src/components/RuoYi/Doc/index.vue ÐÞ¸Ä
@@ -1,7 +1,7 @@
<template>
  <div>
    <svg-icon icon-class="question" @click="goto" />
  </div>
    <div>
        <svg-icon icon-class="question" @click="goto" />
    </div>
</template>
<script setup>
@@ -10,4 +10,4 @@
function goto() {
  window.open(url.value)
}
</script>
</script>
src/components/RuoYiGit/index.vue
ÎļþÃû´Ó src/components/RuoYi/Git/index.vue ÐÞ¸Ä
@@ -1,7 +1,7 @@
<template>
  <div>
    <svg-icon icon-class="github" @click="goto" />
  </div>
    <div>
        <svg-icon icon-class="github" @click="goto" />
    </div>
</template>
<script setup>
src/components/Screenfull/index.vue
@@ -1,16 +1,14 @@
<template>
  <div>
    <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
  </div>
    <div>
        <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
    </div>
</template>
<script setup>
import { useFullscreen } from '@vueuse/core'
const { isFullscreen, enter, exit, toggle } = useFullscreen();
<script setup lang="ts">
const { isFullscreen, toggle } = useFullscreen();
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.screenfull-svg {
  display: inline-block;
  cursor: pointer;
@@ -19,4 +17,4 @@
  height: 20px;
  vertical-align: 10px;
}
</style>
</style>
src/components/SizeSelect/index.vue
@@ -1,45 +1,44 @@
<template>
  <div>
    <el-dropdown trigger="click" @command="handleSetSize">
      <div class="size-icon--style">
        <svg-icon class-name="size-icon" icon-class="size" />
      </div>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
            {{ item.label }}
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</template>
<script setup>
<script setup lang="ts">
import useAppStore from "@/store/modules/app";
import { ComponentInternalInstance } from "vue";
const appStore = useAppStore();
const size = computed(() => appStore.size);
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const sizeOptions = ref([
  { label: "较大", value: "large" },
  { label: "默认", value: "default" },
  { label: "稍小", value: "small" },
]);
function handleSetSize(size) {
  proxy.$modal.loading("正在设置布局大小,请稍候...");
const handleSetSize = (size: string) => {
  proxy?.$modal.loading("正在设置布局大小,请稍候...");
  appStore.setSize(size);
  setTimeout("window.location.reload()", 1000);
}
</script>
<style lang='scss' scoped>
<template>
    <div>
        <el-dropdown trigger="click" @command="handleSetSize">
            <div class="size-icon--style">
                <svg-icon class-name="size-icon" icon-class="size" />
            </div>
            <template #dropdown>
                <el-dropdown-menu>
                    <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
                        {{ item.label }}
                    </el-dropdown-item>
                </el-dropdown-menu>
            </template>
        </el-dropdown>
    </div>
</template>
<style lang="scss" scoped>
.size-icon--style {
  font-size: 18px;
  line-height: 50px;
  padding-right: 7px;
}
</style>
</style>
src/components/SvgIcon/index.vue
@@ -1,39 +1,33 @@
<template>
  <svg :class="svgClass" aria-hidden="true">
    <use :xlink:href="iconName" :fill="color" />
  </svg>
</template>
<script>
export default defineComponent({
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    },
    color: {
      type: String,
      default: ''
    },
<script setup lang="ts">
const props = defineProps({
  iconClass: {
    type: String,
    required: true
  },
  setup(props) {
    return {
      iconName: computed(() => `#icon-${props.iconClass}`),
      svgClass: computed(() => {
        if (props.className) {
          return `svg-icon ${props.className}`
        }
        return 'svg-icon'
      })
    }
  className: {
    type: String,
    default: ''
  },
  color: {
    type: String,
    default: ''
  },
})
const iconName =  computed(() => `#icon-${props.iconClass}`);
const svgClass = computed(() => {
  if (props.className) {
    return `svg-icon ${props.className}`
  }
  return 'svg-icon'
})
</script>
<template>
    <svg :class="svgClass" aria-hidden="true">
        <use :xlink:href="iconName" :fill="color" />
    </svg>
</template>
<style scope lang="scss">
.sub-el-icon,
.nav-icon {
src/components/SvgIcon/svgicon.js
ÎļþÒÑɾ³ý
src/components/TopNav/index.vue
@@ -1,44 +1,15 @@
<template>
  <el-menu
    :default-active="activeMenu"
    mode="horizontal"
    @select="handleSelect"
    :ellipsis="false"
  >
    <template v-for="(item, index) in topMenus">
      <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
        ><svg-icon :icon-class="item.meta.icon" />
        {{ item.meta.title }}</el-menu-item
      >
    </template>
    <!-- é¡¶éƒ¨èœå•超出数量折叠 -->
    <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
      <template #title>更多菜单</template>
      <template v-for="(item, index) in topMenus">
        <el-menu-item
          :index="item.path"
          :key="index"
          v-if="index >= visibleNumber"
          ><svg-icon :icon-class="item.meta.icon" />
          {{ item.meta.title }}</el-menu-item
        >
      </template>
    </el-sub-menu>
  </el-menu>
</template>
<script setup>
import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
<script setup lang="ts">
import { constantRoutes } from '@/router';
import { isHttp } from '@/utils/validate';
import useAppStore from '@/store/modules/app';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
import { RouteOption } from 'vue-router';
// é¡¶éƒ¨æ åˆå§‹æ•°
const visibleNumber = ref(null);
const visibleNumber = ref<number>(-1);
// å½“前激活菜单的 index
const currentIndex = ref(null);
const currentIndex = ref<string>();
// éšè—ä¾§è¾¹æ è·¯ç”±
const hideList = ['/index', '/user/profile'];
@@ -55,12 +26,12 @@
// é¡¶éƒ¨æ˜¾ç¤ºèœå•
const topMenus = computed(() => {
  let topMenus = [];
  let topMenus:RouteOption[] = [];
  routers.value.map((menu) => {
    if (menu.hidden !== true) {
      // å…¼å®¹é¡¶éƒ¨æ ä¸€çº§èœå•内部跳转
      if (menu.path === "/") {
          topMenus.push(menu.children[0]);
          topMenus.push(menu.children? menu.children[0] : menu);
      } else {
          topMenus.push(menu);
      }
@@ -71,21 +42,21 @@
// è®¾ç½®å­è·¯ç”±
const childrenMenus = computed(() => {
  let childrenMenus = [];
  let childrenMenus:RouteOption[] = [];
  routers.value.map((router) => {
    for (let item in router.children) {
      if (router.children[item].parentPath === undefined) {
    router.children?.forEach((item) => {
      if (item.parentPath === undefined) {
        if(router.path === "/") {
          router.children[item].path = "/" + router.children[item].path;
          item.path = "/" + item.path;
        } else {
          if(!isHttp(router.children[item].path)) {
            router.children[item].path = router.path + "/" + router.children[item].path;
          if(!isHttp(item.path)) {
            item.path = router.path + "/" + item.path;
          }
        }
        router.children[item].parentPath = router.path;
        item.parentPath = router.path;
      }
      childrenMenus.push(router.children[item]);
    }
      childrenMenus.push(item);
    })
  })
  return constantRoutes.concat(childrenMenus);
})
@@ -108,12 +79,12 @@
  return activePath;
})
function setVisibleNumber() {
const setVisibleNumber = () => {
  const width = document.body.getBoundingClientRect().width / 3;
  visibleNumber.value = parseInt(width / 85);
  visibleNumber.value = parseInt(String(width / 85));
}
function handleSelect(key, keyPath) {
const handleSelect = (key: string, keyPath: string[]) => {
  currentIndex.value = key;
  const route = routers.value.find(item => item.path === key);
  if (isHttp(key)) {
@@ -121,7 +92,7 @@
    window.open(key, "_blank");
  } else if (!route || !route.children) {
    // æ²¡æœ‰å­è·¯ç”±è·¯å¾„内部打开
    router.push({ path: key });
    router.push({ path: key, fullPath: '' });
    appStore.toggleSideBarHide(true);
  } else {
    // æ˜¾ç¤ºå·¦ä¾§è”动菜单
@@ -130,8 +101,8 @@
  }
}
function activeRoutes(key) {
  let routes = [];
const activeRoutes = (key: string) => {
  let routes:RouteOption[] = [];
  if (childrenMenus.value && childrenMenus.value.length > 0) {
    childrenMenus.value.map((item) => {
      if (key == item.parentPath || (key == "index" && "" == item.path)) {
@@ -159,6 +130,26 @@
})
</script>
<template>
    <el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect" :ellipsis="false">
        <template v-for="(item, index) in topMenus">
            <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
                ><svg-icon :icon-class="item.meta ? item.meta.icon : '' " /> {{ item.meta?.title }}</el-menu-item
            >
        </template>
        <!-- é¡¶éƒ¨èœå•超出数量折叠 -->
        <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
            <template #title>更多菜单</template>
            <template v-for="(item, index) in topMenus">
                <el-menu-item :index="item.path" :key="index" v-if="index >= visibleNumber"
                    ><svg-icon :icon-class="item.meta ? item.meta.icon : '' " /> {{ item.meta?.title }}</el-menu-item
                >
            </template>
        </el-sub-menu>
    </el-menu>
</template>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
  float: left;
src/components/TreeSelect/index.vue
@@ -1,36 +1,5 @@
<template>
  <div class="el-tree-select">
    <el-select
      style="width: 100%"
      v-model="valueId"
      ref="treeSelect"
      :filterable="true"
      :clearable="true"
      @clear="clearHandle"
      :filter-method="selectFilterData"
      :placeholder="placeholder"
    >
      <el-option :value="valueId" :label="valueTitle">
        <el-tree
          id="tree-option"
          ref="selectTree"
          :accordion="accordion"
          :data="options"
          :props="objMap"
          :node-key="objMap.value"
          :expand-on-click-node="false"
          :default-expanded-keys="defaultExpandedKey"
          :filter-node-method="filterNode"
          @node-click="handleNodeClick"
        ></el-tree>
      </el-option>
    </el-select>
  </div>
</template>
<script setup>
const { proxy } = getCurrentInstance();
<script setup lang="ts">
import { ElTreeSelect } from 'element-plus'
const props = defineProps({
  /* é…ç½®é¡¹ */
@@ -68,6 +37,9 @@
  }
})
const selectTree = ref(ElTreeSelect);
const emit = defineEmits(['update:value']);
const valueId = computed({
@@ -77,16 +49,16 @@
  }
});
const valueTitle = ref('');
const defaultExpandedKey = ref([]);
const defaultExpandedKey = ref<any[]>([]);
function initHandle() {
  nextTick(() => {
    const selectedValue = valueId.value;
    if(selectedValue !== null && typeof (selectedValue) !== 'undefined') {
      const node = proxy.$refs.selectTree.getNode(selectedValue)
      const node = selectTree.value.getNode(selectedValue)
      if (node) {
        valueTitle.value = node.data[props.objMap.label]
        proxy.$refs.selectTree.setCurrentKey(selectedValue) // è®¾ç½®é»˜è®¤é€‰ä¸­
        selectTree.value.setCurrentKey(selectedValue) // è®¾ç½®é»˜è®¤é€‰ä¸­
        defaultExpandedKey.value = [selectedValue] // è®¾ç½®é»˜è®¤å±•å¼€
      }
    } else {
@@ -94,17 +66,17 @@
    }
  })
}
function handleNodeClick(node) {
function handleNodeClick(node: any) {
  valueTitle.value = node[props.objMap.label]
  valueId.value = node[props.objMap.value];
  defaultExpandedKey.value = [];
  proxy.$refs.treeSelect.blur()
  selectTree.value.blur()
  selectFilterData('')
}
function selectFilterData(val) {
  proxy.$refs.selectTree.filter(val)
function selectFilterData(val: any) {
  selectTree.value.filter(val)
}
function filterNode(value, data) {
function filterNode(value: any, data: any) {
  if (!value) return true
  return data[props.objMap['label']].indexOf(value) !== -1
}
@@ -128,7 +100,7 @@
})
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
@import "@/assets/styles/variables.module.scss";
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
  padding: 0;
@@ -153,4 +125,34 @@
  background-color: mix(#fff, $--color-primary, 90%);
  color: $--color-primary;
}
</style>
</style>
<template>
    <div class="el-tree-select">
        <el-select
            style="width: 100%"
            v-model="valueId"
            ref="treeSelect"
            :filterable="true"
            :clearable="true"
            @clear="clearHandle"
            :filter-method="selectFilterData"
            :placeholder="placeholder"
        >
            <el-option :value="valueId" :label="valueTitle">
                <el-tree
                    id="tree-option"
                    ref="selectTree"
                    :accordion="accordion"
                    :data="options"
                    :props="objMap"
                    :node-key="objMap.value"
                    :expand-on-click-node="false"
                    :default-expanded-keys="defaultExpandedKey"
                    :filter-node-method="filterNode"
                    @node-click="handleNodeClick"
                ></el-tree>
            </el-option>
        </el-select>
    </div>
</template>
src/components/iFrame/index.vue
@@ -1,14 +1,4 @@
<template>
  <div v-loading="loading" :style="'height:' + height">
    <iframe
      :src="url"
      frameborder="no"
      style="width: 100%; height: 100%"
      scrolling="auto" />
  </div>
</template>
<script setup>
<script setup lang="ts">
const props = defineProps({
  src: {
    type: String,
@@ -29,3 +19,9 @@
  };
})
</script>
<template>
    <div v-loading="loading" :style="'height:' + height">
        <iframe :src="url" frameborder="no" style="width: 100%; height: 100%" scrolling="auto" />
    </div>
</template>
src/directive/common/copyText.js
ÎļþÒÑɾ³ý
src/directive/common/copyText.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
/**
 * v-copyText å¤åˆ¶æ–‡æœ¬å†…容
 * Copyright (c) 2022 ruoyi
 */
export default {
    beforeMount(el: any, { value, arg }: any) {
        if (arg === 'callback') {
            el.$copyCallback = value;
        } else {
            el.$copyValue = value;
            const handler = () => {
                copyTextToClipboard(el.$copyValue);
                if (el.$copyCallback) {
                    el.$copyCallback(el.$copyValue);
                }
            };
            el.addEventListener('click', handler);
            el.$destroyCopy = () => el.removeEventListener('click', handler);
        }
    }
};
function copyTextToClipboard(input: string, { target = document.body } = {}) {
    const element = document.createElement('textarea');
    const previouslyFocusedElement = document.activeElement as HTMLInputElement;
    element.value = input;
    // Prevent keyboard from showing on mobile
    element.setAttribute('readonly', '');
    element.style.contain = 'strict';
    element.style.position = 'absolute';
    element.style.left = '-9999px';
    element.style.fontSize = '12pt'; // Prevent zooming on iOS
    const selection = document.getSelection();
    let originalRange;
    if (selection) {
        originalRange = selection?.rangeCount > 0 && selection.getRangeAt(0);
    }
    target.append(element);
    element.select();
    // Explicit selection workaround for iOS
    element.selectionStart = 0;
    element.selectionEnd = input.length;
    let isSuccess = false;
    try {
        isSuccess = document.execCommand('copy');
    } catch (err) {
        console.error(err);
    }
    element.remove();
    if (originalRange) {
        selection?.removeAllRanges();
        selection?.addRange(originalRange);
    }
    // Get the focus back on the previously focused element, if any
    if (previouslyFocusedElement) {
        previouslyFocusedElement.focus();
    }
    return isSuccess;
}
src/directive/index.js
ÎļþÒÑɾ³ý
src/directive/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import copyText from './common/copyText';
import { hasPermi, hasRoles } from './permission';
import { App } from 'vue';
export default (app: App) => {
    app.directive('copyText', copyText);
    app.directive('hasPermi', hasPermi);
    app.directive('hasRoles', hasRoles);
};
src/directive/permission/hasPermi.js
ÎļþÒÑɾ³ý
src/directive/permission/hasRole.js
ÎļþÒÑɾ³ý
src/directive/permission/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
import { Directive, DirectiveBinding } from 'vue';
import useUserStore from '@/store/modules/user';
/**
 * æ“ä½œæƒé™å¤„理
 */
export const hasPermi: Directive = {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        const { permissions } = useUserStore();
        // ã€Œå…¶ä»–角色」按钮权限校验
        const { value } = binding;
        if (value && value instanceof Array && value.length > 0) {
            const hasPermission = permissions.some((permi) => {
                return permi === '*:*:*' || value.includes(permi);
            });
            if (!hasPermission) {
                el.parentNode && el.parentNode.removeChild(el);
                return false;
            }
        } else {
            throw new Error("check perms! Like v-has-permi=\"['sys:user:add','sys:user:edit']\"");
        }
    }
};
/**
 * è§’色权限处理
 */
export const hasRoles: Directive = {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
        const { value } = binding;
        const { roles } = useUserStore();
        if (value && value instanceof Array && value.length > 0) {
            const hasRole = roles.some((role) => {
                return role === 'admin' || value.includes(role);
            });
            if (!hasRole) {
                el.parentNode && el.parentNode.removeChild(el);
                return false;
            }
        } else {
            throw new Error("check roles! Like v-has-roles=\"['admin','test']\"");
        }
    }
};
src/enums/MenuTypeEnum.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
export enum MenuTypeEnum {
    /**
     * ç›®å½•
     */
    M = 'M',
    /**
     * èœå•
     */
    C = 'C',
    /**
     * æŒ‰é’®
     */
    F = 'F'
}
src/enums/RespEnum.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
export enum HttpStatus {
    /**
     * æ“ä½œæˆåŠŸ
     */
    SUCCESS = 200,
    /**
     * å¯¹è±¡åˆ›å»ºæˆåŠŸ
     */
    CREATED = 201,
    /**
     * è¯·æ±‚已经被接受
     */
    ACCEPTED = 202,
    /**
     * æ“ä½œå·²ç»æ‰§è¡ŒæˆåŠŸï¼Œä½†æ˜¯æ²¡æœ‰è¿”å›žæ•°æ®
     */
    NO_CONTENT = 204,
    /**
     * èµ„源已经被移除
     */
    MOVED_PERM = 301,
    /**
     * é‡å®šå‘
     */
    SEE_OTHER = 303,
    /**
     * èµ„源没有被修改
     */
    NOT_MODIFIED = 304,
    /**
     * å‚数列表错误(缺少,格式不匹配)
     */
    PARAM_ERROR = 400,
    /**
     * æœªæŽˆæƒ
     */
    UNAUTHORIZED = 401,
    /**
     * è®¿é—®å—限,授权过期
     */
    FORBIDDEN = 403,
    /**
     * èµ„源,服务未找到
     */
    NOT_FOUND = 404,
    /**
     * ä¸å…è®¸çš„http方法
     */
    BAD_METHOD = 405,
    /**
     * èµ„源冲突,或者资源被锁
     */
    CONFLICT = 409,
    /**
     * ä¸æ”¯æŒçš„æ•°æ®ï¼Œåª’体类型
     */
    UNSUPPORTED_TYPE = 415,
    /**
     * ç³»ç»Ÿå†…部错误
     */
    SERVER_ERROR = 500,
    /**
     * æŽ¥å£æœªå®žçް
     */
    NOT_IMPLEMENTED = 501,
    /**
     * æœåŠ¡ä¸å¯ç”¨ï¼Œè¿‡è½½æˆ–è€…ç»´æŠ¤
     */
    BAD_GATEWAY = 502,
    /**
     * ç½‘关超时
     */
    GATEWAY_TIMEOUT = 504,
    /**
     * æœªçŸ¥é”™è¯¯
     */
    UNKNOWN_ERROR = 520,
    /**
     * æœåŠ¡æœªçŸ¥é”™è¯¯
     */
    SERVICE_ERROR = 521,
    /**
     * æ•°æ®åº“未知错误
     */
    DATABASE_ERROR = 522,
    /**
     * ç³»ç»Ÿè­¦å‘Šæ¶ˆæ¯
     */
    WARN = 601
}
src/enums/SettingTypeEnum.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
export enum SettingTypeEnum {
    TITLE = 'title',
    THEME = 'theme',
    SIDE_THEME = 'sideTheme',
    SHOW_SETTINGS = 'showSettings',
    TOP_NAV = 'topNav',
    TAGS_VIEW = 'tagsView',
    FIXED_HEADER = 'fixedHeader',
    SIDEBAR_LOGO = 'sidebarLogo',
    DYNAMIC_TITLE = 'dynamicTitle',
    ANIMATION_ENABLE = 'animationEnable',
    LAYOUT = 'layout',
    DARK = 'dark',
    LAYOUT_SETTING = 'layout-setting'
}
src/enums/layout/LayoutEnum.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
export enum ThemeEnum {
    DARK = 'theme-dark',
    LIGHT = 'theme-light'
}
src/lang/en.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
export default {
    // è·¯ç”±å›½é™…化
    route: {
        dashboard: 'Dashboard',
        document: 'Document'
    },
    // ç™»å½•页面国际化
    login: {
        title: 'vue3-element-admin',
        username: 'Username',
        password: 'Password',
        login: 'Login',
        code: 'Verification Code',
        copyright: '',
        icp: '',
        thirdPartyLogin: 'third-party login'
    },
    // å¯¼èˆªæ å›½é™…化
    navbar: {
        dashboard: 'Dashboard',
        logout: 'Logout',
        document: 'Document',
        gitee: 'Gitee'
    }
};
src/lang/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
// è‡ªå®šä¹‰å›½é™…化配置
import { createI18n } from 'vue-i18n';
// æœ¬åœ°è¯­è¨€åŒ…
import enLocale from './en';
import zhCnLocale from './zh-cn';
const messages = {
    'zh-cn': {
        ...zhCnLocale
    },
    en: {
        ...enLocale
    }
};
/**
 * èŽ·å–å½“å‰ç³»ç»Ÿä½¿ç”¨è¯­è¨€å­—ç¬¦ä¸²
 *
 * @returns zh-cn|en ...
 */
export const getLanguage = () => {
    // æœ¬åœ°ç¼“存获取
    let language = localStorage.getItem('language');
    if (language) {
        return language;
    }
    // æµè§ˆå™¨ä½¿ç”¨è¯­è¨€
    language = navigator.language.toLowerCase();
    const locales = Object.keys(messages);
    for (const locale of locales) {
        if (language.indexOf(locale) > -1) {
            return locale;
        }
    }
    return 'zh-cn';
};
const i18n = createI18n({
    legacy: false,
    locale: getLanguage(),
    messages: messages
});
export default i18n;
src/lang/zh-cn.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
export default {
    // è·¯ç”±å›½é™…化
    route: {
        dashboard: '首页',
        document: '项目文档'
    },
    // ç™»å½•页面国际化
    login: {
        title: 'vue3-element-admin',
        username: '用户名',
        password: '密码',
        login: '登 å½•',
        code: '请输入验证码',
        copyright: '',
        icp: '',
        thirdPartyLogin: '第三方登录'
    },
    navbar: {
        dashboard: '首页',
        logout: '注销',
        document: '项目文档',
        gitee: '码云'
    }
};
src/layout/components/AppMain.vue
@@ -1,22 +1,42 @@
<template>
  <section class="app-main">
    <router-view v-slot="{ Component, route }">
      <transition name="fade-transform" mode="out-in">
        <keep-alive :include="tagsViewStore.cachedViews">
          <component v-if="!route.meta.link" :is="Component" :key="route.path"/>
        </keep-alive>
      </transition>
    </router-view>
    <iframe-toggle />
  </section>
</template>
<script setup>
import iframeToggle from "./IframeToggle/index"
import useTagsViewStore from '@/store/modules/tagsView'
const tagsViewStore = useTagsViewStore()
<script lang="ts">
export default {
  name: 'AppMin'
}
</script>
<script setup lang="ts">
import useTagsViewStore from '@/store/modules/tagsView';
import useSettingsStore from '@/store/modules/settings';
import IframeToggle  from './IframeToggle/index.vue'
import { ComponentInternalInstance } from "vue";
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const tagsViewStore = useTagsViewStore();
// éšæœºåŠ¨ç”»é›†åˆ
const animante = ref<string>('');
const animationEnable = ref(useSettingsStore().animationEnable);
watch(()=> useSettingsStore().animationEnable, (val) => {
  animationEnable.value = val;
  if (val) {
    animante.value = proxy?.animate.animateList[Math.round(Math.random() * proxy?.animate.animateList.length)] as string;
  } else {
    animante.value = proxy?.animate.defaultAnimate as string;
  }
}, { immediate: true });
</script>
<template>
    <section class="app-main">
        <router-view v-slot="{ Component, route }">
            <transition :enter-active-class="animante" mode="out-in">
                <keep-alive :include="tagsViewStore.cachedViews">
                    <component v-if="!route.meta.link" :is="Component" :key="route.path" />
                </keep-alive>
            </transition>
        </router-view>
        <iframe-toggle />
    </section>
</template>
<style lang="scss" scoped>
.app-main {
@@ -27,7 +47,7 @@
  overflow: hidden;
}
.fixed-header + .app-main {
.fixed-header+.app-main {
  padding-top: 50px;
}
@@ -37,7 +57,7 @@
    min-height: calc(100vh - 84px);
  }
  .fixed-header + .app-main {
  .fixed-header+.app-main {
    padding-top: 84px;
  }
}
@@ -50,4 +70,4 @@
    padding-right: 17px;
  }
}
</style>
</style>
src/layout/components/IframeToggle/index.vue
@@ -1,19 +1,19 @@
<template>
  <transition-group name="fade-transform" mode="out-in">
    <inner-link
      v-for="(item, index) in tagsViewStore.iframeViews"
      :key="item.path"
      :iframeId="'iframe' + index"
      v-show="route.path === item.path"
      :src="item.meta.link"
    ></inner-link>
  </transition-group>
</template>
<script setup>
import InnerLink from "../InnerLink/index"
import useTagsViewStore from '@/store/modules/tagsView'
<script setup lang="ts">
import InnerLink from "../InnerLink/index.vue";
import useTagsViewStore from '@/store/modules/tagsView';
const route = useRoute();
const tagsViewStore = useTagsViewStore()
</script>
<template>
    <transition-group name="fade-transform" mode="out-in">
        <inner-link
            v-for="(item, index) in tagsViewStore.iframeViews"
            :key="item.path"
            :iframeId="'iframe' + index"
            v-show="route.path === item.path"
            :src="item.meta ? item.meta.link : ''"
        ></inner-link>
    </transition-group>
</template>
src/layout/components/InnerLink/index.vue
@@ -1,15 +1,4 @@
<template>
  <div :style="'height:' + height">
    <iframe
      :id="iframeId"
      style="width: 100%; height: 100%"
      :src="src"
      frameborder="no"
    ></iframe>
  </div>
</template>
<script setup>
<script setup lang="ts">
const props = defineProps({
  src: {
    type: String,
@@ -19,6 +8,11 @@
    type: String
  }
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
</script>
<template>
    <div :style="'height:' + height">
        <iframe :id="iframeId" style="width: 100%; height: 100%" :src="src" frameborder="no"></iframe>
    </div>
</template>
src/layout/components/Navbar.vue
@@ -1,165 +1,155 @@
<template>
  <div class="navbar">
    <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
    <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
    <div class="right-menu flex align-center">
      <template v-if="appStore.device !== 'mobile'">
        <el-select v-model="companyName"
                   clearable
                   filterable
                   reserve-keyword
                   placeholder="请选择租户"
                   v-if="userId === 1"
                   @change="dynamicTenantEvent"
                   @clear="dynamicClearEvent">
          <el-option
              v-for="item in tenantList"
              :key="item.tenantId"
              :label="item.companyName"
              :value="item.tenantId">
          </el-option>
          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
        </el-select>
        <header-search id="header-search" class="right-menu-item" />
        <el-tooltip content="源码地址" effect="dark" placement="bottom">
          <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
        </el-tooltip>
        <el-tooltip content="文档地址" effect="dark" placement="bottom">
          <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
        </el-tooltip>
        <screenfull id="screenfull" class="right-menu-item hover-effect" />
        <el-tooltip content="布局大小" effect="dark" placement="bottom">
          <size-select id="size-select" class="right-menu-item hover-effect" />
        </el-tooltip>
      </template>
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
          <div class="avatar-wrapper">
            <img :src="userStore.avatar" class="user-avatar" />
            <el-icon><caret-bottom /></el-icon>
          </div>
          <template #dropdown>
            <el-dropdown-menu>
              <router-link to="/user/profile" v-if="!dynamic">
                <el-dropdown-item>个人中心</el-dropdown-item>
              </router-link>
              <el-dropdown-item command="setLayout">
                <span>布局设置</span>
              </el-dropdown-item>
              <el-dropdown-item divided command="logout">
                <span>退出登录</span>
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ElMessageBox } from 'element-plus'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
<script setup lang="ts">
import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import { getTenantList } from "@/api/login";
import { dynamicClear, dynamicTenant } from "@/api/system/tenant";
import { ComponentInternalInstance } from "vue";
import { TenantVO } from "@/api/types";
const appStore = useAppStore()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const userId = ref(userStore.userId);
const companyName = ref(undefined);
const tenantList = ref([]);
const tenantList = ref<TenantVO[]>([]);
// æ˜¯å¦åˆ‡æ¢äº†ç§Ÿæˆ·
const dynamic = ref(false);
// ç§Ÿæˆ·å¼€å…³
const tenantEnabled = ref(true);
// åŠ¨æ€åˆ‡æ¢
function dynamicTenantEvent(tenantId) {
const dynamicTenantEvent = async (tenantId: string) => {
  if (companyName.value != null && companyName.value !== '') {
    dynamicTenant(tenantId).then(res => {
      dynamic.value = true;
      proxy.$tab.closeAllPage()
      proxy.$router.push('/')
    });
    await dynamicTenant(tenantId);
    dynamic.value = true;
    proxy?.$tab.closeAllPage();
    proxy?.$router.push('/');
  }
}
function dynamicClearEvent() {
  dynamicClear().then(res => {
    dynamic.value = false;
    proxy.$tab.closeAllPage()
    proxy.$router.push('/')
  });
const dynamicClearEvent = async () => {
  await dynamicClear();
  dynamic.value = false;
  proxy?.$tab.closeAllPage();
  proxy?.$router.push('/')
}
// ç§Ÿæˆ·åˆ—表
function initTenantList() {
  getTenantList().then(res => {
    tenantList.value = res.data;
  });
/** ç§Ÿæˆ·åˆ—表 */
const initTenantList = async () => {
  const { data } = await getTenantList();
  tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
  if (tenantEnabled.value) {
      tenantList.value = data.voList;
  }
}
defineExpose({
  initTenantList,
})
function toggleSideBar() {
const toggleSideBar = () => {
  appStore.toggleSideBar()
}
function handleCommand(command) {
  switch (command) {
    case "setLayout":
      setLayout();
      break;
    case "logout":
      logout();
      break;
    default:
      break;
  }
}
function logout() {
  ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
const logout = async () => {
  await ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    userStore.logOut().then(() => {
      location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
    })
  }).catch(() => { });
  })
  await userStore.logout()
  location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
}
const emits = defineEmits(['setLayout'])
function setLayout() {
const setLayout = () => {
  emits('setLayout');
}
// å®šä¹‰Command方法对象 é€šè¿‡key直接调用方法
const commandMap: {[key: string]: any} = {
  setLayout,
  logout
};
const handleCommand = (command: string) => {
  // åˆ¤æ–­æ˜¯å¦å­˜åœ¨è¯¥æ–¹æ³•
  if (commandMap[command]) {
    commandMap[command]();
  }
}
</script>
<style lang='scss' scoped>
<template>
    <div class="navbar">
        <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
        <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
        <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
:deep .el-select .el-input__wrapper {
        <div class="right-menu flex align-center">
            <template v-if="appStore.device !== 'mobile'">
                <el-select
                    v-model="companyName"
                    clearable
                    filterable
                    reserve-keyword
                    placeholder="请选择租户"
                    v-if="userId === 1 && tenantEnabled"
                    @change="dynamicTenantEvent"
                    @clear="dynamicClearEvent"
                >
                    <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
                    <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
                </el-select>
                <header-search id="header-search" class="right-menu-item" />
                <el-tooltip content="源码地址" effect="dark" placement="bottom">
                    <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
                </el-tooltip>
                <el-tooltip content="文档地址" effect="dark" placement="bottom">
                    <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
                </el-tooltip>
                <el-tooltip content="全屏" effect="dark" placement="bottom">
                    <screenfull id="screenfull" class="right-menu-item hover-effect" />
                </el-tooltip>
                <el-tooltip content="布局大小" effect="dark" placement="bottom">
                    <size-select id="size-select" class="right-menu-item hover-effect" />
                </el-tooltip>
            </template>
            <div class="avatar-container">
                <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
                    <div class="avatar-wrapper">
                        <img :src="userStore.avatar" class="user-avatar" />
                        <el-icon><caret-bottom /></el-icon>
                    </div>
                    <template #dropdown>
                        <el-dropdown-menu>
                            <router-link to="/user/profile" v-if="!dynamic">
                                <el-dropdown-item>个人中心</el-dropdown-item>
                            </router-link>
                            <el-dropdown-item command="setLayout">
                                <span>布局设置</span>
                            </el-dropdown-item>
                            <el-dropdown-item divided command="logout">
                                <span>退出登录</span>
                            </el-dropdown-item>
                        </el-dropdown-menu>
                    </template>
                </el-dropdown>
            </div>
        </div>
    </div>
</template>
<style lang="scss" scoped>
:deep(.el-select .el-input__wrapper) {
  height:30px;
}
src/layout/components/Settings/index.vue
@@ -1,95 +1,13 @@
<template>
  <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
    <div class="setting-drawer-title">
      <h3 class="drawer-title">主题风格设置</h3>
    </div>
    <div class="setting-drawer-block-checbox">
      <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
        <img src="@/assets/images/dark.svg" alt="dark" />
        <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
          <i aria-label="图标: check" class="anticon anticon-check">
            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
              <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
            </svg>
          </i>
        </div>
      </div>
      <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
        <img src="@/assets/images/light.svg" alt="light" />
        <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
          <i aria-label="图标: check" class="anticon anticon-check">
            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
              <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
            </svg>
          </i>
        </div>
      </div>
    </div>
    <div class="drawer-item">
      <span>主题颜色</span>
      <span class="comp-style">
        <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
      </span>
    </div>
    <el-divider />
    <h3 class="drawer-title">系统布局配置</h3>
    <div class="drawer-item">
      <span>开启 TopNav</span>
      <span class="comp-style">
        <el-switch v-model="topNav" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>开启 Tags-Views</span>
      <span class="comp-style">
        <el-switch v-model="tagsView" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>固定 Header</span>
      <span class="comp-style">
        <el-switch v-model="fixedHeader" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>显示 Logo</span>
      <span class="comp-style">
        <el-switch v-model="sidebarLogo" class="drawer-switch" />
      </span>
    </div>
    <div class="drawer-item">
      <span>动态标题</span>
      <span class="comp-style">
        <el-switch v-model="dynamicTitle" class="drawer-switch" />
      </span>
    </div>
    <el-divider />
    <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
    <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
  </el-drawer>
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import originElementPlus from 'element-plus/theme-chalk/index.css'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
<script setup lang="ts">
import { useDynamicTitle } from '@/utils/dynamicTitle'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { handleThemeStyle } from '@/utils/theme'
import { ComponentInternalInstance } from "vue";
import { SettingTypeEnum } from "@/enums/SettingTypeEnum";
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
@@ -103,7 +21,7 @@
const topNav = computed({
  get: () => storeSettings.value.topNav,
  set: (val) => {
    settingsStore.changeSetting({ key: 'topNav', value: val })
    settingsStore.changeSetting({ key: SettingTypeEnum.TOP_NAV, value: val })
    if (!val) {
      appStore.toggleSideBarHide(false);
      permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
@@ -114,44 +32,46 @@
const tagsView = computed({
  get: () => storeSettings.value.tagsView,
  set: (val) => {
    settingsStore.changeSetting({ key: 'tagsView', value: val })
    settingsStore.changeSetting({ key: SettingTypeEnum.TAGS_VIEW, value: val })
  }
})
/**是否需要固定头部 */
const fixedHeader = computed({
  get: () => storeSettings.value.fixedHeader,
  set: (val) => {
    settingsStore.changeSetting({ key: 'fixedHeader', value: val })
    settingsStore.changeSetting({ key: SettingTypeEnum.FIXED_HEADER, value: val })
  }
})
/**是否需要侧边栏的logo */
const sidebarLogo = computed({
  get: () => storeSettings.value.sidebarLogo,
  set: (val) => {
    settingsStore.changeSetting({ key: 'sidebarLogo', value: val })
    settingsStore.changeSetting({ key: SettingTypeEnum.SIDEBAR_LOGO, value: val })
  }
})
/**是否需要侧边栏的动态网页的title */
const dynamicTitle = computed({
  get: () => storeSettings.value.dynamicTitle,
  set: (val) => {
    settingsStore.changeSetting({ key: 'dynamicTitle', value: val })
    settingsStore.changeSetting({ key: SettingTypeEnum.DYNAMIC_TITLE, value: val })
    // åŠ¨æ€è®¾ç½®ç½‘é¡µæ ‡é¢˜
    useDynamicTitle()
  }
})
function themeChange(val) {
  settingsStore.changeSetting({ key: 'theme', value: val })
const themeChange = (val: string | null) => {
  settingsStore.changeSetting({ key: SettingTypeEnum.THEME, value: val })
  theme.value = val;
  handleThemeStyle(val);
  if (val) {
    handleThemeStyle(val);
  }
}
function handleTheme(val) {
  settingsStore.changeSetting({ key: 'sideTheme', value: val })
const handleTheme = (val: string) => {
  settingsStore.changeSetting({ key: SettingTypeEnum.SIDE_THEME, value: val })
  sideTheme.value = val;
}
function saveSetting() {
  proxy.$modal.loading("正在保存到本地,请稍候...");
const saveSetting = () => {
  proxy?.$modal.loading("正在保存到本地,请稍候...");
  let layoutSetting = {
    "topNav": storeSettings.value.topNav,
    "tagsView": storeSettings.value.tagsView,
@@ -162,14 +82,14 @@
    "theme": storeSettings.value.theme
  };
  localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
  setTimeout(proxy.$modal.closeLoading(), 1000)
  setTimeout(() => {proxy?.$modal.closeLoading()}, 1000)
}
function resetSetting() {
  proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
const resetSetting = () => {
  proxy?.$modal.loading("正在清除设置缓存并刷新,请稍候...");
  localStorage.removeItem("layout-setting")
  setTimeout("window.location.reload()", 1000)
}
function openSetting() {
const openSetting = () => {
  showSettings.value = true;
}
@@ -178,7 +98,90 @@
})
</script>
<style lang='scss' scoped>
<template>
    <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
        <div class="setting-drawer-title">
            <h3 class="drawer-title">主题风格设置</h3>
        </div>
        <div class="setting-drawer-block-checbox">
            <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
                <img src="@/assets/images/dark.svg" alt="dark" />
                <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
                    <i aria-label="图标: check" class="anticon anticon-check">
                        <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
                            <path
                                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
                            />
                        </svg>
                    </i>
                </div>
            </div>
            <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
                <img src="@/assets/images/light.svg" alt="light" />
                <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
                    <i aria-label="图标: check" class="anticon anticon-check">
                        <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
                            <path
                                d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
                            />
                        </svg>
                    </i>
                </div>
            </div>
        </div>
        <div class="drawer-item">
            <span>主题颜色</span>
            <span class="comp-style">
                <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange" />
            </span>
        </div>
        <el-divider />
        <h3 class="drawer-title">系统布局配置</h3>
        <div class="drawer-item">
            <span>开启 TopNav</span>
            <span class="comp-style">
                <el-switch v-model="topNav" class="drawer-switch" />
            </span>
        </div>
        <div class="drawer-item">
            <span>开启 Tags-Views</span>
            <span class="comp-style">
                <el-switch v-model="tagsView" class="drawer-switch" />
            </span>
        </div>
        <div class="drawer-item">
            <span>固定 Header</span>
            <span class="comp-style">
                <el-switch v-model="fixedHeader" class="drawer-switch" />
            </span>
        </div>
        <div class="drawer-item">
            <span>显示 Logo</span>
            <span class="comp-style">
                <el-switch v-model="sidebarLogo" class="drawer-switch" />
            </span>
        </div>
        <div class="drawer-item">
            <span>动态标题</span>
            <span class="comp-style">
                <el-switch v-model="dynamicTitle" class="drawer-switch" />
            </span>
        </div>
        <el-divider />
        <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
        <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
    </el-drawer>
</template>
<style lang="scss" scoped>
.setting-drawer-title {
  margin-bottom: 12px;
  color: rgba(0, 0, 0, 0.85);
@@ -238,4 +241,4 @@
    margin: -3px 8px 0px 0px;
  }
}
</style>
</style>
src/layout/components/Sidebar/Link.vue
@@ -1,10 +1,4 @@
<template>
  <component :is="type" v-bind="linkProps()">
    <slot />
  </component>
</template>
<script setup>
<script setup lang="ts">
import { isExternal } from '@/utils/validate'
const props = defineProps({
@@ -15,7 +9,7 @@
})
const isExt = computed(() => {
  return isExternal(props.to)
  return isExternal(props.to as string)
})
const type = computed(() => {
@@ -38,3 +32,9 @@
  }
}
</script>
<template>
    <component :is="type" v-bind="linkProps()">
        <slot />
    </component>
</template>
src/layout/components/Sidebar/Logo.vue
@@ -1,22 +1,9 @@
<template>
  <div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
    <transition name="sidebarLogoFade">
      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
        <img v-if="logo" :src="logo" class="sidebar-logo" />
        <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
      </router-link>
      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
        <img v-if="logo" :src="logo" class="sidebar-logo" />
        <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
      </router-link>
    </transition>
  </div>
</template>
<script setup>
<script setup lang="ts">
import variables from '@/assets/styles/variables.module.scss'
import logo from '@/assets/logo/logo.png'
import useSettingsStore from '@/store/modules/settings'
import { ComponentInternalInstance } from "vue";
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
defineProps({
  collapse: {
@@ -29,6 +16,29 @@
const settingsStore = useSettingsStore();
const sideTheme = computed(() => settingsStore.sideTheme);
</script>
<template>
    <div
        class="sidebar-logo-container"
        :class="{ 'collapse': collapse }"
        :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
    >
        <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
            <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
                <img v-if="logo" :src="logo" class="sidebar-logo" />
                <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
                    {{ title }}
                </h1>
            </router-link>
            <router-link v-else key="expand" class="sidebar-logo-link" to="/">
                <img v-if="logo" :src="logo" class="sidebar-logo" />
                <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
                    {{ title }}
                </h1>
            </router-link>
        </transition>
    </div>
</template>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
@@ -78,4 +88,4 @@
    }
  }
}
</style>
</style>
src/layout/components/Sidebar/SidebarItem.vue
@@ -1,41 +1,14 @@
<template>
  <div v-if="!item.hidden">
    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
        </el-menu-item>
      </app-link>
    </template>
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <template v-if="item.meta" #title>
        <svg-icon :icon-class="item.meta && item.meta.icon" />
        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
      </template>
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-sub-menu>
  </div>
</template>
<script setup>
<script setup lang="ts">
import { isExternal } from '@/utils/validate'
import AppLink from './Link'
import AppLink from './Link.vue'
import { getNormalPath } from '@/utils/ruoyi'
import { RouteOption } from "vue-router";
import { PropType } from "vue";
const props = defineProps({
  // route object
  item: {
    type: Object,
    type: Object as PropType<RouteOption>,
    required: true
  },
  isNest: {
@@ -48,9 +21,9 @@
  }
})
const onlyOneChild = ref({});
const onlyOneChild = ref<any>({});
function hasOneShowingChild(children = [], parent) {
const hasOneShowingChild = (children:RouteOption[] = [], parent: RouteOption) => {
  if (!children) {
    children = [];
  }
@@ -78,7 +51,7 @@
  return false
};
function resolvePath(routePath, routeQuery) {
const resolvePath = (routePath:string, routeQuery?:string): any => {
  if (isExternal(routePath)) {
    return routePath
  }
@@ -92,11 +65,41 @@
  return getNormalPath(props.basePath + '/' + routePath)
}
function hasTitle(title){
  if (title.length > 5) {
    return title;
  } else {
const hasTitle = (title: string | undefined): string => {
  if(!title || title.length <= 5) {
    return "";
  }
  return title;
}
</script>
<template>
    <div v-if="!item.hidden">
        <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
            <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
                <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
                    <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
                    <template #title>
                        <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
                    </template>
                </el-menu-item>
            </app-link>
        </template>
        <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
            <template v-if="item.meta" #title>
                <svg-icon :icon-class="item.meta ? item.meta.icon : '' " />
                <span class="menu-title" :title="hasTitle(item.meta?.title)">{{ item.meta?.title }}</span>
            </template>
            <sidebar-item
                v-for="child in item.children"
                :key="child.path"
                :is-nest="true"
                :item="child as RouteOption"
                :base-path="resolvePath(child.path)"
                class="nest-menu"
            />
        </el-sub-menu>
    </div>
</template>
src/layout/components/Sidebar/index.vue
@@ -1,35 +1,12 @@
<template>
  <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
        :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
        :unique-opened="true"
        :active-text-color="theme"
        :collapse-transition="false"
        mode="vertical"
      >
        <sidebar-item
          v-for="(route, index) in sidebarRouters"
          :key="route.path + index"
          :item="route"
          :base-path="route.path"
        />
      </el-menu>
    </el-scrollbar>
  </div>
</template>
<script setup>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
<script setup lang="ts">
import Logo from './Logo.vue'
import SidebarItem from './SidebarItem.vue'
import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { ComponentInternalInstance } from "vue";
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const appStore = useAppStore()
@@ -47,8 +24,32 @@
  // if set path, the sidebar will highlight the path you set
  if (meta.activeMenu) {
    return meta.activeMenu;
  }
    }
  return path;
})
const bgColor = computed(() => sideTheme.value === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground);
const textColor = computed(() => sideTheme.value === 'theme-dark' ? variables.menuColor : variables.menuLightColor);
</script>
<template>
    <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: bgColor }">
        <logo v-if="showLogo" :collapse="isCollapse" />
        <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
            <transition :enter-active-class="proxy?.animate.menuSearchAnimate.enter" mode="out-in">
                <el-menu
                    :default-active="activeMenu as string"
                    :collapse="isCollapse"
                    :background-color="bgColor"
                    :text-color="textColor"
                    :unique-opened="true"
                    :active-text-color="theme"
                    :collapse-transition="false"
                    mode="vertical"
                >
                    <sidebar-item v-for="(route, index) in sidebarRouters" :key="route.path + index" :item="route" :base-path="route.path" />
                </el-menu>
            </transition>
        </el-scrollbar>
    </div>
</template>
src/layout/components/TagsView/ScrollPane.vue
@@ -1,21 +1,11 @@
<template>
  <el-scrollbar
    ref="scrollContainer"
    :vertical="false"
    class="scroll-container"
    @wheel.prevent="handleScroll"
  >
    <slot />
  </el-scrollbar>
</template>
<script setup>
<script setup lang="ts">
import useTagsViewStore from '@/store/modules/tagsView'
import { ElScrollbar } from 'element-plus';
import { TagView } from 'vue-router'
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance();
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
const scrollContainerRef = ref(ElScrollbar)
const scrollWrapper = computed(() => scrollContainerRef.value.$refs.wrapRef);
onMounted(() => {
  scrollWrapper.value.addEventListener('scroll', emitScroll, true)
@@ -24,12 +14,12 @@
  scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
function handleScroll(e) {
  const eventDelta = e.wheelDelta || -e.deltaY * 40
const handleScroll = (e: WheelEvent) => {
  const eventDelta = (e as any).wheelDelta || -e.deltaY * 40
  const $scrollWrapper = scrollWrapper.value;
  $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const emits = defineEmits()
const emits = defineEmits(['scroll'])
const emitScroll = () => {
  emits('scroll')
}
@@ -37,8 +27,8 @@
const tagsViewStore = useTagsViewStore()
const visitedViews = computed(() => tagsViewStore.visitedViews);
function moveToTarget(currentTag) {
  const $container = proxy.$refs.scrollContainer.$el
const moveToTarget = (currentTag: TagView) => {
  const $container = scrollContainerRef.value.$el
  const $containerWidth = $container.offsetWidth
  const $scrollWrapper = scrollWrapper.value;
@@ -56,10 +46,11 @@
  } else if (lastTag === currentTag) {
    $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
  } else {
    const tagListDom = document.getElementsByClassName('tags-view-item');
    const tagListDom: any = document.getElementsByClassName('tags-view-item');
    const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
    let prevTag = null
    let nextTag = null
    for (const k in tagListDom) {
      if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
        if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
@@ -89,7 +80,13 @@
})
</script>
<style lang='scss' scoped>
<template>
    <el-scrollbar ref="scrollContainerRef" :vertical="false" class="scroll-container" @wheel.prevent="handleScroll">
        <slot />
    </el-scrollbar>
</template>
<style lang="scss" scoped>
.scroll-container {
  white-space: nowrap;
  position: relative;
@@ -102,4 +99,4 @@
    height: 49px;
  }
}
</style>
</style>
src/layout/components/TagsView/index.vue
@@ -1,61 +1,20 @@
<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
      <router-link
        v-for="tag in visitedViews"
        :key="tag.path"
        :data-path="tag.path"
        :class="isActive(tag) ? 'active' : ''"
        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
        class="tags-view-item"
        :style="activeStyle(tag)"
        @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
        @contextmenu.prevent="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
          <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
        </span>
      </router-link>
    </scroll-pane>
    <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
      <li @click="refreshSelectedTag(selectedTag)">
        <refresh-right style="width: 1em; height: 1em;" /> åˆ·æ–°é¡µé¢
      </li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
        <close style="width: 1em; height: 1em;" /> å…³é—­å½“前
      </li>
      <li @click="closeOthersTags">
        <circle-close style="width: 1em; height: 1em;" /> å…³é—­å…¶ä»–
      </li>
      <li v-if="!isFirstView()" @click="closeLeftTags">
        <back style="width: 1em; height: 1em;" /> å…³é—­å·¦ä¾§
      </li>
      <li v-if="!isLastView()" @click="closeRightTags">
        <right style="width: 1em; height: 1em;" /> å…³é—­å³ä¾§
      </li>
      <li @click="closeAllTags(selectedTag)">
        <circle-close style="width: 1em; height: 1em;" /> å…¨éƒ¨å…³é—­
      </li>
    </ul>
  </div>
</template>
<script setup>
import ScrollPane from './ScrollPane'
<script setup lang="ts">
import ScrollPane from './ScrollPane.vue'
import { getNormalPath } from '@/utils/ruoyi'
import useTagsViewStore from '@/store/modules/tagsView'
import useTagsViewStore from "@/store/modules/tagsView";
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { ComponentInternalInstance } from "vue";
import { RouteOption, TagView, RouteLocationRaw } from "vue-router";
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const scrollPaneRef = ref(null);
const selectedTag = ref<TagView>({});
const affixTags = ref<TagView[]>([]);
const scrollPaneRef = ref(ScrollPane);
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const router = useRouter();
@@ -64,53 +23,49 @@
const theme = computed(() => useSettingsStore().theme);
watch(route, () => {
  addTags()
  moveToCurrentTag()
  addTags();
  moveToCurrentTag();
})
watch(visible, (value) => {
  if (value) {
    document.body.addEventListener('click', closeMenu)
    document.body.addEventListener('click', closeMenu);
  } else {
    document.body.removeEventListener('click', closeMenu)
    document.body.removeEventListener('click', closeMenu);
  }
})
onMounted(() => {
  initTags()
  addTags()
})
function isActive(r) {
  return r.path === route.path
const isActive = (r: TagView): boolean => {
  return r.path === route.path;
}
function activeStyle(tag) {
const activeStyle = (tag: TagView) => {
  if (!isActive(tag)) return {};
  return {
    "background-color": theme.value,
    "border-color": theme.value
  };
}
function isAffix(tag) {
  return tag.meta && tag.meta.affix
const isAffix = (tag: TagView) => {
  return tag.meta && tag.meta.affix;
}
function isFirstView() {
const isFirstView = () => {
  try {
    return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
    return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath;
  } catch (err) {
    return false
    return false;
  }
}
function isLastView() {
const isLastView = () => {
  try {
    return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
    return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath;
  } catch (err) {
    return false
    return false;
  }
}
function filterAffixTags(routes, basePath = '') {
  let tags = []
const filterAffixTags = (routes:RouteOption [], basePath = '') => {
  let tags:TagView[] = []
  routes.forEach(route => {
    if (route.meta && route.meta.affix) {
      const tagPath = getNormalPath(basePath + '/' + route.path)
      const tagPath = getNormalPath(basePath + '/' + route.path);
      tags.push({
        fullPath: tagPath,
        path: tagPath,
@@ -119,129 +74,166 @@
      })
    }
    if (route.children) {
      const tempTags = filterAffixTags(route.children, route.path)
      const tempTags = filterAffixTags(route.children, route.path);
      if (tempTags.length >= 1) {
        tags = [...tags, ...tempTags]
        tags = [...tags, ...tempTags];
      }
    }
  })
  return tags
}
function initTags() {
const initTags = () => {
  const res = filterAffixTags(routes.value);
  affixTags.value = res;
  for (const tag of res) {
    // Must have tag name
    if (tag.name) {
       useTagsViewStore().addVisitedView(tag)
      useTagsViewStore().addVisitedView(tag);
    }
  }
}
function addTags() {
  const { name } = route
const addTags = () => {
  const { name } = route;
  if (name) {
    useTagsViewStore().addView(route)
    useTagsViewStore().addView(route);
    if (route.meta.link) {
      useTagsViewStore().addIframeView(route);
    }
  }
  return false
}
function moveToCurrentTag() {
const moveToCurrentTag = () => {
  nextTick(() => {
    for (const r of visitedViews.value) {
      if (r.path === route.path) {
        scrollPaneRef.value.moveToTarget(r);
        // when query is different then update
        if (r.fullPath !== route.fullPath) {
          useTagsViewStore().updateVisitedView(route)
          useTagsViewStore().updateVisitedView(route);
        }
      }
    }
  })
}
function refreshSelectedTag(view) {
  proxy.$tab.refreshPage(view);
const refreshSelectedTag = (view: TagView) => {
  proxy?.$tab.refreshPage(view);
  if (route.meta.link) {
    useTagsViewStore().delIframeView(route);
  }
}
function closeSelectedTag(view) {
  proxy.$tab.closePage(view).then(({ visitedViews }) => {
const closeSelectedTag = (view: TagView) => {
  proxy?.$tab.closePage(view).then(({ visitedViews }: any) => {
    if (isActive(view)) {
      toLastView(visitedViews, view)
      toLastView(visitedViews, view);
    }
  })
}
function closeRightTags() {
  proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
const closeRightTags = () => {
  proxy?.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
    if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
      toLastView(visitedViews)
      toLastView(visitedViews);
    }
  })
}
function closeLeftTags() {
  proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
const closeLeftTags = () => {
  proxy?.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
    if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
      toLastView(visitedViews)
      toLastView(visitedViews);
    }
  })
}
function closeOthersTags() {
  router.push(selectedTag.value).catch(() => { });
  proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
    moveToCurrentTag()
const closeOthersTags = () => {
  router.push(selectedTag.value as RouteLocationRaw).catch(() => { });
  proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
    moveToCurrentTag();
  })
}
function closeAllTags(view) {
  proxy.$tab.closeAllPage().then(({ visitedViews }) => {
const closeAllTags = (view: TagView) => {
  proxy?.$tab.closeAllPage().then(({ visitedViews }) => {
    if (affixTags.value.some(tag => tag.path === route.path)) {
      return
      return;
    }
    toLastView(visitedViews, view)
    toLastView(visitedViews, view);
  })
}
function toLastView(visitedViews, view) {
  const latestView = visitedViews.slice(-1)[0]
const toLastView = (visitedViews:TagView[], view?: TagView) => {
  const latestView = visitedViews.slice(-1)[0];
  if (latestView) {
    router.push(latestView.fullPath)
    router.push(latestView.fullPath as string);
  } else {
    // now the default is to redirect to the home page if there is no tags-view,
    // you can adjust it according to your needs.
    if (view.name === 'Dashboard') {
    if (view?.name === 'Dashboard') {
      // to reload home page
      router.replace({ path: '/redirect' + view.fullPath })
      router.replace({ path: '/redirect' + view?.fullPath });
    } else {
      router.push('/')
      router.push('/');
    }
  }
}
function openMenu(tag, e) {
  const menuMinWidth = 105
  const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
  const offsetWidth = proxy.$el.offsetWidth // container width
  const maxLeft = offsetWidth - menuMinWidth // left boundary
  const l = e.clientX - offsetLeft + 15 // 15: margin right
const openMenu = (tag: TagView, e: MouseEvent) => {
  const menuMinWidth = 105;
  const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
  const offsetWidth = proxy?.$el.offsetWidth; // container width
  const maxLeft = offsetWidth - menuMinWidth; // left boundary
  const l = e.clientX - offsetLeft + 15; // 15: margin right
  if (l > maxLeft) {
    left.value = maxLeft
    left.value = maxLeft;
  } else {
    left.value = l
    left.value = l;
  }
  top.value = e.clientY
  visible.value = true
  selectedTag.value = tag
  visible.value = true;
  selectedTag.value = tag;
}
function closeMenu() {
  visible.value = false
const closeMenu = () => {
  visible.value = false;
}
function handleScroll() {
  closeMenu()
const handleScroll = () => {
  closeMenu();
}
onMounted(() => {
  initTags();
  addTags();
})
</script>
<style lang='scss' scoped>
<template>
    <div id="tags-view-container" class="tags-view-container">
        <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
            <router-link
                v-for="tag in visitedViews"
                :key="tag.path"
                :data-path="tag.path"
                :class="isActive(tag) ? 'active' : ''"
                :to="{ path: tag.path ? tag.path : '', query: tag.query, fullPath: tag.fullPath ? tag.fullPath : '' }"
                class="tags-view-item"
                :style="activeStyle(tag)"
                @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
                @contextmenu.prevent="openMenu(tag, $event)"
            >
                {{ tag.title }}
                <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
                    <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
                </span>
            </router-link>
        </scroll-pane>
        <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
            <li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em;" /> åˆ·æ–°é¡µé¢</li>
            <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em;" /> å…³é—­å½“前</li>
            <li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em;" /> å…³é—­å…¶ä»–</li>
            <li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em;" /> å…³é—­å·¦ä¾§</li>
            <li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em;" /> å…³é—­å³ä¾§</li>
            <li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em;" /> å…¨éƒ¨å…³é—­</li>
        </ul>
    </div>
</template>
<style lang="scss" scoped>
.tags-view-container {
  height: 34px;
  width: 100%;
@@ -335,4 +327,4 @@
    }
  }
}
</style>
</style>
src/layout/components/index.js
ÎļþÒÑɾ³ý
src/layout/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
export { default as AppMain } from './AppMain.vue';
export { default as Navbar } from './Navbar.vue';
export { default as Settings } from './Settings/index.vue';
export { default as TagsView } from './TagsView/index.vue';
src/layout/index.vue
@@ -1,30 +1,11 @@
<template>
  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
    <sidebar v-if="!sidebar.hide" class="sidebar-container" />
    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
      <div :class="{ 'fixed-header': fixedHeader }">
        <navbar ref="navbarRef" @setLayout="setLayout" />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />
      <settings ref="settingRef" />
    </div>
  </div>
</template>
<script setup>
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
<script setup lang="ts">
import SideBar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const needTagsView = computed(() => settingsStore.tagsView);
@@ -37,8 +18,7 @@
  mobile: device.value === 'mobile'
}))
const { proxy } = getCurrentInstance();
const { width, height } = useWindowSize();
const { width } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => {
@@ -53,7 +33,8 @@
  }
})
const navbarRef = ref(null);
const navbarRef = ref(Navbar);
const settingRef = ref(Settings);
onMounted(() => {
  nextTick(() => {
@@ -61,15 +42,29 @@
  })
})
function handleClickOutside() {
const handleClickOutside = () => {
  useAppStore().closeSideBar({ withoutAnimation: false })
}
const settingRef = ref(null);
function setLayout() {
const setLayout = () => {
  settingRef.value.openSetting();
}
</script>
<template>
    <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
        <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
        <side-bar v-if="!sidebar.hide" class="sidebar-container" />
        <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
            <div :class="{ 'fixed-header': fixedHeader }">
                <navbar ref="navbarRef" @setLayout="setLayout" />
                <tags-view v-if="needTagsView" />
            </div>
            <app-main />
            <settings ref="settingRef" />
        </div>
    </div>
</template>
<style lang="scss" scoped>
  @import "@/assets/styles/mixin.scss";
@@ -117,4 +112,4 @@
.mobile .fixed-header {
  width: 100%;
}
</style>
</style>
src/main.js
ÎļþÒÑɾ³ý
src/main.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
import { createApp } from 'vue';
import Cookies from 'js-cookie';
// element-plus
import ElementPlus from 'element-plus';
import locale from 'element-plus/lib/locale/lang/zh-cn';
// global css
import 'uno.css';
import '@/assets/styles/index.scss';
import 'element-plus/theme-chalk/dark/css-vars.css';
// App、router、store
import App from './App.vue';
import store from './store';
import router from './router';
// è‡ªå®šä¹‰æŒ‡ä»¤
import directive from './directive';
// æ³¨å†Œæ’ä»¶
import plugins from './plugins/index'; // plugins
import { download } from '@/utils/request';
// é¢„设动画
import animate from './animate';
// svg图标
import 'virtual:svg-icons-register';
import ElementIcons from '@/plugins/svgicon';
// permission control
import './permission';
import { useDict } from '@/utils/dict';
import { getConfigKey, updateConfigByKey } from '@/api/system/config';
import { parseTime, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi';
const app = createApp(App);
// å…¨å±€æ–¹æ³•挂载
app.config.globalProperties.useDict = useDict;
app.config.globalProperties.getConfigKey = getConfigKey;
app.config.globalProperties.updateConfigByKey = updateConfigByKey;
app.config.globalProperties.download = download;
app.config.globalProperties.parseTime = parseTime;
app.config.globalProperties.handleTree = handleTree;
app.config.globalProperties.addDateRange = addDateRange;
app.config.globalProperties.selectDictLabel = selectDictLabel;
app.config.globalProperties.selectDictLabels = selectDictLabels;
app.config.globalProperties.animate = animate;
app.use(ElementIcons);
app.use(router);
app.use(store);
app.use(plugins);
// è‡ªå®šä¹‰æŒ‡ä»¤
directive(app);
// ä½¿ç”¨element-plus å¹¶ä¸”设置全局的大小
app.use(ElementPlus, {
    locale: locale,
    // æ”¯æŒ large、default、small
    size: Cookies.get('size') || 'default'
});
// ä¿®æ”¹ el-dialog é»˜è®¤ç‚¹å‡»é®ç…§ä¸ºä¸å…³é—­
(app._context.components.ElDialog as any).props.closeOnClickModal.default = false;
app.mount('#app');
src/permission.js
ÎļþÒÑɾ³ý
src/permission.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
import { to as tos } from 'await-to-js';
import router from './router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { getToken } from '@/utils/auth';
import { isHttp } from '@/utils/validate';
import { isRelogin } from '@/utils/request';
import useUserStore from '@/store/modules/user';
import useSettingsStore from '@/store/modules/settings';
import usePermissionStore from '@/store/modules/permission';
NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register'];
router.beforeEach(async (to, from, next) => {
    NProgress.start();
    if (getToken()) {
        to.meta.title && useSettingsStore().setTitle(to.meta.title as string);
        /* has token*/
        if (to.path === '/login') {
            next({ path: '/' });
            NProgress.done();
        } else {
            if (useUserStore().roles.length === 0) {
                isRelogin.show = true;
                // åˆ¤æ–­å½“前用户是否已拉取完user_info信息
                const [err] = await tos(useUserStore().getInfo());
                if (err) {
                    await useUserStore().logout();
                    ElMessage.error(err);
                    next({ path: '/' });
                } else {
                    isRelogin.show = false;
                    const accessRoutes = await usePermissionStore().generateRoutes();
                    // æ ¹æ®roles权限生成可访问的路由表
                    accessRoutes.forEach((route) => {
                        if (!isHttp(route.path)) {
                            router.addRoute(route); // åŠ¨æ€æ·»åŠ å¯è®¿é—®è·¯ç”±è¡¨
                        }
                    });
                    next({ ...to, replace: true }); // hack方法 ç¡®ä¿addRoutes已完成
                }
            } else {
                next();
            }
        }
    } else {
        // æ²¡æœ‰token
        if (whiteList.indexOf(to.path) !== -1) {
            // åœ¨å…ç™»å½•白名单,直接进入
            next();
        } else {
            next(`/login?redirect=${to.fullPath}`); // å¦åˆ™å…¨éƒ¨é‡å®šå‘到登录页
            NProgress.done();
        }
    }
});
router.afterEach(() => {
    NProgress.done();
});
src/plugins/auth.js
ÎļþÒÑɾ³ý
src/plugins/auth.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import useUserStore from '@/store/modules/user';
const authPermission = (permission: string): boolean => {
    const all_permission = '*:*:*';
    const permissions: string[] = useUserStore().permissions;
    if (permission && permission.length > 0) {
        return permissions.some((v) => {
            return all_permission === v || v === permission;
        });
    } else {
        return false;
    }
};
const authRole = (role: string): boolean => {
    const super_admin = 'admin';
    const roles = useUserStore().roles;
    if (role && role.length > 0) {
        return roles.some((v) => {
            return super_admin === v || v === role;
        });
    } else {
        return false;
    }
};
export default {
    // éªŒè¯ç”¨æˆ·æ˜¯å¦å…·å¤‡æŸæƒé™
    hasPermi(permission: string): boolean {
        return authPermission(permission);
    },
    // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šæƒé™ï¼Œåªéœ€åŒ…含其中一个
    hasPermiOr(permissions: string[]): boolean {
        return permissions.some((item) => {
            return authPermission(item);
        });
    },
    // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šæƒé™ï¼Œå¿…须全部拥有
    hasPermiAnd(permissions: string[]): boolean {
        return permissions.every((item) => {
            return authPermission(item);
        });
    },
    // éªŒè¯ç”¨æˆ·æ˜¯å¦å…·å¤‡æŸè§’色
    hasRole(role: string): boolean {
        return authRole(role);
    },
    // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šè§’色,只需包含其中一个
    hasRoleOr(roles: string[]): boolean {
        return roles.some((item) => {
            return authRole(item);
        });
    },
    // éªŒè¯ç”¨æˆ·æ˜¯å¦å«æœ‰æŒ‡å®šè§’色,必须全部拥有
    hasRoleAnd(roles: string[]): boolean {
        return roles.every((item) => {
            return authRole(item);
        });
    }
};
src/plugins/cache.js
ÎļþÒÑɾ³ý
src/plugins/cache.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
const sessionCache = {
    set(key: string, value: any) {
        if (!sessionStorage) {
            return;
        }
        if (key != null && value != null) {
            sessionStorage.setItem(key, value);
        }
    },
    get(key: string) {
        if (!sessionStorage) {
            return null;
        }
        if (key == null) {
            return null;
        }
        return sessionStorage.getItem(key);
    },
    setJSON(key: string, jsonValue: any) {
        if (jsonValue != null) {
            this.set(key, JSON.stringify(jsonValue));
        }
    },
    getJSON(key: string) {
        const value = this.get(key);
        if (value != null) {
            return JSON.parse(value);
        }
    },
    remove(key: string) {
        sessionStorage.removeItem(key);
    }
};
const localCache = {
    set(key: string, value: any) {
        if (!localStorage) {
            return;
        }
        if (key != null && value != null) {
            localStorage.setItem(key, value);
        }
    },
    get(key: string) {
        if (!localStorage) {
            return null;
        }
        if (key == null) {
            return null;
        }
        return localStorage.getItem(key);
    },
    setJSON(key: string, jsonValue: any) {
        if (jsonValue != null) {
            this.set(key, JSON.stringify(jsonValue));
        }
    },
    getJSON(key: string) {
        const value = this.get(key);
        if (value != null) {
            return JSON.parse(value);
        }
    },
    remove(key: string) {
        localStorage.removeItem(key);
    }
};
export default {
    /**
     * ä¼šè¯çº§ç¼“å­˜
     */
    session: sessionCache,
    /**
     * æœ¬åœ°ç¼“å­˜
     */
    local: localCache
};
src/plugins/download.js
ÎļþÒÑɾ³ý
src/plugins/download.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import axios from 'axios';
import FileSaver from 'file-saver';
import { getToken } from '@/utils/auth';
import errorCode from '@/utils/errorCode';
import { blobValidate } from '@/utils/ruoyi';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
const baseURL = import.meta.env.VITE_APP_BASE_API;
let downloadLoadingInstance: LoadingInstance;
export default {
    async oss(ossId: string | number) {
        const url = baseURL + '/system/oss/download/' + ossId;
        downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
        try {
            const res = await axios({
                method: 'get',
                url: url,
                responseType: 'blob',
                headers: { Authorization: 'Bearer ' + getToken() }
            });
            const isBlob = blobValidate(res.data);
            if (isBlob) {
                const blob = new Blob([res.data], { type: 'application/octet-stream' });
                FileSaver.saveAs(blob, decodeURIComponent(res.headers['download-filename'] as string));
            } else {
                this.printErrMsg(res.data);
            }
            downloadLoadingInstance.close();
        } catch (r) {
            console.error(r);
            ElMessage.error('下载文件出现错误,请联系管理员!');
            downloadLoadingInstance.close();
        }
    },
    async zip(url: string, name: string) {
        url = baseURL + url;
        const res = await axios({
            method: 'get',
            url: url,
            responseType: 'blob',
            headers: {
                Authorization: 'Bearer ' + getToken(),
                datasource: localStorage.getItem('dataName')
            }
        });
        const isBlob = blobValidate(res.data);
        if (isBlob) {
            const blob = new Blob([res.data], { type: 'application/zip' });
            FileSaver.saveAs(blob, name);
        } else {
            this.printErrMsg(res.data);
        }
    },
    async printErrMsg(data: any) {
        const resText = await data.text();
        const rspObj = JSON.parse(resText);
        const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
        ElMessage.error(errMsg);
    }
};
src/plugins/index.js
ÎļþÒÑɾ³ý
src/plugins/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
import modal from './modal';
import tab from './tab';
import download from './download';
import cache from './cache';
import auth from './auth';
import { App } from 'vue';
export default function installPlugin(app: App) {
    // é¡µç­¾æ“ä½œ
    app.config.globalProperties.$tab = tab;
    // æ¨¡æ€æ¡†å¯¹è±¡
    app.config.globalProperties.$modal = modal;
    // ç¼“存对象
    app.config.globalProperties.$cache = cache;
    // ä¸‹è½½æ–‡ä»¶
    app.config.globalProperties.$download = download;
    // è®¤è¯å¯¹è±¡
    app.config.globalProperties.$auth = auth;
}
src/plugins/modal.js
ÎļþÒÑɾ³ý
src/plugins/modal.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
import { ElMessage, ElMessageBox, ElNotification, ElLoading, MessageBoxData } from 'element-plus';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
let loadingInstance: LoadingInstance;
export default {
    // æ¶ˆæ¯æç¤º
    msg(content: string) {
        ElMessage.info(content);
    },
    // é”™è¯¯æ¶ˆæ¯
    msgError(content: string) {
        ElMessage.error(content);
    },
    // æˆåŠŸæ¶ˆæ¯
    msgSuccess(content: string) {
        ElMessage.success(content);
    },
    // è­¦å‘Šæ¶ˆæ¯
    msgWarning(content: string) {
        ElMessage.warning(content);
    },
    // å¼¹å‡ºæç¤º
    alert(content: string) {
        ElMessageBox.alert(content, '系统提示');
    },
    // é”™è¯¯æç¤º
    alertError(content: string) {
        ElMessageBox.alert(content, '系统提示', { type: 'error' });
    },
    // æˆåŠŸæç¤º
    alertSuccess(content: string, s: string, p: { dangerouslyUseHTMLString: boolean }) {
        ElMessageBox.alert(content, '系统提示', { type: 'success' });
    },
    // è­¦å‘Šæç¤º
    alertWarning(content: string) {
        ElMessageBox.alert(content, '系统提示', { type: 'warning' });
    },
    // é€šçŸ¥æç¤º
    notify(content: string) {
        ElNotification.info(content);
    },
    // é”™è¯¯é€šçŸ¥
    notifyError(content: string) {
        ElNotification.error(content);
    },
    // æˆåŠŸé€šçŸ¥
    notifySuccess(content: string) {
        ElNotification.success(content);
    },
    // è­¦å‘Šé€šçŸ¥
    notifyWarning(content: string) {
        ElNotification.warning(content);
    },
    // ç¡®è®¤çª—体
    confirm(content: string): Promise<MessageBoxData> {
        return ElMessageBox.confirm(content, '系统提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        });
    },
    // æäº¤å†…容
    prompt(content: string) {
        return ElMessageBox.prompt(content, '系统提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
        });
    },
    // æ‰“开遮罩层
    loading(content: string) {
        loadingInstance = ElLoading.service({
            lock: true,
            text: content,
            background: 'rgba(0, 0, 0, 0.7)'
        });
    },
    // å…³é—­é®ç½©å±‚
    closeLoading() {
        loadingInstance.close();
    }
};
src/plugins/svgicon.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import { App } from 'vue';
export default {
    install: (app: App) => {
        for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
            app.component(key, component);
        }
    }
};
src/plugins/tab.js
ÎļþÒÑɾ³ý
src/plugins/tab.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
import { useTagsViewStore } from '@/store/modules/tagsView';
import router from '@/router';
import { TagView, RouteLocationRaw } from 'vue-router';
export default {
    // åˆ·æ–°å½“前tab页签
    async refreshPage(obj: TagView): Promise<void> {
        const { path, query, matched } = router.currentRoute.value;
        if (obj === undefined) {
            matched.forEach((m) => {
                if (m.components && m.components.default && m.components.default.name) {
                    if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
                        obj = { name: m.components.default.name, path: path, query: query };
                    }
                }
            });
        }
        // prettier-ignore
        await useTagsViewStore().delCachedView(obj)
        router.replace({
            path: '/redirect' + obj.path,
            query: obj.query
        });
    },
    // å…³é—­å½“前tab页签,打开新页签
    closeOpenPage(obj: RouteLocationRaw): void {
        useTagsViewStore().delView(router.currentRoute.value);
        if (obj !== undefined) {
            router.push(obj);
        }
    },
    // å…³é—­æŒ‡å®štab页签
    async closePage(obj?: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] } | any> {
        if (obj === undefined) {
            // prettier-ignore
            const { lastPath } = await useTagsViewStore().delView(router.currentRoute.value) as any
            return router.push(lastPath || '/index');
        }
        return useTagsViewStore().delView(obj);
    },
    // å…³é—­æ‰€æœ‰tab页签
    closeAllPage() {
        return useTagsViewStore().delAllViews();
    },
    // å…³é—­å·¦ä¾§tab页签
    closeLeftPage(obj: TagView) {
        return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
    },
    // å…³é—­å³ä¾§tab页签
    closeRightPage(obj: TagView) {
        return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
    },
    // å…³é—­å…¶ä»–tab页签
    closeOtherPage(obj: TagView) {
        return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
    },
    // æ‰“å¼€tab页签
    openPage(url: RouteLocationRaw) {
        return router.push(url);
    },
    // ä¿®æ”¹tab页签
    updatePage(obj: TagView) {
        return useTagsViewStore().updateVisitedView(obj);
    }
};
src/router/index.js
ÎļþÒÑɾ³ý
src/router/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,179 @@
import { createWebHistory, createRouter, RouteOption } from 'vue-router';
/* Layout */
import Layout from '@/layout/index.vue';
/**
 * Note: è·¯ç”±é…ç½®é¡¹
 *
 * hidden: true                     // å½“设置 true çš„æ—¶å€™è¯¥è·¯ç”±ä¸ä¼šå†ä¾§è¾¹æ å‡ºçް å¦‚401,login等页面,或者如一些编辑页面/edit/1
 * alwaysShow: true                 // å½“你一个路由下面的 children å£°æ˜Žçš„路由大于1个时,自动会变成嵌套的模式--如组件页面
 *                                  // åªæœ‰ä¸€ä¸ªæ—¶ï¼Œä¼šå°†é‚£ä¸ªå­è·¯ç”±å½“做根路由显示在侧边栏--如引导页面
 *                                  // è‹¥ä½ æƒ³ä¸ç®¡è·¯ç”±ä¸‹é¢çš„ children å£°æ˜Žçš„个数都显示你的根路由
 *                                  // ä½ å¯ä»¥è®¾ç½® alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
 * redirect: noRedirect             // å½“设置 noRedirect çš„æ—¶å€™è¯¥è·¯ç”±åœ¨é¢åŒ…屑导航中不可被点击
 * name:'router-name'               // è®¾å®šè·¯ç”±çš„名字,一定要填写不然使用<keep-alive>时会出现各种问题
 * query: '{"id": 1, "name": "ry"}' // è®¿é—®è·¯ç”±çš„默认传递参数
 * roles: ['admin', 'common']       // è®¿é—®è·¯ç”±çš„角色权限
 * permissions: ['a:a:a', 'b:b:b']  // è®¿é—®è·¯ç”±çš„菜单权限
 * meta : {
    noCache: true                   // å¦‚果设置为true,则不会被 <keep-alive> ç¼“å­˜(默认 false)
    title: 'title'                  // è®¾ç½®è¯¥è·¯ç”±åœ¨ä¾§è¾¹æ å’Œé¢åŒ…屑中展示的名字
    icon: 'svg-name'                // è®¾ç½®è¯¥è·¯ç”±çš„图标,对应路径src/assets/icons/svg
    breadcrumb: false               // å¦‚果设置为false,则不会在breadcrumb面包屑中显示
    activeMenu: '/system/user'      // å½“路由设置了该属性,则会高亮相对应的侧边栏。
  }
 */
// å…¬å…±è·¯ç”±
export const constantRoutes: RouteOption[] = [
    {
        path: '/redirect',
        component: Layout,
        hidden: true,
        children: [
            {
                path: '/redirect/:path(.*)',
                component: () => import('@/views/redirect/index.vue')
            }
        ]
    },
    {
        path: '/login',
        component: () => import('@/views/login.vue'),
        hidden: true
    },
    {
        path: '/register',
        component: () => import('@/views/register.vue'),
        hidden: true
    },
    {
        path: '/:pathMatch(.*)*',
        component: () => import('@/views/error/404.vue'),
        hidden: true
    },
    {
        path: '/401',
        component: () => import('@/views/error/401.vue'),
        hidden: true
    },
    {
        path: '',
        component: Layout,
        redirect: '/index',
        children: [
            {
                path: '/index',
                component: () => import('@/views/index.vue'),
                name: 'Index',
                meta: { title: '首页', icon: 'dashboard', affix: true }
            }
        ]
    },
    {
        path: '/user',
        component: Layout,
        hidden: true,
        redirect: 'noredirect',
        children: [
            {
                path: 'profile',
                component: () => import('@/views/system/user/profile/index.vue'),
                name: 'Profile',
                meta: { title: '个人中心', icon: 'user' }
            }
        ]
    }
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
export const dynamicRoutes: RouteOption[] = [
    {
        path: '/system/user-auth',
        component: Layout,
        hidden: true,
        permissions: ['system:user:edit'],
        children: [
            {
                path: 'role/:userId(\\d+)',
                component: () => import('@/views/system/user/authRole.vue'),
                name: 'AuthRole',
                meta: { title: '分配角色', activeMenu: '/system/user', icon: '' }
            }
        ]
    },
    {
        path: '/system/role-auth',
        component: Layout,
        hidden: true,
        permissions: ['system:role:edit'],
        children: [
            {
                path: 'user/:roleId(\\d+)',
                component: () => import('@/views/system/role/authUser.vue'),
                name: 'AuthUser',
                meta: { title: '分配用户', activeMenu: '/system/role', icon: '' }
            }
        ]
    },
    {
        path: '/system/dict-data',
        component: Layout,
        hidden: true,
        permissions: ['system:dict:list'],
        children: [
            {
                path: 'index/:dictId(\\d+)',
                component: () => import('@/views/system/dict/data.vue'),
                name: 'Data',
                meta: { title: '字典数据', activeMenu: '/system/dict', icon: '' }
            }
        ]
    },
    {
        path: '/system/oss-config',
        component: Layout,
        hidden: true,
        permissions: ['monitor:job:list'],
        children: [
            {
                path: 'index',
                component: () => import('@/views/system/oss/config.vue'),
                name: 'OssConfig',
                meta: { title: '配置管理', activeMenu: '/system/oss', icon: '' }
            }
        ]
    },
    {
        path: '/tool/gen-edit',
        component: Layout,
        hidden: true,
        permissions: ['tool:gen:edit'],
        children: [
            {
                path: 'index/:tableId(\\d+)',
                component: () => import('@/views/tool/gen/editTable.vue'),
                name: 'GenEdit',
                meta: { title: '修改生成配置', activeMenu: '/tool/gen', icon: '' }
            }
        ]
    }
];
/**
 * åˆ›å»ºè·¯ç”±
 */
const router = createRouter({
    history: createWebHistory(import.meta.env.VITE_APP_CONTEXT_PATH),
    routes: constantRoutes,
    // åˆ·æ–°æ—¶ï¼Œæ»šåŠ¨æ¡ä½ç½®è¿˜åŽŸ
    scrollBehavior(to, from, savedPosition) {
        if (savedPosition) {
            return savedPosition;
        } else {
            return { top: 0 };
        }
    }
});
export default router;
src/settings.js
ÎļþÒÑɾ³ý
src/settings.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
const setting: DefaultSettings = {
    /**
     * ç½‘页标题
     */
    title: import.meta.env.VITE_APP_TITLE,
    theme: '#409EFF',
    /**
     * ä¾§è¾¹æ ä¸»é¢˜ æ·±è‰²ä¸»é¢˜theme-dark,浅色主题theme-light
     */
    sideTheme: 'theme-dark',
    /**
     * æ˜¯å¦ç³»ç»Ÿå¸ƒå±€é…ç½®
     */
    showSettings: false,
    /**
     * æ˜¯å¦æ˜¾ç¤ºé¡¶éƒ¨å¯¼èˆª
     */
    topNav: false,
    /**
     * æ˜¯å¦æ˜¾ç¤º tagsView
     */
    tagsView: true,
    /**
     * æ˜¯å¦å›ºå®šå¤´éƒ¨
     */
    fixedHeader: false,
    /**
     * æ˜¯å¦æ˜¾ç¤ºlogo
     */
    sidebarLogo: true,
    /**
     * æ˜¯å¦æ˜¾ç¤ºåŠ¨æ€æ ‡é¢˜
     */
    dynamicTitle: false,
    /**
     * @type {string | array} 'production' | ['production', 'development']
     * @description Need show err logs component.
     * The default is only used in the production env
     * If you want to also use it in dev, you can pass ['production', 'development']
     */
    errorLog: 'production',
    animationEnable: false,
    dark: false
};
export default setting;
src/store/index.js
ÎļþÒÑɾ³ý
src/store/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
const store = createPinia();
export default store;
src/store/modules/app.js
ÎļþÒÑɾ³ý
src/store/modules/app.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
import Cookies from 'js-cookie';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
export const useAppStore = defineStore('app', () => {
    const sidebarStatus = Cookies.get('sidebarStatus');
    const sidebar = reactive({
        opened: sidebarStatus ? !!+sidebarStatus : true,
        withoutAnimation: false,
        hide: false
    });
    const device = ref<string>('desktop');
    const size = ref(Cookies.get('size') || 'default');
    // è¯­è¨€
    const language = ref(Cookies.get('language'));
    const locale = computed(() => {
        if (language?.value == 'en') {
            return en;
        } else {
            return zhCn;
        }
    });
    const toggleSideBar = (withoutAnimation?: boolean) => {
        if (sidebar.hide) {
            return false;
        }
        sidebar.opened = !sidebar.opened;
        sidebar.withoutAnimation = withoutAnimation as boolean;
        if (sidebar.opened) {
            Cookies.set('sidebarStatus', '1');
        } else {
            Cookies.set('sidebarStatus', '0');
        }
    };
    const closeSideBar = ({ withoutAnimation }: any): void => {
        Cookies.set('sidebarStatus', '0');
        sidebar.opened = false;
        sidebar.withoutAnimation = withoutAnimation;
    };
    const toggleDevice = (d: string): void => {
        device.value = d;
    };
    const setSize = (s: string): void => {
        size.value = s;
        Cookies.set('size', s);
    };
    const toggleSideBarHide = (status: boolean): void => {
        sidebar.hide = status;
    };
    const changeLanguage = (val: string): void => {
        language.value = val;
    };
    return {
        device,
        sidebar,
        language,
        locale,
        size,
        changeLanguage,
        toggleSideBar,
        closeSideBar,
        toggleDevice,
        setSize,
        toggleSideBarHide
    };
});
export default useAppStore;
src/store/modules/dict.js
ÎļþÒÑɾ³ý
src/store/modules/dict.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
export const useDictStore = defineStore('dict', () => {
    const dict = ref<
        Array<{
            key: string;
            value: DictDataOption[];
        }>
    >([]);
    /**
     * èŽ·å–å­—å…¸
     * @param _key å­—å…¸key
     */
    const getDict = (_key: string): DictDataOption[] | null => {
        if (_key == null && _key == '') {
            return null;
        }
        try {
            for (let i = 0; i < dict.value.length; i++) {
                if (dict.value[i].key == _key) {
                    return dict.value[i].value;
                }
            }
        } catch (e) {
            return null;
        }
        return null;
    };
    /**
     * è®¾ç½®å­—å…¸
     * @param _key å­—å…¸key
     * @param _value å­—å…¸value
     */
    const setDict = (_key: string, _value: DictDataOption[]) => {
        if (_key !== null && _key !== '') {
            dict.value.push({
                key: _key,
                value: _value
            });
        }
    };
    /**
     * åˆ é™¤å­—å…¸
     * @param _key
     */
    const removeDict = (_key: string): boolean => {
        let bln = false;
        try {
            for (let i = 0; i < dict.value.length; i++) {
                if (dict.value[i].key == _key) {
                    dict.value.splice(i, 1);
                    return true;
                }
            }
        } catch (e) {
            bln = false;
        }
        return bln;
    };
    /**
     * æ¸…空字典
     */
    const cleanDict = (): void => {
        dict.value = [];
    };
    return {
        dict,
        getDict,
        setDict,
        removeDict,
        cleanDict
    };
});
export default useDictStore;
src/store/modules/permission.js
ÎļþÒÑɾ³ý
src/store/modules/permission.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
import { defineStore } from 'pinia';
import router, { constantRoutes, dynamicRoutes } from '@/router';
import store from '@/store';
import { getRouters } from '@/api/menu';
import Layout from '@/layout/index.vue';
import ParentView from '@/components/ParentView/index.vue';
import InnerLink from '@/layout/components/InnerLink/index.vue';
import auth from '@/plugins/auth';
import { RouteOption } from 'vue-router';
// åŒ¹é…views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue');
export const usePermissionStore = defineStore('permission', () => {
    const routes = ref<RouteOption[]>([]);
    const addRoutes = ref<RouteOption[]>([]);
    const defaultRoutes = ref<RouteOption[]>([]);
    const topbarRouters = ref<RouteOption[]>([]);
    const sidebarRouters = ref<RouteOption[]>([]);
    const setRoutes = (newRoutes: RouteOption[]): void => {
        addRoutes.value = newRoutes;
        routes.value = constantRoutes.concat(newRoutes);
    };
    const setDefaultRoutes = (routes: RouteOption[]): void => {
        defaultRoutes.value = constantRoutes.concat(routes);
    };
    const setTopbarRoutes = (routes: RouteOption[]): void => {
        topbarRouters.value = routes;
    };
    const setSidebarRouters = (routes: RouteOption[]): void => {
        sidebarRouters.value = routes;
    };
    const generateRoutes = async (): Promise<RouteOption[]> => {
        const res = await getRouters();
        const { data } = res;
        const sdata = JSON.parse(JSON.stringify(data));
        const rdata = JSON.parse(JSON.stringify(data));
        const defaultData = JSON.parse(JSON.stringify(data));
        const sidebarRoutes = filterAsyncRouter(sdata);
        const rewriteRoutes = filterAsyncRouter(rdata, undefined, true);
        const defaultRoutes = filterAsyncRouter(defaultData);
        const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
        asyncRoutes.forEach((route) => {
            router.addRoute(route);
        });
        setRoutes(rewriteRoutes);
        setSidebarRouters(constantRoutes.concat(sidebarRoutes));
        setDefaultRoutes(sidebarRoutes);
        setTopbarRoutes(defaultRoutes);
        return new Promise<RouteOption[]>((resolve) => resolve(rewriteRoutes));
    };
    /**
     * éåŽ†åŽå°ä¼ æ¥çš„è·¯ç”±å­—ç¬¦ä¸²ï¼Œè½¬æ¢ä¸ºç»„ä»¶å¯¹è±¡
     * @param asyncRouterMap åŽå°ä¼ æ¥çš„路由字符串
     * @param lastRouter ä¸Šä¸€çº§è·¯ç”±
     * @param type æ˜¯å¦æ˜¯é‡å†™è·¯ç”±
     */
    const filterAsyncRouter = (asyncRouterMap: RouteOption[], lastRouter?: RouteOption, type = false): RouteOption[] => {
        return asyncRouterMap.filter((route) => {
            if (type && route.children) {
                route.children = filterChildren(route.children, undefined);
            }
            if (route.component) {
                // Layout ParentView ç»„件特殊处理
                if (route.component === 'Layout') {
                    route.component = Layout;
                } else if (route.component === 'ParentView') {
                    route.component = ParentView;
                } else if (route.component === 'InnerLink') {
                    route.component = InnerLink;
                } else {
                    route.component = loadView(route.component);
                }
            }
            if (route.children != null && route.children && route.children.length) {
                route.children = filterAsyncRouter(route.children, route, type);
            } else {
                delete route.children;
                delete route.redirect;
            }
            return true;
        });
    };
    const filterChildren = (childrenMap: RouteOption[], lastRouter?: RouteOption): RouteOption[] => {
        let children: RouteOption[] = [];
        childrenMap.forEach((el) => {
            if (el.children && el.children.length) {
                if (el.component === 'ParentView' && !lastRouter) {
                    el.children.forEach((c) => {
                        c.path = el.path + '/' + c.path;
                        if (c.children && c.children.length) {
                            children = children.concat(filterChildren(c.children, c));
                            return;
                        }
                        children.push(c);
                    });
                    return;
                }
            }
            if (lastRouter) {
                el.path = lastRouter.path + '/' + el.path;
            }
            children = children.concat(el);
        });
        return children;
    };
    return { routes, setRoutes, generateRoutes, setSidebarRouters, topbarRouters, sidebarRouters, defaultRoutes };
});
// åŠ¨æ€è·¯ç”±éåŽ†ï¼ŒéªŒè¯æ˜¯å¦å…·å¤‡æƒé™
export const filterDynamicRoutes = (routes: RouteOption[]) => {
    const res: RouteOption[] = [];
    routes.forEach((route) => {
        if (route.permissions) {
            if (auth.hasPermiOr(route.permissions)) {
                res.push(route);
            }
        } else if (route.roles) {
            if (auth.hasRoleOr(route.roles)) {
                res.push(route);
            }
        }
    });
    return res;
};
export const loadView = (view: any) => {
    let res;
    for (const path in modules) {
        const dir = path.split('views/')[1].split('.vue')[0];
        if (dir === view) {
            res = () => modules[path]();
        }
    }
    return res;
};
// éžsetup
export const usePermissionStoreHook = () => {
    return usePermissionStore(store);
};
export default usePermissionStore;
src/store/modules/settings.js
ÎļþÒÑɾ³ý
src/store/modules/settings.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
import { defineStore } from 'pinia';
import defaultSettings from '@/settings';
import { SettingTypeEnum } from '@/enums/SettingTypeEnum';
import { useDynamicTitle } from '@/utils/dynamicTitle';
import { Ref } from 'vue';
export const useSettingsStore = defineStore('setting', () => {
    const storageSetting = JSON.parse(localStorage.getItem('layout-setting') || '{}');
    const prop: { [key: string]: Ref<any> } = {
        title: ref<string>(''),
        theme: ref<string>(storageSetting.theme || defaultSettings.theme),
        sideTheme: ref<string>(storageSetting.sideTheme || defaultSettings.sideTheme),
        showSettings: ref<boolean>(storageSetting.showSettings),
        topNav: ref<boolean>(storageSetting.topNav || defaultSettings.topNav),
        tagsView: ref<boolean>(storageSetting.tagsView || defaultSettings.tagsView),
        fixedHeader: ref<boolean>(storageSetting.fixedHeader || defaultSettings.fixedHeader),
        sidebarLogo: ref<boolean>(storageSetting.sidebarLogo || defaultSettings.sidebarLogo),
        dynamicTitle: ref<boolean>(storageSetting.dynamicTitle || defaultSettings.dynamicTitle),
        animationEnable: ref<boolean>(storageSetting.animationEnable || defaultSettings.animationEnable),
        dark: ref<boolean>(storageSetting.dark || defaultSettings.dark)
    };
    const { title, theme, sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle, animationEnable, dark } = prop;
    // actions
    const changeSetting = (param: { key: SettingTypeEnum; value: any }) => {
        const { key, value } = param;
        if (key in prop) {
            prop[key].value = value;
        }
    };
    const setTitle = (value: string) => {
        title.value = value;
        useDynamicTitle();
    };
    return {
        title,
        theme,
        sideTheme,
        showSettings,
        topNav,
        tagsView,
        fixedHeader,
        sidebarLogo,
        dynamicTitle,
        animationEnable,
        dark,
        changeSetting,
        setTitle
    };
});
export default useSettingsStore;
src/store/modules/tagsView.js
ÎļþÒÑɾ³ý
src/store/modules/tagsView.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,198 @@
import { TagView } from 'vue-router';
export const useTagsViewStore = defineStore('tagsView', () => {
    const visitedViews = ref<TagView[]>([]);
    const cachedViews = ref<string[]>([]);
    const iframeViews = ref<TagView[]>([]);
    const addView = (view: TagView) => {
        addVisitedView(view);
        addCachedView(view);
    };
    const addIframeView = (view: TagView): void => {
        if (iframeViews.value.some((v) => v.path === view.path)) return;
        iframeViews.value.push(
            Object.assign({}, view, {
                title: view.meta?.title || 'no-name'
            })
        );
    };
    const delIframeView = (view: TagView): Promise<TagView[]> => {
        return new Promise((resolve) => {
            iframeViews.value = iframeViews.value.filter((item) => item.path !== view.path);
            resolve([...iframeViews.value]);
        });
    };
    const addVisitedView = (view: TagView): void => {
        if (visitedViews.value.some((v) => v.path === view.path)) return;
        visitedViews.value.push(
            Object.assign({}, view, {
                title: view.meta?.title || 'no-name'
            })
        );
    };
    const delView = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
        return new Promise((resolve) => {
            delVisitedView(view);
            delCachedView(view);
            resolve({
                visitedViews: [...visitedViews.value],
                cachedViews: [...cachedViews.value]
            });
        });
    };
    const delVisitedView = (view: TagView): Promise<TagView[]> => {
        return new Promise((resolve) => {
            for (const [i, v] of visitedViews.value.entries()) {
                if (v.path === view.path) {
                    visitedViews.value.splice(i, 1);
                    break;
                }
            }
            resolve([...visitedViews.value]);
        });
    };
    const delCachedView = (view: TagView): Promise<string[]> => {
        const viewName = view.name as string;
        return new Promise((resolve) => {
            const index = cachedViews.value.indexOf(viewName);
            index > -1 && cachedViews.value.splice(index, 1);
            resolve([...cachedViews.value]);
        });
    };
    const delOthersViews = (view: TagView): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
        return new Promise((resolve) => {
            delOthersVisitedViews(view);
            delOthersCachedViews(view);
            resolve({
                visitedViews: [...visitedViews.value],
                cachedViews: [...cachedViews.value]
            });
        });
    };
    const delOthersVisitedViews = (view: TagView): Promise<TagView[]> => {
        return new Promise((resolve) => {
            visitedViews.value = visitedViews.value.filter((v) => {
                return v.meta?.affix || v.path === view.path;
            });
            resolve([...visitedViews.value]);
        });
    };
    const delOthersCachedViews = (view: TagView): Promise<string[]> => {
        const viewName = view.name as string;
        return new Promise((resolve) => {
            const index = cachedViews.value.indexOf(viewName);
            if (index > -1) {
                cachedViews.value = cachedViews.value.slice(index, index + 1);
            } else {
                cachedViews.value = [];
            }
            resolve([...cachedViews.value]);
        });
    };
    const delAllViews = (): Promise<{ visitedViews: TagView[]; cachedViews: string[] }> => {
        return new Promise((resolve) => {
            delAllVisitedViews();
            delAllCachedViews();
            resolve({
                visitedViews: [...visitedViews.value],
                cachedViews: [...cachedViews.value]
            });
        });
    };
    const delAllVisitedViews = (): Promise<TagView[]> => {
        return new Promise((resolve) => {
            visitedViews.value = visitedViews.value.filter((tag) => tag.meta?.affix);
            resolve([...visitedViews.value]);
        });
    };
    const delAllCachedViews = (): Promise<string[]> => {
        return new Promise((resolve) => {
            cachedViews.value = [];
            resolve([...cachedViews.value]);
        });
    };
    const updateVisitedView = (view: TagView): void => {
        for (let v of visitedViews.value) {
            if (v.path === view.path) {
                v = Object.assign(v, view);
                break;
            }
        }
    };
    const delRightTags = (view: TagView): Promise<TagView[]> => {
        return new Promise((resolve) => {
            const index = visitedViews.value.findIndex((v) => v.path === view.path);
            if (index === -1) {
                return;
            }
            visitedViews.value = visitedViews.value.filter((item, idx) => {
                if (idx <= index || (item.meta && item.meta.affix)) {
                    return true;
                }
                const i = cachedViews.value.indexOf(item.name as string);
                if (i > -1) {
                    cachedViews.value.splice(i, 1);
                }
                return false;
            });
            resolve([...visitedViews.value]);
        });
    };
    const delLeftTags = (view: TagView): Promise<TagView[]> => {
        return new Promise((resolve) => {
            const index = visitedViews.value.findIndex((v) => v.path === view.path);
            if (index === -1) {
                return;
            }
            visitedViews.value = visitedViews.value.filter((item, idx) => {
                if (idx >= index || (item.meta && item.meta.affix)) {
                    return true;
                }
                const i = cachedViews.value.indexOf(item.name as string);
                if (i > -1) {
                    cachedViews.value.splice(i, 1);
                }
                return false;
            });
            resolve([...visitedViews.value]);
        });
    };
    const addCachedView = (view: TagView): void => {
        const viewName = view.name as string;
        if (cachedViews.value.includes(viewName)) return;
        if (!view.meta?.noCache) {
            cachedViews.value.push(viewName);
        }
    };
    return {
        visitedViews,
        cachedViews,
        iframeViews,
        addVisitedView,
        addCachedView,
        delVisitedView,
        delCachedView,
        updateVisitedView,
        addView,
        delView,
        delAllViews,
        delAllVisitedViews,
        delAllCachedViews,
        delOthersViews,
        delRightTags,
        delLeftTags,
        addIframeView,
        delIframeView
    };
});
export default useTagsViewStore;
src/store/modules/user.js
ÎļþÒÑɾ³ý
src/store/modules/user.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
import { to } from 'await-to-js';
import defAva from '@/assets/images/profile.jpg';
import store from '@/store';
import { getToken, removeToken, setToken } from '@/utils/auth';
import { login as loginApi, logout as logoutApi, getInfo as getUserInfo } from '@/api/login';
import { LoginData } from '@/api/types';
export const useUserStore = defineStore('user', () => {
    const token = ref(getToken());
    const name = ref('');
    const nickname = ref('');
    const userId = ref<string | number>('');
    const avatar = ref('');
    const roles = ref<Array<string>>([]); // ç”¨æˆ·è§’色编码集合 â†’ åˆ¤æ–­è·¯ç”±æƒé™
    const permissions = ref<Array<string>>([]); // ç”¨æˆ·æƒé™ç¼–码集合 â†’ åˆ¤æ–­æŒ‰é’®æƒé™
    /**
     * ç™»å½•
     * @param userInfo
     * @returns
     */
    const login = async (userInfo: LoginData): Promise<void> => {
        const [err, res] = await to(loginApi(userInfo));
        if (res) {
            const data = res.data;
            setToken(data.token);
            token.value = data.token;
            return Promise.resolve();
        }
        return Promise.reject(err);
    };
    // èŽ·å–ç”¨æˆ·ä¿¡æ¯
    const getInfo = async (): Promise<void> => {
        const [err, res] = await to(getUserInfo());
        if (res) {
            const data = res.data;
            const user = data.user;
            const profile = user.avatar == '' || user.avatar == null ? defAva : user.avatar;
            if (data.roles && data.roles.length > 0) {
                // éªŒè¯è¿”回的roles是否是一个非空数组
                roles.value = data.roles;
                permissions.value = data.permissions;
            } else {
                roles.value = ['ROLE_DEFAULT'];
            }
            name.value = user.userName;
            nickname.value = user.nickName;
            avatar.value = profile;
            userId.value = user.userId;
            return Promise.resolve();
        }
        return Promise.reject(err);
    };
    // æ³¨é”€
    const logout = async (): Promise<void> => {
        await logoutApi();
        token.value = '';
        roles.value = [];
        permissions.value = [];
        removeToken();
    };
    return {
        userId,
        token,
        nickname,
        avatar,
        roles,
        permissions,
        login,
        getInfo,
        logout
    };
});
export default useUserStore;
// éžsetup
export function useUserStoreHook() {
    return useUserStore(store);
}
src/types/auto-imports.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,574 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
  const EffectScope: typeof import('vue')['EffectScope']
  const ElForm: typeof import('element-plus/es')['ElForm']
  const ElLoading: typeof import('element-plus/es')['ElLoading']
  const ElMessage: typeof import('element-plus/es')['ElMessage']
  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
  const ElNotification: typeof import('element-plus/es')['ElNotification']
  const ElSelect: typeof import('element-plus/es')['ElSelect']
  const ElTable: typeof import('element-plus/es')['ElTable']
  const ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
  const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
  const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
  const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
  const computed: typeof import('vue')['computed']
  const computedAsync: typeof import('@vueuse/core')['computedAsync']
  const computedEager: typeof import('@vueuse/core')['computedEager']
  const computedInject: typeof import('@vueuse/core')['computedInject']
  const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
  const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
  const controlledRef: typeof import('@vueuse/core')['controlledRef']
  const createApp: typeof import('vue')['createApp']
  const createEventHook: typeof import('@vueuse/core')['createEventHook']
  const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
  const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
  const createPinia: typeof import('pinia')['createPinia']
  const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
  const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
  const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
  const customRef: typeof import('vue')['customRef']
  const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
  const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
  const defineComponent: typeof import('vue')['defineComponent']
  const defineStore: typeof import('pinia')['defineStore']
  const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
  const effectScope: typeof import('vue')['effectScope']
  const extendRef: typeof import('@vueuse/core')['extendRef']
  const getActivePinia: typeof import('pinia')['getActivePinia']
  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
  const getCurrentScope: typeof import('vue')['getCurrentScope']
  const h: typeof import('vue')['h']
  const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
  const inject: typeof import('vue')['inject']
  const isDefined: typeof import('@vueuse/core')['isDefined']
  const isProxy: typeof import('vue')['isProxy']
  const isReactive: typeof import('vue')['isReactive']
  const isReadonly: typeof import('vue')['isReadonly']
  const isRef: typeof import('vue')['isRef']
  const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
  const mapActions: typeof import('pinia')['mapActions']
  const mapGetters: typeof import('pinia')['mapGetters']
  const mapState: typeof import('pinia')['mapState']
  const mapStores: typeof import('pinia')['mapStores']
  const mapWritableState: typeof import('pinia')['mapWritableState']
  const markRaw: typeof import('vue')['markRaw']
  const nextTick: typeof import('vue')['nextTick']
  const onActivated: typeof import('vue')['onActivated']
  const onBeforeMount: typeof import('vue')['onBeforeMount']
  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
  const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
  const onDeactivated: typeof import('vue')['onDeactivated']
  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
  const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
  const onLongPress: typeof import('@vueuse/core')['onLongPress']
  const onMounted: typeof import('vue')['onMounted']
  const onRenderTracked: typeof import('vue')['onRenderTracked']
  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
  const onScopeDispose: typeof import('vue')['onScopeDispose']
  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
  const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
  const onUnmounted: typeof import('vue')['onUnmounted']
  const onUpdated: typeof import('vue')['onUpdated']
  const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
  const provide: typeof import('vue')['provide']
  const reactify: typeof import('@vueuse/core')['reactify']
  const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
  const reactive: typeof import('vue')['reactive']
  const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
  const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
  const reactivePick: typeof import('@vueuse/core')['reactivePick']
  const readonly: typeof import('vue')['readonly']
  const ref: typeof import('vue')['ref']
  const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
  const refDebounced: typeof import('@vueuse/core')['refDebounced']
  const refDefault: typeof import('@vueuse/core')['refDefault']
  const refThrottled: typeof import('@vueuse/core')['refThrottled']
  const refWithControl: typeof import('@vueuse/core')['refWithControl']
  const resolveComponent: typeof import('vue')['resolveComponent']
  const resolveDirective: typeof import('vue')['resolveDirective']
  const resolveRef: typeof import('@vueuse/core')['resolveRef']
  const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
  const setActivePinia: typeof import('pinia')['setActivePinia']
  const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
  const shallowReactive: typeof import('vue')['shallowReactive']
  const shallowReadonly: typeof import('vue')['shallowReadonly']
  const shallowRef: typeof import('vue')['shallowRef']
  const storeToRefs: typeof import('pinia')['storeToRefs']
  const syncRef: typeof import('@vueuse/core')['syncRef']
  const syncRefs: typeof import('@vueuse/core')['syncRefs']
  const templateRef: typeof import('@vueuse/core')['templateRef']
  const throttledRef: typeof import('@vueuse/core')['throttledRef']
  const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
  const toRaw: typeof import('vue')['toRaw']
  const toReactive: typeof import('@vueuse/core')['toReactive']
  const toRef: typeof import('vue')['toRef']
  const toRefs: typeof import('vue')['toRefs']
  const triggerRef: typeof import('vue')['triggerRef']
  const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
  const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
  const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
  const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
  const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
  const unref: typeof import('vue')['unref']
  const unrefElement: typeof import('@vueuse/core')['unrefElement']
  const until: typeof import('@vueuse/core')['until']
  const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
  const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
  const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
  const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
  const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
  const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
  const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
  const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
  const useArraySome: typeof import('@vueuse/core')['useArraySome']
  const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
  const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
  const useAttrs: typeof import('vue')['useAttrs']
  const useBase64: typeof import('@vueuse/core')['useBase64']
  const useBattery: typeof import('@vueuse/core')['useBattery']
  const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
  const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
  const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
  const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
  const useCached: typeof import('@vueuse/core')['useCached']
  const useClipboard: typeof import('@vueuse/core')['useClipboard']
  const useCloned: typeof import('@vueuse/core')['useCloned']
  const useColorMode: typeof import('@vueuse/core')['useColorMode']
  const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
  const useCounter: typeof import('@vueuse/core')['useCounter']
  const useCssModule: typeof import('vue')['useCssModule']
  const useCssVar: typeof import('@vueuse/core')['useCssVar']
  const useCssVars: typeof import('vue')['useCssVars']
  const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
  const useCycleList: typeof import('@vueuse/core')['useCycleList']
  const useDark: typeof import('@vueuse/core')['useDark']
  const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
  const useDebounce: typeof import('@vueuse/core')['useDebounce']
  const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
  const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
  const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
  const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
  const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
  const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
  const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
  const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
  const useDraggable: typeof import('@vueuse/core')['useDraggable']
  const useDropZone: typeof import('@vueuse/core')['useDropZone']
  const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
  const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
  const useElementHover: typeof import('@vueuse/core')['useElementHover']
  const useElementSize: typeof import('@vueuse/core')['useElementSize']
  const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
  const useEventBus: typeof import('@vueuse/core')['useEventBus']
  const useEventListener: typeof import('@vueuse/core')['useEventListener']
  const useEventSource: typeof import('@vueuse/core')['useEventSource']
  const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
  const useFavicon: typeof import('@vueuse/core')['useFavicon']
  const useFetch: typeof import('@vueuse/core')['useFetch']
  const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
  const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
  const useFocus: typeof import('@vueuse/core')['useFocus']
  const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
  const useFps: typeof import('@vueuse/core')['useFps']
  const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
  const useGamepad: typeof import('@vueuse/core')['useGamepad']
  const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
  const useIdle: typeof import('@vueuse/core')['useIdle']
  const useImage: typeof import('@vueuse/core')['useImage']
  const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
  const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
  const useInterval: typeof import('@vueuse/core')['useInterval']
  const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
  const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
  const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
  const useLink: typeof import('vue-router')['useLink']
  const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
  const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
  const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
  const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
  const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
  const useMemoize: typeof import('@vueuse/core')['useMemoize']
  const useMemory: typeof import('@vueuse/core')['useMemory']
  const useMounted: typeof import('@vueuse/core')['useMounted']
  const useMouse: typeof import('@vueuse/core')['useMouse']
  const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
  const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
  const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
  const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
  const useNetwork: typeof import('@vueuse/core')['useNetwork']
  const useNow: typeof import('@vueuse/core')['useNow']
  const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
  const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
  const useOnline: typeof import('@vueuse/core')['useOnline']
  const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
  const useParallax: typeof import('@vueuse/core')['useParallax']
  const usePermission: typeof import('@vueuse/core')['usePermission']
  const usePointer: typeof import('@vueuse/core')['usePointer']
  const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
  const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
  const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
  const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
  const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
  const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
  const useRafFn: typeof import('@vueuse/core')['useRafFn']
  const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
  const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
  const useRoute: typeof import('vue-router')['useRoute']
  const useRouter: typeof import('vue-router')['useRouter']
  const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
  const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
  const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
  const useScroll: typeof import('@vueuse/core')['useScroll']
  const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
  const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
  const useShare: typeof import('@vueuse/core')['useShare']
  const useSlots: typeof import('vue')['useSlots']
  const useSorted: typeof import('@vueuse/core')['useSorted']
  const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
  const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
  const useStepper: typeof import('@vueuse/core')['useStepper']
  const useStorage: typeof import('@vueuse/core')['useStorage']
  const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
  const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
  const useSupported: typeof import('@vueuse/core')['useSupported']
  const useSwipe: typeof import('@vueuse/core')['useSwipe']
  const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
  const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
  const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
  const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
  const useThrottle: typeof import('@vueuse/core')['useThrottle']
  const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
  const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
  const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
  const useTimeout: typeof import('@vueuse/core')['useTimeout']
  const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
  const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
  const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
  const useTitle: typeof import('@vueuse/core')['useTitle']
  const useToNumber: typeof import('@vueuse/core')['useToNumber']
  const useToString: typeof import('@vueuse/core')['useToString']
  const useToggle: typeof import('@vueuse/core')['useToggle']
  const useTransition: typeof import('@vueuse/core')['useTransition']
  const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
  const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
  const useVModel: typeof import('@vueuse/core')['useVModel']
  const useVModels: typeof import('@vueuse/core')['useVModels']
  const useVibrate: typeof import('@vueuse/core')['useVibrate']
  const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
  const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
  const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
  const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
  const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
  const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
  const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
  const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
  const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
  const watch: typeof import('vue')['watch']
  const watchArray: typeof import('@vueuse/core')['watchArray']
  const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
  const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
  const watchEffect: typeof import('vue')['watchEffect']
  const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
  const watchOnce: typeof import('@vueuse/core')['watchOnce']
  const watchPausable: typeof import('@vueuse/core')['watchPausable']
  const watchPostEffect: typeof import('vue')['watchPostEffect']
  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
  const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
  const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
  const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
  const whenever: typeof import('@vueuse/core')['whenever']
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
  interface ComponentCustomProperties {
    readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
    readonly ElForm: UnwrapRef<typeof import('element-plus/es')['ElForm']>
    readonly ElLoading: UnwrapRef<typeof import('element-plus/es')['ElLoading']>
    readonly ElMessage: UnwrapRef<typeof import('element-plus/es')['ElMessage']>
    readonly ElMessageBox: UnwrapRef<typeof import('element-plus/es')['ElMessageBox']>
    readonly ElNotification: UnwrapRef<typeof import('element-plus/es')['ElNotification']>
    readonly ElSelect: UnwrapRef<typeof import('element-plus/es')['ElSelect']>
    readonly ElTable: UnwrapRef<typeof import('element-plus/es')['ElTable']>
    readonly ElTreeSelect: UnwrapRef<typeof import('element-plus/es')['ElTreeSelect']>
    readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
    readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
    readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
    readonly computed: UnwrapRef<typeof import('vue')['computed']>
    readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
    readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
    readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
    readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
    readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
    readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
    readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
    readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
    readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
    readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
    readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
    readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
    readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
    readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
    readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
    readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
    readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
    readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
    readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
    readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
    readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
    readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
    readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
    readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
    readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
    readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
    readonly h: UnwrapRef<typeof import('vue')['h']>
    readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
    readonly inject: UnwrapRef<typeof import('vue')['inject']>
    readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
    readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
    readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
    readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
    readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
    readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
    readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
    readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
    readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
    readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
    readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
    readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
    readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
    readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
    readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
    readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
    readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
    readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
    readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
    readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
    readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
    readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
    readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
    readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
    readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
    readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
    readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
    readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
    readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
    readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
    readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
    readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
    readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
    readonly provide: UnwrapRef<typeof import('vue')['provide']>
    readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
    readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
    readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
    readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
    readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
    readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
    readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
    readonly ref: UnwrapRef<typeof import('vue')['ref']>
    readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
    readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
    readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
    readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
    readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
    readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
    readonly resolveDirective: UnwrapRef<typeof import('vue')['resolveDirective']>
    readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
    readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
    readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
    readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
    readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
    readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
    readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
    readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
    readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
    readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
    readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
    readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
    readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
    readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
    readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
    readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
    readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
    readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
    readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
    readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
    readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
    readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
    readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
    readonly unref: UnwrapRef<typeof import('vue')['unref']>
    readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
    readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
    readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
    readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
    readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
    readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
    readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
    readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
    readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
    readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
    readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
    readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
    readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
    readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
    readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
    readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
    readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
    readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
    readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
    readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
    readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
    readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
    readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
    readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
    readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
    readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
    readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
    readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
    readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
    readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
    readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
    readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
    readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
    readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
    readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
    readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
    readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
    readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
    readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
    readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
    readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
    readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
    readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
    readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
    readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
    readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
    readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
    readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
    readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
    readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
    readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
    readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
    readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
    readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
    readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
    readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
    readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
    readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
    readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
    readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
    readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
    readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
    readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
    readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
    readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
    readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
    readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
    readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
    readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
    readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
    readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
    readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
    readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
    readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
    readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
    readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
    readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
    readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
    readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
    readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
    readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
    readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
    readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
    readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
    readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
    readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
    readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
    readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
    readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
    readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
    readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
    readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
    readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
    readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
    readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
    readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
    readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
    readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
    readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
    readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
    readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
    readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
    readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
    readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
    readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
    readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
    readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
    readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
    readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
    readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
    readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
    readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
    readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
    readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
    readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
    readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
    readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
    readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
    readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
    readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
    readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
    readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
    readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
    readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
    readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
    readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
    readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
    readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
    readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
    readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
    readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
    readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
    readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
    readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
    readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
    readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
    readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
    readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
    readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
    readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
    readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
    readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
    readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
    readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
    readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
    readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
    readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
    readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
    readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
    readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
    readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
    readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
    readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
    readonly watch: UnwrapRef<typeof import('vue')['watch']>
    readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
    readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
    readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
    readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
    readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
    readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
    readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
    readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
    readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
    readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
    readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
    readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
    readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
  }
}
src/types/axios.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
import axios from 'axios';
declare module 'axios' {
    export interface AxiosResponse<T = any> {
        code: number;
        msg: string;
        rows: T;
        total: number;
    }
}
src/types/components.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {}
declare module '@vue/runtime-core' {
  export interface GlobalComponents {
    Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
    DictTag: typeof import('./../components/DictTag/index.vue')['default']
    Editor: typeof import('./../components/Editor/index.vue')['default']
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElDrawer: typeof import('element-plus/es')['ElDrawer']
    ElDropdown: typeof import('element-plus/es')['ElDropdown']
    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
    ElForm: typeof import('element-plus/es')['ElForm']
    ElFormItem: typeof import('element-plus/es')['ElFormItem']
    ElIcon: typeof import('element-plus/es')['ElIcon']
    ElImage: typeof import('element-plus/es')['ElImage']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMenu: typeof import('element-plus/es')['ElMenu']
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadio: typeof import('element-plus/es')['ElRadio']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    ElTable: typeof import('element-plus/es')['ElTable']
    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
    ElTabPane: typeof import('element-plus/es')['ElTabPane']
    ElTabs: typeof import('element-plus/es')['ElTabs']
    ElTag: typeof import('element-plus/es')['ElTag']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTransfer: typeof import('element-plus/es')['ElTransfer']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
    Hamburger: typeof import('./../components/Hamburger/index.vue')['default']
    HeaderSearch: typeof import('./../components/HeaderSearch/index.vue')['default']
    IconSelect: typeof import('./../components/IconSelect/index.vue')['default']
    IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
    IEpCaretTop: typeof import('~icons/ep/caret-top')['default']
    IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default']
    IFrame: typeof import('./../components/iFrame/index.vue')['default']
    ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
    ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']
    Pagination: typeof import('./../components/Pagination/index.vue')['default']
    ParentView: typeof import('./../components/ParentView/index.vue')['default']
    RightToolbar: typeof import('./../components/RightToolbar/index.vue')['default']
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    RuoYiDoc: typeof import('./../components/RuoYiDoc/index.vue')['default']
    RuoYiGit: typeof import('./../components/RuoYiGit/index.vue')['default']
    Screenfull: typeof import('./../components/Screenfull/index.vue')['default']
    SizeSelect: typeof import('./../components/SizeSelect/index.vue')['default']
    SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
    TopNav: typeof import('./../components/TopNav/index.vue')['default']
    TreeSelect: typeof import('./../components/TreeSelect/index.vue')['default']
  }
  export interface ComponentCustomProperties {
    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
  }
}
src/types/element.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
declare type ElTagType = '' | 'success' | 'warning' | 'info' | 'danger' | 'default' | 'primary';
src/types/env.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
declare module '*.vue' {
    import { DefineComponent } from 'vue';
    const component: DefineComponent<{}, {}, any>;
    export default component;
}
declare module '*.avif' {
    const src: string;
    export default src;
}
declare module '*.bmp' {
    const src: string;
    export default src;
}
declare module '*.gif' {
    const src: string;
    export default src;
}
declare module '*.jpg' {
    const src: string;
    export default src;
}
declare module '*.jpeg' {
    const src: string;
    export default src;
}
declare module '*.png' {
    const src: string;
    export default src;
}
declare module '*.webp' {
    const src: string;
    export default src;
}
declare module '*.svg' {
    const src: string;
    export default src;
}
declare module '*.module.css' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
declare module '*.module.scss' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
declare module '*.module.sass' {
    const classes: { readonly [key: string]: string };
    export default classes;
}
// çŽ¯å¢ƒå˜é‡
interface ImportMetaEnv {
    VITE_APP_TITLE: string;
    VITE_APP_PORT: number;
    VITE_APP_BASE_API: string;
    VITE_APP_BASE_URL: string;
    VITE_APP_CONTEXT_PATH: string;
    VITE_APP_MONITRO_ADMIN: string;
    VITE_APP_XXL_JOB_ADMIN: string;
    VITE_APP_ENV: string;
}
interface ImportMeta {
    readonly env: ImportMetaEnv;
    // readonly glob: any;
}
src/types/global.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
import { FormRules } from 'element-plus';
declare global {
    /**
     * ç•Œé¢å­—段隐藏属性
     */
    interface FieldOption {
        key: number;
        label: string;
        visible: boolean;
    }
    /**
     * å¼¹çª—属性
     */
    interface DialogOption {
        /**
         * å¼¹çª—标题
         */
        title?: string;
        /**
         * æ˜¯å¦æ˜¾ç¤º
         */
        visible: boolean;
    }
    interface UploadOption {
        /** è®¾ç½®ä¸Šä¼ çš„请求头部 */
        headers: { [key: string]: any };
        /** ä¸Šä¼ çš„地址 */
        url: string;
    }
    /**
     * å¯¼å…¥å±žæ€§
     */
    interface ImportOption extends UploadOption {
        /** æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚ */
        open: boolean;
        /** å¼¹å‡ºå±‚标题 */
        title: string;
        /** æ˜¯å¦ç¦ç”¨ä¸Šä¼  */
        isUploading: boolean;
        /** å…¶ä»–参数 */
        [key: string]: any;
    }
    /**
     * å­—典数据  æ•°æ®é…ç½®
     */
    interface DictDataOption {
        label: string;
        value: string;
        elTagType?: ElTagType;
        elTagClass?: string;
    }
    interface BaseEntity {
        createBy?: any;
        createTime?: string;
        updateBy?: any;
        updateTime?: any;
    }
    /**
     * åˆ†é¡µæ•°æ®
     * T : è¡¨å•数据
     * D : æŸ¥è¯¢å‚æ•°
     */
    interface PageData<T, D> {
        form: T;
        queryParams: D;
        rules: FormRules;
    }
    /**
     * åˆ†é¡µæŸ¥è¯¢å‚æ•°
     */
    interface PageQuery {
        pageNum: number;
        pageSize: number;
    }
}
export {};
src/types/module.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import modal from '@/plugins/modal';
import tab from '@/plugins/tab';
import { useDict } from '@/utils/dict';
import { addDateRange, handleTree, selectDictLabel, selectDictLabels, parseTime } from '@/utils/ruoyi';
import { getConfigKey, updateConfigByKey } from '@/api/system/config';
import { download as download1 } from '@/utils/request';
import download from '@/plugins/download';
import animate from '@/animate';
declare module 'vue' {
    export interface ComponentCustomProperties {
        // å…¨å±€æ–¹æ³•声明
        $modal: typeof modal;
        $tab: typeof tab;
        $download: typeof download;
        animate: typeof animate;
        useDict: typeof useDict;
        addDateRange: typeof addDateRange;
        download: typeof download1;
        handleTree: typeof handleTree;
        getConfigKey: typeof getConfigKey;
        updateConfigByKey: typeof updateConfigByKey;
        selectDictLabel: typeof selectDictLabel;
        selectDictLabels: typeof selectDictLabels;
        parseTime: typeof parseTime;
    }
}
src/types/router.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
import { RouteRecordRaw } from 'vue-router';
declare module 'vue-router' {
    type RouteOption = {
        hidden?: boolean;
        permissions?: string[];
        roles?: string[];
        component?: any;
        children?: RouteOption[];
        alwaysShow?: boolean;
        parentPath?: string;
        meta?: {
            title: string;
            icon?: string;
        };
    } & RouteRecordRaw;
    interface _RouteLocationBase {
        children?: RouteOption[];
    }
    interface RouteLocationOptions {
        fullPath?: string;
    }
    interface TagView extends Partial<_RouteLocationBase> {
        title?: string;
        meta?: {
            link?: string;
            title?: string;
            affix?: boolean;
            noCache?: boolean;
        };
    }
}
src/types/setting.d.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
declare type DefaultSettings = {
    /**
     * ç½‘页标题
     */
    title?: string;
    /**
     * ä¾§è¾¹æ ä¸»é¢˜ theme-dark | theme-light
     */
    sideTheme?: string;
    /**
     * æ˜¯å¦æ˜¾ç¤ºç³»ç»Ÿå¸ƒå±€è®¾ç½®
     */
    showSettings?: boolean;
    /**
     * æ˜¯å¦æ˜¾ç¤ºé¡¶éƒ¨å¯¼èˆª
     */
    topNav?: boolean;
    /**
     * æ˜¯å¦æ˜¾ç¤ºå¤šæ ‡ç­¾å¯¼èˆª
     */
    tagsView?: boolean;
    /**
     * æ˜¯å¦å›ºå®šå¤´éƒ¨
     */
    fixedHeader?: boolean;
    /**
     * æ˜¯å¦æ˜¾ç¤ºä¾§è¾¹æ Logo
     */
    sidebarLogo?: boolean;
    /**
     * å¯¼èˆªæ å¸ƒå±€
     */
    layout?: string;
    /**
     * ä¸»é¢˜æ¨¡å¼
     */
    theme?: string;
    /**
     * å¸ƒå±€å¤§å°
     */
    size?: string;
    /**
     * è¯­è¨€
     */
    language?: string;
    /**
     * æ˜¯å¦æ˜¾ç¤ºåŠ¨æ€æ ‡é¢˜
     */
    dynamicTitle?: boolean;
    /**
     * æ˜¯å¦å¯ç”¨åŠ¨ç”»æ•ˆæžœ
     */
    animationEnable?: boolean;
    /**
     *  æ˜¯å¦å¯ç”¨æš—黑模式
     *
     * true:暗黑模式
     * false: æ˜Žäº®æ¨¡å¼
     */
    dark?: boolean;
    errorLog?: string;
};
src/utils/auth.js
ÎļþÒÑɾ³ý
src/utils/auth.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import Cookies from 'js-cookie';
const TokenKey = 'Admin-Token';
export const getToken = () => Cookies.get(TokenKey);
export const setToken = (token: string) => Cookies.set(TokenKey, token);
export const removeToken = () => Cookies.remove(TokenKey);
src/utils/dict.js
ÎļþÒÑɾ³ý
src/utils/dict.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
import { getDicts } from '@/api/system/dict/data';
import { useDictStore } from '@/store/modules/dict';
/**
 * èŽ·å–å­—å…¸æ•°æ®
 */
export const useDict = (...args: string[]): { [key: string]: DictDataOption[] } => {
    const res = ref<{
        [key: string]: DictDataOption[];
    }>({});
    return (() => {
        args.forEach(async (dictType) => {
            res.value[dictType] = [];
            const dicts = useDictStore().getDict(dictType);
            if (dicts) {
                res.value[dictType] = dicts;
            } else {
                await getDicts(dictType).then((resp) => {
                    res.value[dictType] = resp.data.map(
                        (p): DictDataOption => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass })
                    );
                    useDictStore().setDict(dictType, res.value[dictType]);
                });
            }
        });
        return res.value;
    })();
};
src/utils/dynamicTitle.js
ÎļþÒÑɾ³ý
src/utils/dynamicTitle.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
import defaultSettings from '@/settings';
import { useSettingsStore } from '@/store/modules/settings';
/**
 * åŠ¨æ€ä¿®æ”¹æ ‡é¢˜
 */
export const useDynamicTitle = () => {
    const settingsStore = useSettingsStore();
    if (settingsStore.dynamicTitle) {
        document.title = settingsStore.title + ' - ' + import.meta.env.VITE_APP_TITLE;
    } else {
        document.title = defaultSettings.title as string;
    }
};
src/utils/errorCode.js
ÎļþÒÑɾ³ý
src/utils/errorCode.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
export const errorCode: any = {
    '401': '认证失败,无法访问系统资源',
    '403': '当前操作没有权限',
    '404': '访问资源不存在',
    default: '系统未知错误,请反馈给管理员'
};
export default errorCode;
src/utils/i18n.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
// translate router.meta.title, be used in breadcrumb sidebar tagsview
import i18n from '@/lang/index';
export const translateRouteTitleI18n = (title: string): string => {
    // åˆ¤æ–­æ˜¯å¦å­˜åœ¨å›½é™…化配置,如果没有原生返回
    const hasKey = i18n.global.te('route.' + title);
    if (hasKey) {
        const translatedTitle = i18n.global.t('route.' + title);
        return translatedTitle;
    }
    return title;
};
src/utils/index.js
ÎļþÒÑɾ³ý
src/utils/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,318 @@
import { parseTime } from '@/utils/ruoyi';
/**
 * è¡¨æ ¼æ—¶é—´æ ¼å¼åŒ–
 */
export const formatDate = (cellValue: string) => {
    if (cellValue == null || cellValue == '') return '';
    const date = new Date(cellValue);
    const year = date.getFullYear();
    const month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1;
    const day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
    const hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
    const minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
    const seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
    return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds;
};
/**
 * @param {number} time
 * @param {string} option
 * @returns {string}
 */
export const formatTime = (time: string, option: string) => {
    let t: number;
    if (('' + time).length === 10) {
        t = parseInt(time) * 1000;
    } else {
        t = +time;
    }
    const d: any = new Date(t);
    const now = Date.now();
    const diff = (now - d) / 1000;
    if (diff < 30) {
        return '刚刚';
    } else if (diff < 3600) {
        // less 1 hour
        return Math.ceil(diff / 60) + '分钟前';
    } else if (diff < 3600 * 24) {
        return Math.ceil(diff / 3600) + '小时前';
    } else if (diff < 3600 * 24 * 2) {
        return '1天前';
    }
    if (option) {
        return parseTime(t, option);
    } else {
        return d.getMonth() + 1 + '月' + d.getDate() + '日' + d.getHours() + '时' + d.getMinutes() + '分';
    }
};
/**
 * @param {string} url
 * @returns {Object}
 */
export const getQueryObject = (url: string) => {
    url = url == null ? window.location.href : url;
    const search = url.substring(url.lastIndexOf('?') + 1);
    const obj: { [key: string]: string } = {};
    const reg = /([^?&=]+)=([^?&=]*)/g;
    search.replace(reg, (rs, $1, $2) => {
        const name = decodeURIComponent($1);
        let val = decodeURIComponent($2);
        val = String(val);
        obj[name] = val;
        return rs;
    });
    return obj;
};
/**
 * @param {string} input value
 * @returns {number} output value
 */
export const byteLength = (str: string) => {
    // returns the byte length of an utf8 string
    let s = str.length;
    for (let i = str.length - 1; i >= 0; i--) {
        const code = str.charCodeAt(i);
        if (code > 0x7f && code <= 0x7ff) s++;
        else if (code > 0x7ff && code <= 0xffff) s += 2;
        if (code >= 0xdc00 && code <= 0xdfff) i--;
    }
    return s;
};
/**
 * @param {Array} actual
 * @returns {Array}
 */
export const cleanArray = (actual: Array<any>) => {
    const newArray = [];
    for (let i = 0; i < actual.length; i++) {
        if (actual[i]) {
            newArray.push(actual[i]);
        }
    }
    return newArray;
};
/**
 * @param {Object} json
 * @returns {Array}
 */
export const param = (json: any) => {
    if (!json) return '';
    return cleanArray(
        Object.keys(json).map((key) => {
            if (json[key] === undefined) return '';
            return encodeURIComponent(key) + '=' + encodeURIComponent(json[key]);
        })
    ).join('&');
};
/**
 * @param {string} url
 * @returns {Object}
 */
export const param2Obj = (url: string) => {
    const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ');
    if (!search) {
        return {};
    }
    const obj: any = {};
    const searchArr = search.split('&');
    searchArr.forEach((v) => {
        const index = v.indexOf('=');
        if (index !== -1) {
            const name = v.substring(0, index);
            const val = v.substring(index + 1, v.length);
            obj[name] = val;
        }
    });
    return obj;
};
/**
 * @param {string} val
 * @returns {string}
 */
export const html2Text = (val: string) => {
    const div = document.createElement('div');
    div.innerHTML = val;
    return div.textContent || div.innerText;
};
/**
 * Merges two objects, giving the last one precedence
 * @param {Object} target
 * @param {(Object|Array)} source
 * @returns {Object}
 */
export const objectMerge = (target: any, source: any | any[]) => {
    if (typeof target !== 'object') {
        target = {};
    }
    if (Array.isArray(source)) {
        return source.slice();
    }
    Object.keys(source).forEach((property) => {
        const sourceProperty = source[property];
        if (typeof sourceProperty === 'object') {
            target[property] = objectMerge(target[property], sourceProperty);
        } else {
            target[property] = sourceProperty;
        }
    });
    return target;
};
/**
 * @param {HTMLElement} element
 * @param {string} className
 */
export const toggleClass = (element: HTMLElement, className: string) => {
    if (!element || !className) {
        return;
    }
    let classString = element.className;
    const nameIndex = classString.indexOf(className);
    if (nameIndex === -1) {
        classString += '' + className;
    } else {
        classString = classString.substring(0, nameIndex) + classString.substring(nameIndex + className.length);
    }
    element.className = classString;
};
/**
 * @param {string} type
 * @returns {Date}
 */
export const getTime = (type: string) => {
    if (type === 'start') {
        return new Date().getTime() - 3600 * 1000 * 24 * 90;
    } else {
        return new Date(new Date().toDateString());
    }
};
/**
 * @param {Function} func
 * @param {number} wait
 * @param {boolean} immediate
 * @return {*}
 */
export const debounce = (func: any, wait: number, immediate: boolean) => {
    let timeout: any, args: any, context: any, timestamp: any, result: any;
    const later = function () {
        // æ®ä¸Šä¸€æ¬¡è§¦å‘æ—¶é—´é—´éš”
        const last = +new Date() - timestamp;
        // ä¸Šæ¬¡è¢«åŒ…装函数被调用时间间隔 last å°äºŽè®¾å®šæ—¶é—´é—´éš” wait
        if (last < wait && last > 0) {
            timeout = setTimeout(later, wait - last);
        } else {
            timeout = null;
            // å¦‚果设定为immediate===true,因为开始边界已经调用过了此处无需调用
            if (!immediate) {
                result = func.apply(context, args);
                if (!timeout) context = args = null;
            }
        }
    };
    return (...args: any) => {
        context = this;
        timestamp = +new Date();
        const callNow = immediate && !timeout;
        // å¦‚果延时不存在,重新设定延时
        if (!timeout) timeout = setTimeout(later, wait);
        if (callNow) {
            result = func.apply(context, args);
            context = args = null;
        }
        return result;
    };
};
/**
 * This is just a simple version of deep copy
 * Has a lot of edge cases bug
 * If you want to use a perfect deep copy, use lodash's _.cloneDeep
 * @param {Object} source
 * @returns {Object}
 */
export const deepClone = (source: any) => {
    if (!source && typeof source !== 'object') {
        throw new Error('error arguments', 'deepClone' as any);
    }
    const targetObj: any = source.constructor === Array ? [] : {};
    Object.keys(source).forEach((keys) => {
        if (source[keys] && typeof source[keys] === 'object') {
            targetObj[keys] = deepClone(source[keys]);
        } else {
            targetObj[keys] = source[keys];
        }
    });
    return targetObj;
};
/**
 * @param {Array} arr
 * @returns {Array}
 */
export const uniqueArr = (arr: any) => {
    return Array.from(new Set(arr));
};
/**
 * @returns {string}
 */
export const createUniqueString = (): string => {
    const timestamp = +new Date() + '';
    const num = (1 + Math.random()) * 65536;
    const randomNum = parseInt(num + '');
    return (+(randomNum + timestamp)).toString(32);
};
/**
 * Check if an element has a class
 * @param ele
 * @param {string} cls
 * @returns {boolean}
 */
export const hasClass = (ele: HTMLElement, cls: string): boolean => {
    return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'));
};
/**
 * Add class to element
 * @param ele
 * @param {string} cls
 */
export const addClass = (ele: HTMLElement, cls: string) => {
    if (!hasClass(ele, cls)) ele.className += ' ' + cls;
};
/**
 * Remove class from element
 * @param ele
 * @param {string} cls
 */
export const removeClass = (ele: HTMLElement, cls: string) => {
    if (hasClass(ele, cls)) {
        const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)');
        ele.className = ele.className.replace(reg, ' ');
    }
};
/**
 * @param {string} path
 * @returns {Boolean}
 */
export const isExternal = (path: string) => {
    return /^(https?:|http?:|mailto:|tel:)/.test(path);
};
src/utils/jsencrypt.js
ÎļþÒÑɾ³ý
src/utils/jsencrypt.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
import JSEncrypt from 'jsencrypt';
// å¯†é’¥å¯¹ç”Ÿæˆ http://web.chacuo.net/netrsakeypair
const publicKey =
    'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' + 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==';
const privateKey =
    'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
    '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
    'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
    'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
    'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
    'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
    'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
    'UP8iWi1Qw0Y=';
// åР坆
export const encrypt = (txt: string) => {
    const encryptor = new JSEncrypt();
    encryptor.setPublicKey(publicKey); // è®¾ç½®å…¬é’¥
    return encryptor.encrypt(txt); // å¯¹æ•°æ®è¿›è¡ŒåР坆
};
// è§£å¯†
export const decrypt = (txt: string) => {
    const encryptor = new JSEncrypt();
    encryptor.setPrivateKey(privateKey); // è®¾ç½®ç§é’¥
    return encryptor.decrypt(txt); // å¯¹æ•°æ®è¿›è¡Œè§£å¯†
};
src/utils/permission.js
ÎļþÒÑɾ³ý
src/utils/permission.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
import useUserStore from '@/store/modules/user';
/**
 * å­—符权限校验
 * @param {Array} value æ ¡éªŒå€¼
 * @returns {Boolean}
 */
export const checkPermi = (value: any) => {
    if (value && value instanceof Array && value.length > 0) {
        const permissions = useUserStore().permissions;
        const permissionDatas = value;
        const all_permission = '*:*:*';
        const hasPermission = permissions.some((permission) => {
            return all_permission === permission || permissionDatas.includes(permission);
        });
        if (!hasPermission) {
            return false;
        }
        return true;
    } else {
        console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`);
        return false;
    }
};
/**
 * è§’色权限校验
 * @param {Array} value æ ¡éªŒå€¼
 * @returns {Boolean}
 */
export const checkRole = (value: any): boolean => {
    if (value && value instanceof Array && value.length > 0) {
        const roles = useUserStore().roles;
        const permissionRoles = value;
        const super_admin = 'admin';
        const hasRole = roles.some((role) => {
            return super_admin === role || permissionRoles.includes(role);
        });
        if (!hasRole) {
            return false;
        }
        return true;
    } else {
        console.error(`need roles! Like checkRole="['admin','editor']"`);
        return false;
    }
};
src/utils/request.js
ÎļþÒÑɾ³ý
src/utils/request.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,159 @@
import axios, { InternalAxiosRequestConfig } from 'axios';
import { useUserStore } from '@/store/modules/user';
import { getToken } from '@/utils/auth';
import { tansParams, blobValidate } from '@/utils/ruoyi';
import cache from '@/plugins/cache';
import { HttpStatus } from '@/enums/RespEnum';
import { errorCode } from '@/utils/errorCode';
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import FileSaver from 'file-saver';
let downloadLoadingInstance: LoadingInstance;
// æ˜¯å¦æ˜¾ç¤ºé‡æ–°ç™»å½•
export const isRelogin = { show: false };
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8';
// å¯¹åº”国际化资源文件后缀
axios.defaults.headers['Content-Language'] = 'zh_CN';
// åˆ›å»º axios å®žä¾‹
const service = axios.create({
    baseURL: import.meta.env.VITE_APP_BASE_API,
    timeout: 50000
});
// è¯·æ±‚拦截器
service.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
        const isToken = (config.headers || {}).isToken === false;
        // æ˜¯å¦éœ€è¦é˜²æ­¢æ•°æ®é‡å¤æäº¤
        const isRepeatSubmit = !(config.headers || {}).repeatSubmit;
        if (getToken() && !isToken) {
            config.headers['Authorization'] = 'Bearer ' + getToken(); // è®©æ¯ä¸ªè¯·æ±‚携带自定义token è¯·æ ¹æ®å®žé™…情况自行修改
        }
        // get请求映射params参数
        if (config.method === 'get' && config.params) {
            let url = config.url + '?' + tansParams(config.params);
            url = url.slice(0, -1);
            config.params = {};
            config.url = url;
        }
        if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
            const requestObj = {
                url: config.url,
                data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
                time: new Date().getTime()
            };
            const sessionObj = cache.session.getJSON('sessionObj');
            if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
                cache.session.setJSON('sessionObj', requestObj);
            } else {
                const s_url = sessionObj.url; // è¯·æ±‚地址
                const s_data = sessionObj.data; // è¯·æ±‚数据
                const s_time = sessionObj.time; // è¯·æ±‚æ—¶é—´
                const interval = 500; // é—´é𔿗¶é—´(ms),小于此时间视为重复提交
                if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
                    const message = '数据正在处理,请勿重复提交';
                    console.warn(`[${s_url}]: ` + message);
                    return Promise.reject(new Error(message));
                } else {
                    cache.session.setJSON('sessionObj', requestObj);
                }
            }
        }
        return config;
    },
    (error: any) => {
        console.log(error);
        return Promise.reject(error);
    }
);
// å“åº”拦截器
service.interceptors.response.use(
    (res) => {
        // æœªè®¾ç½®çŠ¶æ€ç åˆ™é»˜è®¤æˆåŠŸçŠ¶æ€
        const code = res.data.code || HttpStatus.SUCCESS;
        // èŽ·å–é”™è¯¯ä¿¡æ¯
        const msg = errorCode[code] || res.data.msg || errorCode['default'];
        // äºŒè¿›åˆ¶æ•°æ®åˆ™ç›´æŽ¥è¿”回
        if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
            return res.data;
        }
        if (code === 401) {
            // prettier-ignore
            if (!isRelogin.show) {
                isRelogin.show = true;
                ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
                    confirmButtonText: '重新登录',
                    cancelButtonText: '取消',
                    type: 'warning'
                }).then(() => {
                    isRelogin.show = false;
                    useUserStore().logout().then(() => {
                            location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
                        });
                }).catch(() => {
                    isRelogin.show = false;
                });
            }
            return Promise.reject('无效的会话,或者会话已过期,请重新登录。');
        } else if (code === HttpStatus.SERVER_ERROR) {
            console.log(msg);
            ElMessage({ message: msg, type: 'error' });
            return Promise.reject(new Error(msg));
        } else if (code === HttpStatus.WARN) {
            ElMessage({ message: msg, type: 'warning' });
            return Promise.reject(new Error(msg));
        } else if (code !== HttpStatus.SUCCESS) {
            ElNotification.error({ title: msg });
            return Promise.reject('error');
        } else {
            return Promise.resolve(res.data);
        }
    },
    (error) => {
        let { message } = error;
        if (message == 'Network Error') {
            message = '后端接口连接异常';
        } else if (message.includes('timeout')) {
            message = '系统接口请求超时';
        } else if (message.includes('Request failed with status code')) {
            message = '系统接口' + message.substr(message.length - 3) + '异常';
        }
        ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
        return Promise.reject(error);
    }
);
// é€šç”¨ä¸‹è½½æ–¹æ³•
export function download(url: string, params: any, fileName: string) {
    downloadLoadingInstance = ElLoading.service({ text: '正在下载数据,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
    // prettier-ignore
    return service.post(url, params, {
            transformRequest: [
                (params) => {
                    return tansParams(params);
                }
            ],
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            responseType: 'blob'
        }).then(async (resp) => {
            const isLogin = blobValidate(resp);
            if (isLogin) {
                const blob = new Blob([resp as any]);
        FileSaver.saveAs(blob, fileName);
            } else {
                const resText = await resp.data.text();
                const rspObj = JSON.parse(resText);
                const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'];
                ElMessage.error(errMsg);
            }
            downloadLoadingInstance.close();
        }).catch((r) => {
            console.error(r);
            ElMessage.error('下载文件出现错误,请联系管理员!');
            downloadLoadingInstance.close();
        });
}
// å¯¼å‡º axios å®žä¾‹
export default service;
src/utils/ruoyi.js
ÎļþÒÑɾ³ý
src/utils/ruoyi.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,247 @@
// æ—¥æœŸæ ¼å¼åŒ–
export function parseTime(time: any, pattern?: string) {
    if (arguments.length === 0 || !time) {
        return null;
    }
    const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
    let date;
    if (typeof time === 'object') {
        date = time;
    } else {
        if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
            time = parseInt(time);
        } else if (typeof time === 'string') {
            time = time
                .replace(new RegExp(/-/gm), '/')
                .replace('T', ' ')
                .replace(new RegExp(/\.[\d]{3}/gm), '');
        }
        if (typeof time === 'number' && time.toString().length === 10) {
            time = time * 1000;
        }
        date = new Date(time);
    }
    const formatObj: { [key: string]: any } = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
    };
    return format.replace(/{(y|m|d|h|i|s|a)+}/g, (result: string, key: string) => {
        let value = formatObj[key];
        // Note: getDay() returns 0 on Sunday
        if (key === 'a') {
            return ['日', '一', '二', '三', '四', '五', '六'][value];
        }
        if (result.length > 0 && value < 10) {
            value = '0' + value;
        }
        return value || 0;
    });
}
/**
 * æ·»åŠ æ—¥æœŸèŒƒå›´
 * @param params
 * @param dateRange
 * @param propName
 */
export const addDateRange = (params: any, dateRange: any[], propName?: string) => {
    const search = params;
    search.params = typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
    dateRange = Array.isArray(dateRange) ? dateRange : [];
    if (typeof propName === 'undefined') {
        search.params['beginTime'] = dateRange[0];
        search.params['endTime'] = dateRange[1];
    } else {
        search.params['begin' + propName] = dateRange[0];
        search.params['end' + propName] = dateRange[1];
    }
    return search;
};
// å›žæ˜¾æ•°æ®å­—å…¸
export const selectDictLabel = (datas: any, value: number | string) => {
    if (value === undefined) {
        return '';
    }
    const actions = [];
    Object.keys(datas).some((key) => {
        if (datas[key].value == '' + value) {
            actions.push(datas[key].label);
            return true;
        }
    });
    if (actions.length === 0) {
        actions.push(value);
    }
    return actions.join('');
};
// å›žæ˜¾æ•°æ®å­—典(字符串数组)
export const selectDictLabels = (datas: any, value: any, separator: any) => {
    if (value === undefined || value.length === 0) {
        return '';
    }
    if (Array.isArray(value)) {
        value = value.join(',');
    }
    const actions: any[] = [];
    const currentSeparator = undefined === separator ? ',' : separator;
    const temp = value.split(currentSeparator);
    Object.keys(value.split(currentSeparator)).some((val) => {
        let match = false;
        Object.keys(datas).some((key) => {
            if (datas[key].value == '' + temp[val]) {
                actions.push(datas[key].label + currentSeparator);
                match = true;
            }
        });
        if (!match) {
            actions.push(temp[val] + currentSeparator);
        }
    });
    return actions.join('').substring(0, actions.join('').length - 1);
};
// å­—符串格式化(%s )
export function sprintf(str: string) {
    if (arguments.length !== 0) {
        let flag = true,
            i = 1;
        str = str.replace(/%s/g, function () {
            const arg = arguments[i++];
            if (typeof arg === 'undefined') {
                flag = false;
                return '';
            }
            return arg;
        });
        return flag ? str : '';
    }
}
// è½¬æ¢å­—符串,undefined,null等转化为""
export const parseStrEmpty = (str: any) => {
    if (!str || str == 'undefined' || str == 'null') {
        return '';
    }
    return str;
};
// æ•°æ®åˆå¹¶
export const mergeRecursive = (source: any, target: any) => {
    for (const p in target) {
        try {
            if (target[p].constructor == Object) {
                source[p] = mergeRecursive(source[p], target[p]);
            } else {
                source[p] = target[p];
            }
        } catch (e) {
            source[p] = target[p];
        }
    }
    return source;
};
/**
 * æž„造树型结构数据
 * @param {*} data æ•°æ®æº
 * @param {*} id id字段 é»˜è®¤ 'id'
 * @param {*} parentId çˆ¶èŠ‚ç‚¹å­—æ®µ é»˜è®¤ 'parentId'
 * @param {*} children å­©å­èŠ‚ç‚¹å­—æ®µ é»˜è®¤ 'children'
 */
export const handleTree = <T>(data: any[], id?: string, parentId?: string, children?: string): T[] => {
    const config: {
        id: string;
        parentId: string;
        childrenList: string;
    } = {
        id: id || 'id',
        parentId: parentId || 'parentId',
        childrenList: children || 'children'
    };
    const childrenListMap: any = {};
    const nodeIds: any = {};
    const tree: T[] = [];
    for (const d of data) {
        const parentId = d[config.parentId];
        if (childrenListMap[parentId] == null) {
            childrenListMap[parentId] = [];
        }
        nodeIds[d[config.id]] = d;
        childrenListMap[parentId].push(d);
    }
    for (const d of data) {
        const parentId = d[config.parentId];
        if (nodeIds[parentId] == null) {
            tree.push(d);
        }
    }
    const adaptToChildrenList = (o: any) => {
        if (childrenListMap[o[config.id]] !== null) {
            o[config.childrenList] = childrenListMap[o[config.id]];
        }
        if (o[config.childrenList]) {
            for (const c of o[config.childrenList]) {
                adaptToChildrenList(c);
            }
        }
    };
    for (const t of tree) {
        adaptToChildrenList(t);
    }
    return tree;
};
/**
 * å‚数处理
 * @param {*} params  å‚æ•°
 */
export const tansParams = (params: any) => {
    let result = '';
    for (const propName of Object.keys(params)) {
        const value = params[propName];
        const part = encodeURIComponent(propName) + '=';
        if (value !== null && value !== '' && typeof value !== 'undefined') {
            if (typeof value === 'object') {
                for (const key of Object.keys(value)) {
                    if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
                        const params = propName + '[' + key + ']';
                        const subPart = encodeURIComponent(params) + '=';
                        result += subPart + encodeURIComponent(value[key]) + '&';
                    }
                }
            } else {
                result += part + encodeURIComponent(value) + '&';
            }
        }
    }
    return result;
};
// è¿”回项目路径
export const getNormalPath = (p: string): string => {
    if (p.length === 0 || !p || p === 'undefined') {
        return p;
    }
    const res = p.replace('//', '/');
    if (res[res.length - 1] === '/') {
        return res.slice(0, res.length - 1);
    }
    return res;
};
// éªŒè¯æ˜¯å¦ä¸ºblob格式
export const blobValidate = (data: any) => {
    return data.type !== 'application/json';
};
src/utils/scroll-to.js
ÎļþÒÑɾ³ý
src/utils/scroll-to.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
    t /= d / 2;
    if (t < 1) {
        return (c / 2) * t * t + b;
    }
    t--;
    return (-c / 2) * (t * (t - 2) - 1) + b;
};
// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
const requestAnimFrame = (function () {
    return (
        window.requestAnimationFrame ||
        (window as any).webkitRequestAnimationFrame ||
        (window as any).mozRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 1000 / 60);
        }
    );
})();
/**
 * Because it's so fucking difficult to detect the scrolling element, just move them all
 * @param {number} amount
 */
const move = (amount: number) => {
    document.documentElement.scrollTop = amount;
    (document.body.parentNode as HTMLElement).scrollTop = amount;
    document.body.scrollTop = amount;
};
const position = () => {
    return document.documentElement.scrollTop || (document.body.parentNode as HTMLElement).scrollTop || document.body.scrollTop;
};
/**
 * @param {number} to
 * @param {number} duration
 * @param {Function} callback
 */
export const scrollTo = (to: number, duration: number, callback?: any) => {
    const start = position();
    const change = to - start;
    const increment = 20;
    let currentTime = 0;
    duration = typeof duration === 'undefined' ? 500 : duration;
    const animateScroll = function () {
        // increment the time
        currentTime += increment;
        // find the value with the quadratic in-out easing function
        const val = easeInOutQuad(currentTime, start, change, duration);
        // move the document.body
        move(val);
        // do the animation unless its over
        if (currentTime < duration) {
            requestAnimFrame(animateScroll);
        } else {
            if (callback && typeof callback === 'function') {
                // the animation is done so lets callback
                callback();
            }
        }
    };
    animateScroll();
};
src/utils/theme.js
ÎļþÒÑɾ³ý
src/utils/theme.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
// å¤„理主题样式
export const handleThemeStyle = (theme: string) => {
    document.documentElement.style.setProperty('--el-color-primary', theme);
    for (let i = 1; i <= 9; i++) {
        document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`);
    }
    for (let i = 1; i <= 9; i++) {
        document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`);
    }
};
// hex颜色转rgb颜色
export const hexToRgb = (str: string): string[] => {
    str = str.replace('#', '');
    const hexs = str.match(/../g);
    for (let i = 0; i < 3; i++) {
        if (hexs) {
            hexs[i] = String(parseInt(hexs[i], 16));
        }
    }
    return hexs ? hexs : [];
};
// rgb颜色转Hex颜色
export const rgbToHex = (r: string, g: string, b: string) => {
    const hexs = [Number(r).toString(16), Number(g).toString(16), Number(b).toString(16)];
    for (let i = 0; i < 3; i++) {
        if (hexs[i].length == 1) {
            hexs[i] = `0${hexs[i]}`;
        }
    }
    return `#${hexs.join('')}`;
};
// å˜æµ…颜色值
export const getLightColor = (color: string, level: number) => {
    const rgb = hexToRgb(color);
    for (let i = 0; i < 3; i++) {
        const s = (255 - Number(rgb[i])) * level + Number(rgb[i]);
        rgb[i] = String(Math.floor(s));
    }
    return rgbToHex(rgb[0], rgb[1], rgb[2]);
};
// å˜æ·±é¢œè‰²å€¼
export const getDarkColor = (color: string, level: number) => {
    const rgb = hexToRgb(color);
    for (let i = 0; i < 3; i++) {
        rgb[i] = String(Math.floor(Number(rgb[i]) * (1 - level)));
    }
    return rgbToHex(rgb[0], rgb[1], rgb[2]);
};
src/utils/validate.js
ÎļþÒÑɾ³ý
src/utils/validate.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
/**
 * åˆ¤æ–­url是否是http或https
 * @returns {Boolean}
 * @param url
 */
export const isHttp = (url: string): boolean => {
    return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1;
};
/**
 * åˆ¤æ–­path是否为外链
 * @param {string} path
 * @returns {Boolean}
 */
export const isExternal = (path: string) => {
    return /^(https?:|mailto:|tel:)/.test(path);
};
/**
 * @param {string} str
 * @returns {Boolean}
 */
export const validUsername = (str: string) => {
    const valid_map = ['admin', 'editor'];
    return valid_map.indexOf(str.trim()) >= 0;
};
/**
 * @param {string} url
 * @returns {Boolean}
 */
export const validURL = (url: string) => {
    const reg =
        /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/;
    return reg.test(url);
};
/**
 * @param {string} str
 * @returns {Boolean}
 */
export const validLowerCase = (str: string) => {
    const reg = /^[a-z]+$/;
    return reg.test(str);
};
/**
 * @param {string} str
 * @returns {Boolean}
 */
export const validUpperCase = (str: string) => {
    const reg = /^[A-Z]+$/;
    return reg.test(str);
};
/**
 * @param {string} str
 * @returns {Boolean}
 */
export const validAlphabets = (str: string) => {
    const reg = /^[A-Za-z]+$/;
    return reg.test(str);
};
/**
 * @param {string} email
 * @returns {Boolean}
 */
export const validEmail = (email: string) => {
    const reg =
        /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return reg.test(email);
};
/**
 * @param {string} str
 * @returns {Boolean}
 */
export const isString = (str: any) => {
    return typeof str === 'string' || str instanceof String;
};
/**
 * @param {Array} arg
 * @returns {Boolean}
 */
export const isArray = (arg: string | string[]) => {
    if (typeof Array.isArray === 'undefined') {
        return Object.prototype.toString.call(arg) === '[object Array]';
    }
    return Array.isArray(arg);
};
src/views/demo/demo/index.vue
@@ -1,403 +1,357 @@
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
      <el-form-item label="key键" prop="testKey">
        <el-input
          v-model="queryParams.testKey"
          placeholder="请输入key键"
          clearable
          style="width: 200px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="值" prop="value">
        <el-input
          v-model="queryParams.value"
          placeholder="请输入值"
          clearable
          style="width: 200px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="创建时间">
        <el-date-picker
          v-model="daterangeCreateTime"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="daterange"
          range-separator="-"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
        ></el-date-picker>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
        <el-button type="primary" icon="search" @click="handlePage">搜索(自定义分页接口)</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['demo:demo:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['demo:demo:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['demo:demo:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="info"
          plain
          icon="Upload"
          @click="handleImport"
          v-hasPermi="['demo:demo:import']"
        >导入(校验)</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
          v-hasPermi="['demo:demo:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="主键" align="center" prop="id" v-if="columns[0].visible"/>
      <el-table-column label="部门id" align="center" prop="deptId" v-if="columns[1].visible"/>
      <el-table-column label="用户id" align="center" prop="userId" v-if="columns[2].visible"/>
      <el-table-column label="排序号" align="center" prop="orderNum" v-if="columns[3].visible"/>
      <el-table-column label="key键" align="center" prop="testKey" v-if="columns[4].visible"/>
      <el-table-column label="值" align="center" prop="value" v-if="columns[5].visible"/>
      <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[6].visible" width="180">
        <template #default="scope">
          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
        </template>
      </el-table-column>
      <el-table-column label="创建人" align="center" prop="createByName" v-if="columns[7].visible" />
      <el-table-column label="更新时间" align="center" prop="updateTime" v-if="columns[8].visible" width="180">
        <template #default="scope">
          <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
        </template>
      </el-table-column>
      <el-table-column label="更新人" align="center" prop="updateByName" v-if="columns[9].visible" />
      <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:demo:edit']">修改</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:demo:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
    <!-- æ·»åŠ æˆ–ä¿®æ”¹æµ‹è¯•å•è¡¨å¯¹è¯æ¡† -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="demoRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="部门id" prop="deptId">
          <el-input v-model="form.deptId" placeholder="请输入部门id" />
        </el-form-item>
        <el-form-item label="用户id" prop="userId">
          <el-input v-model="form.userId" placeholder="请输入用户id" />
        </el-form-item>
        <el-form-item label="排序号" prop="orderNum">
          <el-input v-model="form.orderNum" placeholder="请输入排序号" />
        </el-form-item>
        <el-form-item label="key键" prop="testKey">
          <el-input v-model="form.testKey" placeholder="请输入key键" />
        </el-form-item>
        <el-form-item label="值" prop="value">
          <el-input v-model="form.value" placeholder="请输入值" />
        </el-form-item>
        <el-form-item label="创建时间" prop="createTime">
          <el-date-picker clearable
                          v-model="form.createTime"
                          type="datetime"
                          value-format="YYYY-MM-DD HH:mm:ss"
                          placeholder="选择创建时间">
          </el-date-picker>
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button :loading="buttonLoading" type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- ç”¨æˆ·å¯¼å…¥å¯¹è¯æ¡† -->
    <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
      <el-upload
        ref="uploadRef"
        :limit="1"
        accept=".xlsx, .xls"
        :headers="upload.headers"
        :action="upload.url + '?updateSupport=' + upload.updateSupport"
        :disabled="upload.isUploading"
        :on-progress="handleFileUploadProgress"
        :on-success="handleFileSuccess"
        :auto-upload="false"
        drag
      >
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      </el-upload>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
        <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script setup name="Demo">
<script setup name="Demo" lang="ts">
import { listDemo, pageDemo, getDemo, delDemo, addDemo, updateDemo } from "@/api/demo/demo";
import {getToken} from "@/utils/auth";
import { getToken } from "@/utils/auth";
import { ComponentInternalInstance } from "vue";
import { ElUpload, UploadFile, UploadFiles, DateModelType } from 'element-plus';
import { DemoForm, DemoQuery, DemoVO } from "@/api/demo/types";
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const demoList = ref([]);
const open = ref(false);
const demoList = ref<DemoVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<string | number>>([])
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const daterangeCreateTime = ref([]);
const daterangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
/*** ç”¨æˆ·å¯¼å…¥å‚æ•° */
const upload = reactive({
  // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚(用户导入)
  open: false,
  // å¼¹å‡ºå±‚标题(用户导入)
  title: "",
  // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
  isUploading: false,
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "demo/demo/importData"
const demoFormRef = ref(ElForm);
const queryFormRef = ref(ElForm);
const uploadRef = ref(ElUpload);
const dialog = reactive<DialogOption>({
    visible: false,
    title: ''
});
/** ç”¨æˆ·å¯¼å…¥å‚æ•° */
const upload = reactive<ImportOption>({
    // æ˜¯å¦æ˜¾ç¤ºå¼¹å‡ºå±‚(用户导入)
    open: false,
    // å¼¹å‡ºå±‚标题(用户导入)
    title: "",
    // æ˜¯å¦ç¦ç”¨ä¸Šä¼ 
    isUploading: false,
    // è®¾ç½®ä¸Šä¼ çš„请求头部
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "demo/demo/importData"
})
// åˆ—显隐信息
const columns = ref([
  { key: 0, label: `主键`, visible: false },
  { key: 1, label: `部门id`, visible: true },
  { key: 2, label: `用户id`, visible: true },
  { key: 3, label: `排序号`, visible: true },
  { key: 4, label: `key键`, visible: true },
  { key: 5, label: `值`, visible: true },
  { key: 6, label: `创建时间`, visible: true },
  { key: 7, label: `创建人`, visible: true },
  { key: 8, label: `更新时间`, visible: true },
  { key: 9, label: `更新人`, visible: true }
const columns = ref<FieldOption[]>([
    { key: 0, label: `主键`, visible: false },
    { key: 1, label: `部门id`, visible: true },
    { key: 2, label: `用户id`, visible: true },
    { key: 3, label: `排序号`, visible: true },
    { key: 4, label: `key键`, visible: true },
    { key: 5, label: `值`, visible: true },
    { key: 6, label: `创建时间`, visible: true },
    { key: 7, label: `创建人`, visible: true },
    { key: 8, label: `更新时间`, visible: true },
    { key: 9, label: `更新人`, visible: true }
]);
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    testKey: undefined,
    value: undefined,
    createTime: undefined,
  },
  rules: {
    testKey: [
      { required: true, message: "key键不能为空", trigger: "blur" }
    ],
    value: [
      { required: true, message: "值不能为空", trigger: "blur" }
    ],
  }
const initDataForm: DemoForm = {
    id: undefined,
    deptId: undefined,
    userId: undefined,
    orderNum: 0,
    testKey: '',
    value: '',
    version: '',
    ossConfigId: undefined,
}
const data = reactive<PageData<DemoForm, DemoQuery>>({
    form: { ...initDataForm },
    queryParams: {
        pageNum: 1,
        pageSize: 10,
        testKey: '',
        value: '',
        createTime: '',
    },
    rules: {
        testKey: [{ required: true, message: "key键不能为空", trigger: "blur" }],
        value: [{ required: true, message: "值不能为空", trigger: "blur" }],
    }
});
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢OSS对象存储列表 */
function getList() {
  loading.value = true;
  listDemo(proxy.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime")).then(response => {
    demoList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
const getList = async () => {
    loading.value = true;
    const res = await listDemo(proxy?.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime"));
    demoList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** è‡ªå®šä¹‰åˆ†é¡µæŸ¥è¯¢ */
function getPage() {
  loading.value = true;
  pageDemo(proxy.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime")).then(response => {
    demoList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
const getPage = async () => {
    loading.value = true;
    const res = await pageDemo(proxy?.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime"));
    demoList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** å–消按钮 */
function cancel() {
  open.value = false;
  reset();
const cancel = () => {
    reset();
    dialog.visible = false;
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    id: undefined,
    deptId: undefined,
    userId: undefined,
    orderNum: undefined,
    testKey: undefined,
    value: undefined,
    version: undefined,
    createTime: undefined,
    createBy: undefined,
    updateTime: undefined,
    updateBy: undefined,
    delFlag: undefined
  };
  proxy.resetForm("demoRef");
const reset = () => {
    form.value = { ...initDataForm };
    demoFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
const handleQuery = () => {
    queryParams.value.pageNum = 1;
    getList();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handlePage() {
  queryParams.value.pageNum = 1;
  getList();
const handlePage = () => {
    queryParams.value.pageNum = 1;
    getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  daterangeCreateTime.value = [];
  proxy.resetForm("queryRef");
  handleQuery();
const resetQuery = () => {
    daterangeCreateTime.value = ['', ''];
    queryFormRef.value.resetFields();
    handleQuery();
}
/** é€‰æ‹©æ¡æ•°  */
function handleSelectionChange(selection) {
  ids.value = selection.map(item => item.id);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
const handleSelectionChange = (selection: DemoVO[]) => {
    ids.value = selection.map(item => item.id);
    single.value = selection.length != 1;
    multiple.value = !selection.length;
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加测试单表";
const handleAdd = () => {
    dialog.visible = true;
    dialog.title = "添加测试单表";
    nextTick(() => {
        reset();
    })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
  loading.value = true;
  reset();
  const ids = row.id || ids.value;
  getDemo(ids).then((response) => {
    loading.value = false;
    form.value = response.data;
    open.value = true;
    title.value = "修改测试单表";
  });
const handleUpdate = async (row?: DemoVO) => {
    loading.value = true;
    dialog.visible = true;
    dialog.title = "修改测试单表";
    const _ids = row?.id || ids.value[0];
    const res = await getDemo(_ids);
    nextTick(() => {
        reset();
        Object.assign(form.value, res.data)
        loading.value = false;
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["demoRef"].validate(valid => {
    if (valid) {
      buttonLoading.value = true;
      if (form.value.ossConfigId != null) {
        updateDemo(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        }).finally(() => {
          buttonLoading.value = false;
        });
      } else {
        addDemo(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        }).finally(() => {
          buttonLoading.value = false;
        });
      }
    }
  });
const submitForm = () => {
    demoFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            buttonLoading.value = true;
            if (form.value.ossConfigId) {
                await updateDemo(form.value).finally(() => buttonLoading.value = false);
            } else {
                await addDemo(form.value).finally(() => buttonLoading.value = false);
            }
            proxy?.$modal.msgSuccess("操作成功");
            dialog.visible = false;
            getList();
        }
    });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const ids = row.id || ids.value;
  proxy.$modal.confirm('是否确认删除测试单表编号为"' + ids + '"的数据项?').then(() => {
    loading.value = true;
    return delDemo(ids);
  }).then(() => {
    loading.value = false;
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).finally(() => {
    loading.value = false;
  });
const handleDelete = async (row?: DemoVO) => {
    const _ids = row?.id || ids.value;
    await proxy?.$modal.confirm('是否确认删除测试单表编号为"' + _ids + '"的数据项?');
    await delDemo(_ids).finally(() => loading.value = false);
    loading.value = false;
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
/** å¯¼å…¥æŒ‰é’®æ“ä½œ */
function handleImport() {
  upload.title = "测试导入";
  upload.open = true;
const handleImport = () => {
    upload.title = "测试导入";
    upload.open = true;
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
  proxy.download("demo/demo/export", {
    ...queryParams.value,
  },`demo_${new Date().getTime()}.xlsx`);
const handleExport = () => {
    proxy?.download("demo/demo/export", {
        ...queryParams.value,
    }, `demo_${new Date().getTime()}.xlsx`);
}
/**文件上传中处理 */
const handleFileUploadProgress = (event, file, fileList) => {
  upload.isUploading = true;
const handleFileUploadProgress = () => {
    upload.isUploading = true;
}
/** æ–‡ä»¶ä¸Šä¼ æˆåŠŸå¤„ç† */
const handleFileSuccess = (response, file, fileList) => {
  upload.open = false;
  upload.isUploading = false;
  proxy.$refs["uploadRef"].clearFiles();
  proxy.$alert(response.msg, "导入结果", { dangerouslyUseHTMLString: true });
  getList();
const handleFileSuccess = (res: any, file: UploadFile, fileList: UploadFiles) => {
    upload.open = false;
    upload.isUploading = false;
    uploadRef.value.clearFiles();
    ElMessageBox.alert(res.msg, "导入结果", { dangerouslyUseHTMLString: true });
    getList();
}
/** æäº¤ä¸Šä¼ æ–‡ä»¶ */
function submitFileForm() {
  proxy.$refs["uploadRef"].submit();
    uploadRef.value.submit();
}
getList()
getPage()
onMounted(() => {
    getList()
    getPage()
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="key键" prop="testKey">
                        <el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="值" prop="value">
                        <el-input v-model="queryParams.value" placeholder="请输入值" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="创建时间">
                        <el-date-picker
                            v-model="daterangeCreateTime"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            type="daterange"
                            range-separator="-"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
                        ></el-date-picker>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
                        <el-button type="primary" icon="search" @click="handlePage">搜索(自定义分页接口)</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['demo:demo:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['demo:demo:edit']">修改</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['demo:demo:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['demo:demo:import']">导入(校验)</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['demo:demo:export']">导出</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="主键" align="center" prop="id" v-if="columns[0].visible" />
                <el-table-column label="部门id" align="center" prop="deptId" v-if="columns[1].visible" />
                <el-table-column label="用户id" align="center" prop="userId" v-if="columns[2].visible" />
                <el-table-column label="排序号" align="center" prop="orderNum" v-if="columns[3].visible" />
                <el-table-column label="key键" align="center" prop="testKey" v-if="columns[4].visible" />
                <el-table-column label="值" align="center" prop="value" v-if="columns[5].visible" />
                <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[6].visible" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="创建人" align="center" prop="createByName" v-if="columns[7].visible" />
                <el-table-column label="更新时间" align="center" prop="updateTime" v-if="columns[8].visible" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="更新人" align="center" prop="updateByName" v-if="columns[9].visible" />
                <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:demo:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:demo:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹æµ‹è¯•å•è¡¨å¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
            <el-form ref="demoFormRef" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="部门id" prop="deptId">
                    <el-input v-model="form.deptId" placeholder="请输入部门id" />
                </el-form-item>
                <el-form-item label="用户id" prop="userId">
                    <el-input v-model="form.userId" placeholder="请输入用户id" />
                </el-form-item>
                <el-form-item label="排序号" prop="orderNum">
                    <el-input v-model="form.orderNum" placeholder="请输入排序号" />
                </el-form-item>
                <el-form-item label="key键" prop="testKey">
                    <el-input v-model="form.testKey" placeholder="请输入key键" />
                </el-form-item>
                <el-form-item label="值" prop="value">
                    <el-input v-model="form.value" placeholder="请输入值" />
                </el-form-item>
                <el-form-item label="创建时间" prop="createTime">
                    <el-date-picker clearable v-model="form.createTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择创建时间">
                    </el-date-picker>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button :loading="buttonLoading" type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
        <!-- ç”¨æˆ·å¯¼å…¥å¯¹è¯æ¡† -->
        <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
            <el-upload
                ref="uploadRef"
                :limit="1"
                accept=".xlsx, .xls"
                :headers="upload.headers"
                :action="upload.url + '?updateSupport=' + upload.updateSupport"
                :disabled="upload.isUploading"
                :on-progress="handleFileUploadProgress"
                :on-success="handleFileSuccess"
                :auto-upload="false"
                drag
            >
                <i class="el-icon-upload"></i>
                <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
            </el-upload>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitFileForm">ç¡® å®š</el-button>
                    <el-button @click="upload.open = false">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/demo/tree/index.vue
@@ -1,131 +1,31 @@
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
      <el-form-item label="树节点名" prop="treeName">
        <el-input
          v-model="queryParams.treeName"
          placeholder="请输入树节点名"
          clearable
          style="width: 200px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="创建时间">
        <el-date-picker
          v-model="daterangeCreateTime"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="daterange"
          range-separator="-"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
        ></el-date-picker>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
<script setup name="Tree" lang="ts">
import { listTree, getTree, delTree, addTree, updateTree } from '@/api/demo/tree';
import { DemoTreeVO, DemoTreeForm, DemoTreeOptionsType, DemoTreeQuery } from '@/api/demo/types';
import { ComponentInternalInstance } from 'vue';
import { ElTree, ElForm, ElTable, DateModelType } from 'element-plus';
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['demo:tree:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
            type="info"
            plain
            icon="Sort"
            @click="toggleExpandAll"
        >展开/折叠</el-button>
      </el-col>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
    <el-table
      v-if="refreshTable"
      v-loading="loading"
      :data="treeList"
      row-key="id"
      :default-expand-all="isExpandAll"
      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
    >
      <el-table-column label="父id" prop="parentId" v-if="columns[0].visible" />
      <el-table-column label="部门id" align="center" prop="deptId" v-if="columns[1].visible" />
      <el-table-column label="用户id" align="center" prop="userId" v-if="columns[2].visible" />
      <el-table-column label="树节点名" align="center" prop="treeName" v-if="columns[3].visible" />
      <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[4].visible" width="180">
        <template #default="scope">
          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:tree:edit']">修改</el-button>
          <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['demo:tree:add']">新增</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:tree:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- æ·»åŠ æˆ–ä¿®æ”¹æµ‹è¯•æ ‘è¡¨å¯¹è¯æ¡† -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="treeRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="父id" prop="parentId">
          <treeselect
              v-model:value="form.parentId"
              :options="treeOptions"
              :objMap="{ value: 'id', label: 'true_name', children: 'children' }"
              placeholder="请选择父id"
          />
        </el-form-item>
        <el-form-item label="部门id" prop="deptId">
          <el-input v-model="form.deptId" placeholder="请输入部门id" />
        </el-form-item>
        <el-form-item label="用户id" prop="userId">
          <el-input v-model="form.userId" placeholder="请输入用户id" />
        </el-form-item>
        <el-form-item label="树节点名" prop="treeName">
          <el-input v-model="form.treeName" placeholder="请输入树节点名" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button :loading="buttonLoading" type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup name="Tree">
import { listTree, getTree, delTree, addTree, updateTree } from "@/api/demo/tree";
const { proxy } = getCurrentInstance();
const treeList = ref([]);
const open = ref(false);
const treeList = ref<DemoTreeVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const isExpandAll = ref(true);
const refreshTable = ref(true);
const treeOptions = ref(undefined);
const daterangeCreateTime = ref([]);
const treeOptions = ref<DemoTreeOptionsType[]>([]);
const daterangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
const treeRef = ref(ElTree);
const qeuryFormRef = ref(ElForm);
const demoTreeTableRef = ref(ElTable)
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
});
// åˆ—显隐信息
const columns = ref([
const columns = ref<FieldOption[]>([
  { key: 0, label: `父id`, visible: false },
  { key: 1, label: `部门id`, visible: true },
  { key: 2, label: `用户id`, visible: true },
@@ -133,128 +33,132 @@
  { key: 4, label: `创建时间`, visible: true }
]);
const data = reactive({
const initFormData = {
  id: undefined,
  parentId: undefined,
  deptId: undefined,
  userId: undefined,
  treeName: ''
}
const data = reactive<PageData<DemoTreeForm, DemoTreeQuery>>({
  // æŸ¥è¯¢å‚æ•°
  queryParams: {
    treeName: null,
    createTime: null,
    treeName: '',
    createTime: '',
  },
  // è¡¨å•参数
  form: {},
  form: {...initFormData},
  // è¡¨å•校验
  rules: {
    treeName: [
      { required: true, message: "树节点名不能为空", trigger: "blur" }
    ],
    treeName: [{ required: true, message: "树节点名不能为空", trigger: "blur" }],
  }
});
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢æµ‹è¯•树表列表 */
function getList() {
const getList = () => {
  loading.value = true;
  listTree(proxy.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime")).then(response => {
    treeList.value = proxy.handleTree(response.data, "id", "parentId");
    total.value = response.total;
  listTree(proxy?.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime")).then(res => {
    const data = proxy?.handleTree<DemoTreeVO>(res.data, "id", "parentId");
    if (data) {
      treeList.value = data
    }
    loading.value = false;
  });
}
/** æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构 */
async function getTreeselect() {
  await listTree().then(response => {
    treeOptions.value = [];
    const data = { id: 0, treeName: '顶级节点', children: [] };
    data.children = proxy.handleTree(response.data, "id", "parentId");
    treeOptions.value.push(data);
const getTreeSelect = async () => {
  listTree(proxy?.addDateRange(queryParams.value, daterangeCreateTime.value, "CreateTime")).then(res => {
  const topData: DemoTreeOptionsType = { id: 0, treeName: '顶级节点', children: [] };
  topData.children = proxy?.handleTree<DemoTreeOptionsType>(res.data, "id", "parentId");
  treeOptions.value.push(topData);
  });
}
/** å–消按钮 */
function cancel() {
  open.value = false;
const cancel = () => {
  reset();
  dialog.visible = false;
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    id: undefined,
    parentId: undefined,
    deptId: undefined,
    userId: undefined,
    treeName: undefined,
    version: undefined,
    createTime: undefined,
    createBy: undefined,
    updateTime: undefined,
    updateBy: undefined,
    delFlag: undefined
  };
  proxy.resetForm("treeRef");
const reset = () => {
  form.value = {...initFormData}
  treeRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  daterangeCreateTime.value = [];
  proxy.resetForm("queryRef");
const resetQuery = () => {
  daterangeCreateTime.value = ['', ''];
  qeuryFormRef.value.resetFields();
  handleQuery();
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
async function handleAdd(row) {
  reset();
  await getTreeselect();
  if (row != null && row.id) {
    form.value.parentId = row.id;
  } else {
    form.value.parentId = 0;
  }
  open.value = true;
  title.value = "添加测试树表";
const handleAdd = (row?: DemoTreeVO) => {
  dialog.visible = true;
  dialog.title = "添加测试树表";
  nextTick(() => {
    reset();
    getTreeSelect();
    if (row != null && row.id) {
      form.value.parentId = row.id;
    } else {
      form.value.parentId = 0;
    }
  })
}
/** å±•å¼€/折叠操作 */
function toggleExpandAll() {
  refreshTable.value = false;
const handleToggleExpandAll = () => {
  isExpandAll.value = !isExpandAll.value;
  nextTick(() => {
    refreshTable.value = true;
  });
  toggleExpandAll(treeList.value, isExpandAll.value)
}
/** å±•å¼€/折叠所有 */
const toggleExpandAll = (data: DemoTreeVO[], status: boolean) => {
  data.forEach((item) => {
    demoTreeTableRef.value.toggleRowExpansion(item, status)
    if(item.children && item.children.length > 0) toggleExpandAll(item.children, status)
  })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
async function handleUpdate(row) {
const handleUpdate = async (row: DemoTreeVO) => {
  loading.value = true;
  reset();
  await getTreeselect();
  if (row != null) {
    form.value.parentId = row.id;
  }
  getTree(row.id).then((response) => {
    loading.value = false;
    form.value = response.data;
    open.value = true;
    title.value = "修改测试树表";
  });
  dialog.visible = true;
  dialog.title = "修改测试树表";
  nextTick(async () => {
    reset();
    getTreeSelect();
    if (row) {
      form.value.parentId = row.id;
    }
    getTree(row.id).then((response) => {
      loading.value = false;
      form.value = response.data;
    });
  })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["treeRef"].validate(valid => {
const submitForm = () => {
  treeRef.value.validate((valid: boolean) => {
    if (valid) {
      buttonLoading.value = true;
      if (form.value.ossConfigId != null) {
        updateTree(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
      if (form.value.id != null) {
        updateTree(form.value).then(() => {
          proxy?.$modal.msgSuccess("修改成功");
          dialog.visible = false;
          getList();
        }).finally(() => {
          buttonLoading.value = false;
        });
      } else {
        addTree(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
        addTree(form.value).then(() => {
          proxy?.$modal.msgSuccess("新增成功");
          dialog.visible = false;
          getList();
        }).finally(() => {
          buttonLoading.value = false;
@@ -264,18 +168,126 @@
  });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  proxy.$modal.confirm('是否确认删除测试单表编号为"' + row.id + '"的数据项?').then(() => {
const handleDelete = (row: DemoTreeVO) => {
  proxy?.$modal.confirm('是否确认删除测试单表编号为"' + row.id + '"的数据项?').then(() => {
    loading.value = true;
    return delTree(row.id);
  }).then(() => {
    loading.value = false;
    getList();
    proxy.$modal.msgSuccess("删除成功");
    proxy?.$modal.msgSuccess("删除成功");
  }).finally(() => {
    loading.value = false;
  });
}
getList()
onMounted(() => {
  getList()
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="qeuryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="树节点名" prop="treeName">
                        <el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="创建时间">
                        <el-date-picker
                            v-model="daterangeCreateTime"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            type="daterange"
                            range-separator="-"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
                        ></el-date-picker>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['demo:tree:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
                    </el-col>
                    <right-toolbar :columns="columns" v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table
                v-if="refreshTable"
                v-loading="loading"
                :data="treeList"
                row-key="id"
                :default-expand-all="isExpandAll"
                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
                ref="demoTreeTableRef"
            >
                <el-table-column label="父id" prop="parentId" v-if="columns[0].visible" />
                <el-table-column label="部门id" align="center" prop="deptId" v-if="columns[1].visible" />
                <el-table-column label="用户id" align="center" prop="userId" v-if="columns[2].visible" />
                <el-table-column label="树节点名" align="center" prop="treeName" v-if="columns[3].visible" />
                <el-table-column label="创建时间" align="center" prop="createTime" v-if="columns[4].visible" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['demo:tree:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="新增" placement="top">
                            <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['demo:tree:add']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['demo:tree:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹æµ‹è¯•æ ‘è¡¨å¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
            <el-form ref="treeRef" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="父id" prop="parentId">
                    <el-tree-select
                        v-model="form.parentId"
                        :data="treeOptions"
                        :props="{ value: 'id', label: 'treeName', children: 'children' }"
                        value-key="id"
                        check-strictly
                        placeholder="请选择父id"
                    />
                </el-form-item>
                <el-form-item label="部门id" prop="deptId">
                    <el-input v-model="form.deptId" placeholder="请输入部门id" />
                </el-form-item>
                <el-form-item label="用户id" prop="userId">
                    <el-input v-model="form.userId" placeholder="请输入用户id" />
                </el-form-item>
                <el-form-item label="树节点名" prop="treeName">
                    <el-input v-model="form.treeName" placeholder="请输入树节点名" />
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button :loading="buttonLoading" type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/error/401.vue
@@ -1,42 +1,37 @@
<template>
  <div class="errPage-container">
    <el-button icon="arrow-left" class="pan-back-btn" @click="back">
      è¿”回
    </el-button>
    <el-row>
      <el-col :span="12">
        <h1 class="text-jumbo text-ginormous">
          401错误!
        </h1>
        <h2>您没有访问权限!</h2>
        <h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
        <ul class="list-unstyled">
          <li class="link-type">
            <router-link to="/">
              å›žé¦–页
            </router-link>
          </li>
        </ul>
      </el-col>
      <el-col :span="12">
        <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
      </el-col>
    </el-row>
  </div>
    <div class="errPage-container">
        <el-button icon="arrow-left" class="pan-back-btn" @click="back"> è¿”回 </el-button>
        <el-row>
            <el-col :span="12">
                <h1 class="text-jumbo text-ginormous">401错误!</h1>
                <h2>您没有访问权限!</h2>
                <h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
                <ul class="list-unstyled">
                    <li class="link-type">
                        <router-link to="/"> å›žé¦–页 </router-link>
                    </li>
                </ul>
            </el-col>
            <el-col :span="12">
                <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." />
            </el-col>
        </el-row>
    </div>
</template>
<script setup>
import errImage from "@/assets/401_images/401.gif";
<script setup lang="ts">
import errImage from '@/assets/401_images/401.gif';
import { ComponentInternalInstance } from "vue";
let { proxy } = getCurrentInstance();
let { proxy } = getCurrentInstance() as ComponentInternalInstance;
const errGif = ref(errImage + "?" + +new Date());
function back() {
  if (proxy.$route.query.noGoBack) {
  if (proxy?.$route.query.noGoBack) {
    proxy.$router.push({ path: "/" });
  } else {
    proxy.$router.go(-1);
    proxy?.$router.go(-1);
  }
}
</script>
src/views/error/404.vue
@@ -1,31 +1,27 @@
<template>
  <div class="wscn-http404-container">
    <div class="wscn-http404">
      <div class="pic-404">
        <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
        <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
        <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
        <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
      </div>
      <div class="bullshit">
        <div class="bullshit__oops">
          404错误!
        </div>
        <div class="bullshit__headline">
          {{ message }}
        </div>
        <div class="bullshit__info">
          å¯¹ä¸èµ·ï¼Œæ‚¨æ­£åœ¨å¯»æ‰¾çš„页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
        </div>
        <router-link to="/index" class="bullshit__return-home">
          è¿”回首页
        </router-link>
      </div>
    </div>
  </div>
    <div class="wscn-http404-container">
        <div class="wscn-http404">
            <div class="pic-404">
                <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404" />
                <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404" />
                <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404" />
                <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404" />
            </div>
            <div class="bullshit">
                <div class="bullshit__oops">404错误!</div>
                <div class="bullshit__headline">
                    {{ message }}
                </div>
                <div class="bullshit__info">
                    å¯¹ä¸èµ·ï¼Œæ‚¨æ­£åœ¨å¯»æ‰¾çš„页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
                </div>
                <router-link to="/index" class="bullshit__return-home"> è¿”回首页 </router-link>
            </div>
        </div>
    </div>
</template>
<script setup>
<script setup lang="ts">
let message = computed(() => {
  return '找不到网页!'
})
src/views/index.vue
@@ -1,139 +1,103 @@
<template>
  <div class="app-container home">
    <el-row :gutter="20">
      <el-col :sm="24" :lg="12" style="padding-left: 20px">
        <h2>RuoYi-Vue-Plus多租户管理系统</h2>
        <p>
          RuoYi-Vue-Plus æ˜¯åŸºäºŽ RuoYi-Vue é’ˆå¯¹ åˆ†å¸ƒå¼é›†ç¾¤ åœºæ™¯å‡çº§(不兼容原框架)
          <br/>
          * å‰ç«¯å¼€å‘框架 Vue3、TS、Element Plus<br/>
          * åŽç«¯å¼€å‘框架 Spring Boot<br/>
          * å®¹å™¨æ¡†æž¶ Undertow åŸºäºŽ Netty çš„高性能容器<br/>
          * æƒé™è®¤è¯æ¡†æž¶ Sa-Token æ”¯æŒå¤šç»ˆç«¯è®¤è¯ç³»ç»Ÿ<br/>
          * å…³ç³»æ•°æ®åº“ MySQL é€‚配 8.X æœ€ä½Ž 5.7<br/>
          * ç¼“存数据库 Redis é€‚配 6.X æœ€ä½Ž 4.X<br/>
          * æ•°æ®åº“框架 Mybatis-Plus å¿«é€Ÿ CRUD å¢žåŠ å¼€å‘æ•ˆçŽ‡<br/>
          * æ•°æ®åº“框架 p6spy æ›´å¼ºåŠ²çš„ SQL åˆ†æž<br/>
          * å¤šæ•°æ®æºæ¡†æž¶ dynamic-datasource æ”¯æŒä¸»ä»Žä¸Žå¤šç§ç±»æ•°æ®åº“异构<br/>
          * åºåˆ—化框架 Jackson ç»Ÿä¸€ä½¿ç”¨ jackson é«˜æ•ˆå¯é <br/>
          * Redis客户端 Redisson æ€§èƒ½å¼ºåŠ²ã€API丰富<br/>
          * åˆ†å¸ƒå¼é™æµ Redisson å…¨å±€ã€è¯·æ±‚IP、集群ID å¤šç§é™æµ<br/>
          * åˆ†å¸ƒå¼é” Lock4j æ³¨è§£é”ã€å·¥å…·é” å¤šç§å¤šæ ·<br/>
          * åˆ†å¸ƒå¼å¹‚ç­‰ Lock4j åŸºäºŽåˆ†å¸ƒå¼é”å®žçް<br/>
          * åˆ†å¸ƒå¼é“¾è·¯è¿½è¸ª SkyWalking æ”¯æŒé“¾è·¯è¿½è¸ªã€ç½‘格分析、度量聚合、可视化<br/>
          * åˆ†å¸ƒå¼ä»»åŠ¡è°ƒåº¦ Xxl-Job é«˜æ€§èƒ½ é«˜å¯é  æ˜“扩展<br/>
          * æ–‡ä»¶å­˜å‚¨ Minio æœ¬åœ°å­˜å‚¨<br/>
          * æ–‡ä»¶å­˜å‚¨ ä¸ƒç‰›ã€é˜¿é‡Œã€è…¾è®¯    äº‘存储<br/>
          * ç›‘控框架 SpringBoot-Admin å…¨æ–¹ä½æœåŠ¡ç›‘æŽ§<br/>
          * æ ¡éªŒæ¡†æž¶ Validation å¢žå¼ºæŽ¥å£å®‰å…¨æ€§ ä¸¥è°¨æ€§<br/>
          * Excel框架 Alibaba EasyExcel æ€§èƒ½ä¼˜å¼‚ æ‰©å±•性强<br/>
          * æ–‡æ¡£æ¡†æž¶ SpringDoc、javadoc æ— æ³¨è§£é›¶å…¥ä¾µåŸºäºŽjava注释<br/>
          * å·¥å…·ç±»æ¡†æž¶ Hutool、Lombok å‡å°‘代码冗余 å¢žåŠ å®‰å…¨æ€§<br/>
          * ä»£ç ç”Ÿæˆå™¨ é€‚配MP、SpringDoc规范化代码 ä¸€é”®ç”Ÿæˆå‰åŽç«¯ä»£ç <br/>
          * éƒ¨ç½²æ–¹å¼ Docker å®¹å™¨ç¼–排 ä¸€é”®éƒ¨ç½²ä¸šåŠ¡é›†ç¾¤<br/>
          * å›½é™…化 SpringMessage Spring标准国际化方案<br/>
        </p>
        <p>
          <b>当前版本:</b> <span>v5.0.0</span>
        </p>
        <p>
          <el-tag type="danger">&yen;免费开源</el-tag>
        </p>
        <p>
          <el-button
            type="primary"
            icon="Cloudy"
            plain
            @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')"
            >访问码云</el-button
          >
          <el-button
            type="primary"
            icon="Cloudy"
            plain
            @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')"
            >访问GitHub</el-button
          >
          <el-button
            type="primary"
            icon="Cloudy"
            plain
            @click="goTarget('https://javalionli.gitee.io/plus-doc/#/ruoyi-vue-plus/changlog')"
          >更新日志</el-button
          >
        </p>
      </el-col>
    <div class="app-container home">
        <el-row :gutter="20">
            <el-col :sm="24" :lg="12" style="padding-left: 20px">
                <h2>RuoYi-Vue-Plus多租户管理系统</h2>
                <p>
                    RuoYi-Vue-Plus æ˜¯åŸºäºŽ RuoYi-Vue é’ˆå¯¹ åˆ†å¸ƒå¼é›†ç¾¤ åœºæ™¯å‡çº§(不兼容原框架)
                    <br />
                    * å‰ç«¯å¼€å‘框架 Vue3、TS、Element Plus<br />
                    * åŽç«¯å¼€å‘框架 Spring Boot<br />
                    * å®¹å™¨æ¡†æž¶ Undertow åŸºäºŽ Netty çš„高性能容器<br />
                    * æƒé™è®¤è¯æ¡†æž¶ Sa-Token æ”¯æŒå¤šç»ˆç«¯è®¤è¯ç³»ç»Ÿ<br />
                    * å…³ç³»æ•°æ®åº“ MySQL é€‚配 8.X æœ€ä½Ž 5.7<br />
                    * ç¼“存数据库 Redis é€‚配 6.X æœ€ä½Ž 4.X<br />
                    * æ•°æ®åº“框架 Mybatis-Plus å¿«é€Ÿ CRUD å¢žåŠ å¼€å‘æ•ˆçŽ‡<br />
                    * æ•°æ®åº“框架 p6spy æ›´å¼ºåŠ²çš„ SQL åˆ†æž<br />
                    * å¤šæ•°æ®æºæ¡†æž¶ dynamic-datasource æ”¯æŒä¸»ä»Žä¸Žå¤šç§ç±»æ•°æ®åº“异构<br />
                    * åºåˆ—化框架 Jackson ç»Ÿä¸€ä½¿ç”¨ jackson é«˜æ•ˆå¯é <br />
                    * Redis客户端 Redisson æ€§èƒ½å¼ºåŠ²ã€API丰富<br />
                    * åˆ†å¸ƒå¼é™æµ Redisson å…¨å±€ã€è¯·æ±‚IP、集群ID å¤šç§é™æµ<br />
                    * åˆ†å¸ƒå¼é” Lock4j æ³¨è§£é”ã€å·¥å…·é” å¤šç§å¤šæ ·<br />
                    * åˆ†å¸ƒå¼å¹‚ç­‰ Lock4j åŸºäºŽåˆ†å¸ƒå¼é”å®žçް<br />
                    * åˆ†å¸ƒå¼é“¾è·¯è¿½è¸ª SkyWalking æ”¯æŒé“¾è·¯è¿½è¸ªã€ç½‘格分析、度量聚合、可视化<br />
                    * åˆ†å¸ƒå¼ä»»åŠ¡è°ƒåº¦ Xxl-Job é«˜æ€§èƒ½ é«˜å¯é  æ˜“扩展<br />
                    * æ–‡ä»¶å­˜å‚¨ Minio æœ¬åœ°å­˜å‚¨<br />
                    * æ–‡ä»¶å­˜å‚¨ ä¸ƒç‰›ã€é˜¿é‡Œã€è…¾è®¯ äº‘存储<br />
                    * ç›‘控框架 SpringBoot-Admin å…¨æ–¹ä½æœåŠ¡ç›‘æŽ§<br />
                    * æ ¡éªŒæ¡†æž¶ Validation å¢žå¼ºæŽ¥å£å®‰å…¨æ€§ ä¸¥è°¨æ€§<br />
                    * Excel框架 Alibaba EasyExcel æ€§èƒ½ä¼˜å¼‚ æ‰©å±•性强<br />
                    * æ–‡æ¡£æ¡†æž¶ SpringDoc、javadoc æ— æ³¨è§£é›¶å…¥ä¾µåŸºäºŽjava注释<br />
                    * å·¥å…·ç±»æ¡†æž¶ Hutool、Lombok å‡å°‘代码冗余 å¢žåŠ å®‰å…¨æ€§<br />
                    * ä»£ç ç”Ÿæˆå™¨ é€‚配MP、SpringDoc规范化代码 ä¸€é”®ç”Ÿæˆå‰åŽç«¯ä»£ç <br />
                    * éƒ¨ç½²æ–¹å¼ Docker å®¹å™¨ç¼–排 ä¸€é”®éƒ¨ç½²ä¸šåŠ¡é›†ç¾¤<br />
                    * å›½é™…化 SpringMessage Spring标准国际化方案<br />
                </p>
                <p><b>当前版本:</b> <span>v5.0.0</span></p>
                <p>
                    <el-tag type="danger">&yen;免费开源</el-tag>
                </p>
                <p>
                    <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
                    <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
                    <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://javalionli.gitee.io/plus-doc/#/ruoyi-vue-plus/changlog')"
                        >更新日志</el-button
                    >
                </p>
            </el-col>
      <el-col :sm="24" :lg="12" style="padding-left: 20px">
        <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
        <p>
          RuoYi-Cloud-Plus å¾®æœåŠ¡é€šç”¨æƒé™ç®¡ç†ç³»ç»Ÿ é‡å†™ RuoYi-Cloud å…¨æ–¹ä½å‡çº§(不兼容原框架)
          <br/>
          * å‰ç«¯å¼€å‘框架 Vue3、TS、Element UI<br/>
          * åŽç«¯å¼€å‘框架 Spring Boot<br/>
          * å¾®æœåŠ¡å¼€å‘æ¡†æž¶ Spring Cloud、Spring Cloud Alibaba<br/>
          * å®¹å™¨æ¡†æž¶ Undertow åŸºäºŽ XNIO çš„高性能容器<br/>
          * æƒé™è®¤è¯æ¡†æž¶ Sa-Token、Jwt æ”¯æŒå¤šç»ˆç«¯è®¤è¯ç³»ç»Ÿ<br/>
          * å…³ç³»æ•°æ®åº“ MySQL é€‚配 8.X æœ€ä½Ž 5.7<br/>
          * å…³ç³»æ•°æ®åº“ Oracle é€‚配 11g 12c<br/>
          * å…³ç³»æ•°æ®åº“ PostgreSQL é€‚配 13 14<br/>
          * å…³ç³»æ•°æ®åº“ SQLServer é€‚配 2017 2019<br/>
          * ç¼“存数据库 Redis é€‚配 6.X æœ€ä½Ž 5.X<br/>
          * åˆ†å¸ƒå¼æ³¨å†Œä¸­å¿ƒ Alibaba Nacos é‡‡ç”¨2.X åŸºäºŽGRPC通信高性能<br/>
          * åˆ†å¸ƒå¼é…ç½®ä¸­å¿ƒ Alibaba Nacos é‡‡ç”¨2.X åŸºäºŽGRPC通信高性能<br/>
          * æœåŠ¡ç½‘å…³ Spring Cloud Gateway å“åº”式高性能网关<br/>
          * è´Ÿè½½å‡è¡¡ Spring Cloud Loadbalancer è´Ÿè½½å‡è¡¡å¤„理<br/>
          * RPC远程调用 Apache Dubbo åŽŸç”Ÿæ€ä½¿ç”¨ä½“éªŒã€é«˜æ€§èƒ½<br/>
          * åˆ†å¸ƒå¼é™æµç†”æ–­ Alibaba Sentinel æ— ä¾µå…¥ã€é«˜æ‰©å±•<br/>
          * åˆ†å¸ƒå¼äº‹åŠ¡ Alibaba Seata æ— ä¾µå…¥ã€é«˜æ‰©å±• æ”¯æŒ å››ç§æ¨¡å¼<br/>
          * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— Spring Cloud Stream é—¨é¢æ¡†æž¶å…¼å®¹å„种MQ集成<br/>
          * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— Apache Kafka é«˜æ€§èƒ½é«˜é€Ÿåº¦<br/>
          * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— Apache RocketMQ é«˜å¯ç”¨åŠŸèƒ½å¤šæ ·<br/>
          * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— RabbitMQ æ”¯æŒå„种扩展插件功能多样性<br/>
          * åˆ†å¸ƒå¼æœç´¢å¼•擎 ElasticSearch ä¸šç•ŒçŸ¥å<br/>
          * åˆ†å¸ƒå¼é“¾è·¯è¿½è¸ª Apache SkyWalking é“¾è·¯è¿½è¸ªã€ç½‘格分析、度量聚合、可视化<br/>
          * åˆ†å¸ƒå¼æ—¥å¿—中心 ELK ä¸šç•Œæˆç†Ÿè§£å†³æ–¹æ¡ˆ<br/>
          * åˆ†å¸ƒå¼ç›‘控 Prometheus、Grafana å…¨æ–¹ä½æ€§èƒ½ç›‘控<br/>
          * å…¶ä½™ä¸Ž Vue ç‰ˆæœ¬ä¸€è‡´<br/>
        </p>
        <p>
          <b>当前版本:</b> <span>v2.0.0</span>
        </p>
        <p>
          <el-tag type="danger">&yen;免费开源</el-tag>
        </p>
        <p>
          <el-button
              type="primary"
              icon="Cloudy"
              plain
              @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')"
          >访问码云</el-button
          >
          <el-button
              type="primary"
              icon="Cloudy"
              plain
              @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')"
          >访问GitHub</el-button
          >
          <el-button
              type="primary"
              icon="Cloudy"
              plain
              @click="goTarget('https://javalionli.gitee.io/plus-doc/#/ruoyi-cloud-plus/changlog')"
          >更新日志</el-button
          >
        </p>
      </el-col>
    </el-row>
    <el-divider />
  </div>
            <el-col :sm="24" :lg="12" style="padding-left: 20px">
                <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
                <p>
                    RuoYi-Cloud-Plus å¾®æœåŠ¡é€šç”¨æƒé™ç®¡ç†ç³»ç»Ÿ é‡å†™ RuoYi-Cloud å…¨æ–¹ä½å‡çº§(不兼容原框架)
                    <br />
                    * å‰ç«¯å¼€å‘框架 Vue3、TS、Element UI<br />
                    * åŽç«¯å¼€å‘框架 Spring Boot<br />
                    * å¾®æœåŠ¡å¼€å‘æ¡†æž¶ Spring Cloud、Spring Cloud Alibaba<br />
                    * å®¹å™¨æ¡†æž¶ Undertow åŸºäºŽ XNIO çš„高性能容器<br />
                    * æƒé™è®¤è¯æ¡†æž¶ Sa-Token、Jwt æ”¯æŒå¤šç»ˆç«¯è®¤è¯ç³»ç»Ÿ<br />
                    * å…³ç³»æ•°æ®åº“ MySQL é€‚配 8.X æœ€ä½Ž 5.7<br />
                    * å…³ç³»æ•°æ®åº“ Oracle é€‚配 11g 12c<br />
                    * å…³ç³»æ•°æ®åº“ PostgreSQL é€‚配 13 14<br />
                    * å…³ç³»æ•°æ®åº“ SQLServer é€‚配 2017 2019<br />
                    * ç¼“存数据库 Redis é€‚配 6.X æœ€ä½Ž 5.X<br />
                    * åˆ†å¸ƒå¼æ³¨å†Œä¸­å¿ƒ Alibaba Nacos é‡‡ç”¨2.X åŸºäºŽGRPC通信高性能<br />
                    * åˆ†å¸ƒå¼é…ç½®ä¸­å¿ƒ Alibaba Nacos é‡‡ç”¨2.X åŸºäºŽGRPC通信高性能<br />
                    * æœåŠ¡ç½‘å…³ Spring Cloud Gateway å“åº”式高性能网关<br />
                    * è´Ÿè½½å‡è¡¡ Spring Cloud Loadbalancer è´Ÿè½½å‡è¡¡å¤„理<br />
                    * RPC远程调用 Apache Dubbo åŽŸç”Ÿæ€ä½¿ç”¨ä½“éªŒã€é«˜æ€§èƒ½<br />
                    * åˆ†å¸ƒå¼é™æµç†”æ–­ Alibaba Sentinel æ— ä¾µå…¥ã€é«˜æ‰©å±•<br />
                    * åˆ†å¸ƒå¼äº‹åŠ¡ Alibaba Seata æ— ä¾µå…¥ã€é«˜æ‰©å±• æ”¯æŒ å››ç§æ¨¡å¼<br />
                    * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— Spring Cloud Stream é—¨é¢æ¡†æž¶å…¼å®¹å„种MQ集成<br />
                    * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— Apache Kafka é«˜æ€§èƒ½é«˜é€Ÿåº¦<br />
                    * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— Apache RocketMQ é«˜å¯ç”¨åŠŸèƒ½å¤šæ ·<br />
                    * åˆ†å¸ƒå¼æ¶ˆæ¯é˜Ÿåˆ— RabbitMQ æ”¯æŒå„种扩展插件功能多样性<br />
                    * åˆ†å¸ƒå¼æœç´¢å¼•擎 ElasticSearch ä¸šç•ŒçŸ¥å<br />
                    * åˆ†å¸ƒå¼é“¾è·¯è¿½è¸ª Apache SkyWalking é“¾è·¯è¿½è¸ªã€ç½‘格分析、度量聚合、可视化<br />
                    * åˆ†å¸ƒå¼æ—¥å¿—中心 ELK ä¸šç•Œæˆç†Ÿè§£å†³æ–¹æ¡ˆ<br />
                    * åˆ†å¸ƒå¼ç›‘控 Prometheus、Grafana å…¨æ–¹ä½æ€§èƒ½ç›‘控<br />
                    * å…¶ä½™ä¸Ž Vue ç‰ˆæœ¬ä¸€è‡´<br />
                </p>
                <p><b>当前版本:</b> <span>v2.0.0</span></p>
                <p>
                    <el-tag type="danger">&yen;免费开源</el-tag>
                </p>
                <p>
                    <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
                    <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
                    <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://javalionli.gitee.io/plus-doc/#/ruoyi-cloud-plus/changlog')"
                        >更新日志</el-button
                    >
                </p>
            </el-col>
        </el-row>
        <el-divider />
    </div>
</template>
<script setup name="Index">
<script setup name="Index" lang="ts">
function goTarget(url) {
function goTarget(url:string) {
  window.open(url, '__blank')
}
</script>
@@ -201,4 +165,3 @@
  }
}
</style>
src/views/login.vue
@@ -1,185 +1,176 @@
<template>
  <div class="login">
    <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
      <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
      <el-form-item prop="tenantId">
        <el-select v-model="loginForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
          <el-option
              v-for="item in tenantList"
              :key="item.tenantId"
              :label="item.companyName"
              :value="item.tenantId">
          </el-option>
          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
        </el-select>
      </el-form-item>
      <el-form-item prop="username">
        <el-input
          v-model="loginForm.username"
          type="text"
          size="large"
          auto-complete="off"
          placeholder="账号"
        >
          <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model="loginForm.password"
          type="password"
          size="large"
          auto-complete="off"
          placeholder="密码"
          @keyup.enter="handleLogin"
        >
          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="captchaEnabled">
        <el-input
          v-model="loginForm.code"
          size="large"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
          @keyup.enter="handleLogin"
        >
          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
        </el-input>
        <div class="login-code">
          <img :src="codeUrl" @click="getCode" class="login-code-img"/>
        </div>
      </el-form-item>
      <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="large"
          type="primary"
          style="width:100%;"
          @click.prevent="handleLogin"
        >
          <span v-if="!loading">登 å½•</span>
          <span v-else>登 å½• ä¸­...</span>
        </el-button>
        <div style="float: right;" v-if="register">
          <router-link class="link-type" :to="'/register'">立即注册</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  åº•部  -->
    <div class="el-login-footer">
      <span>Copyright Â© 2018-2023 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>
<script setup lang="ts">
import { getCodeImg, getTenantList } from '@/api/login';
import Cookies from 'js-cookie';
import { encrypt, decrypt } from '@/utils/jsencrypt';
import { useUserStore } from '@/store/modules/user';
import { LoginData, TenantVO } from '@/api/types';
import { FormRules } from 'element-plus';
import { to } from 'await-to-js';
<script setup>
import { getCodeImg, getTenantList } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const userStore = useUserStore();
const router = useRouter();
const { proxy } = getCurrentInstance();
const loginForm = ref({
const loginForm = ref<LoginData>({
  tenantId: "000000",
  username: "admin",
  password: "admin123",
  username: 'admin',
  password: 'admin123',
  rememberMe: false,
  code: "",
  uuid: ""
  code: '',
  uuid: ''
});
const loginRules = {
const loginRules: FormRules = {
  tenantId: [{ required: true, trigger: "blur", message: "请输入您的租户编号" }],
  username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
  password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
  username: [{ required: true, trigger: 'blur', message: '请输入您的账号' }],
  password: [{ required: true, trigger: 'blur', message: '请输入您的密码' }],
  code: [{ required: true, trigger: 'change', message: '请输入验证码' }]
};
const codeUrl = ref("");
const codeUrl = ref('');
const loading = ref(false);
// éªŒè¯ç å¼€å…³
const captchaEnabled = ref(true);
// ç§Ÿæˆ·å¼€å…³
const tenantEnabled = ref(true);
// æ³¨å†Œå¼€å…³
const register = ref(false);
const redirect = ref(undefined);
const loginRef = ref(ElForm);
// ç§Ÿæˆ·åˆ—表
const tenantList = ref([]);
const tenantList = ref<TenantVO[]>([]);
function handleLogin() {
  proxy.$refs.loginRef.validate(valid => {
const handleLogin = () => {
  loginRef.value.validate(async (valid:boolean, fields: any) => {
    if (valid) {
      loading.value = true;
      // å‹¾é€‰äº†éœ€è¦è®°ä½å¯†ç è®¾ç½®åœ¨ cookie ä¸­è®¾ç½®è®°ä½ç”¨æˆ·åå’Œå¯†ç 
      if (loginForm.value.rememberMe) {
        Cookies.set("tenantId", loginForm.value.tenantId, { expires: 30 });
        Cookies.set("username", loginForm.value.username, { expires: 30 });
        Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
        Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
        Cookies.set('username', loginForm.value.username, { expires: 30 });
        Cookies.set('password', String(encrypt(loginForm.value.password)), { expires: 30 });
        Cookies.set('rememberMe', String(loginForm.value.rememberMe), { expires: 30 });
      } else {
        // å¦åˆ™ç§»é™¤
        Cookies.remove("tenantId");
        Cookies.remove("username");
        Cookies.remove("password");
        Cookies.remove("rememberMe");
        Cookies.remove('username');
        Cookies.remove('password');
        Cookies.remove('rememberMe');
      }
      // è°ƒç”¨action的登录方法
      userStore.login(loginForm.value).then(() => {
        router.push({ path: redirect.value || "/" });
      }).catch(() => {
      // prittier-ignore
      const [err] = await to(userStore.login(loginForm.value));
      if (!err) {
        await router.push({ path: redirect.value || '/' });
      } else {
        loading.value = false;
        // é‡æ–°èŽ·å–éªŒè¯ç 
        if (captchaEnabled.value) {
          getCode();
          await getCode();
        }
      });
      }
    } else {
      console.log('error submit!', fields);
    }
  });
}
};
function getCode() {
  getCodeImg().then(res => {
    captchaEnabled.value = res.data.captchaEnabled === undefined ? true : res.data.captchaEnabled;
    if (captchaEnabled.value) {
      codeUrl.value = "data:image/gif;base64," + res.data.img;
      loginForm.value.uuid = res.data.uuid;
    }
  });
}
/**
 * èŽ·å–éªŒè¯ç 
 */
const getCode = async () => {
  const res = await getCodeImg();
  const { data } = res;
  captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
  if (captchaEnabled.value) {
    codeUrl.value = 'data:image/gif;base64,' + data.img;
    loginForm.value.uuid = data.uuid;
  }
};
function initTenantList() {
  getTenantList().then(res => {
    tenantList.value = res.data;
    if (tenantList.value != null && tenantList.value.length !== 0) {
      loginForm.value.tenantId = tenantList.value[0].tenantId;
    }
  });
}
function getCookie() {
const getCookie = () => {
  const tenantId = Cookies.get("tenantId");
  const username = Cookies.get("username");
  const password = Cookies.get("password");
  const rememberMe = Cookies.get("rememberMe");
  const username = Cookies.get('username');
  const password = Cookies.get('password');
  const rememberMe = Cookies.get('rememberMe');
  loginForm.value = {
    tenantId: tenantId === undefined ? loginForm.value.tenantId : tenantId,
    username: username === undefined ? loginForm.value.username : username,
    password: password === undefined ? loginForm.value.password : decrypt(password),
    password: password === undefined ? loginForm.value.password : (decrypt(password) as string),
    rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
  };
}
getCode();
initTenantList();
getCookie();
/**
 * èŽ·å–ç§Ÿæˆ·åˆ—è¡¨
 */
const initTenantList = async () => {
  const { data } = await getTenantList();
  tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
  if (tenantEnabled.value) {
    tenantList.value = data.voList;
    if (tenantList.value != null && tenantList.value.length !== 0) {
      loginForm.value.tenantId = tenantList.value[0].tenantId;
    }
  }
}
onMounted(() => {
  getCode();
  initTenantList();
  getCookie();
});
</script>
<style lang='scss' scoped>
<template>
    <div class="login">
        <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
            <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
            <el-form-item prop="tenantId" v-if="tenantEnabled">
                <el-select v-model="loginForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
                    <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
                    <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
                </el-select>
            </el-form-item>
            <el-form-item prop="username">
                <el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
                    <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
                </el-input>
            </el-form-item>
            <el-form-item prop="password">
                <el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin">
                    <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
                </el-input>
            </el-form-item>
            <el-form-item prop="code" v-if="captchaEnabled">
                <el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleLogin">
                    <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
                </el-input>
                <div class="login-code">
                    <img :src="codeUrl" @click="getCode" class="login-code-img" />
                </div>
            </el-form-item>
            <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
            <el-form-item style="width:100%;">
                <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
                    <span v-if="!loading">登 å½•</span>
                    <span v-else>登 å½• ä¸­...</span>
                </el-button>
                <div style="float: right;" v-if="register">
                    <router-link class="link-type" :to="'/register'">立即注册</router-link>
                </div>
            </el-form-item>
        </el-form>
        <!--  åº•部  -->
        <div class="el-login-footer">
            <span>Copyright Â© 2018-2023 ruoyi.vip All Rights Reserved.</span>
        </div>
    </div>
</template>
<style lang="scss" scoped>
.login {
  display: flex;
  justify-content: center;
@@ -233,7 +224,7 @@
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-family: Arial,serif;
  font-size: 12px;
  letter-spacing: 1px;
}
src/views/monitor/admin/index.vue
@@ -1,13 +1,9 @@
<template>
  <div>
    <i-frame v-model:src="url"></i-frame>
  </div>
    <div>
        <i-frame v-model:src="url"></i-frame>
    </div>
</template>
<script setup>
import iFrame from '@/components/iFrame'
import { ref } from 'vue';
<script setup lang="ts">
const url = ref(import.meta.env.VITE_APP_MONITRO_ADMIN);
</script>
src/views/monitor/cache/index.vue
@@ -1,129 +1,188 @@
<template>
  <div class="app-container">
    <el-row>
      <el-col :span="24" class="card-box">
        <el-card>
          <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">基本信息</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <table cellspacing="0" style="width: 100%">
              <tbody>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">Redis版本</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">运行模式</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">端口</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">客户端数</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">运行时间(天)</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">使用内存</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">使用CPU</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">内存配置</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
                </tr>
                <tr>
                  <td class="el-table__cell is-leaf"><div class="cell">AOF是否开启</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "否" : "是" }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">RDB是否成功</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">Key数量</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
                  <td class="el-table__cell is-leaf"><div class="cell">网络入口/出口</div></td>
                  <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
                </tr>
              </tbody>
            </table>
          </div>
        </el-card>
      </el-col>
      <el-col :span="12" class="card-box">
        <el-card>
          <template #header><PieChart style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">命令统计</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <div ref="commandstats" style="height: 420px" />
          </div>
        </el-card>
      </el-col>
      <el-col :span="12" class="card-box">
        <el-card>
          <template #header><Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存信息</span></template>
          <div class="el-table el-table--enable-row-hover el-table--medium">
            <div ref="usedmemory" style="height: 420px" />
          </div>
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>
<script setup name="Cache">
<script setup name="Cache" lang="ts">
import { getCache } from '@/api/monitor/cache';
import * as echarts from 'echarts';
import { ComponentInternalInstance } from "vue";
const cache = ref([]);
const commandstats = ref(null);
const usedmemory = ref(null);
const { proxy } = getCurrentInstance();
const cache = ref<any>({});
const commandstats = ref();
const usedmemory = ref();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
function getList() {
  proxy.$modal.loading("正在加载缓存监控数据,请稍候!");
  getCache().then(response => {
    proxy.$modal.closeLoading();
    cache.value = response.data;
const getList = async () => {
    proxy?.$modal.loading("正在加载缓存监控数据,请稍候!");
    const res = await getCache();
    proxy?.$modal.closeLoading();
    cache.value = res.data;
    const commandstatsIntance = echarts.init(commandstats.value, "macarons");
    commandstatsIntance.setOption({
        tooltip: {
            trigger: "item",
            formatter: "{a} <br/>{b} : {c} ({d}%)"
        },
        series: [
            {
                name: "命令",
                type: "pie",
                roseType: "radius",
                radius: [15, 95],
                center: ["50%", "38%"],
                data: res.data.commandStats,
                animationEasing: "cubicInOut",
                animationDuration: 1000
            }
        ]
    });
    const commandstatsIntance = echarts.init(commandstats.value, "macarons");
    commandstatsIntance.setOption({
      tooltip: {
        trigger: "item",
        formatter: "{a} <br/>{b} : {c} ({d}%)"
      },
      series: [
        {
          name: "命令",
          type: "pie",
          roseType: "radius",
          radius: [15, 95],
          center: ["50%", "38%"],
          data: response.data.commandStats,
          animationEasing: "cubicInOut",
          animationDuration: 1000
        }
      ]
    });
    const usedmemoryInstance = echarts.init(usedmemory.value, "macarons");
    usedmemoryInstance.setOption({
      tooltip: {
        formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
      },
      series: [
        {
          name: "峰值",
          type: "gauge",
          min: 0,
          max: 1000,
          detail: {
            formatter: cache.value.info.used_memory_human
          },
          data: [
            {
              value: parseFloat(cache.value.info.used_memory_human),
              name: "内存消耗"
            }
          ]
        }
      ]
    })
  })
    const usedmemoryInstance = echarts.init(usedmemory.value, "macarons");
    usedmemoryInstance.setOption({
        tooltip: {
            formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
        },
        series: [
            {
                name: "峰值",
                type: "gauge",
                min: 0,
                max: 1000,
                detail: {
                    formatter: cache.value.info.used_memory_human
                },
                data: [
                    {
                        value: parseFloat(cache.value.info.used_memory_human),
                        name: "内存消耗"
                    }
                ]
            }
        ]
    })
}
getList();
onMounted(() => {
    getList();
})
</script>
<template>
    <div class="p-2">
        <el-row>
            <el-col :span="24" class="card-box">
                <el-card>
                    <template #header>
                        <Monitor style="width: 1em; height: 1em; vertical-align: middle;" />
                        <span style="vertical-align: middle;">基本信息</span>
                    </template>
                    <div class="el-table el-table--enable-row-hover el-table--medium">
                        <table style="width: 100%">
                            <tbody>
                                <tr>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">Redis版本</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">运行模式</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.redis_mode === "standalone" ? "单机" : "集群" }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">端口</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">客户端数</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div>
                                    </td>
                                </tr>
                                <tr>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">运行时间(天)</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">使用内存</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">使用CPU</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">内存配置</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div>
                                    </td>
                                </tr>
                                <tr>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">AOF是否开启</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.aof_enabled === "0" ? "否" : "是" }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">RDB是否成功</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">Key数量</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.dbSize">{{ cache.dbSize }}</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell">网络入口/出口</div>
                                    </td>
                                    <td class="el-table__cell is-leaf">
                                        <div class="cell" v-if="cache.info">
                                            {{ cache.info.instantaneous_input_kbps }}kps/{{ cache.info.instantaneous_output_kbps }}kps
                                        </div>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                    </div>
                </el-card>
            </el-col>
            <el-col :span="12" class="card-box">
                <el-card>
                    <template #header>
                        <PieChart style="width: 1em; height: 1em; vertical-align: middle;" />
                        <span style="vertical-align: middle;">命令统计</span>
                    </template>
                    <div class="el-table el-table--enable-row-hover el-table--medium">
                        <div ref="commandstats" style="height: 420px" />
                    </div>
                </el-card>
            </el-col>
            <el-col :span="12" class="card-box">
                <el-card>
                    <template #header>
                        <Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">内存信息</span>
                    </template>
                    <div class="el-table el-table--enable-row-hover el-table--medium">
                        <div ref="usedmemory" style="height: 420px" />
                    </div>
                </el-card>
            </el-col>
        </el-row>
    </div>
</template>
src/views/monitor/logininfor/index.vue
@@ -1,225 +1,197 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
         <el-form-item label="登录地址" prop="ipaddr">
            <el-input
               v-model="queryParams.ipaddr"
               placeholder="请输入登录地址"
               clearable
               style="width: 240px;"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="用户名称" prop="userName">
            <el-input
               v-model="queryParams.userName"
               placeholder="请输入用户名称"
               clearable
               style="width: 240px;"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select
               v-model="queryParams.status"
               placeholder="登录状态"
               clearable
               style="width: 240px"
            >
               <el-option
                  v-for="dict in sys_common_status"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item label="登录时间" style="width: 308px">
            <el-date-picker
               v-model="dateRange"
               value-format="YYYY-MM-DD HH:mm:ss"
               type="daterange"
               range-separator="-"
               start-placeholder="开始日期"
               end-placeholder="结束日期"
               :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
            ></el-date-picker>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
<script setup name="Logininfor" lang="ts">
import { list, delLoginInfo, cleanLoginInfo, unlockLoginInfo } from "@/api/monitor/loginInfo";
import { ComponentInternalInstance } from "vue";
import { LoginInfoQuery, LoginInfoVO } from "@/api/monitor/loginInfo/types";
import { DateModelType } from 'element-plus';
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               :disabled="multiple"
               @click="handleDelete"
               v-hasPermi="['monitor:logininfor:remove']"
            >删除</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               @click="handleClean"
               v-hasPermi="['monitor:logininfor:remove']"
            >清空</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Unlock"
               :disabled="single"
               @click="handleUnlock"
               v-hasPermi="['monitor:logininfor:unlock']"
            >解锁</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="warning"
               plain
               icon="Download"
               @click="handleExport"
               v-hasPermi="['monitor:logininfor:export']"
            >导出</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_common_status } = toRefs<any>(proxy?.useDict("sys_common_status"));
      <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="访问编号" align="center" prop="infoId" />
         <el-table-column label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
         <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
         <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
         <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
         <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
         <el-table-column label="登录状态" align="center" prop="status">
            <template #default="scope">
               <dict-tag :options="sys_common_status" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="描述" align="center" prop="msg" :show-overflow-tooltip="true" />
         <el-table-column label="访问时间" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
            <template #default="scope">
               <span>{{ parseTime(scope.row.loginTime) }}</span>
            </template>
         </el-table-column>
      </el-table>
      <pagination
         v-show="total > 0"
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
      />
   </div>
</template>
<script setup name="Logininfor">
import { list, delLogininfor, cleanLogininfor, unlockLogininfor } from "@/api/monitor/logininfor";
const { proxy } = getCurrentInstance();
const { sys_common_status } = proxy.useDict("sys_common_status");
const logininforList = ref([]);
const loginInfoList = ref<LoginInfoVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const selectName = ref("");
const selectName = ref<Array<string>>([]);
const total = ref(0);
const dateRange = ref([]);
const defaultSort = ref({ prop: "loginTime", order: "descending" });
const dateRange = ref<[DateModelType,DateModelType]>(['', '']);
const defaultSort = ref<any>({ prop: "loginTime", order: "descending" });
const queryFormRef = ref(ElForm);
const loginInfoTableRef = ref(ElTable);
// æŸ¥è¯¢å‚æ•°
const queryParams = ref({
const queryParams = ref<LoginInfoQuery>({
  pageNum: 1,
  pageSize: 10,
  ipaddr: undefined,
  userName: undefined,
  status: undefined,
  orderByColumn: undefined,
  isAsc: undefined
  ipaddr: '',
  userName: '',
  status: '',
  orderByColumn: defaultSort.value.prop,
  isAsc: defaultSort.value.order
});
/** æŸ¥è¯¢ç™»å½•日志列表 */
function getList() {
const getList = async () => {
  loading.value = true;
  list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
    logininforList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
    const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
    loginInfoList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  dateRange.value = [];
  proxy.resetForm("queryRef");
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value.resetFields();
  queryParams.value.pageNum = 1;
  proxy.$refs["logininforRef"].sort(defaultSort.value.prop, defaultSort.value.order);
  loginInfoTableRef.value.sort(defaultSort.value.prop, defaultSort.value.order);
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
const handleSelectionChange = (selection: LoginInfoVO[]) => {
  ids.value = selection.map(item => item.infoId);
  multiple.value = !selection.length;
  single.value = selection.length != 1;
  selectName.value = selection.map(item => item.userName);
}
/** æŽ’序触发事件 */
function handleSortChange(column, prop, order) {
const handleSortChange = (column: any) => {
  queryParams.value.orderByColumn = column.prop;
  queryParams.value.isAsc = column.order;
  getList();
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const infoIds = row.infoId || ids.value;
  proxy.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?').then(function () {
    return delLogininfor(infoIds);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const handleDelete = async (row?: LoginInfoVO) => {
  const infoIds = row?.infoId || ids.value;
    await proxy?.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?');
    await delLoginInfo(infoIds);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
/** æ¸…空按钮操作 */
function handleClean() {
  proxy.$modal.confirm("是否确认清空所有登录日志数据项?").then(function () {
    return cleanLogininfor();
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("清空成功");
  }).catch(() => {});
const handleClean = async () => {
    await proxy?.$modal.confirm("是否确认清空所有登录日志数据项?");
    await cleanLoginInfo();
    getList();
    proxy?.$modal.msgSuccess("清空成功");
}
/** è§£é”æŒ‰é’®æ“ä½œ */
function handleUnlock() {
const handleUnlock = async () => {
  const username = selectName.value;
  proxy.$modal.confirm('是否确认解锁用户"' + username + '"数据项?').then(function () {
    return unlockLogininfor(username);
  }).then(() => {
    proxy.$modal.msgSuccess("用户" + username + "解锁成功");
  }).catch(() => {});
    await proxy?.$modal.confirm('是否确认解锁用户"' + username + '"数据项?');
    await unlockLoginInfo(username);
    proxy?.$modal.msgSuccess("用户" + username + "解锁成功");
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
  proxy.download("monitor/logininfor/export", {
const handleExport = () => {
  proxy?.download("monitor/logininfor/export", {
    ...queryParams.value,
  }, `config_${new Date().getTime()}.xlsx`);
}
getList();
onMounted(() => {
  getList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="登录地址" prop="ipaddr">
                        <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable style="width: 240px;" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="用户名称" prop="userName">
                        <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px;" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="状态" prop="status">
                        <el-select v-model="queryParams.status" placeholder="登录状态" clearable style="width: 240px">
                            <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="登录时间" style="width: 308px">
                        <el-date-picker
                            v-model="dateRange"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            type="daterange"
                            range-separator="-"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
                        ></el-date-picker>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['monitor:logininfor:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" @click="handleClean" v-hasPermi="['monitor:logininfor:remove']">清空</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock" v-hasPermi="['monitor:logininfor:unlock']">
                            è§£é”
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['monitor:logininfor:export']">导出</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table
                ref="loginInfoTableRef"
                v-loading="loading"
                :data="loginInfoList"
                @selection-change="handleSelectionChange"
                :default-sort="defaultSort"
                @sort-change="handleSortChange"
            >
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="访问编号" align="center" prop="infoId" />
                <el-table-column
                    label="用户名称"
                    align="center"
                    prop="userName"
                    :show-overflow-tooltip="true"
                    sortable="custom"
                    :sort-orders="['descending', 'ascending']"
                />
                <el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
                <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
                <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
                <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
                <el-table-column label="登录状态" align="center" prop="status">
                    <template #default="scope">
                        <dict-tag :options="sys_common_status" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="描述" align="center" prop="msg" :show-overflow-tooltip="true" />
                <el-table-column label="访问时间" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.loginTime) }}</span>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
    </div>
</template>
src/views/monitor/online/index.vue
@@ -1,106 +1,104 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true">
         <el-form-item label="登录地址" prop="ipaddr">
            <el-input
               v-model="queryParams.ipaddr"
               placeholder="请输入登录地址"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="用户名称" prop="userName">
            <el-input
               v-model="queryParams.userName"
               placeholder="请输入用户名称"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
      <el-table
         v-loading="loading"
         :data="onlineList.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
         style="width: 100%;"
      >
         <el-table-column label="序号" width="50" type="index" align="center">
            <template #default="scope">
               <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
            </template>
         </el-table-column>
         <el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
         <el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
         <el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
         <el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
         <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
         <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
         <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
         <el-table-column label="登录时间" align="center" prop="loginTime" width="180">
            <template #default="scope">
               <span>{{ parseTime(scope.row.loginTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">强退</el-button>
            </template>
         </el-table-column>
      </el-table>
      <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
   </div>
</template>
<script setup name="Online">
<script setup name="Online" lang="ts">
import { forceLogout, list as initData } from "@/api/monitor/online";
import { ComponentInternalInstance } from "vue";
import { OnlineQuery, OnlineVO } from "@/api/monitor/online/types";
const { proxy } = getCurrentInstance();
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const onlineList = ref([]);
const onlineList = ref<OnlineVO[]>([]);
const loading = ref(true);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(10);
const queryParams = ref({
  ipaddr: undefined,
  userName: undefined
const queryFormRef = ref(ElForm);
const queryParams = ref<OnlineQuery>({
    pageNum: 1,
    pageSize: 10,
    ipaddr: '',
    userName: ''
});
/** æŸ¥è¯¢ç™»å½•日志列表 */
function getList() {
  loading.value = true;
  initData(queryParams.value).then(response => {
    onlineList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
const getList = async () => {
    loading.value = true;
    const res = await initData(queryParams.value);
    onlineList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
  pageNum.value = 1;
  getList();
const handleQuery = () => {
    queryParams.value.pageNum = 1;
    getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef");
  handleQuery();
const resetQuery = () => {
    queryFormRef.value.resetFields();
    handleQuery();
}
/** å¼ºé€€æŒ‰é’®æ“ä½œ */
function handleForceLogout(row) {
    proxy.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?').then(function () {
  return forceLogout(row.tokenId);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const handleForceLogout = async (row: OnlineVO) => {
    await proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?');
    await forceLogout(row.tokenId);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
getList();
onMounted(() => {
    getList();
})
</script>
<template>
    <div class="p-2">
        <div class="search">
            <el-form :model="queryParams" ref="queryFormRef" :inline="true">
                <el-form-item label="登录地址" prop="ipaddr">
                    <el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable style="width: 200px" @keyup.enter="handleQuery" />
                </el-form-item>
                <el-form-item label="用户名称" prop="userName">
                    <el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                    <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                </el-form-item>
            </el-form>
        </div>
        <div class="panel">
            <el-table
                v-loading="loading"
                :data="onlineList.slice((queryParams.pageNum - 1) * queryParams.pageSize, queryParams.pageNum * queryParams.pageSize)"
                style="width: 100%;"
            >
                <el-table-column label="序号" width="50" type="index" align="center">
                    <template #default="scope">
                        <span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
                <el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
                <el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
                <el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
                <el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
                <el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
                <el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
                <el-table-column label="登录时间" align="center" prop="loginTime" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.loginTime) }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="强退" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">
                            </el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" />
        </div>
    </div>
</template>
src/views/monitor/operlog/index.vue
@@ -1,291 +1,295 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
         <el-form-item label="系统模块" prop="title">
            <el-input
               v-model="queryParams.title"
               placeholder="请输入系统模块"
               clearable
               style="width: 240px;"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="操作人员" prop="operName">
            <el-input
               v-model="queryParams.operName"
               placeholder="请输入操作人员"
               clearable
               style="width: 240px;"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="类型" prop="businessType">
            <el-select
               v-model="queryParams.businessType"
               placeholder="操作类型"
               clearable
               style="width: 240px"
            >
               <el-option
                  v-for="dict in sys_oper_type"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select
               v-model="queryParams.status"
               placeholder="操作状态"
               clearable
               style="width: 240px"
            >
               <el-option
                  v-for="dict in sys_common_status"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item label="操作时间" style="width: 308px">
            <el-date-picker
               v-model="dateRange"
               value-format="YYYY-MM-DD HH:mm:ss"
               type="daterange"
               range-separator="-"
               start-placeholder="开始日期"
               end-placeholder="结束日期"
               :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
            ></el-date-picker>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
<script setup name="Operlog" lang="ts">
import { list, delOperlog, cleanOperlog } from '@/api/monitor/operlog';
import { ComponentInternalInstance } from 'vue';
import { OperLogForm, OperLogQuery, OperLogVO } from '@/api/monitor/operlog/types';
import { DateModelType } from 'element-plus';
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               :disabled="multiple"
               @click="handleDelete"
               v-hasPermi="['monitor:operlog:remove']"
            >删除</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               @click="handleClean"
               v-hasPermi="['monitor:operlog:remove']"
            >清空</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="warning"
               plain
               icon="Download"
               @click="handleExport"
               v-hasPermi="['monitor:operlog:export']"
            >导出</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_oper_type, sys_common_status } = toRefs<any>(proxy?.useDict("sys_oper_type","sys_common_status"));
      <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
         <el-table-column type="selection" width="50" align="center" />
         <el-table-column label="日志编号" align="center" prop="operId" />
         <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
         <el-table-column label="操作类型" align="center" prop="businessType">
            <template #default="scope">
               <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
            </template>
         </el-table-column>
         <el-table-column label="操作人员" align="center" width="110" prop="operName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
         <el-table-column label="主机" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
         <el-table-column label="操作状态" align="center" prop="status">
            <template #default="scope">
               <dict-tag :options="sys_common_status" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
            <template #default="scope">
               <span>{{ parseTime(scope.row.operTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="消耗时间" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']">
            <template #default="scope">
               <span>{{ scope.row.costTime }}毫秒</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">详细</el-button>
            </template>
         </el-table-column>
      </el-table>
      <pagination
         v-show="total > 0"
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
      />
      <!-- æ“ä½œæ—¥å¿—详细 -->
      <el-dialog title="操作日志详细" v-model="open" width="700px" append-to-body>
         <el-form :model="form" label-width="100px">
            <el-row>
               <el-col :span="12">
                  <el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
                  <el-form-item
                    label="登录信息:"
                  >{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
                  <el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
               </el-col>
               <el-col :span="6">
                  <el-form-item label="操作状态:">
                     <div v-if="form.status === 0">正常</div>
                     <div v-else-if="form.status === 1">失败</div>
                  </el-form-item>
               </el-col>
               <el-col :span="8">
                  <el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
               </el-col>
               <el-col :span="10">
                  <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
               </el-col>
            </el-row>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button @click="open = false">关 é—­</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
<script setup name="Operlog">
import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog";
const { proxy } = getCurrentInstance();
const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type","sys_common_status");
const operlogList = ref([]);
const open = ref(false);
const operlogList = ref<OperLogVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const ids = ref<Array<number | string>>([]);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const dateRange = ref([]);
const defaultSort = ref({ prop: "operTime", order: "descending" });
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const defaultSort = ref<any>({ prop: "operTime", order: "descending" });
const data = reactive({
  form: {},
const operLogTableRef = ref(ElTable);
const queryFormRef = ref(ElForm);
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
});
const data = reactive<PageData<OperLogForm, OperLogQuery>>({
  form: {
    operId: undefined,
    tenantId: undefined,
    title: '',
    businessType: 0,
    businessTypes: undefined,
    method: '',
    requestMethod: '',
    operatorType: 0,
    operName: '',
    deptName: '',
    operUrl: '',
    operIp: '',
    operLocation: '',
    operParam: '',
    jsonResult: '',
    status: 0,
    errorMsg: '',
    operTime: '',
    costTime: 0
  },
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    title: undefined,
    operName: undefined,
    businessType: undefined,
    status: undefined
  }
    title: '',
    operName: '',
    businessType: '',
    status: '',
    orderByColumn: defaultSort.value.prop,
    isAsc: defaultSort.value.order
  },
  rules: {}
});
const { queryParams, form } = toRefs(data);
/** æŸ¥è¯¢ç™»å½•日志 */
function getList() {
const getList = async () => {
  loading.value = true;
  list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
    operlogList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
    const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
    operlogList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** æ“ä½œæ—¥å¿—类型字典翻译 */
function typeFormat(row, column) {
  return proxy.selectDictLabel(sys_oper_type.value, row.businessType);
const typeFormat = (row: OperLogForm) => {
  return proxy?.selectDictLabel(sys_oper_type.value, row.businessType);
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  dateRange.value = [];
  proxy.resetForm("queryRef");
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value.resetFields();
  queryParams.value.pageNum = 1;
  proxy.$refs["operlogRef"].sort(defaultSort.value.prop, defaultSort.value.order);
  operLogTableRef.value.sort(defaultSort.value.prop, defaultSort.value.order);
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
const handleSelectionChange = (selection: OperLogVO[]) => {
  ids.value = selection.map(item => item.operId);
  multiple.value = !selection.length;
}
/** æŽ’序触发事件 */
function handleSortChange(column, prop, order) {
const handleSortChange = (column: any) => {
  queryParams.value.orderByColumn = column.prop;
  queryParams.value.isAsc = column.order;
  getList();
}
/** è¯¦ç»†æŒ‰é’®æ“ä½œ */
function handleView(row) {
  open.value = true;
const handleView = (row: OperLogVO) => {
  dialog.visible = true;
  form.value = row;
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const operIds = row.operId || ids.value;
  proxy.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?').then(function () {
    return delOperlog(operIds);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const handleDelete = async (row?: OperLogVO) => {
  const operIds = row?.operId || ids.value;
    await proxy?.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?');
    await delOperlog(operIds);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
/** æ¸…空按钮操作 */
function handleClean() {
  proxy.$modal.confirm("是否确认清空所有操作日志数据项?").then(function () {
    return cleanOperlog();
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("清空成功");
  }).catch(() => {});
const handleClean = async () => {
    await proxy?.$modal.confirm("是否确认清空所有操作日志数据项?");
    await cleanOperlog();
    getList();
    proxy?.$modal.msgSuccess("清空成功");
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
  proxy.download("monitor/operlog/export",{
const handleExport = () => {
  proxy?.download("monitor/operlog/export", {
    ...queryParams.value,
  }, `config_${new Date().getTime()}.xlsx`);
}
getList();
onMounted(() => {
  getList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="系统模块" prop="title">
                        <el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable style="width: 240px;" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="操作人员" prop="operName">
                        <el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable style="width: 240px;" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="类型" prop="businessType">
                        <el-select v-model="queryParams.businessType" placeholder="操作类型" clearable style="width: 240px">
                            <el-option v-for="dict in sys_oper_type" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="状态" prop="status">
                        <el-select v-model="queryParams.status" placeholder="操作状态" clearable style="width: 240px">
                            <el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="操作时间" style="width: 308px">
                        <el-date-picker
                            v-model="dateRange"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            type="daterange"
                            range-separator="-"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
                        ></el-date-picker>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['monitor:operlog:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="WarnTriangleFilled" @click="handleClean" v-hasPermi="['monitor:operlog:remove']">清空</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['monitor:operlog:export']">导出</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table
                ref="operLogTableRef"
                v-loading="loading"
                :data="operlogList"
                @selection-change="handleSelectionChange"
                :default-sort="defaultSort"
                @sort-change="handleSortChange"
            >
                <el-table-column type="selection" width="50" align="center" />
                <el-table-column label="日志编号" align="center" prop="operId" />
                <el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
                <el-table-column label="操作类型" align="center" prop="businessType">
                    <template #default="scope">
                        <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
                    </template>
                </el-table-column>
                <el-table-column
                    label="操作人员"
                    align="center"
                    width="110"
                    prop="operName"
                    :show-overflow-tooltip="true"
                    sortable="custom"
                    :sort-orders="['descending', 'ascending']"
                />
                <el-table-column label="主机" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
                <el-table-column label="操作状态" align="center" prop="status">
                    <template #default="scope">
                        <dict-tag :options="sys_common_status" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.operTime) }}</span>
                    </template>
                </el-table-column>
                <el-table-column
                    label="消耗时间"
                    align="center"
                    prop="costTime"
                    width="110"
                    :show-overflow-tooltip="true"
                    sortable="custom"
                    :sort-orders="['descending', 'ascending']"
                >
                    <template #default="scope">
                        <span>{{ scope.row.costTime }}毫秒</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" fixed="right" align="center" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="详细" placement="top">
                            <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:operlog:query']"> </el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ“ä½œæ—¥å¿—详细 -->
        <el-dialog title="操作日志详细" v-model="dialog.visible" width="700px" append-to-body>
            <el-form :model="form" label-width="100px">
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
                        <el-form-item label="登录信息:">{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
                        <el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="请求参数:">{{form.operParam}}</el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
                    </el-col>
                    <el-col :span="6">
                        <el-form-item label="操作状态:">
                            <div v-if="form.status === 0">正常</div>
                            <div v-else-if="form.status === 1">失败</div>
                        </el-form-item>
                    </el-col>
                    <el-col :span="8">
                        <el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
                    </el-col>
                    <el-col :span="10">
                        <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button @click="dialog.visible = false">关 é—­</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/monitor/xxljob/index.vue
@@ -1,13 +1,9 @@
<template>
  <div>
    <i-frame v-model:src="url"></i-frame>
  </div>
    <div>
        <i-frame v-model:src="url"></i-frame>
    </div>
</template>
<script setup>
import iFrame from '@/components/iFrame'
import { ref } from 'vue';
<script setup lang="ts">
const url = ref(import.meta.env.VITE_APP_XXL_JOB_ADMIN);
</script>
src/views/redirect/index.vue
@@ -1,5 +1,5 @@
<template>
  <div></div>
    <div></div>
</template>
<script setup>
@@ -11,4 +11,4 @@
const { path } = params
router.replace({ path: '/' + path, query })
</script>
</script>
src/views/register.vue
@@ -1,99 +1,12 @@
<template>
  <div class="register">
    <el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
      <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
      <el-form-item prop="tenantId">
        <el-select v-model="registerForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
          <el-option
              v-for="item in tenantList"
              :key="item.tenantId"
              :label="item.companyName"
              :value="item.tenantId">
          </el-option>
          <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
        </el-select>
      </el-form-item>
      <el-form-item prop="username">
        <el-input
          v-model="registerForm.username"
          type="text"
          size="large"
          auto-complete="off"
          placeholder="账号"
        >
          <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="password">
        <el-input
          v-model="registerForm.password"
          type="password"
          size="large"
          auto-complete="off"
          placeholder="密码"
          @keyup.enter="handleRegister"
        >
          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="confirmPassword">
        <el-input
          v-model="registerForm.confirmPassword"
          type="password"
          size="large"
          auto-complete="off"
          placeholder="确认密码"
          @keyup.enter="handleRegister"
        >
          <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
        </el-input>
      </el-form-item>
      <el-form-item prop="code" v-if="captchaEnabled">
        <el-input
          size="large"
          v-model="registerForm.code"
          auto-complete="off"
          placeholder="验证码"
          style="width: 63%"
          @keyup.enter="handleRegister"
        >
          <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
        </el-input>
        <div class="register-code">
          <img :src="codeUrl" @click="getCode" class="register-code-img"/>
        </div>
      </el-form-item>
      <el-form-item style="width:100%;">
        <el-button
          :loading="loading"
          size="large"
          type="primary"
          style="width:100%;"
          @click.prevent="handleRegister"
        >
          <span v-if="!loading">注 å†Œ</span>
          <span v-else>注 å†Œ ä¸­...</span>
        </el-button>
        <div style="float: right;">
          <router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
        </div>
      </el-form-item>
    </el-form>
    <!--  åº•部  -->
    <div class="el-register-footer">
      <span>Copyright Â© 2018-2023 ruoyi.vip All Rights Reserved.</span>
    </div>
  </div>
</template>
<script setup>
import { ElMessageBox } from "element-plus";
import { getCodeImg, register, getTenantList } from "@/api/login";
<script setup lang="ts">
import { getCodeImg, register, getTenantList } from '@/api/login';
import { RegisterForm, TenantVO } from '@/api/types';
import { FormRules } from 'element-plus';
import { to } from 'await-to-js';
const router = useRouter();
const { proxy } = getCurrentInstance();
const registerForm = ref({
const registerForm = ref<RegisterForm>({
  tenantId: "",
  username: "",
  password: "",
@@ -103,7 +16,11 @@
  userType: "sys_user"
});
const equalToPassword = (rule, value, callback) => {
// ç§Ÿæˆ·å¼€å…³
const tenantEnabled = ref(true);
const equalToPassword = (rule: any, value: string, callback: any) => {
  if (registerForm.value.password !== value) {
    callback(new Error("两次输入的密码不一致"));
  } else {
@@ -111,7 +28,7 @@
  }
};
const registerRules = {
const registerRules: FormRules = {
  tenantId: [
    { required: true, trigger: "blur", message: "请输入您的租户编号" }
  ],
@@ -129,59 +46,119 @@
  ],
  code: [{ required: true, trigger: "change", message: "请输入验证码" }]
};
const codeUrl = ref("");
const loading = ref(false);
const captchaEnabled = ref(true);
const registerRef = ref(ElForm);
// ç§Ÿæˆ·åˆ—表
const tenantList = ref([]);
const tenantList = ref<TenantVO[]>([]);
function handleRegister() {
  proxy.$refs.registerRef.validate(valid => {
const handleRegister = () => {
  registerRef.value.validate(async (valid: boolean) => {
    if (valid) {
      loading.value = true;
      register(registerForm.value).then(res => {
      const [err] = await to(register(registerForm.value));
      if (!err) {
        const username = registerForm.value.username;
        ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " æ³¨å†ŒæˆåŠŸï¼</font>", "系统提示", {
        await ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " æ³¨å†ŒæˆåŠŸï¼</font>", "系统提示", {
          dangerouslyUseHTMLString: true,
          type: "success",
        }).then(() => {
          router.push("/login");
        }).catch(() => {});
      }).catch(() => {
        });
        await router.push("/login");
      } else {
        loading.value = false;
        if (captchaEnabled) {
          getCode();
        }
      });
      }
    }
  });
}
function getCode() {
  getCodeImg().then(res => {
    captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
const getCode = async () => {
  const { data } = await getCodeImg();
  captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
    if (captchaEnabled.value) {
      codeUrl.value = "data:image/gif;base64," + res.img;
      registerForm.value.uuid = res.uuid;
      codeUrl.value = "data:image/gif;base64," + data.img;
      registerForm.value.uuid = data.uuid;
    }
  });
}
function initTenantList() {
  getTenantList().then(res => {
    tenantList.value = res.data;
    if (tenantList.value != null && tenantList.value.length !== 0) {
      loginForm.value.tenantId = tenantList.value[0].tenantId;
const initTenantList = async () => {
  const { data } = await getTenantList();
    tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
    if (tenantEnabled.value) {
      tenantList.value = data.voList;
      if (tenantList.value != null && tenantList.value.length !== 0) {
          registerForm.value.tenantId = tenantList.value[0].tenantId;
      }
    }
  });
}
getCode();
initTenantList();
onMounted(() => {
  getCode();
  initTenantList();
})
</script>
<style lang='scss' scoped>
<template>
    <div class="register">
        <el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
            <h3 class="title">RuoYi-Vue-Plus多租户管理系统</h3>
            <el-form-item prop="tenantId" v-if="tenantEnabled">
                <el-select v-model="registerForm.tenantId" filterable placeholder="请选择/输入公司名称" style="width: 100%">
                    <el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
                    <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
                </el-select>
            </el-form-item>
            <el-form-item prop="username">
                <el-input v-model="registerForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
                    <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
                </el-input>
            </el-form-item>
            <el-form-item prop="password">
                <el-input v-model="registerForm.password" type="password" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleRegister">
                    <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
                </el-input>
            </el-form-item>
            <el-form-item prop="confirmPassword">
                <el-input
                    v-model="registerForm.confirmPassword"
                    type="password"
                    size="large"
                    auto-complete="off"
                    placeholder="确认密码"
                    @keyup.enter="handleRegister"
                >
                    <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
                </el-input>
            </el-form-item>
            <el-form-item prop="code" v-if="captchaEnabled">
                <el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleRegister">
                    <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
                </el-input>
                <div class="register-code">
                    <img :src="codeUrl" @click="getCode" class="register-code-img" />
                </div>
            </el-form-item>
            <el-form-item style="width:100%;">
                <el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleRegister">
                    <span v-if="!loading">注 å†Œ</span>
                    <span v-else>注 å†Œ ä¸­...</span>
                </el-button>
                <div style="float: right;">
                    <router-link class="link-type" :to="'/login'">使用已有账户登录</router-link>
                </div>
            </el-form-item>
        </el-form>
        <!--  åº•部  -->
        <div class="el-register-footer">
            <span>Copyright Â© 2018-2023 ruoyi.vip All Rights Reserved.</span>
        </div>
    </div>
</template>
<style lang="scss" scoped>
.register {
  display: flex;
  justify-content: center;
@@ -190,8 +167,9 @@
  background-image: url("../assets/images/login-background.jpg");
  background-size: cover;
}
.title {
  margin: 0px auto 30px auto;
  margin: 0 auto 30px auto;
  text-align: center;
  color: #707070;
}
@@ -201,32 +179,39 @@
  background: #ffffff;
  width: 400px;
  padding: 25px 25px 5px 25px;
  .el-input {
    height: 40px;
    input {
      height: 40px;
    }
  }
  .input-icon {
    height: 39px;
    width: 14px;
    margin-left: 0px;
    margin-left: 0;
  }
}
.register-tip {
  font-size: 13px;
  text-align: center;
  color: #bfbfbf;
}
.register-code {
  width: 33%;
  height: 40px;
  float: right;
  img {
    cursor: pointer;
    vertical-align: middle;
  }
}
.el-register-footer {
  height: 40px;
  line-height: 40px;
@@ -235,10 +220,11 @@
  width: 100%;
  text-align: center;
  color: #fff;
  font-family: Arial;
  font-family: Arial, serif;
  font-size: 12px;
  letter-spacing: 1px;
}
.register-code-img {
  height: 40px;
  padding-left: 12px;
src/views/system/config/index.vue
@@ -1,195 +1,43 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
         <el-form-item label="参数名称" prop="configName">
            <el-input
               v-model="queryParams.configName"
               placeholder="请输入参数名称"
               clearable
               style="width: 240px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="参数键名" prop="configKey">
            <el-input
               v-model="queryParams.configKey"
               placeholder="请输入参数键名"
               clearable
               style="width: 240px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="系统内置" prop="configType">
            <el-select v-model="queryParams.configType" placeholder="系统内置" clearable>
               <el-option
                  v-for="dict in sys_yes_no"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item label="创建时间" style="width: 308px;">
            <el-date-picker
               v-model="dateRange"
               value-format="YYYY-MM-DD HH:mm:ss"
               type="daterange"
               range-separator="-"
               start-placeholder="开始日期"
               end-placeholder="结束日期"
               :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
            ></el-date-picker>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Plus"
               @click="handleAdd"
               v-hasPermi="['system:config:add']"
            >新增</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="success"
               plain
               icon="Edit"
               :disabled="single"
               @click="handleUpdate"
               v-hasPermi="['system:config:edit']"
            >修改</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               :disabled="multiple"
               @click="handleDelete"
               v-hasPermi="['system:config:remove']"
            >删除</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="warning"
               plain
               icon="Download"
               @click="handleExport"
               v-hasPermi="['system:config:export']"
            >导出</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Refresh"
               @click="handleRefreshCache"
               v-hasPermi="['system:config:remove']"
            >刷新缓存</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="参数主键" align="center" prop="configId" v-if="false" />
         <el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
         <el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
         <el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
         <el-table-column label="系统内置" align="center" prop="configType">
            <template #default="scope">
               <dict-tag :options="sys_yes_no" :value="scope.row.configType" />
            </template>
         </el-table-column>
         <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:config:edit']" >修改</el-button>
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:config:remove']">删除</el-button>
            </template>
         </el-table-column>
      </el-table>
      <pagination
         v-show="total > 0"
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
      />
      <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
         <el-form ref="configRef" :model="form" :rules="rules" label-width="80px">
            <el-form-item label="参数名称" prop="configName">
               <el-input v-model="form.configName" placeholder="请输入参数名称" />
            </el-form-item>
            <el-form-item label="参数键名" prop="configKey">
               <el-input v-model="form.configKey" placeholder="请输入参数键名" />
            </el-form-item>
            <el-form-item label="参数键值" prop="configValue">
               <el-input v-model="form.configValue" placeholder="请输入参数键值" />
            </el-form-item>
            <el-form-item label="系统内置" prop="configType">
               <el-radio-group v-model="form.configType">
                  <el-radio
                     v-for="dict in sys_yes_no"
                     :key="dict.value"
                     :label="dict.value"
                  >{{ dict.label }}</el-radio>
               </el-radio-group>
            </el-form-item>
            <el-form-item label="备注" prop="remark">
               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
            </el-form-item>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
               <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
<script setup name="Config">
<script setup name="Config" lang="ts">
import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from "@/api/system/config";
import { ConfigForm, ConfigQuery, ConfigVO } from "@/api/system/config/types";
import { ComponentInternalInstance } from "vue";
import { DateModelType } from 'element-plus';
const { proxy } = getCurrentInstance();
const { sys_yes_no } = proxy.useDict("sys_yes_no");
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_yes_no } = toRefs<any>(proxy?.useDict("sys_yes_no"));
const configList = ref([]);
const open = ref(false);
const configList = ref<ConfigVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const dateRange = ref([]);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const data = reactive({
  form: {},
const queryFormRef = ref(ElForm);
const configFormRef = ref(ElForm);
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
});
const initFormData: ConfigForm = {
  configId: undefined,
  configName: '',
  configKey: '',
  configValue: '',
  configType: "Y",
  remark: ''
}
const data = reactive<PageData<ConfigForm, ConfigQuery>>({
  form: {...initFormData},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    configName: undefined,
    configKey: undefined,
    configType: undefined
    configName: '',
    configKey: '',
    configType: '',
  },
  rules: {
    configName: [{ required: true, message: "参数名称不能为空", trigger: "blur" }],
@@ -201,106 +49,213 @@
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢å‚数列表 */
function getList() {
const getList = async () => {
  loading.value = true;
  listConfig(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
    configList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
    const res = await listConfig(proxy?.addDateRange(queryParams.value, dateRange.value));
    configList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** å–消按钮 */
function cancel() {
  open.value = false;
const cancel = () => {
  reset();
  dialog.visible = false;
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    configId: undefined,
    configName: undefined,
    configKey: undefined,
    configValue: undefined,
    configType: "Y",
    remark: undefined
  };
  proxy.resetForm("configRef");
const reset = () => {
    form.value = {...initFormData};
  configFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  dateRange.value = [];
  proxy.resetForm("queryRef");
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value.resetFields();
  handleQuery();
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
const handleSelectionChange = (selection: ConfigVO[]) => {
  ids.value = selection.map(item => item.configId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加参数";
const handleAdd = () => {
  dialog.visible = true;
  dialog.title = "添加参数";
  nextTick(() => {
    reset();
  })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
  reset();
  const configId = row.configId || ids.value;
  getConfig(configId).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改参数";
  });
const handleUpdate = (row?: ConfigVO) => {
    dialog.visible = true;
    dialog.title = "修改参数";
    const configId = row?.configId || ids.value[0];
  nextTick(async () => {
        reset();
        const res = await getConfig(configId);
        form.value = res.data;
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["configRef"].validate(valid => {
    if (valid) {
      if (form.value.configId != undefined) {
        updateConfig(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addConfig(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
const submitForm = () => {
  configFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            form.value.configId ? await updateConfig(form.value) : await addConfig(form.value);
            proxy?.$modal.msgSuccess("操作成功");
            dialog.visible = false;
            getList();
    }
  });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const configIds = row.configId || ids.value;
  proxy.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?').then(function () {
    return delConfig(configIds);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const handleDelete = async (row?: ConfigVO) => {
  const configIds = row?.configId || ids.value;
    await proxy?.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?');
    await delConfig(configIds);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
  proxy.download("system/config/export", {
const handleExport = () => {
  proxy?.download("system/config/export", {
    ...queryParams.value
  }, `config_${new Date().getTime()}.xlsx`);
}
/** åˆ·æ–°ç¼“存按钮操作 */
function handleRefreshCache() {
  refreshCache().then(() => {
    proxy.$modal.msgSuccess("刷新缓存成功");
  });
const handleRefreshCache = async () => {
    await refreshCache();
    proxy?.$modal.msgSuccess("刷新缓存成功");
}
getList();
onMounted(() => {
    getList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="参数名称" prop="configName">
                        <el-input v-model="queryParams.configName" placeholder="请输入参数名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="参数键名" prop="configKey">
                        <el-input v-model="queryParams.configKey" placeholder="请输入参数键名" clearable style="width: 240px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="系统内置" prop="configType">
                        <el-select v-model="queryParams.configType" placeholder="系统内置" clearable>
                            <el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="创建时间" style="width: 308px;">
                        <el-date-picker
                            v-model="dateRange"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            type="daterange"
                            range-separator="-"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
                        ></el-date-picker>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:config:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:config:edit']">
                            ä¿®æ”¹
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:config:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:config:export']">导出</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Refresh" @click="handleRefreshCache" v-hasPermi="['system:config:remove']">刷新缓存</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="参数主键" align="center" prop="configId" v-if="false" />
                <el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
                <el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
                <el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
                <el-table-column label="系统内置" align="center" prop="configType">
                    <template #default="scope">
                        <dict-tag :options="sys_yes_no" :value="scope.row.configType" />
                    </template>
                </el-table-column>
                <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime) }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:config:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:config:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
            <el-form ref="configFormRef" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="参数名称" prop="configName">
                    <el-input v-model="form.configName" placeholder="请输入参数名称" />
                </el-form-item>
                <el-form-item label="参数键名" prop="configKey">
                    <el-input v-model="form.configKey" placeholder="请输入参数键名" />
                </el-form-item>
                <el-form-item label="参数键值" prop="configValue">
                    <el-input v-model="form.configValue" placeholder="请输入参数键值" />
                </el-form-item>
                <el-form-item label="系统内置" prop="configType">
                    <el-radio-group v-model="form.configType">
                        <el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="备注" prop="remark">
                    <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/system/dept/index.vue
@@ -1,163 +1,49 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="部门名称" prop="deptName">
            <el-input
               v-model="queryParams.deptName"
               placeholder="请输入部门名称"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select v-model="queryParams.status" placeholder="部门状态" clearable style="width: 200px">
               <el-option
                  v-for="dict in sys_normal_disable"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
<script setup name="Dept" lang="ts">
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
import { ComponentInternalInstance } from 'vue';
import { DeptForm, DeptQuery, DeptVO } from "@/api/system/dept/types";
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Plus"
               @click="handleAdd"
               v-hasPermi="['system:dept:add']"
            >新增</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="info"
               plain
               icon="Sort"
               @click="toggleExpandAll"
            >展开/折叠</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
interface DeptOptionsType {
  deptId: number | string;
  deptName: string;
  children: DeptOptionsType[];
      <el-table
         v-if="refreshTable"
         v-loading="loading"
         :data="deptList"
         row-key="deptId"
         :default-expand-all="isExpandAll"
         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
      >
         <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
         <el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
         <el-table-column prop="status" label="状态" width="100">
            <template #default="scope">
               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="创建时间" align="center" prop="createTime" width="200">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">修改</el-button>
               <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">新增</el-button>
               <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">删除</el-button>
            </template>
         </el-table-column>
      </el-table>
}
      <!-- æ·»åŠ æˆ–ä¿®æ”¹éƒ¨é—¨å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="600px" append-to-body>
         <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
            <el-row>
               <el-col :span="24" v-if="form.parentId !== 0">
                  <el-form-item label="上级部门" prop="parentId">
                     <el-tree-select
                        v-model="form.parentId"
                        :data="deptOptions"
                        :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
                        value-key="deptId"
                        placeholder="选择上级部门"
                        check-strictly
                     />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="部门名称" prop="deptName">
                     <el-input v-model="form.deptName" placeholder="请输入部门名称" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="显示排序" prop="orderNum">
                     <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="负责人" prop="leader">
                     <el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="联系电话" prop="phone">
                     <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="邮箱" prop="email">
                     <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="部门状态">
                     <el-radio-group v-model="form.status">
                        <el-radio
                           v-for="dict in sys_normal_disable"
                           :key="dict.value"
                           :label="dict.value"
                        >{{ dict.label }}</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
            </el-row>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
               <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
<script setup name="Dept">
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept";
const deptList = ref<DeptVO[]>([])
const loading = ref(true)
const showSearch = ref(true)
const deptOptions = ref<DeptOptionsType[]>([])
const isExpandAll = ref(true)
const { proxy } = getCurrentInstance();
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const deptList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const title = ref("");
const deptOptions = ref([]);
const isExpandAll = ref(true);
const refreshTable = ref(true);
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
});
const data = reactive({
  form: {},
const deptTableRef = ref(ElTable);
const queryFormRef = ref(ElForm);
const deptFormRef = ref(ElForm);
const initFormData: DeptForm = {
  deptId: undefined,
  parentId: undefined,
  deptName: undefined,
  orderNum: 0,
  leader: undefined,
  phone: undefined,
  email: undefined,
  status: "0"
}
const data = reactive<PageData<DeptForm, DeptQuery>>({
  form: {...initFormData},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    deptName: undefined,
    status: undefined
  },
@@ -168,111 +54,239 @@
    email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
    phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
  },
});
})
const { queryParams, form, rules } = toRefs(data);
const { queryParams, form, rules } = toRefs<PageData<DeptForm, DeptQuery>>(data)
/** æŸ¥è¯¢éƒ¨é—¨åˆ—表 */
function getList() {
  loading.value = true;
  listDept(queryParams.value).then(response => {
    deptList.value = proxy.handleTree(response.data, "deptId");
    loading.value = false;
  });
/** æŸ¥è¯¢èœå•列表 */
const getList = async () => {
    loading.value = true;
  const res = await listDept(queryParams.value);
    const data = proxy?.handleTree<DeptVO>(res.data, "deptId")
    if (data) {
        deptList.value = data
    }
    loading.value = false
}
/** å–消按钮 */
function cancel() {
  open.value = false;
  reset();
const cancel = () => {
  reset()
  dialog.visible = false
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    deptId: undefined,
    parentId: undefined,
    deptName: undefined,
    orderNum: 0,
    leader: undefined,
    phone: undefined,
    email: undefined,
    status: "0"
  };
  proxy.resetForm("deptRef");
const reset = () => {
  form.value = {...initFormData};
  deptFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef");
  handleQuery();
const resetQuery = () => {
  queryFormRef.value.resetFields();
  handleQuery()
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd(row) {
  reset();
  listDept().then(response => {
    deptOptions.value = proxy.handleTree(response.data, "deptId");
  });
  if (row != undefined) {
    form.value.parentId = row.deptId;
  }
  open.value = true;
  title.value = "添加部门";
const handleAdd = (row?: DeptVO) => {
  listDept().then(res => {
    const data = proxy?.handleTree<DeptOptionsType>(res.data, "deptId");
    if (data) {
      deptOptions.value = data
      dialog.visible = true;
      dialog.title = "添加部门";
      nextTick(() => {
        reset();
        if (row && row.deptId) {
          form.value.parentId = row?.parentId;
        }
      })
    }
  })
}
/** å±•å¼€/折叠操作 */
function toggleExpandAll() {
  refreshTable.value = false;
const handleToggleExpandAll = () => {
  isExpandAll.value = !isExpandAll.value;
  nextTick(() => {
    refreshTable.value = true;
  });
  toggleExpandAll(deptList.value, isExpandAll.value)
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
  reset();
  getDept(row.deptId).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改部门";
    listDeptExcludeChild(row.deptId).then(response => {
      deptOptions.value = proxy.handleTree(response.data, "deptId");
      if (deptOptions.value.length == 0) {
        const noResultsOptions = { deptId: proxy.form.parentId, deptName: proxy.form.parentName, children: [] };
        deptOptions.value.push(noResultsOptions);
      }
    });
  });
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["deptRef"].validate(valid => {
    if (valid) {
      if (form.value.deptId != undefined) {
        updateDept(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addDept(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  proxy.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
    return delDept(row.deptId);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
/** å±•å¼€/折叠所有 */
const toggleExpandAll = (data: DeptVO[], status: boolean) => {
  data.forEach((item) => {
    deptTableRef.value.toggleRowExpansion(item, status)
    if(item.children && item.children.length > 0) toggleExpandAll(item.children, status)
  })
}
getList();
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row: DeptVO) => {
    const res = await getDept(row.deptId);
    dialog.visible = true;
    dialog.title = "修改部门";
    nextTick(async () => {
        reset();
        form.value = res.data
        const response = await listDeptExcludeChild(row.deptId);
        const data = proxy?.handleTree<DeptOptionsType>(response.data, "deptId")
        if (data) {
            deptOptions.value = data;
            if (data.length === 0) {
                const noResultsOptions: DeptOptionsType = { deptId: res.data.parentId, deptName: res.data.parentName, children: [] };
                deptOptions.value.push(noResultsOptions);
            }
        }
    })
}
/** æäº¤æŒ‰é’® */
const submitForm = () => {
  deptFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            form.value.deptId ? await updateDept(form.value) : await addDept(form.value);
            proxy?.$modal.msgSuccess("操作成功");
            dialog.visible = false;
            getList();
    }
  })
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row: DeptVO) => {
    await proxy?.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?');
    await delDept(row.deptId);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
onMounted(() => {
  getList();
});
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
                    <el-form-item label="菜单名称" prop="menuName">
                        <el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="状态" prop="status">
                        <el-select v-model="queryParams.status" placeholder="部门状态" clearable>
                            <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:dept:add']">新增 </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table
                v-loading="loading"
                :data="deptList"
                row-key="deptId"
                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
                ref="deptTableRef"
                :default-expand-all="isExpandAll"
            >
                <el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
                <el-table-column prop="orderNum" align="center" label="排序" width="200"></el-table-column>
                <el-table-column prop="status" align="center" label="状态" width="100">
                    <template #default="scope">
                        <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="创建时间" align="center" prop="createTime" width="200">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime) }}</span>
                    </template>
                </el-table-column>
                <el-table-column fixed="right" align="center" label="操作">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']" />
                        </el-tooltip>
                        <el-tooltip content="新增" placement="top">
                            <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']" />
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']" />
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
        <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-bod width="600px">
            <el-form ref="deptFormRef" :model="form" :rules="rules" label-width="80px">
                <el-row>
                    <el-col :span="24" v-if="form.parentId !== 0">
                        <el-form-item label="上级部门" prop="parentId">
                            <el-tree-select
                                v-model="form.parentId"
                                :data="deptOptions"
                                :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
                                value-key="deptId"
                                placeholder="选择上级部门"
                                check-strictly
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="部门名称" prop="deptName">
                            <el-input v-model="form.deptName" placeholder="请输入部门名称" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="显示排序" prop="orderNum">
                            <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="负责人" prop="leader">
                            <el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="联系电话" prop="phone">
                            <el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="邮箱" prop="email">
                            <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="部门状态">
                            <el-radio-group v-model="form.status">
                                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label
                                }}</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/system/dict/data.vue
@@ -1,350 +1,312 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="字典名称" prop="dictType">
            <el-select v-model="queryParams.dictType" style="width: 200px">
               <el-option
                  v-for="item in typeOptions"
                  :key="item.dictId"
                  :label="item.dictName"
                  :value="item.dictType"
               />
            </el-select>
         </el-form-item>
         <el-form-item label="字典标签" prop="dictLabel">
            <el-input
               v-model="queryParams.dictLabel"
               placeholder="请输入字典标签"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select v-model="queryParams.status" placeholder="数据状态" clearable style="width: 200px">
               <el-option
                  v-for="dict in sys_normal_disable"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Plus"
               @click="handleAdd"
               v-hasPermi="['system:dict:add']"
            >新增</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="success"
               plain
               icon="Edit"
               :disabled="single"
               @click="handleUpdate"
               v-hasPermi="['system:dict:edit']"
            >修改</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               :disabled="multiple"
               @click="handleDelete"
               v-hasPermi="['system:dict:remove']"
            >删除</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="warning"
               plain
               icon="Download"
               @click="handleExport"
               v-hasPermi="['system:dict:export']"
            >导出</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="warning"
               plain
               icon="Close"
               @click="handleClose"
            >关闭</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="字典编码" align="center" prop="dictCode" v-if="false" />
         <el-table-column label="字典标签" align="center" prop="dictLabel">
            <template #default="scope">
               <span v-if="scope.row.listClass == '' || scope.row.listClass == 'default'">{{ scope.row.dictLabel }}</span>
               <el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass">{{ scope.row.dictLabel }}</el-tag>
            </template>
         </el-table-column>
         <el-table-column label="字典键值" align="center" prop="dictValue" />
         <el-table-column label="字典排序" align="center" prop="dictSort" />
         <el-table-column label="状态" align="center" prop="status">
            <template #default="scope">
               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">修改</el-button>
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除</el-button>
            </template>
         </el-table-column>
      </el-table>
      <pagination
         v-show="total > 0"
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
      />
      <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
         <el-form ref="dataRef" :model="form" :rules="rules" label-width="80px">
            <el-form-item label="字典类型">
               <el-input v-model="form.dictType" :disabled="true" />
            </el-form-item>
            <el-form-item label="数据标签" prop="dictLabel">
               <el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
            </el-form-item>
            <el-form-item label="数据键值" prop="dictValue">
               <el-input v-model="form.dictValue" placeholder="请输入数据键值" />
            </el-form-item>
            <el-form-item label="样式属性" prop="cssClass">
               <el-input v-model="form.cssClass" placeholder="请输入样式属性" />
            </el-form-item>
            <el-form-item label="显示排序" prop="dictSort">
               <el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
            </el-form-item>
            <el-form-item label="回显样式" prop="listClass">
               <el-select v-model="form.listClass">
                  <el-option
                     v-for="item in listClassOptions"
                     :key="item.value"
                     :label="item.label + '(' + item.value + ')'"
                     :value="item.value"
                  ></el-option>
               </el-select>
            </el-form-item>
            <el-form-item label="状态" prop="status">
               <el-radio-group v-model="form.status">
                  <el-radio
                     v-for="dict in sys_normal_disable"
                     :key="dict.value"
                     :label="dict.value"
                  >{{ dict.label }}</el-radio>
               </el-radio-group>
            </el-form-item>
            <el-form-item label="备注" prop="remark">
               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
            </el-form-item>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
               <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
<script setup name="Data">
<script setup name="Data" lang="ts">
import useDictStore from '@/store/modules/dict'
import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type";
import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data";
import { DictTypeVO } from '@/api/system/dict/type/types';
import { ComponentInternalInstance } from "vue";
import { DictDataForm, DictDataQuery, DictDataVO } from "@/api/system/dict/data/types";
import { ElForm } from 'element-plus';
const { proxy } = getCurrentInstance();
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"));
const route = useRoute();
const dataList = ref([]);
const open = ref(false);
const dataList = ref<DictDataVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const defaultDictType = ref("");
const typeOptions = ref([]);
const route = useRoute();
const typeOptions = ref<DictTypeVO[]>([]);
const dataFormRef = ref(ElForm);
const queryFormRef = ref(ElForm);
const dialog = reactive<DialogOption>({
    visible: false,
    title: ''
});
// æ•°æ®æ ‡ç­¾å›žæ˜¾æ ·å¼
const listClassOptions = ref([
  { value: "default", label: "默认" },
  { value: "primary", label: "主要" },
  { value: "success", label: "成功" },
  { value: "info", label: "信息" },
  { value: "warning", label: "警告" },
  { value: "danger", label: "危险" }
const listClassOptions = ref<Array<{ value: string, label: string }>>([
    { value: "default", label: "默认" },
    { value: "primary", label: "主要" },
    { value: "success", label: "成功" },
    { value: "info", label: "信息" },
    { value: "warning", label: "警告" },
    { value: "danger", label: "危险" }
]);
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    dictName: undefined,
    dictType: undefined,
    status: undefined
  },
  rules: {
    dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
    dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
    dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
  }
const initFormData: DictDataForm = {
    dictCode: undefined,
    dictLabel: '',
    dictValue: '',
    cssClass: '',
    listClass: "default",
    dictSort: 0,
    status: "0",
    remark: ''
}
const data = reactive<PageData<DictDataForm, DictDataQuery>>({
    form: { ...initFormData },
    queryParams: {
        pageNum: 1,
        pageSize: 10,
        dictName: '',
        dictType: '',
        status: '',
        dictLabel: ''
    },
    rules: {
        dictLabel: [{ required: true, message: "数据标签不能为空", trigger: "blur" }],
        dictValue: [{ required: true, message: "数据键值不能为空", trigger: "blur" }],
        dictSort: [{ required: true, message: "数据顺序不能为空", trigger: "blur" }]
    }
});
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢å­—典类型详细 */
function getTypes(dictId) {
  getType(dictId).then(response => {
    queryParams.value.dictType = response.data.dictType;
    defaultDictType.value = response.data.dictType;
    getList();
  });
const getTypes = async (dictId: string | number) => {
    const { data } = await getType(dictId);
    queryParams.value.dictType = data.dictType;
    defaultDictType.value = data.dictType;
    getList();
}
/** æŸ¥è¯¢å­—典类型列表 */
function getTypeList() {
  getDictOptionselect().then(response => {
    typeOptions.value = response.data;
  });
const getTypeList = async () => {
    const res = await getDictOptionselect()
    typeOptions.value = res.data;
}
/** æŸ¥è¯¢å­—典数据列表 */
function getList() {
  loading.value = true;
  listData(queryParams.value).then(response => {
    dataList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
const getList = async () => {
    loading.value = true;
    const res = await listData(queryParams.value);
    dataList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** å–消按钮 */
function cancel() {
  open.value = false;
  reset();
const cancel = () => {
    dialog.visible = false;
    reset();
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    dictCode: undefined,
    dictLabel: undefined,
    dictValue: undefined,
    cssClass: undefined,
    listClass: "default",
    dictSort: 0,
    status: "0",
    remark: undefined
  };
  proxy.resetForm("dataRef");
const reset = () => {
    form.value = { ...initFormData };
    dataFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
const handleQuery = () => {
    queryParams.value.pageNum = 1;
    getList();
}
/** è¿”回按钮操作 */
function handleClose() {
  const obj = { path: "/system/dict" };
  proxy.$tab.closeOpenPage(obj);
const handleClose = () => {
    const obj = { path: "/system/dict" };
    proxy?.$tab.closeOpenPage(obj);
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef");
  queryParams.value.dictType = defaultDictType;
  handleQuery();
const resetQuery = () => {
    queryFormRef.value.resetFields();
    queryParams.value.dictType = defaultDictType.value;
    handleQuery();
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加字典数据";
  form.value.dictType = queryParams.value.dictType;
const handleAdd = () => {
    dialog.visible = true;
    dialog.title = "添加字典数据";
    nextTick(() => {
        reset();
        form.value.dictType = queryParams.value.dictType;
    })
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
  ids.value = selection.map(item => item.dictCode);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
const handleSelectionChange = (selection: DictDataVO[]) => {
    ids.value = selection.map(item => item.dictCode);
    single.value = selection.length != 1;
    multiple.value = !selection.length;
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
  reset();
  const dictCode = row.dictCode || ids.value;
  getData(dictCode).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改字典数据";
  });
const handleUpdate = (row?: DictDataVO) => {
    const dictCode = row?.dictCode || ids.value[0];
    dialog.visible = true;
    dialog.title = "修改字典数据";
    nextTick(async () => {
        const res =  await getData(dictCode);
        reset();
        form.value = res.data;
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["dataRef"].validate(valid => {
    if (valid) {
      if (form.value.dictCode != undefined) {
        updateData(form.value).then(response => {
          useDictStore().removeDict(queryParams.value.dictType);
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addData(form.value).then(response => {
          useDictStore().removeDict(queryParams.value.dictType);
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
const submitForm = () => {
    dataFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            form.value.dictCode ? await updateData(form.value) : await addData(form.value);
            useDictStore().removeDict(queryParams.value.dictType);
            proxy?.$modal.msgSuccess("操作成功");
            dialog.visible = false;
            getList();
        }
    });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const dictCodes = row.dictCode || ids.value;
  proxy.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?').then(function() {
    return delData(dictCodes);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
    useDictStore().removeDict(queryParams.value.dictType);
  }).catch(() => {});
const handleDelete = async (row?: DictDataVO) => {
    const dictCodes = row?.dictCode || ids.value;
    await proxy?.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?');
    await delData(dictCodes);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
    useDictStore().removeDict(queryParams.value.dictType);
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
  proxy.download("system/dict/data/export", {
    ...queryParams.value
  }, `dict_data_${new Date().getTime()}.xlsx`);
const handleExport = () => {
    proxy?.download("system/dict/data/export", {
        ...queryParams.value
    }, `dict_data_${new Date().getTime()}.xlsx`);
}
getTypes(route.params && route.params.dictId);
getTypeList();
onMounted(() => {
    getTypes(route.params && route.params.dictId as string);
    getTypeList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="字典名称" prop="dictType">
                        <el-select v-model="queryParams.dictType" style="width: 200px">
                            <el-option v-for="item in typeOptions" :key="item.dictId" :label="item.dictName" :value="item.dictType" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="字典标签" prop="dictLabel">
                        <el-input v-model="queryParams.dictLabel" placeholder="请输入字典标签" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="状态" prop="status">
                        <el-select v-model="queryParams.status" placeholder="数据状态" clearable style="width: 200px">
                            <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:dict:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:dict:edit']">修改</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:dict:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:dict:export']">导出</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="字典编码" align="center" prop="dictCode" v-if="false" />
                <el-table-column label="字典标签" align="center" prop="dictLabel">
                    <template #default="scope">
                        <span v-if="scope.row.listClass === '' || scope.row.listClass === 'default'">{{ scope.row.dictLabel }}</span>
                        <el-tag v-else :type="scope.row.listClass === 'primary' ? '' : scope.row.listClass">{{ scope.row.dictLabel
                        }}</el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="字典键值" align="center" prop="dictValue" />
                <el-table-column label="字典排序" align="center" prop="dictSort" />
                <el-table-column label="状态" align="center" prop="status">
                    <template #default="scope">
                        <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime) }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
            <el-form ref="dataFormRef" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="字典类型">
                    <el-input v-model="form.dictType" :disabled="true" />
                </el-form-item>
                <el-form-item label="数据标签" prop="dictLabel">
                    <el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
                </el-form-item>
                <el-form-item label="数据键值" prop="dictValue">
                    <el-input v-model="form.dictValue" placeholder="请输入数据键值" />
                </el-form-item>
                <el-form-item label="样式属性" prop="cssClass">
                    <el-input v-model="form.cssClass" placeholder="请输入样式属性" />
                </el-form-item>
                <el-form-item label="显示排序" prop="dictSort">
                    <el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
                </el-form-item>
                <el-form-item label="回显样式" prop="listClass">
                    <el-select v-model="form.listClass">
                        <el-option
                            v-for="item in listClassOptions"
                            :key="item.value"
                            :label="item.label + '(' + item.value + ')'"
                            :value="item.value"
                        ></el-option>
                    </el-select>
                </el-form-item>
                <el-form-item label="状态" prop="status">
                    <el-radio-group v-model="form.status">
                        <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="备注" prop="remark">
                    <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/system/dict/index.vue
@@ -1,203 +1,46 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
         <el-form-item label="字典名称" prop="dictName">
            <el-input
               v-model="queryParams.dictName"
               placeholder="请输入字典名称"
               clearable
               style="width: 240px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="字典类型" prop="dictType">
            <el-input
               v-model="queryParams.dictType"
               placeholder="请输入字典类型"
               clearable
               style="width: 240px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select
               v-model="queryParams.status"
               placeholder="字典状态"
               clearable
               style="width: 240px"
            >
               <el-option
                  v-for="dict in sys_normal_disable"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item label="创建时间" style="width: 308px">
            <el-date-picker
               v-model="dateRange"
               value-format="YYYY-MM-DD HH:mm:ss"
               type="daterange"
               range-separator="-"
               start-placeholder="开始日期"
               end-placeholder="结束日期"
               :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
            ></el-date-picker>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Plus"
               @click="handleAdd"
               v-hasPermi="['system:dict:add']"
            >新增</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="success"
               plain
               icon="Edit"
               :disabled="single"
               @click="handleUpdate"
               v-hasPermi="['system:dict:edit']"
            >修改</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               :disabled="multiple"
               @click="handleDelete"
               v-hasPermi="['system:dict:remove']"
            >删除</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="warning"
               plain
               icon="Download"
               @click="handleExport"
               v-hasPermi="['system:dict:export']"
            >导出</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Refresh"
               @click="handleRefreshCache"
               v-hasPermi="['system:dict:remove']"
            >刷新缓存</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="字典编号" align="center" prop="dictId" v-if="false" />
         <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true"/>
         <el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
            <template #default="scope">
               <router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
                  <span>{{ scope.row.dictType }}</span>
               </router-link>
            </template>
         </el-table-column>
         <el-table-column label="状态" align="center" prop="status">
            <template #default="scope">
               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="180">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">修改</el-button>
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">删除</el-button>
            </template>
         </el-table-column>
      </el-table>
      <pagination
         v-show="total > 0"
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
      />
      <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="500px" append-to-body>
         <el-form ref="dictRef" :model="form" :rules="rules" label-width="80px">
            <el-form-item label="字典名称" prop="dictName">
               <el-input v-model="form.dictName" placeholder="请输入字典名称" />
            </el-form-item>
            <el-form-item label="字典类型" prop="dictType">
               <el-input v-model="form.dictType" placeholder="请输入字典类型" />
            </el-form-item>
            <el-form-item label="状态" prop="status">
               <el-radio-group v-model="form.status">
                  <el-radio
                     v-for="dict in sys_normal_disable"
                     :key="dict.value"
                     :label="dict.value"
                  >{{ dict.label }}</el-radio>
               </el-radio-group>
            </el-form-item>
            <el-form-item label="备注" prop="remark">
               <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
            </el-form-item>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
               <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
<script setup name="Dict">
<script setup name="Dict" lang="ts">
import useDictStore from '@/store/modules/dict'
import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type";
import { ComponentInternalInstance } from "vue";
import { DictTypeForm, DictTypeQuery, DictTypeVO } from "@/api/system/dict/type/types";
import { DateModelType } from 'element-plus';
const { proxy } = getCurrentInstance();
const { sys_normal_disable } = proxy.useDict("sys_normal_disable");
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_normal_disable"))
const typeList = ref([]);
const open = ref(false);
const typeList = ref<DictTypeVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const dateRange = ref([]);
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
const data = reactive({
  form: {},
const dictFormRef = ref(ElForm);
const queryFormRef = ref(ElForm);
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
});
const initFormData: DictTypeForm = {
  dictId: undefined,
  dictName: '',
  dictType: '',
  status: "0",
  remark: ''
}
const data = reactive<PageData<DictTypeForm, DictTypeQuery>>({
  form: {...initFormData},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    dictName: undefined,
    dictType: undefined,
    status: undefined
    dictName: '',
    dictType: '',
    status: ''
  },
  rules: {
    dictName: [{ required: true, message: "字典名称不能为空", trigger: "blur" }],
@@ -208,106 +51,216 @@
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢å­—典类型列表 */
function getList() {
const getList = () => {
  loading.value = true;
  listType(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
    typeList.value = response.rows;
    total.value = response.total;
  listType(proxy?.addDateRange(queryParams.value, dateRange.value)).then(res => {
    typeList.value = res.rows;
    total.value = res.total;
    loading.value = false;
  });
}
/** å–消按钮 */
function cancel() {
  open.value = false;
const cancel = () => {
  reset();
  dialog.visible = false;
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    dictId: undefined,
    dictName: undefined,
    dictType: undefined,
    status: "0",
    remark: undefined
  };
  proxy.resetForm("dictRef");
const reset = () => {
    form.value = {...initFormData};
    dictFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  dateRange.value = [];
  proxy.resetForm("queryRef");
const resetQuery = () => {
  dateRange.value = ['', ''];
  queryFormRef.value.resetFields();
  handleQuery();
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加字典类型";
const handleAdd = () => {
  dialog.visible = true;
  dialog.title = "添加字典类型";
    nextTick(() => {
        reset();
    })
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
const handleSelectionChange = (selection: DictTypeVO[]) =>  {
  ids.value = selection.map(item => item.dictId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
  reset();
  const dictId = row.dictId || ids.value;
  getType(dictId).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改字典类型";
  });
const handleUpdate = (row?: DictTypeVO) => {
    dialog.visible = true;
    dialog.title = "修改字典类型";
    const dictId = row?.dictId || ids.value[0];
    nextTick(async () => {
        reset();
        const res = await getType(dictId);
    form.value = res.data;
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["dictRef"].validate(valid => {
    if (valid) {
      if (form.value.dictId != undefined) {
        updateType(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addType(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
const submitForm = () => {
  dictFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            form.value.dictId ? await updateType(form.value) : await addType(form.value);
            proxy?.$modal.msgSuccess("操作成功");
            dialog.visible = false;
            getList();
    }
  });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const dictIds = row.dictId || ids.value;
  proxy.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?').then(function() {
    return delType(dictIds);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const handleDelete = async (row?: DictTypeVO) => {
  const dictIds = row?.dictId || ids.value;
    await proxy?.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?');
    await delType(dictIds);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
/** å¯¼å‡ºæŒ‰é’®æ“ä½œ */
function handleExport() {
  proxy.download("system/dict/type/export", {
const handleExport = () => {
  proxy?.download("system/dict/type/export", {
    ...queryParams.value
  }, `dict_${new Date().getTime()}.xlsx`);
}
/** åˆ·æ–°ç¼“存按钮操作 */
function handleRefreshCache() {
  refreshCache().then(() => {
    proxy.$modal.msgSuccess("刷新成功");
    useDictStore().cleanDict();
  });
const handleRefreshCache = async () => {
    await refreshCache();
    proxy?.$modal.msgSuccess("刷新成功");
    useDictStore().cleanDict();
}
getList();
onMounted(()=>{
  getList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="字典名称" prop="dictName">
                        <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable style="width: 240px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="字典类型" prop="dictType">
                        <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable style="width: 240px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="状态" prop="status">
                        <el-select v-model="queryParams.status" placeholder="字典状态" clearable style="width: 240px">
                            <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item label="创建时间" style="width: 308px">
                        <el-date-picker
                            v-model="dateRange"
                            value-format="YYYY-MM-DD HH:mm:ss"
                            type="daterange"
                            range-separator="-"
                            start-placeholder="开始日期"
                            end-placeholder="结束日期"
                            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
                        ></el-date-picker>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:dict:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:dict:edit']">修改</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:dict:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:dict:export']">导出</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Refresh" @click="handleRefreshCache" v-hasPermi="['system:dict:remove']">刷新缓存</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="字典编号" align="center" prop="dictId" v-if="false" />
                <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
                <el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
                    <template #default="scope">
                        <router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
                            <span>{{ scope.row.dictType }}</span>
                        </router-link>
                    </template>
                </el-table-column>
                <el-table-column label="状态" align="center" prop="status">
                    <template #default="scope">
                        <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime) }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹å‚æ•°é…ç½®å¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
            <el-form ref="dictFormRef" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="字典名称" prop="dictName">
                    <el-input v-model="form.dictName" placeholder="请输入字典名称" />
                </el-form-item>
                <el-form-item label="字典类型" prop="dictType">
                    <el-input v-model="form.dictType" placeholder="请输入字典类型" />
                </el-form-item>
                <el-form-item label="状态" prop="status">
                    <el-radio-group v-model="form.status">
                        <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="备注" prop="remark">
                    <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/system/menu/index.vue
@@ -1,441 +1,417 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="菜单名称" prop="menuName">
            <el-input
               v-model="queryParams.menuName"
               placeholder="请输入菜单名称"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="状态" prop="status">
            <el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px">
               <el-option
                  v-for="dict in sys_normal_disable"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
<script setup name="Menu" lang="ts">
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu';
import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types';
import { ComponentInternalInstance } from 'vue';
import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
import { ElTable, ElForm } from 'element-plus';
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Plus"
               @click="handleAdd"
               v-hasPermi="['system:menu:add']"
            >新增</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="info"
               plain
               icon="Sort"
               @click="toggleExpandAll"
            >展开/折叠</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
interface MenuOptionsType {
    menuId: number;
    menuName: string;
    children: MenuOptionsType[] | undefined;
}
      <el-table
         v-if="refreshTable"
         v-loading="loading"
         :data="menuList"
         row-key="menuId"
         :default-expand-all="isExpandAll"
         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
      >
         <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
         <el-table-column prop="icon" label="图标" align="center" width="100">
            <template #default="scope">
               <svg-icon :icon-class="scope.row.icon" />
            </template>
         </el-table-column>
         <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
         <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
         <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
         <el-table-column prop="status" label="状态" width="80">
            <template #default="scope">
               <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="创建时间" align="center" width="160" prop="createTime">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime) }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">修改</el-button>
               <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']">新增</el-button>
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']">删除</el-button>
            </template>
         </el-table-column>
      </el-table>
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict("sys_show_hide", "sys_normal_disable"));
      <!-- æ·»åŠ æˆ–ä¿®æ”¹èœå•å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="680px" append-to-body>
         <el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
            <el-row>
               <el-col :span="24">
                  <el-form-item label="上级菜单">
                     <el-tree-select
                        v-model="form.parentId"
                        :data="menuOptions"
                        :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
                        value-key="menuId"
                        placeholder="选择上级菜单"
                        check-strictly
                     />
                  </el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="菜单类型" prop="menuType">
                     <el-radio-group v-model="form.menuType">
                        <el-radio label="M">目录</el-radio>
                        <el-radio label="C">菜单</el-radio>
                        <el-radio label="F">按钮</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
               <el-col :span="24" v-if="form.menuType != 'F'">
                  <el-form-item label="菜单图标" prop="icon">
                     <el-popover
                        placement="bottom-start"
                        :width="540"
                        v-model:visible="showChooseIcon"
                        trigger="click"
                        @show="showSelectIcon"
                     >
                        <template #reference>
                           <el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" v-click-outside="hideSelectIcon" readonly>
                              <template #prefix>
                                 <svg-icon
                                    v-if="form.icon"
                                    :icon-class="form.icon"
                                    class="el-input__icon"
                                    style="height: 32px;width: 16px;"
                                 />
                                 <el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
                              </template>
                           </el-input>
                        </template>
                        <icon-select ref="iconSelectRef" @selected="selected" />
                     </el-popover>
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="菜单名称" prop="menuName">
                     <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="显示排序" prop="orderNum">
                     <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType != 'F'">
                  <el-form-item>
                     <template #label>
                        <span>
                           <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>是否外链
                        </span>
                     </template>
                     <el-radio-group v-model="form.isFrame">
                        <el-radio label="0">是</el-radio>
                        <el-radio label="1">否</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType != 'F'">
                  <el-form-item prop="path">
                     <template #label>
                        <span>
                           <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           è·¯ç”±åœ°å€
                        </span>
                     </template>
                     <el-input v-model="form.path" placeholder="请输入路由地址" />
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType == 'C'">
                  <el-form-item prop="component">
                     <template #label>
                        <span>
                           <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           ç»„件路径
                        </span>
                     </template>
                     <el-input v-model="form.component" placeholder="请输入组件路径" />
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType != 'M'">
                  <el-form-item>
                     <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
                     <template #label>
                        <span>
                           <el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           æƒé™å­—符
                        </span>
                     </template>
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType == 'C'">
                  <el-form-item>
                     <el-input v-model="form.queryParam" placeholder="请输入路由参数" maxlength="255" />
                     <template #label>
                        <span>
                           <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           è·¯ç”±å‚æ•°
                        </span>
                     </template>
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType == 'C'">
                  <el-form-item>
                     <template #label>
                        <span>
                           <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           æ˜¯å¦ç¼“å­˜
                        </span>
                     </template>
                     <el-radio-group v-model="form.isCache">
                        <el-radio label="0">缓存</el-radio>
                        <el-radio label="1">不缓存</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType != 'F'">
                  <el-form-item>
                     <template #label>
                        <span>
                           <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           æ˜¾ç¤ºçŠ¶æ€
                        </span>
                     </template>
                     <el-radio-group v-model="form.visible">
                        <el-radio
                           v-for="dict in sys_show_hide"
                           :key="dict.value"
                           :label="dict.value"
                        >{{ dict.label }}</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType != 'F'">
                  <el-form-item>
                     <template #label>
                        <span>
                           <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           èœå•状态
                        </span>
                     </template>
                     <el-radio-group v-model="form.status">
                        <el-radio
                           v-for="dict in sys_normal_disable"
                           :key="dict.value"
                           :label="dict.value"
                        >{{ dict.label }}</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
            </el-row>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
               <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
const menuList = ref<MenuVO[]>([])
const loading = ref(true)
const showSearch = ref(true)
const menuOptions = ref<MenuOptionsType[]>([])
const isExpandAll = ref(false)
<script setup name="Menu">
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect";
import { ClickOutside as vClickOutside } from 'element-plus'
const { proxy } = getCurrentInstance();
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
const menuList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const title = ref("");
const menuOptions = ref([]);
const isExpandAll = ref(false);
const refreshTable = ref(true);
const showChooseIcon = ref(false);
const iconSelectRef = ref(null);
const data = reactive({
  form: {},
  queryParams: {
    menuName: undefined,
    visible: undefined
  },
  rules: {
    menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
    orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
    path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
  },
const dialog = reactive<DialogOption>({
    visible: false,
    title: ''
});
const { queryParams, form, rules } = toRefs(data);
const queryFormRef = ref(ElForm);
const menuFormRef = ref(ElForm);
const initFormData = {
    path: '',
    menuId: undefined,
    parentId: 0,
    menuName: '',
    icon: '',
    menuType: MenuTypeEnum.M,
    orderNum: 1,
    isFrame: "1",
    isCache: "0",
    visible: "0",
    status: "0"
}
const data = reactive<PageData<MenuForm, MenuQuery>>({
    form: { ...initFormData },
    queryParams: {
        menuName: undefined,
        status: undefined
    },
    rules: {
        menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
        orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
        path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
    },
})
const menuTableRef = ref(ElTable);
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data)
/** æŸ¥è¯¢èœå•列表 */
function getList() {
  loading.value = true;
  listMenu(queryParams.value).then(response => {
    menuList.value = proxy.handleTree(response.data, "menuId");
    loading.value = false;
  });
const getList = async () => {
    loading.value = true
    const res = await listMenu(queryParams.value);
    const data = proxy?.handleTree<MenuVO>(res.data, "menuId")
    if (data) {
        menuList.value = data
    }
    loading.value = false
}
/** æŸ¥è¯¢èœå•下拉树结构 */
function getTreeselect() {
  menuOptions.value = [];
  listMenu().then(response => {
    const menu = { menuId: 0, menuName: "主类目", children: [] };
    menu.children = proxy.handleTree(response.data, "menuId");
    menuOptions.value.push(menu);
  });
const getTreeselect = async () => {
    menuOptions.value = []
    const response = await listMenu();
    const menu: MenuOptionsType = { menuId: 0, menuName: "主类目", children: [] }
    menu.children = proxy?.handleTree<MenuOptionsType>(response.data, "menuId")
    menuOptions.value.push(menu)
}
/** å–消按钮 */
function cancel() {
  open.value = false;
  reset();
const cancel = () => {
    reset()
    dialog.visible = false
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    menuId: undefined,
    parentId: 0,
    menuName: undefined,
    icon: undefined,
    menuType: "M",
    orderNum: undefined,
    isFrame: "1",
    isCache: "0",
    visible: "0",
    status: "0"
  };
  proxy.resetForm("menuRef");
}
/** å±•示下拉图标 */
function showSelectIcon() {
  iconSelectRef.value.reset();
  showChooseIcon.value = true;
}
/** é€‰æ‹©å›¾æ ‡ */
function selected(name) {
  form.value.icon = name;
  showChooseIcon.value = false;
}
/** å›¾æ ‡å¤–层点击隐藏下拉列表 */
function hideSelectIcon(event) {
  var elem = event.relatedTarget || event.srcElement || event.target || event.currentTarget;
  var className = elem.className;
  if (className !== "el-input__inner") {
    showChooseIcon.value = false;
  }
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef");
  handleQuery();
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd(row) {
  reset();
  getTreeselect();
  if (row != null && row.menuId) {
    form.value.parentId = row.menuId;
  } else {
    form.value.parentId = 0;
  }
  open.value = true;
  title.value = "添加菜单";
}
/** å±•å¼€/折叠操作 */
function toggleExpandAll() {
  refreshTable.value = false;
  isExpandAll.value = !isExpandAll.value;
  nextTick(() => {
    refreshTable.value = true;
  });
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
async function handleUpdate(row) {
  reset();
  await getTreeselect();
  getMenu(row.menuId).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改菜单";
  });
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["menuRef"].validate(valid => {
    if (valid) {
      if (form.value.menuId != undefined) {
        updateMenu(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addMenu(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  proxy.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
    return delMenu(row.menuId);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const reset = () => {
    form.value = { ...initFormData };
    menuFormRef.value.resetFields();
}
getList();
/** æœç´¢æŒ‰é’®æ“ä½œ */
const handleQuery = () => {
    getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
const resetQuery = () => {
    queryFormRef.value.resetFields();
    handleQuery();
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
const handleAdd = (row?: MenuVO) => {
    dialog.visible = true;
    dialog.title = "添加菜单";
    getTreeselect();
    nextTick(() => {
        reset();
        row && row.menuId ? form.value.parentId = row.menuId : form.value.parentId = 0;
    })
}
/** å±•å¼€/折叠操作 */
const handleToggleExpandAll = () => {
    isExpandAll.value = !isExpandAll.value;
    toggleExpandAll(menuList.value, isExpandAll.value)
}
/** å±•å¼€/折叠所有 */
const toggleExpandAll = (data: MenuVO[], status: boolean) => {
    data.forEach((item: MenuVO) => {
        menuTableRef.value.toggleRowExpansion(item, status)
        if (item.children && item.children.length > 0) toggleExpandAll(item.children, status)
    })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
const handleUpdate = async (row: MenuVO) => {
    await getTreeselect();
    dialog.visible = true;
    dialog.title = "修改菜单";
    await nextTick(async () => {
        if (row.menuId) {
            const { data } = await getMenu(row.menuId);
            reset();
            form.value = data;
        }
    })
}
/** æäº¤æŒ‰é’® */
const submitForm = () => {
    menuFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            form.value.menuId ? await updateMenu(form.value) : await addMenu(form.value);
            proxy?.$modal.msgSuccess("操作成功");
            dialog.visible = false;
            getList();
        }
    })
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
const handleDelete = async (row: MenuVO) => {
    await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
    await delMenu(row.menuId);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
onMounted(() => {
    getList();
});
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="68px">
                    <el-form-item label="菜单名称" prop="menuName">
                        <el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="状态" prop="status">
                        <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
                            <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['system:menu:add']">新增 </el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table
                v-loading="loading"
                :data="menuList"
                row-key="menuId"
                :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
                border
                ref="menuTableRef"
                :default-expand-all="isExpandAll"
            >
                <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
                <el-table-column prop="icon" label="图标" align="center" width="100">
                    <template #default="scope">
                        <svg-icon :icon-class="scope.row.icon" />
                    </template>
                </el-table-column>
                <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
                <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
                <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
                <el-table-column prop="status" label="状态" width="80">
                    <template #default="scope">
                        <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="创建时间" align="center" prop="createTime">
                    <template #default="scope">
                        <span>{{ scope.row.createTime }}</span>
                    </template>
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="180">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']" />
                        </el-tooltip>
                        <el-tooltip content="新增" placement="top">
                            <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']" />
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']" />
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
        </el-card>
        <el-dialog :title="dialog.title" v-model="dialog.visible" destroy-on-close append-to-bod width="750px">
            <el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
                <el-row>
                    <el-col :span="24">
                        <el-form-item label="上级菜单">
                            <el-tree-select
                                v-model="form.parentId"
                                :data="menuOptions"
                                :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
                                value-key="menuId"
                                placeholder="选择上级菜单"
                                check-strictly
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="菜单类型" prop="menuType">
                            <el-radio-group v-model="form.menuType">
                                <el-radio label="M">目录</el-radio>
                                <el-radio label="C">菜单</el-radio>
                                <el-radio label="F">按钮</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span="24" v-if="form.menuType !== 'F'">
                        <el-form-item label="菜单图标" prop="icon">
                            <!-- å›¾æ ‡é€‰æ‹©å™¨ -->
                            <icon-select v-model="form.icon" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="菜单名称" prop="menuName">
                            <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="显示排序" prop="orderNum">
                            <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType !== 'F'">
                        <el-form-item>
                            <template #label>
                                <span>
                                    <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon> </el-tooltip
                                    >是否外链
                                </span>
                            </template>
                            <el-radio-group v-model="form.isFrame">
                                <el-radio label="0">是</el-radio>
                                <el-radio label="1">否</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType !== 'F'">
                        <el-form-item prop="path">
                            <template #label>
                                <span>
                                    <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    è·¯ç”±åœ°å€
                                </span>
                            </template>
                            <el-input v-model="form.path" placeholder="请输入路由地址" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType === 'C'">
                        <el-form-item prop="component">
                            <template #label>
                                <span>
                                    <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    ç»„件路径
                                </span>
                            </template>
                            <el-input v-model="form.component" placeholder="请输入组件路径" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType !== 'M'">
                        <el-form-item>
                            <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
                            <template #label>
                                <span>
                                    <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    æƒé™å­—符
                                </span>
                            </template>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType === 'C'">
                        <el-form-item>
                            <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
                            <template #label>
                                <span>
                                    <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    è·¯ç”±å‚æ•°
                                </span>
                            </template>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType === 'C'">
                        <el-form-item>
                            <template #label>
                                <span>
                                    <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    æ˜¯å¦ç¼“å­˜
                                </span>
                            </template>
                            <el-radio-group v-model="form.isCache">
                                <el-radio label="0">缓存</el-radio>
                                <el-radio label="1">不缓存</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType !== 'F'">
                        <el-form-item>
                            <template #label>
                                <span>
                                    <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    æ˜¾ç¤ºçŠ¶æ€
                                </span>
                            </template>
                            <el-radio-group v-model="form.visible">
                                <el-radio v-for="dict in sys_show_hide" :key="dict.value" :label="dict.value">{{ dict.label }} </el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12" v-if="form.menuType !== 'F'">
                        <el-form-item>
                            <template #label>
                                <span>
                                    <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
                                        <el-icon>
                                            <question-filled />
                                        </el-icon>
                                    </el-tooltip>
                                    èœå•状态
                                </span>
                            </template>
                            <el-radio-group v-model="form.status">
                                <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">
                                    {{ dict.label }}
                                </el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/system/notice/index.vue
@@ -1,283 +1,248 @@
<template>
   <div class="app-container">
      <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
         <el-form-item label="公告标题" prop="noticeTitle">
            <el-input
               v-model="queryParams.noticeTitle"
               placeholder="请输入公告标题"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="操作人员" prop="createByName">
            <el-input
               v-model="queryParams.createByName"
               placeholder="请输入操作人员"
               clearable
               style="width: 200px"
               @keyup.enter="handleQuery"
            />
         </el-form-item>
         <el-form-item label="类型" prop="noticeType">
            <el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable style="width: 200px">
               <el-option
                  v-for="dict in sys_notice_type"
                  :key="dict.value"
                  :label="dict.label"
                  :value="dict.value"
               />
            </el-select>
         </el-form-item>
         <el-form-item>
            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
         </el-form-item>
      </el-form>
      <el-row :gutter="10" class="mb8">
         <el-col :span="1.5">
            <el-button
               type="primary"
               plain
               icon="Plus"
               @click="handleAdd"
               v-hasPermi="['system:notice:add']"
            >新增</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="success"
               plain
               icon="Edit"
               :disabled="single"
               @click="handleUpdate"
               v-hasPermi="['system:notice:edit']"
            >修改</el-button>
         </el-col>
         <el-col :span="1.5">
            <el-button
               type="danger"
               plain
               icon="Delete"
               :disabled="multiple"
               @click="handleDelete"
               v-hasPermi="['system:notice:remove']"
            >删除</el-button>
         </el-col>
         <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
      </el-row>
      <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="55" align="center" />
         <el-table-column label="序号" align="center" prop="noticeId" width="100" v-if="false" />
         <el-table-column
            label="公告标题"
            align="center"
            prop="noticeTitle"
            :show-overflow-tooltip="true"
         />
         <el-table-column label="公告类型" align="center" prop="noticeType" width="100">
            <template #default="scope">
               <dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
            </template>
         </el-table-column>
         <el-table-column label="状态" align="center" prop="status" width="100">
            <template #default="scope">
               <dict-tag :options="sys_notice_status" :value="scope.row.status" />
            </template>
         </el-table-column>
         <el-table-column label="创建者" align="center" prop="createByName" width="100" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="100">
            <template #default="scope">
               <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
            </template>
         </el-table-column>
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
            <template #default="scope">
               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:edit']">修改</el-button>
               <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:notice:remove']" >删除</el-button>
            </template>
         </el-table-column>
      </el-table>
      <pagination
         v-show="total > 0"
         :total="total"
         v-model:page="queryParams.pageNum"
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
      />
      <!-- æ·»åŠ æˆ–ä¿®æ”¹å…¬å‘Šå¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="780px" append-to-body>
         <el-form ref="noticeRef" :model="form" :rules="rules" label-width="80px">
            <el-row>
               <el-col :span="12">
                  <el-form-item label="公告标题" prop="noticeTitle">
                     <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="公告类型" prop="noticeType">
                     <el-select v-model="form.noticeType" placeholder="请选择">
                        <el-option
                           v-for="dict in sys_notice_type"
                           :key="dict.value"
                           :label="dict.label"
                           :value="dict.value"
                        ></el-option>
                     </el-select>
                  </el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="状态">
                     <el-radio-group v-model="form.status">
                        <el-radio
                           v-for="dict in sys_notice_status"
                           :key="dict.value"
                           :label="dict.value"
                        >{{ dict.label }}</el-radio>
                     </el-radio-group>
                  </el-form-item>
               </el-col>
               <el-col :span="24">
                  <el-form-item label="内容">
                    <editor v-model="form.noticeContent" :min-height="192"/>
                  </el-form-item>
               </el-col>
            </el-row>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
               <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
               <el-button @click="cancel">取 æ¶ˆ</el-button>
            </div>
         </template>
      </el-dialog>
   </div>
</template>
<script setup name="Notice">
<script setup name="Notice" lang="ts">
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice";
import { ComponentInternalInstance } from "vue";
import { NoticeForm, NoticeQuery, NoticeVO } from "@/api/system/notice/types";
import { ElForm } from 'element-plus';
const { proxy } = getCurrentInstance();
const { sys_notice_status, sys_notice_type } = proxy.useDict("sys_notice_status", "sys_notice_type");
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_notice_status, sys_notice_type } = toRefs<any>(proxy?.useDict("sys_notice_status", "sys_notice_type"));
const noticeList = ref([]);
const open = ref(false);
const noticeList = ref<NoticeVO[]>([]);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    noticeTitle: undefined,
    createByName: undefined,
    status: undefined
  },
  rules: {
    noticeTitle: [{ required: true, message: "公告标题不能为空", trigger: "blur" }],
    noticeType: [{ required: true, message: "公告类型不能为空", trigger: "change" }]
  },
const queryFormRef = ref(ElForm);
const noticeFormRef = ref(ElForm);
const dialog = reactive<DialogOption>({
    visible: false,
    title: ''
});
const initFormData: NoticeForm = {
    noticeId: undefined,
    noticeTitle: '',
    noticeType: '',
    noticeContent: '',
    status: "0",
    remark: '',
    createByName: ''
}
const data = reactive<PageData<NoticeForm, NoticeQuery>>({
    form: { ...initFormData },
    queryParams: {
        pageNum: 1,
        pageSize: 10,
        noticeTitle: '',
        createByName: '',
        status: '',
        noticeType: ''
    },
    rules: {
        noticeTitle: [{ required: true, message: "公告标题不能为空", trigger: "blur" }],
        noticeType: [{ required: true, message: "公告类型不能为空", trigger: "change" }]
    },
});
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢å…¬å‘Šåˆ—表 */
function getList() {
  loading.value = true;
  listNotice(queryParams.value).then(response => {
    noticeList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
const getList = async () => {
    loading.value = true;
    const res = await listNotice(queryParams.value);
    noticeList.value = res.rows;
    total.value = res.total;
    loading.value = false;
}
/** å–消按钮 */
function cancel() {
  open.value = false;
  reset();
const cancel = () => {
    reset();
    dialog.visible = false;
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    noticeId: undefined,
    noticeTitle: undefined,
    noticeType: undefined,
    noticeContent: undefined,
    status: "0"
  };
  proxy.resetForm("noticeRef");
const reset = () => {
    form.value = { ...initFormData };
    noticeFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
const handleQuery = () => {
    queryParams.value.pageNum = 1;
    getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef");
  handleQuery();
const resetQuery = () => {
    queryFormRef.value.resetFields();
    handleQuery();
}
/** å¤šé€‰æ¡†é€‰ä¸­æ•°æ® */
function handleSelectionChange(selection) {
  ids.value = selection.map(item => item.noticeId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
const handleSelectionChange = (selection: NoticeVO[]) => {
    ids.value = selection.map(item => item.noticeId);
    single.value = selection.length != 1;
    multiple.value = !selection.length;
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加公告";
const handleAdd = () => {
    dialog.visible = true;
    dialog.title = "添加公告";
    nextTick(() => {
        reset();
    })
}
/**修改按钮操作 */
function handleUpdate(row) {
  reset();
  const noticeId = row.noticeId || ids.value;
  getNotice(noticeId).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改公告";
  });
const handleUpdate = (row?: NoticeVO) => {
    dialog.visible = true;
    dialog.title = "修改公告";
    nextTick(async () => {
        const noticeId = row?.noticeId || ids.value[0];
        reset();
        const { data } = await getNotice(noticeId);
        form.value = data;
    })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["noticeRef"].validate(valid => {
    if (valid) {
      if (form.value.noticeId != undefined) {
        updateNotice(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addNotice(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
const submitForm = () => {
    noticeFormRef.value.validate(async (valid: boolean) => {
        if (valid) {
            form.value.noticeId ? await updateNotice(form.value) : await addNotice(form.value);
            proxy?.$modal.msgSuccess("修改成功");
            dialog.visible = false;
            getList();
        }
    });
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const noticeIds = row.noticeId || ids.value
  proxy.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?').then(function() {
    return delNotice(noticeIds);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
const handleDelete = async (row?: NoticeVO) => {
    const noticeIds = row?.noticeId || ids.value
    await proxy?.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?');
    await delNotice(noticeIds);
    getList();
    proxy?.$modal.msgSuccess("删除成功");
}
getList();
onMounted(() => {
    getList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="公告标题" prop="noticeTitle">
                        <el-input v-model="queryParams.noticeTitle" placeholder="请输入公告标题" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="操作人员" prop="createByName">
                        <el-input v-model="queryParams.createByName" placeholder="请输入操作人员" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="类型" prop="noticeType">
                        <el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable style="width: 200px">
                            <el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value" />
                        </el-select>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:notice:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:notice:edit']"
                            >修改</el-button
                        >
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:notice:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="序号" align="center" prop="noticeId" width="100" v-if="false" />
                <el-table-column label="公告标题" align="center" prop="noticeTitle" :show-overflow-tooltip="true" />
                <el-table-column label="公告类型" align="center" prop="noticeType" width="100">
                    <template #default="scope">
                        <dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
                    </template>
                </el-table-column>
                <el-table-column label="状态" align="center" prop="status" width="100">
                    <template #default="scope">
                        <dict-tag :options="sys_notice_status" :value="scope.row.status" />
                    </template>
                </el-table-column>
                <el-table-column label="创建者" align="center" prop="createByName" width="100" />
                <el-table-column label="创建时间" align="center" prop="createTime" width="100">
                    <template #default="scope">
                        <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
                    </template>
                </el-table-column>
                <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:notice:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹å…¬å‘Šå¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="780px" append-to-body>
            <el-form ref="noticeFormRef" :model="form" :rules="rules" label-width="80px">
                <el-row>
                    <el-col :span="12">
                        <el-form-item label="公告标题" prop="noticeTitle">
                            <el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="公告类型" prop="noticeType">
                            <el-select v-model="form.noticeType" placeholder="请选择">
                                <el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="状态">
                            <el-radio-group v-model="form.status">
                                <el-radio v-for="dict in sys_notice_status" :key="dict.value" :label="dict.value">{{ dict.label
                                }}</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span="24">
                        <el-form-item label="内容">
                            <editor v-model="form.noticeContent" :min-height="192" />
                        </el-form-item>
                    </el-col>
                </el-row>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
src/views/system/oss/config.vue
@@ -1,170 +1,4 @@
<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
      <el-form-item label="配置key" prop="configKey">
        <el-input
          v-model="queryParams.configKey"
          placeholder="配置key"
          clearable
          style="width: 200px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="桶名称" prop="bucketName">
        <el-input
          v-model="queryParams.bucketName"
          placeholder="请输入桶名称"
          clearable
          style="width: 200px"
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="是否默认" prop="status">
        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
          <el-option key="0" label="是" value="0"/>
          <el-option key="1" label="否" value="1"/>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['system:oss:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['system:oss:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['system:oss:remove']"
        >删除</el-button>
      </el-col>
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <el-table v-loading="loading" :data="ossConfigList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="主建" align="center" prop="ossConfigId" v-if="columns[0].visible"/>
      <el-table-column label="配置key" align="center" prop="configKey" v-if="columns[1].visible" />
      <el-table-column label="访问站点" align="center" prop="endpoint" v-if="columns[2].visible" width="200" />
      <el-table-column label="自定义域名" align="center" prop="domain" v-if="columns[3].visible" width="200" />
      <el-table-column label="桶名称" align="center" prop="bucketName" v-if="columns[4].visible" />
      <el-table-column label="前缀" align="center" prop="prefix" v-if="columns[5].visible" />
      <el-table-column label="域" align="center" prop="region" v-if="columns[6].visible" />
      <el-table-column label="桶权限类型" align="center" prop="accessPolicy" v-if="columns[7].visible" >
        <template #default="scope">
          <el-tag type="warning" v-if="scope.row.accessPolicy === '0'">private</el-tag>
          <el-tag type="success" v-if="scope.row.accessPolicy === '1'">public</el-tag>
          <el-tag type="info" v-if="scope.row.accessPolicy === '2'">custom</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="是否默认" align="center" prop="status" v-if="columns[8].visible">
        <template #default="scope">
          <el-switch
            v-model="scope.row.status"
            active-value="0"
            inactive-value="1"
            @change="handleStatusChange(scope.row)"
          ></el-switch>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:oss:edit']">修改</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
    <!-- æ·»åŠ æˆ–ä¿®æ”¹å¯¹è±¡å­˜å‚¨é…ç½®å¯¹è¯æ¡† -->
    <el-dialog :title="title" v-model="open" width="800px" append-to-body>
      <el-form ref="ossConfigRef" :model="form" :rules="rules" label-width="120px">
        <el-form-item label="配置key" prop="configKey">
          <el-input v-model="form.configKey" placeholder="请输入配置key" />
        </el-form-item>
        <el-form-item label="访问站点" prop="endpoint">
          <el-input v-model="form.endpoint" placeholder="请输入访问站点" />
        </el-form-item>
        <el-form-item label="自定义域名" prop="domain">
          <el-input v-model="form.domain" placeholder="请输入自定义域名" />
        </el-form-item>
        <el-form-item label="accessKey" prop="accessKey">
          <el-input v-model="form.accessKey" placeholder="请输入accessKey" />
        </el-form-item>
        <el-form-item label="secretKey" prop="secretKey">
          <el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
        </el-form-item>
        <el-form-item label="桶名称" prop="bucketName">
          <el-input v-model="form.bucketName" placeholder="请输入桶名称" />
        </el-form-item>
        <el-form-item label="前缀" prop="prefix">
          <el-input v-model="form.prefix" placeholder="请输入前缀" />
        </el-form-item>
        <el-form-item label="是否HTTPS">
          <el-radio-group v-model="form.isHttps">
            <el-radio
              v-for="dict in sys_yes_no"
              :key="dict.value"
              :label="dict.value"
            >{{dict.label}}</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="桶权限类型">
          <el-radio-group v-model="form.accessPolicy">
            <el-radio label="0">private</el-radio>
            <el-radio label="1">public</el-radio>
            <el-radio label="2">custom</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="域" prop="region">
          <el-input v-model="form.region" placeholder="请输入域" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button :loading="buttonLoading" type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup name="OssConfig">
<script setup name="OssConfig" lang="ts">
import {
  listOssConfig,
  getOssConfig,
@@ -173,23 +7,33 @@
  updateOssConfig,
  changeOssConfigStatus
} from "@/api/system/ossConfig";
import { ComponentInternalInstance } from "vue";
import { OssConfigForm, OssConfigQuery, OssConfigVO } from "@/api/system/ossConfig/types";
import { ElForm } from 'element-plus';
const { proxy } = getCurrentInstance();
const { sys_yes_no } = proxy.useDict("sys_yes_no");
const ossConfigList = ref([]);
const open = ref(false);
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { sys_yes_no } = toRefs<any>(proxy?.useDict("sys_yes_no"));
const ossConfigList = ref<OssConfigVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const ids = ref<Array<number | string>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const queryFormRef = ref(ElForm);
const ossConfigFormRef = ref(ElForm);
const dialog = reactive<DialogOption>({
  visible: false,
  title: ''
});
// åˆ—显隐信息
const columns = ref([
const columns = ref<FieldOption[]>([
  { key: 0, label: `主建`, visible: true },
  { key: 1, label: `配置key`, visible: false },
  { key: 2, label: `访问站点`, visible: true },
@@ -201,20 +45,34 @@
  { key: 8, label: `状态`, visible: true }
]);
const data = reactive({
  form: {},
const initFormData: OssConfigForm = {
  ossConfigId: undefined,
  configKey: '',
  accessKey: '',
  secretKey: '',
  bucketName: '',
  prefix: '',
  endpoint: '',
  domain: '',
  isHttps: "N",
  accessPolicy: "1",
  region: '',
  status: "1",
  remark: '',
}
const data = reactive<PageData<OssConfigForm, OssConfigQuery>>({
  form: { ...initFormData },
  // æŸ¥è¯¢å‚æ•°
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    configKey: undefined,
    bucketName: undefined,
    status: undefined,
    configKey: '',
    bucketName: '',
    status: '',
  },
  rules: {
    configKey: [
      { required: true, message: "configKey不能为空", trigger: "blur" },
    ],
    configKey: [{ required: true, message: "configKey不能为空", trigger: "blur" },],
    accessKey: [
      { required: true, message: "accessKey不能为空", trigger: "blur" },
      {
@@ -251,132 +109,238 @@
        trigger: "blur",
      },
    ],
    accessPolicy:[
      { required: true, message: "accessPolicy不能为空", trigger: "blur" }
    ]
    accessPolicy: [{ required: true, message: "accessPolicy不能为空", trigger: "blur" }]
  }
});
const { queryParams, form, rules } = toRefs(data);
/** æŸ¥è¯¢å¯¹è±¡å­˜å‚¨é…ç½®åˆ—表 */
function getList() {
const getList = async () => {
  loading.value = true;
  listOssConfig(queryParams.value).then((response) => {
    ossConfigList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
  const res = await listOssConfig(queryParams.value);
  ossConfigList.value = res.rows;
  total.value = res.total;
  loading.value = false;
}
/** å–消按钮 */
function cancel() {
  open.value = false;
const cancel = () => {
  dialog.visible = false;
  reset();
}
/** è¡¨å•重置 */
function reset() {
  form.value = {
    ossConfigId: undefined,
    configKey: undefined,
    accessKey: undefined,
    secretKey: undefined,
    bucketName: undefined,
    prefix: undefined,
    endpoint: undefined,
    domain: undefined,
    isHttps: "N",
    accessPolicy: "1",
    region: undefined,
    status: "1",
    remark: undefined,
  };
  proxy.resetForm("ossConfigRef");
const reset = () => {
  form.value = { ...initFormData };
  ossConfigFormRef.value.resetFields();
}
/** æœç´¢æŒ‰é’®æ“ä½œ */
function handleQuery() {
const handleQuery = () => {
  queryParams.value.pageNum = 1;
  getList();
}
/** é‡ç½®æŒ‰é’®æ“ä½œ */
function resetQuery() {
  proxy.resetForm("queryRef");
const resetQuery = () => {
  queryFormRef.value.resetFields();
  handleQuery();
}
/** é€‰æ‹©æ¡æ•°  */
function handleSelectionChange(selection) {
const handleSelectionChange = (selection: OssConfigVO[]) => {
  ids.value = selection.map(item => item.ossConfigId);
  single.value = selection.length != 1;
  multiple.value = !selection.length;
}
/** æ–°å¢žæŒ‰é’®æ“ä½œ */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加对象存储配置";
const handleAdd = () => {
  dialog.visible = true;
  dialog.title = "添加对象存储配置";
  nextTick(() => {
    reset();
  })
}
/** ä¿®æ”¹æŒ‰é’®æ“ä½œ */
function handleUpdate(row) {
const handleUpdate = (row?: OssConfigVO) => {
  loading.value = true;
  reset();
  const ossConfigId = row.ossConfigId || ids.value;
  getOssConfig(ossConfigId).then((response) => {
  dialog.visible = true;
  dialog.title = "修改对象存储配置";
  const ossConfigId = row?.ossConfigId || ids.value[0];
  nextTick(async () => {
    reset();
    const res = await getOssConfig(ossConfigId);
    loading.value = false;
    form.value = response.data;
    open.value = true;
    title.value = "修改对象存储配置";
  });
    form.value = res.data;
  })
}
/** æäº¤æŒ‰é’® */
function submitForm() {
  proxy.$refs["ossConfigRef"].validate(valid => {
const submitForm = () => {
  ossConfigFormRef.value.validate(async (valid: boolean) => {
    if (valid) {
      buttonLoading.value = true;
      if (form.value.ossConfigId != null) {
        updateOssConfig(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        }).finally(() => {
          buttonLoading.value = false;
        });
      if (form.value.ossConfigId) {
        await updateOssConfig(form.value).finally(() => buttonLoading.value = false);
      } else {
        addOssConfig(this.form).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        }).finally(() => {
          buttonLoading.value = false;
        });
        await addOssConfig(form.value).finally(() => buttonLoading.value = false);
      }
      proxy?.$modal.msgSuccess("新增成功");
      dialog.visible = false;
      getList();
    }
  });
}
/** ç”¨æˆ·çŠ¶æ€ä¿®æ”¹  */
function handleStatusChange(row) {
/** çŠ¶æ€ä¿®æ”¹  */
const  handleStatusChange = async (row: OssConfigVO) => {
  let text = row.status === "0" ? "启用" : "停用";
  proxy.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?').then(() => {
    return changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
  }).then(() => {
  try {
    await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
    await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
    getList()
    proxy.$modal.msgSuccess(text + "成功");
  }).catch(function () {
    proxy?.$modal.msgSuccess(text + "成功");
  } catch { return } finally {
    row.status = row.status === "0" ? "1" : "0";
  });
  }
}
/** åˆ é™¤æŒ‰é’®æ“ä½œ */
function handleDelete(row) {
  const ossConfigIds = row.ossConfigId || ids.value;
  proxy.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?').then(() => {
    loading.value = true;
    return delOssConfig(ossConfigIds);
  }).then(() => {
    loading.value = false;
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).finally(() => {
    loading.value = false;
  });
const handleDelete = async (row?: OssConfigVO) => {
  const ossConfigIds = row?.ossConfigId || ids.value;
  await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
  loading.value = true;
  await delOssConfig(ossConfigIds).finally(() => loading.value = false);
  getList();
  proxy?.$modal.msgSuccess("删除成功");
}
getList();
onMounted(() => {
  getList();
})
</script>
<template>
    <div class="p-2">
        <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
            <div class="search" v-show="showSearch">
                <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
                    <el-form-item label="配置key" prop="configKey">
                        <el-input v-model="queryParams.configKey" placeholder="配置key" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="桶名称" prop="bucketName">
                        <el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
                    </el-form-item>
                    <el-form-item label="是否默认" prop="status">
                        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
                            <el-option key="0" label="是" value="0" />
                            <el-option key="1" label="否" value="1" />
                        </el-select>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
                        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
                    </el-form-item>
                </el-form>
            </div>
        </transition>
        <el-card shadow="never">
            <template #header>
                <el-row :gutter="10" class="mb8">
                    <el-col :span="1.5">
                        <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:oss:add']">新增</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:oss:edit']">修改</el-button>
                    </el-col>
                    <el-col :span="1.5">
                        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:oss:remove']">
                            åˆ é™¤
                        </el-button>
                    </el-col>
                    <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
                </el-row>
            </template>
            <el-table v-loading="loading" :data="ossConfigList" @selection-change="handleSelectionChange">
                <el-table-column type="selection" width="55" align="center" />
                <el-table-column label="主建" align="center" prop="ossConfigId" v-if="columns[0].visible" />
                <el-table-column label="配置key" align="center" prop="configKey" v-if="columns[1].visible" />
                <el-table-column label="访问站点" align="center" prop="endpoint" v-if="columns[2].visible" width="200" />
                <el-table-column label="自定义域名" align="center" prop="domain" v-if="columns[3].visible" width="200" />
                <el-table-column label="桶名称" align="center" prop="bucketName" v-if="columns[4].visible" />
                <el-table-column label="前缀" align="center" prop="prefix" v-if="columns[5].visible" />
                <el-table-column label="域" align="center" prop="region" v-if="columns[6].visible" />
                <el-table-column label="桶权限类型" align="center" prop="accessPolicy" v-if="columns[7].visible">
                    <template #default="scope">
                        <el-tag type="warning" v-if="scope.row.accessPolicy === '0'">private</el-tag>
                        <el-tag type="success" v-if="scope.row.accessPolicy === '1'">public</el-tag>
                        <el-tag type="info" v-if="scope.row.accessPolicy === '2'">custom</el-tag>
                    </template>
                </el-table-column>
                <el-table-column label="是否默认" align="center" prop="status" v-if="columns[8].visible">
                    <template #default="scope">
                        <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
                    </template>
                </el-table-column>
                <el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
                    <template #default="scope">
                        <el-tooltip content="修改" placement="top">
                            <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:oss:edit']"></el-button>
                        </el-tooltip>
                        <el-tooltip content="删除" placement="top">
                            <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']"></el-button>
                        </el-tooltip>
                    </template>
                </el-table-column>
            </el-table>
            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
        </el-card>
        <!-- æ·»åŠ æˆ–ä¿®æ”¹å¯¹è±¡å­˜å‚¨é…ç½®å¯¹è¯æ¡† -->
        <el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
            <el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
                <el-form-item label="配置key" prop="configKey">
                    <el-input v-model="form.configKey" placeholder="请输入配置key" />
                </el-form-item>
                <el-form-item label="访问站点" prop="endpoint">
                    <el-input v-model="form.endpoint" placeholder="请输入访问站点" />
                </el-form-item>
                <el-form-item label="自定义域名" prop="domain">
                    <el-input v-model="form.domain" placeholder="请输入自定义域名" />
                </el-form-item>
                <el-form-item label="accessKey" prop="accessKey">
                    <el-input v-model="form.accessKey" placeholder="请输入accessKey" />
                </el-form-item>
                <el-form-item label="secretKey" prop="secretKey">
                    <el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
                </el-form-item>
                <el-form-item label="桶名称" prop="bucketName">
                    <el-input v-model="form.bucketName" placeholder="请输入桶名称" />
                </el-form-item>
                <el-form-item label="前缀" prop="prefix">
                    <el-input v-model="form.prefix" placeholder="请输入前缀" />
                </el-form-item>
                <el-form-item label="是否HTTPS">
                    <el-radio-group v-model="form.isHttps">
                        <el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="桶权限类型">
                    <el-radio-group v-model="form.accessPolicy">
                        <el-radio label="0">private</el-radio>
                        <el-radio label="1">public</el-radio>
                        <el-radio label="2">custom</el-radio>
                    </el-radio-group>
                </el-form-item>
                <el-form-item label="域" prop="region">
                    <el-input v-model="form.region" placeholder="请输入域" />
                </el-form-item>
                <el-form-item label="备注" prop="remark">
                    <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
                </el-form-item>
            </el-form>
            <template #footer>
                <div class="dialog-footer">
                    <el-button :loading="buttonLoading" type="primary" @click="submitForm">ç¡® å®š</el-button>
                    <el-button @click="cancel">取 æ¶ˆ</el-button>
                </div>
            </template>
        </el-dialog>
    </div>
</template>
在上述文件截断后对比
src/views/system/oss/index.vue src/views/system/post/index.vue src/views/system/role/authUser.vue src/views/system/role/index.vue src/views/system/role/selectUser.vue src/views/system/tenant/index.vue src/views/system/tenantPackage/index.vue src/views/system/user/authRole.vue src/views/system/user/index.vue src/views/system/user/profile/index.vue src/views/system/user/profile/resetPwd.vue src/views/system/user/profile/userAvatar.vue src/views/system/user/profile/userInfo.vue src/views/tool/build/index.vue src/views/tool/gen/basicInfoForm.vue src/views/tool/gen/editTable.vue src/views/tool/gen/genInfoForm.vue src/views/tool/gen/importTable.vue src/views/tool/gen/index.vue tsconfig.json vite.config.js (已删除) vite.config.ts vite/plugins/auto-import.js (已删除) vite/plugins/compression.js (已删除) vite/plugins/index.js (已删除) vite/plugins/setup-extend.js (已删除) vite/plugins/svg-icon.js (已删除)