From 488206779f4c29afb5a26a7952a2228cf33f1be9 Mon Sep 17 00:00:00 2001 From: alex_q <914269@qq.com> Date: Fri, 12 Jun 2026 17:24:25 +0800 Subject: [PATCH] Initial commit --- .editorconfig | 15 + .env.development | 17 + .env.production | 8 + .eslintrc-auto-import.json | 316 +++ .gitignore | 20 + .husky/commit-msg | 1 + .husky/pre-commit | 1 + .prettierignore | 12 + .prettierrc.yaml | 41 + .stylelintignore | 11 + .stylelintrc.cjs | 38 + .vscode/extensions.json | 11 + .vscode/settings.json | 81 + .vscode/vue3.0.code-snippets | 23 + .vscode/vue3.2.code-snippets | 17 + .vscode/vue3.3.code-snippets | 21 + CHANGELOG.md | 386 +++ README.en-US.md | 222 ++ README.md | 221 ++ commitlint.config.cjs | 93 + eslint.config.ts | 176 ++ index.html | 90 + mock/auth.mock.ts | 44 + mock/base.ts | 7 + mock/dept.mock.ts | 153 ++ mock/dict.mock.ts | 312 +++ mock/log.mock.ts | 207 ++ mock/menu.mock.ts | 2082 +++++++++++++++++ mock/notice.mock.ts | 416 ++++ mock/role.mock.ts | 334 +++ mock/user.mock.ts | 251 ++ package.json | 128 + public/favicon.ico | Bin 0 -> 3439 bytes .../710b7a81cbe748f28bfa10c44382fd67.jpg | Bin 0 -> 20291 bytes .../332f8fa543dc4f7f8262e05aae95a110.jpg | Bin 0 -> 19961 bytes .../4d806fda9d0f48c289028aef9c7e7e57.jpg | Bin 0 -> 20291 bytes .../de041f1795904b6c8a4e97ffb3072fb4.jpg | Bin 0 -> 19961 bytes public/files/default.gif | Bin 0 -> 6334 bytes src/App.vue | 32 + src/api/auth.api.ts | 86 + src/api/codegen.api.ts | 191 ++ src/api/file.api.ts | 83 + src/api/rnb/index.ts | 254 ++ src/api/system/config.api.ts | 95 + src/api/system/dept.api.ts | 130 + src/api/system/dict.api.ts | 310 +++ src/api/system/log.api.ts | 121 + src/api/system/menu.api.ts | 209 ++ src/api/system/notice.api.ts | 199 ++ src/api/system/role.api.ts | 138 ++ src/api/system/user.api.ts | 384 +++ src/assets/icons/api.svg | 1 + src/assets/icons/backtop.svg | 1 + src/assets/icons/bilibili.svg | 1 + src/assets/icons/browser.svg | 1 + src/assets/icons/captcha.svg | 1 + src/assets/icons/cascader.svg | 1 + src/assets/icons/client.svg | 1 + src/assets/icons/close.svg | 1 + src/assets/icons/close_all.svg | 1 + src/assets/icons/close_left.svg | 1 + src/assets/icons/close_other.svg | 1 + src/assets/icons/close_right.svg | 1 + src/assets/icons/cnblogs.svg | 1 + src/assets/icons/code.svg | 1 + src/assets/icons/collapse.svg | 1 + src/assets/icons/csdn.svg | 6 + src/assets/icons/dict.svg | 1 + src/assets/icons/document.svg | 1 + src/assets/icons/down.svg | 1 + src/assets/icons/download.svg | 1 + src/assets/icons/enter.svg | 1 + src/assets/icons/esc.svg | 1 + src/assets/icons/file.svg | 1 + src/assets/icons/fullscreen-exit.svg | 1 + src/assets/icons/fullscreen.svg | 1 + src/assets/icons/gitcode.svg | 1 + src/assets/icons/gitee.svg | 1 + src/assets/icons/github.svg | 1 + src/assets/icons/homepage.svg | 1 + src/assets/icons/java.svg | 1 + src/assets/icons/juejin.svg | 1 + src/assets/icons/language.svg | 1 + src/assets/icons/menu.svg | 1 + src/assets/icons/message.svg | 1 + src/assets/icons/monitor.svg | 1 + src/assets/icons/project.svg | 1 + src/assets/icons/qq.svg | 1 + src/assets/icons/refresh.svg | 1 + src/assets/icons/role.svg | 1 + src/assets/icons/search.svg | 1 + src/assets/icons/setting.svg | 1 + src/assets/icons/size.svg | 1 + src/assets/icons/system.svg | 1 + src/assets/icons/table.svg | 1 + src/assets/icons/todo.svg | 1 + src/assets/icons/tree.svg | 1 + src/assets/icons/typescript.svg | 1 + src/assets/icons/up.svg | 1 + src/assets/icons/user.svg | 1 + src/assets/icons/visitor.svg | 1 + src/assets/icons/vue.svg | 1 + src/assets/icons/wechat.svg | 1 + src/assets/icons/xml.svg | 1 + src/assets/images/401.svg | 398 ++++ src/assets/images/404.svg | 340 +++ src/assets/images/login-bg.svg | 71 + src/assets/logo.png | Bin 0 -> 38468 bytes src/components/AppLink/index.vue | 38 + src/components/Breadcrumb/index.vue | 85 + src/components/CURD/PageContent.vue | 934 ++++++++ src/components/CURD/PageModal.vue | 271 +++ src/components/CURD/PageSearch.vue | 161 ++ src/components/CURD/types.ts | 223 ++ src/components/CURD/usePage.ts | 105 + src/components/CommonWrapper/index.vue | 21 + src/components/CopyButton/index.vue | 62 + src/components/DarkModeSwitch/index.vue | 39 + src/components/Dict/DictLabel.vue | 66 + src/components/Dict/index.vue | 123 + src/components/ECharts/index.vue | 81 + src/components/Fullscreen/index.vue | 11 + src/components/GithubCorner/index.vue | 62 + src/components/Hamburger/index.vue | 63 + src/components/IconSelect/index.vue | 207 ++ src/components/InputTag/index.vue | 73 + src/components/LangSelect/index.vue | 49 + src/components/MenuSearch/index.vue | 521 +++++ src/components/NoticeDropdown/index.vue | 215 ++ src/components/Pagination/index.vue | 91 + src/components/SizeSelect/index.vue | 42 + src/components/TableSelect/index.vue | 357 +++ src/components/TextScroll/index.vue | 412 ++++ src/components/Upload/FileUpload.vue | 310 +++ src/components/Upload/MultiImageUpload.vue | 215 ++ src/components/Upload/SingleImageUpload.vue | 201 ++ src/components/WangEditor/index.vue | 87 + src/directive/index.ts | 9 + src/directive/permission/index.ts | 64 + src/enums/api/result.enum.ts | 23 + src/enums/codegen/form.enum.ts | 15 + src/enums/codegen/query.enum.ts | 37 + src/enums/index.ts | 11 + src/enums/settings/device.enum.ts | 14 + src/enums/settings/layout.enum.ts | 53 + src/enums/settings/locale.enum.ts | 14 + src/enums/settings/theme.enum.ts | 32 + src/enums/system/menu.enum.ts | 7 + src/hooks/index.ts | 7 + src/hooks/websocket/core/useStomp.ts | 312 +++ src/hooks/websocket/index.ts | 11 + src/hooks/websocket/services/useDictSync.ts | 197 ++ .../websocket/services/useOnlineCount.ts | 180 ++ src/lang/index.ts | 31 + src/lang/package/en.ts | 80 + src/lang/package/zh-cn.ts | 80 + src/layout/components/AppMain/index.vue | 36 + .../NavBar/components/NavbarRight.vue | 127 + src/layout/components/NavBar/index.vue | 40 + .../Settings/components/LayoutSelect.vue | 140 ++ src/layout/components/Settings/index.vue | 193 ++ .../Sidebar/components/SidebarLogo.vue | 53 + .../Sidebar/components/SidebarMenu.vue | 132 ++ .../Sidebar/components/SidebarMenuItem.vue | 201 ++ .../components/SidebarMenuItemTitle.vue | 53 + .../Sidebar/components/SidebarMixTopMenu.vue | 111 + src/layout/components/Sidebar/index.vue | 45 + src/layout/components/TagsView/index.vue | 540 +++++ src/layout/index.vue | 306 +++ src/main.ts | 21 + src/plugins/icons.ts | 9 + src/plugins/index.ts | 31 + src/plugins/permission.ts | 88 + src/router/index.ts | 73 + src/settings.ts | 37 + src/store/index.ts | 17 + src/store/modules/app.store.ts | 145 ++ src/store/modules/dict.store.ts | 72 + src/store/modules/permission.store.ts | 111 + src/store/modules/settings.store.ts | 97 + src/store/modules/tags-view.store.ts | 257 ++ src/store/modules/user.store.ts | 122 + src/styles/dark/css-vars.css | 7 + src/styles/index.scss | 64 + src/styles/reset.scss | 77 + src/styles/variables.module.scss | 11 + src/styles/variables.scss | 69 + src/types/auto-imports.d.ts | 991 ++++++++ src/types/components.d.ts | 100 + src/types/env.d.ts | 33 + src/types/global.d.ts | 109 + src/types/router.d.ts | 54 + src/types/shims-vue.d.ts | 5 + src/types/socket.d.ts | 6 + src/types/websocket.ts | 15 + src/utils/auth.ts | 27 + src/utils/i18n.ts | 12 + src/utils/index.ts | 58 + src/utils/nprogress.ts | 18 + src/utils/request.ts | 105 + src/utils/theme.ts | 106 + src/views/codegen/index.vue | 866 +++++++ src/views/dashboard/index.vue | 420 ++++ src/views/demo/api/apifox.vue | 27 + src/views/demo/curd/config/add.ts | 137 ++ src/views/demo/curd/config/content.ts | 137 ++ src/views/demo/curd/config/edit.ts | 127 + src/views/demo/curd/config/options.ts | 31 + src/views/demo/curd/config/search.ts | 63 + src/views/demo/curd/config2/add.ts | 55 + src/views/demo/curd/config2/content.ts | 136 ++ src/views/demo/curd/config2/edit.ts | 105 + src/views/demo/curd/config2/search.ts | 152 ++ src/views/demo/curd/index.vue | 213 ++ src/views/demo/dict-sync.vue | 308 +++ src/views/demo/dictionary.vue | 48 + src/views/demo/drag.vue | 123 + src/views/demo/icon-selector.vue | 21 + src/views/demo/icons.vue | 129 + src/views/demo/internal-doc.vue | 25 + .../children/children/level3-1.vue | 5 + .../children/children/level3-2.vue | 5 + .../demo/multi-level/children/level2.vue | 7 + src/views/demo/multi-level/level1.vue | 16 + src/views/demo/route-param.vue | 16 + src/views/demo/signature.vue | 185 ++ src/views/demo/table-select/config/select.ts | 121 + src/views/demo/table-select/index.vue | 54 + src/views/demo/text-scroll.vue | 30 + src/views/demo/upload.vue | 40 + src/views/demo/wang-editor.vue | 24 + src/views/demo/websocket.vue | 246 ++ src/views/error/401.vue | 25 + src/views/error/404.vue | 59 + src/views/login/components/Login.vue | 220 ++ src/views/login/components/Register.vue | 203 ++ src/views/login/components/ResetPwd.vue | 57 + src/views/login/index.vue | 85 + src/views/profile/index.vue | 591 +++++ src/views/redirect/index.vue | 15 + src/views/rnb/accounts.vue | 633 +++++ src/views/rnb/amount/amoRanges.vue | 167 ++ src/views/rnb/amount/amoReports.vue | 152 ++ src/views/rnb/amount/amoUsers.vue | 165 ++ src/views/rnb/extends.vue | 372 +++ src/views/rnb/oplogs.vue | 222 ++ src/views/rnb/software/softwareList.vue | 308 +++ src/views/system/config/index.vue | 293 +++ src/views/system/dept/index.vue | 320 +++ src/views/system/dict/dict-item.vue | 289 +++ src/views/system/dict/index.vue | 300 +++ src/views/system/log/index.vue | 118 + src/views/system/menu/index.vue | 547 +++++ .../system/notice/components/MyNotice.vue | 204 ++ src/views/system/notice/index.vue | 472 ++++ src/views/system/role/index.vue | 474 ++++ src/views/system/user/components/DeptTree.vue | 70 + .../system/user/components/UserImport.vue | 199 ++ src/views/system/user/index.vue | 491 ++++ tsconfig.json | 35 + uno.config.ts | 83 + vite.config.ts | 234 ++ 262 files changed, 31355 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.development create mode 100644 .env.production create mode 100644 .eslintrc-auto-import.json create mode 100644 .gitignore create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 .prettierignore create mode 100644 .prettierrc.yaml create mode 100644 .stylelintignore create mode 100644 .stylelintrc.cjs create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/vue3.0.code-snippets create mode 100644 .vscode/vue3.2.code-snippets create mode 100644 .vscode/vue3.3.code-snippets create mode 100644 CHANGELOG.md create mode 100644 README.en-US.md create mode 100644 README.md create mode 100644 commitlint.config.cjs create mode 100644 eslint.config.ts create mode 100644 index.html create mode 100644 mock/auth.mock.ts create mode 100644 mock/base.ts create mode 100644 mock/dept.mock.ts create mode 100644 mock/dict.mock.ts create mode 100644 mock/log.mock.ts create mode 100644 mock/menu.mock.ts create mode 100644 mock/notice.mock.ts create mode 100644 mock/role.mock.ts create mode 100644 mock/user.mock.ts create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/files/20250716/710b7a81cbe748f28bfa10c44382fd67.jpg create mode 100644 public/files/20250717/332f8fa543dc4f7f8262e05aae95a110.jpg create mode 100644 public/files/20250717/4d806fda9d0f48c289028aef9c7e7e57.jpg create mode 100644 public/files/20250717/de041f1795904b6c8a4e97ffb3072fb4.jpg create mode 100644 public/files/default.gif create mode 100644 src/App.vue create mode 100644 src/api/auth.api.ts create mode 100644 src/api/codegen.api.ts create mode 100644 src/api/file.api.ts create mode 100644 src/api/rnb/index.ts create mode 100644 src/api/system/config.api.ts create mode 100644 src/api/system/dept.api.ts create mode 100644 src/api/system/dict.api.ts create mode 100644 src/api/system/log.api.ts create mode 100644 src/api/system/menu.api.ts create mode 100644 src/api/system/notice.api.ts create mode 100644 src/api/system/role.api.ts create mode 100644 src/api/system/user.api.ts create mode 100644 src/assets/icons/api.svg create mode 100644 src/assets/icons/backtop.svg create mode 100644 src/assets/icons/bilibili.svg create mode 100644 src/assets/icons/browser.svg create mode 100644 src/assets/icons/captcha.svg create mode 100644 src/assets/icons/cascader.svg create mode 100644 src/assets/icons/client.svg create mode 100644 src/assets/icons/close.svg create mode 100644 src/assets/icons/close_all.svg create mode 100644 src/assets/icons/close_left.svg create mode 100644 src/assets/icons/close_other.svg create mode 100644 src/assets/icons/close_right.svg create mode 100644 src/assets/icons/cnblogs.svg create mode 100644 src/assets/icons/code.svg create mode 100644 src/assets/icons/collapse.svg create mode 100644 src/assets/icons/csdn.svg create mode 100644 src/assets/icons/dict.svg create mode 100644 src/assets/icons/document.svg create mode 100644 src/assets/icons/down.svg create mode 100644 src/assets/icons/download.svg create mode 100644 src/assets/icons/enter.svg create mode 100644 src/assets/icons/esc.svg create mode 100644 src/assets/icons/file.svg create mode 100644 src/assets/icons/fullscreen-exit.svg create mode 100644 src/assets/icons/fullscreen.svg create mode 100644 src/assets/icons/gitcode.svg create mode 100644 src/assets/icons/gitee.svg create mode 100644 src/assets/icons/github.svg create mode 100644 src/assets/icons/homepage.svg create mode 100644 src/assets/icons/java.svg create mode 100644 src/assets/icons/juejin.svg create mode 100644 src/assets/icons/language.svg create mode 100644 src/assets/icons/menu.svg create mode 100644 src/assets/icons/message.svg create mode 100644 src/assets/icons/monitor.svg create mode 100644 src/assets/icons/project.svg create mode 100644 src/assets/icons/qq.svg create mode 100644 src/assets/icons/refresh.svg create mode 100644 src/assets/icons/role.svg create mode 100644 src/assets/icons/search.svg create mode 100644 src/assets/icons/setting.svg create mode 100644 src/assets/icons/size.svg create mode 100644 src/assets/icons/system.svg create mode 100644 src/assets/icons/table.svg create mode 100644 src/assets/icons/todo.svg create mode 100644 src/assets/icons/tree.svg create mode 100644 src/assets/icons/typescript.svg create mode 100644 src/assets/icons/up.svg create mode 100644 src/assets/icons/user.svg create mode 100644 src/assets/icons/visitor.svg create mode 100644 src/assets/icons/vue.svg create mode 100644 src/assets/icons/wechat.svg create mode 100644 src/assets/icons/xml.svg create mode 100644 src/assets/images/401.svg create mode 100644 src/assets/images/404.svg create mode 100644 src/assets/images/login-bg.svg create mode 100644 src/assets/logo.png create mode 100644 src/components/AppLink/index.vue create mode 100644 src/components/Breadcrumb/index.vue create mode 100644 src/components/CURD/PageContent.vue create mode 100644 src/components/CURD/PageModal.vue create mode 100644 src/components/CURD/PageSearch.vue create mode 100644 src/components/CURD/types.ts create mode 100644 src/components/CURD/usePage.ts create mode 100644 src/components/CommonWrapper/index.vue create mode 100644 src/components/CopyButton/index.vue create mode 100644 src/components/DarkModeSwitch/index.vue create mode 100644 src/components/Dict/DictLabel.vue create mode 100644 src/components/Dict/index.vue create mode 100644 src/components/ECharts/index.vue create mode 100644 src/components/Fullscreen/index.vue create mode 100644 src/components/GithubCorner/index.vue create mode 100644 src/components/Hamburger/index.vue create mode 100644 src/components/IconSelect/index.vue create mode 100644 src/components/InputTag/index.vue create mode 100644 src/components/LangSelect/index.vue create mode 100644 src/components/MenuSearch/index.vue create mode 100644 src/components/NoticeDropdown/index.vue create mode 100644 src/components/Pagination/index.vue create mode 100644 src/components/SizeSelect/index.vue create mode 100644 src/components/TableSelect/index.vue create mode 100644 src/components/TextScroll/index.vue create mode 100644 src/components/Upload/FileUpload.vue create mode 100644 src/components/Upload/MultiImageUpload.vue create mode 100644 src/components/Upload/SingleImageUpload.vue create mode 100644 src/components/WangEditor/index.vue create mode 100644 src/directive/index.ts create mode 100644 src/directive/permission/index.ts create mode 100644 src/enums/api/result.enum.ts create mode 100644 src/enums/codegen/form.enum.ts create mode 100644 src/enums/codegen/query.enum.ts create mode 100644 src/enums/index.ts create mode 100644 src/enums/settings/device.enum.ts create mode 100644 src/enums/settings/layout.enum.ts create mode 100644 src/enums/settings/locale.enum.ts create mode 100644 src/enums/settings/theme.enum.ts create mode 100644 src/enums/system/menu.enum.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/websocket/core/useStomp.ts create mode 100644 src/hooks/websocket/index.ts create mode 100644 src/hooks/websocket/services/useDictSync.ts create mode 100644 src/hooks/websocket/services/useOnlineCount.ts create mode 100644 src/lang/index.ts create mode 100644 src/lang/package/en.ts create mode 100644 src/lang/package/zh-cn.ts create mode 100644 src/layout/components/AppMain/index.vue create mode 100644 src/layout/components/NavBar/components/NavbarRight.vue create mode 100644 src/layout/components/NavBar/index.vue create mode 100644 src/layout/components/Settings/components/LayoutSelect.vue create mode 100644 src/layout/components/Settings/index.vue create mode 100644 src/layout/components/Sidebar/components/SidebarLogo.vue create mode 100644 src/layout/components/Sidebar/components/SidebarMenu.vue create mode 100644 src/layout/components/Sidebar/components/SidebarMenuItem.vue create mode 100644 src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue create mode 100644 src/layout/components/Sidebar/components/SidebarMixTopMenu.vue create mode 100644 src/layout/components/Sidebar/index.vue create mode 100644 src/layout/components/TagsView/index.vue create mode 100644 src/layout/index.vue create mode 100644 src/main.ts create mode 100644 src/plugins/icons.ts create mode 100644 src/plugins/index.ts create mode 100644 src/plugins/permission.ts create mode 100644 src/router/index.ts create mode 100644 src/settings.ts create mode 100644 src/store/index.ts create mode 100644 src/store/modules/app.store.ts create mode 100644 src/store/modules/dict.store.ts create mode 100644 src/store/modules/permission.store.ts create mode 100644 src/store/modules/settings.store.ts create mode 100644 src/store/modules/tags-view.store.ts create mode 100644 src/store/modules/user.store.ts create mode 100644 src/styles/dark/css-vars.css create mode 100644 src/styles/index.scss create mode 100644 src/styles/reset.scss create mode 100644 src/styles/variables.module.scss create mode 100644 src/styles/variables.scss create mode 100644 src/types/auto-imports.d.ts create mode 100644 src/types/components.d.ts create mode 100644 src/types/env.d.ts create mode 100644 src/types/global.d.ts create mode 100644 src/types/router.d.ts create mode 100644 src/types/shims-vue.d.ts create mode 100644 src/types/socket.d.ts create mode 100644 src/types/websocket.ts create mode 100644 src/utils/auth.ts create mode 100644 src/utils/i18n.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/nprogress.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/theme.ts create mode 100644 src/views/codegen/index.vue create mode 100644 src/views/dashboard/index.vue create mode 100644 src/views/demo/api/apifox.vue create mode 100644 src/views/demo/curd/config/add.ts create mode 100644 src/views/demo/curd/config/content.ts create mode 100644 src/views/demo/curd/config/edit.ts create mode 100644 src/views/demo/curd/config/options.ts create mode 100644 src/views/demo/curd/config/search.ts create mode 100644 src/views/demo/curd/config2/add.ts create mode 100644 src/views/demo/curd/config2/content.ts create mode 100644 src/views/demo/curd/config2/edit.ts create mode 100644 src/views/demo/curd/config2/search.ts create mode 100644 src/views/demo/curd/index.vue create mode 100644 src/views/demo/dict-sync.vue create mode 100644 src/views/demo/dictionary.vue create mode 100644 src/views/demo/drag.vue create mode 100644 src/views/demo/icon-selector.vue create mode 100644 src/views/demo/icons.vue create mode 100644 src/views/demo/internal-doc.vue create mode 100644 src/views/demo/multi-level/children/children/level3-1.vue create mode 100644 src/views/demo/multi-level/children/children/level3-2.vue create mode 100644 src/views/demo/multi-level/children/level2.vue create mode 100644 src/views/demo/multi-level/level1.vue create mode 100644 src/views/demo/route-param.vue create mode 100644 src/views/demo/signature.vue create mode 100644 src/views/demo/table-select/config/select.ts create mode 100644 src/views/demo/table-select/index.vue create mode 100644 src/views/demo/text-scroll.vue create mode 100644 src/views/demo/upload.vue create mode 100644 src/views/demo/wang-editor.vue create mode 100644 src/views/demo/websocket.vue create mode 100644 src/views/error/401.vue create mode 100644 src/views/error/404.vue create mode 100644 src/views/login/components/Login.vue create mode 100644 src/views/login/components/Register.vue create mode 100644 src/views/login/components/ResetPwd.vue create mode 100644 src/views/login/index.vue create mode 100644 src/views/profile/index.vue create mode 100644 src/views/redirect/index.vue create mode 100644 src/views/rnb/accounts.vue create mode 100644 src/views/rnb/amount/amoRanges.vue create mode 100644 src/views/rnb/amount/amoReports.vue create mode 100644 src/views/rnb/amount/amoUsers.vue create mode 100644 src/views/rnb/extends.vue create mode 100644 src/views/rnb/oplogs.vue create mode 100644 src/views/rnb/software/softwareList.vue create mode 100644 src/views/system/config/index.vue create mode 100644 src/views/system/dept/index.vue create mode 100644 src/views/system/dict/dict-item.vue create mode 100644 src/views/system/dict/index.vue create mode 100644 src/views/system/log/index.vue create mode 100644 src/views/system/menu/index.vue create mode 100644 src/views/system/notice/components/MyNotice.vue create mode 100644 src/views/system/notice/index.vue create mode 100644 src/views/system/role/index.vue create mode 100644 src/views/system/user/components/DeptTree.vue create mode 100644 src/views/system/user/components/UserImport.vue create mode 100644 src/views/system/user/index.vue create mode 100644 tsconfig.json create mode 100644 uno.config.ts create mode 100644 vite.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..00ee2de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org +root = true + +# 表示所有文件适用 +[*] +charset = utf-8 # 设置文件字符集为 utf-8 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +insert_final_newline = true # 始终在文件末尾插入一个新行 + +# 表示仅 md 文件适用以下规则 +[*.md] +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..b87bd81 --- /dev/null +++ b/.env.development @@ -0,0 +1,17 @@ +# 应用端口 +VITE_APP_PORT=3000 + +# 代理前缀 +VITE_APP_BASE_API=/dev-api + +# 接口地址 +VITE_APP_API_URL=http://localhost:10800 +# VITE_APP_API_URL=http://23.236.66.46:88 # 线上环境 + +# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws +VITE_APP_WS_ENDPOINT= + +# 启用 Mock 服务 +VITE_MOCK_DEV_SERVER=false + +VITE_BASE_FILE_URL=\files diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..61ed25f --- /dev/null +++ b/.env.production @@ -0,0 +1,8 @@ +# 代理前缀 +VITE_APP_BASE_API='/prod-api' + + +# WebSocket端点(可选) +#VITE_APP_WS_ENDPOINT=wss://api.youlai.tech/ws + +VITE_BASE_FILE_URL=/files diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json new file mode 100644 index 0000000..0e8546f --- /dev/null +++ b/.eslintrc-auto-import.json @@ -0,0 +1,316 @@ +{ + "globals": { + "Component": true, + "ComponentPublicInstance": true, + "ComputedRef": true, + "EffectScope": true, + "ElMessage": true, + "ElMessageBox": true, + "ElNotification": true, + "InjectionKey": true, + "PropType": true, + "Ref": true, + "VNode": 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, + "createReusableTemplate": true, + "createSharedComposable": true, + "createTemplatePromise": 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, + "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, + "toValue": true, + "triggerRef": true, + "tryOnBeforeMount": true, + "tryOnBeforeUnmount": true, + "tryOnMounted": true, + "tryOnScopeDispose": true, + "tryOnUnmounted": true, + "unref": true, + "unrefElement": true, + "until": true, + "useActiveElement": true, + "useAnimate": true, + "useArrayDifference": true, + "useArrayEvery": true, + "useArrayFilter": true, + "useArrayFind": true, + "useArrayFindIndex": true, + "useArrayFindLast": true, + "useArrayIncludes": 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, + "useParentElement": true, + "usePerformanceObserver": 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, + "watchDeep": true, + "watchEffect": true, + "watchIgnorable": true, + "watchImmediate": true, + "watchOnce": true, + "watchPausable": true, + "watchPostEffect": true, + "watchSyncEffect": true, + "watchThrottled": true, + "watchTriggerable": true, + "watchWithFilter": true, + "useRoute": true, + "useRouter": true, + "storeToRefs": true, + "whenever": true, + "DirectiveBinding": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, + "MaybeRef": true, + "MaybeRefOrGetter": true, + "WritableComputedRef": true, + "acceptHMRUpdate": true, + "createPinia": true, + "defineStore": true, + "getActivePinia": true, + "injectLocal": true, + "mapActions": true, + "mapGetters": true, + "mapState": true, + "mapStores": true, + "mapWritableState": true, + "onBeforeRouteLeave": true, + "onBeforeRouteUpdate": true, + "onWatcherCleanup": true, + "provideLocal": true, + "setActivePinia": true, + "setMapStoreSuffix": true, + "useClipboardItems": true, + "useI18n": true, + "useId": true, + "useLink": true, + "useModel": true, + "useTemplateRef": true + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6d77ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +.history + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +stats.html +pnpm-lock.yaml +package-lock.json +.stylelintcache +.eslintcache diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..fd2bf70 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no-install commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..b18d041 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run lint:lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..44421a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +dist +node_modules +public +.husky +.vscode +.idea +*.sh +*.md + +src/assets +stats.html +pnpm-lock.yaml diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..d9cf0c7 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,41 @@ +# 在单参数箭头函数中始终添加括号 +arrowParens: "always" +# JSX 多行元素的闭合标签另起一行 +bracketSameLine: false +# 对象字面量中的括号之间添加空格 +bracketSpacing: true +# 自动格式化嵌入的代码(如 Markdown 和 HTML 内的代码) +embeddedLanguageFormatting: "auto" +# 忽略 HTML 空白敏感度,将空白视为非重要内容 +htmlWhitespaceSensitivity: "ignore" +# 不插入 @prettier 的 pragma 注释 +insertPragma: false +# 在 JSX 中使用双引号 +jsxSingleQuote: false +# 每行代码的最大长度限制为 100 字符 +printWidth: 100 +# 在 Markdown 中保留原有的换行格式 +proseWrap: "preserve" +# 仅在必要时添加对象属性的引号 +quoteProps: "as-needed" +# 不要求文件开头插入 @prettier 的 pragma 注释 +requirePragma: false +# 在语句末尾添加分号 +semi: true +# 使用双引号而不是单引号 +singleQuote: false +# 缩进使用 2 个空格 +tabWidth: 2 +# 在多行元素的末尾添加逗号(ES5 支持的对象、数组等) +trailingComma: "es5" +# 使用空格而不是制表符缩进 +useTabs: false +# Vue 文件中的 ", + "", + "", + "" + ], + "description": "Vue3.0" + } +} diff --git a/.vscode/vue3.2.code-snippets b/.vscode/vue3.2.code-snippets new file mode 100644 index 0000000..a083940 --- /dev/null +++ b/.vscode/vue3.2.code-snippets @@ -0,0 +1,17 @@ +{ + "Vue3.2+快速生成模板": { + "scope": "vue", + "prefix": "Vue3.2+", + "body": [ + "", + "", + "", + "", + "", + "" + ], + "description": "Vue3.2+" + } +} diff --git a/.vscode/vue3.3.code-snippets b/.vscode/vue3.3.code-snippets new file mode 100644 index 0000000..705e04f --- /dev/null +++ b/.vscode/vue3.3.code-snippets @@ -0,0 +1,21 @@ +{ + "Vue3.3+defineOptions快速生成模板": { + "scope": "vue", + "prefix": "Vue3.3+", + "body": [ + "", + "", + "", + "", + "", + "" + ], + "description": "Vue3.3+defineOptions快速生成模板" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aef3f6d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,386 @@ + +# 2.11.5 (2024/6/18) + +## ✨ feat + +- 支持后端文件导入([#142](https://github.com/youlaitech/vue3-element-admin/pull/142)) [@cshaptx4869](https://github.com/cshaptx4869) + + +## 🐛 fix +- vue-dev-tools 插件导致菜单路由切换卡死,暂时关闭 ([28349e](https://github.com/youlaitech/vue3-element-admin/commit/28349efe147afab36531ba148eaac3a448fe6c71)) [@haoxianrui](https://github.com/haoxianrui) + + + +# 2.11.4 (2024/6/16) + +## ✨ feat + +- 操作栏增加render配置参数([#138](https://github.com/youlaitech/vue3-element-admin/pull/140)) [@cshaptx4869](https://github.com/cshaptx4869) +- 左侧工具栏增加type配置参数([#141](https://github.com/youlaitech/vue3-element-admin/pull/141)) [@diamont1001](https://github.com/diamont1001) + +## ♻️ refactor +- 更换权限分配弹窗类型为 drawer 并添加父子联动开关([2d9193](https://github.com/youlaitech/vue3-element-admin/commit/2d9193c47fd224f01f82b9c0b2bbeb5e7cb33584)) [@haoxianrui](https://github.com/haoxianrui) + + + +# 2.11.3 (2024/6/11) + +## ✨ feat + +- 支持默认工具栏的导入([#138](https://github.com/youlaitech/vue3-element-admin/pull/138)) [@cshaptx4869](https://github.com/cshaptx4869) +- 添加CURD导入示例([19e7bb](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869) + +## ♻️ refactor +- 修改导出全量数据选项文本([904fec](https://github.com/youlaitech/vue3-element-admin/commit/904fecad65217650482fcdbb10ffb7f3d27eb9ea)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 🐛 fix +- 菜单列表未适配el-icon导致图标不显示问题修复([e72b68](https://github.com/youlaitech/vue3-element-admin/commit/e72b68337562b5a7ea24ad55bbe00023e1266b40)) [@haoxianrui](https://github.com/haoxianrui) + +# 2.11.2 (2024/6/8) + +## ✨ feat + +- 支持表格远程筛选([#131](https://github.com/youlaitech/vue3-element-admin/pull/131)) [@cshaptx4869](https://github.com/cshaptx4869) +- 支持标签输入框([#132](https://github.com/youlaitech/vue3-element-admin/pull/132)) [@cshaptx4869](https://github.com/cshaptx4869) +- 表单项支持tips配置([#133](https://github.com/youlaitech/vue3-element-admin/pull/133)) [@cshaptx4869](https://github.com/cshaptx4869) +- 前端导出支持全量数据([#134](https://github.com/youlaitech/vue3-element-admin/pull/134)) [@cshaptx4869](https://github.com/cshaptx4869) +- 支持选中数据导出([#135](https://github.com/youlaitech/vue3-element-admin/pull/135)) [@cshaptx4869](https://github.com/cshaptx4869) +- 表格默认工具栏的导出、搜索按钮增加权限点控制([883128](https://github.com/youlaitech/vue3-element-admin/commit/8831289b655f2cc086ecdababaa89f8d8a087c42)) [@cshaptx4869](https://github.com/cshaptx4869) +- 页签title支持动态设置([23876a](https://github.com/youlaitech/vue3-element-admin/commit/23876aa396143bf77cb5c86af8d6023d9ff6555a)) [@haoxianrui](https://github.com/haoxianrui) + +## ♻️ refactor +- 默认工具栏支持自定义([#136](https://github.com/youlaitech/vue3-element-admin/pull/136)) [@cshaptx4869](https://github.com/cshaptx4869) +- 未配置全量导出接口时选项隐藏([eab91ef](https://github.com/youlaitech/vue3-element-admin/commit/eab91effd6a01d5a3d9257249c8d06aa252b3bf8)) [@cshaptx4869](https://github.com/cshaptx4869) + +## 🐛 fix +- 修复注销登出后redirect跳转路由参数丢失([5626017](https://github.com/youlaitech/vue3-element-admin/commit/562601736731afd20bb1a5140d856f6515720159)) [@haoxianrui](https://github.com/haoxianrui) + +# 2.11.1 (2024/6/6) + +## ✨ feat + +- 增加pagination、request、parseData配置参数([#119](https://github.com/youlaitech/vue3-element-admin/pull/119)) [@cshaptx4869](https://github.com/cshaptx4869) +- 增加返回顶部功能([#120](https://github.com/youlaitech/vue3-element-admin/pull/120)) [@cshaptx4869](https://github.com/cshaptx4869) +- 支持前端导出([#126](https://github.com/youlaitech/vue3-element-admin/pull/126)) [@cshaptx4869](https://github.com/cshaptx4869) + +## ♻️ refactor +- 重构布局样式(解决页面抖动问题)([#116](https://github.com/youlaitech/vue3-element-admin/pull/116)) [@cshaptx4869](https://github.com/cshaptx4869) +- 修改CURD示例编辑弹窗尺寸([#121](https://github.com/youlaitech/vue3-element-admin/pull/121)) [@cshaptx4869](https://github.com/cshaptx4869) +- 统一注册vue插件([#122](https://github.com/youlaitech/vue3-element-admin/pull/122)) [@cshaptx4869](https://github.com/cshaptx4869) +- 默认主题跟随系统([#128](https://github.com/youlaitech/vue3-element-admin/pull/128)) [@cshaptx4869](https://github.com/cshaptx4869) +- 增加"scss.lint.unknownAtRules": "ignore"代码,解决style中使用@apply提示unknow at rules@apply提示问题([Gitee#22](https://gitee.com/youlaiorg/vue3-element-admin/pulls/22)) [@zjsy521](https://gitee.com/zjsy521) + +## 🐛 fix +- 修复左侧布局移动端菜单弹出样式 ([#117](https://github.com/youlaitech/vue3-element-admin/pull/117)) [@cshaptx4869](https://github.com/cshaptx4869) + +- 修复编辑后未清空id再新增菜单覆盖的问题([0e78eeb](https://github.com/youlaitech/vue3-element-admin/commit/0e78eeb75008fa8e9732b1b4e7d7a1ea345c7a1b)) [@haoxianrui](https://github.com/haoxianrui) +- 修复水印层级问题([#123](https://github.com/youlaitech/vue3-element-admin/pull/123)) [@cshaptx4869](https://github.com/cshaptx4869) +- 修复混合布局样式问题([#124](https://github.com/youlaitech/vue3-element-admin/pull/124)) [@cshaptx4869](https://github.com/cshaptx4869) +- 修复关闭弹窗时没有clearValidate问题([#125](https://github.com/youlaitech/vue3-element-admin/pull/125)) [@andm31](https://github.com/andm31) + + + +# 2.11.0 (2024/5/27) + +## ✨ feat +- 菜单添加路由参数设置(author by [haoxianrui](https://github.com/haoxianrui)) +- 增加列表选择组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加列表选择组件使用示例(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加defaultToolbar配置参数(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 表单弹窗支持drawer模式(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 表单项增加computed和watchEffect配置(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 支持switch属性修改(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 表单项增加文本类型支持(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 列表列增加show配置项(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 支持搜索表单显隐控制(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 支持input属性修改(author by [cshaptx4869](https://github.com/cshaptx4869)) +- search配置新增函数能力拓展(author by [xiudaozhe](https://github.com/xiudaozhe)) +- 表格新增列设置控制(author by [haoxianrui](https://github.com/haoxianrui)) +- 搜索添加展开和收缩(author by [haoxianrui](https://github.com/haoxianrui)) +- watch函数增加配置项参数返回(author by [cshaptx4869](https://github.com/cshaptx4869)) + +## ♻️ refactor +- 重构图标选择组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 重构列表选择组件默认样式 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 加强对话框表单组件和列表选择组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- routeMeta增加alwaysShow字段声明(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 分页组件增加溢出滚动效果(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修正登录表单的Ref类型(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 点击表格刷新按钮不重置页码(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 筛选列超出一定高度滚动(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化加强initFn函数,表单项增加initFn函数(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 重构watch、computed、watchEffect调用(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改操作成功提示(author by [cshaptx4869](https://github.com/cshaptx4869)) +- PageSearch 改用card作为容器,样式改用unocss写法(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化首页 loading 动画效果author by [haoxianrui](https://github.com/haoxianrui)) + + +## 🐛 fix +- 路由是否始终显示不限制只有顶级目录才有的配置,开放至菜单 (author by [haoxianrui](https://github.com/haoxianrui)) +- sockjs-client 报错 global is not defined 导致开发环境无法打开 WebSocket 页面问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 发送用户重启密码功能,最少为6位字符(小于6位登陆时不允许的问题) (author by [dreamnyj](https://gitee.com/dreamnyj)) +- 修复系统设置面板滚动条问题(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修复表单插槽失效问题(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改tagsview刷新丢失query问题(author by [xiudaozhe](https://github.com/xiudaozhe)) + +## 📦️ build +- 升级 NPM 包版本至最新 (author by [haoxianrui](https://github.com/haoxianrui)) + +## ⚙️ ci +- 规整脚本执行命令(author by [cshaptx4869](https://github.com/cshaptx4869)) + + +# 2.10.1 (2024/5/4) + +## ♻️ refactor +- 抽离CURD的使用部分代码为Hooks实现(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改CURD导入权限点标识名(author by [cshaptx4869](https://github.com/cshaptx4869)) +- cURD表单字段支持watch监听(author by [cshaptx4869](https://github.com/cshaptx4869)) +- cURD表单input支持number修饰(author by [cshaptx4869](https://github.com/cshaptx4869)) +- cURD表单组件支持checkbox多选框(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化axios响应数据TS类型提示(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修改CURD表单组件自定义类型的attrs传值(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 同步重置密码按钮权限标识重命名(author by [haoxianrui](https://github.com/haoxianrui)) +- 重构API为静态方法实现模块化管理,并将types.ts重命名为model.ts用于存放接口模型定义(author by [haoxianrui](https://github.com/haoxianrui)) + + +## 🐛 fix +- sockjs-client 报错 global is not defined 导致开发环境无法打开 WebSocket 页面问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 主题颜色设置覆盖暗黑模式下el-table行激活的背景色问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 修复因API接口调整而影响的调用页面的问题 (author by [haoxianrui](https://github.com/haoxianrui)) + +## 📦️ build +- 升级 NPM 包版本至最新 (author by [haoxianrui](https://github.com/haoxianrui)) + + +# 2.10.0 (2024/4/26) +## ✨ feat +- 封装增删改查组件(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 集成 vite-plugin-vue-devtools 插件(author by [Tricker39](https://github.com/Tricker39)) +- 增加CURD配置化实现(author by [cshaptx4869](https://github.com/cshaptx4869)) + + +# 2.9.3 (2024/04/14) +## ✨ feat +- 增加vue文件代码片段(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 菜单 hover 背景色添加值全局SCSS变量进行控制(author by [haoxianrui](https://github.com/haoxianrui)) + +## ♻️ refactor +- 加强基础国际化(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加语言和布局大小枚举类型(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 增加侧边栏状态枚举类型(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 使用布局枚举替换字面量(author by [haoxianrui](https://github.com/haoxianrui)) +- 控制台使用静态数据循环渲染(author by [april](mailto:april@zen-game.cn)) +- 本地缓存的 token 变量重命名(author by [haoxianrui](https://github.com/haoxianrui)) +- 完善 Vite 环境变量类型声明(author by [haoxianrui](https://github.com/haoxianrui)) + +## 🐛 fix +- 修复构建时提示iconComponent.name可能为undefined的报错 (author by [wangji1042](https://github.com/wangji1042)) +- 修复浏览器密码自动填充时可能存在的报错 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修复eslint报错(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 移动端下点击左侧菜单节点后关闭侧边栏(author by [haoxianrui](https://github.com/haoxianrui)) +- 添加 size 类型断言修复类型报错(author by [haoxianrui](https://github.com/haoxianrui)) + +## 📦️ build +- husky9.x版本适配 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 升级 npm 包版本至最新(author by [haoxianrui](https://github.com/haoxianrui)) + +# 2.9.2 (2024/03/05) +## ✨ feat +- vscode开发扩展推荐(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 完善基础增删改查Mock接口(author by [haoxianrui](https://github.com/haoxianrui)) + +## ♻️ refactor +- 修改login密码框功能实现(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 弱化页面进入动画效果(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 取消推荐TypeScript Vue Plugin (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 网站加载动画替换 (author by [haoxianrui](https://github.com/haoxianrui)) +- 优化主题和主题色监听,避免多个页面重复初始化 (author by [haoxianrui](https://github.com/haoxianrui)) + +## 🐛 fix +- AppMain 高度在非固定头部不正确导致出现滚动条问题修复 (author by [haoxianrui](https://github.com/haoxianrui)) +- 修复混合模式开启固定Head时的样式问题 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 设置面板统一字体大小 (author by [cshaptx4869](https://github.com/cshaptx4869)) + +## 📦️build +- 通过env配置控制mock服务 (author by [cshaptx4869](https://github.com/cshaptx4869)) +- 升级依赖包至最新版本 (author by [haoxianrui](https://github.com/haoxianrui)) +- 定义vite全局常量替换项目标题和版本 (author by [cshaptx4869](https://github.com/cshaptx4869)) + +# 2.9.1 (2024/02/28) +## ♻️ refactor +- 项目配置按钮移入navbar(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 优化user数据定义(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 统一设置栏的 SVG 图标风格 + +## 🐛 fix +- 规整一些开发依赖(author by [cshaptx4869](https://github.com/cshaptx4869)) +- 修复登录页主题切换问题 (author by [cshaptx4869](https://github.com/cshaptx4869)) + +## 🚀 pref + +- 压缩图片资源 (author by [cshaptx4869](https://github.com/cshaptx4869)) + + +# 2.9.0 (2024/02/25) + +## ✨ feat +- 引入 animate.css 动画库 +- 新增水印和配置 +- 动态路由菜单支持 element plus 的图标 + +## ♻️ refactor +- Layout 布局重构和相关问题修复 +- sass 使用 @use 替代 @import 引入外部文件指令 + +## 🐛 fix +- 修复管理页面部分弹窗无法打开问题 +- 主题颜色设置按钮 hover 等未变化问题修复 + + +# 2.8.1 (2024/01/10) + +## ✨ feat +- 替换 Mock 解决方案 vite-plugin-mock 为 vite-plugin-mock-dev-server 适配 Vite5 + +# 2.8.0 (2023/12/27) + +## ⬆️ chore +- 升级 Vite4 至 Vite5 + +# 2.7.1 (2023/12/12) + +## ♻️ refactor +- 将打包后的文件进行分类 (author by [ityangzhiwen](https://gitee.com/ityangzhiwen)) + +# 2.7.0 (2023/11/19) + +## ♻️ refactor +- 代码重构优化 +- 修改自动导入组件类型声明文件路径 +- 完善 typescript 类型 + +## 🐛 fix +- 修复管理页面部分弹窗无法打开问题 + + +# 2.7.0 (2023/11/19) + +## ♻️ refactor +- 代码重构 +- 修改自动导入组件类型声明文件路径 +- 完善 typescript 类型 + +## 🐛 fix +- 修复管理页面部分弹窗无法打开问题 + + +# 2.6.3 (2023/10/22) + +## ✨ feat +- 菜单管理新增目录只有一级子路由是否始终显示(alwaysShow)和路由页面是否缓存(keepAlive)的配置 +- 接口文档新增 swagger、knife4j +- 引入和支持 tsx + +## ♻️ refactor +- 代码瘦身,整理并删除未使用的 svg +- 控制台样式优化 + +## 🐛 fix +- 菜单栏折叠和展开的图标暗黑模式显示问题修复 + + +# 2.6.2 (2023/10/11) + +## 🐛 fix +- 主题设置未持久化问题 +- UnoCSS 插件无智能提示 + +## ♻️ refactor +- WebSocket 演示样式和代码优化 +- 用户管理代码重构 + +# 2.6.1 (2023/9/4) + +## 🐛 fix +- 导航顶部模式、混合模式样式在固定 Header 出现的样式问题修复 +- 固定 Header 没有持久化问题修复 +- 字典回显兼容 String 和 Number 类型 + +# 2.6.0 (2023/8/24)💥💥💥 + +## ✨ feat +- 导航顶部模式、混合模式支持(author by [april-tong](https://april-tong.com/)) +- 平台文档(内嵌)(author by [april-tong](https://april-tong.com/)) + +# 2.5.0 (2023/8/8) + +## ✨ feat +- 新增 Mock(author by [ygcaicn](https://github.com/ygcaicn)) +- 图标 DEMO(author by [ygcaicn](https://github.com/ygcaicn)) + +## 🐛 fix +- 字典支持 Number 类型 + +# 2.4.1 (2023/7/20) + +## ✨ feat +- 整合 vite-plugin-compression 插件打包优化(3.66MB → 1.58MB) (author by [april-tong](https://april-tong.com/)) +- 字典组件封装(author by [haoxr](https://juejin.cn/user/4187394044331261/posts)) + +## 🐛 fix +- 分页组件hidden无效 +- 签名无法保存至后端 +- Git 提交 stylelint 校验部分机器报错 + +# 2.4.0 (2023/6/17) + +## ✨ feat +- 新增组件标签输入框(author by [april-tong](https://april-tong.com/)) +- 新增组件签名(author by [april-tong](https://april-tong.com/)) +- 新增组件表格(author by [april-tong](https://april-tong.com/)) +- Echarts 图表添加下载功能 author by [april-tong](https://april-tong.com/)) + +## ♻️ refactor +- 限制包管理器为 pnpm 和 node 版本16+ +- 自定义组件自动导入配置 +- 搜索框样式写法优化 + +## 🐛 fix +- 用户导入的部门回显成数字问题修复 + +## ⬆️ chore +- element-plus 版本升级 2.3.5 → 2.3.6 + +# 2.3.1 (2023/5/21) + +## 🔄 refactor +- 组件示例文件名称优化 + +# 2.2.2 (2023/5/11) + +## ✨ feat +- 组件封装示例添加源码地址 +- 角色、菜单、部门、字段按钮添加权限控制 + + +# 2.3.0 (2023/5/12) + +## ⬆️ chore +- vue 版本升级 3.2.45 → 3.3.1 ([CHANGELOG](https://github.com/vuejs/core/blob/main/CHANGELOG.md)) +- vite 版本升级 4.3.1 → 4.3.5 + +## ♻️ refactor +- 使用 vue 3.3 版本新特性 `defineOptions` 在 `setup` 定义组件名称,移除重复的 `script` 标签 + +# 2.2.2 (2023/5/11) + +## ✨ feat +- 用户新增提交添加 `vueUse` 的 `useDebounceFn` 函数实现按钮防抖节流 + + +# 2.2.1 (2023/4/25) + +## 🐛 fix +- 图标选择器组件使用 `onClickOutside` 未排除下拉弹出框元素导致无法输入搜索。 + diff --git a/README.en-US.md b/README.en-US.md new file mode 100644 index 0000000..0a92b17 --- /dev/null +++ b/README.en-US.md @@ -0,0 +1,222 @@ +
+ vue3-element-admin +

