├── .env ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.html ├── mock ├── data │ ├── admin.js │ └── user.js └── index.js ├── package.json ├── pay.png ├── public └── favicon.ico ├── src ├── App.vue ├── api │ ├── common.js │ └── index.js ├── assets │ └── images │ │ ├── 401.png │ │ ├── 404.png │ │ ├── loginbg.png │ │ └── tabs.png ├── components │ └── crud │ │ ├── brisk-dialogcom.vue │ │ ├── brisk-operate.vue │ │ ├── brisk-pagination.vue │ │ ├── brisk-search-btn.vue │ │ ├── brisk-toolbar.vue │ │ └── index.js ├── lang │ ├── en.js │ ├── index.js │ ├── modules │ │ ├── admin │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── adminGroup │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── adminLog │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── adminRule │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── auth │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── crud │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── dashboard │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── error_page │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ ├── nested │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ │ └── profile │ │ │ ├── index_en.js │ │ │ └── index_zh.js │ └── zh.js ├── main.js ├── router │ └── index.js ├── settings │ ├── settings.js │ └── skin.js ├── store │ ├── getters.js │ ├── index.js │ └── modules │ │ ├── app.js │ │ ├── settings.js │ │ └── user.js ├── styles │ ├── color.scss │ └── element │ │ └── index.scss ├── utils │ ├── http │ │ ├── axios.js │ │ └── index.js │ ├── index.js │ └── validate.js ├── vendor │ └── Export2Excel.js └── views │ ├── 401 │ └── index.vue │ ├── 404 │ └── index.vue │ ├── admin │ └── index.vue │ ├── adminGroup │ └── index.vue │ ├── adminLog │ └── index.vue │ ├── adminRule │ └── index.vue │ ├── dashboard │ └── index.vue │ ├── layout │ ├── components │ │ ├── aside │ │ │ ├── index.vue │ │ │ ├── menu.vue │ │ │ └── menuItem.vue │ │ ├── header │ │ │ └── index.vue │ │ ├── setting │ │ │ └── index.vue │ │ └── tabs │ │ │ └── index.vue │ ├── index.vue │ ├── mixin │ │ └── ResizeHandler.js │ └── nestedComponent.vue │ ├── login │ └── index.vue │ ├── menu1 │ └── index.vue │ ├── menu2 │ └── index.vue │ ├── menu3 │ └── index.vue │ ├── menu4 │ └── index.vue │ └── profile │ └── index.vue └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_URL=http://127.0.0.1/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | package-lock.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | ## 1.4.0 (2021-12-25) 3 | 4 | - 1.升级element-plus至1.3.0-beta.5版本并更新所有依赖版本 5 | - 2.去除所有size单独配置 6 | - 3.优化header高度 7 | - 4.优化tabs栏样式 8 | - 5.更改resize事件弃用的触发标准 9 | 10 | ## 1.3.0 (2021-12-25) 11 | 12 | - 1.升级element-plus至1.2.0-beta.6版本并更新所有依赖版本 13 | - 2.vite.config.js相应升级变更 14 | - 3.新增系统主题色动态设置 15 | - 4.皮肤配置优化 16 | - 5.侧边菜单栏优化使其更美观 17 | - 6.公共组件优化调整 18 | - 7.element-plus废弃Font-Icon字体图标相应升级为element-plus/icons-vue 19 | - 8.element-plus自定义主题scss覆盖相应升级改造 20 | - 9.全局配置element-plus组件为small,去除之前每个页面的单独配置 21 | - 10.公共页面样式相应优化使其更美观 22 | 23 | ## 1.2.0 (2021-09-15) 24 | 25 | - 1.实现刷新当前路由 26 | - 2.皮肤配置调整加入logo背景色和文本颜色 27 | - 3.新增皮肤两个方案 28 | - 4.引入第三方icon库 29 | - 5.tabs标签渲染自定义icon图标 30 | - 6.菜单配置更改为第三方icon图标 31 | - 7.header内使用图标均更换为第三方icon库图标 32 | 33 | ## 1.1.0 (2021-09-14) 34 | 35 | - 1.新增tab标签栏 36 | - 2.路由配置新增tabShow以及keepAlive配置 37 | - 3.路由规则调整为首次进入时加载权限规则 38 | - 4.新增路由缓存以及去除缓存 39 | 40 | ## 1.0.1 (2021-09-03) 41 | 42 | - 皮肤配置新增选中颜色配置 43 | - 菜单选中颜色以及未选中颜色适配 44 | - store数据状态管理文件改为自动导入 45 | - lang语言包文件改为自动导入 46 | 47 | ## 1.0.0 (2021-08-23) 48 | 49 | - 第一个正式版发布 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 3 | [Brisk-Admin](https://github.com/ZHT131/brisk-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/vue-next)、[Vite](https://github.com/vitejs/vite)、 [element-plus](https://element-plus.gitee.io/)、JavaScript的中后台解决方案,支持移动端响应式布局,它使用了最新的前端技术栈,并提炼了典型的业务页面,包括二次封装组件、动态路由菜单、国际化、动态换肤等功能,它可以帮助你快速搭建中后台项目,该项目使用最新的前端技术栈,使用javascript语法保留了对不熟悉typescript语法用户的友好,同时框架很适合轻松上手使用,希望给你带来帮助。 4 | 5 | ## 特性 6 | - **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发 7 | - **皮肤**:可配置的主题 8 | - **Mock 数据** 内置 Mock 数据方案 9 | - **动态路由** 内置动态路由权限生成方案 10 | - **crud组件** 二次封装了crud常用组件方案 11 | - **国际化** 内置国际化方案 12 | 13 | ## 在线预览 14 | - [brisk-admin](http://brisk-admin.ybym.top/) 15 | 16 | 账号:admin或editor,密码:123456(随意) 17 | 18 | ## 文档 19 | 20 | [文档地址](http://brisk-admin-doc.ybym.top/) 21 | 22 | ## 准备 23 | 24 | - [node](http://nodejs.org/) 和 [git](https://git-scm.com/) -项目开发环境 25 | - [Vite](https://vitejs.dev/) - 熟悉 vite 特性 26 | - [Vue3](https://v3.vuejs.org/) - 熟悉 Vue 基础语法 27 | - [Es6+](http://es6.ruanyifeng.com/) - 熟悉 es6 基本语法 28 | - [Vue-Router-Next](https://next.router.vuejs.org/) - 熟悉 vue-router 基本使用 29 | - [element-plus](https://element-plus.gitee.io/) - ui 基本使用 30 | - [Mock.js](https://github.com/nuysoft/Mock) - mockjs 基本语法 31 | 32 | ## 安装使用 33 | 34 | - 获取项目代码 35 | 36 | ```bash 37 | git clone https://github.com/ZHT131/brisk-admin.git 38 | ``` 39 | 40 | - 安装依赖 41 | 42 | ```bash 43 | cd brisk-admin 44 | 45 | npm install 46 | 47 | ``` 48 | 49 | - 运行 50 | 51 | ```bash 52 | npm run dev 53 | ``` 54 | 55 | - 打包 56 | 57 | ```bash 58 | npm run build 59 | ``` 60 | 61 | ## 更新日志 62 | 63 | [CHANGELOG](./CHANGELOG.md) 64 | 65 | ## 如何贡献 66 | 67 | 非常欢迎你的加入![提一个 Issue](https://github.com/ZHT131/brisk-admin/issues) 或者提交一个 Pull Request。 68 | 69 | **Pull Request:** 70 | 71 | 1. Fork 代码! 72 | 2. 创建自己的分支: `git checkout -b feat/xxxx` 73 | 3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'` 74 | 4. 推送您的分支: `git push origin feat/xxxx` 75 | 5. 提交`pull request` 76 | 77 | ## Git 贡献提交规范 78 | 79 | - 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)) 80 | 81 | - `feat` 增加新功能 82 | - `fix` 修复问题/BUG 83 | - `style` 代码风格相关无影响运行结果的 84 | - `perf` 优化/性能提升 85 | - `refactor` 重构 86 | - `revert` 撤销修改 87 | - `test` 测试相关 88 | - `docs` 文档/注释 89 | - `chore` 依赖更新/脚手架配置修改等 90 | - `workflow` 工作流改进 91 | - `ci` 持续集成 92 | - `types` 类型定义文件更改 93 | - `wip` 开发中 94 | 95 | ## 浏览器支持 96 | 97 | 本地开发推荐使用`Chrome 80+` 浏览器 98 | 99 | 支持现代浏览器, 不支持 IE 100 | 101 | 102 | ## 交流 103 | 104 | `Brisk Admin` 是完全开源免费的项目,在帮助开发者更方便地进行后台管理系统开发,同时也提供 QQ 交流群使用问题欢迎在群内提问。 105 | 106 | - QQ 群 [782498919](https://qm.qq.com/cgi-bin/qm/qr?k=88SWYIJNDUZaM_S2R4iN1uGOeh8ujqb0&jump_from=webapi) 107 | 108 | ## 赞助 109 | #### 如果你觉得这个项目帮助到了你,你可以帮作者买一杯果汁表示鼓励 🍹。 110 | 111 | ![donate](./pay.png) 112 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ymadmin 8 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /mock/data/admin.js: -------------------------------------------------------------------------------- 1 | import Mock from "mockjs"; 2 | 3 | const adminUser = (params) => { 4 | let body = JSON.parse(params.body); 5 | let data = { 6 | page: body.page, 7 | size: body.size, 8 | rows: [], 9 | total: 30, 10 | }; 11 | 12 | for (let index = 0; index < body.size; index++) { 13 | data.rows.push( 14 | Mock.mock({ 15 | id: "@integer(1, 100)", 16 | username: "@string('lower', 5)", 17 | nickname: "@ctitle", 18 | group_id: "@integer(1, 10)", 19 | "group|2": { 20 | name: "管理员组", 21 | id: 1, 22 | }, 23 | status: 1, 24 | status_text: "正常", 25 | createtime: "@datetime", 26 | }) 27 | ); 28 | } 29 | return data; 30 | }; 31 | 32 | const adminGroup = (params) => { 33 | let body = JSON.parse(params.body); 34 | let data = { 35 | page: body.page, 36 | size: body.size, 37 | rows: [], 38 | total: 30, 39 | }; 40 | 41 | for (let index = 0; index < body.size; index++) { 42 | data.rows.push( 43 | Mock.mock({ 44 | id: "@integer(1, 100000)", 45 | pid: 0, 46 | name: "@ctitle", 47 | status: 1, 48 | status_text: "正常", 49 | createtime: "@datetime", 50 | children: [ 51 | { 52 | id: "@integer(1, 100)", 53 | pid: 1, 54 | name: "@ctitle", 55 | status: 1, 56 | status_text: "正常", 57 | createtime: "@datetime", 58 | }, 59 | ], 60 | }) 61 | ); 62 | } 63 | return data; 64 | }; 65 | 66 | const adminLog = (params) => { 67 | let body = JSON.parse(params.body); 68 | let data = { 69 | page: body.page, 70 | size: body.size, 71 | rows: [], 72 | total: 30, 73 | }; 74 | 75 | for (let index = 0; index < body.size; index++) { 76 | data.rows.push( 77 | Mock.mock({ 78 | id: "@integer(1, 100)", 79 | username: "@string('lower', 5)", 80 | title: "@ctitle", 81 | ip: "127.0.0.1", 82 | path_url: "/admin/index/index", 83 | status: 1, 84 | status_text: "正常", 85 | createtime: "@datetime", 86 | }) 87 | ); 88 | } 89 | return data; 90 | }; 91 | 92 | const adminRule = (params) => { 93 | let body = JSON.parse(params.body); 94 | let data = { 95 | page: body.page, 96 | size: body.size, 97 | rows: [], 98 | total: 30, 99 | }; 100 | 101 | for (let index = 0; index < body.size; index++) { 102 | data.rows.push( 103 | Mock.mock({ 104 | id: "@integer(1, 100)", 105 | pid: 0, 106 | title: "@ctitle", 107 | rule: "auth/admin", 108 | status: 1, 109 | status_text: "正常", 110 | createtime: "@datetime", 111 | children: [ 112 | { 113 | id: "@integer(1, 100)", 114 | pid: 1, 115 | title: "@ctitle", 116 | rule: "auth/admin", 117 | status: 1, 118 | status_text: "正常", 119 | createtime: "@datetime", 120 | }, 121 | ], 122 | }) 123 | ); 124 | } 125 | return data; 126 | }; 127 | 128 | export { adminUser, adminGroup, adminLog, adminRule }; 129 | -------------------------------------------------------------------------------- /mock/data/user.js: -------------------------------------------------------------------------------- 1 | import Mock from "mockjs"; 2 | 3 | const login = (params) => { 4 | const data = JSON.parse(params.body); 5 | const users = { 6 | admin: { 7 | token: "admin-token", 8 | avatar: 9 | "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif", 10 | nickname: "Admin", 11 | group: "admin", 12 | }, 13 | editor: { 14 | token: "editor-token", 15 | avatar: 16 | "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif", 17 | nickname: "Editor", 18 | group: "editor", 19 | }, 20 | }; 21 | if (!users[data.username]) { 22 | return { 23 | code: 0, 24 | message: "Account and password are incorrect.", 25 | }; 26 | } 27 | 28 | return { 29 | code: 1, 30 | data: users[data.username], 31 | }; 32 | }; 33 | 34 | const loginOut = (params) => { 35 | return { 36 | code: 1, 37 | data: params, 38 | }; 39 | }; 40 | 41 | const authRoutes = (params) => { 42 | let body = JSON.parse(params.body); 43 | if (body.group == "admin") { 44 | // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 45 | // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 46 | // 若你想不管路由下面的 children 声明的个数都显示你的根路由 47 | // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 48 | // 你可以设置 keepAlive: true,这样它就会缓存页面 49 | const routes = [ 50 | { 51 | path: "/", 52 | component: "layout/index.vue", 53 | redirect: "/dashboard", 54 | meta: { 55 | title: "home", 56 | icon: "ri-home-line", 57 | keepAlive: false, 58 | tabShow: false, 59 | }, 60 | alwaysShow: false, 61 | name: "app", 62 | children: [ 63 | { 64 | path: "dashboard", 65 | component: "dashboard/index.vue", 66 | name: "dashboard", 67 | meta: { 68 | title: "dashboard", 69 | icon: "ri-home-line", 70 | keepAlive: true, 71 | tabShow: true, 72 | }, 73 | redirect: null, 74 | alwaysShow: false, 75 | }, 76 | ], 77 | }, 78 | { 79 | path: "/profile", 80 | component: "layout/index.vue", 81 | redirect: "/profile/index", 82 | meta: { 83 | title: "profile", 84 | icon: "ri-home-line", 85 | keepAlive: false, 86 | tabShow: false, 87 | }, 88 | name: "profile", 89 | alwaysShow: false, 90 | hidden: true, 91 | children: [ 92 | { 93 | path: "index", 94 | component: "profile/index.vue", 95 | name: "profileIndex", 96 | meta: { 97 | title: "profileIndex", 98 | icon: "ri-home-line", 99 | keepAlive: true, 100 | tabShow: true, 101 | }, 102 | redirect: null, 103 | alwaysShow: false, 104 | }, 105 | ], 106 | }, 107 | { 108 | path: "/auth", 109 | component: "layout/index.vue", 110 | redirect: null, 111 | meta: { 112 | title: "auth", 113 | icon: "ri-file-user-line", 114 | keepAlive: false, 115 | tabShow: false, 116 | }, 117 | alwaysShow: true, 118 | name: "auth", 119 | children: [ 120 | { 121 | path: "admin", 122 | component: "admin/index.vue", 123 | name: "admin", 124 | meta: { 125 | title: "admin", 126 | icon: "ri-admin-line", 127 | keepAlive: true, 128 | tabShow: true, 129 | }, 130 | redirect: null, 131 | alwaysShow: false, 132 | }, 133 | { 134 | path: "adminLog", 135 | component: "adminLog/index.vue", 136 | name: "adminLog", 137 | meta: { 138 | title: "adminLog", 139 | icon: "ri-file-list-line", 140 | keepAlive: true, 141 | tabShow: true, 142 | }, 143 | alwaysShow: false, 144 | redirect: null, 145 | }, 146 | { 147 | path: "adminGroup", 148 | component: "adminGroup/index.vue", 149 | name: "adminGroup", 150 | meta: { 151 | title: "adminGroup", 152 | icon: "ri-group-line", 153 | keepAlive: true, 154 | tabShow: true, 155 | }, 156 | alwaysShow: false, 157 | redirect: null, 158 | }, 159 | { 160 | path: "adminRule", 161 | component: "adminRule/index.vue", 162 | name: "adminRule", 163 | meta: { 164 | title: "adminRule", 165 | icon: "ri-menu-line", 166 | keepAlive: true, 167 | tabShow: true, 168 | }, 169 | alwaysShow: false, 170 | redirect: null, 171 | }, 172 | ], 173 | }, 174 | { 175 | path: "/error_page", 176 | component: "layout/index.vue", 177 | name: "error_page", 178 | redirect: null, 179 | meta: { 180 | title: "error_page", 181 | icon: "ri-error-warning-line", 182 | keepAlive: false, 183 | tabShow: false, 184 | }, 185 | alwaysShow: true, 186 | children: [ 187 | { 188 | path: "401", 189 | component: "401/index.vue", 190 | name: "page401", 191 | meta: { 192 | title: "page401", 193 | icon: "ri-error-warning-line", 194 | keepAlive: true, 195 | tabShow: true, 196 | }, 197 | redirect: null, 198 | alwaysShow: false, 199 | }, 200 | { 201 | path: "404", 202 | component: "404/index.vue", 203 | name: "page404", 204 | meta: { 205 | title: "page404", 206 | icon: "ri-error-warning-line", 207 | keepAlive: true, 208 | tabShow: true, 209 | }, 210 | redirect: null, 211 | alwaysShow: false, 212 | }, 213 | ], 214 | }, 215 | //嵌套路由示例 216 | { 217 | path: "/nested", 218 | component: "layout/index.vue", 219 | name: "nested", 220 | redirect: null, 221 | meta: { 222 | title: "nested", 223 | icon: "ri-stack-fill", 224 | keepAlive: false, 225 | tabShow: false, 226 | }, 227 | alwaysShow: true, 228 | children: [ 229 | { 230 | path: "menu", 231 | component: "noComponent", 232 | name: "menu", 233 | meta: { 234 | title: "menu", 235 | icon: "ri-apps-2-fill", 236 | keepAlive: false, 237 | tabShow: false, 238 | }, 239 | redirect: null, 240 | alwaysShow: true, 241 | children: [ 242 | { 243 | path: "menu2", 244 | component: "noComponent", 245 | name: "menu2", 246 | meta: { 247 | title: "menu2", 248 | icon: "ri-apps-2-fill", 249 | keepAlive: false, 250 | tabShow: false, 251 | }, 252 | redirect: null, 253 | alwaysShow: true, 254 | children: [ 255 | { 256 | path: "menu4", 257 | component: "menu4/index.vue", 258 | name: "menu4", 259 | meta: { 260 | title: "menu4", 261 | icon: "ri-apps-2-fill", 262 | keepAlive: true, 263 | tabShow: true, 264 | }, 265 | redirect: null, 266 | alwaysShow: false, 267 | }, 268 | ], 269 | }, 270 | { 271 | path: "menu3", 272 | component: "menu3/index.vue", 273 | name: "menu3", 274 | meta: { 275 | title: "menu3", 276 | icon: "ri-apps-2-fill", 277 | keepAlive: true, 278 | tabShow: true, 279 | }, 280 | redirect: null, 281 | alwaysShow: false, 282 | }, 283 | ], 284 | }, 285 | { 286 | path: "menu1", 287 | component: "menu1/index.vue", 288 | name: "menu1", 289 | meta: { 290 | title: "menu1", 291 | icon: "ri-apps-2-fill", 292 | keepAlive: true, 293 | tabShow: true, 294 | }, 295 | redirect: null, 296 | alwaysShow: false, 297 | }, 298 | ], 299 | }, 300 | ]; 301 | return { 302 | code: 1, 303 | data: routes, 304 | }; 305 | } else { 306 | // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面 307 | // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 308 | // 若你想不管路由下面的 children 声明的个数都显示你的根路由 309 | // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由 310 | // 你可以设置 keepAlive: true,这样它就会缓存页面 311 | const routes = [ 312 | { 313 | path: "/", 314 | component: "layout/index.vue", 315 | redirect: "/dashboard", 316 | meta: { 317 | title: "home", 318 | icon: "ri-home-line", 319 | keepAlive: false, 320 | tabShow: false, 321 | }, 322 | alwaysShow: false, 323 | name: "app", 324 | children: [ 325 | { 326 | path: "dashboard", 327 | component: "dashboard/index.vue", 328 | name: "dashboard", 329 | meta: { 330 | title: "dashboard", 331 | icon: "ri-home-line", 332 | keepAlive: true, 333 | tabShow: true, 334 | }, 335 | redirect: null, 336 | alwaysShow: false, 337 | }, 338 | ], 339 | }, 340 | { 341 | path: "/profile", 342 | component: "layout/index.vue", 343 | redirect: "/profile/index", 344 | meta: { 345 | title: "profile", 346 | icon: "ri-home-line", 347 | keepAlive: false, 348 | tabShow: false, 349 | }, 350 | name: "profile", 351 | alwaysShow: false, 352 | hidden: true, 353 | children: [ 354 | { 355 | path: "index", 356 | component: "profile/index.vue", 357 | name: "profileIndex", 358 | meta: { 359 | title: "profileIndex", 360 | icon: "ri-home-line", 361 | keepAlive: true, 362 | tabShow: true, 363 | }, 364 | redirect: null, 365 | alwaysShow: false, 366 | }, 367 | ], 368 | }, 369 | { 370 | path: "/auth", 371 | component: "layout/index.vue", 372 | redirect: null, 373 | meta: { 374 | title: "auth", 375 | icon: "ri-file-user-line", 376 | keepAlive: false, 377 | tabShow: false, 378 | }, 379 | alwaysShow: true, 380 | name: "auth", 381 | children: [ 382 | { 383 | path: "admin", 384 | component: "admin/index.vue", 385 | name: "admin", 386 | meta: { 387 | title: "admin", 388 | icon: "ri-admin-line", 389 | keepAlive: true, 390 | tabShow: true, 391 | }, 392 | redirect: null, 393 | alwaysShow: false, 394 | }, 395 | { 396 | path: "adminLog", 397 | component: "adminLog/index.vue", 398 | name: "adminLog", 399 | meta: { 400 | title: "adminLog", 401 | icon: "ri-file-list-line", 402 | keepAlive: true, 403 | tabShow: true, 404 | }, 405 | alwaysShow: false, 406 | redirect: null, 407 | }, 408 | ], 409 | }, 410 | { 411 | path: "/error_page", 412 | component: "layout/index.vue", 413 | name: "error_page", 414 | redirect: null, 415 | meta: { 416 | title: "error_page", 417 | icon: "ri-error-warning-line", 418 | keepAlive: false, 419 | tabShow: false, 420 | }, 421 | alwaysShow: true, 422 | children: [ 423 | { 424 | path: "401", 425 | component: "401/index.vue", 426 | name: "page401", 427 | meta: { 428 | title: "page401", 429 | icon: "ri-error-warning-line", 430 | keepAlive: true, 431 | tabShow: true, 432 | }, 433 | redirect: null, 434 | alwaysShow: false, 435 | }, 436 | { 437 | path: "404", 438 | component: "404/index.vue", 439 | name: "page404", 440 | meta: { 441 | title: "page404", 442 | icon: "ri-error-warning-line", 443 | keepAlive: true, 444 | tabShow: true, 445 | }, 446 | redirect: null, 447 | alwaysShow: false, 448 | }, 449 | ], 450 | }, 451 | //嵌套路由示例 452 | { 453 | path: "/nested", 454 | component: "layout/index.vue", 455 | name: "nested", 456 | redirect: null, 457 | meta: { 458 | title: "nested", 459 | icon: "ri-stack-fill", 460 | keepAlive: false, 461 | tabShow: false, 462 | }, 463 | alwaysShow: true, 464 | children: [ 465 | { 466 | path: "menu", 467 | component: "noComponent", 468 | name: "menu", 469 | meta: { 470 | title: "menu", 471 | icon: "ri-apps-2-fill", 472 | keepAlive: false, 473 | tabShow: false, 474 | }, 475 | redirect: null, 476 | alwaysShow: true, 477 | children: [ 478 | { 479 | path: "menu2", 480 | component: "noComponent", 481 | name: "menu2", 482 | meta: { 483 | title: "menu2", 484 | icon: "ri-apps-2-fill", 485 | keepAlive: false, 486 | tabShow: false, 487 | }, 488 | redirect: null, 489 | alwaysShow: true, 490 | children: [ 491 | { 492 | path: "menu4", 493 | component: "menu4/index.vue", 494 | name: "menu4", 495 | meta: { 496 | title: "menu4", 497 | icon: "ri-apps-2-fill", 498 | keepAlive: true, 499 | tabShow: true, 500 | }, 501 | redirect: null, 502 | alwaysShow: false, 503 | }, 504 | ], 505 | }, 506 | { 507 | path: "menu3", 508 | component: "menu3/index.vue", 509 | name: "menu3", 510 | meta: { 511 | title: "menu3", 512 | icon: "ri-apps-2-fill", 513 | keepAlive: true, 514 | tabShow: true, 515 | }, 516 | redirect: null, 517 | alwaysShow: false, 518 | }, 519 | ], 520 | }, 521 | { 522 | path: "menu1", 523 | component: "menu1/index.vue", 524 | name: "menu1", 525 | meta: { 526 | title: "menu1", 527 | icon: "ri-apps-2-fill", 528 | keepAlive: true, 529 | tabShow: true, 530 | }, 531 | redirect: null, 532 | alwaysShow: false, 533 | }, 534 | ], 535 | }, 536 | ]; 537 | return { 538 | code: 1, 539 | data: routes, 540 | }; 541 | } 542 | }; 543 | 544 | export { login, loginOut, authRoutes }; 545 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | import Mock from "mockjs"; 2 | import { adminUser, adminGroup, adminLog, adminRule } from "./data/admin"; 3 | import { login, loginOut, authRoutes } from "./data/user"; 4 | Mock.mock("http://127.0.0.1/api/login", "post", login); 5 | Mock.mock("http://127.0.0.1/api/authRoutes", "post", authRoutes); 6 | Mock.mock("http://127.0.0.1/api/adminUser", "post", adminUser); 7 | Mock.mock("http://127.0.0.1/api/adminGroup", "post", adminGroup); 8 | Mock.mock("http://127.0.0.1/api/adminLog", "post", adminLog); 9 | Mock.mock("http://127.0.0.1/api/adminRule", "post", adminRule); 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-web", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@antv/g2": "^4.1.37", 11 | "@element-plus/icons-vue": "^0.2.4", 12 | "axios": "^0.24.0", 13 | "element-plus": "^1.3.0-beta.5", 14 | "file-saver": "^2.0.5", 15 | "js-cookie": "^3.0.1", 16 | "mockjs": "^1.1.0", 17 | "nprogress": "^0.2.0", 18 | "remixicon": "^2.5.0", 19 | "screenfull": "^6.0.0", 20 | "vue": "^3.2.25", 21 | "vue-i18n": "^9.1.7", 22 | "vue-router": "^4.0.11", 23 | "vuex": "^4.0.2", 24 | "xlsx": "^0.17.5" 25 | }, 26 | "devDependencies": { 27 | "@vitejs/plugin-vue": "^2.0.1", 28 | "unplugin-element-plus": "^0.2.0", 29 | "sass": "^1.47.0", 30 | "vite": "^2.7.10" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHT131/brisk-admin/4b40d75d3fad7503fbe3160acdd3a055de35d97b/pay.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHT131/brisk-admin/4b40d75d3fad7503fbe3160acdd3a055de35d97b/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 34 | -------------------------------------------------------------------------------- /src/api/common.js: -------------------------------------------------------------------------------- 1 | import axios from "../utils/http/axios"; 2 | //公共请求 3 | export function comReq(url, method, data) { 4 | return axios({ 5 | url: url, 6 | method: method, 7 | data, 8 | config: {}, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from "../utils/http/axios" 2 | 3 | export const login = (data) => { 4 | return axios({ 5 | url: "api/login", 6 | method: "post", 7 | data, 8 | config: {} 9 | }) 10 | } 11 | export const authRoutes = (data) => { 12 | return axios({ 13 | url: "api/authRoutes", 14 | method: "post", 15 | data, 16 | config: {} 17 | }) 18 | } -------------------------------------------------------------------------------- /src/assets/images/401.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHT131/brisk-admin/4b40d75d3fad7503fbe3160acdd3a055de35d97b/src/assets/images/401.png -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHT131/brisk-admin/4b40d75d3fad7503fbe3160acdd3a055de35d97b/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/loginbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHT131/brisk-admin/4b40d75d3fad7503fbe3160acdd3a055de35d97b/src/assets/images/loginbg.png -------------------------------------------------------------------------------- /src/assets/images/tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZHT131/brisk-admin/4b40d75d3fad7503fbe3160acdd3a055de35d97b/src/assets/images/tabs.png -------------------------------------------------------------------------------- /src/components/crud/brisk-dialogcom.vue: -------------------------------------------------------------------------------- 1 | 2 | 16 | 77 | 82 | -------------------------------------------------------------------------------- /src/components/crud/brisk-operate.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/crud/brisk-pagination.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | 18 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/crud/brisk-search-btn.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 16 | 24 | -------------------------------------------------------------------------------- /src/components/crud/brisk-toolbar.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 68 | 69 | 86 | -------------------------------------------------------------------------------- /src/components/crud/index.js: -------------------------------------------------------------------------------- 1 | import { comReq } from "~/api/common.js"; 2 | import { ElMessage } from "element-plus"; 3 | function curd(options) { 4 | return { 5 | data() { 6 | return { 7 | //显示/隐藏搜索栏 8 | showSearch: false, 9 | // 主页操作栏显示哪些按钮 10 | toolShow: { 11 | add: true, 12 | edit: true, 13 | del: true, 14 | export: true, 15 | }, 16 | //显示/隐藏列数据 17 | showColumns: {}, 18 | //表格加载状态 19 | loadingStatus: false, 20 | //表格列筛选数据 21 | tableColumns: [], 22 | //表格列选中状态 23 | allColumnsSelected: true, 24 | allColumnsSelectedIndeterminate: false, 25 | //分页组件属性 26 | pageMobileLayout: "total, prev, pager, next", 27 | pageDesktopLayout: "total, sizes, prev, pager, next, jumper", 28 | currentPage: 1, 29 | pageTotal: 0, 30 | pageSize: 10, 31 | pageSizes: [10, 20, 30, 40, 50], 32 | //表格数据 33 | tableData: [], 34 | //请求url 35 | url: "", 36 | //操作栏显示按钮 37 | showOperate: { 38 | view: true, 39 | edit: true, 40 | del: true, 41 | }, 42 | addDialogFormVisible: false, 43 | editDialogFormVisible: false, 44 | detailDialogFormVisible: false, 45 | multipleSelection: [], 46 | exportLoading: false, //导出按钮动画 47 | filename: "", //导出文件名 48 | autoWidth: true, //导出自动宽度 49 | bookType: "xlsx", //导出文件类型 50 | //导入并合并初始化数据 51 | ...options, 52 | }; 53 | }, 54 | created() { 55 | // 加载数据 56 | this.getList(); 57 | }, 58 | methods: { 59 | // 请求查询列表数据 60 | getList() { 61 | this.loadingStatus = true; 62 | comReq(this.url, "post", { 63 | page: this.currentPage, 64 | size: this.pageSize, 65 | }) 66 | .then((res) => { 67 | this.loadingStatus = false; 68 | this.tableData = res.rows; 69 | this.pageTotal = res.total; 70 | }) 71 | .catch(() => { 72 | this.loadStatus = false; 73 | }); 74 | }, 75 | // 全选列 76 | handleCheckAllChange(val) { 77 | if (val === false) { 78 | this.allColumnsSelected = true; 79 | return; 80 | } 81 | this.tableColumns.forEach((column) => { 82 | if (!column.visible) { 83 | column.visible = true; 84 | this.updateColumnVisible(column); 85 | } 86 | }); 87 | this.allColumnsSelected = val; 88 | this.allColumnsSelectedIndeterminate = false; 89 | }, 90 | // 单选列 91 | handleCheckChange(item) { 92 | let totalCount = 0; 93 | let selectedCount = 0; 94 | this.tableColumns.forEach((column) => { 95 | ++totalCount; 96 | selectedCount += column.visible ? 1 : 0; 97 | }); 98 | if (selectedCount === 0) { 99 | console.log("至少选择一项"); 100 | this.$nextTick(function () { 101 | item.visible = true; 102 | }); 103 | return; 104 | } 105 | this.allColumnsSelected = selectedCount === totalCount; 106 | this.allColumnsSelectedIndeterminate = 107 | selectedCount !== totalCount && selectedCount !== 0; 108 | this.updateColumnVisible(item); 109 | }, 110 | //更新列 111 | updateColumnVisible(item) { 112 | this.showColumns[item.property] = item.visible; 113 | }, 114 | // 显示/隐藏搜索 115 | changeSearchShow() { 116 | this.showSearch = !this.showSearch; 117 | }, 118 | // 刷新表格数据 119 | refresh() { 120 | this.getList(); 121 | }, 122 | //提交搜索 123 | submitSearchForm() { 124 | this.$refs.searchForm.validate((valid) => { 125 | if (valid) { 126 | console.log("submit!"); 127 | } else { 128 | console.log("error submit!!"); 129 | return false; 130 | } 131 | }); 132 | }, 133 | //重置搜索 134 | resetSearchForm() { 135 | this.$refs.searchForm.resetFields(); 136 | }, 137 | //每页条数变化 138 | handleSizeChange(val) { 139 | this.pageSize = val; 140 | this.currentPage = 1; 141 | this.getList(); 142 | }, 143 | //页码变化 144 | handleCurrentChange(val) { 145 | this.currentPage = val; 146 | this.getList(); 147 | }, 148 | //表格选择变化 149 | selectionChange(val) { 150 | this.multipleSelection = val; 151 | }, 152 | //新增 153 | handleAdd() { 154 | this.addDialogFormVisible = true; 155 | }, 156 | //选择编辑 157 | handleSelectEdit() { 158 | if (this.multipleSelection.length === 0) { 159 | return ElMessage("请先选择需要编辑的数据"); 160 | } 161 | this.editForm = this.multipleSelection[0]; 162 | this.editDialogFormVisible = true; 163 | }, 164 | //选择删除 165 | handleSelectDel() { 166 | if (this.multipleSelection.length === 0) { 167 | return ElMessage("请先选择需要删除的数据"); 168 | } 169 | this.$confirm( 170 | this.$t("delConfirm.delmsg"), 171 | this.$t("delConfirm.title"), 172 | { 173 | confirmButtonText: this.$t("delConfirm.comfirm"), 174 | cancelButtonText: this.$t("delConfirm.cancle"), 175 | type: "warning", 176 | } 177 | ) 178 | .then(() => { 179 | this.$message({ 180 | type: "success", 181 | message: "删除成功!", 182 | }); 183 | }) 184 | .catch(() => { 185 | this.$message({ 186 | type: "info", 187 | message: "已取消删除", 188 | }); 189 | }); 190 | }, 191 | //导出 192 | handleExport(type) { 193 | this.bookType = type; 194 | this.exportLoading = true; 195 | let tHeader = []; 196 | let filterVal = []; 197 | this.tableColumns.map((item) => { 198 | tHeader.push(this.$t(item.label)); 199 | filterVal.push(item.property); 200 | }); 201 | import("~/vendor/Export2Excel").then((excel) => { 202 | const list = this.tableData; 203 | const data = this.formatJson(filterVal, list); 204 | excel.export_json_to_excel({ 205 | header: tHeader, 206 | data, 207 | filename: this.filename, 208 | autoWidth: this.autoWidth, 209 | bookType: this.bookType, 210 | }); 211 | this.exportLoading = false; 212 | }); 213 | }, 214 | //处理并获取导出数据 215 | formatJson(filterVal, jsonData) { 216 | return jsonData.map((v) => 217 | filterVal.map((j) => { 218 | if (j.indexOf("_dot_") != -1) { 219 | let arr = j.split("_dot_"); 220 | let val = this.exportTreeFilter(v, arr); 221 | return val; 222 | } else { 223 | return v[j]; 224 | } 225 | }) 226 | ); 227 | }, 228 | //递归处理关联嵌套数据 229 | exportTreeFilter(v, arr) { 230 | if (arr.length > 0) { 231 | let newarr = arr; 232 | for (let index = 0; index < arr.length; index++) { 233 | const item = arr[index]; 234 | newarr.splice(index, 1); 235 | return this.exportTreeFilter(v[item], newarr); 236 | } 237 | } else { 238 | return v; 239 | } 240 | }, 241 | //查看 242 | handleView(row) { 243 | this.detailDialogFormVisible = true; 244 | }, 245 | //编辑 246 | handleEdit(row) { 247 | this.editForm = row; 248 | this.editDialogFormVisible = true; 249 | }, 250 | //删除 251 | handleDel(row) { 252 | console.log(row); 253 | this.$confirm( 254 | this.$t("delConfirm.delmsg"), 255 | this.$t("delConfirm.title"), 256 | { 257 | confirmButtonText: this.$t("delConfirm.comfirm"), 258 | cancelButtonText: this.$t("delConfirm.cancle"), 259 | type: "warning", 260 | } 261 | ) 262 | .then(() => { 263 | this.$message({ 264 | type: "success", 265 | message: "删除成功!", 266 | }); 267 | }) 268 | .catch(() => { 269 | this.$message({ 270 | type: "info", 271 | message: "已取消删除", 272 | }); 273 | }); 274 | }, 275 | //提交新增 276 | addSubmit() { 277 | console.log(this.addForm); 278 | ElMessage.success({ 279 | message: "演示执行新增提交", 280 | type: "success", 281 | }); 282 | this.addDialogFormVisible = false; 283 | }, 284 | //取消新增 285 | addCancle() { 286 | this.$refs.addForm.resetFields(); 287 | this.addDialogFormVisible = false; 288 | }, 289 | //提交编辑 290 | editSubmit() { 291 | console.log(this.addForm); 292 | ElMessage.success({ 293 | message: "演示执行编辑提交", 294 | type: "success", 295 | }); 296 | this.editDialogFormVisible = false; 297 | }, 298 | //取消编辑 299 | editCancle() { 300 | this.$refs.editForm.resetFields(); 301 | this.editDialogFormVisible = false; 302 | }, 303 | //详情确认 304 | detailCancle() { 305 | this.detailDialogFormVisible = false; 306 | }, 307 | //详情取消 308 | detailSubmit() { 309 | this.detailDialogFormVisible = false; 310 | }, 311 | }, 312 | }; 313 | } 314 | 315 | export default curd; 316 | 317 | // 分页mixins 318 | var pagination = { 319 | props: { 320 | currentPage: { 321 | type: Number, 322 | default: 1, 323 | }, 324 | pageSize: { 325 | type: Number, 326 | default: 10, 327 | }, 328 | pageSizes: { 329 | type: Array, 330 | default: [], 331 | }, 332 | pageTotal: { 333 | type: Number, 334 | default: 0, 335 | }, 336 | pageMobileLayout: { 337 | type: String, 338 | default: "", 339 | }, 340 | pageDesktopLayout: { 341 | type: String, 342 | default: "", 343 | }, 344 | device: { 345 | type: String, 346 | default: "", 347 | }, 348 | }, 349 | methods: { 350 | //每页条数变化 351 | handleSizeChange(val) { 352 | this.$emit("handleSizeChange", val); 353 | }, 354 | //当前页码变化 355 | handleCurrentChange(val) { 356 | this.$emit("handleCurrentChange", val); 357 | }, 358 | }, 359 | }; 360 | // 搜索mixins 361 | var search = { 362 | props: {}, 363 | methods: { 364 | // 提交搜索 365 | submitSearchForm(val) { 366 | this.$emit("submitSearchForm", val); 367 | }, 368 | // 重置搜索 369 | resetSearchForm(item) { 370 | this.$emit("resetSearchForm", item); 371 | }, 372 | }, 373 | }; 374 | // 工具栏mixins 375 | var tools = { 376 | props: { 377 | toolShow: { 378 | type: Object, 379 | default: {}, 380 | }, 381 | tableColumns: { 382 | type: Array, 383 | default: [], 384 | }, 385 | allColumnsSelected: { 386 | type: Boolean, 387 | default: true, 388 | }, 389 | allColumnsSelectedIndeterminate: { 390 | type: Boolean, 391 | default: false, 392 | }, 393 | exportLoading: { 394 | type: Boolean, 395 | default: false, 396 | }, 397 | }, 398 | methods: { 399 | // 全选列 400 | checkAllChange(val) { 401 | this.$emit("handleCheckAllChange", val); 402 | }, 403 | // 单选列 404 | checkChange(item) { 405 | this.$emit("handleCheckChange", item); 406 | }, 407 | changeSearchShow() { 408 | this.$emit("changeSearchShow"); 409 | }, 410 | refresh() { 411 | this.$emit("refresh"); 412 | }, 413 | }, 414 | }; 415 | 416 | // 操作栏mixins 417 | var operate = { 418 | props: { 419 | width: { 420 | type: Number, 421 | default: 180, 422 | }, 423 | device: { 424 | type: String, 425 | default: "", 426 | }, 427 | showOperate: { 428 | type: Object, 429 | default: {}, 430 | }, 431 | }, 432 | methods: { 433 | handleView(row) { 434 | this.$emit("handleView", row); 435 | }, 436 | handleEdit(row) { 437 | this.$emit("handleEdit", row); 438 | }, 439 | handleDel(row) { 440 | this.$emit("handleDel", row); 441 | }, 442 | }, 443 | }; 444 | 445 | export { pagination, search, tools, operate }; 446 | -------------------------------------------------------------------------------- /src/lang/en.js: -------------------------------------------------------------------------------- 1 | //en 2 | const modulesFiles = import.meta.globEager("./modules/*/index_en.js"); 3 | const modules = Object.keys(modulesFiles).reduce((modules, modulePath) => { 4 | const value = modulesFiles[modulePath]; 5 | Object.assign(modules, value.default); 6 | return modules; 7 | }, {}); 8 | 9 | export default { 10 | app: { 11 | home: "home", 12 | setting_title: "setting", 13 | }, 14 | userDropdown: { 15 | userinfo: "userinfo", 16 | loginout: "loginout", 17 | }, 18 | login: { 19 | login: "login", 20 | username: "username", 21 | password: "password", 22 | usernamePlaceholder: "please enter user name", 23 | passwordPlaceholder: "Please enter the password", 24 | loginBtn: "login", 25 | }, 26 | 401: { 27 | 401: "401", 28 | }, 29 | 404: { 30 | 404: "404", 31 | }, 32 | ...modules, 33 | }; 34 | -------------------------------------------------------------------------------- /src/lang/index.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from "vue-i18n"; 2 | import enLocale from "element-plus/lib/locale/lang/en"; 3 | import zhLocale from "element-plus/lib/locale/lang/zh-cn"; 4 | import enLang from "./en"; 5 | import zhLang from "./zh"; 6 | import Cookies from "js-cookie"; 7 | 8 | const messages = { 9 | [enLocale.name]: { 10 | // el 这个属性很关键,一定要保证有这个属性, 11 | el: enLocale.el, 12 | // 定义您自己的字典,但是请不要和 `el` 重复,这样会导致 ElementPlus 内部组件的翻译失效. 13 | ...enLang, 14 | }, 15 | [zhLocale.name]: { 16 | el: zhLocale.el, 17 | // 定义您自己的字典,但是请不要和 `el` 重复,这样会导致 ElementPlus 内部组件的翻译失效. 18 | ...zhLang, 19 | }, 20 | }; 21 | 22 | const i18n = createI18n({ 23 | locale: Cookies.get("language") == "en" ? enLocale.name : zhLocale.name,//加入判断修复刷新页面不渲染选中语言的问题 24 | fallbackLocale: enLocale.name, 25 | messages, 26 | }); 27 | 28 | export default i18n; 29 | -------------------------------------------------------------------------------- /src/lang/modules/admin/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | admin: { 4 | admin: "admin", 5 | field: { 6 | id: "id", 7 | username: "username", 8 | nickname: "nickname", 9 | group_id: "group_id", 10 | group: { 11 | name: "group_name", 12 | }, 13 | status: "status", 14 | status_1: "status_1", 15 | status_2: "status_2", 16 | }, 17 | component: { 18 | select_placeholder: "choose", 19 | addlog_add_title: "add", 20 | addlog_edit_title: "edit", 21 | addlog_detail_title: "detail", 22 | detail_title: "title", 23 | detail_content: "content", 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/lang/modules/admin/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | admin: { 4 | admin: "管理员管理", 5 | field: { 6 | id: "ID", 7 | username: "用户名", 8 | nickname: "昵称", 9 | group_id: "组别ID", 10 | group: { 11 | name: "角色组名", 12 | }, 13 | status: "状态", 14 | status_1: "正常", 15 | status_2: "禁用", 16 | }, 17 | component: { 18 | select_placeholder: "请选择", 19 | addlog_add_title: "新增", 20 | addlog_edit_title: "编辑", 21 | addlog_detail_title: "详情", 22 | detail_title:"标题", 23 | detail_content:"内容", 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/lang/modules/adminGroup/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | adminGroup: { 4 | adminGroup: "adminGroup", 5 | field: { 6 | id: "id", 7 | name: "name", 8 | pid: "pid", 9 | status: "status", 10 | status_1: "status_1", 11 | status_2: "status_2", 12 | }, 13 | component: { 14 | select_placeholder: "choose", 15 | addlog_add_title: "add", 16 | addlog_edit_title: "edit", 17 | addlog_detail_title: "detail", 18 | detail_title: "title", 19 | detail_content: "content", 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lang/modules/adminGroup/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | adminGroup: { 4 | adminGroup: "管理员角色组", 5 | field: { 6 | id: "ID", 7 | name: "角色名", 8 | pid: "父ID", 9 | status: "状态", 10 | status_1: "正常", 11 | status_2: "禁用", 12 | }, 13 | component: { 14 | select_placeholder: "请选择", 15 | addlog_add_title: "新增", 16 | addlog_edit_title: "编辑", 17 | addlog_detail_title: "详情", 18 | detail_title: "标题", 19 | detail_content: "内容", 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lang/modules/adminLog/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | adminLog: { 4 | adminLog: "adminLog", 5 | field: { 6 | id: "id", 7 | username: "username", 8 | title: "title", 9 | path_url: "path_url", 10 | ip:'IP', 11 | status: "status", 12 | status_1: "status_1", 13 | status_2: "status_2", 14 | }, 15 | component: { 16 | select_placeholder: "choose", 17 | addlog_add_title: "add", 18 | addlog_edit_title: "edit", 19 | addlog_detail_title: "detail", 20 | detail_title: "title", 21 | detail_content: "content", 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/lang/modules/adminLog/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | adminLog: { 4 | adminLog: "管理员日志", 5 | field: { 6 | id: "ID", 7 | username: "用户名", 8 | title: "标题", 9 | path_url: "操作Url", 10 | ip:'IP', 11 | status: "状态", 12 | status_1: "正常", 13 | status_2: "禁用", 14 | }, 15 | component: { 16 | select_placeholder: "请选择", 17 | addlog_add_title: "新增", 18 | addlog_edit_title: "编辑", 19 | addlog_detail_title: "详情", 20 | detail_title: "标题", 21 | detail_content: "内容", 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/lang/modules/adminRule/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | adminRule: { 4 | adminRule: "adminRule", 5 | field: { 6 | id: "id", 7 | title: "title", 8 | rule: "rule", 9 | status: "status", 10 | status_1: "status_1", 11 | status_2: "status_2", 12 | }, 13 | component: { 14 | select_placeholder: "choose", 15 | addlog_add_title: "add", 16 | addlog_edit_title: "edit", 17 | addlog_detail_title: "detail", 18 | detail_title: "title", 19 | detail_content: "content", 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lang/modules/adminRule/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | adminRule: { 4 | adminRule: "菜单规则", 5 | field: { 6 | id: "ID", 7 | title: "标题", 8 | rule: "规则", 9 | status: "状态", 10 | status_1: "正常", 11 | status_2: "禁用", 12 | }, 13 | component: { 14 | select_placeholder: "请选择", 15 | addlog_add_title: "新增", 16 | addlog_edit_title: "编辑", 17 | addlog_detail_title: "详情", 18 | detail_title: "标题", 19 | detail_content: "内容", 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/lang/modules/auth/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | auth:{ 4 | auth:'auth', 5 | } 6 | } -------------------------------------------------------------------------------- /src/lang/modules/auth/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | auth:{ 4 | auth:'权限管理', 5 | } 6 | } -------------------------------------------------------------------------------- /src/lang/modules/crud/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | searchBtn: { 4 | query: "query", 5 | reset: "reset", 6 | }, 7 | toolBar: { 8 | add: "add", 9 | edit: "edit", 10 | delete: "delet", 11 | export: "export", 12 | select_all: "select_all", 13 | }, 14 | table: { 15 | operate: "operate", 16 | }, 17 | dialogcom: { 18 | comfirm: "comfirm", 19 | cancle: "cancle", 20 | }, 21 | delConfirm: { 22 | delmsg: 23 | "This operation will permanently delete the file, do you want to continue?", 24 | title: "hint", 25 | comfirm: "comfirm", 26 | cancle: "cancle", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/lang/modules/crud/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | searchBtn: { 4 | query: "查询", 5 | reset: "重置", 6 | }, 7 | toolBar: { 8 | add: "新增", 9 | edit: "编辑", 10 | delete: "删除", 11 | export: "导出", 12 | select_all: "全选", 13 | }, 14 | table: { 15 | operate: "操作", 16 | }, 17 | dialogcom: { 18 | comfirm: "确定", 19 | cancle: "取消", 20 | }, 21 | delConfirm: { 22 | delmsg: "此操作将永久删除该文件, 是否继续?", 23 | title: "提示", 24 | comfirm: "确定", 25 | cancle: "取消", 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/lang/modules/dashboard/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | dashboard:{ 4 | dashboard:'dashboard', 5 | totalUserNumber:'Total number of users', 6 | totalVisits:'Total visits', 7 | totalOrderNumber:'Total number of orders', 8 | totalSalesAmount:'Total sales amount', 9 | sales: 'Sales(yuan)', 10 | orderNumber: 'Number of order(single)', 11 | wxPay: 'WeChat Pay', 12 | aliPay: 'Pay with Ali-Pay', 13 | walletPay: 'Wallet payment', 14 | otherPay: 'Other payment', 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lang/modules/dashboard/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | dashboard: { 4 | dashboard: '控制台', 5 | totalUserNumber: '总用户数', 6 | totalVisits: '总访问量', 7 | totalOrderNumber: '总订单数', 8 | totalSalesAmount: '总销售金额', 9 | sales: '销售额(元)', 10 | orderNumber: '订单数(单)', 11 | wxPay: '微信支付', 12 | aliPay: '支付宝支付', 13 | walletPay: '钱包支付', 14 | otherPay: '其他支付', 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lang/modules/error_page/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | error_page:{ 4 | error_page:'error_page', 5 | }, 6 | page401:{ 7 | page401:'401' 8 | }, 9 | page404:{ 10 | page404:'404' 11 | } 12 | } -------------------------------------------------------------------------------- /src/lang/modules/error_page/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | error_page:{ 4 | error_page:'错误页面', 5 | }, 6 | page401:{ 7 | page401:'401' 8 | }, 9 | page404:{ 10 | page404:'404' 11 | } 12 | } -------------------------------------------------------------------------------- /src/lang/modules/nested/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | nested:{ 4 | nested:'nested' 5 | }, 6 | menu:{ 7 | menu:'menu', 8 | }, 9 | menu1:{ 10 | menu1:'menu1', 11 | }, 12 | menu2:{ 13 | menu2:'menu2' 14 | }, 15 | menu3:{ 16 | menu3:'menu3' 17 | }, 18 | menu4:{ 19 | menu4:'menu4' 20 | } 21 | } -------------------------------------------------------------------------------- /src/lang/modules/nested/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | nested:{ 4 | nested:'嵌套路由' 5 | }, 6 | menu:{ 7 | menu:'菜单', 8 | }, 9 | menu1:{ 10 | menu1:'菜单1', 11 | }, 12 | menu2:{ 13 | menu2:'菜单2' 14 | }, 15 | menu3:{ 16 | menu3:'菜单3' 17 | }, 18 | menu4:{ 19 | menu4:'菜单4' 20 | } 21 | } -------------------------------------------------------------------------------- /src/lang/modules/profile/index_en.js: -------------------------------------------------------------------------------- 1 | // en 2 | export default { 3 | profile:{ 4 | profile:'profile' 5 | }, 6 | profileIndex:{ 7 | profileIndex:'profileIndex' 8 | } 9 | } -------------------------------------------------------------------------------- /src/lang/modules/profile/index_zh.js: -------------------------------------------------------------------------------- 1 | // zh 2 | export default { 3 | profile:{ 4 | profile:'个人中心' 5 | }, 6 | profileIndex:{ 7 | profileIndex:'个人资料' 8 | } 9 | } -------------------------------------------------------------------------------- /src/lang/zh.js: -------------------------------------------------------------------------------- 1 | //zh 2 | const modulesFiles = import.meta.globEager("./modules/*/index_zh.js"); 3 | const modules = Object.keys(modulesFiles).reduce((modules, modulePath) => { 4 | const value = modulesFiles[modulePath]; 5 | Object.assign(modules, value.default); 6 | return modules; 7 | }, {}); 8 | 9 | export default { 10 | app: { 11 | home: "首页", 12 | setting_title: "应用设置", 13 | }, 14 | userDropdown: { 15 | userinfo: "个人资料", 16 | loginout: "退出", 17 | }, 18 | login: { 19 | login: "登录", 20 | username: "用户名", 21 | password: "密码", 22 | usernamePlaceholder: "请输入用户名", 23 | passwordPlaceholder: "请输入密码", 24 | loginBtn: "登录", 25 | }, 26 | 401: { 27 | 401: "401", 28 | }, 29 | 404: { 30 | 404: "404", 31 | }, 32 | ...modules, 33 | }; 34 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import router from "./router/index"; 3 | import store from "./store"; 4 | import ElementPlus from "element-plus"; 5 | import "element-plus/dist/index.css"; 6 | import "./styles/color.scss"; 7 | import 'remixicon/fonts/remixicon.css' 8 | import "../mock"; 9 | import i18n from "./lang/index"; 10 | import App from "./App.vue"; 11 | const app = createApp(App); 12 | app.use(ElementPlus, { size: "default", zIndex: 3000 }); 13 | app.use(router); 14 | app.use(store); 15 | app.use(i18n); 16 | app.mount("#app"); 17 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | //1.引入vue-router 2 | import { createRouter, createWebHashHistory } from "vue-router"; 3 | import Cookies from "js-cookie"; 4 | import store from "../store"; 5 | import { filterAsyncRoutes, sameLevelRoutes } from "../utils/index"; 6 | import i18n from "../lang/index"; 7 | import NProgress from "nprogress"; // progress bar 8 | import "nprogress/nprogress.css"; // progress bar style 9 | // 2. 定义一些路由 10 | // 每个路由都需要映射到一个组件。 11 | export const constantRoutes = [ 12 | { 13 | path: "/login", 14 | name: "login", 15 | meta: { title: "login", icon: "el-icon-menu" }, 16 | component: () => import("~/views/login/index.vue"), 17 | hidden: true, 18 | }, 19 | { 20 | path: "/401", 21 | name: "401", 22 | meta: { title: "401", icon: "el-icon-menu" }, 23 | component: () => import("~/views/401/index.vue"), 24 | hidden: true, 25 | }, 26 | { 27 | path: "/404", 28 | name: "404", 29 | meta: { title: "404", icon: "el-icon-menu" }, 30 | component: () => import("~/views/404/index.vue"), 31 | hidden: true, 32 | }, 33 | ]; 34 | 35 | // 3. 创建路由实例并传递 `routes` 配置 36 | // 你可以在这里输入更多的配置,但我们在这里 37 | // 暂时保持简单 38 | const router = createRouter({ 39 | // 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。 40 | history: createWebHashHistory(), 41 | routes: constantRoutes, 42 | }); 43 | 44 | //导航守卫中不会删除路由的白名单,对应上面constantRoutes(本地默认路由)名称 45 | const WhiteList = ["login", "401", "404"]; 46 | //5.此处可以添加全局路由守卫以方便鉴权,也可以独立鉴权文件然后在main.js引入即可! 47 | //我在这里直接放在路由文件中 48 | 49 | // 前置守卫:路由跳转之前 50 | // to 要进入的路由 51 | // from 从那个路由过来的 52 | router.beforeEach(async (to, form) => { 53 | // start progress bar 54 | NProgress.start(); 55 | // 动态修改网页标题 56 | if (to.meta.title) { 57 | const { t } = i18n.global; 58 | document.title = t(`${to.meta.title}.${to.meta.title}`); 59 | } 60 | //执行登录鉴权,未登录跳转登录页 61 | if (!Cookies.get("token")) { 62 | if (to.path == "/login") { 63 | NProgress.done(); 64 | return; 65 | } else { 66 | NProgress.done(); 67 | return "/login"; 68 | } 69 | } 70 | //已登录,如果地址为login执行跳转至控制台 71 | if (to.path == "/login") { 72 | NProgress.done(); 73 | return "/dashboard"; 74 | } 75 | //是否已同步路由规则 76 | if (!store.state.user.getIsDynamicRoute) { 77 | //获取用户对应菜单权限 78 | const accessRoutes = await store.dispatch("user/getUserRoutes"); 79 | // routes must be a non-empty array 80 | if (!accessRoutes || accessRoutes.length <= 0) { 81 | console.log("routes must be a non-null array!"); 82 | } 83 | let routes = filterAsyncRoutes(accessRoutes); 84 | // 将三级及以上路由数据拍平成二级 85 | routes.map((item) => { 86 | if (item.children) { 87 | item.children = sameLevelRoutes(item.children, [ 88 | { 89 | path: item.path, 90 | title: item.meta.title, 91 | }, 92 | ]); 93 | } 94 | }); 95 | //根据权限添加路由 96 | routes.forEach((item) => { 97 | router.addRoute(item); 98 | }); 99 | } 100 | //添加之前判断要跳转的路由是否存在 101 | let has_route = router.hasRoute(to.name); 102 | //判断是否存在执行重定向避免刷新页面404 103 | if (has_route) { 104 | NProgress.done(); 105 | return; 106 | } else { 107 | //判断最新路由数组中是否含有当前即将跳转页面 108 | if ( 109 | router.getRoutes().findIndex((value) => value.path === to.fullPath) == -1 110 | ) { 111 | NProgress.done(); 112 | //不存在,返回404页面 113 | return "/404"; 114 | } else { 115 | NProgress.done(); 116 | //重定向 117 | return to.fullPath; 118 | } 119 | } 120 | }); 121 | 122 | // 全局解析守卫: 同时在所有组件内守卫和异步路由组件被解析之后 和beforeEach区别是在导航被确认之前 123 | router.beforeResolve((to, form) => {}); 124 | 125 | // 后置守卫:路由跳转之后 126 | router.afterEach((to, form) => { 127 | if (to.meta.tabShow) { 128 | store.dispatch("app/addTabs", { 129 | fullPath: to.fullPath, 130 | name: to.name, 131 | meta: to.meta, 132 | }); 133 | } 134 | store.dispatch("user/activeRoute", to.fullPath); 135 | //恢复原始keepalive 136 | store.dispatch("user/getKeepAlive"); 137 | }); 138 | 139 | export default router; 140 | -------------------------------------------------------------------------------- /src/settings/settings.js: -------------------------------------------------------------------------------- 1 | const settings = { 2 | APP_NAME: "Brisk-Admin", //logo名称 3 | LOGO_GRAM: "Brisk", //logo名称简写 4 | SKIN_CHOOSE: "aside_black_nav_white", //默认皮肤 5 | COLOR_PRIMARY: "#409EFF",//系统主题色 6 | }; 7 | export default settings; 8 | -------------------------------------------------------------------------------- /src/settings/skin.js: -------------------------------------------------------------------------------- 1 | const skin = { 2 | aside_white_nav_white: { 3 | className: "aside_white_nav_white", 4 | asideBackground: "#ffffff", 5 | asideColor: "#000000", 6 | logoBackground: "#ffffff", 7 | logoColor: "#000000", 8 | navBackground: "#ffffff", 9 | navColor: "#000000", 10 | activeColor: "var(--el-color-primary)", 11 | }, 12 | aside_black_nav_white: { 13 | className: "aside_black_nav_white", 14 | asideBackground: "#222d32", 15 | asideColor: "#ffffff", 16 | logoColor: "#ffffff", 17 | logoBackground: "#222d32", 18 | navBackground: "#ffffff", 19 | navColor: "#000000", 20 | activeColor: "var(--el-color-primary)", 21 | }, 22 | aside_white_nav_black: { 23 | className: "aside_white_nav_black", 24 | asideBackground: "#ffffff", 25 | asideColor: "#000000", 26 | logoColor: "#ffffff", 27 | logoBackground: "#222d32", 28 | navBackground: "#222d32", 29 | navColor: "#ffffff", 30 | activeColor: "var(--el-color-primary)", 31 | }, 32 | aside_purple_nav_white: { 33 | className: "aside_purple_nav_white", 34 | asideBackground: "#605ca8", 35 | asideColor: "#ffffff", 36 | logoColor: "#ffffff", 37 | logoBackground: "#605ca8", 38 | navBackground: "#ffffff", 39 | navColor: "#000000", 40 | activeColor: "var(--el-color-primary)", 41 | }, 42 | aside_yellow_nav_white: { 43 | className: "aside_yellow_nav_white", 44 | asideBackground: "#f39c12", 45 | asideColor: "#ffffff", 46 | logoColor: "#ffffff", 47 | logoBackground: "#f39c12", 48 | navBackground: "#ffffff", 49 | navColor: "#000000", 50 | activeColor: "var(--el-color-primary)", 51 | }, 52 | aside_white_nav_yellow: { 53 | className: "aside_white_nav_yellow", 54 | asideBackground: "#ffffff", 55 | asideColor: "#000000", 56 | logoColor: "#ffffff", 57 | logoBackground: "#f39c12", 58 | navBackground: "#f39c12", 59 | navColor: "#ffffff", 60 | activeColor: "var(--el-color-primary)", 61 | }, 62 | }; 63 | export default skin; 64 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | language: state => state.app.language, 3 | device: state => state.app.device, 4 | sidebar: state => state.app.sidebar, 5 | showSet: state => state.app.showSet, 6 | routes: state => state.user.routes, 7 | singleRoutes: state => state.user.singleRoutes, 8 | activeRoute: state => state.user.activeRoute, 9 | skinChoose: state => state.settings.skinChoose, 10 | } 11 | export default getters 12 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import getters from "./getters"; 3 | 4 | const modulesFiles = import.meta.globEager("./modules/*.js"); 5 | const modules = Object.keys(modulesFiles).reduce((modules, modulePath) => { 6 | const moduleName = modulePath.replace(/(.*\/)*([^.]+).*/gi, "$2"); 7 | const value = modulesFiles[modulePath]; 8 | modules[moduleName] = value.default; 9 | return modules; 10 | }, {}); 11 | 12 | const store = createStore({ 13 | modules, 14 | getters, 15 | }); 16 | 17 | export default store; 18 | -------------------------------------------------------------------------------- /src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | import settings from "../../settings/settings"; 3 | import router from "~/router/index.js"; 4 | 5 | const state = { 6 | sidebar: false, 7 | showSet: false, 8 | device: "desktop", 9 | language: Cookies.get("language") 10 | ? Cookies.get("language") 11 | : settings.LANGUAGE, 12 | tabsList: [], 13 | tabActive: "", 14 | }; 15 | 16 | const mutations = { 17 | TOGGLE_DEVICE: (state, device) => { 18 | state.device = device; 19 | }, 20 | TOGGLE_SIDEBAR: (state, sidebar) => { 21 | state.sidebar = sidebar; 22 | }, 23 | SET_LANGUAGE: (state, language) => { 24 | state.language = language; 25 | Cookies.set("language", language); 26 | }, 27 | SET_SHOWSET: (state, showSet) => { 28 | state.showSet = showSet; 29 | }, 30 | SET_TABACTIVE: (state, tabActive) => { 31 | state.tabActive = tabActive; 32 | }, 33 | SET_TABSLIST: (state, tabsList) => { 34 | state.tabsList = tabsList; 35 | }, 36 | }; 37 | 38 | const actions = { 39 | toggleDevice({ commit }, device) { 40 | commit("TOGGLE_DEVICE", device); 41 | }, 42 | toggleSidebar({ commit }, sidebar) { 43 | commit("TOGGLE_SIDEBAR", sidebar); 44 | }, 45 | setLanguage({ commit }, language) { 46 | commit("SET_LANGUAGE", language); 47 | }, 48 | setShowSet({ commit }, showSet) { 49 | commit("SET_SHOWSET", showSet); 50 | }, 51 | //登录重置标签 52 | refTabs({ state, commit }) { 53 | let tabsList = []; 54 | commit("SET_TABACTIVE", ""); 55 | commit("SET_TABSLIST", tabsList); 56 | localStorage.setItem("tabsList", JSON.stringify(tabsList)); 57 | localStorage.setItem("tabActive", ""); 58 | }, 59 | //初始化标签 60 | initTabs({ state, commit }) { 61 | let tabsList = JSON.parse(localStorage.getItem("tabsList")); 62 | let tabActive = localStorage.getItem("tabActive"); 63 | if (tabsList && tabActive) { 64 | commit("SET_TABACTIVE", tabActive); 65 | commit("SET_TABSLIST", tabsList); 66 | } 67 | }, 68 | //添加标签 69 | addTabs({ state, commit }, route) { 70 | let tabsList = state.tabsList; 71 | const isExists = tabsList.some((item) => item.fullPath == route.fullPath); 72 | if (!isExists) { 73 | tabsList.push(route); 74 | } 75 | commit("SET_TABACTIVE", route.fullPath); 76 | commit("SET_TABSLIST", tabsList); 77 | localStorage.setItem("tabsList", JSON.stringify(tabsList)); 78 | localStorage.setItem("tabActive", route.fullPath); 79 | }, 80 | //点击标签切换选中 81 | clickTab({ state, commit }, index) { 82 | let tab = state.tabsList[index]; 83 | commit("SET_TABACTIVE", tab.fullPath); 84 | localStorage.setItem("tabActive", tab.fullPath); 85 | router.push({ path: tab.fullPath }); 86 | }, 87 | // 关闭其他标签 88 | closeOtherTabs({ state, commit }, route) { 89 | let tabsList = state.tabsList; 90 | tabsList = tabsList.filter((item) => item.fullPath == route.fullPath); 91 | commit("SET_TABSLIST", tabsList); 92 | localStorage.setItem("tabsList", JSON.stringify(tabsList)); 93 | }, 94 | // 关闭当前页 95 | closeCurrentTab({ state, commit }, obj) { 96 | let tabsList = state.tabsList; 97 | const index = tabsList.findIndex((item) => item.fullPath == obj.fullPath); 98 | tabsList.splice(index, 1); 99 | commit("SET_TABSLIST", tabsList); 100 | localStorage.setItem("tabsList", JSON.stringify(tabsList)); 101 | if (obj.type == "current") { 102 | //打开最后一个tab页面 103 | commit("SET_TABACTIVE", tabsList[tabsList.length - 1].fullPath); 104 | localStorage.setItem("tabActive", tabsList[tabsList.length - 1].fullPath); 105 | router.push({ path: tabsList[tabsList.length - 1].fullPath }); 106 | } 107 | }, 108 | }; 109 | 110 | export default { 111 | namespaced: true, 112 | state, 113 | mutations, 114 | actions, 115 | }; 116 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | import skin from "../../settings/skin"; 3 | import settings from "../../settings/settings"; 4 | const state = { 5 | appName: settings.APP_NAME, //logo名称 6 | logogram: settings.LOGO_GRAM, //logo名称简写 7 | skinChoose: localStorage.getItem("skinChoose") 8 | ? skin[localStorage.getItem("skinChoose")] 9 | : skin[settings.SKIN_CHOOSE], 10 | colorPrimary: localStorage.getItem("colorPrimary") 11 | ? localStorage.getItem("colorPrimary") 12 | : settings.COLOR_PRIMARY, 13 | // tagsView: true, //是否需要标签栏 14 | }; 15 | 16 | const mutations = { 17 | CHANGE_SETTING: (state, { key, value }) => { 18 | if (state.hasOwnProperty(key)) { 19 | state[key] = value; 20 | if (key === "skinChoose") { 21 | localStorage.setItem("skinChoose", value.className); 22 | } 23 | } 24 | }, 25 | COLOR_PRIMARY: (state, color) => { 26 | state.color = color; 27 | }, 28 | }; 29 | 30 | const actions = { 31 | changeSetting({ commit }, data) { 32 | commit("CHANGE_SETTING", data); 33 | }, 34 | setColorPrimary({ commit }, color) { 35 | localStorage.setItem("colorPrimary", color); 36 | commit("COLOR_PRIMARY", color); 37 | }, 38 | }; 39 | 40 | export default { 41 | namespaced: true, 42 | state, 43 | mutations, 44 | actions, 45 | }; 46 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | import { authRoutes } from "~/api"; 3 | import { singleAsyncRoutes } from "../../utils/index"; 4 | 5 | const state = { 6 | token: Cookies.get("token"), 7 | userinfo: Cookies.get("userinfo") ? JSON.parse(Cookies.get("userinfo")) : {}, 8 | routes: [], 9 | singleRoutes: [], 10 | activeRoute: Cookies.get("activeRoute") ? Cookies.get("activeRoute") : "/", 11 | keepAliveRoutes: [], 12 | getIsDynamicRoute: false, 13 | }; 14 | 15 | const mutations = { 16 | SET_TOKEN: (state, token) => { 17 | state.token = token; 18 | }, 19 | SET_USERINFO: (state, userinfo) => { 20 | state.userinfo = userinfo; 21 | }, 22 | SET_ROUTES: (state, routes) => { 23 | state.routes = routes; 24 | }, 25 | SET_SINGLEROUTES: (state, routes) => { 26 | state.singleRoutes = routes; 27 | }, 28 | SET_KEEPALIVEROUTES: (state, routes) => { 29 | state.keepAliveRoutes = routes; 30 | }, 31 | SET_ACTIVEROUTE: (state, activeRoute) => { 32 | state.activeRoute = activeRoute; 33 | }, 34 | SET_DYNAMICROUTE: (state, status) => { 35 | state.getIsDynamicRoute = status; 36 | }, 37 | }; 38 | 39 | const actions = { 40 | loginSet({ commit }, userinfo) { 41 | commit("SET_TOKEN", userinfo.token); 42 | commit("SET_USERINFO", userinfo); 43 | Cookies.set("token", userinfo.token); 44 | Cookies.set("userinfo", JSON.stringify(userinfo)); 45 | }, 46 | loginOutSet({ commit }) { 47 | commit("SET_TOKEN", null); 48 | commit("SET_USERINFO", null); 49 | Cookies.remove("token"); 50 | }, 51 | activeRoute({ commit }, path) { 52 | commit("SET_ACTIVEROUTE", path); 53 | Cookies.set("activeRoute", path); 54 | }, 55 | getUserRoutes({ state, commit, dispatch }) { 56 | let userinfo = state.userinfo; 57 | return new Promise((resolve, reject) => { 58 | authRoutes({ 59 | group: userinfo.group, 60 | }) 61 | .then((res) => { 62 | let routes = res.data; 63 | commit("SET_ROUTES", routes); 64 | let single = singleAsyncRoutes(routes); 65 | commit("SET_SINGLEROUTES", single); 66 | commit("SET_DYNAMICROUTE", true); 67 | dispatch("getKeepAlive"); 68 | resolve(routes); 69 | }) 70 | .catch((err) => { 71 | reject(false); 72 | }); 73 | }); 74 | }, 75 | getKeepAlive({ state, commit }) { 76 | let keepAliveRoutes = []; 77 | state.singleRoutes.map((item) => { 78 | if (item.meta.keepAlive) { 79 | keepAliveRoutes.push(item.name); 80 | } 81 | }); 82 | commit("SET_KEEPALIVEROUTES", keepAliveRoutes); 83 | }, 84 | setKeepAlive({ commit }, keepAliveRoutes) { 85 | commit("SET_KEEPALIVEROUTES", keepAliveRoutes); 86 | }, 87 | setDynamicRoute({ commit }, status) { 88 | commit("SET_DYNAMICROUTE", status); 89 | }, 90 | }; 91 | 92 | export default { 93 | namespaced: true, 94 | state, 95 | mutations, 96 | actions, 97 | }; 98 | -------------------------------------------------------------------------------- /src/styles/color.scss: -------------------------------------------------------------------------------- 1 | /* 此文件要在element主样式文件之后引入 */ 2 | /* 用了这种方法后,你不需要重写任何elementplus组件的颜色 */ 3 | 4 | :root { 5 | // 这里可以设置你自定义的颜色变量 6 | // 这个是element主要按钮:active的颜色,当主题更改后此变量的值也随之更改 7 | --el-color-primary-dark: #0d84ff; 8 | } 9 | 10 | /* 核心组件的变量,下面这些样式是必须要写的 */ 11 | 12 | .el-link.el-link--primary:hover { 13 | color: var(--el-color-primary-light-2) !important; 14 | } 15 | 16 | .el-tag { 17 | --el-tag-bg-color: var(--el-color-primary-light-9); 18 | --el-tag-border-color: var(--el-color-primary-light-8); 19 | --el-tag-text-color: var(--el-color-primary); 20 | --el-tag-hover-color: var(--el-color-primary); 21 | } 22 | 23 | .el-button--default:active { 24 | color: var(--el-color-primary-dark) !important; 25 | border-color: var(--el-color-primary-dark) !important; 26 | } 27 | 28 | .el-button--primary { 29 | --el-button-bg-color: var(--el-color-primary) !important; 30 | --el-button-border-color: var(--el-color-primary) !important; 31 | --el-button-hover-bg-color: var(--el-color-primary-light-2) !important; 32 | --el-button-hover-border-color: var(--el-color-primary-light-2) !important; 33 | --el-button-active-bg-color: var(--el-color-primary-dark) !important; 34 | --el-button-active-border-color: var(--el-color-primary-dark) !important; 35 | } 36 | 37 | // 你也可以降nProgress的颜色改成element主色调 38 | #nprogress { 39 | & .bar { 40 | background-color: var(--el-color-primary) !important; 41 | } 42 | & .peg { 43 | box-shadow: 0 0 10px var(--el-color-primary), 0 0 5px var(--el-color-primary) !important; 44 | } 45 | & .spinner-icon { 46 | border-top-color: var(--el-color-primary); 47 | border-left-color: var(--el-color-primary); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/styles/element/index.scss: -------------------------------------------------------------------------------- 1 | //在这里添加覆盖主题色的scss 2 | -------------------------------------------------------------------------------- /src/utils/http/axios.js: -------------------------------------------------------------------------------- 1 | import instance from "./index" 2 | /** 3 | * @param {String} method 请求的方法:get、post、delete、put 4 | * @param {String} url 请求的url: 5 | * @param {Object} data 请求的参数 6 | * @param {Object} config 请求的配置 7 | * @returns {Promise} 返回一个promise对象,其实就相当于axios请求数据的返回值 8 | */ 9 | 10 | const axios = ({ 11 | method, 12 | url, 13 | data, 14 | config 15 | }) => { 16 | method = method.toLowerCase(); 17 | if (method == 'post') { 18 | return instance.post(url, data, {...config}) 19 | } else if (method == 'get') { 20 | return instance.get(url, { 21 | params: data, 22 | ...config 23 | }) 24 | } else if (method == 'delete') { 25 | return instance.delete(url, { 26 | params: data, 27 | ...config 28 | }, ) 29 | } else if (method == 'put') { 30 | return instance.put(url, data,{...config}) 31 | } else { 32 | console.error('未知的method' + method) 33 | return false 34 | } 35 | } 36 | export default axios; -------------------------------------------------------------------------------- /src/utils/http/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { 3 | ElLoading, 4 | ElMessage 5 | } from 'element-plus'; 6 | //创建axios的一个实例 7 | var instance = axios.create({ 8 | baseURL: import.meta.env.VITE_APP_URL, //接口统一域名 9 | timeout: 6000, //设置超时 10 | headers: { 11 | 'Content-Type': 'application/json;charset=UTF-8;', 12 | } 13 | }) 14 | let loading; 15 | //正在请求的数量 16 | let requestCount = 0 17 | //显示loading 18 | const showLoading = () => { 19 | if (requestCount === 0 && !loading) { 20 | loading = ElLoading.service({ 21 | text: "Loading ", 22 | background: 'rgba(0, 0, 0, 0.7)', 23 | spinner: 'el-icon-loading', 24 | }) 25 | } 26 | requestCount++; 27 | } 28 | //隐藏loading 29 | const hideLoading = () => { 30 | requestCount-- 31 | if (requestCount == 0) { 32 | loading.close() 33 | } 34 | } 35 | 36 | //请求拦截器 37 | instance.interceptors.request.use((config) => { 38 | showLoading() 39 | // 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了 40 | const token = window.localStorage.getItem('token'); 41 | token && (config.headers.Authorization = token) 42 | //若请求方式为post,则将data参数转为JSON字符串 43 | if (config.method === 'POST') { 44 | config.data = JSON.stringify(config.data); 45 | } 46 | return config; 47 | }, (error) => 48 | // 对请求错误做些什么 49 | Promise.reject(error)); 50 | 51 | //响应拦截器 52 | instance.interceptors.response.use((response) => { 53 | hideLoading() 54 | //响应成功 55 | return response.data; 56 | }, (error) => { 57 | console.log(error) 58 | //响应错误 59 | if (error.response && error.response.status) { 60 | const status = error.response.status 61 | switch (status) { 62 | case 401: 63 | message = '没有权限'; 64 | break; 65 | case 404: 66 | message = '请求地址出错'; 67 | break; 68 | case 408: 69 | message = '请求超时'; 70 | break; 71 | case 500: 72 | message = '服务器内部错误!'; 73 | break; 74 | default: 75 | message = '请求失败' 76 | } 77 | ElMessage.error(message); 78 | return Promise.reject(error); 79 | } 80 | return Promise.reject(error); 81 | }); 82 | 83 | 84 | export default instance; -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { h } from "vue"; 2 | import { isExternal } from "./validate"; 3 | //先进行views下所有vue文件动态导入声明,以便后台动态返回路由进行本地动态导入,目前这个只支持一层文件夹,如需多层请追加 /* 4 | const modules = import.meta.glob("../views/*/*.vue"); 5 | /** 6 | * 转化路由component实例化本地文件 7 | * @param routes asyncRoutes 8 | */ 9 | export function filterAsyncRoutes(routes) { 10 | const res = []; 11 | routes.forEach((route) => { 12 | let tmp = { ...route }; 13 | if (tmp.children) { 14 | tmp.children = filterAsyncRoutes(tmp.children); 15 | } 16 | if (tmp.component == "noComponent") { 17 | tmp.component = modules[`../views/layout/nestedComponent.vue`]; 18 | } else { 19 | tmp.component = modules[`../views/${route.component}`]; 20 | } 21 | res.push(tmp); 22 | }); 23 | return res; 24 | } 25 | 26 | /** 27 | * 将多层嵌套路由处理成平级 28 | * @param routes asyncRoutes 29 | */ 30 | export function sameLevelRoutes(routes, breadcrumb, baseUrl = "") { 31 | let res = []; 32 | routes.forEach((route) => { 33 | const tmp = { ...route }; 34 | if (tmp.children) { 35 | let childrenBaseUrl = ""; 36 | if (baseUrl == "") { 37 | childrenBaseUrl = tmp.path; 38 | } else if (tmp.path != "") { 39 | childrenBaseUrl = `${baseUrl}/${tmp.path}`; 40 | } 41 | let childrenBreadcrumb = deepClone(breadcrumb); 42 | if (route.meta.breadcrumb !== false) { 43 | childrenBreadcrumb.push({ 44 | path: childrenBaseUrl, 45 | title: route.meta.title, 46 | }); 47 | } 48 | let tmpRoute = deepClone(route); 49 | tmpRoute.path = childrenBaseUrl; 50 | tmpRoute.meta.breadcrumbNeste = childrenBreadcrumb; 51 | delete tmpRoute.children; 52 | res.push(tmpRoute); 53 | let childrenRoutes = sameLevelRoutes( 54 | tmp.children, 55 | childrenBreadcrumb, 56 | childrenBaseUrl 57 | ); 58 | childrenRoutes.map((item) => { 59 | // 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复 60 | if (res.some((v) => v.path == item.path)) { 61 | res.forEach((v, i) => { 62 | if (v.path == item.path) { 63 | res[i] = item; 64 | } 65 | }); 66 | } else { 67 | res.push(item); 68 | } 69 | }); 70 | } else { 71 | if (baseUrl != "") { 72 | if (tmp.path != "") { 73 | tmp.path = `${baseUrl}/${tmp.path}`; 74 | } else { 75 | tmp.path = baseUrl; 76 | } 77 | } 78 | // 处理面包屑导航 79 | let tmpBreadcrumb = deepClone(breadcrumb); 80 | if (tmp.meta.breadcrumb !== false) { 81 | tmpBreadcrumb.push({ 82 | path: tmp.path, 83 | title: tmp.meta.title, 84 | }); 85 | } 86 | tmp.meta.breadcrumbNeste = tmpBreadcrumb; 87 | res.push(tmp); 88 | } 89 | }); 90 | return res; 91 | } 92 | 93 | /** 94 | * 多维路由转化一维 95 | * @param routes asyncRoutes 96 | */ 97 | export function singleAsyncRoutes(routes) { 98 | var res = []; 99 | routes.forEach((route) => { 100 | let tmp = { 101 | ...route, 102 | }; 103 | res.push(tmp); 104 | let path_text = tmp.path; 105 | if (tmp.children) { 106 | let arr = singleAsyncRoutes(tmp.children); 107 | arr.forEach((child) => { 108 | //判断是否是外部链接 109 | if (!isExternal(child.path)) { 110 | //判断path最后一位是否为/ 111 | if (path_text.substr(path_text.length - 1, 1) != "/") { 112 | child.path = path_text + "/" + child.path; 113 | } else { 114 | child.path = path_text + child.path; 115 | } 116 | } 117 | }); 118 | res = res.concat(arr); 119 | } 120 | }); 121 | return res; 122 | } 123 | /** 124 | * 深拷贝 125 | */ 126 | export function deepClone(target) { 127 | // 定义一个变量 128 | let result; 129 | // 如果当前需要深拷贝的是一个对象的话 130 | if (typeof target === "object") { 131 | // 如果是一个数组的话 132 | if (Array.isArray(target)) { 133 | result = []; // 将result赋值为一个数组,并且执行遍历 134 | for (let i in target) { 135 | // 递归克隆数组中的每一项 136 | result.push(deepClone(target[i])); 137 | } 138 | // 判断如果当前的值是null的话;直接赋值为null 139 | } else if (target === null) { 140 | result = null; 141 | // 判断如果当前的值是一个RegExp对象的话,直接赋值 142 | } else if (target.constructor === RegExp) { 143 | result = target; 144 | } else { 145 | // 否则是普通对象,直接for in循环,递归赋值对象的所有值 146 | result = {}; 147 | for (let i in target) { 148 | result[i] = deepClone(target[i]); 149 | } 150 | } 151 | // 如果不是对象的话,就是基本数据类型,那么直接赋值 152 | } else { 153 | result = target; 154 | } 155 | // 返回最终结果 156 | return result; 157 | } 158 | //用户修复element-plus主题色变更某些组件不改变问题 159 | export const mix = (color1, color2, weight) => { 160 | weight = Math.max(Math.min(Number(weight), 1), 0); 161 | let r1 = parseInt(color1.substring(1, 3), 16); 162 | let g1 = parseInt(color1.substring(3, 5), 16); 163 | let b1 = parseInt(color1.substring(5, 7), 16); 164 | let r2 = parseInt(color2.substring(1, 3), 16); 165 | let g2 = parseInt(color2.substring(3, 5), 16); 166 | let b2 = parseInt(color2.substring(5, 7), 16); 167 | let r = Math.round(r1 * (1 - weight) + r2 * weight); 168 | let g = Math.round(g1 * (1 - weight) + g2 * weight); 169 | let b = Math.round(b1 * (1 - weight) + b2 * weight); 170 | r = ("0" + (r || 0).toString(16)).slice(-2); 171 | g = ("0" + (g || 0).toString(16)).slice(-2); 172 | b = ("0" + (b || 0).toString(16)).slice(-2); 173 | return "#" + r + g + b; 174 | } -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @returns {Boolean} 4 | */ 5 | export function isExternal(path) { 6 | return /^(https?:|mailto:|tel:)/.test(path) 7 | } 8 | 9 | /** 10 | * @param {string} str 11 | * @returns {Boolean} 12 | */ 13 | export function validUsername(str) { 14 | const valid_map = ['admin', 'editor'] 15 | return valid_map.indexOf(str.trim()) >= 0 16 | } 17 | 18 | /** 19 | * @param {string} url 20 | * @returns {Boolean} 21 | */ 22 | export function validURL(url) { 23 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 24 | return reg.test(url) 25 | } 26 | 27 | /** 28 | * @param {string} str 29 | * @returns {Boolean} 30 | */ 31 | export function validLowerCase(str) { 32 | const reg = /^[a-z]+$/ 33 | return reg.test(str) 34 | } 35 | 36 | /** 37 | * @param {string} str 38 | * @returns {Boolean} 39 | */ 40 | export function validUpperCase(str) { 41 | const reg = /^[A-Z]+$/ 42 | return reg.test(str) 43 | } 44 | 45 | /** 46 | * @param {string} str 47 | * @returns {Boolean} 48 | */ 49 | export function validAlphabets(str) { 50 | const reg = /^[A-Za-z]+$/ 51 | return reg.test(str) 52 | } 53 | 54 | /** 55 | * @param {string} email 56 | * @returns {Boolean} 57 | */ 58 | export function validEmail(email) { 59 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 60 | return reg.test(email) 61 | } 62 | 63 | /** 64 | * @param {string} str 65 | * @returns {Boolean} 66 | */ 67 | export function isString(str) { 68 | if (typeof str === 'string' || str instanceof String) { 69 | return true 70 | } 71 | return false 72 | } 73 | 74 | /** 75 | * @param {Array} arg 76 | * @returns {Boolean} 77 | */ 78 | export function isArray(arg) { 79 | if (typeof Array.isArray === 'undefined') { 80 | return Object.prototype.toString.call(arg) === '[object Array]' 81 | } 82 | return Array.isArray(arg) 83 | } 84 | -------------------------------------------------------------------------------- /src/vendor/Export2Excel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { saveAs } from 'file-saver' 3 | import XLSX from 'xlsx' 4 | 5 | function generateArray(table) { 6 | var out = []; 7 | var rows = table.querySelectorAll('tr'); 8 | var ranges = []; 9 | for (var R = 0; R < rows.length; ++R) { 10 | var outRow = []; 11 | var row = rows[R]; 12 | var columns = row.querySelectorAll('td'); 13 | for (var C = 0; C < columns.length; ++C) { 14 | var cell = columns[C]; 15 | var colspan = cell.getAttribute('colspan'); 16 | var rowspan = cell.getAttribute('rowspan'); 17 | var cellValue = cell.innerText; 18 | if (cellValue !== "" && cellValue == +cellValue) cellValue = +cellValue; 19 | 20 | //Skip ranges 21 | ranges.forEach(function (range) { 22 | if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) { 23 | for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null); 24 | } 25 | }); 26 | 27 | //Handle Row Span 28 | if (rowspan || colspan) { 29 | rowspan = rowspan || 1; 30 | colspan = colspan || 1; 31 | ranges.push({ 32 | s: { 33 | r: R, 34 | c: outRow.length 35 | }, 36 | e: { 37 | r: R + rowspan - 1, 38 | c: outRow.length + colspan - 1 39 | } 40 | }); 41 | }; 42 | 43 | //Handle Value 44 | outRow.push(cellValue !== "" ? cellValue : null); 45 | 46 | //Handle Colspan 47 | if (colspan) 48 | for (var k = 0; k < colspan - 1; ++k) outRow.push(null); 49 | } 50 | out.push(outRow); 51 | } 52 | return [out, ranges]; 53 | }; 54 | 55 | function datenum(v, date1904) { 56 | if (date1904) v += 1462; 57 | var epoch = Date.parse(v); 58 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); 59 | } 60 | 61 | function sheet_from_array_of_arrays(data, opts) { 62 | var ws = {}; 63 | var range = { 64 | s: { 65 | c: 10000000, 66 | r: 10000000 67 | }, 68 | e: { 69 | c: 0, 70 | r: 0 71 | } 72 | }; 73 | for (var R = 0; R != data.length; ++R) { 74 | for (var C = 0; C != data[R].length; ++C) { 75 | if (range.s.r > R) range.s.r = R; 76 | if (range.s.c > C) range.s.c = C; 77 | if (range.e.r < R) range.e.r = R; 78 | if (range.e.c < C) range.e.c = C; 79 | var cell = { 80 | v: data[R][C] 81 | }; 82 | if (cell.v == null) continue; 83 | var cell_ref = XLSX.utils.encode_cell({ 84 | c: C, 85 | r: R 86 | }); 87 | 88 | if (typeof cell.v === 'number') cell.t = 'n'; 89 | else if (typeof cell.v === 'boolean') cell.t = 'b'; 90 | else if (cell.v instanceof Date) { 91 | cell.t = 'n'; 92 | cell.z = XLSX.SSF._table[14]; 93 | cell.v = datenum(cell.v); 94 | } else cell.t = 's'; 95 | 96 | ws[cell_ref] = cell; 97 | } 98 | } 99 | if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range); 100 | return ws; 101 | } 102 | 103 | function Workbook() { 104 | if (!(this instanceof Workbook)) return new Workbook(); 105 | this.SheetNames = []; 106 | this.Sheets = {}; 107 | } 108 | 109 | function s2ab(s) { 110 | var buf = new ArrayBuffer(s.length); 111 | var view = new Uint8Array(buf); 112 | for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; 113 | return buf; 114 | } 115 | 116 | export function export_table_to_excel(id) { 117 | var theTable = document.getElementById(id); 118 | var oo = generateArray(theTable); 119 | var ranges = oo[1]; 120 | 121 | /* original data */ 122 | var data = oo[0]; 123 | var ws_name = "SheetJS"; 124 | 125 | var wb = new Workbook(), 126 | ws = sheet_from_array_of_arrays(data); 127 | 128 | /* add ranges to worksheet */ 129 | // ws['!cols'] = ['apple', 'banan']; 130 | ws['!merges'] = ranges; 131 | 132 | /* add worksheet to workbook */ 133 | wb.SheetNames.push(ws_name); 134 | wb.Sheets[ws_name] = ws; 135 | 136 | var wbout = XLSX.write(wb, { 137 | bookType: 'xlsx', 138 | bookSST: false, 139 | type: 'binary' 140 | }); 141 | 142 | saveAs(new Blob([s2ab(wbout)], { 143 | type: "application/octet-stream" 144 | }), "test.xlsx") 145 | } 146 | 147 | export function export_json_to_excel({ 148 | multiHeader = [], 149 | header, 150 | data, 151 | filename, 152 | merges = [], 153 | autoWidth = true, 154 | bookType = 'xlsx' 155 | } = {}) { 156 | /* original data */ 157 | filename = filename || 'excel-list' 158 | data = [...data] 159 | data.unshift(header); 160 | 161 | for (let i = multiHeader.length - 1; i > -1; i--) { 162 | data.unshift(multiHeader[i]) 163 | } 164 | 165 | var ws_name = "SheetJS"; 166 | var wb = new Workbook(), 167 | ws = sheet_from_array_of_arrays(data); 168 | 169 | if (merges.length > 0) { 170 | if (!ws['!merges']) ws['!merges'] = []; 171 | merges.forEach(item => { 172 | ws['!merges'].push(XLSX.utils.decode_range(item)) 173 | }) 174 | } 175 | 176 | if (autoWidth) { 177 | /*设置worksheet每列的最大宽度*/ 178 | const colWidth = data.map(row => row.map(val => { 179 | /*先判断是否为null/undefined*/ 180 | if (val == null) { 181 | return { 182 | 'wch': 10 183 | }; 184 | } 185 | /*再判断是否为中文*/ 186 | else if (val.toString().charCodeAt(0) > 255) { 187 | return { 188 | 'wch': val.toString().length * 2 189 | }; 190 | } else { 191 | return { 192 | 'wch': val.toString().length 193 | }; 194 | } 195 | })) 196 | /*以第一行为初始值*/ 197 | let result = colWidth[0]; 198 | for (let i = 1; i < colWidth.length; i++) { 199 | for (let j = 0; j < colWidth[i].length; j++) { 200 | if (result[j]['wch'] < colWidth[i][j]['wch']) { 201 | result[j]['wch'] = colWidth[i][j]['wch']; 202 | } 203 | } 204 | } 205 | ws['!cols'] = result; 206 | } 207 | 208 | /* add worksheet to workbook */ 209 | wb.SheetNames.push(ws_name); 210 | wb.Sheets[ws_name] = ws; 211 | 212 | var wbout = XLSX.write(wb, { 213 | bookType: bookType, 214 | bookSST: false, 215 | type: 'binary' 216 | }); 217 | saveAs(new Blob([s2ab(wbout)], { 218 | type: "application/octet-stream" 219 | }), `${filename}.${bookType}`); 220 | } 221 | -------------------------------------------------------------------------------- /src/views/401/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /src/views/404/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /src/views/admin/index.vue: -------------------------------------------------------------------------------- 1 | 133 | 134 | 265 | 266 | 268 | -------------------------------------------------------------------------------- /src/views/adminGroup/index.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 191 | 192 | 195 | -------------------------------------------------------------------------------- /src/views/adminLog/index.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 243 | 244 | 247 | -------------------------------------------------------------------------------- /src/views/adminRule/index.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 215 | 216 | 219 | -------------------------------------------------------------------------------- /src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 218 | 219 | 241 | -------------------------------------------------------------------------------- /src/views/layout/components/aside/index.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 49 | 60 | -------------------------------------------------------------------------------- /src/views/layout/components/aside/menu.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/layout/components/aside/menuItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/layout/components/header/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 109 | 110 | -------------------------------------------------------------------------------- /src/views/layout/components/setting/index.vue: -------------------------------------------------------------------------------- 1 | 118 | 169 | 170 | -------------------------------------------------------------------------------- /src/views/layout/components/tabs/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 92 | 93 | -------------------------------------------------------------------------------- /src/views/layout/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 91 | 92 | -------------------------------------------------------------------------------- /src/views/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '~/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | 10 | } 11 | }, 12 | beforeMount() { 13 | window.addEventListener('resize', this.$_resizeHandler) 14 | }, 15 | beforeUnmount() { 16 | window.removeEventListener('resize', this.$_resizeHandler) 17 | }, 18 | mounted() { 19 | const isMobile = this.$_isMobile() 20 | if (isMobile) { 21 | store.dispatch('app/toggleDevice', 'mobile') 22 | store.dispatch('app/toggleSidebar', false) 23 | } else { 24 | store.dispatch('app/toggleDevice', 'desktop') 25 | store.dispatch('app/toggleSidebar', false) 26 | } 27 | }, 28 | methods: { 29 | // use $_ for mixins properties 30 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 31 | $_isMobile() { 32 | const rect = body.getBoundingClientRect() 33 | return rect.width - 1 < WIDTH 34 | }, 35 | $_resizeHandler() { 36 | if (!document.hidden) { 37 | const isMobile = this.$_isMobile() 38 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 39 | 40 | if (isMobile) { 41 | store.dispatch('app/toggleSidebar', false) 42 | } else { 43 | store.dispatch('app/toggleSidebar', false) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/views/layout/nestedComponent.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 74 | 75 | -------------------------------------------------------------------------------- /src/views/menu1/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/menu2/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/views/menu3/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/menu4/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/profile/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import ElementPlus from "unplugin-element-plus/vite"; 4 | const { resolve } = require("path"); 5 | /** 6 | * @type {import('vite').UserConfig} 7 | */ 8 | export default defineConfig(({ command, mode }) => { 9 | let vueI18n = {}; 10 | if (command === "serve") { 11 | vueI18n["vue-i18n"] = "vue-i18n/dist/vue-i18n.cjs.js"; //解决dev运行警告You are running the esm-bundler build of vue-i18n. 12 | } 13 | return { 14 | plugins: [ 15 | vue(), 16 | ElementPlus({ 17 | useSource: true, 18 | }), 19 | ], 20 | resolve: { 21 | alias: { 22 | "~/": `${resolve(__dirname, "src")}/`, 23 | ...vueI18n, 24 | }, 25 | }, 26 | // base: "./", 27 | server: { 28 | host: "127.0.0.1", 29 | port: 8086, 30 | open: true, 31 | // 反向代理 32 | proxy: { 33 | "/api": { 34 | target: "http://127.0.0.1:80/", 35 | changeOrigin: true, 36 | rewrite: (path) => path.replace(/^\/api/, ""), 37 | }, 38 | }, 39 | }, 40 | css: { 41 | preprocessorOptions: { 42 | scss: { 43 | additionalData: `@use "~/styles/element/index.scss" as *;`, 44 | }, 45 | }, 46 | }, 47 | }; 48 | }); 49 | --------------------------------------------------------------------------------