vue3-element-admin

+ + + + + + + + + + + 有来技术 + + + 有来技术 + + + 有来技术 + + +
+ +![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png") + + +
+ 🖥️ Live Preview | 📲 Mobile Preview | 📑 Documentation| 🌐 Official Website | 💬 中文 +
+ + +## Introduction + +[vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) is a minimalist enterprise-level backend management frontend template built with Vue3, Vite, TypeScript, and Element-Plus. It comes with complementary Java backend [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) and Node backend [youlai-nest](https://gitee.com/youlaiorg/youlai-nest). A simplified version [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) and a JavaScript version [vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin) are also available for developers to quickly start development. + + +## Project Features + +- **Simple and Easy-to-use**: Upgraded version of [vue-element-admin](https://gitee.com/panjiachen/vue-element-admin) for Vue3, with minimal encapsulation and easy to get started. +- **Data Interaction**: Support for `Mock` data and [online API documentation](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5), with accompanying [Java](https://gitee.com/youlaiorg/youlai-boot) and [Node](https://gitee.com/youlaiorg/youlai-nest) backend source code. + +- **System Functions**: Provides user management, role management, menu management, department management, dictionary management, and other functional modules. +- **Permission Management**: Supports dynamic routing, button permissions, role permissions, and data permissions. + +- **Infrastructure**: Provides internationalization, multiple layouts, dark mode, full screen, watermark, API documentation, and code generator functionality. +- **Continuous Updates**: Project is continuously updated with real-time updates of tools and dependencies. + + +## Project Screenshots + +🖥️ **Dashboard** + +![](https://www.youlai.tech/storage/blog/2025/04/30/20250430085342.png) + +⚡**API Documentation** + +![](https://www.youlai.tech/storage/blog/2025/01/18/20250118183539.png) + +📲 **Mobile Version** + +![](https://www.youlai.tech/storage/blog/2025/04/30/app.jpg) + +## Project Source Code + +| Project | Gitee | Github | GitCode| +| ---- | ----| ---- | ---- | +| vue3-element-admin ✅| [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) | +| vue3-element-admin JS Version| [vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin-js) | [vue3-element-admin-js](https://github.com/youlaitech/vue3-element-admin-js) | [vue3-element-admin-js](https://gitcode.com/youlai/vue3-element-admin-js) | +| vue3-element-admin Lite Version | [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | [vue3-element-template](https://github.com/youlaitech/vue3-element-template) |[vue3-element-template](https://gitcode.com/youlai/vue3-element-template)| +| vue-uniapp-admin Mobile Version | [vue-uniapp-admin](https://gitee.com/youlaiorg/vue-uniapp-admin) | [vue-uniapp-admin](https://github.com/youlaitech/vue-uniapp-admin) |[vue-uniapp-admin](https://gitcode.com/youlai/vue-uniapp-admin)| +| Java Backend | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot.git) |[youlai-boot](https://gitcode.com/youlai/youlai-boot.git)| +| Node Backend | [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | [youlai-nest](https://github.com/haoxianrui/youlai-nest.git) |[youlai-nest](https://gitcode.com/youlai/youlai-nest.git)| + + + +## Development Guide + +| Name | Link | +|---------------|--------------------| +| Video Tutorial | [https://www.bilibili.com/video/BV1eFUuYyEFj](https://www.bilibili.com/video/BV1eFUuYyEFj) | +| Project Setup | [Building a Backend Management System from Scratch with Vue3, Vite, TypeScript, and Element-Plus](https://blog.csdn.net/u013737132/article/details/130191394) | +| Official Documentation | [https://www.youlai.tech/vue3-element-admin/](https://www.youlai.tech/vue3-element-admin/) | +| Code Standards | [ESLint V9 + Prettier + Stylelint + EditorConfig for Standardized and Unified Frontend Code Style](https://youlai.blog.csdn.net/article/details/145608723) | +| Commit Standards | [Husky + Lint-staged + Commitlint + Commitizen + cz-git for Git Commit Standards](https://youlai.blog.csdn.net/article/details/145615236) | +| API Documentation | [https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) | + + + +## Project Setup + + +- **Environment Preparation** + +| Environment Type | Name | +|----------------|-----------------------------| +| **Development Tool** | [Visual Studio Code](https://code.visualstudio.com/Download) | +| **Runtime Environment** | Node 18 + (Recommended [22.9.0](https://npmmirror.com/mirrors/node/v22.9.0/)) | +> ⚠️ Note: Node.js version 20.6.0 has compatibility issues, please don't use it + + +- **Quick Start** + +```bash +# Clone repository +git clone https://gitee.com/youlaiorg/vue3-element-admin.git + +# Change directory +cd vue3-element-admin + +# Install pnpm +npm install pnpm -g + +# Set mirror source (optional) +pnpm config set registry https://registry.npmmirror.com + +# Install dependencies +pnpm install + +# Start development server +pnpm run dev +``` + + +## Project Deployment + +After executing the `pnpm run build` command, the project will be bundled and a `dist` directory will be generated. Next, upload the files from the `dist` directory to the `/usr/share/nginx/html` directory on your server and configure Nginx for reverse proxy. + +```bash +pnpm run build +``` + +Here is an example Nginx configuration: + +```nginx +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + # Reverse proxy configuration + location /prod-api/ { + # Please replace api.youlai.tech with your backend API address, and keep the trailing slash / + proxy_pass http://api.youlai.tech/; + } +} +``` + +For more detailed information, please refer to this article: [Nginx Installation and Configuration](https://blog.csdn.net/u013737132/article/details/145667694). + +## Local Mock + +The project supports both online and local Mock interfaces. By default, it uses online interfaces. To switch to Mock interfaces, modify the `VITE_MOCK_DEV_SERVER` value in the `.env.development` file to `true`. + +## Backend API + +> If you have a basic understanding of Java development, follow these steps to convert online API to local backend API and create an enterprise-level full-stack development environment to help you on your full-stack journey. + +1. Get the backend source code based on `Java` and `SpringBoot` from [youlai-boot](https://gitee.com/youlaiorg/youlai-boot.git). +2. Follow the instructions in the backend project's README.md to [set up and run locally](https://gitee.com/youlaiorg/youlai-boot#%E9%A1%B9%E7%9B%AE%E8%BF%90%E8%A1%8C). +3. Modify the value of `VITE_APP_API_URL` in the `.env.development` file, changing it from https://api.youlai.tech to http://localhost:8989. + + +## Notes + +- **Auto import plugin is disabled by default** + + Component type declarations have been automatically generated for the template project. If you add and use new components, follow the instructions in the screenshot to enable automatic generation. After automatic generation is complete, remember to set it back to `false` to avoid conflicts. + + ![](https://foruda.gitee.com/images/1687755823137387608/412ea803_716974.png) + +- **Blank page when accessing the project** + + Try upgrading your browser, as older browser engines may not support certain new JavaScript syntax, such as optional chaining operator `?.`. + +- **Project synchronization with repository updates** + + After synchronizing the project with repository updates, it is recommended to run `pnpm install` to update dependencies before starting. + +- **Red highlight on project components, functions, and imports** + + Restart VSCode to try again. + +- **Other issues** + + If you have any other issues or suggestions, please open an [ISSUE](https://gitee.com/youlaiorg/vue3-element-admin/issues/new). + + +## Commit Conventions + +Execute `pnpm run commit` to invoke interactive git commit and complete the information input and selection according to the prompts. + +![](https://foruda.gitee.com/images/1687755823165218215/c1705416_716974.png) + + +## Project Statistics + +![](https://repobeats.axiom.co/api/embed/aa7cca3d6fa9c308fc659fa6e09af9a1910506c3.svg "Repobeats analytics image") + + +Thanks to all the contributors! + +[![contributors](https://contrib.rocks/image?repo=youlaitech/vue3-element-admin)](https://github.com/youlaitech/vue3-element-admin/graphs/contributors) + + +## Special Thanks + +- Thanks to the [GitCode](https://gitcode.com/) official [G-Star](https://gitcode.com/g-star) certification + ![](https://foruda.gitee.com/images/1728577513089814203/95f2a70d_716974.jpeg) + +## Community + +① Follow the "Youlai Tech" WeChat Official Account, click the **Group Chat** menu to get the QR code (this measure is taken to prevent ads from entering the group, thanks for your understanding and support). + +② Directly add WeChat **`haoxianrui`** with a note indicating "Frontend/Backend/Full Stack". + +![Youlai Tech WeChat Official Account](https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..70cf5dc --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +
+ vue3-element-admin +

vue3-element-admin

+ + + + + +
+ + + + + 有来技术 + + + 有来技术 + + + 有来技术 + + +
+ +![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png") + + +
+ 🖥️ 在线预览 | 📲 移动端预览 | 📑 阅读文档| 🌐 官网 | 💬 English +
+ + +## 项目简介 + +[vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) 基于 Vue3、Vite、TypeScript 和 Element-Plus 搭建的极简开箱即用企业级后台管理前端模板。 配套 Java 后端 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) 和 Node 后端 [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) 。 提供开发简版[vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) 和 JS 版本[vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin) 供开发者快速开发。 + + +## 项目特色 + +- **简洁易用**:基于 [vue-element-admin](https://gitee.com/panjiachen/vue-element-admin) 升级的 Vue3 版本,无过渡封装 ,易上手。 +- **数据交互**: 支持 `Mock` 数据和[线上接口文档](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5),并提供配套的 [Java](https://gitee.com/youlaiorg/youlai-boot) 和 [Node](https://gitee.com/youlaiorg/youlai-nest) 后端源码。 + +- **系统功能:** 提供用户管理、角色管理、菜单管理、部门管理、字典管理等功能模块。 +- **权限管理:** 支持动态路由、按钮权限、角色权限和数据权限等多种权限管理方式。 + +- **基础设施:** 提供国际化、多布局、暗黑模式、全屏、水印、接口文档和代码生成器等功能。 +- **持续更新**:项目持续开源更新,实时更新工具和依赖。 + + +## 项目截图 + +🖥️ **控制台** + +![](https://www.youlai.tech/storage/blog/2025/04/30/20250430085342.png) + +⚡**接口文档** + +![](https://www.youlai.tech/storage/blog/2025/01/18/20250118183539.png) + +📲 **移动端** + +![](https://www.youlai.tech/storage/blog/2025/04/30/app.jpg) + +## 项目源码 + +| 项目 | Gitee | Github | GitCode| +| ---- | ----| ---- | ---- | +| vue3-element-admin ✅| [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) | +| vue3-element-admin JS版| [vue3-element-admin-js](https://gitee.com/youlaiorg/vue3-element-admin-js) | [vue3-element-admin-js](https://github.com/youlaitech/vue3-element-admin-js) | [vue3-element-admin-js](https://gitcode.com/youlai/vue3-element-admin-js) | +| vue3-element-admin 精简版 | [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | [vue3-element-template](https://github.com/youlaitech/vue3-element-template) |[vue3-element-template](https://gitcode.com/youlai/vue3-element-template)| +| vue-uniapp-admin 移动版 | [vue-uniapp-admin](https://gitee.com/youlaiorg/vue-uniapp-admin) | [vue-uniapp-admin](https://github.com/youlaitech/vue-uniapp-admin) |[vue-uniapp-admin](https://gitcode.com/youlai/vue-uniapp-admin)| +| Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot.git) |[youlai-boot](https://gitcode.com/youlai/youlai-boot.git)| +| Node 后端 | [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | [youlai-nest](https://github.com/haoxianrui/youlai-nest.git) |[youlai-nest](https://gitcode.com/youlai/youlai-nest.git)| + + + +## 开发指南 + +| 名称 | 地址 | +|---------------|--------------------| +| 视频教程 | [https://www.bilibili.com/video/BV1eFUuYyEFj](https://www.bilibili.com/video/BV1eFUuYyEFj) | +| 项目搭建 | [基于 Vue3 + Vite + TypeScript + Element-Plus 从0到1搭建后台管理系统](https://blog.csdn.net/u013737132/article/details/130191394) | +| 官方文档 | [https://www.youlai.tech/vue3-element-admin/](https://www.youlai.tech/vue3-element-admin/) | +| 代码规范 | [ESLint V9 + Prettier + Stylelint + EditorConfig 约束和统一前端代码规范](https://youlai.blog.csdn.net/article/details/145608723) | +| 提交规范 | [Husky + Lint-staged + Commitlint + Commitizen + cz-git 配置 Git 提交规范](https://youlai.blog.csdn.net/article/details/145615236) | +| 接口文档 | [https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) | + + + +## 项目启动 + + +- **环境准备** + +| 环境类型 | 名称 | +|----------------|-----------------------------| +| **开发工具** | [Visual Studio Code](https://code.visualstudio.com/Download) | +| **运行环境** | Node 18 + (推荐[22.9.0](https://npmmirror.com/mirrors/node/v22.9.0/)) | +> ⚠️ 注意:Node.js 20.6.0版本存在兼容性问题,请勿使用 + + +- **快速开始** + +```bash +# 克隆代码 +git clone https://gitee.com/youlaiorg/vue3-element-admin.git + +# 切换目录 +cd vue3-element-admin + +# 安装 pnpm +npm install pnpm -g + +# 设置镜像源(可忽略) +pnpm config set registry https://registry.npmmirror.com + +# 安装依赖 +pnpm install + +# 启动运行 +pnpm run dev +``` + + +## 项目部署 + +执行 `pnpm run build` 命令后,项目将被打包并生成 `dist` 目录。接下来,将 `dist` 目录下的文件上传到服务器 `/usr/share/nginx/html` 目录下,并配置 Nginx 进行反向代理。 + +```bash +pnpm run build +``` + +以下是 Nginx 的配置示例: + +```nginx +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + # 反向代理配置 + location /prod-api/ { + # 请将 api.youlai.tech 替换为您的后端 API 地址,并注意保留后面的斜杠 / + proxy_pass http://api.youlai.tech/; + } +} +``` + +更多详细信息,请参考这篇文章:[Nginx 安装和配置](https://blog.csdn.net/u013737132/article/details/145667694)。 + +## 本地Mock + +项目同时支持在线和本地 Mock 接口,默认使用线上接口,如需替换为 Mock 接口,修改文件 `.env.development` 的 `VITE_MOCK_DEV_SERVER` 为 `true` **即可**。 + +## 后端接口 + +> 如果您具备Java开发基础,按照以下步骤将在线接口转为本地后端接口,创建企业级前后端分离开发环境,助您走向全栈之路。 + +1. 获取基于 `Java` 和 `SpringBoot` 开发的后端 [youlai-boot](https://gitee.com/youlaiorg/youlai-boot.git) 源码。 +2. 根据后端工程的说明文档 [README.md](https://gitee.com/youlaiorg/youlai-boot#%E9%A1%B9%E7%9B%AE%E8%BF%90%E8%A1%8C) 完成本地启动。 +3. 修改 `.env.development` 文件中的 `VITE_APP_API_URL` 的值,将其从 https://api.youlai.tech 更改为 http://localhost:8989 即可。 + + +## 注意事项 + +- **自动导入插件自动生成默认关闭** + + 模板项目的组件类型声明已自动生成。如果添加和使用新的组件,请按照图示方法开启自动生成。在自动生成完成后,记得将其设置为 `false`,避免重复执行引发冲突。 + + ![](https://foruda.gitee.com/images/1687755823137387608/412ea803_716974.png) + +- **项目启动浏览器访问空白** + + 请升级浏览器尝试,低版本浏览器内核可能不支持某些新的 JavaScript 语法,比如可选链操作符 `?.`。 + +- **项目同步仓库更新升级** + + 项目同步仓库更新升级之后,建议 `pnpm install` 安装更新依赖之后启动 。 + +- **项目组件、函数和引用爆红** + + 重启 VSCode 尝试 + +- **其他问题** + + 如果有其他问题或者建议,建议 [ISSUE](https://gitee.com/youlaiorg/vue3-element-admin/issues/new) + + +## 提交规范 + +执行 `pnpm run commit` 唤起 git commit 交互,根据提示完成信息的输入和选择。 + +![](https://foruda.gitee.com/images/1687755823165218215/c1705416_716974.png) + + +## 项目统计 + +![](https://repobeats.axiom.co/api/embed/aa7cca3d6fa9c308fc659fa6e09af9a1910506c3.svg "Repobeats analytics image") + + +Thanks to all the contributors! + +[![contributors](https://contrib.rocks/image?repo=youlaitech/vue3-element-admin)](https://github.com/youlaitech/vue3-element-admin/graphs/contributors) + + +## 特别感谢 + +- 感谢 [GitCode](https://gitcode.com/) 官方的 [G-Star](https://gitcode.com/g-star) 认证 + ![](https://foruda.gitee.com/images/1728577513089814203/95f2a70d_716974.jpeg) + +## 加群交流 + +① 关注「有来技术」公众号,点击菜单 **交流群** 获取加群二维码(此举防止广告进群,感谢理解和支持)。 + +② 直接添加微信 **`haoxianrui`** 备注「前端/后端/全栈」。 + +![有来技术公众号](https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png) diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..5f556e2 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,93 @@ +module.exports = { + // 继承的规则 + extends: ["@commitlint/config-conventional"], + // 自定义规则 + rules: { + // @see https://commitlint.js.org/#/reference-rules + + // 提交类型枚举,git提交type必须是以下类型 + "type-enum": [ + 2, + "always", + [ + "feat", // 新增功能 + "fix", // 修复缺陷 + "docs", // 文档变更 + "style", // 代码格式(不影响功能,例如空格、分号等格式修正) + "refactor", // 代码重构(不包括 bug 修复、功能新增) + "perf", // 性能优化 + "test", // 添加疏漏测试或已有测试改动 + "build", // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等) + "ci", // 修改 CI 配置、脚本 + "revert", // 回滚 commit + "chore", // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例) + "wip", // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例) + ], + ], + "subject-case": [0], // subject大小写不做校验 + }, + + prompt: { + messages: { + type: "选择你要提交的类型 :", + scope: "选择一个提交范围(可选):", + customScope: "请输入自定义的提交范围 :", + subject: "填写简短精炼的变更描述 :\n", + body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n', + breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n', + footerPrefixesSelect: "选择关联issue前缀(可选):", + customFooterPrefix: "输入自定义issue前缀 :", + footer: "列举关联issue (可选) 例如: #31, #I3244 :\n", + generatingByAI: "正在通过 AI 生成你的提交简短描述...", + generatedSelectByAI: "选择一个 AI 生成的简短描述:", + confirmCommit: "是否提交或修改commit ?", + }, + // prettier-ignore + types: [ + { value: "feat", name: "特性: ✨ 新增功能", emoji: ":sparkles:" }, + { value: "fix", name: "修复: 🐛 修复缺陷", emoji: ":bug:" }, + { value: "docs", name: "文档: 📝 文档变更(更新README文件,或者注释)", emoji: ":memo:" }, + { value: "style", name: "格式: 🌈 代码格式(空格、格式化、缺失的分号等)", emoji: ":lipstick:" }, + { value: "refactor", name: "重构: 🔄 代码重构(不修复错误也不添加特性的代码更改)", emoji: ":recycle:" }, + { value: "perf", name: "性能: 🚀 性能优化", emoji: ":zap:" }, + { value: "test", name: "测试: 🧪 添加疏漏测试或已有测试改动", emoji: ":white_check_mark:"}, + { value: "build", name: "构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)", emoji: ":package:"}, + { value: "ci", name: "集成: ⚙️ 修改 CI 配置、脚本", emoji: ":ferris_wheel:"}, + { value: "revert", name: "回退: ↩️ 回滚 commit",emoji: ":rewind:"}, + { value: "chore", name: "其他: 🛠️ 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)", emoji: ":hammer:"}, + { value: "wip", name: "开发中: 🚧 开发阶段临时提交", emoji: ":construction:"}, + ], + useEmoji: true, + emojiAlign: "center", + useAI: false, + aiNumber: 1, + themeColorCode: "", + scopes: [], + allowCustomScopes: true, + allowEmptyScopes: true, + customScopesAlign: "bottom", + customScopesAlias: "custom", + emptyScopesAlias: "empty", + upperCaseSubject: false, + markBreakingChangeMode: false, + allowBreakingChanges: ["feat", "fix"], + breaklineNumber: 100, + breaklineChar: "|", + skipQuestions: [], + issuePrefixes: [{ value: "closed", name: "closed: ISSUES has been processed" }], + customIssuePrefixAlign: "top", + emptyIssuePrefixAlias: "skip", + customIssuePrefixAlias: "custom", + allowCustomIssuePrefix: true, + allowEmptyIssuePrefix: true, + confirmColorize: true, + maxHeaderLength: Infinity, + maxSubjectLength: Infinity, + minSubjectLength: 0, + scopeOverrides: undefined, + defaultBody: "", + defaultIssues: "", + defaultScope: "", + defaultSubject: "", + }, +}; diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 0000000..319b85f --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,176 @@ +// https://eslint.org/docs/latest/use/configure/configuration-files-new + +import eslint from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; +import pluginTypeScript from "@typescript-eslint/eslint-plugin"; +import parserTypeScript from "@typescript-eslint/parser"; +import vueParser from "vue-eslint-parser"; +import globals from "globals"; +import configPrettier from "eslint-config-prettier"; + +// 解析自动导入配置 +import fs from "node:fs"; +let autoImportGlobals = {}; +try { + autoImportGlobals = + JSON.parse(fs.readFileSync("./.eslintrc-auto-import.json", "utf-8")).globals || {}; +} catch (error) { + // 文件不存在或解析错误时使用空对象 + console.warn("Could not load auto-import globals", error); +} + +// Element Plus组件 +const elementPlusComponents = { + // Element Plus 组件添加为全局变量,避免 no-undef 报错 + ElInput: "readonly", + ElSelect: "readonly", + ElSwitch: "readonly", + ElCascader: "readonly", + ElInputNumber: "readonly", + ElTimePicker: "readonly", + ElTimeSelect: "readonly", + ElDatePicker: "readonly", + ElTreeSelect: "readonly", + ElText: "readonly", + ElRadioGroup: "readonly", + ElCheckboxGroup: "readonly", + ElOption: "readonly", + ElRadio: "readonly", + ElCheckbox: "readonly", + ElInputTag: "readonly", + ElForm: "readonly", + ElFormItem: "readonly", + ElTable: "readonly", + ElTableColumn: "readonly", + ElButton: "readonly", + ElDialog: "readonly", + ElPagination: "readonly", + ElMessage: "readonly", + ElMessageBox: "readonly", + ElNotification: "readonly", + ElTree: "readonly", +}; + +export default [ + // 全局配置 + { + // 指定要检查的文件 + files: ["**/*.js", "**/*.ts", "**/*.vue"], + ignores: ["node_modules/**", "dist/**", "build/**"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.browser, // 浏览器环境全局变量 + ...globals.node, // Node.js 环境全局变量 + ...autoImportGlobals, // 自动导入的 API 函数 + ...elementPlusComponents, // Element Plus 组件 + // 全局类型定义,解决 TypeScript 中定义但 ESLint 不识别的问题 + PageQuery: "readonly", + PageResult: "readonly", + OptionType: "readonly", + ResponseData: "readonly", + ExcelResult: "readonly", + TagView: "readonly", + AppSettings: "readonly", + __APP_INFO__: "readonly", + }, + parser: parserTypeScript, + parserOptions: { + sourceType: "module", + }, + }, + rules: { + // 全局规则 + "no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + ignoreRestSiblings: true, + argsIgnorePattern: "^_", // 忽略以下划线开头的参数 + varsIgnorePattern: "^[A-Z][A-Z0-9_]*$", // 忽略全大写的常量/枚举 + }, + ], + // 禁用未定义变量检查,TypeScript 已处理类型检查 + "no-undef": "off", + }, + }, + + // 基础 JavaScript 配置 + eslint.configs.recommended, + + // Vue 配置 + { + files: ["**/*.vue"], + plugins: { + vue: pluginVue, + }, + languageOptions: { + parser: vueParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: "module", + parser: parserTypeScript, + }, + }, + processor: pluginVue.processors[".vue"], + rules: { + "vue/multi-word-component-names": "off", + "vue/no-v-html": "off", + "vue/require-default-prop": "off", + "vue/html-self-closing": [ + "error", + { + html: { + void: "always", + normal: "never", + component: "always", + }, + }, + ], + "vue/no-unused-vars": "off", + }, + }, + + // TypeScript 配置 + { + files: ["**/*.ts", "**/*.tsx", "**/*.vue"], + plugins: { + "@typescript-eslint": pluginTypeScript, + }, + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + vars: "all", + args: "after-used", + ignoreRestSiblings: true, + argsIgnorePattern: "^_", // 忽略以下划线开头的参数 + varsIgnorePattern: "^[A-Z][A-Z0-9_]*$", // 忽略全大写的常量/枚举 + }, + ], + }, + }, + + // CURD 组件配置 + { + files: ["**/components/CURD/**/*.{ts,vue}"], + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off", + }, + }, + + // Prettier 集成 + { + rules: { + ...configPrettier.rules, + }, + }, +]; diff --git a/index.html b/index.html new file mode 100644 index 0000000..82beb14 --- /dev/null +++ b/index.html @@ -0,0 +1,90 @@ + + + + + + + + + 红与蓝授权管理系统 + + + +
+
+
+
+
+
+
+
+
+ + + + + diff --git a/mock/auth.mock.ts b/mock/auth.mock.ts new file mode 100644 index 0000000..153142c --- /dev/null +++ b/mock/auth.mock.ts @@ -0,0 +1,44 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "auth/captcha", + method: ["GET"], + body: { + code: "00000", + data: { + captchaKey: "534b8ef2b0a24121bec76391ddd159f9", + captchaBase64: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAAkCAIAAADNSmkJAAAFKUlEQVR4Xu2ZXUwcVRiGV70wMWo08V5NvPXCrDbFaGpMaZW2hqQxaoiJTRsaMBCNSYtpa2JTKiFSelFa+Q/QZcMWqEhBlh+htbEpZhMrBQrlJ0hBywLLyrJ0WZbje3bqOvPNLHPWrDvdOE9ONmfe78zkzMs335wzWJhJQrBQweS/wTQ6QWgYHdoIOcecOe05O+t2WkutO+p2ZF3Ksg/YV9ZW6FATYajR3nveg60H9327r3O8c35lHgp+r05dPdJzBL73TPSQ8SaCKIxGLsPlop+K0JHrEkPuoT31e5qGmmjARACF0agYyGVNlyVm/pzZXrN9fHGcBkz0UBid+31u93i3XFFT80vN8cvHqWqih8Lo1NpUqS5vwh3vnd223VQ10UNh9NbyrcFQUK6oCawHUipSqGqiB83oBf+CXFGDMp1mS6OqiR4Ko7FexkpOrqhpHGw82nOUqiZ6KIzGrkRuorW0dJMmOy+hOCfYGzb2RBFv6HRO0gEJw/U7y+pgL1bwmTxexN6sZ31TdEwEhdG+gA+7EqyXpUO1uZH20cWL8hMTRt1N9tBXzCJrOIRoCPJpSO2RAp4HmtCdIfZ+2JWgEBN9LbR28seTGU0Zue1tMLp+YIAMSADzfvbkKX4/eb28j4YODiGin3heqmIlLja5hAUCu+nmGY3JWKvpMAlqNGgebsauBOvlqSX+JEx7p7EbTLen53XlzfmWUioqXikrc68Y8N2juJ/fyVsNChGHEE//rBANYWaZz+TRQqpLaBgNsPfDrgSpbS21YtV87IdjrlkX9JZbt5DOma2t9ITo5F+5glN22WwL/n+yDv00mw06orKxOqQ5+J04hhViwzAXETIcJDVm8uxZqktoGx2Nj9t43Wgaul/ERQiGQvtbWnDWgZYW9CXlQFjZ/7ciyHNn+Z2MexTimIeLz59TiIln0M1e+IbPpOAaDUnEYPTi6iqKxpbycs/qKo1tCslfKcffPn9enuMiPPY1vxO/ckeFQ4h46cdGqUWoidE/y54q5tPY5WDrGzQqIXot4BgchEE57e00IMCw2/1qZSVO/7SjA78o9INzcxsbrL+fnTnDDh9mmZn8F30oG1Hm+nABv5mQMopDS/h1HxtqTzWbABMe9sxpPoe9zezeOo1GELqWhPS8t46M0IAYHbdvR1aHbaOjbjfLz2eFhez6dba4yAfgF30o0BFVE8+Mjh/wFxPI+I5mAEHU6Ls+38vhTFwOBGhMDF8gkFpbC5ffsdv/uBs6dIj19dExEtARVXv9YNbop8NFY3aZ6gRRo+tu3IBHnzmdNCBMXldXJKPfL74WzWUJRE+coDUknqsOdZXQbAJYwluVTbOZI3Qt8GFzMwxyjo3RgBiN4fr+elXVpZGRLWXl6PdOTtJBSlBDUK/lnIrjOlrtqWYTQDJaF6FrTXu9sOa1ysrVoM5HVE1GFxZQcyJ/p+xzv6K/rbr6N6+XDpUBl0tKFIrbz78qWB6YnWFMCBld4XLBms+7df75ook/GNzb0GCV7U1Qfz9p64TyQWNjYD3qe9rj4SMJtQP3MyjSDPzWIRHPjH7X4YAvfXoPuyZf9Pbi3PcuXIh4mp3NllYC6XY79C+jl2o8PBipxjnBttn4MgMNnWgfcRJGPI2OL8hTj3LloIlmRicvBhiNykvecpqoa3RSY4DRcLAwyicuOepVR1JjgNFYHWONHL04czTX0UmNAUYD7Pr+xc4wqTHGaBb2OtZvHUmNYUazcA2J6etdUmOk0f8rTKMTxF91RG0D1SwYGwAAAABJRU5ErkJggg==", + }, + msg: "一切ok", + }, + }, + + { + url: "auth/login", + method: ["POST"], + body: { + code: "00000", + data: { + accessToken: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImRlcHRJZCI6MSwiZGF0YVNjb3BlIjoxLCJ1c2VySWQiOjIsImlhdCI6MTcyODE5MzA1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhZDg3NzlhZDZlYWY0OWY3OTE4M2ZmYmI5OWM4MjExMSJ9.58YHwL3sNNC22jyAmOZeSm-7MITzfHb_epBIz7LvWeA", + tokenType: "Bearer", + refreshToken: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImRlcHRJZCI6MSwiZGF0YVNjb3BlIjoxLCJ1c2VySWQiOjIsImlhdCI6MTcyODE5MzA1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJhZDg3NzlhZDZlYWY0OWY3OTE4M2ZmYmI5OWM4MjExMSJ9.58YHwL3sNNC22jyAmOZeSm-7MITzfHb_epBIz7LvWeA", + expires: null, + }, + msg: "一切ok", + }, + }, + + { + url: "auth/logout", + method: ["DELETE"], + body: { + code: "00000", + data: {}, + msg: "string", + }, + }, +]); diff --git a/mock/base.ts b/mock/base.ts new file mode 100644 index 0000000..e68e642 --- /dev/null +++ b/mock/base.ts @@ -0,0 +1,7 @@ +import path from "path"; +import { createDefineMock } from "vite-plugin-mock-dev-server"; + +export const defineMock = createDefineMock((mock) => { + // 拼接url + mock.url = path.join(import.meta.env.VITE_APP_BASE_API + "/api/v1/", mock.url); +}); diff --git a/mock/dept.mock.ts b/mock/dept.mock.ts new file mode 100644 index 0000000..e9b6d1b --- /dev/null +++ b/mock/dept.mock.ts @@ -0,0 +1,153 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "dept/options", + method: ["GET"], + body: { + code: "00000", + data: [ + { + value: 1, + label: "有来技术", + children: [ + { + value: 2, + label: "研发部门", + }, + { + value: 3, + label: "测试部门", + }, + ], + }, + ], + msg: "一切ok", + }, + }, + + { + url: "dept", + method: ["GET"], + body: { + code: "00000", + data: [ + { + id: 1, + parentId: 0, + name: "有来技术", + code: "YOULAI", + sort: 1, + status: 1, + children: [ + { + id: 2, + parentId: 1, + name: "研发部门", + code: "RD001", + sort: 1, + status: 1, + children: [], + createTime: null, + updateTime: "2022-04-19 12:46", + }, + { + id: 3, + parentId: 1, + name: "测试部门", + code: "QA001", + sort: 1, + status: 1, + children: [], + createTime: null, + updateTime: "2022-04-19 12:46", + }, + ], + createTime: null, + updateTime: null, + }, + ], + msg: "一切ok", + }, + }, + + // 新增部门 + { + url: "dept", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "新增部门" + body.name + "成功", + }; + }, + }, + + // 获取部门表单数据 + { + url: "dept/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: deptMap[params.id], + msg: "一切ok", + }; + }, + }, + + // 修改部门 + { + url: "dept/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改部门" + body.name + "成功", + }; + }, + }, + + // 删除部门 + { + url: "dept/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除部门" + params.id + "成功", + }; + }, + }, +]); + +// 部门映射表数据 +const deptMap: Record = { + 1: { + id: 1, + name: "有来技术", + code: "YOULAI", + parentId: 0, + status: 1, + sort: 1, + }, + 2: { + id: 2, + name: "研发部门", + code: "RD001", + parentId: 1, + status: 1, + sort: 1, + }, + 3: { + id: 3, + name: "测试部门", + code: "QA001", + parentId: 1, + status: 1, + sort: 1, + }, +}; diff --git a/mock/dict.mock.ts b/mock/dict.mock.ts new file mode 100644 index 0000000..c108bb5 --- /dev/null +++ b/mock/dict.mock.ts @@ -0,0 +1,312 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "dicts/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 1, + name: "性别", + dictCode: "gender", + status: 1, + }, + ], + total: 1, + }, + msg: "一切ok", + }, + }, + + /** + * 字典列表 + */ + { + url: "dicts", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + value: "gender", + label: "性别", + }, + ], + total: 1, + }, + msg: "一切ok", + }, + }, + + // 新增字典 + { + url: "dicts", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "新增字典" + body.name + "成功", + }; + }, + }, + + // 获取字典表单数据 + { + url: "dicts/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: dictMap[params.id], + msg: "一切ok", + }; + }, + }, + + // 修改字典 + { + url: "dicts/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改字典" + body.name + "成功", + }; + }, + }, + + // 删除字典 + { + url: "dicts/:ids", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除字典" + params.ids + "成功", + }; + }, + }, + + //--------------------------------------------------- + // 字典项相关接口 + //--------------------------------------------------- + + // 字典项分页列表 + { + url: "dicts/:dictCode/items/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 1, + dictCode: "gender", + label: "男", + value: "1", + sort: 1, + status: 1, + }, + { + id: 2, + dictCode: "gender", + label: "女", + value: "2", + sort: 2, + status: 1, + }, + { + id: 3, + dictCode: "gender", + label: "保密", + value: "0", + sort: 3, + status: 1, + }, + ], + total: 3, + }, + msg: "一切ok", + }, + }, + // 字典项列表 + { + url: "dicts/:dictCode/items", + method: ["GET"], + body: ({ params }) => { + const dictCode = params.dictCode; + + let list = null; + + if (dictCode == "gender") { + list = [ + { + value: "1", + label: "男", + }, + { + value: "2", + label: "女", + }, + { + value: "0", + label: "保密", + }, + ]; + } else if (dictCode == "notice_level") { + list = [ + { + value: "L", + label: "低", + tag: "info", + }, + { + value: "M", + label: "中", + tag: "warning", + }, + { + value: "H", + label: "高", + tag: "danger", + }, + ]; + } else if (dictCode == "notice_type") { + list = [ + { + value: "1", + label: "系统升级", + tag: "success", + }, + { + value: "2", + label: "系统维护", + tag: "primary", + }, + { + value: "3", + label: "安全警告", + tag: "danger", + }, + { + value: "4", + label: "假期通知", + tag: "success", + }, + { + value: "5", + label: "公司新闻", + tag: "primary", + }, + { + value: "99", + label: "其他", + tag: "info", + }, + ]; + } + + return { + code: "00000", + data: list, + msg: "一切ok", + }; + }, + }, + // 新增字典项 + { + url: "dicts/:dictCode/items", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "新增字典" + body.name + "成功", + }; + }, + }, + + // 字典项表单数据 + { + url: "dicts/:dictCode/items/:itemId/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: dictItemMap[params.itemId], + msg: "一切ok", + }; + }, + }, + + // 修改字典项 + { + url: "dicts/:dictCode/items/:itemId", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改字典项" + body.name + "成功", + }; + }, + }, + + // 删除字典 + { + url: "dicts/:dictCode/items/:itemId", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除字典" + params.itemId + "成功", + }; + }, + }, +]); + +// 字典映射表数据 +const dictMap: Record = { + 1: { + id: 1, + name: "性别", + dictCode: "gender", + status: 1, + }, +}; + +// 字典项映射表数据 +const dictItemMap: Record = { + 1: { + id: 1, + value: "1", + label: "男", + sort: 1, + status: 1, + tagType: "primary", + }, + 2: { + id: 2, + value: "2", + label: "女", + sort: 2, + status: 1, + tagType: "danger", + }, + 3: { + id: 3, + value: "0", + label: "保密", + sort: 3, + status: 1, + tagType: "info", + }, +}; diff --git a/mock/log.mock.ts b/mock/log.mock.ts new file mode 100644 index 0000000..b75963e --- /dev/null +++ b/mock/log.mock.ts @@ -0,0 +1,207 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "logs/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 36192, + module: "菜单", + content: "菜单列表", + requestUri: "/api/v1/menus", + method: null, + ip: "183.156.148.241", + region: "浙江省 杭州市", + browser: "Chrome 109.0.0.0", + os: "OSX", + executionTime: 5, + createBy: null, + createTime: "2024-07-07 20:38:47", + operator: "系统管理员", + }, + { + id: 36190, + module: "字典", + content: "字典分页列表", + requestUri: "/api/v1/dict/page", + method: null, + ip: "183.156.148.241", + region: "浙江省 杭州市", + browser: "Chrome 109.0.0.0", + os: "OSX", + executionTime: 9, + createBy: null, + createTime: "2024-07-07 20:38:45", + operator: "系统管理员", + }, + { + id: 36193, + module: "部门", + content: "部门列表", + requestUri: "/api/v1/dept", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 27, + createBy: null, + createTime: "2024-07-07 20:38:45", + operator: "系统管理员", + }, + { + id: 36191, + module: "菜单", + content: "菜单列表", + requestUri: "/api/v1/menus", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 39, + createBy: null, + createTime: "2024-07-07 20:38:44", + operator: "系统管理员", + }, + { + id: 36189, + module: "角色", + content: "角色分页列表", + requestUri: "/api/v1/roles/page", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 55, + createBy: null, + createTime: "2024-07-07 20:38:43", + operator: "系统管理员", + }, + { + id: 36188, + module: "用户", + content: "用户分页列表", + requestUri: "/api/v1/users/page", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 92, + createBy: null, + createTime: "2024-07-07 20:38:42", + operator: "系统管理员", + }, + { + id: 36187, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 19340, + createBy: null, + createTime: "2024-07-07 20:38:09", + operator: "系统管理员", + }, + { + id: 36186, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "192.168.31.134", + region: "0 内网IP", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 19869, + createBy: null, + createTime: "2024-07-07 20:37:59", + operator: "系统管理员", + }, + { + id: 36185, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "112.103.111.59", + region: "黑龙江省 哈尔滨市", + browser: "Chrome 97.0.4692.98", + os: "Android", + executionTime: 96, + createBy: null, + createTime: "2024-07-07 20:37:21", + operator: "系统管理员", + }, + { + id: 36184, + module: "登录", + content: "登录", + requestUri: "/api/v1/auth/login", + method: null, + ip: "114.86.204.190", + region: "上海 上海市", + browser: "Chrome 125.0.0.0", + os: "Windows 10 or Windows Server 2016", + executionTime: 89, + createBy: null, + createTime: "2024-07-07 20:29:37", + operator: "系统管理员", + }, + ], + total: 36188, + }, + msg: "一切ok", + }, + }, + { + url: "logs/visit-trend", + method: ["GET"], + body: { + code: "00000", + data: { + dates: [ + "2024-06-30", + "2024-07-01", + "2024-07-02", + "2024-07-03", + "2024-07-04", + "2024-07-05", + "2024-07-06", + "2024-07-07", + ], + pvList: [1751, 5168, 4882, 5301, 4721, 4885, 1901, 1003], + uvList: null, + ipList: [207, 566, 565, 631, 579, 496, 222, 152], + }, + msg: "一切ok", + }, + }, + + { + url: "logs/visit-stats", + method: ["GET"], + body: { + code: "00000", + data: { + todayPvCount: 1629, + totalPvCount: 286086, + pvGrowthRate: -0.65, + todayIpCount: 169, + totalIpCount: 19985, + ipGrowthRate: -0.57, + }, + msg: "一切ok", + }, + }, +]); diff --git a/mock/menu.mock.ts b/mock/menu.mock.ts new file mode 100644 index 0000000..02d72db --- /dev/null +++ b/mock/menu.mock.ts @@ -0,0 +1,2082 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "menus/routes", + method: ["GET"], + body: { + code: "00000", + data: [ + { + path: "/system", + component: "Layout", + redirect: "/system/user", + name: "/system", + meta: { + title: "系统管理", + icon: "system", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "user", + component: "system/user/index", + name: "User", + meta: { + title: "用户管理", + icon: "el-icon-User", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "role", + component: "system/role/index", + name: "Role", + meta: { + title: "角色管理", + icon: "role", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "menu", + component: "system/menu/index", + name: "SysMenu", + meta: { + title: "菜单管理", + icon: "menu", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dept", + component: "system/dept/index", + name: "Dept", + meta: { + title: "部门管理", + icon: "tree", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dict", + component: "system/dict/index", + name: "Dict", + meta: { + title: "字典管理", + icon: "dict", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "log", + component: "system/log/index", + name: "Log", + meta: { + title: "系统日志", + icon: "document", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dict-item", + component: "system/dict/dict-item", + name: "DictItem", + meta: { + title: "字典项", + icon: "", + hidden: true, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "config", + component: "system/config/index", + name: "Config", + meta: { + title: "系统配置", + icon: "setting", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "notice", + component: "system/notice/index", + name: "Notice", + meta: { + title: "通知公告", + icon: "", + hidden: false, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/codegen", + component: "Layout", + name: "/codegen", + meta: { + title: "系统工具", + icon: "menu", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "codegen", + component: "codegen/index", + name: "Codegen", + meta: { + title: "代码生成", + icon: "code", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/api", + component: "Layout", + name: "/api", + meta: { + title: "接口文档", + icon: "api", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "apifox", + component: "demo/api/apifox", + name: "Apifox", + meta: { + title: "Apifox", + icon: "api", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/doc", + component: "Layout", + redirect: "https://juejin.cn/post/7228990409909108793", + name: "/doc", + meta: { + title: "平台文档", + icon: "document", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "internal-doc", + component: "demo/internal-doc", + name: "InternalDoc", + meta: { + title: "document", + icon: "document", + hidden: false, + alwaysShow: false, + params: null, + }, + }, + { + path: "https://juejin.cn/post/7228990409909108793", + name: "Https://juejin.cn/post/7228990409909108793", + meta: { + title: "平台文档(外链)", + icon: "link", + hidden: false, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/multi-level", + component: "Layout", + name: "/multiLevel", + meta: { + title: "多级菜单", + icon: "cascader", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "multi-level1", + component: "demo/multi-level/level1", + name: "MultiLevel1", + meta: { + title: "菜单一级", + icon: "", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "multi-level2", + component: "demo/multi-level/children/level2", + name: "MultiLevel2", + meta: { + title: "菜单二级", + icon: "", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "multi-level3-1", + component: "demo/multi-level/children/children/level3-1", + name: "MultiLevel31", + meta: { + title: "菜单三级-1", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "multi-level3-2", + component: "demo/multi-level/children/children/level3-2", + name: "MultiLevel32", + meta: { + title: "菜单三级-2", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + ], + }, + ], + }, + { + path: "/component", + component: "Layout", + name: "/component", + meta: { + title: "组件封装", + icon: "menu", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "curd", + component: "demo/curd/index", + name: "Curd", + meta: { + title: "增删改查", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "table-select", + component: "demo/table-select/index", + name: "TableSelect", + meta: { + title: "列表选择器", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "wang-editor", + component: "demo/wang-editor", + name: "WangEditor", + meta: { + title: "富文本编辑器", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "upload", + component: "demo/upload", + name: "Upload", + meta: { + title: "图片上传", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "dict-demo", + component: "demo/dictionary", + name: "DictDemo", + meta: { + title: "字典组件", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "icon-selector", + component: "demo/icon-selector", + name: "IconSelector", + meta: { + title: "图标选择器", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "drag", + component: "demo/drag", + name: "Drag", + meta: { + title: "拖拽组件", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "text-scroll", + component: "demo/text-scroll", + name: "TextScroll", + meta: { + title: "滚动文本", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + ], + }, + { + path: "/route-param", + component: "Layout", + name: "/routeParam", + meta: { + title: "路由参数", + icon: "el-icon-ElementPlus", + hidden: false, + alwaysShow: true, + params: null, + }, + children: [ + { + path: "route-param-type1", + component: "demo/route-param", + name: "RouteParamType1", + meta: { + title: "参数(type=1)", + icon: "el-icon-Star", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: { + type: "1", + }, + }, + }, + { + path: "route-param-type2", + component: "demo/route-param", + name: "RouteParamType2", + meta: { + title: "参数(type=2)", + icon: "el-icon-StarFilled", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: { + type: "2", + }, + }, + }, + ], + }, + { + path: "/function", + component: "Layout", + name: "/function", + meta: { + title: "功能演示", + icon: "menu", + hidden: false, + alwaysShow: false, + params: null, + }, + children: [ + { + path: "icon-demo", + component: "demo/icons", + name: "IconDemo", + meta: { + title: "Icons", + icon: "el-icon-Notification", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "/function/websocket", + component: "demo/websocket", + name: "/function/websocket", + meta: { + title: "Websocket", + icon: "", + hidden: false, + keepAlive: true, + alwaysShow: false, + params: null, + }, + }, + { + path: "other/:id", + component: "demo/other", + name: "Other/:id", + meta: { + title: "敬请期待...", + icon: "", + hidden: false, + alwaysShow: false, + params: null, + }, + }, + ], + }, + ], + msg: "一切ok", + }, + }, + + // 获取菜单树形表格列表 + { + url: "menus", + method: ["GET"], + body: { + code: "00000", + data: [ + { + id: 1, + parentId: 0, + name: "系统管理", + type: "CATALOG", + routeName: "", + routePath: "/system", + component: "Layout", + sort: 1, + visible: 1, + icon: "system", + redirect: "/system/user", + perm: null, + children: [ + { + id: 2, + parentId: 1, + name: "用户管理", + type: "MENU", + routeName: "User", + routePath: "user", + component: "system/user/index", + sort: 1, + visible: 1, + icon: "el-icon-User", + redirect: null, + perm: null, + children: [ + { + id: 105, + parentId: 2, + name: "用户查询", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 0, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:query", + children: [], + }, + { + id: 31, + parentId: 2, + name: "用户新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: "sys:user:add", + children: [], + }, + { + id: 32, + parentId: 2, + name: "用户编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: "", + perm: "sys:user:edit", + children: [], + }, + { + id: 33, + parentId: 2, + name: "用户删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: "", + perm: "sys:user:delete", + children: [], + }, + { + id: 88, + parentId: 2, + name: "重置密码", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 4, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:password:reset", + children: [], + }, + { + id: 106, + parentId: 2, + name: "用户导入", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 5, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:import", + children: [], + }, + { + id: 107, + parentId: 2, + name: "用户导出", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 6, + visible: 1, + icon: "", + redirect: null, + perm: "sys:user:export", + children: [], + }, + ], + }, + { + id: 3, + parentId: 1, + name: "角色管理", + type: "MENU", + routeName: "Role", + routePath: "role", + component: "system/role/index", + sort: 2, + visible: 1, + icon: "role", + redirect: null, + perm: null, + children: [ + { + id: 70, + parentId: 3, + name: "角色新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:role:add", + children: [], + }, + { + id: 71, + parentId: 3, + name: "角色编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:role:edit", + children: [], + }, + { + id: 72, + parentId: 3, + name: "角色删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:role:delete", + children: [], + }, + ], + }, + { + id: 4, + parentId: 1, + name: "菜单管理", + type: "MENU", + routeName: "Menu", + routePath: "menu", + component: "system/menu/index", + sort: 3, + visible: 1, + icon: "menu", + redirect: null, + perm: null, + children: [ + { + id: 73, + parentId: 4, + name: "菜单新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:menu:add", + children: [], + }, + { + id: 74, + parentId: 4, + name: "菜单编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:menu:edit", + children: [], + }, + { + id: 75, + parentId: 4, + name: "菜单删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:menu:delete", + children: [], + }, + ], + }, + { + id: 5, + parentId: 1, + name: "部门管理", + type: "MENU", + routeName: "Dept", + routePath: "dept", + component: "system/dept/index", + sort: 4, + visible: 1, + icon: "tree", + redirect: null, + perm: null, + children: [ + { + id: 76, + parentId: 5, + name: "部门新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dept:add", + children: [], + }, + { + id: 77, + parentId: 5, + name: "部门编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dept:edit", + children: [], + }, + { + id: 78, + parentId: 5, + name: "部门删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dept:delete", + children: [], + }, + ], + }, + { + id: 6, + parentId: 1, + name: "字典管理", + type: "MENU", + routeName: "Dict", + routePath: "dict", + component: "system/dict/index", + sort: 5, + visible: 1, + icon: "dict", + redirect: null, + perm: null, + children: [ + { + id: 79, + parentId: 6, + name: "字典新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict:add", + children: [], + }, + { + id: 81, + parentId: 6, + name: "字典编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict_type:edit", + children: [], + }, + { + id: 84, + parentId: 6, + name: "字典删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict_type:delete", + children: [], + }, + ], + }, + { + id: 135, + parentId: 1, + name: "字典项", + type: "MENU", + routeName: "DictData", + routePath: "dict-item", + component: "system/dict/dict-item", + sort: 6, + visible: 0, + icon: "", + redirect: null, + perm: null, + children: [ + { + id: 136, + parentId: 135, + name: "字典项新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 4, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict-item:add", + children: [], + }, + { + id: 137, + parentId: 135, + name: "字典项编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 5, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict-item:edit", + children: [], + }, + { + id: 138, + parentId: 135, + name: "字典项删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 6, + visible: 1, + icon: "", + redirect: null, + perm: "sys:dict-item:delete", + children: [], + }, + ], + }, + { + id: 117, + parentId: 1, + name: "系统日志", + type: "MENU", + routeName: "Log", + routePath: "log", + component: "system/log/index", + sort: 6, + visible: 1, + icon: "document", + redirect: null, + perm: null, + children: [], + }, + { + id: 120, + parentId: 1, + name: "系统配置", + type: "MENU", + routeName: "Config", + routePath: "config", + component: "system/config/index", + sort: 7, + visible: 1, + icon: "setting", + redirect: null, + perm: null, + children: [ + { + id: 121, + parentId: 120, + name: "查询系统配置", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:config:query", + children: [], + }, + { + id: 122, + parentId: 120, + name: "新增系统配置", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:config:add", + children: [], + }, + { + id: 123, + parentId: 120, + name: "修改系统配置", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:config:update", + children: [], + }, + { + id: 124, + parentId: 120, + name: "删除系统配置", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 4, + visible: 1, + icon: "", + redirect: null, + perm: "sys:config:delete", + children: [], + }, + { + id: 125, + parentId: 120, + name: "刷新系统配置", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 5, + visible: 1, + icon: "", + redirect: null, + perm: "sys:config:refresh", + children: [], + }, + ], + }, + { + id: 126, + parentId: 1, + name: "通知公告", + type: "MENU", + routeName: "Notice", + routePath: "notice", + component: "system/notice/index", + sort: 9, + visible: 1, + icon: "", + redirect: null, + perm: null, + children: [ + { + id: 127, + parentId: 126, + name: "查询", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: "sys:notice:query", + children: [], + }, + { + id: 128, + parentId: 126, + name: "新增", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 2, + visible: 1, + icon: "", + redirect: null, + perm: "sys:notice:add", + children: [], + }, + { + id: 129, + parentId: 126, + name: "编辑", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 3, + visible: 1, + icon: "", + redirect: null, + perm: "sys:notice:edit", + children: [], + }, + { + id: 130, + parentId: 126, + name: "删除", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 4, + visible: 1, + icon: "", + redirect: null, + perm: "sys:notice:delete", + children: [], + }, + { + id: 133, + parentId: 126, + name: "发布", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 5, + visible: 1, + icon: "", + redirect: null, + perm: "sys:notice:publish", + children: [], + }, + { + id: 134, + parentId: 126, + name: "撤回", + type: "BUTTON", + routeName: null, + routePath: "", + component: null, + sort: 6, + visible: 1, + icon: "", + redirect: null, + perm: "sys:notice:revoke", + children: [], + }, + ], + }, + ], + }, + { + id: 118, + parentId: 0, + name: "系统工具", + type: "CATALOG", + routeName: null, + routePath: "/codegen", + component: "Layout", + sort: 2, + visible: 1, + icon: "menu", + redirect: null, + perm: null, + children: [ + { + id: 119, + parentId: 118, + name: "代码生成", + type: "MENU", + routeName: "Codegen", + routePath: "codegen", + component: "codegen/index", + sort: 1, + visible: 1, + icon: "code", + redirect: null, + perm: null, + children: [], + }, + ], + }, + { + id: 40, + parentId: 0, + name: "接口文档", + type: "CATALOG", + routeName: null, + routePath: "/api", + component: "Layout", + sort: 7, + visible: 1, + icon: "api", + redirect: "", + perm: null, + children: [ + { + id: 41, + parentId: 40, + name: "Apifox", + type: "MENU", + routeName: null, + routePath: "apifox", + component: "demo/api/apifox", + sort: 1, + visible: 1, + icon: "api", + redirect: "", + perm: null, + children: [], + }, + ], + }, + { + id: 26, + parentId: 0, + name: "平台文档", + type: "CATALOG", + routeName: null, + routePath: "/doc", + component: "Layout", + sort: 8, + visible: 1, + icon: "document", + redirect: "https://juejin.cn/post/7228990409909108793", + perm: null, + children: [ + { + id: 102, + parentId: 26, + name: "平台文档(内嵌)", + type: "EXTLINK", + routeName: null, + routePath: "internal-doc", + component: "demo/internal-doc", + sort: 1, + visible: 1, + icon: "document", + redirect: "", + perm: null, + children: [], + }, + { + id: 30, + parentId: 26, + name: "平台文档(外链)", + type: "EXTLINK", + routeName: null, + routePath: "https://juejin.cn/post/7228990409909108793", + component: "", + sort: 2, + visible: 1, + icon: "link", + redirect: "", + perm: null, + children: [], + }, + ], + }, + { + id: 20, + parentId: 0, + name: "多级菜单", + type: "CATALOG", + routeName: null, + routePath: "/multi-level", + component: "Layout", + sort: 9, + visible: 1, + icon: "cascader", + redirect: "", + perm: null, + children: [ + { + id: 21, + parentId: 20, + name: "菜单一级", + type: "MENU", + routeName: null, + routePath: "multi-level1", + component: "demo/multi-level/level1", + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [ + { + id: 22, + parentId: 21, + name: "菜单二级", + type: "MENU", + routeName: null, + routePath: "multi-level2", + component: "demo/multi-level/children/level2", + sort: 1, + visible: 1, + icon: "", + redirect: null, + perm: null, + children: [ + { + id: 23, + parentId: 22, + name: "菜单三级-1", + type: "MENU", + routeName: null, + routePath: "multi-level3-1", + component: "demo/multi-level/children/children/level3-1", + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 24, + parentId: 22, + name: "菜单三级-2", + type: "MENU", + routeName: null, + routePath: "multi-level3-2", + component: "demo/multi-level/children/children/level3-2", + sort: 2, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + ], + }, + ], + }, + ], + }, + { + id: 36, + parentId: 0, + name: "组件封装", + type: "CATALOG", + routeName: null, + routePath: "/component", + component: "Layout", + sort: 10, + visible: 1, + icon: "menu", + redirect: "", + perm: null, + children: [ + { + id: 108, + parentId: 36, + name: "增删改查", + type: "MENU", + routeName: null, + routePath: "curd", + component: "demo/curd/index", + sort: 0, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 109, + parentId: 36, + name: "列表选择器", + type: "MENU", + routeName: null, + routePath: "table-select", + component: "demo/table-select/index", + sort: 1, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 37, + parentId: 36, + name: "富文本编辑器", + type: "MENU", + routeName: null, + routePath: "wang-editor", + component: "demo/wang-editor", + sort: 2, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 38, + parentId: 36, + name: "图片上传", + type: "MENU", + routeName: null, + routePath: "upload", + component: "demo/upload", + sort: 3, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 95, + parentId: 36, + name: "字典组件", + type: "MENU", + routeName: null, + routePath: "dict-demo", + component: "demo/dict", + sort: 4, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 39, + parentId: 36, + name: "图标选择器", + type: "MENU", + routeName: null, + routePath: "icon-selector", + component: "demo/icon-selector", + sort: 4, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + ], + }, + { + id: 110, + parentId: 0, + name: "路由参数", + type: "CATALOG", + routeName: null, + routePath: "/route-param", + component: "Layout", + sort: 11, + visible: 1, + icon: "el-icon-ElementPlus", + redirect: null, + perm: null, + children: [ + { + id: 111, + parentId: 110, + name: "参数(type=1)", + type: "MENU", + routeName: null, + routePath: "route-param-type1", + component: "demo/route-param", + sort: 1, + visible: 1, + icon: "el-icon-Star", + redirect: null, + perm: null, + children: [], + }, + { + id: 112, + parentId: 110, + name: "参数(type=2)", + type: "MENU", + routeName: null, + routePath: "route-param-type2", + component: "demo/route-param", + sort: 2, + visible: 1, + icon: "el-icon-StarFilled", + redirect: null, + perm: null, + children: [], + }, + ], + }, + { + id: 89, + parentId: 0, + name: "功能演示", + type: "CATALOG", + routeName: null, + routePath: "/function", + component: "Layout", + sort: 12, + visible: 1, + icon: "menu", + redirect: "", + perm: null, + children: [ + { + id: 97, + parentId: 89, + name: "Icons", + type: "MENU", + routeName: null, + routePath: "icon-demo", + component: "demo/icons", + sort: 2, + visible: 1, + icon: "el-icon-Notification", + redirect: "", + perm: null, + children: [], + }, + { + id: 90, + parentId: 89, + name: "Websocket", + type: "MENU", + routeName: null, + routePath: "/function/websocket", + component: "demo/websocket", + sort: 3, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + { + id: 91, + parentId: 89, + name: "敬请期待...", + type: "CATALOG", + routeName: null, + routePath: "other/:id", + component: "demo/other", + sort: 4, + visible: 1, + icon: "", + redirect: "", + perm: null, + children: [], + }, + ], + }, + ], + msg: "一切ok", + }, + }, + + // 获取菜单树形下拉列表 + { + url: "menus/options", + method: ["GET"], + body: { + code: "00000", + data: [ + { + value: "1", + label: "系统管理", + children: [ + { + value: "2", + label: "用户管理", + children: [ + { + value: "105", + label: "用户查询", + }, + { + value: "31", + label: "用户新增", + }, + { + value: "32", + label: "用户编辑", + }, + { + value: "33", + label: "用户删除", + }, + { + value: "88", + label: "重置密码", + }, + { + value: "106", + label: "用户导入", + }, + { + value: "107", + label: "用户导出", + }, + ], + }, + { + value: "3", + label: "角色管理", + children: [ + { + value: "139", + label: "角色查询", + }, + { + value: "70", + label: "角色新增", + }, + { + value: "71", + label: "角色编辑", + }, + { + value: "72", + label: "角色删除", + }, + ], + }, + { + value: "4", + label: "菜单管理", + children: [ + { + value: "73", + label: "菜单新增", + }, + { + value: "140", + label: "菜单查询", + }, + { + value: "75", + label: "菜单删除", + }, + { + value: "74", + label: "菜单编辑", + }, + ], + }, + { + value: "5", + label: "部门管理", + children: [ + { + value: "76", + label: "部门新增", + }, + { + value: "141", + label: "部门查询", + }, + { + value: "77", + label: "部门编辑", + }, + { + value: "78", + label: "部门删除", + }, + ], + }, + { + value: "6", + label: "字典管理", + children: [ + { + value: "79", + label: "字典新增", + }, + { + value: "142", + label: "字典查询", + }, + { + value: "81", + label: "字典编辑", + }, + { + value: "84", + label: "字典删除", + }, + ], + }, + { + value: "117", + label: "系统日志", + }, + { + value: "135", + label: "字典项", + children: [ + { + value: "143", + label: "字典项查询", + }, + { + value: "136", + label: "字典项新增", + }, + { + value: "137", + label: "字典项编辑", + }, + { + value: "138", + label: "字典项删除", + }, + ], + }, + { + value: "120", + label: "系统配置", + children: [ + { + value: "121", + label: "系统配置查询", + }, + { + value: "122", + label: "系统配置新增", + }, + { + value: "123", + label: "系统配置修改", + }, + { + value: "124", + label: "系统配置删除", + }, + { + value: "125", + label: "系统配置刷新", + }, + ], + }, + { + value: "126", + label: "通知公告", + children: [ + { + value: "127", + label: "通知查询", + }, + { + value: "128", + label: "通知新增", + }, + { + value: "129", + label: "通知编辑", + }, + { + value: "130", + label: "通知删除", + }, + { + value: "133", + label: "通知发布", + }, + { + value: "134", + label: "通知撤回", + }, + ], + }, + ], + }, + { + value: "118", + label: "系统工具", + children: [ + { + value: "119", + label: "代码生成", + }, + ], + }, + { + value: "40", + label: "接口文档", + children: [ + { + value: "41", + label: "Apifox", + }, + ], + }, + { + value: "26", + label: "平台文档", + children: [ + { + value: "102", + label: "document", + }, + { + value: "30", + label: "平台文档(外链)", + }, + ], + }, + { + value: "20", + label: "多级菜单", + children: [ + { + value: "21", + label: "菜单一级", + children: [ + { + value: "22", + label: "菜单二级", + children: [ + { + value: "23", + label: "菜单三级-1", + }, + { + value: "24", + label: "菜单三级-2", + }, + ], + }, + ], + }, + ], + }, + { + value: "36", + label: "组件封装", + children: [ + { + value: "108", + label: "增删改查", + }, + { + value: "109", + label: "列表选择器", + }, + { + value: "37", + label: "富文本编辑器", + }, + { + value: "38", + label: "图片上传", + }, + { + value: "39", + label: "图标选择器", + }, + { + value: "95", + label: "字典组件", + }, + ], + }, + { + value: "110", + label: "路由参数", + children: [ + { + value: "111", + label: "参数(type=1)", + }, + { + value: "112", + label: "参数(type=2)", + }, + ], + }, + { + value: "89", + label: "功能演示", + children: [ + { + value: "97", + label: "Icons", + }, + { + value: "90", + label: "Websocket", + }, + { + value: "91", + label: "敬请期待...", + }, + ], + }, + ], + + msg: "一切ok", + }, + }, + + // 新增菜单 + { + url: "menus", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "新增菜单" + body.name + "成功", + }; + }, + }, + + // 获取菜单表单数据 + { + url: "menus/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: menuMap[params.id], + msg: "一切ok", + }; + }, + }, + + // 修改菜单 + { + url: "menus/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改菜单" + body.name + "成功", + }; + }, + }, + + // 删除菜单 + { + url: "menus/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除菜单" + params.id + "成功", + }; + }, + }, +]); + +// 菜单映射表数据 +const menuMap: Record = { + 1: { + id: 1, + parentId: 0, + name: "系统管理", + type: "CATALOG", + routeName: "", + routePath: "/system", + component: "Layout", + perm: null, + visible: 1, + sort: 1, + icon: "system", + redirect: "/system/user", + keepAlive: null, + alwaysShow: null, + params: null, + }, + 2: { + id: 2, + parentId: 1, + name: "用户管理", + type: "MENU", + routeName: "User", + routePath: "user", + component: "system/user/index", + perm: null, + visible: 1, + sort: 1, + icon: "user", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 3: { + id: 3, + parentId: 1, + name: "角色管理", + type: "MENU", + routeName: "Role", + routePath: "role", + component: "system/role/index", + perm: null, + visible: 1, + sort: 2, + icon: "role", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 4: { + id: 4, + parentId: 1, + name: "菜单管理", + type: "MENU", + routeName: "Menu", + routePath: "menu", + component: "system/menu/index", + perm: null, + visible: 1, + sort: 3, + icon: "menu", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 5: { + id: 5, + parentId: 1, + name: "部门管理", + type: "MENU", + routeName: "Dept", + routePath: "dept", + component: "system/dept/index", + perm: null, + visible: 1, + sort: 4, + icon: "tree", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, + 6: { + id: 6, + parentId: 1, + name: "字典管理", + type: "MENU", + routeName: "Dict", + routePath: "dict", + component: "system/dict/index", + perm: null, + visible: 1, + sort: 5, + icon: "dict", + redirect: null, + keepAlive: 1, + alwaysShow: null, + }, +}; diff --git a/mock/notice.mock.ts b/mock/notice.mock.ts new file mode 100644 index 0000000..c140c16 --- /dev/null +++ b/mock/notice.mock.ts @@ -0,0 +1,416 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "notices/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 1, + title: "v2.12.0 新增系统日志,访问趋势统计功能。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:21", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 2, + title: "v2.13.0 新增菜单搜索。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:22", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 3, + title: "\r\nv2.14.0 新增个人中心。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:23", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 4, + title: "v2.15.0 登录页面改造。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:24", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 5, + title: "v2.16.0 通知公告、字典翻译组件。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:25", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 6, + title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", + publishStatus: 1, + type: 2, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:26", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 7, + title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", + publishStatus: 1, + type: 3, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:27", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 8, + title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", + publishStatus: 1, + type: 4, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:28", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 9, + title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", + publishStatus: 1, + type: 5, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:29", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + { + id: 10, + title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + publishTime: "2024-09-30 17:30", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + ], + total: 10, + }, + msg: "一切ok", + }, + }, + + // 新增通知 + { + url: "notices", + method: ["POST"], + body() { + return { + code: "00000", + data: null, + msg: "新增成功", + }; + }, + }, + + // 获取通知表单数据 + { + url: "notices/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: noticeMap[params.id], + msg: "一切ok", + }; + }, + }, + + // 获取通知详情 + { + url: "notices/:id/detail", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: noticeMap[params.id], + msg: "一切ok", + }; + }, + }, + // 修改通知 + { + url: "roles/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改通知" + body.name + "成功", + }; + }, + }, + + // 删除通知 + { + url: "roles/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除通知" + params.id + "成功", + }; + }, + }, + + // 我的通知分页列表 + { + url: "notices/my-page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 10, + title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", + type: 1, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:30", + isRead: 0, + }, + { + id: 9, + title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", + type: 5, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:29", + isRead: 0, + }, + { + id: 8, + title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", + type: 4, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:28", + isRead: 0, + }, + { + id: 7, + title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", + type: 3, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:27", + isRead: 0, + }, + { + id: 6, + title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", + type: 2, + level: "L", + publisherName: "系统管理员", + publishTime: "2024-09-30 17:26", + isRead: 0, + }, + ], + total: 10, + }, + msg: "一切ok", + }, + }, +]); + +// 通知映射表数据 +const noticeMap: Record = { + 1: { + id: 1, + title: "v2.12.0 新增系统日志,访问趋势统计功能。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:21", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 2: { + id: 2, + title: "v2.13.0 新增菜单搜索。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:22", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 3: { + id: 3, + title: "\r\nv2.14.0 新增个人中心。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:23", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 4: { + id: 4, + title: "v2.15.0 登录页面改造。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:24", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + + 5: { + id: 5, + title: "v2.16.0 通知公告、字典翻译组件。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:25", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 6: { + id: 6, + title: "系统将于本周六凌晨 2 点进行维护,预计维护时间为 2 小时。", + publishStatus: 1, + type: 2, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:26", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 7: { + id: 7, + title: "最近发现一些钓鱼邮件,请大家提高警惕,不要点击陌生链接。", + publishStatus: 1, + type: 3, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:27", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 8: { + id: 8, + title: "国庆假期从 10 月 1 日至 10 月 7 日放假,共 7 天。", + publishStatus: 1, + type: 4, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:28", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 9: { + id: 9, + title: "公司将在 10 月 15 日举办新产品发布会,敬请期待。", + publishStatus: 1, + type: 5, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:29", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, + 10: { + id: 10, + title: "v2.16.1 版本修复了 WebSocket 重复连接导致的后台线程阻塞问题,优化了通知公告。", + publishStatus: 1, + type: 1, + publisherName: "系统管理员", + level: "L", + levelLabel: null, + publishTime: "2024-09-30 17:30", + isRead: null, + targetType: 1, + createTime: "2024-09-28 11:21", + revokeTime: "2024-09-30 17:21", + }, +}; diff --git a/mock/role.mock.ts b/mock/role.mock.ts new file mode 100644 index 0000000..ee85699 --- /dev/null +++ b/mock/role.mock.ts @@ -0,0 +1,334 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "roles/options", + method: ["GET"], + body: { + code: "00000", + data: [ + { + value: 2, + label: "系统管理员", + }, + { + value: 4, + label: "系统管理员1", + }, + { + value: 5, + label: "系统管理员2", + }, + { + value: 6, + label: "系统管理员3", + }, + { + value: 7, + label: "系统管理员4", + }, + { + value: 8, + label: "系统管理员5", + }, + { + value: 9, + label: "系统管理员6", + }, + { + value: 10, + label: "系统管理员7", + }, + { + value: 11, + label: "系统管理员8", + }, + { + value: 12, + label: "系统管理员9", + }, + { + value: 3, + label: "访问游客", + }, + ], + msg: "一切ok", + }, + }, + + { + url: "roles/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 2, + name: "系统管理员", + code: "ADMIN", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 3, + name: "访问游客", + code: "GUEST", + status: 1, + sort: 3, + createTime: "2021-05-26 15:49:05", + updateTime: "2019-05-05 16:00:00", + }, + { + id: 4, + name: "系统管理员1", + code: "ADMIN1", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 5, + name: "系统管理员2", + code: "ADMIN2", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 6, + name: "系统管理员3", + code: "ADMIN3", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 7, + name: "系统管理员4", + code: "ADMIN4", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 8, + name: "系统管理员5", + code: "ADMIN5", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 9, + name: "系统管理员6", + code: "ADMIN6", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: "2023-12-04 11:43:15", + }, + { + id: 10, + name: "系统管理员7", + code: "ADMIN7", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + { + id: 11, + name: "系统管理员8", + code: "ADMIN8", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + ], + total: 10, + }, + msg: "一切ok", + }, + }, + + // 新增角色 + { + url: "roles", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "新增角色" + body.name + "成功", + }; + }, + }, + + // 获取角色表单数据 + { + url: "roles/:id/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: roleMap[params.id], + msg: "一切ok", + }; + }, + }, + // 修改角色 + { + url: "roles/:id", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改角色" + body.name + "成功", + }; + }, + }, + + // 删除角色 + { + url: "roles/:id", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除角色" + params.id + "成功", + }; + }, + }, + // 获取角色拥有的菜单ID + { + url: "roles/:id/menuIds", + method: ["GET"], + body: ({}) => { + return { + code: "00000", + data: [ + 1, 2, 31, 32, 33, 88, 3, 70, 71, 72, 4, 73, 75, 74, 5, 76, 77, 78, 6, 79, 81, 84, 85, 86, + 87, 40, 41, 26, 30, 20, 21, 22, 23, 24, 89, 90, 91, 36, 37, 38, 39, 93, 94, 95, 97, 102, + 89, 90, 91, 93, 94, 95, 97, 102, 103, 104, + ], + msg: "一切ok", + }; + }, + }, + // 保存角色菜单 + { + url: "roles/:id/menus", + method: ["PUT"], + body: { + code: "00000", + data: null, + msg: "一切ok", + }, + }, +]); + +// 角色映射表数据 +const roleMap: Record = { + 2: { + id: 2, + name: "系统管理员", + code: "ADMIN", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 3: { + id: 3, + name: "访问游客", + code: "GUEST", + status: 1, + sort: 3, + createTime: "2021-05-26 15:49:05", + updateTime: "2019-05-05 16:00:00", + }, + 4: { + id: 4, + name: "系统管理员1", + code: "ADMIN1", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 5: { + id: 5, + name: "系统管理员2", + code: "ADMIN2", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + + 6: { + id: 6, + name: "系统管理员3", + code: "ADMIN3", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 7: { + id: 7, + name: "系统管理员4", + code: "ADMIN4", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 8: { + id: 8, + name: "系统管理员5", + code: "ADMIN5", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 9: { + id: 9, + name: "系统管理员6", + code: "ADMIN6", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: "2023-12-04 11:43:15", + }, + 10: { + id: 10, + name: "系统管理员7", + code: "ADMIN7", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, + 11: { + id: 11, + name: "系统管理员8", + code: "ADMIN8", + status: 1, + sort: 2, + createTime: "2021-03-25 12:39:54", + updateTime: null, + }, +}; diff --git a/mock/user.mock.ts b/mock/user.mock.ts new file mode 100644 index 0000000..bf2fc98 --- /dev/null +++ b/mock/user.mock.ts @@ -0,0 +1,251 @@ +import { defineMock } from "./base"; + +export default defineMock([ + { + url: "users/me", + method: ["GET"], + body: { + code: "00000", + data: { + userId: 2, + username: "admin", + nickname: "系统管理员", + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + roles: ["ADMIN"], + perms: [ + "sys:user:query", + "sys:user:add", + "sys:user:edit", + "sys:user:delete", + "sys:user:import", + "sys:user:export", + "sys:user:reset-password", + + "sys:role:query", + "sys:role:add", + "sys:role:edit", + "sys:role:delete", + + "sys:dept:query", + "sys:dept:add", + "sys:dept:edit", + "sys:dept:delete", + + "sys:menu:query", + "sys:menu:add", + "sys:menu:edit", + "sys:menu:delete", + + "sys:dict:query", + "sys:dict:add", + "sys:dict:edit", + "sys:dict:delete", + "sys:dict:delete", + + "sys:dict-item:query", + "sys:dict-item:add", + "sys:dict-item:edit", + "sys:dict-item:delete", + + "sys:notice:query", + "sys:notice:add", + "sys:notice:edit", + "sys:notice:delete", + "sys:notice:revoke", + "sys:notice:publish", + + "sys:config:query", + "sys:config:add", + "sys:config:update", + "sys:config:delete", + "sys:config:refresh", + ], + }, + msg: "一切ok", + }, + }, + + { + url: "users/page", + method: ["GET"], + body: { + code: "00000", + data: { + list: [ + { + id: 2, + username: "admin", + nickname: "系统管理员", + mobile: "17621210366", + gender: 1, + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "", + status: 1, + deptId: 1, + roleIds: [2], + }, + { + id: 3, + username: "test", + nickname: "测试小用户", + mobile: "17621210366", + gender: 1, + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "youlaitech@163.com", + status: 1, + deptId: 3, + roleIds: [3], + }, + ], + total: 2, + }, + msg: "一切ok", + }, + }, + + // 新增用户 + { + url: "users", + method: ["POST"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "新增用户" + body.nickname + "成功", + }; + }, + }, + + // 获取用户表单数据 + { + url: "users/:userId/form", + method: ["GET"], + body: ({ params }) => { + return { + code: "00000", + data: userMap[params.userId], + msg: "一切ok", + }; + }, + }, + // 修改用户 + { + url: "users/:userId", + method: ["PUT"], + body({ body }) { + return { + code: "00000", + data: null, + msg: "修改用户" + body.nickname + "成功", + }; + }, + }, + + // 删除用户 + { + url: "users/:userId", + method: ["DELETE"], + body({ params }) { + return { + code: "00000", + data: null, + msg: "删除用户" + params.id + "成功", + }; + }, + }, + + // 重置密码 + { + url: "users/:userId/password/reset", + method: ["PUT"], + body({ query }) { + return { + code: "00000", + data: null, + msg: "重置密码成功,新密码为:" + query.password, + }; + }, + }, + + // 导出Excel + { + url: "users/_export", + method: ["GET"], + headers: { + "Content-Disposition": "attachment; filename=%E7%94%A8%E6%88%B7%E5%88%97%E8%A1%A8.xlsx", + "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }, + }, + + { + url: "users/profile", + method: ["GET"], + body: { + code: "00000", + data: { + id: 2, + username: "admin", + nickname: "系统管理员", + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + gender: 1, + mobile: "17621210366", + email: null, + deptName: "有来技术", + roleNames: "系统管理员", + createTime: "2019-10-10", + }, + }, + }, + + { + url: "users/profile", + method: ["PUT"], + body() { + return { + code: "00000", + data: null, + msg: "修改个人信息成功", + }; + }, + }, + + { + url: "users/password", + method: ["PUT"], + body() { + return { + code: "00000", + data: null, + msg: "修改密码成功", + }; + }, + }, +]); + +// 用户映射表数据 +const userMap: Record = { + 2: { + id: 2, + username: "admin", + nickname: "系统管理员", + mobile: "17621210366", + gender: 1, + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "", + status: 1, + deptId: 1, + roleIds: [2], + }, + 3: { + id: 3, + username: "test", + nickname: "测试小用户", + mobile: "17621210366", + gender: 1, + avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif", + email: "youlaitech@163.com", + status: 1, + deptId: 3, + roleIds: [3], + }, +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..79308da --- /dev/null +++ b/package.json @@ -0,0 +1,128 @@ +{ + "name": "红与蓝授权管理", + "description": "红与蓝授权管理系统", + "version": "1.8", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit & vite build", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit", + "lint:eslint": "eslint --cache \"src/**/*.{vue,ts,js}\" --fix", + "lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,css,scss,vue,html,md}\"", + "lint:stylelint": "stylelint --cache \"**/*.{css,scss,vue}\" --fix", + "lint:lint-staged": "lint-staged", + "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:stylelint", + "preinstall": "npx only-allow pnpm", + "prepare": "husky", + "commit": "git-cz" + }, + "config": { + "commitizen": { + "path": "node_modules/cz-git" + } + }, + "lint-staged": { + "*.{js,ts}": [ + "eslint --fix", + "prettier --write" + ], + "*.{cjs,json}": [ + "prettier --write" + ], + "*.{vue,html}": [ + "eslint --fix", + "prettier --write", + "stylelint --fix" + ], + "*.{scss,css}": [ + "stylelint --fix", + "prettier --write" + ], + "*.md": [ + "prettier --write" + ] + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@stomp/stompjs": "^7.1.1", + "@vueuse/core": "^12.8.2", + "@wangeditor-next/editor": "^5.6.34", + "@wangeditor-next/editor-for-vue": "^5.1.14", + "animate.css": "^4.1.1", + "axios": "^1.9.0", + "codemirror": "^5.65.19", + "codemirror-editor-vue3": "^2.8.0", + "default-passive-events": "^2.0.0", + "echarts": "^5.6.0", + "element-plus": "^2.11.7", + "exceljs": "^4.4.0", + "lodash-es": "^4.17.21", + "nprogress": "^0.2.0", + "path-browserify": "^1.0.1", + "path-to-regexp": "^8.2.0", + "pinia": "^3.0.2", + "qs": "^6.14.0", + "sortablejs": "^1.15.6", + "vue": "^3.5.13", + "vue-draggable-plus": "^0.6.0", + "vue-i18n": "^11.1.3", + "vue-router": "^4.5.1" + }, + "devDependencies": { + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@eslint/js": "^9.26.0", + "@iconify/utils": "^2.3.0", + "@types/codemirror": "^5.60.15", + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.15.18", + "@types/nprogress": "^0.2.3", + "@types/path-browserify": "^1.0.3", + "@types/qs": "^6.9.18", + "@types/sortablejs": "^1.15.8", + "@typescript-eslint/eslint-plugin": "^8.32.1", + "@typescript-eslint/parser": "^8.32.1", + "@vitejs/plugin-vue": "^5.2.4", + "autoprefixer": "^10.4.21", + "commitizen": "^4.3.1", + "cz-git": "^1.11.1", + "eslint": "^9.26.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.0", + "eslint-plugin-vue": "^10.1.0", + "globals": "^15.15.0", + "husky": "^9.1.7", + "lint-staged": "^15.5.2", + "postcss": "^8.5.3", + "postcss-html": "^1.8.0", + "postcss-scss": "^4.0.9", + "prettier": "^3.5.3", + "sass": "^1.88.0", + "stylelint": "^16.19.1", + "stylelint-config-html": "^1.1.0", + "stylelint-config-recess-order": "^6.0.0", + "stylelint-config-recommended": "^15.0.0", + "stylelint-config-recommended-scss": "^14.1.0", + "stylelint-config-recommended-vue": "^1.6.0", + "stylelint-prettier": "^5.0.3", + "terser": "^5.39.1", + "typescript": "^5.8.3", + "typescript-eslint": "^8.32.1", + "unocss": "65.4.3", + "unplugin-auto-import": "^19.2.0", + "unplugin-vue-components": "^28.5.0", + "vite": "6.3.2", + "vite-plugin-mock-dev-server": "^1.8.7", + "vue-eslint-parser": "^10.1.3", + "vue-tsc": "^2.2.10" + }, + "engines": { + "node": ">=18.0.0" + }, + "repository": "https://gitee.com/youlaiorg/vue3-element-admin.git", + "author": "有来开源组织", + "license": "MIT" +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..328ef79852e3ce88858e7617d79f2146f06fc4ee GIT binary patch literal 3439 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 Z>n*&xiYE2qrC$6$#|-}$h5PS;{{X3grc(d_ literal 0 HcmV?d00001 diff --git a/public/files/20250717/332f8fa543dc4f7f8262e05aae95a110.jpg b/public/files/20250717/332f8fa543dc4f7f8262e05aae95a110.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c617464987cb7f50d5902f8e91e59771c55270aa GIT binary patch literal 19961 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�|+2EFuk;g2N*`^_p}2j+kp6!dyeik|3n*` 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 Z>n*&xiYE2qrC$6$#|-}$h5PS;{{X3grc(d_ literal 0 HcmV?d00001 diff --git a/public/files/20250717/de041f1795904b6c8a4e97ffb3072fb4.jpg b/public/files/20250717/de041f1795904b6c8a4e97ffb3072fb4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c617464987cb7f50d5902f8e91e59771c55270aa GIT binary patch literal 19961 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�|+2EFuk;g2N*`^_p}2j+kp6!dyeik|3n*` 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 literal 0 HcmV?d00001 diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..f13a1a0 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/api/auth.api.ts b/src/api/auth.api.ts new file mode 100644 index 0000000..35c28cf --- /dev/null +++ b/src/api/auth.api.ts @@ -0,0 +1,86 @@ +import request from "@/utils/request"; + +const AUTH_BASE_URL = "/api/v1/auth"; + +const AuthAPI = { + /** 登录接口*/ + login(data: LoginFormData) { + const formData = new FormData(); + formData.append("username", data.username); + formData.append("password", data.password); + formData.append("captchaKey", data.captchaKey); + formData.append("captchaCode", data.captchaCode); + return request({ + url: `${AUTH_BASE_URL}/login`, + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** 刷新 token 接口*/ + refreshToken(refreshToken: string) { + return request({ + url: `${AUTH_BASE_URL}/refresh-token`, + method: "post", + params: { refreshToken: refreshToken }, + headers: { + Authorization: "no-auth", + }, + }); + }, + + /** 注销登录接口 */ + logout() { + return request({ + url: `${AUTH_BASE_URL}/logout`, + method: "delete", + }); + }, + + /** 获取验证码接口*/ + getCaptcha() { + return request({ + url: `${AUTH_BASE_URL}/captcha`, + method: "get", + }); + }, +}; + +export default AuthAPI; + +/** 登录表单数据 */ +export interface LoginFormData { + /** 用户名 */ + username: string; + /** 密码 */ + password: string; + /** 验证码缓存key */ + captchaKey: string; + /** 验证码 */ + captchaCode: string; + /** 记住我 */ + rememberMe: boolean; +} + +/** 登录响应 */ +export interface LoginResult { + /** 访问令牌 */ + accessToken: string; + /** 刷新令牌 */ + refreshToken: string; + /** 令牌类型 */ + tokenType: string; + /** 过期时间(秒) */ + expiresIn: number; +} + +/** 验证码信息 */ +export interface CaptchaInfo { + /** 验证码缓存key */ + captchaKey: string; + /** 验证码图片Base64字符串 */ + captchaBase64: string; +} diff --git a/src/api/codegen.api.ts b/src/api/codegen.api.ts new file mode 100644 index 0000000..b3b613f --- /dev/null +++ b/src/api/codegen.api.ts @@ -0,0 +1,191 @@ +import request from "@/utils/request"; + +const GENERATOR_BASE_URL = "/api/v1/codegen"; + +const GeneratorAPI = { + /** 获取数据表分页列表 */ + getTablePage(params: TablePageQuery) { + return request>({ + url: `${GENERATOR_BASE_URL}/table/page`, + method: "get", + params: params, + }); + }, + + /** 获取代码生成配置 */ + getGenConfig(tableName: string) { + return request({ + url: `${GENERATOR_BASE_URL}/${tableName}/config`, + method: "get", + }); + }, + + /** 获取代码生成配置 */ + saveGenConfig(tableName: string, data: GenConfigForm) { + return request({ + url: `${GENERATOR_BASE_URL}/${tableName}/config`, + method: "post", + data: data, + }); + }, + + /** 获取代码生成预览数据 */ + getPreviewData(tableName: string) { + return request({ + url: `${GENERATOR_BASE_URL}/${tableName}/preview`, + method: "get", + }); + }, + + /** 重置代码生成配置 */ + resetGenConfig(tableName: string) { + return request({ + url: `${GENERATOR_BASE_URL}/${tableName}/config`, + method: "delete", + }); + }, + + /** + * 下载 ZIP 文件 + * @param url + * @param fileName + */ + download(tableName: string) { + return request({ + url: `${GENERATOR_BASE_URL}/${tableName}/download`, + method: "get", + responseType: "blob", + }).then((response) => { + const fileName = decodeURI( + response.headers["content-disposition"].split(";")[1].split("=")[1] + ); + + const blob = new Blob([response.data], { type: "application/zip" }); + const a = document.createElement("a"); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName; + a.click(); + window.URL.revokeObjectURL(url); + }); + }, +}; + +export default GeneratorAPI; + +/** 代码生成预览对象 */ +export interface GeneratorPreviewVO { + /** 文件生成路径 */ + path: string; + /** 文件名称 */ + fileName: string; + /** 文件内容 */ + content: string; +} + +/** 数据表分页查询参数 */ +export interface TablePageQuery extends PageQuery { + /** 关键字(表名) */ + keywords?: string; +} + +/** 数据表分页对象 */ +export interface TablePageVO { + /** 表名称 */ + tableName: string; + + /** 表描述 */ + tableComment: string; + + /** 存储引擎 */ + engine: string; + + /** 字符集排序规则 */ + tableCollation: string; + + /** 创建时间 */ + createTime: string; +} + +/** 代码生成配置表单 */ +export interface GenConfigForm { + /** 主键 */ + id?: string; + + /** 表名 */ + tableName?: string; + + /** 业务名 */ + businessName?: string; + + /** 模块名 */ + moduleName?: string; + + /** 包名 */ + packageName?: string; + + /** 实体名 */ + entityName?: string; + + /** 作者 */ + author?: string; + + /** 上级菜单 */ + parentMenuId?: string; + + /** 后端应用名 */ + backendAppName?: string; + /** 前端应用名 */ + frontendAppName?: string; + + /** 字段配置列表 */ + fieldConfigs?: FieldConfig[]; +} + +/** 字段配置 */ +export interface FieldConfig { + /** 主键 */ + id?: string; + + /** 列名 */ + columnName?: string; + + /** 列类型 */ + columnType?: string; + + /** 字段名 */ + fieldName?: string; + + /** 字段类型 */ + fieldType?: string; + + /** 字段描述 */ + fieldComment?: string; + + /** 是否在列表显示 */ + isShowInList?: number; + + /** 是否在表单显示 */ + isShowInForm?: number; + + /** 是否在查询条件显示 */ + isShowInQuery?: number; + + /** 是否必填 */ + isRequired?: number; + + /** 表单类型 */ + formType?: number; + + /** 查询类型 */ + queryType?: number; + + /** 字段长度 */ + maxLength?: number; + + /** 字段排序 */ + fieldSort?: number; + + /** 字典类型 */ + dictType?: string; +} diff --git a/src/api/file.api.ts b/src/api/file.api.ts new file mode 100644 index 0000000..7c1b1d4 --- /dev/null +++ b/src/api/file.api.ts @@ -0,0 +1,83 @@ +import request from "@/utils/request"; + +const FileAPI = { + /** + * 上传文件 + * + * @param formData + */ + upload(formData: FormData) { + return request({ + url: "/api/v1/files", + method: "post", + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** + * 上传文件 + */ + uploadFile(file: File, path?: string) { + const formData = new FormData(); + formData.append("file", file); + formData.append("path", path ? path : ""); + return request({ + url: "/api/v1/files", + method: "post", + timeout: 300000, + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** + * 删除文件 + * + * @param filePath 文件完整路径 + */ + delete(filePath?: string) { + return request({ + url: "/api/v1/files", + method: "delete", + params: { filePath: filePath }, + }); + }, + + /** + * 下载文件 + * @param url + * @param fileName + */ + download(url: string, fileName?: string) { + return request({ + url: url, + method: "get", + responseType: "blob", + }).then((res) => { + const blob = new Blob([res.data]); + const a = document.createElement("a"); + const url = window.URL.createObjectURL(blob); + a.href = url; + a.download = fileName || "下载文件"; + a.click(); + window.URL.revokeObjectURL(url); + }); + }, +}; + +export default FileAPI; + +/** + * 文件API类型声明 + */ +export interface FileInfo { + /** 文件名 */ + name: string; + /** 文件路径 */ + url: string; +} diff --git a/src/api/rnb/index.ts b/src/api/rnb/index.ts new file mode 100644 index 0000000..42ab568 --- /dev/null +++ b/src/api/rnb/index.ts @@ -0,0 +1,254 @@ +import request from "@/utils/request"; + +const RNB_BASE_URL = "/api/v1/rnb"; + +const RnBAPI = { + pageAccAdvance(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageAccAdvance`, + method: "get", + params: query, + }); + }, + pageAccBase(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageAccBase`, + method: "get", + params: query, + }); + }, + pageAccAdmin(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageAccAdmin`, + method: "get", + params: query, + }); + }, + newAccount() { + return request({ + url: `${RNB_BASE_URL}/newAccount`, + method: "post", + }); + }, + saveAccount(account: BaseAccount) { + return request({ + url: `${RNB_BASE_URL}/saveAccount`, + method: "post", + data: account, + }); + }, + setAccStatusById(id: number, status: number) { + return request({ + url: `${RNB_BASE_URL}/account/${id}`, + method: "put", + params: { status: status }, + }); + }, + delAccount(id: number) { + return request({ + url: `${RNB_BASE_URL}/account/${id}`, + method: "delete", + }); + }, + + pageExent(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageExtend`, + method: "get", + params: query, + }); + }, + + saveExtend(extend: ExtendVO) { + return request({ + url: `${RNB_BASE_URL}/saveExtend`, + method: "post", + data: extend, + }); + }, + + cancelExtendById(id: number) { + return request({ + url: `${RNB_BASE_URL}/extend/${id}`, + method: "put", + }); + }, + + pageOplog(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageOplog`, + method: "get", + params: query, + }); + }, + + pageAmount(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageAmount`, + method: "get", + params: query, + }); + }, + + pageAmountUser(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageAmountUser`, + method: "get", + params: query, + }); + }, + pageAmountSum(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageAmountSum`, + method: "get", + params: query, + }); + }, + pageSoftware(query: PageQuery) { + return request>({ + url: `${RNB_BASE_URL}/pageSoftware`, + method: "get", + params: query, + }); + }, + saveSoftware(sw: SoftwareVO) { + return request({ + url: `${RNB_BASE_URL}/saveSoftware`, + method: "post", + data: sw, + }); + }, + setSoftwareById(id: number, status: number) { + return request({ + url: `${RNB_BASE_URL}/software/${id}`, + method: "put", + params: { status: status }, + }); + }, +}; + +export default RnBAPI; + +export interface BaseAccount { + id?: number; //演出ID + bacAccount?: string; //百家乐账号 + bacPlatform?: string; //百家乐平台 + linkAccount?: string; //关联账号 + linkPlatform?: string; //关联平台 + expireTime?: number; //有效期 + remark?: string; //注释 + status?: number; //状态 +} +export interface AccountVO extends BaseAccount { + addTime?: number; //添加时间 + addBy?: string; //添加入 + editBy?: string; //修改人 + logTime?: number; //登录时间 + logCount?: number; //登录次数 + appVer?: number; //app版本 + amount?: number; //实打流水 +} +export interface AccountPageQuery extends PageQuery { + bacAccount?: string; //百家乐账号 + linkAccount?: string; //关联账号 + linkPlatform?: string; //关联平台 + orderBy?: string; //排序列 + order?: string; //排序方式 +} + +export interface ExtendVO { + id?: number; + accId?: number; + bacAccount?: string; + bacPlatform?: string; + linkAccount?: string; + linkPlatform?: string; + extend?: number; + type?: number; + fee?: number; + reason?: string; + addBy?: string; + addTime?: number; + cancel?: number; + remark?: string; + editBy?: string; + editTime?: number; +} +export interface ExtendPageQuery extends PageQuery { + type?: number; //类型 免费/收费 + bacAccount?: string; //百家乐账号 + cancel?: number; //是否撤销 + opTime?: number[]; //操作时间段 +} + +export interface OplogVO { + id?: number; + opertion?: number; //操作 + opBy?: string; //操作人 + remark?: string; //操作描述 + opTime?: number; //操作时间 +} +export interface OplogPageQuery extends PageQuery { + operation?: number; //操作 + opBy?: string; //操作人 + keywords?: string; //操作描述关键字 + opTime?: number[]; //操作时间段 +} +export interface AmountVO { + id: number; + uid: number; //用户ID + amount: number; //流水 + reportTime: number; //上报时间 +} +export interface AmountUserVO { + uid: number; //用户ID + bacAccount?: string; + bacPlatform?: string; + linkAccount?: string; + linkPlatform?: string; + amount: number; //流水 +} +export interface AmountPageQuery extends PageQuery { + uid?: number; //用户ID + bacAccount?: string; //百家乐账户 + bacPlatform?: string; //百家乐平台 + linkPlatform?: string; //关联平台 + reportTime?: number[]; //上报时间段 +} +export interface AmountSumVO { + sid: number; //日期ID + amountTotal: number; //总流水 + amountHy?: number; //恒耀流水 + amountHb?: number; //浩博流水 + amountCly?: number; //彩乐园流水 + amountAbg?: number; //欧博流水 + amountDb?: number; //多宝流水 + amountOther?: number; //其它流水 +} +export interface AmountSumPageQuery extends PageQuery { + sumType?: number; //统计模式:0=日/1=周/2=月/3=年 + ranges?: number[]; //查询时间段 +} +export interface SoftwareVO { + sid?: number; //软件ID + sname?: string; //软件名称 + verLatest?: number; //最新版本号 + verValid?: number; //有效版本号 + upgradePath?: string; //更新包地址 + deprecated?: number; //停用状态 0=正常 1=停用 +} + +export const LinkPlatforms = [ + { + value: "恒耀", + label: "恒耀", + }, + { + value: "浩博", + label: "浩博", + }, + { + value: "彩乐园", + label: "彩乐园", + }, +]; diff --git a/src/api/system/config.api.ts b/src/api/system/config.api.ts new file mode 100644 index 0000000..f280ee1 --- /dev/null +++ b/src/api/system/config.api.ts @@ -0,0 +1,95 @@ +import request from "@/utils/request"; + +const CONFIG_BASE_URL = "/api/v1/config"; + +const ConfigAPI = { + /** 系统配置分页 */ + getPage(queryParams?: ConfigPageQuery) { + return request>({ + url: `${CONFIG_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** 系统配置表单数据 */ + getFormData(id: string) { + return request({ + url: `${CONFIG_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** 新增系统配置 */ + create(data: ConfigForm) { + return request({ + url: `${CONFIG_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** 更新系统配置 */ + update(id: string, data: ConfigForm) { + return request({ + url: `${CONFIG_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 删除系统配置 + * + * @param ids 系统配置ID + */ + deleteById(id: string) { + return request({ + url: `${CONFIG_BASE_URL}/${id}`, + method: "delete", + }); + }, + + refreshCache() { + return request({ + url: `${CONFIG_BASE_URL}/refresh`, + method: "PUT", + }); + }, +}; + +export default ConfigAPI; + +/** $系统配置分页查询参数 */ +export interface ConfigPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string; +} + +/** 系统配置表单对象 */ +export interface ConfigForm { + /** 主键 */ + id?: string; + /** 配置名称 */ + configName?: string; + /** 配置键 */ + configKey?: string; + /** 配置值 */ + configValue?: string; + /** 描述、备注 */ + remark?: string; +} + +/** 系统配置分页对象 */ +export interface ConfigPageVO { + /** 主键 */ + id?: string; + /** 配置名称 */ + configName?: string; + /** 配置键 */ + configKey?: string; + /** 配置值 */ + configValue?: string; + /** 描述、备注 */ + remark?: string; +} diff --git a/src/api/system/dept.api.ts b/src/api/system/dept.api.ts new file mode 100644 index 0000000..200065d --- /dev/null +++ b/src/api/system/dept.api.ts @@ -0,0 +1,130 @@ +import request from "@/utils/request"; + +const DEPT_BASE_URL = "/api/v1/dept"; + +const DeptAPI = { + /** + * 获取部门列表 + * + * @param queryParams 查询参数(可选) + * @returns 部门树形表格数据 + */ + getList(queryParams?: DeptQuery) { + return request({ + url: `${DEPT_BASE_URL}`, + method: "get", + params: queryParams, + }); + }, + + /** 获取部门下拉列表 */ + getOptions() { + return request({ + url: `${DEPT_BASE_URL}/options`, + method: "get", + }); + }, + + /** + * 获取部门表单数据 + * + * @param id 部门ID + * @returns 部门表单数据 + */ + getFormData(id: string) { + return request({ + url: `${DEPT_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 新增部门 + * + * @param data 部门表单数据 + * @returns 请求结果 + */ + create(data: DeptForm) { + return request({ + url: `${DEPT_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 修改部门 + * + * @param id 部门ID + * @param data 部门表单数据 + * @returns 请求结果 + */ + update(id: string, data: DeptForm) { + return request({ + url: `${DEPT_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 删除部门 + * + * @param ids 部门ID,多个以英文逗号(,)分隔 + * @returns 请求结果 + */ + deleteByIds(ids: string) { + return request({ + url: `${DEPT_BASE_URL}/${ids}`, + method: "delete", + }); + }, +}; + +export default DeptAPI; + +/** 部门查询参数 */ +export interface DeptQuery { + /** 搜索关键字 */ + keywords?: string; + /** 状态 */ + status?: number; +} + +/** 部门类型 */ +export interface DeptVO { + /** 子部门 */ + children?: DeptVO[]; + /** 创建时间 */ + createTime?: Date; + /** 部门ID */ + id?: string; + /** 部门名称 */ + name?: string; + /** 部门编号 */ + code?: string; + /** 父部门ID */ + parentid?: string; + /** 排序 */ + sort?: number; + /** 状态(1:启用;0:禁用) */ + status?: number; + /** 修改时间 */ + updateTime?: Date; +} + +/** 部门表单类型 */ +export interface DeptForm { + /** 部门ID(新增不填) */ + id?: string; + /** 部门名称 */ + name?: string; + /** 部门编号 */ + code?: string; + /** 父部门ID */ + parentId: string; + /** 排序 */ + sort?: number; + /** 状态(1:启用;0:禁用) */ + status?: number; +} diff --git a/src/api/system/dict.api.ts b/src/api/system/dict.api.ts new file mode 100644 index 0000000..2c2d9f8 --- /dev/null +++ b/src/api/system/dict.api.ts @@ -0,0 +1,310 @@ +import request from "@/utils/request"; + +const DICT_BASE_URL = "/api/v1/dicts"; + +const DictAPI = { + //--------------------------------------------------- + // 字典相关接口 + //--------------------------------------------------- + + /** + * 字典分页列表 + * + * @param queryParams 查询参数 + * @returns 字典分页结果 + */ + getPage(queryParams: DictPageQuery) { + return request>({ + url: `${DICT_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 字典列表 + * + * @returns 字典列表 + */ + getList() { + return request({ + url: `${DICT_BASE_URL}`, + method: "get", + }); + }, + + /** + * 字典表单数据 + * + * @param id 字典ID + * @returns 字典表单数据 + */ + getFormData(id: string) { + return request({ + url: `${DICT_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 新增字典 + * + * @param data 字典表单数据 + */ + create(data: DictForm) { + return request({ + url: `${DICT_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 修改字典 + * + * @param id 字典ID + * @param data 字典表单数据 + */ + update(id: string, data: DictForm) { + return request({ + url: `${DICT_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 删除字典 + * + * @param ids 字典ID,多个以英文逗号(,)分隔 + */ + deleteByIds(ids: string) { + return request({ + url: `${DICT_BASE_URL}/${ids}`, + method: "delete", + }); + }, + + //--------------------------------------------------- + // 字典项相关接口 + //--------------------------------------------------- + /** + * 获取字典分页列表 + * + * @param queryParams 查询参数 + * @returns 字典分页结果 + */ + getDictItemPage(dictCode: string, queryParams: DictItemPageQuery) { + return request>({ + url: `${DICT_BASE_URL}/${dictCode}/items/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取字典项列表 + */ + getDictItems(dictCode: string) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items`, + method: "get", + }); + }, + + /** + * 新增字典项 + */ + createDictItem(dictCode: string, data: DictItemForm) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items`, + method: "post", + data: data, + }); + }, + + /** + * 获取字典项表单数据 + * + * @param id 字典项ID + * @returns 字典项表单数据 + */ + getDictItemFormData(dictCode: string, id: string) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items/${id}/form`, + method: "get", + }); + }, + + /** + * 修改字典项 + */ + updateDictItem(dictCode: string, id: string, data: DictItemForm) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 删除字典项 + */ + deleteDictItems(dictCode: string, ids: string) { + return request({ + url: `${DICT_BASE_URL}/${dictCode}/items/${ids}`, + method: "delete", + }); + }, +}; + +export default DictAPI; + +/** + * 字典查询参数 + */ +export interface DictPageQuery extends PageQuery { + /** + * 关键字(字典名称/编码) + */ + keywords?: string; + + /** + * 字典状态(1:启用,0:禁用) + */ + status?: number; +} + +/** + * 字典分页对象 + */ +export interface DictPageVO { + /** + * 字典ID + */ + id: string; + /** + * 字典名称 + */ + name: string; + /** + * 字典编码 + */ + dictCode: string; + /** + * 字典状态(1:启用,0:禁用) + */ + status: number; +} + +/** + * 字典 + */ +export interface DictForm { + /** + * 字典ID + */ + id?: string; + /** + * 字典名称 + */ + name?: string; + /** + * 字典编码 + */ + dictCode?: string; + /** + * 字典状态(1-启用,0-禁用) + */ + status?: number; + /** + * 备注 + */ + remark?: string; +} + +/** + * 字典查询参数 + */ +export interface DictItemPageQuery extends PageQuery { + /** 关键字(字典数据值/标签) */ + keywords?: string; + + /** 字典编码 */ + dictCode?: string; +} + +/** + * 字典分页对象 + */ +export interface DictItemPageVO { + /** + * 字典ID + */ + id: string; + /** + * 字典编码 + */ + dictCode: string; + /** + * 字典数据值 + */ + value: string; + /** + * 字典数据标签 + */ + label: string; + /** + * 状态(1:启用,0:禁用) + */ + status: number; + /** + * 字典排序 + */ + sort?: number; +} + +/** + * 字典 + */ +export interface DictItemForm { + /** + * 字典ID + */ + id?: string; + /** + * 字典编码 + */ + dictCode?: string; + /** + * 字典数据值 + */ + value?: string; + /** + * 字典数据标签 + */ + label?: string; + /** + * 状态(1:启用,0:禁用) + */ + status?: number; + /** + * 字典排序 + */ + sort?: number; + + /** + * 标签类型 + */ + tagType?: "success" | "warning" | "info" | "primary" | "danger" | undefined; +} + +/** + * 字典项下拉选项 + */ +export interface DictItemOption { + value: number | string; + label: string; + tagType?: "" | "success" | "info" | "warning" | "danger"; + [key: string]: any; +} diff --git a/src/api/system/log.api.ts b/src/api/system/log.api.ts new file mode 100644 index 0000000..402478a --- /dev/null +++ b/src/api/system/log.api.ts @@ -0,0 +1,121 @@ +import request from "@/utils/request"; + +const LOG_BASE_URL = "/api/v1/logs"; + +const LogAPI = { + /** + * 获取日志分页列表 + * + * @param queryParams 查询参数 + */ + getPage(queryParams: LogPageQuery) { + return request>({ + url: `${LOG_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取访问趋势 + * + * @param queryParams + * @returns + */ + getVisitTrend(queryParams: VisitTrendQuery) { + return request({ + url: `${LOG_BASE_URL}/visit-trend`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取访问统计 + * + * @param queryParams + * @returns + */ + getVisitStats() { + return request({ + url: `${LOG_BASE_URL}/visit-stats`, + method: "get", + }); + }, +}; + +export default LogAPI; + +/** + * 日志分页查询对象 + */ +export interface LogPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string; + /** 操作时间 */ + createTime?: [string, string]; +} + +/** + * 系统日志分页VO + */ +export interface LogPageVO { + /** 主键 */ + id: string; + /** 日志模块 */ + module: string; + /** 日志内容 */ + content: string; + /** 请求路径 */ + requestUri: string; + /** 请求方法 */ + method: string; + /** IP 地址 */ + ip: string; + /** 地区 */ + region: string; + /** 浏览器 */ + browser: string; + /** 终端系统 */ + os: string; + /** 执行时间(毫秒) */ + executionTime: number; + /** 操作人 */ + operator: string; +} + +/** 访问趋势视图对象 */ +export interface VisitTrendVO { + /** 日期列表 */ + dates: string[]; + /** 浏览量(PV) */ + pvList: number[]; + /** 访客数(UV) */ + uvList: number[]; + /** IP数 */ + ipList: number[]; +} + +/** 访问趋势查询参数 */ +export interface VisitTrendQuery { + /** 开始日期 */ + startDate: string; + /** 结束日期 */ + endDate: string; +} + +/** 访问统计 */ +export interface VisitStatsVO { + /** 今日访客数(UV) */ + todayUvCount: number; + /** 总访客数 */ + totalUvCount: number; + /** 访客数同比增长率(相对于昨天同一时间段的增长率) */ + uvGrowthRate: number; + /** 今日浏览量(PV) */ + todayPvCount: number; + /** 总浏览量 */ + totalPvCount: number; + /** 同比增长率(相对于昨天同一时间段的增长率) */ + pvGrowthRate: number; +} diff --git a/src/api/system/menu.api.ts b/src/api/system/menu.api.ts new file mode 100644 index 0000000..06c11ed --- /dev/null +++ b/src/api/system/menu.api.ts @@ -0,0 +1,209 @@ +import request from "@/utils/request"; +// 菜单基础URL +const MENU_BASE_URL = "/api/v1/menus"; + +const MenuAPI = { + /** + * 获取当前用户的路由列表 + *

+ * 无需传入角色,后端解析token获取角色自行判断是否拥有路由的权限 + * + * @returns 路由列表 + */ + getRoutes() { + return request({ + url: `${MENU_BASE_URL}/routes`, + method: "get", + }); + }, + + /** + * 获取菜单树形列表 + * + * @param queryParams 查询参数 + * @returns 菜单树形列表 + */ + getList(queryParams: MenuQuery) { + return request({ + url: `${MENU_BASE_URL}`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取菜单下拉数据源 + * + * @returns 菜单下拉数据源 + */ + getOptions(onlyParent?: boolean) { + return request({ + url: `${MENU_BASE_URL}/options`, + method: "get", + params: { onlyParent: onlyParent }, + }); + }, + + /** + * 获取菜单表单数据 + * + * @param id 菜单ID + */ + getFormData(id: string) { + return request({ + url: `${MENU_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 添加菜单 + * + * @param data 菜单表单数据 + * @returns 请求结果 + */ + create(data: MenuForm) { + return request({ + url: `${MENU_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 修改菜单 + * + * @param id 菜单ID + * @param data 菜单表单数据 + * @returns 请求结果 + */ + update(id: string, data: MenuForm) { + return request({ + url: `${MENU_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 删除菜单 + * + * @param id 菜单ID + * @returns 请求结果 + */ + deleteById(id: string) { + return request({ + url: `${MENU_BASE_URL}/${id}`, + method: "delete", + }); + }, +}; + +export default MenuAPI; + +import type { MenuTypeEnum } from "@/enums/system/menu.enum"; + +/** 菜单查询参数 */ +export interface MenuQuery { + /** 搜索关键字 */ + keywords?: string; +} + +/** 菜单视图对象 */ +export interface MenuVO { + /** 子菜单 */ + children?: MenuVO[]; + /** 组件路径 */ + component?: string; + /** ICON */ + icon?: string; + /** 菜单ID */ + id?: string; + /** 菜单名称 */ + name?: string; + /** 父菜单ID */ + parentId?: string; + /** 按钮权限标识 */ + perm?: string; + /** 跳转路径 */ + redirect?: string; + /** 路由名称 */ + routeName?: string; + /** 路由相对路径 */ + routePath?: string; + /** 菜单排序(数字越小排名越靠前) */ + sort?: number; + /** 菜单 */ + type?: MenuTypeEnum; + /** 菜单是否可见(1:显示;0:隐藏) */ + visible?: number; +} + +/** 菜单表单对象 */ +export interface MenuForm { + /** 菜单ID */ + id?: string; + /** 父菜单ID */ + parentId?: string; + /** 菜单名称 */ + name?: string; + /** 菜单是否可见(1-是 0-否) */ + visible: number; + /** ICON */ + icon?: string; + /** 排序 */ + sort?: number; + /** 路由名称 */ + routeName?: string; + /** 路由路径 */ + routePath?: string; + /** 组件路径 */ + component?: string; + /** 跳转路由路径 */ + redirect?: string; + /** 菜单 */ + type?: MenuTypeEnum; + /** 权限标识 */ + perm?: string; + /** 【菜单】是否开启页面缓存 */ + keepAlive?: number; + /** 【目录】只有一个子路由是否始终显示 */ + alwaysShow?: number; + /** 参数 */ + params?: KeyValue[]; +} + +interface KeyValue { + key: string; + value: string; +} + +/** RouteVO,路由对象 */ +export interface RouteVO { + /** 子路由列表 */ + children: RouteVO[]; + /** 组件路径 */ + component?: string; + /** 路由属性 */ + meta?: Meta; + /** 路由名称 */ + name?: string; + /** 路由路径 */ + path?: string; + /** 跳转链接 */ + redirect?: string; +} + +/** Meta,路由属性 */ +export interface Meta { + /** 【目录】只有一个子路由是否始终显示 */ + alwaysShow?: boolean; + /** 是否隐藏(true-是 false-否) */ + hidden?: boolean; + /** ICON */ + icon?: string; + /** 【菜单】是否开启页面缓存 */ + keepAlive?: boolean; + /** 路由title */ + title?: string; +} diff --git a/src/api/system/notice.api.ts b/src/api/system/notice.api.ts new file mode 100644 index 0000000..b5e65e7 --- /dev/null +++ b/src/api/system/notice.api.ts @@ -0,0 +1,199 @@ +import request from "@/utils/request"; + +const NOTICE_BASE_URL = "/api/v1/notices"; + +const NoticeAPI = { + /** 获取通知公告分页数据 */ + getPage(queryParams?: NoticePageQuery) { + return request>({ + url: `${NOTICE_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取通知公告表单数据 + * + * @param id 通知 + * @returns 通知表单数据 + */ + getFormData(id: string) { + return request({ + url: `${NOTICE_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** + * 添加通知公告 + * + * @param data 通知表单数据 + * @returns + */ + create(data: NoticeForm) { + return request({ + url: `${NOTICE_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 更新通知公告 + * + * @param id 通知ID + * @param data 通知表单数据 + */ + update(id: string, data: NoticeForm) { + return request({ + url: `${NOTICE_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 批量删除通知公告,多个以英文逗号(,)分割 + * + * @param ids 通知公告ID字符串,多个以英文逗号(,)分割 + */ + deleteByIds(ids: string) { + return request({ + url: `${NOTICE_BASE_URL}/${ids}`, + method: "delete", + }); + }, + + /** + * 发布通知 + * + * @param id 被发布的通知公告id + * @returns + */ + publish(id: string) { + return request({ + url: `${NOTICE_BASE_URL}/${id}/publish`, + method: "put", + }); + }, + + /** + * 撤回通知 + * + * @param id 撤回的通知id + * @returns + */ + revoke(id: string) { + return request({ + url: `${NOTICE_BASE_URL}/${id}/revoke`, + method: "put", + }); + }, + /** + * 查看通知 + * + * @param id + */ + getDetail(id: string) { + return request({ + url: `${NOTICE_BASE_URL}/${id}/detail`, + method: "get", + }); + }, + + /* 全部已读 */ + readAll() { + return request({ + url: `${NOTICE_BASE_URL}/read-all`, + method: "put", + }); + }, + + /** 获取我的通知分页列表 */ + getMyNoticePage(queryParams?: NoticePageQuery) { + return request>({ + url: `${NOTICE_BASE_URL}/my-page`, + method: "get", + params: queryParams, + }); + }, +}; + +export default NoticeAPI; + +/** 通知公告分页查询参数 */ +export interface NoticePageQuery extends PageQuery { + /** 标题 */ + title?: string; + /** 发布状态(0:未发布,1:已发布,-1:已撤回) */ + publishStatus?: number; + + isRead?: number; +} + +/** 通知公告表单对象 */ +export interface NoticeForm { + id?: string; + /** 通知标题 */ + title?: string; + /** 通知内容 */ + content?: string; + /** 通知类型 */ + type?: number; + /** 优先级(L:低,M:中,H:高) */ + level?: string; + /** 目标类型(1-全体 2-指定) */ + targetType?: number; + /** 目标ID合集,以,分割 */ + targetUserIds?: string; +} + +/** 通知公告分页对象 */ +export interface NoticePageVO { + id: string; + /** 通知标题 */ + title?: string; + /** 通知内容 */ + content?: string; + /** 通知类型 */ + type?: number; + /** 发布人 */ + publisherId?: bigint; + /** 优先级(0-低 1-中 2-高) */ + priority?: number; + /** 目标类型(0-全体 1-指定) */ + targetType?: number; + /** 发布状态(0-未发布 1已发布 2已撤回) */ + publishStatus?: number; + /** 发布时间 */ + publishTime?: Date; + /** 撤回时间 */ + revokeTime?: Date; +} + +export interface NoticeDetailVO { + /** 通知ID */ + id?: string; + + /** 通知标题 */ + title?: string; + + /** 通知内容 */ + content?: string; + + /** 通知类型 */ + type?: number; + + /** 发布人 */ + publisherName?: string; + + /** 优先级(L-低 M-中 H-高) */ + level?: string; + + /** 发布时间 */ + publishTime?: Date; + + /** 发布状态 */ + publishStatus?: number; +} diff --git a/src/api/system/role.api.ts b/src/api/system/role.api.ts new file mode 100644 index 0000000..be68022 --- /dev/null +++ b/src/api/system/role.api.ts @@ -0,0 +1,138 @@ +import request from "@/utils/request"; + +const ROLE_BASE_URL = "/api/v1/roles"; + +const RoleAPI = { + /** 获取角色分页数据 */ + getPage(queryParams?: RolePageQuery) { + return request>({ + url: `${ROLE_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** 获取角色下拉数据源 */ + getOptions() { + return request({ + url: `${ROLE_BASE_URL}/options`, + method: "get", + }); + }, + /** + * 获取角色的菜单ID集合 + * + * @param roleId 角色ID + * @returns 角色的菜单ID集合 + */ + getRoleMenuIds(roleId: string) { + return request({ + url: `${ROLE_BASE_URL}/${roleId}/menuIds`, + method: "get", + }); + }, + + /** + * 分配菜单权限 + * + * @param roleId 角色ID + * @param data 菜单ID集合 + */ + updateRoleMenus(roleId: string, data: number[]) { + return request({ + url: `${ROLE_BASE_URL}/${roleId}/menus`, + method: "put", + data: data, + }); + }, + + /** + * 获取角色表单数据 + * + * @param id 角色ID + * @returns 角色表单数据 + */ + getFormData(id: string) { + return request({ + url: `${ROLE_BASE_URL}/${id}/form`, + method: "get", + }); + }, + + /** 添加角色 */ + create(data: RoleForm) { + return request({ + url: `${ROLE_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 更新角色 + * + * @param id 角色ID + * @param data 角色表单数据 + */ + update(id: string, data: RoleForm) { + return request({ + url: `${ROLE_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 批量删除角色,多个以英文逗号(,)分割 + * + * @param ids 角色ID字符串,多个以英文逗号(,)分割 + */ + deleteByIds(ids: string) { + return request({ + url: `${ROLE_BASE_URL}/${ids}`, + method: "delete", + }); + }, +}; + +export default RoleAPI; + +/** 角色分页查询参数 */ +export interface RolePageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string; +} + +/** 角色分页对象 */ +export interface RolePageVO { + /** 角色ID */ + id?: string; + /** 角色编码 */ + code?: string; + /** 角色名称 */ + name?: string; + /** 排序 */ + sort?: number; + /** 角色状态 */ + status?: number; + /** 创建时间 */ + createTime?: Date; + /** 修改时间 */ + updateTime?: Date; +} + +/** 角色表单对象 */ +export interface RoleForm { + /** 角色ID */ + id?: string; + /** 角色编码 */ + code?: string; + /** 数据权限 */ + dataScope?: number; + /** 角色名称 */ + name?: string; + /** 排序 */ + sort?: number; + /** 角色状态(1-正常;0-停用) */ + status?: number; +} diff --git a/src/api/system/user.api.ts b/src/api/system/user.api.ts new file mode 100644 index 0000000..1d16fd1 --- /dev/null +++ b/src/api/system/user.api.ts @@ -0,0 +1,384 @@ +import request from "@/utils/request"; + +const USER_BASE_URL = "/api/v1/users"; + +const UserAPI = { + /** + * 获取当前登录用户信息 + * + * @returns 登录用户昵称、头像信息,包括角色和权限 + */ + getInfo() { + return request({ + url: `${USER_BASE_URL}/me`, + method: "get", + }); + }, + + /** + * 获取用户分页列表 + * + * @param queryParams 查询参数 + */ + getPage(queryParams: UserPageQuery) { + return request>({ + url: `${USER_BASE_URL}/page`, + method: "get", + params: queryParams, + }); + }, + + /** + * 获取用户表单详情 + * + * @param userId 用户ID + * @returns 用户表单详情 + */ + getFormData(userId: string) { + return request({ + url: `${USER_BASE_URL}/${userId}/form`, + method: "get", + }); + }, + + /** + * 添加用户 + * + * @param data 用户表单数据 + */ + create(data: UserForm) { + return request({ + url: `${USER_BASE_URL}`, + method: "post", + data: data, + }); + }, + + /** + * 修改用户 + * + * @param id 用户ID + * @param data 用户表单数据 + */ + update(id: string, data: UserForm) { + return request({ + url: `${USER_BASE_URL}/${id}`, + method: "put", + data: data, + }); + }, + + /** + * 修改用户密码 + * + * @param id 用户ID + * @param password 新密码 + */ + resetPassword(id: string, password: string) { + return request({ + url: `${USER_BASE_URL}/${id}/password/reset`, + method: "put", + params: { password: password }, + }); + }, + + /** + * 批量删除用户,多个以英文逗号(,)分割 + * + * @param ids 用户ID字符串,多个以英文逗号(,)分割 + */ + deleteByIds(ids: string) { + return request({ + url: `${USER_BASE_URL}/${ids}`, + method: "delete", + }); + }, + + /** 下载用户导入模板 */ + downloadTemplate() { + return request({ + url: `${USER_BASE_URL}/template`, + method: "get", + responseType: "blob", + }); + }, + + /** + * 导出用户 + * + * @param queryParams 查询参数 + */ + export(queryParams: UserPageQuery) { + return request({ + url: `${USER_BASE_URL}/export`, + method: "get", + params: queryParams, + responseType: "blob", + }); + }, + + /** + * 导入用户 + * + * @param deptId 部门ID + * @param file 导入文件 + */ + import(deptId: string, file: File) { + const formData = new FormData(); + formData.append("file", file); + return request({ + url: `${USER_BASE_URL}/import`, + method: "post", + params: { deptId: deptId }, + data: formData, + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, + + /** 获取个人中心用户信息 */ + getProfile() { + return request({ + url: `${USER_BASE_URL}/profile`, + method: "get", + }); + }, + + /** 修改个人中心用户信息 */ + updateProfile(data: UserProfileForm) { + return request({ + url: `${USER_BASE_URL}/profile`, + method: "put", + data: data, + }); + }, + + /** 修改个人中心用户密码 */ + changePassword(data: PasswordChangeForm) { + return request({ + url: `${USER_BASE_URL}/password`, + method: "put", + data: data, + }); + }, + + /** 发送短信验证码(绑定或更换手机号)*/ + sendMobileCode(mobile: string) { + return request({ + url: `${USER_BASE_URL}/mobile/code`, + method: "post", + params: { mobile: mobile }, + }); + }, + + /** 绑定或更换手机号 */ + bindOrChangeMobile(data: MobileUpdateForm) { + return request({ + url: `${USER_BASE_URL}/mobile`, + method: "put", + data: data, + }); + }, + + /** 发送邮箱验证码(绑定或更换邮箱)*/ + sendEmailCode(email: string) { + return request({ + url: `${USER_BASE_URL}/email/code`, + method: "post", + params: { email: email }, + }); + }, + + /** 绑定或更换邮箱 */ + bindOrChangeEmail(data: EmailUpdateForm) { + return request({ + url: `${USER_BASE_URL}/email`, + method: "put", + data: data, + }); + }, + + /** + * 获取用户下拉列表 + */ + getOptions() { + return request({ + url: `${USER_BASE_URL}/options`, + method: "get", + }); + }, +}; + +export default UserAPI; + +/** 登录用户信息 */ +export interface UserInfo { + /** 用户ID */ + userId?: string; + + /** 用户名 */ + username?: string; + + /** 昵称 */ + nickname?: string; + + /** 头像URL */ + avatar?: string; + + /** 角色 */ + roles: string[]; + + /** 权限 */ + perms: string[]; +} + +/** + * 用户分页查询对象 + */ +export interface UserPageQuery extends PageQuery { + /** 搜索关键字 */ + keywords?: string; + + /** 用户状态 */ + status?: number; + + /** 部门ID */ + deptId?: string; + + /** 开始时间 */ + createTime?: [string, string]; +} + +/** 用户分页对象 */ +export interface UserPageVO { + /** 用户ID */ + id: string; + /** 用户头像URL */ + avatar?: string; + /** 创建时间 */ + createTime?: Date; + /** 部门名称 */ + deptName?: string; + /** 用户邮箱 */ + email?: string; + /** 性别 */ + gender?: number; + /** 手机号 */ + mobile?: string; + /** 用户昵称 */ + nickname?: string; + /** 角色名称,多个使用英文逗号(,)分割 */ + roleNames?: string; + /** 用户状态(1:启用;0:禁用) */ + status?: number; + /** 用户名 */ + username?: string; +} + +/** 用户表单类型 */ +export interface UserForm { + /** 用户ID */ + id?: string; + /** 用户头像 */ + avatar?: string; + /** 部门ID */ + deptId?: string; + /** 邮箱 */ + email?: string; + /** 性别 */ + gender?: number; + /** 手机号 */ + mobile?: string; + /** 昵称 */ + nickname?: string; + /** 角色ID集合 */ + roleIds?: number[]; + /** 用户状态(1:正常;0:禁用) */ + status?: number; + /** 用户名 */ + username?: string; +} + +/** 个人中心用户信息 */ +export interface UserProfileVO { + /** 用户ID */ + id?: string; + + /** 用户名 */ + username?: string; + + /** 昵称 */ + nickname?: string; + + /** 头像URL */ + avatar?: string; + + /** 性别 */ + gender?: number; + + /** 手机号 */ + mobile?: string; + + /** 邮箱 */ + email?: string; + + /** 部门名称 */ + deptName?: string; + + /** 角色名称,多个使用英文逗号(,)分割 */ + roleNames?: string; + + /** 创建时间 */ + createTime?: Date; +} + +/** 个人中心用户信息表单 */ +export interface UserProfileForm { + /** 用户ID */ + id?: string; + + /** 用户名 */ + username?: string; + + /** 昵称 */ + nickname?: string; + + /** 头像URL */ + avatar?: string; + + /** 性别 */ + gender?: number; + + /** 手机号 */ + mobile?: string; + + /** 邮箱 */ + email?: string; +} + +/** 修改密码表单 */ +export interface PasswordChangeForm { + /** 原密码 */ + oldPassword?: string; + /** 新密码 */ + newPassword?: string; + /** 确认新密码 */ + confirmPassword?: string; +} + +/** 修改手机表单 */ +export interface MobileUpdateForm { + /** 手机号 */ + mobile?: string; + /** 验证码 */ + code?: string; +} + +/** 修改邮箱表单 */ +export interface EmailUpdateForm { + /** 邮箱 */ + email?: string; + /** 验证码 */ + code?: string; +} diff --git a/src/assets/icons/api.svg b/src/assets/icons/api.svg new file mode 100644 index 0000000..0181bdd --- /dev/null +++ b/src/assets/icons/api.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/backtop.svg b/src/assets/icons/backtop.svg new file mode 100644 index 0000000..f8e6aa0 --- /dev/null +++ b/src/assets/icons/backtop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/bilibili.svg b/src/assets/icons/bilibili.svg new file mode 100644 index 0000000..b86747c --- /dev/null +++ b/src/assets/icons/bilibili.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/browser.svg b/src/assets/icons/browser.svg new file mode 100644 index 0000000..15c3927 --- /dev/null +++ b/src/assets/icons/browser.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/captcha.svg b/src/assets/icons/captcha.svg new file mode 100644 index 0000000..8b1da30 --- /dev/null +++ b/src/assets/icons/captcha.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/cascader.svg b/src/assets/icons/cascader.svg new file mode 100644 index 0000000..57209bf --- /dev/null +++ b/src/assets/icons/cascader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/client.svg b/src/assets/icons/client.svg new file mode 100644 index 0000000..7373b3d --- /dev/null +++ b/src/assets/icons/client.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..e99c978 --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_all.svg b/src/assets/icons/close_all.svg new file mode 100644 index 0000000..2005198 --- /dev/null +++ b/src/assets/icons/close_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_left.svg b/src/assets/icons/close_left.svg new file mode 100644 index 0000000..fc5cf71 --- /dev/null +++ b/src/assets/icons/close_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_other.svg b/src/assets/icons/close_other.svg new file mode 100644 index 0000000..27ffc32 --- /dev/null +++ b/src/assets/icons/close_other.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/close_right.svg b/src/assets/icons/close_right.svg new file mode 100644 index 0000000..b96dc1c --- /dev/null +++ b/src/assets/icons/close_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/cnblogs.svg b/src/assets/icons/cnblogs.svg new file mode 100644 index 0000000..4920a4c --- /dev/null +++ b/src/assets/icons/cnblogs.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/code.svg b/src/assets/icons/code.svg new file mode 100644 index 0000000..d8b546c --- /dev/null +++ b/src/assets/icons/code.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/collapse.svg b/src/assets/icons/collapse.svg new file mode 100644 index 0000000..1507568 --- /dev/null +++ b/src/assets/icons/collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/csdn.svg b/src/assets/icons/csdn.svg new file mode 100644 index 0000000..e16bad0 --- /dev/null +++ b/src/assets/icons/csdn.svg @@ -0,0 +1,6 @@ + + ic/csdn + + + + diff --git a/src/assets/icons/dict.svg b/src/assets/icons/dict.svg new file mode 100644 index 0000000..db60220 --- /dev/null +++ b/src/assets/icons/dict.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/document.svg b/src/assets/icons/document.svg new file mode 100644 index 0000000..aaa0574 --- /dev/null +++ b/src/assets/icons/document.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/down.svg b/src/assets/icons/down.svg new file mode 100644 index 0000000..5fc8b88 --- /dev/null +++ b/src/assets/icons/down.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000..a8077dc --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/enter.svg b/src/assets/icons/enter.svg new file mode 100644 index 0000000..9e199df --- /dev/null +++ b/src/assets/icons/enter.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/esc.svg b/src/assets/icons/esc.svg new file mode 100644 index 0000000..2f85dd2 --- /dev/null +++ b/src/assets/icons/esc.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/file.svg b/src/assets/icons/file.svg new file mode 100644 index 0000000..fac9bf0 --- /dev/null +++ b/src/assets/icons/file.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/fullscreen-exit.svg b/src/assets/icons/fullscreen-exit.svg new file mode 100644 index 0000000..2452f2b --- /dev/null +++ b/src/assets/icons/fullscreen-exit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/fullscreen.svg b/src/assets/icons/fullscreen.svg new file mode 100644 index 0000000..4b6ee11 --- /dev/null +++ b/src/assets/icons/fullscreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/gitcode.svg b/src/assets/icons/gitcode.svg new file mode 100644 index 0000000..7a02760 --- /dev/null +++ b/src/assets/icons/gitcode.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/gitee.svg b/src/assets/icons/gitee.svg new file mode 100644 index 0000000..c799c2f --- /dev/null +++ b/src/assets/icons/gitee.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/github.svg b/src/assets/icons/github.svg new file mode 100644 index 0000000..1adfa4e --- /dev/null +++ b/src/assets/icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/homepage.svg b/src/assets/icons/homepage.svg new file mode 100644 index 0000000..1e1feab --- /dev/null +++ b/src/assets/icons/homepage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/java.svg b/src/assets/icons/java.svg new file mode 100644 index 0000000..eaa93db --- /dev/null +++ b/src/assets/icons/java.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/juejin.svg b/src/assets/icons/juejin.svg new file mode 100644 index 0000000..937ace3 --- /dev/null +++ b/src/assets/icons/juejin.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/language.svg b/src/assets/icons/language.svg new file mode 100644 index 0000000..e754062 --- /dev/null +++ b/src/assets/icons/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/menu.svg b/src/assets/icons/menu.svg new file mode 100644 index 0000000..f5875d3 --- /dev/null +++ b/src/assets/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/message.svg b/src/assets/icons/message.svg new file mode 100644 index 0000000..deacdc3 --- /dev/null +++ b/src/assets/icons/message.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/monitor.svg b/src/assets/icons/monitor.svg new file mode 100644 index 0000000..f153b9c --- /dev/null +++ b/src/assets/icons/monitor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/project.svg b/src/assets/icons/project.svg new file mode 100644 index 0000000..eaf6a12 --- /dev/null +++ b/src/assets/icons/project.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/qq.svg b/src/assets/icons/qq.svg new file mode 100644 index 0000000..a59086b --- /dev/null +++ b/src/assets/icons/qq.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/refresh.svg b/src/assets/icons/refresh.svg new file mode 100644 index 0000000..e598ed1 --- /dev/null +++ b/src/assets/icons/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/role.svg b/src/assets/icons/role.svg new file mode 100644 index 0000000..5d25278 --- /dev/null +++ b/src/assets/icons/role.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/search.svg b/src/assets/icons/search.svg new file mode 100644 index 0000000..2312daf --- /dev/null +++ b/src/assets/icons/search.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/setting.svg b/src/assets/icons/setting.svg new file mode 100644 index 0000000..fbc4945 --- /dev/null +++ b/src/assets/icons/setting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/size.svg b/src/assets/icons/size.svg new file mode 100644 index 0000000..f92f852 --- /dev/null +++ b/src/assets/icons/size.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/system.svg b/src/assets/icons/system.svg new file mode 100644 index 0000000..2e6045b --- /dev/null +++ b/src/assets/icons/system.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/table.svg b/src/assets/icons/table.svg new file mode 100644 index 0000000..1a16abb --- /dev/null +++ b/src/assets/icons/table.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/todo.svg b/src/assets/icons/todo.svg new file mode 100644 index 0000000..f48e667 --- /dev/null +++ b/src/assets/icons/todo.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/tree.svg b/src/assets/icons/tree.svg new file mode 100644 index 0000000..51aea8f --- /dev/null +++ b/src/assets/icons/tree.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/typescript.svg b/src/assets/icons/typescript.svg new file mode 100644 index 0000000..781d6f8 --- /dev/null +++ b/src/assets/icons/typescript.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/up.svg b/src/assets/icons/up.svg new file mode 100644 index 0000000..3b6c535 --- /dev/null +++ b/src/assets/icons/up.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/user.svg b/src/assets/icons/user.svg new file mode 100644 index 0000000..8e693ec --- /dev/null +++ b/src/assets/icons/user.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/visitor.svg b/src/assets/icons/visitor.svg new file mode 100644 index 0000000..1fd8dbe --- /dev/null +++ b/src/assets/icons/visitor.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/vue.svg b/src/assets/icons/vue.svg new file mode 100644 index 0000000..456f876 --- /dev/null +++ b/src/assets/icons/vue.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/wechat.svg b/src/assets/icons/wechat.svg new file mode 100644 index 0000000..2fc5803 --- /dev/null +++ b/src/assets/icons/wechat.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/xml.svg b/src/assets/icons/xml.svg new file mode 100644 index 0000000..f041213 --- /dev/null +++ b/src/assets/icons/xml.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/401.svg b/src/assets/images/401.svg new file mode 100644 index 0000000..6096d41 --- /dev/null +++ b/src/assets/images/401.svg @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/404.svg b/src/assets/images/404.svg new file mode 100644 index 0000000..b9bf23c --- /dev/null +++ b/src/assets/images/404.svg @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/login-bg.svg b/src/assets/images/login-bg.svg new file mode 100644 index 0000000..a0fbc13 --- /dev/null +++ b/src/assets/images/login-bg.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5872ea8f6cd1bc3b3130a687608d09265479d6c7 GIT binary patch literal 38468 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%0D1xD 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 U@=SUabqx6E>lkTQY2J + + + + + + diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue new file mode 100644 index 0000000..9d49e1f --- /dev/null +++ b/src/components/Breadcrumb/index.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/components/CURD/PageContent.vue b/src/components/CURD/PageContent.vue new file mode 100644 index 0000000..ace91a2 --- /dev/null +++ b/src/components/CURD/PageContent.vue @@ -0,0 +1,934 @@ + + + + + diff --git a/src/components/CURD/PageModal.vue b/src/components/CURD/PageModal.vue new file mode 100644 index 0000000..fe1fa5c --- /dev/null +++ b/src/components/CURD/PageModal.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/src/components/CURD/PageSearch.vue b/src/components/CURD/PageSearch.vue new file mode 100644 index 0000000..afd380e --- /dev/null +++ b/src/components/CURD/PageSearch.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/src/components/CURD/types.ts b/src/components/CURD/types.ts new file mode 100644 index 0000000..1909543 --- /dev/null +++ b/src/components/CURD/types.ts @@ -0,0 +1,223 @@ +import type { DialogProps, DrawerProps, FormItemRule, PaginationProps } from "element-plus"; +import type { FormProps, TableProps, ColProps, ButtonProps, CardProps } from "element-plus"; +import type PageContent from "./PageContent.vue"; +import type PageModal from "./PageModal.vue"; +import type PageSearch from "./PageSearch.vue"; +import { CSSProperties } from "vue"; + +export type PageSearchInstance = InstanceType; +export type PageContentInstance = InstanceType; +export type PageModalInstance = InstanceType; + +export type IObject = Record; + +type DateComponent = "date-picker" | "time-picker" | "time-select" | "custom-tag" | "input-tag"; +type InputComponent = "input" | "select" | "input-number" | "cascader" | "tree-select"; +type OtherComponent = "text" | "radio" | "checkbox" | "switch" | "icon-select" | "custom"; +export type ISearchComponent = DateComponent | InputComponent; +export type IComponentType = DateComponent | InputComponent | OtherComponent; + +type ToolbarLeft = "add" | "delete" | "import" | "export"; +type ToolbarRight = "refresh" | "filter" | "imports" | "exports" | "search"; +type ToolbarTable = "edit" | "view" | "delete"; +export type IToolsButton = { + name: string; // 按钮名称 + text?: string; // 按钮文本 + perm?: Array | string; // 权限标识(可以是完整权限字符串如'sys:user:add'或操作权限如'add') + attrs?: Partial & { style?: CSSProperties }; // 按钮属性 + render?: (row: IObject) => boolean; // 条件渲染 +}; +export type IToolsDefault = ToolbarLeft | ToolbarRight | ToolbarTable | IToolsButton; + +export interface IOperateData { + name: string; + row: IObject; + column: IObject; + $index: number; +} + +export interface ISearchConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string; + // 标签冒号(默认:false) + colon?: boolean; + // 表单项(默认:[]) + formItems?: IFormItems; + // 是否开启展开和收缩(默认:true) + isExpandable?: boolean; + // 默认展示的表单项数量(默认:3) + showNumber?: number; + // 卡片属性 + cardAttrs?: Partial & { style?: CSSProperties }; + // form组件属性 + form?: IForm; + // 自适应网格布局(使用时表单不要添加 style: { width: "200px" }) + grid?: boolean | "left" | "right"; +} + +export interface IContentConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string; + // table组件属性 + table?: Omit, "data">; + // 分页组件位置(默认:left) + pagePosition?: "left" | "right"; + // pagination组件属性 + pagination?: + | boolean + | Partial< + Omit< + PaginationProps, + "v-model:page-size" | "v-model:current-page" | "total" | "currentPage" + > + >; + // 列表的网络请求函数(需返回promise) + indexAction: (queryParams: T) => Promise; + // 默认的分页相关的请求参数 + request?: { + pageName: string; + limitName: string; + }; + // 数据格式解析的回调函数 + parseData?: (res: any) => { + total: number; + list: IObject[]; + [key: string]: any; + }; + // 修改属性的网络请求函数(需返回promise) + modifyAction?: (data: { + [key: string]: any; + field: string; + value: boolean | string | number; + }) => Promise; + // 删除的网络请求函数(需返回promise) + deleteAction?: (ids: string) => Promise; + // 后端导出的网络请求函数(需返回promise) + exportAction?: (queryParams: T) => Promise; + // 前端全量导出的网络请求函数(需返回promise) + exportsAction?: (queryParams: T) => Promise; + // 导入模板 + importTemplate?: string | (() => Promise); + // 后端导入的网络请求函数(需返回promise) + importAction?: (file: File) => Promise; + // 前端导入的网络请求函数(需返回promise) + importsAction?: (data: IObject[]) => Promise; + // 主键名(默认为id) + pk?: string; + // 表格工具栏(默认:add,delete,export,也可自定义) + toolbar?: Array; + // 表格工具栏右侧图标(默认:refresh,filter,imports,exports,search) + defaultToolbar?: Array; + // table组件列属性(额外的属性templet,operat,slotName) + cols: Array<{ + type?: "default" | "selection" | "index" | "expand"; + label?: string; + prop?: string; + width?: string | number; + align?: "left" | "center" | "right"; + columnKey?: string; + reserveSelection?: boolean; + // 列是否显示 + show?: boolean; + // 模板 + templet?: + | "image" + | "list" + | "url" + | "switch" + | "input" + | "price" + | "percent" + | "icon" + | "date" + | "tool" + | "custom"; + // image模板相关参数 + imageWidth?: number; + imageHeight?: number; + // list模板相关参数 + selectList?: IObject; + // switch模板相关参数 + activeValue?: boolean | string | number; + inactiveValue?: boolean | string | number; + activeText?: string; + inactiveText?: string; + // input模板相关参数 + inputType?: string; + // price模板相关参数 + priceFormat?: string; + // date模板相关参数 + dateFormat?: string; + // tool模板相关参数 + operat?: Array; + // filter值拼接符 + filterJoin?: string; + [key: string]: any; + // 初始化数据函数 + initFn?: (item: IObject) => void; + }>; +} + +export interface IModalConfig { + // 权限前缀(如sys:user,用于组成权限标识),不提供则不进行权限校验 + permPrefix?: string; + // 标签冒号(默认:false) + colon?: boolean; + // 主键名(主要用于编辑数据,默认为id) + pk?: string; + // 组件类型(默认:dialog) + component?: "dialog" | "drawer"; + // dialog组件属性 + dialog?: Partial>; + // drawer组件属性 + drawer?: Partial>; + // form组件属性 + form?: IForm; + // 表单项 + formItems: IFormItems; + // 提交之前处理 + beforeSubmit?: (data: T) => void; + // 提交的网络请求函数(需返回promise) + formAction?: (data: T) => Promise; +} + +export type IForm = Partial>; + +// 表单项 +export type IFormItems = Array<{ + // 组件类型(如input,select,radio,custom等) + type: T; + // 标签提示 + tips?: string | IObject; + // 标签文本 + label: string; + // 键名 + prop: string; + // 组件属性 + attrs?: IObject; + // 组件可选项(只适用于select,radio,checkbox组件) + options?: Array<{ label: string; value: any; [key: string]: any }> | Ref; + // 验证规则 + rules?: FormItemRule[]; + // 初始值 + initialValue?: any; + // 插槽名(适用于自定义组件,设置类型为custom) + slotName?: string; + // 是否隐藏 + hidden?: boolean; + // layout组件Col属性 + col?: Partial; + // 组件事件 + events?: Record void>; + // 初始化数据函数扩展 + initFn?: (item: IObject) => void; +}>; + +export interface IPageForm { + // 主键名(主要用于编辑数据,默认为id) + pk?: string; + // form组件属性 + form?: IForm; + // 表单项 + formItems: IFormItems; +} diff --git a/src/components/CURD/usePage.ts b/src/components/CURD/usePage.ts new file mode 100644 index 0000000..dd2d7be --- /dev/null +++ b/src/components/CURD/usePage.ts @@ -0,0 +1,105 @@ +import { ref } from "vue"; +import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from "./types"; + +function usePage() { + const searchRef = ref(); + const contentRef = ref(); + const addModalRef = ref(); + const editModalRef = ref(); + + // 搜索 + function handleQueryClick(queryParams: IObject) { + const filterParams = contentRef.value?.getFilterParams(); + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); + } + // 重置 + function handleResetClick(queryParams: IObject) { + const filterParams = contentRef.value?.getFilterParams(); + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); + } + // 新增 + function handleAddClick(RefImpl?: Ref) { + if (RefImpl) { + RefImpl?.value.setModalVisible(); + RefImpl?.value.handleDisabled(false); + } else { + addModalRef.value?.setModalVisible(); + addModalRef.value?.handleDisabled(false); + } + } + // 编辑 + async function handleEditClick( + row: IObject, + callback?: (result?: IObject) => IObject, + RefImpl?: Ref + ) { + if (RefImpl) { + RefImpl.value?.setModalVisible(); + RefImpl.value?.handleDisabled(false); + let from = await (callback?.(row) ?? Promise.resolve(row)); + RefImpl.value?.setFormData(from ? from : row); + } else { + editModalRef.value?.setModalVisible(); + editModalRef.value?.handleDisabled(false); + let from = await (callback?.(row) ?? Promise.resolve(row)); + editModalRef.value?.setFormData(from ? from : row); + } + } + // 查看 + async function handleViewClick( + row: IObject, + callback?: (result?: IObject) => IObject, + RefImpl?: Ref + ) { + if (RefImpl) { + RefImpl.value?.setModalVisible(); + RefImpl.value?.handleDisabled(true); + let from = await (callback?.(row) ?? Promise.resolve(row)); + RefImpl.value?.setFormData(from ? from : row); + } else { + editModalRef.value?.setModalVisible(); + editModalRef.value?.handleDisabled(true); + let from = await (callback?.(row) ?? Promise.resolve(row)); + editModalRef.value?.setFormData(from ? from : row); + } + } + // 表单提交 + function handleSubmitClick() { + //根据检索条件刷新列表数据 + const queryParams = searchRef.value?.getQueryParams(); + contentRef.value?.fetchPageData(queryParams, true); + } + // 导出 + function handleExportClick() { + // 根据检索条件导出数据 + const queryParams = searchRef.value?.getQueryParams(); + contentRef.value?.exportPageData(queryParams); + } + // 搜索显隐 + function handleSearchClick() { + searchRef.value?.toggleVisible(); + } + // 涮选数据 + function handleFilterChange(filterParams: IObject) { + const queryParams = searchRef.value?.getQueryParams(); + contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true); + } + + return { + searchRef, + contentRef, + addModalRef, + editModalRef, + handleQueryClick, + handleResetClick, + handleAddClick, + handleEditClick, + handleViewClick, + handleSubmitClick, + handleExportClick, + handleSearchClick, + handleFilterChange, + }; +} + +export default usePage; diff --git a/src/components/CommonWrapper/index.vue b/src/components/CommonWrapper/index.vue new file mode 100644 index 0000000..9898849 --- /dev/null +++ b/src/components/CommonWrapper/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/src/components/CopyButton/index.vue b/src/components/CopyButton/index.vue new file mode 100644 index 0000000..5179633 --- /dev/null +++ b/src/components/CopyButton/index.vue @@ -0,0 +1,62 @@ + + + + diff --git a/src/components/DarkModeSwitch/index.vue b/src/components/DarkModeSwitch/index.vue new file mode 100644 index 0000000..c5991b2 --- /dev/null +++ b/src/components/DarkModeSwitch/index.vue @@ -0,0 +1,39 @@ + + diff --git a/src/components/Dict/DictLabel.vue b/src/components/Dict/DictLabel.vue new file mode 100644 index 0000000..c660347 --- /dev/null +++ b/src/components/Dict/DictLabel.vue @@ -0,0 +1,66 @@ + + diff --git a/src/components/Dict/index.vue b/src/components/Dict/index.vue new file mode 100644 index 0000000..79d98e4 --- /dev/null +++ b/src/components/Dict/index.vue @@ -0,0 +1,123 @@ + + + diff --git a/src/components/ECharts/index.vue b/src/components/ECharts/index.vue new file mode 100644 index 0000000..03b8b98 --- /dev/null +++ b/src/components/ECharts/index.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/Fullscreen/index.vue b/src/components/Fullscreen/index.vue new file mode 100644 index 0000000..bd888fe --- /dev/null +++ b/src/components/Fullscreen/index.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/GithubCorner/index.vue b/src/components/GithubCorner/index.vue new file mode 100644 index 0000000..7f91959 --- /dev/null +++ b/src/components/GithubCorner/index.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue new file mode 100644 index 0000000..58d2bd1 --- /dev/null +++ b/src/components/Hamburger/index.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue new file mode 100644 index 0000000..b96e93e --- /dev/null +++ b/src/components/IconSelect/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/src/components/InputTag/index.vue b/src/components/InputTag/index.vue new file mode 100644 index 0000000..a22b993 --- /dev/null +++ b/src/components/InputTag/index.vue @@ -0,0 +1,73 @@ + + diff --git a/src/components/LangSelect/index.vue b/src/components/LangSelect/index.vue new file mode 100644 index 0000000..fab9e71 --- /dev/null +++ b/src/components/LangSelect/index.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/MenuSearch/index.vue b/src/components/MenuSearch/index.vue new file mode 100644 index 0000000..85d4586 --- /dev/null +++ b/src/components/MenuSearch/index.vue @@ -0,0 +1,521 @@ + + + + + diff --git a/src/components/NoticeDropdown/index.vue b/src/components/NoticeDropdown/index.vue new file mode 100644 index 0000000..0333ed8 --- /dev/null +++ b/src/components/NoticeDropdown/index.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue new file mode 100644 index 0000000..7017d95 --- /dev/null +++ b/src/components/Pagination/index.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue new file mode 100644 index 0000000..7d49bc5 --- /dev/null +++ b/src/components/SizeSelect/index.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/TableSelect/index.vue b/src/components/TableSelect/index.vue new file mode 100644 index 0000000..95fb808 --- /dev/null +++ b/src/components/TableSelect/index.vue @@ -0,0 +1,357 @@ + + + + + diff --git a/src/components/TextScroll/index.vue b/src/components/TextScroll/index.vue new file mode 100644 index 0000000..d37e1e7 --- /dev/null +++ b/src/components/TextScroll/index.vue @@ -0,0 +1,412 @@ + + + + + + diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue new file mode 100644 index 0000000..f70892b --- /dev/null +++ b/src/components/Upload/FileUpload.vue @@ -0,0 +1,310 @@ + + + + diff --git a/src/components/Upload/MultiImageUpload.vue b/src/components/Upload/MultiImageUpload.vue new file mode 100644 index 0000000..aeb2667 --- /dev/null +++ b/src/components/Upload/MultiImageUpload.vue @@ -0,0 +1,215 @@ + + + + diff --git a/src/components/Upload/SingleImageUpload.vue b/src/components/Upload/SingleImageUpload.vue new file mode 100644 index 0000000..cda5cfd --- /dev/null +++ b/src/components/Upload/SingleImageUpload.vue @@ -0,0 +1,201 @@ + + + + + + diff --git a/src/components/WangEditor/index.vue b/src/components/WangEditor/index.vue new file mode 100644 index 0000000..fe8f893 --- /dev/null +++ b/src/components/WangEditor/index.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/directive/index.ts b/src/directive/index.ts new file mode 100644 index 0000000..9c22eb6 --- /dev/null +++ b/src/directive/index.ts @@ -0,0 +1,9 @@ +import type { App } from "vue"; + +import { hasPerm } from "./permission"; + +// 全局注册 directive +export function setupDirective(app: App) { + // 使 v-hasPerm 在所有组件中都可用 + app.directive("hasPerm", hasPerm); +} diff --git a/src/directive/permission/index.ts b/src/directive/permission/index.ts new file mode 100644 index 0000000..2683d22 --- /dev/null +++ b/src/directive/permission/index.ts @@ -0,0 +1,64 @@ +import type { Directive, DirectiveBinding } from "vue"; + +import { useUserStore } from "@/store"; + +/** + * 按钮权限 + */ +export const hasPerm: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const requiredPerms = binding.value; + + // 校验传入的权限值是否合法 + if (!requiredPerms || (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))) { + throw new Error( + "需要提供权限标识!例如:v-has-perm=\"'sys:user:add'\" 或 v-has-perm=\"['sys:user:add', 'sys:user:edit']\"" + ); + } + + const { roles, perms } = useUserStore().userInfo; + + // 超级管理员拥有所有权限,如果是”*:*:*”权限标识,则不需要进行权限校验 + if (roles.includes("ROOT") || requiredPerms.includes("*:*:*")) { + return; + } + + // 检查权限 + const hasAuth = Array.isArray(requiredPerms) + ? requiredPerms.some((perm) => perms.includes(perm)) + : perms.includes(requiredPerms); + + // 如果没有权限,移除该元素 + if (!hasAuth && el.parentNode) { + el.parentNode.removeChild(el); + } + }, +}; + +/** + * 角色权限指令 + */ +export const hasRole: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const requiredRoles = binding.value; + + // 校验传入的角色值是否合法 + if (!requiredRoles || (typeof requiredRoles !== "string" && !Array.isArray(requiredRoles))) { + throw new Error( + "需要提供角色标识!例如:v-has-role=\"'ADMIN'\" 或 v-has-role=\"['ADMIN', 'TEST']\"" + ); + } + + const { roles } = useUserStore().userInfo; + + // 检查是否有对应角色权限 + const hasAuth = Array.isArray(requiredRoles) + ? requiredRoles.some((role) => roles.includes(role)) + : roles.includes(requiredRoles); + + // 如果没有权限,移除元素 + if (!hasAuth && el.parentNode) { + el.parentNode.removeChild(el); + } + }, +}; diff --git a/src/enums/api/result.enum.ts b/src/enums/api/result.enum.ts new file mode 100644 index 0000000..349a628 --- /dev/null +++ b/src/enums/api/result.enum.ts @@ -0,0 +1,23 @@ +/** + * 响应码枚举 + */ +export const enum ResultEnum { + /** + * 成功 + */ + SUCCESS = "00000", + /** + * 错误 + */ + ERROR = "B0001", + + /** + * 访问令牌无效或过期 + */ + ACCESS_TOKEN_INVALID = "A0230", + + /** + * 刷新令牌无效或过期 + */ + REFRESH_TOKEN_INVALID = "A0231", +} diff --git a/src/enums/codegen/form.enum.ts b/src/enums/codegen/form.enum.ts new file mode 100644 index 0000000..96c5462 --- /dev/null +++ b/src/enums/codegen/form.enum.ts @@ -0,0 +1,15 @@ +/** + * 表单类型枚举 + */ +export const FormTypeEnum: Record = { + INPUT: { value: 1, label: "输入框" }, + SELECT: { value: 2, label: "下拉框" }, + RADIO: { value: 3, label: "单选框" }, + CHECK_BOX: { value: 4, label: "复选框" }, + INPUT_NUMBER: { value: 5, label: "数字输入框" }, + SWITCH: { value: 6, label: "开关" }, + TEXT_AREA: { value: 7, label: "文本域" }, + DATE: { value: 8, label: "日期框" }, + DATE_TIME: { value: 9, label: "日期时间框" }, + HIDDEN: { value: 10, label: "隐藏域" }, +}; diff --git a/src/enums/codegen/query.enum.ts b/src/enums/codegen/query.enum.ts new file mode 100644 index 0000000..253efc0 --- /dev/null +++ b/src/enums/codegen/query.enum.ts @@ -0,0 +1,37 @@ +/** + * 查询类型枚举 + */ +export const QueryTypeEnum: Record = { + /** 等于 */ + EQ: { value: 1, label: "=" }, + + /** 模糊匹配 */ + LIKE: { value: 2, label: "LIKE '%s%'" }, + + /** 包含 */ + IN: { value: 3, label: "IN" }, + + /** 范围 */ + BETWEEN: { value: 4, label: "BETWEEN" }, + + /** 大于 */ + GT: { value: 5, label: ">" }, + + /** 大于等于 */ + GE: { value: 6, label: ">=" }, + + /** 小于 */ + LT: { value: 7, label: "<" }, + + /** 小于等于 */ + LE: { value: 8, label: "<=" }, + + /** 不等于 */ + NE: { value: 9, label: "!=" }, + + /** 左模糊匹配 */ + LIKE_LEFT: { value: 10, label: "LIKE '%s'" }, + + /** 右模糊匹配 */ + LIKE_RIGHT: { value: 11, label: "LIKE 's%'" }, +}; diff --git a/src/enums/index.ts b/src/enums/index.ts new file mode 100644 index 0000000..d56b6ad --- /dev/null +++ b/src/enums/index.ts @@ -0,0 +1,11 @@ +export * from "./api/result.enum"; + +export * from "./codegen/form.enum"; +export * from "./codegen/query.enum"; + +export * from "./settings/layout.enum"; +export * from "./settings/theme.enum"; +export * from "./settings/locale.enum"; +export * from "./settings/device.enum"; + +export * from "./system/menu.enum"; diff --git a/src/enums/settings/device.enum.ts b/src/enums/settings/device.enum.ts new file mode 100644 index 0000000..709bcb3 --- /dev/null +++ b/src/enums/settings/device.enum.ts @@ -0,0 +1,14 @@ +/** + * 设备枚举 + */ +export const enum DeviceEnum { + /** + * 宽屏设备 + */ + DESKTOP = "desktop", + + /** + * 窄屏设备 + */ + MOBILE = "mobile", +} diff --git a/src/enums/settings/layout.enum.ts b/src/enums/settings/layout.enum.ts new file mode 100644 index 0000000..e1e4406 --- /dev/null +++ b/src/enums/settings/layout.enum.ts @@ -0,0 +1,53 @@ +/** + * 菜单布局枚举 + */ +export const enum LayoutMode { + /** + * 左侧菜单布局 + */ + LEFT = "left", + /** + * 顶部菜单布局 + */ + TOP = "top", + + /** + * 混合菜单布局 + */ + MIX = "mix", +} + +/** + * 侧边栏状态枚举 + */ +export const enum SidebarStatus { + /** + * 展开 + */ + OPENED = "opened", + + /** + * 关闭 + */ + CLOSED = "closed", +} + +/** + * 组件尺寸枚举 + */ +export const enum ComponentSize { + /** + * 默认 + */ + DEFAULT = "default", + + /** + * 大型 + */ + LARGE = "large", + + /** + * 小型 + */ + SMALL = "small", +} diff --git a/src/enums/settings/locale.enum.ts b/src/enums/settings/locale.enum.ts new file mode 100644 index 0000000..a50b655 --- /dev/null +++ b/src/enums/settings/locale.enum.ts @@ -0,0 +1,14 @@ +/** + * 语言枚举 + */ +export const enum LanguageEnum { + /** + * 中文 + */ + ZH_CN = "zh-cn", + + /** + * 英文 + */ + EN = "en", +} diff --git a/src/enums/settings/theme.enum.ts b/src/enums/settings/theme.enum.ts new file mode 100644 index 0000000..21799c3 --- /dev/null +++ b/src/enums/settings/theme.enum.ts @@ -0,0 +1,32 @@ +/** + * 主题枚举 + */ +export const enum ThemeMode { + /** + * 明亮主题 + */ + LIGHT = "light", + /** + * 暗黑主题 + */ + DARK = "dark", + + /** + * 系统自动 + */ + AUTO = "auto", +} + +/** + * 侧边栏配色方案枚举 + */ +export const enum SidebarColor { + /** + * 经典蓝 + */ + CLASSIC_BLUE = "classic-blue", + /** + * 极简白 + */ + MINIMAL_WHITE = "minimal-white", +} diff --git a/src/enums/system/menu.enum.ts b/src/enums/system/menu.enum.ts new file mode 100644 index 0000000..3ed2f37 --- /dev/null +++ b/src/enums/system/menu.enum.ts @@ -0,0 +1,7 @@ +// 核心枚举定义 +export enum MenuTypeEnum { + CATALOG = 2, // 目录 + MENU = 1, // 菜单 + BUTTON = 4, // 按钮 + EXTLINK = 3, // 外链 +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..abab537 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,7 @@ +/** + * 全局Hooks入口文件 + * 导出所有可用的Hooks + */ + +// 导出WebSocket相关Hook +export * from "./websocket"; diff --git a/src/hooks/websocket/core/useStomp.ts b/src/hooks/websocket/core/useStomp.ts new file mode 100644 index 0000000..d96c36e --- /dev/null +++ b/src/hooks/websocket/core/useStomp.ts @@ -0,0 +1,312 @@ +import { Client, IMessage, StompSubscription } from "@stomp/stompjs"; +import { getAccessToken } from "@/utils/auth"; +import { ref, watch } from "vue"; + +export interface UseStompOptions { + /** WebSocket 地址,不传时使用 VITE_APP_WS_ENDPOINT 环境变量 */ + brokerURL?: string; + /** 用于鉴权的 token,不传时使用 getAccessToken() 的返回值 */ + token?: string; + /** 重连延迟,单位毫秒,默认为 8000 */ + reconnectDelay?: number; + /** 连接超时时间,单位毫秒,默认为 10000 */ + connectionTimeout?: number; + /** 是否开启指数退避重连策略 */ + useExponentialBackoff?: boolean; + /** 最大重连次数,默认为 5 */ + maxReconnectAttempts?: number; + /** 最大重连延迟,单位毫秒,默认为 60000 */ + maxReconnectDelay?: number; + /** 是否开启调试日志 */ + debug?: boolean; +} + +/** + * STOMP WebSocket连接Hook + * 用于管理WebSocket连接的建立、断开、重连和消息订阅 + */ +export function useStomp(options: UseStompOptions = {}) { + // 默认值:brokerURL 从环境变量中获取,token 从 getAccessToken() 获取 + const defaultBrokerURL = import.meta.env.VITE_APP_WS_ENDPOINT || ""; + + const brokerURL = ref(options.brokerURL ?? defaultBrokerURL); + // 默认配置参数 + const reconnectDelay = options.reconnectDelay ?? 15000; // 默认15秒重连间隔 + const connectionTimeout = options.connectionTimeout ?? 10000; + const useExponentialBackoff = options.useExponentialBackoff ?? false; + const maxReconnectAttempts = options.maxReconnectAttempts ?? 3; // 最多重连3次 + const maxReconnectDelay = options.maxReconnectDelay ?? 60000; + + // 连接状态标记 + const isConnected = ref(false); + // 重连尝试次数 + const reconnectCount = ref(0); + // 重连计时器 + let reconnectTimer: any = null; + // 连接超时计时器 + let connectionTimeoutTimer: any = null; + // 存储所有订阅 + const subscriptions = new Map(); + + // 用于保存 STOMP 客户端的实例 + let client = ref(null); + + /** + * 初始化 STOMP 客户端 + */ + const initializeClient = () => { + if (client.value) { + return; + } + + // 检查WebSocket端点是否配置 + if (!brokerURL.value) { + console.error("WebSocket连接失败: 未配置WebSocket端点URL"); + return; + } + + // 每次连接前重新获取最新令牌,不依赖之前的token值 + const currentToken = getAccessToken(); + + // 检查令牌是否为空,如果为空则不进行连接 + if (!currentToken) { + console.error("WebSocket连接失败:授权令牌为空,请先登录"); + return; + } + + // 创建 STOMP 客户端 + client.value = new Client({ + brokerURL: brokerURL.value, + connectHeaders: { + Authorization: `Bearer ${currentToken}`, + }, + debug: options.debug ? console.log : () => {}, + reconnectDelay: useExponentialBackoff ? 0 : reconnectDelay, // 禁用内置重连机制 + heartbeatIncoming: 4000, + heartbeatOutgoing: 4000, + }); + + // 设置连接监听器 + client.value.onConnect = () => { + isConnected.value = true; + reconnectCount.value = 0; + clearTimeout(connectionTimeoutTimer); + console.log("WebSocket连接已建立"); + }; + + // 设置断开连接监听器 + client.value.onDisconnect = () => { + isConnected.value = false; + console.log("WebSocket连接已断开"); + + // 如果使用自定义指数退避重连策略,则在这里处理 + if (useExponentialBackoff && reconnectCount.value < maxReconnectAttempts) { + handleReconnect(); + } + }; + + // 设置 Web Socket 关闭监听器 + client.value.onWebSocketClose = (event) => { + isConnected.value = false; + console.log(`WebSocket已关闭: ${event?.code} ${event?.reason}`); + + // 如果是授权问题导致的关闭,尝试重新获取令牌 + if (event?.code === 1000 || event?.code === 1006 || event?.code === 1008) { + console.log("可能是授权问题导致连接关闭,尝试重新建立连接"); + + // 等待一段时间后再尝试重连,避免立即重连 + setTimeout(() => { + // 强制重新初始化客户端,获取最新令牌 + client.value = null; + + // 检查当前是否有有效令牌 + const freshToken = getAccessToken(); + if (freshToken) { + initializeClient(); + connect(); + } else { + console.warn("没有有效令牌,暂不重连WebSocket"); + } + }, 3000); + } + }; + + // 设置错误监听器 + client.value.onStompError = (frame) => { + console.error("STOMP错误:", frame.headers, frame.body); + + // 检查是否是授权错误 + if ( + frame.headers?.message?.includes("Unauthorized") || + frame.body?.includes("Unauthorized") || + frame.body?.includes("Token") + ) { + console.warn("WebSocket授权错误,请检查登录状态"); + } + }; + }; + + /** + * 处理重连逻辑 + */ + const handleReconnect = () => { + if (reconnectCount.value >= maxReconnectAttempts) { + console.error(`已达到最大重连次数(${maxReconnectAttempts}),停止重连`); + return; + } + + reconnectCount.value++; + console.log(`尝试重连(${reconnectCount.value}/${maxReconnectAttempts})...`); + + // 使用指数退避策略增加重连间隔 + const delay = useExponentialBackoff + ? Math.min(reconnectDelay * Math.pow(2, reconnectCount.value - 1), maxReconnectDelay) + : reconnectDelay; + + // 清除之前的计时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + } + + // 设置重连计时器 + reconnectTimer = setTimeout(() => { + if (!isConnected.value && client.value) { + client.value.activate(); + } + }, delay); + }; + + // 监听 brokerURL 的变化,若地址改变则重新初始化 + watch(brokerURL, (newURL, oldURL) => { + if (newURL !== oldURL) { + console.log(`brokerURL changed from ${oldURL} to ${newURL}`); + // 断开当前连接,重新激活客户端 + if (client.value && client.value.connected) { + client.value.deactivate(); + } + brokerURL.value = newURL; + initializeClient(); // 重新初始化客户端 + } + }); + + // 初始化客户端 + initializeClient(); + + /** + * 激活连接(如果已经连接或正在激活则直接返回) + */ + const connect = () => { + // 检查是否有配置WebSocket端点 + if (!brokerURL.value) { + console.error("WebSocket连接失败: 未配置WebSocket端点URL"); + return; + } + + if (!client.value) { + initializeClient(); + } + + if (!client.value) { + console.error("STOMP客户端初始化失败"); + return; + } + + // 避免重复连接:检查是否已连接或正在连接 + if (client.value.connected) { + console.log("WebSocket已经连接,跳过重复连接"); + return; + } + + if (client.value.active) { + console.log("WebSocket连接正在进行中,跳过重复连接请求"); + return; + } + + // 设置连接超时 + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = setTimeout(() => { + if (!isConnected.value) { + console.warn("WebSocket连接超时"); + if (useExponentialBackoff) { + handleReconnect(); + } + } + }, connectionTimeout); + + client.value.activate(); + }; + + /** + * 订阅指定主题 + * @param destination 目标主题地址 + * @param callback 接收到消息时的回调函数 + * @returns 返回订阅 id,用于后续取消订阅 + */ + const subscribe = (destination: string, callback: (_message: IMessage) => void): string => { + if (!client.value) { + return ""; + } + + if (!client.value.connected) { + return ""; + } + + try { + const subscription = client.value.subscribe(destination, callback); + subscriptions.set(subscription.id, subscription); + console.log(`订阅成功: ${destination}, ID: ${subscription.id}`); + return subscription.id; + } catch (error) { + console.error(`订阅失败(${destination}):`, error); + return ""; + } + }; + + /** + * 取消指定订阅 + * @param subscriptionId 要取消的订阅 id + */ + const unsubscribe = (subscriptionId: string) => { + const subscription = subscriptions.get(subscriptionId); + if (subscription) { + subscription.unsubscribe(); + subscriptions.delete(subscriptionId); + } + }; + + /** + * 主动断开连接(如果未连接则不执行) + */ + const disconnect = () => { + if (client.value && !(client.value.connected || client.value.active)) { + console.log("Already disconnected, skipping disconnect() call."); + return; + } + + // 清除所有计时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + if (connectionTimeoutTimer) { + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = null; + } + + client.value?.deactivate(); + isConnected.value = false; + reconnectCount.value = 0; + }; + + return { + client, + isConnected, + reconnectCount, + connect, + subscribe, + unsubscribe, + disconnect, + brokerURL, + }; +} diff --git a/src/hooks/websocket/index.ts b/src/hooks/websocket/index.ts new file mode 100644 index 0000000..3d44e74 --- /dev/null +++ b/src/hooks/websocket/index.ts @@ -0,0 +1,11 @@ +/** + * WebSocket相关Hook入口文件 + * 统一导出所有WebSocket相关Hook + */ + +// 核心基础Hook +export { useStomp } from "./core/useStomp"; + +// 业务服务Hook +export { useOnlineCount } from "./services/useOnlineCount"; +export { useDictSync } from "./services/useDictSync"; diff --git a/src/hooks/websocket/services/useDictSync.ts b/src/hooks/websocket/services/useDictSync.ts new file mode 100644 index 0000000..d5546a3 --- /dev/null +++ b/src/hooks/websocket/services/useDictSync.ts @@ -0,0 +1,197 @@ +import { useDictStoreHook } from "@/store/modules/dict.store"; +import { useStomp } from "../core/useStomp"; +import { IMessage } from "@stomp/stompjs"; +import { ref } from "vue"; + +// 字典消息类型 +export interface DictMessage { + dictCode: string; + timestamp: number; +} + +// 字典事件回调类型 +export type DictMessageCallback = (_message: DictMessage) => void; + +// 全局单例实例 +let instance: ReturnType | null = null; + +/** + * 创建字典同步Hook + * 负责监听后端字典变更并同步到前端 + */ +function createDictSyncHook() { + const dictStore = useDictStoreHook(); + + // 使用现有的useStomp,配置适合字典场景的重连参数 + const { isConnected, connect, subscribe, unsubscribe, disconnect } = useStomp({ + reconnectDelay: 20000, // 字典更新重连时间 + connectionTimeout: 15000, // 连接超时阈值 + useExponentialBackoff: false, // 使用固定间隔重连策略 + maxReconnectAttempts: 3, // 最多重连3次 + }); + + // 存储订阅ID + const subscriptionIds = ref([]); + + // 已订阅的主题 + const subscribedTopics = ref>(new Set()); + + // 消息回调函数列表 + const messageCallbacks = ref([]); + + /** + * 注册字典消息回调 + * @param callback 回调函数 + */ + const onDictMessage = (callback: DictMessageCallback) => { + messageCallbacks.value.push(callback); + return () => { + // 返回取消注册的函数 + const index = messageCallbacks.value.indexOf(callback); + if (index !== -1) { + messageCallbacks.value.splice(index, 1); + } + }; + }; + + /** + * 初始化WebSocket + */ + const initWebSocket = async () => { + try { + // 检查是否配置了WebSocket端点 + const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; + if (!wsEndpoint) { + console.log("[WebSocket] 未配置WebSocket端点,跳过连接"); + return; + } + + // 连接WebSocket + connect(); + + // 设置字典订阅 + setupDictSubscription(); + } catch (error) { + console.error("[WebSocket] 初始化失败:", error); + } + }; + + /** + * 关闭WebSocket + */ + const closeWebSocket = () => { + // 取消所有订阅 + subscriptionIds.value.forEach((id) => { + unsubscribe(id); + }); + subscriptionIds.value = []; + subscribedTopics.value.clear(); + + // 断开连接 + disconnect(); + }; + + /** + * 设置字典订阅 + */ + const setupDictSubscription = () => { + const topic = "/topic/dict"; + + // 防止重复订阅 + if (subscribedTopics.value.has(topic)) { + console.log(`跳过重复订阅: ${topic}`); + return; + } + + console.log(`开始尝试订阅字典主题: ${topic}`); + + // 使用简化的重试逻辑,依赖useStomp的连接管理 + const attemptSubscribe = () => { + if (!isConnected.value) { + console.log("等待WebSocket连接建立..."); + // 10秒后再次尝试 + setTimeout(attemptSubscribe, 10000); + return; + } + + // 检查是否已订阅 + if (subscribedTopics.value.has(topic)) { + return; + } + + console.log(`连接已建立,开始订阅: ${topic}`); + + // 订阅字典更新 + const subId = subscribe(topic, (message: IMessage) => { + handleDictEvent(message); + }); + + if (subId) { + subscriptionIds.value.push(subId); + subscribedTopics.value.add(topic); + console.log(`字典主题订阅成功: ${topic}`); + } else { + console.warn(`字典主题订阅失败: ${topic}`); + } + }; + + // 开始尝试订阅 + attemptSubscribe(); + }; + + /** + * 处理字典事件 + * @param message STOMP消息 + */ + const handleDictEvent = (message: IMessage) => { + if (!message.body) return; + + try { + // 记录接收到的消息 + console.log(`收到字典更新消息: ${message.body}`); + + // 尝试解析消息 + const parsedData = JSON.parse(message.body) as DictMessage; + const dictCode = parsedData.dictCode; + + if (!dictCode) return; + + // 清除缓存,等待按需加载 + dictStore.removeDictItem(dictCode); + console.log(`字典缓存已清除: ${dictCode}`); + + // 调用所有注册的回调函数 + messageCallbacks.value.forEach((callback) => { + try { + callback(parsedData); + } catch (callbackError) { + console.error("[WebSocket] 回调执行失败:", callbackError); + } + }); + + // 显示提示消息 + console.info(`字典 ${dictCode} 已变更,将在下次使用时自动加载`); + } catch (error) { + console.error("[WebSocket] 解析消息失败:", error); + } + }; + + return { + isConnected, + initWebSocket, + closeWebSocket, + handleDictEvent, + onDictMessage, + }; +} + +/** + * 字典同步Hook + * 用于监听后端字典变更并同步到前端 + */ +export function useDictSync() { + if (!instance) { + instance = createDictSyncHook(); + } + return instance; +} diff --git a/src/hooks/websocket/services/useOnlineCount.ts b/src/hooks/websocket/services/useOnlineCount.ts new file mode 100644 index 0000000..be81365 --- /dev/null +++ b/src/hooks/websocket/services/useOnlineCount.ts @@ -0,0 +1,180 @@ +import { ref, onMounted, onUnmounted, watch } from "vue"; +import { useStomp } from "../core/useStomp"; +import { ElMessage } from "element-plus"; +import { getAccessToken } from "@/utils/auth"; + +/** + * 在线用户计数Hook + * 用于订阅后端推送的在线用户数量变化 + */ +export function useOnlineCount() { + // 在线用户数量 + const onlineUserCount = ref(0); + + // 最后更新时间戳 + const lastUpdateTime = ref(0); + + // 连接状态 + const isConnected = ref(false); + + // 连接正在尝试中 + const isConnecting = ref(false); + + // 使用Stomp客户端 - 配置使用指数退避策略 + const { + connect, + subscribe, + unsubscribe, + disconnect, + isConnected: stompConnected, + } = useStomp({ + reconnectDelay: 15000, // 重连基础延迟 + maxReconnectAttempts: 3, // 重连次数上限 + connectionTimeout: 10000, // 连接超时 + useExponentialBackoff: true, // 启用指数退避 + }); + + // 订阅ID + let subscriptionId = ""; + + // 连接超时计时器 + let connectionTimeoutTimer: any = null; + + // 监听Stomp连接状态 + watch(stompConnected, (connected) => { + if (connected && isConnecting.value) { + isConnected.value = true; + isConnecting.value = false; + + // 一旦连接成功,立即订阅主题 + subscribeToOnlineCount(); + console.log("WebSocket连接成功,已订阅在线用户计数主题"); + } + }); + + /** + * 订阅在线用户计数主题 + */ + const subscribeToOnlineCount = () => { + if (!stompConnected.value) { + // 10秒后重试订阅 + setTimeout(subscribeToOnlineCount, 10000); + return; + } + + // 如果已经订阅,先取消订阅 + if (subscriptionId) { + unsubscribe(subscriptionId); + } + + // 订阅在线用户计数主题 + subscriptionId = subscribe("/topic/online-count", (message) => { + try { + const data = message.body; + + const jsonData = JSON.parse(data); + const count = typeof jsonData === "number" ? jsonData : jsonData.count; + + if (!isNaN(count)) { + onlineUserCount.value = count; + lastUpdateTime.value = Date.now(); + } + } catch (error) { + console.error("解析在线用户数量失败:", error); + } + }); + }; + + /** + * 初始化WebSocket连接并订阅在线用户主题 + */ + const initWebSocket = () => { + if (isConnecting.value) return; + + // 检查WebSocket端点是否配置 + const wsEndpoint = import.meta.env.VITE_APP_WS_ENDPOINT; + if (!wsEndpoint) { + console.log("未配置WebSocket端点(VITE_APP_WS_ENDPOINT),跳过WebSocket连接"); + return; + } + + // 检查是否有可用的令牌 + const hasToken = !!getAccessToken(); + if (!hasToken) { + console.log("没有检测到有效令牌,不尝试WebSocket连接"); + return; + } + + isConnecting.value = true; + + // 连接WebSocket + connect(); + + // 设置连接超时显示UI提示 + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = setTimeout(() => { + if (!isConnected.value) { + console.warn("WebSocket连接超时,将自动尝试重连"); + ElMessage.warning("正在尝试连接服务器,请稍候..."); + + // 超时后尝试重新连接 + closeWebSocket(); + setTimeout(() => { + // 再次检查令牌有效性 + if (getAccessToken()) { + initWebSocket(); + } else { + console.log("令牌无效,放弃重连"); + } + }, 3000); + } + }, 10000); // 较长的UI提示超时 + + // 监听连接状态变化,连接成功后清除超时计时器 + const unwatch = watch(stompConnected, (connected) => { + if (connected) { + clearTimeout(connectionTimeoutTimer); + unwatch(); + } + }); + }; + + /** + * 关闭WebSocket连接 + */ + const closeWebSocket = () => { + if (subscriptionId) { + unsubscribe(subscriptionId); + subscriptionId = ""; + } + + // 清除连接超时计时器 + if (connectionTimeoutTimer) { + clearTimeout(connectionTimeoutTimer); + connectionTimeoutTimer = null; + } + + disconnect(); + isConnected.value = false; + isConnecting.value = false; + }; + + // 组件挂载时初始化WebSocket + onMounted(() => { + initWebSocket(); + }); + + // 组件卸载时关闭WebSocket + onUnmounted(() => { + closeWebSocket(); + }); + + return { + onlineUserCount, + lastUpdateTime, + isConnected, + isConnecting, + initWebSocket, + closeWebSocket, + }; +} diff --git a/src/lang/index.ts b/src/lang/index.ts new file mode 100644 index 0000000..c94e018 --- /dev/null +++ b/src/lang/index.ts @@ -0,0 +1,31 @@ +import type { App } from "vue"; +import { createI18n } from "vue-i18n"; +import { useAppStoreHook } from "@/store/modules/app.store"; +// 本地语言包 +import enLocale from "./package/en"; +import zhCnLocale from "./package/zh-cn"; + +const appStore = useAppStoreHook(); + +const messages = { + "zh-cn": { + ...zhCnLocale, + }, + en: { + ...enLocale, + }, +}; + +const i18n = createI18n({ + legacy: false, + locale: appStore.language, + messages: messages, + globalInjection: true, +}); + +// 全局注册 i18n +export function setupI18n(app: App) { + app.use(i18n); +} + +export default i18n; diff --git a/src/lang/package/en.ts b/src/lang/package/en.ts new file mode 100644 index 0000000..7365366 --- /dev/null +++ b/src/lang/package/en.ts @@ -0,0 +1,80 @@ +export default { + // 菜单国际化 + route: { + dashboard: "Dashboard", + document: "Document", + }, + // 登录页面国际化 + login: { + themeToggle: "Theme Switch", + languageToggle: "Language Switch", + dark: "Dark", + light: "Light", + username: "Username", + password: "Password", + login: "Login", + captchaCode: "Verify Code", + capsLock: "Caps Lock is On", + rememberMe: "Remember Me", + forgetPassword: "Forget Password?", + message: { + username: { + required: "Please enter Username", + }, + password: { + required: "Please enter Password", + min: "The password can not be less than 6 digits", + confirm: "Please confirm the password again", + inconformity: "The two password entries are inconsistent", + }, + captchaCode: { + required: "Please enter Verify Code", + }, + }, + otherLoginMethods: "Other", + resetPassword: "Reset password", + thinkOfPasswd: "Remember your password?", + register: "Register account", + agree: "I have read and agree to the", + userAgreement: "User Agreement", + haveAccount: "Already have an account?", + noAccount: "Don't have an account?", + quickFill: "Quick fill", + reg: "Register", + }, + // 导航栏国际化 + navbar: { + dashboard: "Dashboard", + logout: "Logout", + document: "Document", + gitee: "Gitee", + profile: "User Profile", + }, + sizeSelect: { + tooltip: "Layout Size", + default: "Default", + large: "Large", + small: "Small", + message: { + success: "Switch Layout Size Successful!", + }, + }, + langSelect: { + message: { + success: "Switch Language Successful!", + }, + }, + settings: { + project: "Project Settings", + theme: "Theme", + interface: "Interface", + navigation: "Navigation", + themeColor: "Theme Color", + tagsView: "Tags View", + sidebarLogo: "Sidebar Logo", + sidebarColorScheme: "Sidebar Color Scheme", + watermark: "Watermark", + classicBlue: "Classic Blue", + minimalWhite: "Minimal White", + }, +}; diff --git a/src/lang/package/zh-cn.ts b/src/lang/package/zh-cn.ts new file mode 100644 index 0000000..0438454 --- /dev/null +++ b/src/lang/package/zh-cn.ts @@ -0,0 +1,80 @@ +export default { + // 菜单国际化 + route: { + dashboard: "首页", + document: "项目文档", + }, + // 登录页面国际化 + login: { + themeToggle: "主题切换", + languageToggle: "语言切换", + dark: "暗黑", + light: "明亮", + username: "用户名", + password: "密码", + login: "登 录", + captchaCode: "验证码", + capsLock: "大写锁定已打开", + rememberMe: "记住我", + forgetPassword: "忘记密码?", + message: { + username: { + required: "请输入用户名", + }, + password: { + required: "请输入密码", + min: "密码不能少于6位", + confirm: "请再次确认密码", + inconformity: "两次密码输入不一致", + }, + captchaCode: { + required: "请输入验证码", + }, + }, + otherLoginMethods: "其他", + resetPassword: "重置密码", + thinkOfPasswd: "想起密码?", + register: "注册账号", + agree: "我已同意并阅读", + userAgreement: "用户协议", + haveAccount: "已有账号?", + noAccount: "您没有账号?", + quickFill: "快速填写", + reg: "注 册", + }, + // 导航栏国际化 + navbar: { + dashboard: "首页", + logout: "注销登录", + document: "项目文档", + gitee: "项目地址", + profile: "个人中心", + }, + sizeSelect: { + tooltip: "布局大小", + default: "默认", + large: "大型", + small: "小型", + message: { + success: "切换布局大小成功!", + }, + }, + langSelect: { + message: { + success: "切换语言成功!", + }, + }, + settings: { + project: "页面配置", + theme: "主题设置", + interface: "界面设置", + navigation: "导航设置", + themeColor: "主题颜色", + tagsView: "开启 Tags-View", + sidebarLogo: "侧边栏 Logo", + sidebarColorScheme: "侧边栏配色", + watermark: "开启水印", + classicBlue: "经典蓝", + minimalWhite: "极简白", + }, +}; diff --git a/src/layout/components/AppMain/index.vue b/src/layout/components/AppMain/index.vue new file mode 100644 index 0000000..398ee21 --- /dev/null +++ b/src/layout/components/AppMain/index.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/layout/components/NavBar/components/NavbarRight.vue b/src/layout/components/NavBar/components/NavbarRight.vue new file mode 100644 index 0000000..ce5dce5 --- /dev/null +++ b/src/layout/components/NavBar/components/NavbarRight.vue @@ -0,0 +1,127 @@ + + + + diff --git a/src/layout/components/NavBar/index.vue b/src/layout/components/NavBar/index.vue new file mode 100644 index 0000000..5a4a73e --- /dev/null +++ b/src/layout/components/NavBar/index.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/src/layout/components/Settings/components/LayoutSelect.vue b/src/layout/components/Settings/components/LayoutSelect.vue new file mode 100644 index 0000000..488341c --- /dev/null +++ b/src/layout/components/Settings/components/LayoutSelect.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue new file mode 100644 index 0000000..332d9ba --- /dev/null +++ b/src/layout/components/Settings/index.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/src/layout/components/Sidebar/components/SidebarLogo.vue b/src/layout/components/Sidebar/components/SidebarLogo.vue new file mode 100644 index 0000000..5fa80e9 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarLogo.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/layout/components/Sidebar/components/SidebarMenu.vue b/src/layout/components/Sidebar/components/SidebarMenu.vue new file mode 100644 index 0000000..4c8b36f --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMenu.vue @@ -0,0 +1,132 @@ + + + + diff --git a/src/layout/components/Sidebar/components/SidebarMenuItem.vue b/src/layout/components/Sidebar/components/SidebarMenuItem.vue new file mode 100644 index 0000000..ede0ef3 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMenuItem.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue b/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue new file mode 100644 index 0000000..67bf325 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMenuItemTitle.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue b/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue new file mode 100644 index 0000000..7f0f415 --- /dev/null +++ b/src/layout/components/Sidebar/components/SidebarMixTopMenu.vue @@ -0,0 +1,111 @@ + + + + diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue new file mode 100644 index 0000000..606a7b7 --- /dev/null +++ b/src/layout/components/Sidebar/index.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue new file mode 100644 index 0000000..d020958 --- /dev/null +++ b/src/layout/components/TagsView/index.vue @@ -0,0 +1,540 @@ + + + + + diff --git a/src/layout/index.vue b/src/layout/index.vue new file mode 100644 index 0000000..4ac10c6 --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,306 @@ + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..1b27464 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,21 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import setupPlugins from "@/plugins"; + +// 暗黑主题样式 +import "element-plus/theme-chalk/dark/css-vars.css"; +// 暗黑模式自定义变量 +import "@/styles/dark/css-vars.css"; +import "@/styles/index.scss"; +import "uno.css"; + +// 全局引入 animate.css +import "animate.css"; + +// 自动为某些默认事件(如 touchstart、wheel 等)添加 { passive: true },提升滚动性能并消除控制台的非被动事件监听警告 +// import "default-passive-events"; + +const app = createApp(App); +// 注册插件 +app.use(setupPlugins); +app.mount("#app"); diff --git a/src/plugins/icons.ts b/src/plugins/icons.ts new file mode 100644 index 0000000..fa85ba1 --- /dev/null +++ b/src/plugins/icons.ts @@ -0,0 +1,9 @@ +import type { App } from "vue"; +import * as ElementPlusIconsVue from "@element-plus/icons-vue"; + +// 注册所有图标 +export function setupElIcons(app: App) { + for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); + } +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts new file mode 100644 index 0000000..4e90985 --- /dev/null +++ b/src/plugins/index.ts @@ -0,0 +1,31 @@ +import type { App } from "vue"; + +import { setupDirective } from "@/directive"; +import { setupI18n } from "@/lang"; +import { setupRouter } from "@/router"; +import { setupStore } from "@/store"; +import { setupElIcons } from "./icons"; +import { setupPermission } from "./permission"; +// import { setupWebSocket } from "./websocket"; +import { InstallCodeMirror } from "codemirror-editor-vue3"; + +export default { + install(app: App) { + // 自定义指令(directive) + setupDirective(app); + // 路由(router) + setupRouter(app); + // 状态管理(store) + setupStore(app); + // 国际化 + setupI18n(app); + // Element-plus图标 + setupElIcons(app); + // 路由守卫 + setupPermission(); + // WebSocket服务 + // setupWebSocket(); + // 注册 CodeMirror + app.use(InstallCodeMirror); + }, +}; diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts new file mode 100644 index 0000000..8f5a07d --- /dev/null +++ b/src/plugins/permission.ts @@ -0,0 +1,88 @@ +import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router"; +import NProgress from "@/utils/nprogress"; +import { getAccessToken } from "@/utils/auth"; +import router from "@/router"; +import { usePermissionStore, useUserStore } from "@/store"; + +export function setupPermission() { + // 白名单路由 + const whiteList = ["/login"]; + + router.beforeEach(async (to, from, next) => { + NProgress.start(); + + const isLogin = !!getAccessToken(); // 判断是否登录 + if (isLogin) { + if (to.path === "/login") { + // 已登录,访问登录页,跳转到首页 + next({ path: "/" }); + } else { + const permissionStore = usePermissionStore(); + // 判断路由是否加载完成 + if (permissionStore.isRoutesLoaded) { + if (to.matched.length === 0) { + // 路由未匹配,跳转到404 + next("/404"); + } else { + // 动态设置页面标题 + const title = (to.params.title as string) || (to.query.title as string); + if (title) { + to.meta.title = title; + } + next(); + } + } else { + try { + // 生成动态路由 + const dynamicRoutes = await permissionStore.generateRoutes(); + dynamicRoutes.forEach((route: RouteRecordRaw) => router.addRoute(route)); + next({ ...to, replace: true }); + } catch (error) { + console.error(error); + // 路由加载失败,重置 token 并重定向到登录页 + await useUserStore().clearSessionAndCache(); + redirectToLogin(to, next); + NProgress.done(); + } + } + } + } else { + // 未登录,判断是否在白名单中 + if (whiteList.includes(to.path)) { + next(); + } else { + // 不在白名单,重定向到登录页 + redirectToLogin(to, next); + NProgress.done(); + } + } + }); + + // 后置守卫,保证每次路由跳转结束时关闭进度条 + router.afterEach(() => { + NProgress.done(); + }); +} + +// 重定向到登录页 +function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) { + const params = new URLSearchParams(to.query as Record); + const queryString = params.toString(); + const redirect = queryString ? `${to.path}?${queryString}` : to.path; + next(`/login?redirect=${encodeURIComponent(redirect)}`); +} + +/** 判断是否有权限 */ +export function hasAuth(value: string | string[], type: "button" | "role" = "button") { + const { roles, perms } = useUserStore().userInfo; + + // 超级管理员 拥有所有权限 + if (type === "button" && roles.includes("ROOT")) { + return true; + } + + const auths = type === "button" ? perms : roles; + return typeof value === "string" + ? auths.includes(value) + : value.some((perm) => auths.includes(perm)); +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..47c86f1 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,73 @@ +import type { App } from "vue"; +import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router"; + +export const Layout = () => import("@/layout/index.vue"); + +// 静态路由 +export const constantRoutes: RouteRecordRaw[] = [ + { + path: "/redirect", + component: Layout, + meta: { hidden: true }, + children: [ + { + path: "/redirect/:path(.*)", + component: () => import("@/views/redirect/index.vue"), + }, + ], + }, + + { + path: "/login", + component: () => import("@/views/login/index.vue"), + meta: { hidden: true }, + }, + { + path: "/", + name: "/", + component: Layout, + redirect: "/dashboard", + children: [ + { + path: "dashboard", + component: () => import("@/views/dashboard/index.vue"), + // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致 + // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude + name: "Dashboard", + meta: { + title: "dashboard", + icon: "homepage", + affix: true, + keepAlive: true, + }, + }, + { + path: "401", + component: () => import("@/views/error/401.vue"), + meta: { hidden: true }, + }, + { + path: "404", + component: () => import("@/views/error/404.vue"), + meta: { hidden: true }, + }, + ], + }, +]; + +/** + * 创建路由 + */ +const router = createRouter({ + history: createWebHistory(), //createWebHashHistory(), + routes: constantRoutes, + // 刷新时,滚动条位置还原 + scrollBehavior: () => ({ left: 0, top: 0 }), +}); + +// 全局注册 router +export function setupRouter(app: App) { + app.use(router); +} + +export default router; diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..dbd14af --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,37 @@ +import { LayoutMode, ComponentSize, SidebarColor, ThemeMode, LanguageEnum } from "./enums"; + +const { pkg } = __APP_INFO__; + +// 检查用户的操作系统是否使用深色模式 +const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); + +const defaultSettings: AppSettings = { + // 系统Title + title: pkg.name, + // 系统版本 + version: pkg.version, + // 是否显示设置 + showSettings: true, + // 是否显示标签视图 + tagsView: true, + // 是否显示侧边栏Logo + sidebarLogo: true, + // 布局方式,默认为左侧布局 + layout: LayoutMode.LEFT, + // 主题,根据操作系统的色彩方案自动选择 + theme: mediaQueryList.matches ? ThemeMode.DARK : ThemeMode.LIGHT, + // 组件大小 default | medium | small | large + size: ComponentSize.DEFAULT, + // 语言 + language: LanguageEnum.ZH_CN, + // 主题颜色 + themeColor: "#4080FF", + // 是否开启水印 + watermarkEnabled: false, + // 水印内容 + watermarkContent: pkg.name, + // 侧边栏配色方案 + sidebarColorScheme: SidebarColor.CLASSIC_BLUE, +}; + +export default defaultSettings; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..9236155 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,17 @@ +import type { App } from "vue"; +import { createPinia } from "pinia"; + +const store = createPinia(); + +// 全局注册 store +export function setupStore(app: App) { + app.use(store); +} + +export * from "./modules/app.store"; +export * from "./modules/permission.store"; +export * from "./modules/settings.store"; +export * from "./modules/tags-view.store"; +export * from "./modules/user.store"; +export * from "./modules/dict.store"; +export { store }; diff --git a/src/store/modules/app.store.ts b/src/store/modules/app.store.ts new file mode 100644 index 0000000..9c89737 --- /dev/null +++ b/src/store/modules/app.store.ts @@ -0,0 +1,145 @@ +import defaultSettings from "@/settings"; + +// 导入 Element Plus 中英文语言包 +import zhCn from "element-plus/es/locale/lang/zh-cn"; +import en from "element-plus/es/locale/lang/en"; +import { store } from "@/store"; +import { DeviceEnum } from "@/enums/settings/device.enum"; +import { SidebarStatus } from "@/enums/settings/layout.enum"; + +export const useAppStore = defineStore("app", () => { + // 设备类型 + const device = useStorage("device", DeviceEnum.DESKTOP); + // 布局大小 + const size = useStorage("size", defaultSettings.size); + // 用户列表行数 + const accountPgSize = useStorage("accountPgSize", 10); + // 延期列表行数 + const extendPgSize = useStorage("extendPgSize", 10); + // 操作记录行数 + const oplogPgSize = useStorage("oplogPgSize", 10); + // 语言 + const language = useStorage("language", defaultSettings.language); + // 侧边栏状态 + const sidebarStatus = useStorage("sidebarStatus", SidebarStatus.CLOSED); + const sidebar = reactive({ + opened: sidebarStatus.value === SidebarStatus.OPENED, + withoutAnimation: false, + }); + + // 顶部菜单激活路径 + const activeTopMenuPath = useStorage("activeTopMenuPath", ""); + + /** + * 根据语言标识读取对应的语言包 + */ + const locale = computed(() => { + if (language?.value == "en") { + return en; + } else { + return zhCn; + } + }); + + // 切换侧边栏 + function toggleSidebar() { + sidebar.opened = !sidebar.opened; + sidebarStatus.value = sidebar.opened ? SidebarStatus.OPENED : SidebarStatus.CLOSED; + } + + // 关闭侧边栏 + function closeSideBar() { + sidebar.opened = false; + sidebarStatus.value = SidebarStatus.CLOSED; + } + + // 打开侧边栏 + function openSideBar() { + sidebar.opened = true; + sidebarStatus.value = SidebarStatus.OPENED; + } + + // 切换设备 + function toggleDevice(val: string) { + device.value = val; + } + + /** + * 改变布局大小 + * + * @param val 布局大小 default | small | large + */ + function changeSize(val: string) { + size.value = val; + } + /** + * 改变列表行数 + * + * @param val + */ + function changePageSize(type: string, val: number) { + if (type == "account") accountPgSize.value = val; + else if (type == "extend") extendPgSize.value = val; + else if (type == "oplog") oplogPgSize.value = val; + else if (type == "amoreport") oplogPgSize.value = val; + else if (type == "amouser") oplogPgSize.value = val; + else if (type == "amorange") oplogPgSize.value = val; + else if (type == "software") oplogPgSize.value = val; + } + + /** + * 获取列表行数 + * + * @param val + */ + function getPageSize(type: string) { + if (type == "account") return accountPgSize.value; + else if (type == "extend") return extendPgSize.value; + else if (type == "oplog") return oplogPgSize.value; + else if (type == "amoreport") return oplogPgSize.value; + else if (type == "amouser") return oplogPgSize.value; + else if (type == "amorange") return oplogPgSize.value; + else if (type == "software") return oplogPgSize.value; + } + + /** + * 切换语言 + * + * @param val + */ + function changeLanguage(val: string) { + language.value = val; + } + /** + * 混合模式顶部切换 + */ + function activeTopMenu(val: string) { + activeTopMenuPath.value = val; + } + return { + device, + sidebar, + language, + locale, + size, + activeTopMenu, + toggleDevice, + changeSize, + changePageSize, + getPageSize, + changeLanguage, + toggleSidebar, + closeSideBar, + openSideBar, + activeTopMenuPath, + }; +}); + +/** + * 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。 + * 官方文档解释了如何在组件外部使用 Pinia Store: + * https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component + */ +export function useAppStoreHook() { + return useAppStore(store); +} diff --git a/src/store/modules/dict.store.ts b/src/store/modules/dict.store.ts new file mode 100644 index 0000000..84f7ff5 --- /dev/null +++ b/src/store/modules/dict.store.ts @@ -0,0 +1,72 @@ +import { store } from "@/store"; +import DictAPI, { type DictItemOption } from "@/api/system/dict.api"; + +export const useDictStore = defineStore("dict", () => { + // 字典数据缓存 + const dictCache = useStorage>("dict_cache", {}); + + // 请求队列(防止重复请求) + const requestQueue: Record> = {}; + + /** + * 缓存字典数据 + * @param dictCode 字典编码 + * @param data 字典项列表 + */ + const cacheDictItems = (dictCode: string, data: DictItemOption[]) => { + dictCache.value[dictCode] = data; + }; + + /** + * 加载字典数据(如果缓存中没有则请求) + * @param dictCode 字典编码 + */ + const loadDictItems = async (dictCode: string) => { + if (dictCache.value[dictCode]) return; + // 防止重复请求 + if (!requestQueue[dictCode]) { + requestQueue[dictCode] = DictAPI.getDictItems(dictCode).then((data) => { + cacheDictItems(dictCode, data); + Reflect.deleteProperty(requestQueue, dictCode); + }); + } + await requestQueue[dictCode]; + }; + + /** + * 获取字典项列表 + * @param dictCode 字典编码 + * @returns 字典项列表 + */ + const getDictItems = (dictCode: string): DictItemOption[] => { + return dictCache.value[dictCode] || []; + }; + + /** + * 移除指定字典项 + * @param dictCode 字典编码 + */ + const removeDictItem = (dictCode: string) => { + if (dictCache.value[dictCode]) { + Reflect.deleteProperty(dictCache.value, dictCode); + } + }; + + /** + * 清空字典缓存 + */ + const clearDictCache = () => { + dictCache.value = {}; + }; + + return { + loadDictItems, + getDictItems, + removeDictItem, + clearDictCache, + }; +}); + +export function useDictStoreHook() { + return useDictStore(store); +} diff --git a/src/store/modules/permission.store.ts b/src/store/modules/permission.store.ts new file mode 100644 index 0000000..db1ca5f --- /dev/null +++ b/src/store/modules/permission.store.ts @@ -0,0 +1,111 @@ +import type { RouteRecordRaw } from "vue-router"; +import { constantRoutes } from "@/router"; +import { store } from "@/store"; +import router from "@/router"; + +import MenuAPI, { type RouteVO } from "@/api/system/menu.api"; +const modules = import.meta.glob("../../views/**/**.vue"); +const Layout = () => import("@/layout/index.vue"); + +export const usePermissionStore = defineStore("permission", () => { + // 储所有路由,包括静态路由和动态路由 + const routes = ref([]); + // 混合模式左侧菜单路由 + const mixedLayoutLeftRoutes = ref([]); + // 路由是否加载完成 + const isRoutesLoaded = ref(false); + + /** + * 获取后台动态路由数据,解析并注册到全局路由 + * + * @returns Promise 解析后的动态路由列表 + */ + function generateRoutes() { + return new Promise((resolve, reject) => { + MenuAPI.getRoutes() + .then((data) => { + const dynamicRoutes = parseDynamicRoutes(data); + routes.value = [...constantRoutes, ...dynamicRoutes]; + isRoutesLoaded.value = true; + resolve(dynamicRoutes); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * 根据父菜单路径设置混合模式左侧菜单 + * + * @param parentPath 父菜单的路径,用于查找对应的菜单项 + */ + const setMixedLayoutLeftRoutes = (parentPath: string) => { + const matchedItem = routes.value.find((item) => item.path === parentPath); + if (matchedItem && matchedItem.children) { + mixedLayoutLeftRoutes.value = matchedItem.children; + } + }; + + /** + * 重置路由 + */ + const resetRouter = () => { + // 从 router 实例中移除动态路由 + routes.value.forEach((route) => { + if (route.name && !constantRoutes.find((r) => r.name === route.name)) { + router.removeRoute(route.name); + } + }); + + // 清空本地存储的路由和菜单数据 + routes.value = []; + mixedLayoutLeftRoutes.value = []; + isRoutesLoaded.value = false; + }; + + return { + routes, + mixedLayoutLeftRoutes, + isRoutesLoaded, + generateRoutes, + setMixedLayoutLeftRoutes, + resetRouter, + }; +}); + +/** + * 解析后端返回的路由数据并转换为 Vue Router 兼容的路由配置 + * + * @param rawRoutes 后端返回的原始路由数据 + * @returns 解析后的路由配置数组 + */ +const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => { + const parsedRoutes: RouteRecordRaw[] = []; + + rawRoutes.forEach((route) => { + const normalizedRoute = { ...route } as RouteRecordRaw; + + // 处理组件路径 + normalizedRoute.component = + normalizedRoute.component?.toString() === "Layout" + ? Layout + : modules[`../../views/${normalizedRoute.component}.vue`] || + modules["../../views/error-page/404.vue"]; + + // 递归解析子路由 + if (normalizedRoute.children) { + normalizedRoute.children = parseDynamicRoutes(route.children); + } + + parsedRoutes.push(normalizedRoute); + }); + + return parsedRoutes; +}; +/** + * 在组件外使用 Pinia store 实例 @see https://pinia.vuejs.org/core-concepts/outside-component-usage.html + */ +export function usePermissionStoreHook() { + return usePermissionStore(store); +} diff --git a/src/store/modules/settings.store.ts b/src/store/modules/settings.store.ts new file mode 100644 index 0000000..f4b0aff --- /dev/null +++ b/src/store/modules/settings.store.ts @@ -0,0 +1,97 @@ +import defaultSettings from "@/settings"; +import { SidebarColor, ThemeMode } from "@/enums/settings/theme.enum"; +import { LayoutMode } from "@/enums/settings/layout.enum"; +import { applyTheme, generateThemeColors, toggleDarkMode, toggleSidebarColor } from "@/utils/theme"; + +type SettingsValue = boolean | string; + +export const useSettingsStore = defineStore("setting", () => { + // 基本设置 + const settingsVisible = ref(false); + // 标签视图 + const tagsView = useStorage("tagsView", defaultSettings.tagsView); + // 侧边栏 Logo + const sidebarLogo = useStorage("sidebarLogo", defaultSettings.sidebarLogo); + // 侧边栏配色方案 (经典蓝/极简白) + const sidebarColorScheme = useStorage( + "sidebarColorScheme", + defaultSettings.sidebarColorScheme + ); + // 布局 + const layout = useStorage("layout", defaultSettings.layout as LayoutMode); + // 水印 + const watermarkEnabled = useStorage( + "watermarkEnabled", + defaultSettings.watermarkEnabled + ); + + // 主题 + const themeColor = useStorage("themeColor", defaultSettings.themeColor); + const theme = useStorage("theme", defaultSettings.theme); + + // 监听主题变化 + watch( + [theme, themeColor], + ([newTheme, newThemeColor]) => { + toggleDarkMode(newTheme === ThemeMode.DARK); + const colors = generateThemeColors(newThemeColor, newTheme); + applyTheme(colors); + }, + { immediate: true } + ); + + // 监听浅色侧边栏配色方案变化 + watch( + [sidebarColorScheme], + ([newSidebarColorScheme]) => { + toggleSidebarColor(newSidebarColorScheme === SidebarColor.CLASSIC_BLUE); + }, + { immediate: true } + ); + + // 设置映射 + const settingsMap: Record> = { + tagsView, + sidebarLogo, + sidebarColorScheme, + layout, + watermarkEnabled, + }; + + function changeSetting({ key, value }: { key: string; value: SettingsValue }) { + const setting = settingsMap[key]; + if (setting) setting.value = value; + } + + function changeTheme(val: ThemeMode) { + theme.value = val; + } + + function changeSidebarColor(val: string) { + sidebarColorScheme.value = val; + } + + function changeThemeColor(color: string) { + themeColor.value = color; + } + + function changeLayout(val: LayoutMode) { + layout.value = val; + } + + return { + settingsVisible, + tagsView, + sidebarLogo, + sidebarColorScheme, + layout, + themeColor, + theme, + watermarkEnabled, + changeSetting, + changeTheme, + changeThemeColor, + changeLayout, + changeSidebarColor, + }; +}); diff --git a/src/store/modules/tags-view.store.ts b/src/store/modules/tags-view.store.ts new file mode 100644 index 0000000..7a39ffb --- /dev/null +++ b/src/store/modules/tags-view.store.ts @@ -0,0 +1,257 @@ +export const useTagsViewStore = defineStore("tagsView", () => { + const visitedViews = ref([]); + const cachedViews = ref([]); + const router = useRouter(); + const route = useRoute(); + + /** + * 添加已访问视图到已访问视图列表中 + */ + function addVisitedView(view: TagView) { + // 如果已经存在于已访问的视图列表中或者是重定向地址,则不再添加 + if (view.path.startsWith("/redirect")) { + return; + } + if (visitedViews.value.some((v) => v.name === view.name)) { + return; + } + // 如果视图是固定的(affix),则在已访问的视图列表的开头添加 + if (view.affix) { + visitedViews.value.unshift(view); + } else { + // 如果视图不是固定的,则在已访问的视图列表的末尾添加 + visitedViews.value.push(view); + } + } + + /** + * 添加缓存视图到缓存视图列表中 + */ + function addCachedView(view: TagView) { + const viewName = view.name; + // 如果缓存视图名称已经存在于缓存视图列表中,则不再添加 + if (cachedViews.value.includes(viewName)) { + return; + } + + // 如果视图需要缓存(keepAlive),则将其路由名称添加到缓存视图列表中 + if (view.keepAlive) { + cachedViews.value.push(viewName); + } + } + + /** + * 从已访问视图列表中删除指定的视图 + */ + function delVisitedView(view: 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]); + }); + } + + function delCachedView(view: TagView) { + const viewName = view.name; + return new Promise((resolve) => { + const index = cachedViews.value.indexOf(viewName); + if (index > -1) { + cachedViews.value.splice(index, 1); + } + resolve([...cachedViews.value]); + }); + } + function delOtherVisitedViews(view: TagView) { + return new Promise((resolve) => { + visitedViews.value = visitedViews.value.filter((v) => { + return v?.affix || v.path === view.path; + }); + resolve([...visitedViews.value]); + }); + } + + function delOtherCachedViews(view: TagView) { + 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 { + // if index = -1, there is no cached tags + cachedViews.value = []; + } + resolve([...cachedViews.value]); + }); + } + + function updateVisitedView(view: TagView) { + for (let v of visitedViews.value) { + if (v.path === view.path) { + v = Object.assign(v, view); + break; + } + } + } + + function addView(view: TagView) { + addVisitedView(view); + addCachedView(view); + } + + function delView(view: TagView) { + return new Promise((resolve) => { + delVisitedView(view); + delCachedView(view); + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value], + }); + }); + } + + function delOtherViews(view: TagView) { + return new Promise((resolve) => { + delOtherVisitedViews(view); + delOtherCachedViews(view); + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value], + }); + }); + } + + function delLeftViews(view: TagView) { + return new Promise((resolve) => { + const currIndex = visitedViews.value.findIndex((v) => v.path === view.path); + if (currIndex === -1) { + return; + } + visitedViews.value = visitedViews.value.filter((item, index) => { + if (index >= currIndex || item?.affix) { + return true; + } + + const cacheIndex = cachedViews.value.indexOf(item.name); + if (cacheIndex > -1) { + cachedViews.value.splice(cacheIndex, 1); + } + return false; + }); + resolve({ + visitedViews: [...visitedViews.value], + }); + }); + } + + function delRightViews(view: TagView) { + return new Promise((resolve) => { + const currIndex = visitedViews.value.findIndex((v) => v.path === view.path); + if (currIndex === -1) { + return; + } + visitedViews.value = visitedViews.value.filter((item, index) => { + if (index <= currIndex || item?.affix) { + return true; + } + }); + resolve({ + visitedViews: [...visitedViews.value], + }); + }); + } + + function delAllViews() { + return new Promise((resolve) => { + const affixTags = visitedViews.value.filter((tag) => tag?.affix); + visitedViews.value = affixTags; + cachedViews.value = []; + resolve({ + visitedViews: [...visitedViews.value], + cachedViews: [...cachedViews.value], + }); + }); + } + + function delAllVisitedViews() { + return new Promise((resolve) => { + const affixTags = visitedViews.value.filter((tag) => tag?.affix); + visitedViews.value = affixTags; + resolve([...visitedViews.value]); + }); + } + + function delAllCachedViews() { + return new Promise((resolve) => { + cachedViews.value = []; + resolve([...cachedViews.value]); + }); + } + + /** + * 关闭当前tagView + */ + function closeCurrentView() { + const tags: TagView = { + name: route.name as string, + title: route.meta.title as string, + path: route.path, + fullPath: route.fullPath, + affix: route.meta?.affix, + keepAlive: route.meta?.keepAlive, + query: route.query, + }; + delView(tags).then((res: any) => { + if (isActive(tags)) { + toLastView(res.visitedViews, tags); + } + }); + } + + function isActive(tag: TagView) { + return tag.path === route.path; + } + + function toLastView(visitedViews: TagView[], view?: TagView) { + const latestView = visitedViews.slice(-1)[0]; + if (latestView && latestView.fullPath) { + router.push(latestView.fullPath); + } 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") { + // to reload home page + router.replace("/redirect" + view.fullPath); + } else { + router.push("/"); + } + } + } + + return { + visitedViews, + cachedViews, + addVisitedView, + addCachedView, + delVisitedView, + delCachedView, + delOtherVisitedViews, + delOtherCachedViews, + updateVisitedView, + addView, + delView, + delOtherViews, + delLeftViews, + delRightViews, + delAllViews, + delAllVisitedViews, + delAllCachedViews, + closeCurrentView, + isActive, + toLastView, + }; +}); diff --git a/src/store/modules/user.store.ts b/src/store/modules/user.store.ts new file mode 100644 index 0000000..d4de358 --- /dev/null +++ b/src/store/modules/user.store.ts @@ -0,0 +1,122 @@ +import { store } from "@/store"; +import { usePermissionStoreHook } from "@/store/modules/permission.store"; +import { useDictStoreHook } from "@/store/modules/dict.store"; + +import AuthAPI, { type LoginFormData } from "@/api/auth.api"; +import UserAPI, { type UserInfo } from "@/api/system/user.api"; + +import { setAccessToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/auth"; + +export const useUserStore = defineStore("user", () => { + const userInfo = useStorage("userInfo", {} as UserInfo); + + /** + * 登录 + * + * @param {LoginFormData} + * @returns + */ + function login(LoginFormData: LoginFormData) { + return new Promise((resolve, reject) => { + AuthAPI.login(LoginFormData) + .then((data) => { + const { accessToken, refreshToken } = data; + setAccessToken(accessToken); // eyJhbGciOiJIUzI1NiJ9.xxx.xxx + setRefreshToken(refreshToken); + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * 获取用户信息 + * + * @returns {UserInfo} 用户信息 + */ + function getUserInfo() { + return new Promise((resolve, reject) => { + UserAPI.getInfo() + .then((data) => { + if (!data) { + reject("Verification failed, please Login again."); + return; + } + Object.assign(userInfo.value, { ...data }); + resolve(data); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * 登出 + */ + function logout() { + return new Promise((resolve, reject) => { + AuthAPI.logout() + .then(() => { + clearSessionAndCache(); + resolve(); + }) + .catch((error) => { + reject(error); + }); + }); + } + + /** + * 刷新 token + */ + function refreshToken() { + const refreshToken = getRefreshToken(); + return new Promise((resolve, reject) => { + AuthAPI.refreshToken(refreshToken) + .then((data) => { + const { accessToken, refreshToken } = data; + setAccessToken(accessToken); + setRefreshToken(refreshToken); + resolve(); + }) + .catch((error) => { + console.log(" refreshToken 刷新失败", error); + reject(error); + }); + }); + } + + /** + * 清除用户会话和缓存 + */ + function clearSessionAndCache() { + return new Promise((resolve) => { + clearToken(); + usePermissionStoreHook().resetRouter(); + useDictStoreHook().clearDictCache(); + userInfo.value = {} as UserInfo; + resolve(); + }); + } + + return { + userInfo, + getUserInfo, + login, + logout, + clearSessionAndCache, + refreshToken, + }; +}); + +/** + * 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。 + * 官方文档解释了如何在组件外部使用 Pinia Store: + * https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component + */ +export function useUserStoreHook() { + return useUserStore(store); +} diff --git a/src/styles/dark/css-vars.css b/src/styles/dark/css-vars.css new file mode 100644 index 0000000..823aabf --- /dev/null +++ b/src/styles/dark/css-vars.css @@ -0,0 +1,7 @@ +/* 暗黑模式通过 CSS 自定义变量,官方链接:https://element-plus.org/zh-CN/guide/dark-mode.html#%E9%80%9A%E8%BF%87-css */ +html.dark { + .el-table { + /* 自定义表格选中高亮时当前行的背景颜色 */ + --el-table-current-row-bg-color: var(--el-fill-color-light); + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..99d7f3b --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,64 @@ +@use "./reset"; + +.app-container { + padding: 15px; +} + +// 进度条颜色 +#nprogress .bar { + background-color: var(--el-color-primary); +} + +// 全局搜索区域样式 +.search-container { + padding: 18px 16px 0; + margin-bottom: 16px; + background-color: var(--el-bg-color-overlay); + border: 1px solid var(--el-border-color-light); + border-radius: 4px; + + .search-buttons { + margin-right: 0; + } + + .el-form-item { + margin-bottom: 18px; + } +} + +// 表格区域样式 +.data-table { + margin-bottom: 16px; + + // 表格工具栏区域 + &__toolbar { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + + &--actions, + &--tools { + display: flex; + gap: 8px; + } + } + + // 表格内容区域 + &__content { + margin: 8px 0; + } + + // 分页区域 + .el-pagination { + justify-content: flex-end; + margin-top: 16px; + } +} + +// 抽屉和对话框底部按钮区域 +.dialog-footer { + display: flex; + gap: 8px; + justify-content: flex-end; + padding-top: 16px; +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss new file mode 100644 index 0000000..b3b43c7 --- /dev/null +++ b/src/styles/reset.scss @@ -0,0 +1,77 @@ +*, +::before, +::after { + box-sizing: border-box; + border-color: currentcolor; + border-style: solid; + border-width: 0; +} + +#app { + width: 100%; + height: 100%; +} + +html { + box-sizing: border-box; + width: 100%; + height: 100%; + line-height: 1.5; + tab-size: 4; + text-size-adjust: 100%; +} + +body { + width: 100%; + height: 100%; + margin: 0; + font-family: + "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", + Arial, sans-serif; + line-height: inherit; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizelegibility; +} + +a { + color: inherit; + text-decoration: inherit; +} + +img, +svg { + display: inline-block; +} + +svg { + // 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 + vertical-align: -0.15em; +} + +ul, +li { + padding: 0; + margin: 0; + list-style: none; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +a, +a:focus, +a:hover { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +a:focus, +a:active, +div:focus { + outline: none; +} diff --git a/src/styles/variables.module.scss b/src/styles/variables.module.scss new file mode 100644 index 0000000..20a624d --- /dev/null +++ b/src/styles/variables.module.scss @@ -0,0 +1,11 @@ +/* stylelint-disable property-no-unknown */ +:export { + sidebar-width: $sidebar-width; + navbar-height: $navbar-height; + tags-view-height: $tags-view-height; + menu-background: $menu-background; + menu-text: $menu-text; + menu-active-text: $menu-active-text; + menu-hover: $menu-hover; +} +/* stylelint-enable property-no-unknown */ diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 0000000..7987c80 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,69 @@ +@forward "element-plus/theme-chalk/src/common/var.scss" with ( + $colors: ( + "primary": ( + "base": #4080ff, + ), + "success": ( + "base": #23c343, + ), + "warning": ( + "base": #ff9a2e, + ), + "danger": ( + "base": #f76560, + ), + "info": ( + "base": #a9aeb8, + ), + ), + + $bg-color: ( + "page": #f5f8fd, + ) +); + +/** 全局SCSS变量 */ + +:root { + --menu-background: #fff; // 菜单背景色 + --menu-text: #212121; // 菜单文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定 + --menu-active-text: var( + --el-menu-active-color + ); // 菜单激活文字颜色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定 + + --menu-hover: #e6f4ff; // 菜单悬停背景色 浅色主题-白色侧边栏配色下仅占位,实际颜色由 el-menu-item 组件决定 + --sidebar-logo-background: #f5f5f5; // 侧边栏 Logo 背景色 + --sidebar-logo-text-color: #333; // 侧边栏 Logo 文字颜色 +} + +/** 浅色主题-深蓝色侧边栏配色 */ +html.sidebar-color-blue { + --menu-background: #304156; // 菜单背景色 + --menu-text: #bfcbd9; // 菜单文字颜色 + --menu-active-text: var(--el-menu-active-color); // 菜单激活文字颜色 + --menu-hover: #263445; // 菜单悬停背景色 + --sidebar-logo-background: #2d3748; // 侧边栏 Logo 背景色 + --sidebar-logo-text-color: #fff; // 侧边栏 Logo 文字颜色 +} + +/** 暗黑主题 */ +html.dark { + --menu-background: var(--el-bg-color-overlay); + --menu-text: #fff; + --menu-active-text: var(--el-menu-active-color); + --menu-hover: rgb(0 0 0 / 20%); + --sidebar-logo-background: rgb(0 0 0 / 20%); + --sidebar-logo-text-color: #fff; +} + +$menu-background: var(--menu-background); // 菜单背景色 +$menu-text: var(--menu-text); // 菜单文字颜色 +$menu-active-text: var(--menu-active-text); // 菜单激活文字颜色 +$menu-hover: var(--menu-hover); // 菜单悬停背景色 +$sidebar-logo-background: var(--sidebar-logo-background); // 侧边栏 Logo 背景色 +$sidebar-logo-text-color: var(--sidebar-logo-text-color); // 侧边栏 Logo 文字颜色 + +$sidebar-width: 210px; // 侧边栏宽度 +$sidebar-width-collapsed: 54px; // 侧边栏收缩宽度 +$navbar-height: 50px; // 导航栏高度 +$tags-view-height: 34px; // TagsView 高度 diff --git a/src/types/auto-imports.d.ts b/src/types/auto-imports.d.ts new file mode 100644 index 0000000..20b192e --- /dev/null +++ b/src/types/auto-imports.d.ts @@ -0,0 +1,991 @@ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: (typeof import("vue"))["EffectScope"]; + const ElForm: (typeof import("element-plus/es"))["ElForm"]; + 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 ElTree: (typeof import("element-plus/es"))["ElTree"]; + 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 createReusableTemplate: (typeof import("@vueuse/core"))["createReusableTemplate"]; + const createSharedComposable: (typeof import("@vueuse/core"))["createSharedComposable"]; + const createTemplatePromise: (typeof import("@vueuse/core"))["createTemplatePromise"]; + 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 injectLocal: (typeof import("@vueuse/core"))["injectLocal"]; + 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 provideLocal: (typeof import("@vueuse/core"))["provideLocal"]; + 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 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 toValue: (typeof import("vue"))["toValue"]; + 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 useAnimate: (typeof import("@vueuse/core"))["useAnimate"]; + const useArrayDifference: (typeof import("@vueuse/core"))["useArrayDifference"]; + 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 useArrayFindLast: (typeof import("@vueuse/core"))["useArrayFindLast"]; + const useArrayIncludes: (typeof import("@vueuse/core"))["useArrayIncludes"]; + 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 useArrayUnique: (typeof import("@vueuse/core"))["useArrayUnique"]; + 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 useClipboardItems: (typeof import("@vueuse/core"))["useClipboardItems"]; + 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 useI18n: (typeof import("vue-i18n"))["useI18n"]; + 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 useParentElement: (typeof import("@vueuse/core"))["useParentElement"]; + const usePerformanceObserver: (typeof import("@vueuse/core"))["usePerformanceObserver"]; + const usePermission: (typeof import("@vueuse/core"))["usePermission"]; + const usePointer: (typeof import("@vueuse/core"))["usePointer"]; + const usePointerLock: (typeof import("@vueuse/core"))["usePointerLock"]; + 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 usePrevious: (typeof import("@vueuse/core"))["usePrevious"]; + 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 watchDeep: (typeof import("@vueuse/core"))["watchDeep"]; + const watchEffect: (typeof import("vue"))["watchEffect"]; + const watchIgnorable: (typeof import("@vueuse/core"))["watchIgnorable"]; + const watchImmediate: (typeof import("@vueuse/core"))["watchImmediate"]; + 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 type re-export +declare global { + // @ts-ignore + export type { + Component, + ComponentPublicInstance, + ComputedRef, + ExtractDefaultPropTypes, + ExtractPropTypes, + ExtractPublicPropTypes, + InjectionKey, + PropType, + Ref, + VNode, + WritableComputedRef, + } from "vue"; + import("vue"); +} +// for vue template auto import +import { UnwrapRef } from "vue"; +declare module "vue" { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>; + readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>; + readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>; + 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 createReusableTemplate: UnwrapRef< + (typeof import("@vueuse/core"))["createReusableTemplate"] + >; + readonly createSharedComposable: UnwrapRef< + (typeof import("@vueuse/core"))["createSharedComposable"] + >; + readonly createTemplatePromise: UnwrapRef< + (typeof import("@vueuse/core"))["createTemplatePromise"] + >; + 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 injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>; + 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 provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>; + 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 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 toValue: UnwrapRef<(typeof import("vue"))["toValue"]>; + 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 useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>; + readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>; + 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 useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>; + readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>; + 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 useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>; + 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 useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>; + 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 useI18n: UnwrapRef<(typeof import("vue-i18n"))["useI18n"]>; + 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 useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>; + readonly usePerformanceObserver: UnwrapRef< + (typeof import("@vueuse/core"))["usePerformanceObserver"] + >; + readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>; + readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>; + readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>; + 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 usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>; + 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 watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>; + readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>; + readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>; + readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>; + 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"]>; + } +} +declare module "@vue/runtime-core" { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef<(typeof import("vue"))["EffectScope"]>; + readonly ElMessage: UnwrapRef<(typeof import("element-plus/es"))["ElMessage"]>; + readonly ElMessageBox: UnwrapRef<(typeof import("element-plus/es"))["ElMessageBox"]>; + 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 createReusableTemplate: UnwrapRef< + (typeof import("@vueuse/core"))["createReusableTemplate"] + >; + readonly createSharedComposable: UnwrapRef< + (typeof import("@vueuse/core"))["createSharedComposable"] + >; + readonly createTemplatePromise: UnwrapRef< + (typeof import("@vueuse/core"))["createTemplatePromise"] + >; + 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 injectLocal: UnwrapRef<(typeof import("@vueuse/core"))["injectLocal"]>; + 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 provideLocal: UnwrapRef<(typeof import("@vueuse/core"))["provideLocal"]>; + 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 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 toValue: UnwrapRef<(typeof import("vue"))["toValue"]>; + 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 useAnimate: UnwrapRef<(typeof import("@vueuse/core"))["useAnimate"]>; + readonly useArrayDifference: UnwrapRef<(typeof import("@vueuse/core"))["useArrayDifference"]>; + 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 useArrayFindLast: UnwrapRef<(typeof import("@vueuse/core"))["useArrayFindLast"]>; + readonly useArrayIncludes: UnwrapRef<(typeof import("@vueuse/core"))["useArrayIncludes"]>; + 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 useArrayUnique: UnwrapRef<(typeof import("@vueuse/core"))["useArrayUnique"]>; + 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 useClipboardItems: UnwrapRef<(typeof import("@vueuse/core"))["useClipboardItems"]>; + 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 useI18n: UnwrapRef<(typeof import("vue-i18n"))["useI18n"]>; + 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 useParentElement: UnwrapRef<(typeof import("@vueuse/core"))["useParentElement"]>; + readonly usePerformanceObserver: UnwrapRef< + (typeof import("@vueuse/core"))["usePerformanceObserver"] + >; + readonly usePermission: UnwrapRef<(typeof import("@vueuse/core"))["usePermission"]>; + readonly usePointer: UnwrapRef<(typeof import("@vueuse/core"))["usePointer"]>; + readonly usePointerLock: UnwrapRef<(typeof import("@vueuse/core"))["usePointerLock"]>; + 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 usePrevious: UnwrapRef<(typeof import("@vueuse/core"))["usePrevious"]>; + 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 watchDeep: UnwrapRef<(typeof import("@vueuse/core"))["watchDeep"]>; + readonly watchEffect: UnwrapRef<(typeof import("vue"))["watchEffect"]>; + readonly watchIgnorable: UnwrapRef<(typeof import("@vueuse/core"))["watchIgnorable"]>; + readonly watchImmediate: UnwrapRef<(typeof import("@vueuse/core"))["watchImmediate"]>; + 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"]>; + } +} diff --git a/src/types/components.d.ts b/src/types/components.d.ts new file mode 100644 index 0000000..6347a7f --- /dev/null +++ b/src/types/components.d.ts @@ -0,0 +1,100 @@ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module "vue" { + export interface GlobalComponents { + AppLink: (typeof import("./../components/AppLink/index.vue"))["default"]; + AppMain: (typeof import("./../layout/components/AppMain/index.vue"))["default"]; + Breadcrumb: (typeof import("./../components/Breadcrumb/index.vue"))["default"]; + CopyButton: (typeof import("./../components/CopyButton/index.vue"))["default"]; + CURD: (typeof import("./../components/CURD/index.vue"))["default"]; + Dict: (typeof import("./../components/Dict/index.vue"))["default"]; + DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"]; + ECharts: (typeof import("./../components/ECharts/index.vue"))["default"]; + ElBacktop: (typeof import("element-plus/es"))["ElBacktop"]; + 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"]; + ElCascader: (typeof import("element-plus/es"))["ElCascader"]; + ElCheckbox: (typeof import("element-plus/es"))["ElCheckbox"]; + ElCheckboxGroup: (typeof import("element-plus/es"))["ElCheckboxGroup"]; + ElCol: (typeof import("element-plus/es"))["ElCol"]; + ElColorPicker: (typeof import("element-plus/es"))["ElColorPicker"]; + ElConfigProvider: (typeof import("element-plus/es"))["ElConfigProvider"]; + 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"]; + ElInputTag: (typeof import("element-plus/es"))["ElInputTag"]; + 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"]; + ElStatistic: (typeof import("element-plus/es"))["ElStatistic"]; + 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"]; + ElTag: (typeof import("element-plus/es"))["ElTag"]; + ElText: (typeof import("element-plus/es"))["ElText"]; + ElTimeSelect: (typeof import("element-plus/es"))["ElTimeSelect"]; + ElTooltip: (typeof import("element-plus/es"))["ElTooltip"]; + ElTree: (typeof import("element-plus/es"))["ElTree"]; + ElTreeSelect: (typeof import("element-plus/es"))["ElTreeSelect"]; + ElUpload: (typeof import("element-plus/es"))["ElUpload"]; + ElWatermark: (typeof import("element-plus/es"))["ElWatermark"]; + ElSkeleton: (typeof import("element-plus/es"))["ElSkeleton"]; + FileUpload: (typeof import("./../components/Upload/FileUpload.vue"))["default"]; + Form: (typeof import("./../components/CURD/Form.vue"))["default"]; + Fullscreen: (typeof import("./../components/Fullscreen/index.vue"))["default"]; + GithubCorner: (typeof import("./../components/GithubCorner/index.vue"))["default"]; + Hamburger: (typeof import("./../components/Hamburger/index.vue"))["default"]; + IconSelect: (typeof import("./../components/IconSelect/index.vue"))["default"]; + LangSelect: (typeof import("./../components/LangSelect/index.vue"))["default"]; + MenuSearch: (typeof import("./../components/MenuSearch/index.vue"))["default"]; + MultiImageUpload: (typeof import("./../components/Upload/MultiImageUpload.vue"))["default"]; + NoticeDropdown: (typeof import("./../components/Notice/NoticeDropdown.vue"))["default"]; + LayoutSelect: (typeof import("./../layout/components/Settings/components/LayoutSelect.vue"))["default"]; + PageContent: (typeof import("./../components/CURD/PageContent.vue"))["default"]; + PageModal: (typeof import("./../components/CURD/PageModal.vue"))["default"]; + PageSearch: (typeof import("./../components/CURD/PageSearch.vue"))["default"]; + Pagination: (typeof import("./../components/Pagination/index.vue"))["default"]; + RouterLink: (typeof import("vue-router"))["RouterLink"]; + RouterView: (typeof import("vue-router"))["RouterView"]; + Settings: (typeof import("./../layout/components/Settings/index.vue"))["default"]; + Sidebar: (typeof import("./../layout/components/Sidebar/index.vue"))["default"]; + SidebarLogo: (typeof import("./../layout/components/Sidebar/components/SidebarLogo.vue"))["default"]; + SidebarMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMenu.vue"))["default"]; + SidebarMenuItem: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItem.vue"))["default"]; + SidebarMenuItemTitle: (typeof import("./../layout/components/Sidebar/components/SidebarMenuItemTitle.vue"))["default"]; + SidebarMixTopMenu: (typeof import("./../layout/components/Sidebar/components/SidebarMixTopMenu.vue"))["default"]; + SingleImageUpload: (typeof import("./../components/Upload/SingleImageUpload.vue"))["default"]; + SizeSelect: (typeof import("./../components/SizeSelect/index.vue"))["default"]; + TableSelect: (typeof import("./../components/TableSelect/index.vue"))["default"]; + TagsView: (typeof import("./../layout/components/TagsView/index.vue"))["default"]; + WangEditor: (typeof import("./../components/WangEditor/index.vue"))["default"]; + } + export interface ComponentCustomProperties { + vLoading: (typeof import("element-plus/es"))["ElLoadingDirective"]; + } +} diff --git a/src/types/env.d.ts b/src/types/env.d.ts new file mode 100644 index 0000000..301f03e --- /dev/null +++ b/src/types/env.d.ts @@ -0,0 +1,33 @@ +// https://cn.vitejs.dev/guide/env-and-mode + +// TypeScript 类型提示都为 string: https://github.com/vitejs/vite/issues/6930 +interface ImportMetaEnv { + /** 应用端口 */ + VITE_APP_PORT: number; + /** API 基础路径(代理前缀) */ + VITE_APP_BASE_API: string; + /** API 地址 */ + VITE_APP_API_URL: string; + /** 是否开启 Mock 服务 */ + VITE_MOCK_DEV_SERVER: boolean; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +/** + * 平台的名称、版本、运行所需的`node`版本、依赖、构建时间的类型提示 + */ +declare const __APP_INFO__: { + pkg: { + name: string; + version: string; + engines: { + node: string; + }; + dependencies: Record; + devDependencies: Record; + }; + buildTimestamp: number; +}; diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..54d2a35 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,109 @@ +declare global { + /** + * 响应数据 + */ + interface ResponseData { + code: string; + data: T; + msg: string; + } + + /** + * 分页查询参数 + */ + interface PageQuery { + pageNum: number; + pageSize: number; + } + + /** + * 分页响应对象 + */ + interface PageResult { + /** 数据列表 */ + list: T; + /** 总数 */ + total: number; + } + + /** + * 页签对象 + */ + interface TagView { + /** 页签名称 */ + name: string; + /** 页签标题 */ + title: string; + /** 页签路由路径 */ + path: string; + /** 页签路由完整路径 */ + fullPath: string; + /** 页签图标 */ + icon?: string; + /** 是否固定页签 */ + affix?: boolean; + /** 是否开启缓存 */ + keepAlive?: boolean; + /** 路由查询参数 */ + query?: any; + } + + /** + * 系统设置 + */ + interface AppSettings { + /** 系统标题 */ + title: string; + /** 系统版本 */ + version: string; + /** 是否显示设置 */ + showSettings: boolean; + /** 是否显示多标签导航 */ + tagsView: boolean; + /** 是否显示侧边栏Logo */ + sidebarLogo: boolean; + /** 导航栏布局(left|top|mix) */ + layout: "left" | "top" | "mix"; + /** 主题颜色 */ + themeColor: string; + /** 主题模式(dark|light) */ + theme: import("@/enums/settings/theme.enum").ThemeMode; + /** 布局大小(default |large |small) */ + size: string; + /** 语言( zh-cn| en) */ + language: string; + /** 是否开启水印 */ + watermarkEnabled: boolean; + /** 水印内容 */ + watermarkContent: string; + /** 侧边栏配色方案 */ + sidebarColorScheme: "classic-blue" | "minimal-white"; + } + + /** + * 下拉选项数据类型 + */ + interface OptionType { + /** 值 */ + value: string | number; + /** 文本 */ + label: string; + /** 子列表 */ + children?: OptionType[]; + } + + /** + * 导入结果 + */ + interface ExcelResult { + /** 状态码 */ + code: string; + /** 无效数据条数 */ + invalidCount: number; + /** 有效数据条数 */ + validCount: number; + /** 错误信息 */ + messageList: Array; + } +} +export {}; diff --git a/src/types/router.d.ts b/src/types/router.d.ts new file mode 100644 index 0000000..17a3cb4 --- /dev/null +++ b/src/types/router.d.ts @@ -0,0 +1,54 @@ +import "vue-router"; + +declare module "vue-router" { + // https://router.vuejs.org/zh/guide/advanced/meta.html#typescript + // 可以通过扩展 RouteMeta 接口来输入 meta 字段 + interface RouteMeta { + /** + * 菜单名称 + * @example 'Dashboard' + */ + title?: string; + + /** + * 菜单图标 + * @example 'el-icon-edit' + */ + icon?: string; + + /** + * 是否隐藏菜单 + * true 隐藏, false 显示 + * @default false + */ + hidden?: boolean; + + /** + * 始终显示父级菜单,即使只有一个子菜单 + * true 显示父级菜单, false 隐藏父级菜单,显示唯一子节点 + * @default false + */ + alwaysShow?: boolean; + + /** + * 是否固定在页签上 + * true 固定, false 不固定 + * @default false + */ + affix?: boolean; + + /** + * 是否缓存页面 + * true 缓存, false 不缓存 + * @default false + */ + keepAlive?: boolean; + + /** + * 是否在面包屑导航中隐藏 + * true 隐藏, false 显示 + * @default false + */ + breadcrumb?: boolean; + } +} diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts new file mode 100644 index 0000000..d77b62b --- /dev/null +++ b/src/types/shims-vue.d.ts @@ -0,0 +1,5 @@ +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/src/types/socket.d.ts b/src/types/socket.d.ts new file mode 100644 index 0000000..15a8ed7 --- /dev/null +++ b/src/types/socket.d.ts @@ -0,0 +1,6 @@ +// https://github.com/sockjs/sockjs-client/issues/565 + +declare module "sockjs-client/dist/sockjs.min.js" { + import Client from "sockjs-client"; + export default Client; +} diff --git a/src/types/websocket.ts b/src/types/websocket.ts new file mode 100644 index 0000000..30b56a2 --- /dev/null +++ b/src/types/websocket.ts @@ -0,0 +1,15 @@ +/** + * WebSocket相关类型定义 + */ + +/** + * 字典WebSocket事件类型 + */ +export interface DictWebSocketEvent { + /** 事件类型:更新或删除 */ + type: "DICT_UPDATED" | "DICT_DELETED"; + /** 字典编码 */ + dictCode: string; + /** 时间戳 */ + timestamp: number; +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..cfbedf8 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,27 @@ +// 访问 token 缓存的 key +const ACCESS_TOKEN_KEY = "access_token"; +// 刷新 token 缓存的 key +const REFRESH_TOKEN_KEY = "refresh_token"; + +function getAccessToken(): string { + return localStorage.getItem(ACCESS_TOKEN_KEY) || ""; +} + +function setAccessToken(token: string) { + localStorage.setItem(ACCESS_TOKEN_KEY, token); +} + +function getRefreshToken(): string { + return localStorage.getItem(REFRESH_TOKEN_KEY) || ""; +} + +function setRefreshToken(token: string) { + localStorage.setItem(REFRESH_TOKEN_KEY, token); +} + +function clearToken() { + localStorage.removeItem(ACCESS_TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); +} + +export { getAccessToken, setAccessToken, clearToken, getRefreshToken, setRefreshToken }; diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts new file mode 100644 index 0000000..17ed904 --- /dev/null +++ b/src/utils/i18n.ts @@ -0,0 +1,12 @@ +// translate router.meta.title, be used in breadcrumb sidebar tagsview +import i18n from "@/lang/index"; + +export function translateRouteTitle(title: any) { + // 判断是否存在国际化配置,如果没有原生返回 + const hasKey = i18n.global.te("route." + title); + if (hasKey) { + const translatedTitle = i18n.global.t("route." + title); + return translatedTitle; + } + return title; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..67fce78 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,58 @@ +/** + * Check if an element has a class + * @param {HTMLElement} ele + * @param {string} cls + * @returns {boolean} + */ +export function hasClass(ele: HTMLElement, cls: string) { + return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)")); +} + +/** + * Add class to element + * @param {HTMLElement} ele + * @param {string} cls + */ +export function addClass(ele: HTMLElement, cls: string) { + if (!hasClass(ele, cls)) ele.className += " " + cls; +} + +/** + * Remove class from element + * @param {HTMLElement} ele + * @param {string} cls + */ +export function 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 function isExternal(path: string) { + const isExternal = /^(https?:|http?:|mailto:|tel:)/.test(path); + return isExternal; +} + +/** + * 格式化增长率,保留两位小数 ,并且去掉末尾的0 取绝对值 + * + * @param growthRate + * @returns + */ +export function formatGrowthRate(growthRate: number) { + if (growthRate === 0) { + return "-"; + } + + const formattedRate = Math.abs(growthRate * 100) + .toFixed(2) + .replace(/\.?0+$/, ""); + return formattedRate + "%"; +} diff --git a/src/utils/nprogress.ts b/src/utils/nprogress.ts new file mode 100644 index 0000000..c1d5f23 --- /dev/null +++ b/src/utils/nprogress.ts @@ -0,0 +1,18 @@ +import NProgress from "nprogress"; +import "nprogress/nprogress.css"; + +// 进度条 +NProgress.configure({ + // 动画方式 + easing: "ease", + // 递增进度条的速度 + speed: 500, + // 是否显示加载ico + showSpinner: false, + // 自动递增间隔 + trickleSpeed: 200, + // 初始化时的最小百分比 + minimum: 0.3, +}); + +export default NProgress; diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..c3bd61f --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,105 @@ +import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from "axios"; +import qs from "qs"; +import { useUserStoreHook } from "@/store/modules/user.store"; +import { ResultEnum } from "@/enums/api/result.enum"; +import { getAccessToken } from "@/utils/auth"; +import router from "@/router"; + +// 创建 axios 实例 +const service = axios.create({ + baseURL: import.meta.env.VITE_APP_BASE_API, + timeout: 50000, + headers: { "Content-Type": "application/json;charset=utf-8" }, + paramsSerializer: (params) => qs.stringify(params), +}); +// 请求拦截器 +service.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const accessToken = getAccessToken(); + // 如果 Authorization 设置为 no-auth,则不携带 Token + if (config.headers.Authorization !== "no-auth" && accessToken) { + config.headers.Authorization = `Bearer ${accessToken}`; + } else { + delete config.headers.Authorization; + } + return config; + }, + (error) => Promise.reject(error) +); +// 响应拦截器 +service.interceptors.response.use( + (response: AxiosResponse) => { + // 如果响应是二进制流,则直接返回,用于下载文件、Excel 导出等 + if (response.config.responseType === "blob") { + return response; + } + const { code, data, msg } = response.data; + if (code === ResultEnum.SUCCESS) { + return data; + } + ElMessage.error(msg || "系统出错"); + return Promise.reject(new Error(msg || "Error")); + }, + async (error) => { + console.error("request error", error); // for debug + const { config, response } = error; + if (response) { + const { code, msg } = response.data; + if (code === ResultEnum.ACCESS_TOKEN_INVALID) { + // Token 过期,刷新 Token + return handleTokenRefresh(config); + } else if (code === ResultEnum.REFRESH_TOKEN_INVALID) { + // 刷新 Token 过期,跳转登录页 + await handleSessionExpired(); + return Promise.reject(new Error(msg || "Error")); + } else { + ElMessage.error(msg || "系统出错"); + } + } + return Promise.reject(error.message); + } +); +export default service; +// 是否正在刷新标识,避免重复刷新 +let isRefreshing = false; +// 因 Token 过期导致的请求等待队列 +const waitingQueue: Array<() => void> = []; +// 刷新 Token 处理 +async function handleTokenRefresh(config: InternalAxiosRequestConfig) { + return new Promise((resolve) => { + // 封装需要重试的请求 + const retryRequest = () => { + config.headers.Authorization = `Bearer ${getAccessToken()}`; + resolve(service(config)); + }; + waitingQueue.push(retryRequest); + if (!isRefreshing) { + isRefreshing = true; + useUserStoreHook() + .refreshToken() + .then(() => { + // 依次重试队列中所有请求, 重试后清空队列 + waitingQueue.forEach((callback) => callback()); + waitingQueue.length = 0; + }) + .catch(async (error) => { + console.error("handleTokenRefresh error", error); + // 刷新 Token 失败,跳转登录页 + await handleSessionExpired(); + }) + .finally(() => { + isRefreshing = false; + }); + } + }); +} +// 处理会话过期 +async function handleSessionExpired() { + ElNotification({ + title: "提示", + message: "您的会话已过期,请重新登录", + type: "info", + }); + await useUserStoreHook().clearSessionAndCache(); + router.push("/login"); +} diff --git a/src/utils/theme.ts b/src/utils/theme.ts new file mode 100644 index 0000000..558503e --- /dev/null +++ b/src/utils/theme.ts @@ -0,0 +1,106 @@ +import { ThemeMode } from "@/enums"; + +// 辅助函数:将十六进制颜色转换为 RGB +function hexToRgb(hex: string): [number, number, number] { + const bigint = parseInt(hex.slice(1), 16); + return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255]; +} + +// 辅助函数:将 RGB 转换为十六进制颜色 +function rgbToHex(r: number, g: number, b: number): string { + return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; +} + +// 辅助函数:调整颜色亮度 +/** function adjustBrightness(hex: string, factor: number, theme: string): string { + const rgb = hexToRgb(hex); + // 是否是暗黑模式 + const isDarkMode = theme === "dark" ? 0 : 255; + const newRgb = rgb.map((val) => + Math.max(0, Math.min(255, Math.round(val + (isDarkMode - val) * factor))) + ) as [number, number, number]; + return rgbToHex(...newRgb); +} */ + +/** + * 加深颜色值 + * @param {String} color 颜色值字符串 + * @param {Number} level 加深的程度,限0-1之间 + * @returns {String} 返回处理后的颜色值 + */ +export function getDarkColor(color: string, level: number): string { + const rgb = hexToRgb(color); + for (let i = 0; i < 3; i++) rgb[i] = Math.round(20.5 * level + rgb[i] * (1 - level)); + return rgbToHex(rgb[0], rgb[1], rgb[2]); +} + +/** + * 变浅颜色值 + * @param {String} color 颜色值字符串 + * @param {Number} level 加深的程度,限0-1之间 + * @returns {String} 返回处理后的颜色值 + */ +export const getLightColor = (color: string, level: number): string => { + const rgb = hexToRgb(color); + for (let i = 0; i < 3; i++) rgb[i] = Math.round(255 * level + rgb[i] * (1 - level)); + return rgbToHex(rgb[0], rgb[1], rgb[2]); +}; + +/** + * 生成主题色 + * @param primary 主题色 + * @param theme 主题类型 + */ +export function generateThemeColors(primary: string, theme: ThemeMode) { + const colors: Record = { + primary, + }; + + // 生成浅色变体 + for (let i = 1; i <= 9; i++) { + colors[`primary-light-${i}`] = + theme === ThemeMode.LIGHT + ? `${getLightColor(primary, i / 10)}` + : `${getDarkColor(primary, i / 10)}`; + } + + // 生成深色变体 + colors["primary-dark-2"] = + theme === ThemeMode.LIGHT ? `${getLightColor(primary, 0.2)}` : `${getDarkColor(primary, 0.3)}`; + + return colors; +} + +export function applyTheme(colors: Record) { + const el = document.documentElement; + + Object.entries(colors).forEach(([key, value]) => { + el.style.setProperty(`--el-color-${key}`, value); + }); +} + +/** + * 切换暗黑模式 + * + * @param isDark 是否启用暗黑模式 + */ +export function toggleDarkMode(isDark: boolean) { + if (isDark) { + document.documentElement.classList.add(ThemeMode.DARK); + } else { + document.documentElement.classList.remove(ThemeMode.DARK); + } +} + +/** + * 切换浅色主题下的侧边栏颜色方案 + * + * @param isBlue 布尔值,表示是否开启深蓝色侧边栏颜色方案 + */ +export function toggleSidebarColor(isBuleSidebar: boolean) { + if (isBuleSidebar) { + document.documentElement.classList.add("sidebar-color-blue"); + } else { + document.documentElement.classList.remove("sidebar-color-blue"); + } +} diff --git a/src/views/codegen/index.vue b/src/views/codegen/index.vue new file mode 100644 index 0000000..b0bc366 --- /dev/null +++ b/src/views/codegen/index.vue @@ -0,0 +1,866 @@ + + + diff --git a/src/views/dashboard/index.vue b/src/views/dashboard/index.vue new file mode 100644 index 0000000..cdcb06f --- /dev/null +++ b/src/views/dashboard/index.vue @@ -0,0 +1,420 @@ + + + + + diff --git a/src/views/demo/api/apifox.vue b/src/views/demo/api/apifox.vue new file mode 100644 index 0000000..0eb334b --- /dev/null +++ b/src/views/demo/api/apifox.vue @@ -0,0 +1,27 @@ + +