├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.en.md ├── README.md ├── index.html ├── mock └── app-data.js ├── package.json ├── postcss.config.js ├── public └── favicon.ico ├── src ├── assets │ ├── error.gif │ ├── loading.gif │ ├── logo.png │ ├── nothing.png │ ├── react-logo.jpg │ ├── snow.gif │ └── styles │ │ ├── default.less │ │ └── global.less ├── components │ ├── Bread │ │ ├── index.less │ │ └── index.tsx │ ├── CanvasBack │ │ ├── index.less │ │ └── index.tsx │ ├── ErrorBoundary │ │ ├── index.less │ │ └── index.tsx │ ├── Footer │ │ ├── index.less │ │ └── index.tsx │ ├── Header │ │ ├── index.less │ │ └── index.tsx │ ├── Icon │ │ └── index.tsx │ ├── Loading │ │ ├── index.less │ │ └── index.tsx │ ├── Menu │ │ ├── index.less │ │ └── index.tsx │ └── TreeChose │ │ ├── PowerTreeTable.tsx │ │ └── RoleTree.tsx ├── config │ └── index.ts ├── layouts │ ├── BasicLayout.less │ ├── BasicLayout.tsx │ ├── UserLayout.less │ └── UserLayout.tsx ├── main.tsx ├── models │ ├── app.ts │ ├── index.type.ts │ └── sys.ts ├── pages │ ├── ErrorPages │ │ ├── 401.tsx │ │ ├── 404.tsx │ │ └── index.less │ ├── Home │ │ ├── index.less │ │ └── index.tsx │ ├── Login │ │ ├── index.less │ │ └── index.tsx │ └── System │ │ ├── MenuAdmin │ │ ├── index.less │ │ ├── index.tsx │ │ └── index.type.ts │ │ ├── PowerAdmin │ │ ├── index.less │ │ ├── index.tsx │ │ └── index.type.ts │ │ ├── RoleAdmin │ │ ├── index.less │ │ ├── index.tsx │ │ └── index.type.ts │ │ └── UserAdmin │ │ ├── index.less │ │ ├── index.tsx │ │ └── index.type.ts ├── router │ ├── AuthProvider.tsx │ └── index.tsx ├── store │ └── index.ts ├── util │ ├── axios.ts │ ├── json.ts │ └── tools.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | scripts 4 | src/assets 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | ], 13 | overrides: [], 14 | parser: "@typescript-eslint/parser", 15 | parserOptions: { 16 | ecmaVersion: "latest", 17 | sourceType: "module", 18 | project: ["tsconfig.json", "vite.config.ts"], 19 | tsconfigRootDir: __dirname, 20 | }, 21 | plugins: ["react", "@typescript-eslint", "react-hooks"], 22 | settings: { 23 | react: { 24 | version: "detect", 25 | }, 26 | }, 27 | rules: { 28 | "prefer-const": "warn", 29 | "no-prototype-builtins": "off", 30 | "no-empty": "warn", 31 | "@typescript-eslint/no-explicit-any": "off", 32 | "@typescript-eslint/ban-ts-comment": "warn", 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | pnpm-lock.yaml 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 木兰宽松许可证, 第2版 2 | 3 | 木兰宽松许可证, 第2版 4 | 2020年1月 http://license.coscl.org.cn/MulanPSL2 5 | 6 | 7 | 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: 8 | 9 | 0. 定义 10 | 11 | “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 12 | 13 | “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 14 | 15 | “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 16 | 17 | “法人实体”是指提交贡献的机构及其“关联实体”。 18 | 19 | “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 20 | 21 | 1. 授予版权许可 22 | 23 | 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 24 | 25 | 2. 授予专利许可 26 | 27 | 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 28 | 29 | 3. 无商标许可 30 | 31 | “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 32 | 33 | 4. 分发限制 34 | 35 | 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 36 | 37 | 5. 免责声明与责任限制 38 | 39 | “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 40 | 41 | 6. 语言 42 | “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 43 | 44 | 条款结束 45 | 46 | 如何将木兰宽松许可证,第2版,应用到您的软件 47 | 48 | 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: 49 | 50 | 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; 51 | 52 | 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; 53 | 54 | 3, 请将如下声明文本放入每个源文件的头部注释中。 55 | 56 | Copyright (c) [Year] [name of copyright holder] 57 | [Software Name] is licensed under Mulan PSL v2. 58 | You can use this software according to the terms and conditions of the Mulan PSL v2. 59 | You may obtain a copy of Mulan PSL v2 at: 60 | http://license.coscl.org.cn/MulanPSL2 61 | THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. 62 | See the Mulan PSL v2 for more details. 63 | 64 | 65 | Mulan Permissive Software License,Version 2 66 | 67 | Mulan Permissive Software License,Version 2 (Mulan PSL v2) 68 | January 2020 http://license.coscl.org.cn/MulanPSL2 69 | 70 | Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: 71 | 72 | 0. Definition 73 | 74 | Software means the program and related documents which are licensed under this License and comprise all Contribution(s). 75 | 76 | Contribution means the copyrightable work licensed by a particular Contributor under this License. 77 | 78 | Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. 79 | 80 | Legal Entity means the entity making a Contribution and all its Affiliates. 81 | 82 | Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. 83 | 84 | 1. Grant of Copyright License 85 | 86 | Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. 87 | 88 | 2. Grant of Patent License 89 | 90 | Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. 91 | 92 | 3. No Trademark License 93 | 94 | No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. 95 | 96 | 4. Distribution Restriction 97 | 98 | You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. 99 | 100 | 5. Disclaimer of Warranty and Limitation of Liability 101 | 102 | THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 103 | 104 | 6. Language 105 | 106 | THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. 107 | 108 | END OF THE TERMS AND CONDITIONS 109 | 110 | How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software 111 | 112 | To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: 113 | 114 | i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; 115 | 116 | ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; 117 | 118 | iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. 119 | 120 | 121 | Copyright (c) [Year] [name of copyright holder] 122 | [Software Name] is licensed under Mulan PSL v2. 123 | You can use this software according to the terms and conditions of the Mulan PSL v2. 124 | You may obtain a copy of Mulan PSL v2 at: 125 | http://license.coscl.org.cn/MulanPSL2 126 | THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. 127 | See the Mulan PSL v2 for more details. 128 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # [React-admin](https://github.com/javaLuo/react-admin/) · ![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg) 2 | 3 | 标准后台管理系统解决方案
4 | 动态菜单配置,权限精确到按钮
5 | 6 | ## what's this? 7 | 8 | react+redux 后台管理系统脚手架
9 | react+redux+vite+antd 10 | 11 | 15 | 16 | ## 构建 Start 17 | 18 | ```javascript 19 | pnpm install // 安装依赖模块 20 | pnpm run dev // 运行开发环境 21 | pnpm run build // 正式打包,生成最终代码 22 | pnpm run preview // 本地运行正式打包后的最终代码 23 | pnpm run prettier // 一键格式化代码 24 | ``` 25 | 26 | ## 最近更新 27 | 28 | - 接入了vite打包,比自己配webpack要好多了 29 | - 不再需要`yarn dll` 30 | - (2020/03/13 正在进行) 升到antd4, 使用@rematch, 修改权限、菜单、角色后需更新用户信息 ,Typescript,menu的构建递归,添加权限/菜单的模态框需要加一个是否将该权限/菜单赋予给某些角色 31 | - 把所有包版本都升级到了最新 React16.7,webpack4.29,babel7... 32 | - 去掉了一些鸡肋的东西,真正项目中基本都不会用到 33 | - 去掉了 css-module,感觉太不方便了 34 | 35 | ## 前后端分离,权限是怎么控制的 36 | 37 | 在数据库里存储着权限的信息,可以在页面里各种编辑。
38 | 但最终实现,仍然是在页面里写死的,前端写在页面里的权限信息跟数据库里的信息一一对应就实现了权限控制。
39 | 更好的方法除非是使用 SSR 服务端渲染,直接把权限注入到页面中,就像传统的 JSP 那样。 40 | 41 | ## 内置通用功能 42 | 43 | 用户管理 增删改查 分配角色
44 |   角色管理 增删改查 分配菜单和权限
45 |   权限管理 增删改查
46 |   菜单管理 增删改查
47 | 48 | 关系:权限 依附于 菜单 依附于 角色 依附于 用户 49 | 50 | ## 预览地址 Demo 51 | 52 | https://isluo.com/work/admin/
53 | 账号:admin / user
54 | 密码:123456 / 123456 55 | 56 | ## 参考 57 | 58 | react-luo: https://github.com/javaLuo/react-luo
59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [React-admin](https://github.com/javaLuo/react-admin/) · ![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg) 2 | 3 | 标准后台管理系统解决方案
4 | 动态菜单配置,权限精确到按钮
5 | 6 | ## what's this? 7 | 8 | react+redux 后台管理系统脚手架
9 | react+redux+vite+antd 10 | 11 | 15 | 16 | ## 构建 Start 17 | 18 | ```javascript 19 | pnpm install // 安装依赖模块 20 | pnpm run dev // 运行开发环境 21 | pnpm run build // 正式打包,生成最终代码 22 | pnpm run preview // 本地运行正式打包后的最终代码 23 | pnpm run prettier // 一键格式化代码 24 | ``` 25 | 26 | ## 最近更新 27 | 28 | - 接入了vite打包,比自己配webpack要好多了 29 | 30 | ## 前后端分离,权限是怎么控制的 31 | 32 | 在数据库里存储着权限的信息,可以在页面里各种编辑。
33 | 但最终实现,仍然是在页面里写死的,前端写在页面里的权限信息跟数据库里的信息一一对应就实现了权限控制。
34 | 更好的方法除非是使用 SSR 服务端渲染,直接把权限注入到页面中,就像传统的 JSP 那样。 35 | 36 | ## 内置通用功能 37 | 38 | 用户管理 增删改查 分配角色
39 |   角色管理 增删改查 分配菜单和权限
40 |   权限管理 增删改查
41 |   菜单管理 增删改查
42 | 43 | 关系:权限 依附于 菜单 依附于 角色 依附于 用户 44 | 45 | ## 预览地址 Demo 46 | 47 | https://isluo.com/work/admin/
48 | 账号:admin / user
49 | 密码:123456 / 123456 50 | 51 | ## 参考 52 | 53 | react-luo: https://github.com/javaLuo/react-luo
54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Admin 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /mock/app-data.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // 不需要下面这几行,只是本地发布DEMO用 3 | // const Mock = require("mockjs"); 4 | // Mock.setup({ 5 | // timeout: "0-500", 6 | // }); 7 | 8 | /** 9 | * 模拟数据 10 | * 这个文件使用了兼容IE11的语法, 11 | * 也没有弄成ts,因为server.js中要用到此文件 12 | * **/ 13 | 14 | // ID序列 15 | let id_sequence = 1000; 16 | 17 | // 所有的用户数据 18 | const users = [ 19 | { 20 | id: 1, 21 | username: "admin", 22 | password: "123456", 23 | phone: "13600000000", 24 | email: "admin@react.com", 25 | desc: "超级管理员", 26 | conditions: 1, 27 | roles: [1, 2, 3], 28 | }, 29 | { 30 | id: 2, 31 | username: "user", 32 | password: "123456", 33 | phone: "13600000001", 34 | email: "user@react.com", 35 | desc: "普通管理员", 36 | conditions: 1, 37 | roles: [2], 38 | }, 39 | { 40 | id: 3, 41 | username: "user", 42 | password: "123456", 43 | phone: "13600000001", 44 | email: "user@react.com", 45 | desc: "普通管理员3", 46 | conditions: 1, 47 | roles: [2], 48 | }, 49 | { 50 | id: 4, 51 | username: "user", 52 | password: "123456", 53 | phone: "13600000001", 54 | email: "user@react.com", 55 | desc: "普通管理员4", 56 | conditions: 1, 57 | roles: [2], 58 | }, 59 | { 60 | id: 5, 61 | username: "user", 62 | password: "123456", 63 | phone: "13600000001", 64 | email: "user@react.com", 65 | desc: "普通管理员5", 66 | conditions: 1, 67 | roles: [2], 68 | }, 69 | ]; 70 | 71 | // 所有的菜单数据 72 | const menus = [ 73 | { 74 | id: 1, 75 | title: "首页", 76 | icon: "icon-home", 77 | url: "/home", 78 | parent: null, 79 | desc: "首页", 80 | sorts: 0, 81 | conditions: 1, 82 | }, 83 | { 84 | id: 2, 85 | title: "系统管理", 86 | icon: "icon-setting", 87 | url: "/system", 88 | parent: null, 89 | desc: "系统管理目录分支", 90 | sorts: 1, 91 | conditions: 1, 92 | }, 93 | { 94 | id: 3, 95 | title: "用户管理", 96 | icon: "icon-user", 97 | url: "/system/useradmin", 98 | parent: 2, 99 | desc: "系统管理/用户管理", 100 | sorts: 0, 101 | conditions: 1, 102 | }, 103 | { 104 | id: 4, 105 | title: "角色管理", 106 | icon: "icon-team", 107 | url: "/system/roleadmin", 108 | parent: 2, 109 | desc: "系统管理/角色管理", 110 | sorts: 1, 111 | conditions: 1, 112 | }, 113 | { 114 | id: 5, 115 | title: "权限管理", 116 | icon: "icon-safetycertificate", 117 | url: "/system/poweradmin", 118 | parent: 2, 119 | desc: "系统管理/权限管理", 120 | sorts: 2, 121 | conditions: 1, 122 | }, 123 | { 124 | id: 6, 125 | title: "菜单管理", 126 | icon: "icon-appstore", 127 | url: "/system/menuadmin", 128 | parent: 2, 129 | desc: "系统管理/菜单管理", 130 | sorts: 3, 131 | conditions: 1, 132 | }, 133 | ]; 134 | 135 | // 所有的权限数据 136 | const powers = [ 137 | { 138 | id: 1, 139 | menu: 3, 140 | title: "新增", 141 | code: "user:add", 142 | desc: "用户管理 - 添加权限", 143 | sorts: 1, 144 | conditions: 1, 145 | }, 146 | { 147 | id: 2, 148 | menu: 3, 149 | title: "修改", 150 | code: "user:up", 151 | desc: "用户管理 - 修改权限", 152 | sorts: 2, 153 | conditions: 1, 154 | }, 155 | { 156 | id: 3, 157 | menu: 3, 158 | title: "查看", 159 | code: "user:query", 160 | desc: "用户管理 - 查看权限", 161 | sorts: 3, 162 | conditions: 1, 163 | }, 164 | { 165 | id: 4, 166 | menu: 3, 167 | title: "删除", 168 | code: "user:del", 169 | desc: "用户管理 - 删除权限", 170 | sorts: 4, 171 | conditions: 1, 172 | }, 173 | { 174 | id: 5, 175 | menu: 3, 176 | title: "分配角色", 177 | code: "user:role", 178 | desc: "用户管理 - 分配角色权限", 179 | sorts: 5, 180 | conditions: 1, 181 | }, 182 | 183 | { 184 | id: 6, 185 | menu: 4, 186 | title: "新增", 187 | code: "role:add", 188 | desc: "角色管理 - 添加权限", 189 | sorts: 1, 190 | conditions: 1, 191 | }, 192 | { 193 | id: 7, 194 | menu: 4, 195 | title: "修改", 196 | code: "role:up", 197 | desc: "角色管理 - 修改权限", 198 | sorts: 2, 199 | conditions: 1, 200 | }, 201 | { 202 | id: 8, 203 | menu: 4, 204 | title: "查看", 205 | code: "role:query", 206 | desc: "角色管理 - 查看权限", 207 | sorts: 3, 208 | conditions: 1, 209 | }, 210 | { 211 | id: 18, 212 | menu: 4, 213 | title: "分配权限", 214 | code: "role:power", 215 | desc: "角色管理 - 分配权限", 216 | sorts: 4, 217 | conditions: 1, 218 | }, 219 | { 220 | id: 9, 221 | menu: 4, 222 | title: "删除", 223 | code: "role:del", 224 | desc: "角色管理 - 删除权限", 225 | sorts: 5, 226 | conditions: 1, 227 | }, 228 | 229 | { 230 | id: 10, 231 | menu: 5, 232 | title: "新增", 233 | code: "power:add", 234 | desc: "权限管理 - 添加权限", 235 | sorts: 1, 236 | conditions: 1, 237 | }, 238 | { 239 | id: 11, 240 | menu: 5, 241 | title: "修改", 242 | code: "power:up", 243 | desc: "权限管理 - 修改权限", 244 | sorts: 2, 245 | conditions: 1, 246 | }, 247 | { 248 | id: 12, 249 | menu: 5, 250 | title: "查看", 251 | code: "power:query", 252 | desc: "权限管理 - 查看权限", 253 | sorts: 3, 254 | conditions: 1, 255 | }, 256 | { 257 | id: 13, 258 | menu: 5, 259 | title: "删除", 260 | code: "power:del", 261 | desc: "权限管理 - 删除权限", 262 | sorts: 2, 263 | conditions: 1, 264 | }, 265 | 266 | { 267 | id: 14, 268 | menu: 6, 269 | title: "新增", 270 | code: "menu:add", 271 | desc: "菜单管理 - 添加权限", 272 | sorts: 1, 273 | conditions: 1, 274 | }, 275 | { 276 | id: 15, 277 | menu: 6, 278 | title: "修改", 279 | code: "menu:up", 280 | desc: "菜单管理 - 修改权限", 281 | sorts: 2, 282 | conditions: 1, 283 | }, 284 | { 285 | id: 16, 286 | menu: 6, 287 | title: "查看", 288 | code: "menu:query", 289 | desc: "菜单管理 - 查看权限", 290 | sorts: 3, 291 | conditions: 1, 292 | }, 293 | { 294 | id: 17, 295 | menu: 6, 296 | title: "删除", 297 | code: "menu:del", 298 | desc: "菜单管理 - 删除权限", 299 | sorts: 2, 300 | conditions: 1, 301 | }, 302 | ]; 303 | // 所有的角色数据 304 | const roles = [ 305 | { 306 | id: 1, 307 | title: "超级管理员", 308 | desc: "超级管理员拥有所有权限", 309 | sorts: 1, 310 | conditions: 1, 311 | menuAndPowers: [ 312 | { menuId: 1, powers: [] }, 313 | { menuId: 2, powers: [] }, 314 | { menuId: 3, powers: [1, 2, 3, 4, 5] }, 315 | { menuId: 4, powers: [6, 7, 8, 9, 18] }, 316 | { menuId: 5, powers: [10, 11, 12, 13] }, 317 | { menuId: 6, powers: [14, 15, 16, 17] }, 318 | ], 319 | }, 320 | { 321 | id: 2, 322 | title: "普通管理员", 323 | desc: "普通管理员", 324 | sorts: 2, 325 | conditions: 1, 326 | menuAndPowers: [ 327 | { menuId: 1, powers: [] }, 328 | { menuId: 2, powers: [] }, 329 | { menuId: 3, powers: [3] }, 330 | { menuId: 4, powers: [6, 7, 8, 18] }, 331 | { menuId: 5, powers: [10, 11, 12] }, 332 | { menuId: 6, powers: [14, 15, 16] }, 333 | ], 334 | }, 335 | { 336 | id: 3, 337 | title: "运维人员", 338 | desc: "运维人员不能删除对象", 339 | sorts: 3, 340 | conditions: 1, 341 | menuAndPowers: [ 342 | { menuId: 1, powers: [] }, 343 | { menuId: 2, powers: [] }, 344 | { menuId: 3, powers: [3] }, 345 | { menuId: 4, powers: [7, 8] }, 346 | { menuId: 5, powers: [11, 12] }, 347 | { menuId: 6, powers: [15, 16] }, 348 | ], 349 | }, 350 | ]; 351 | 352 | /** 353 | * 工具 - decode 354 | * **/ 355 | const decode = function (str) { 356 | if (!str) { 357 | return str; 358 | } 359 | try { 360 | return decodeURIComponent(str); 361 | } catch (e) { 362 | return str; 363 | } 364 | }; 365 | 366 | /** 367 | * 方法 368 | * **/ 369 | // 登录 370 | const onLogin = function (p) { 371 | const u = users.find(function (item) { 372 | return item.username === p.username; 373 | }); 374 | if (!u) { 375 | return { status: 204, data: null, message: "该用户不存在" }; 376 | } else if (u.password !== p.password) { 377 | return { status: 204, data: null, message: "密码错误" }; 378 | } 379 | return { status: 200, data: u, message: "登录成功" }; 380 | }; 381 | // 获取所有菜单 382 | const getMenus = function (p) { 383 | return { status: 200, data: menus, message: "success" }; 384 | }; 385 | // 获取菜单(根据ID) 386 | const getMenusById = function (p) { 387 | // const p = JSON.parse(request.body); 388 | let res = []; 389 | if (p.id instanceof Array) { 390 | res = menus.filter(function (item) { 391 | return p.id.includes(item.id); 392 | }); 393 | } else { 394 | const t = menus.find(function (item) { 395 | return item.id === p.id; 396 | }); 397 | res.push(t); 398 | } 399 | return { status: 200, data: res, message: "success" }; 400 | }; 401 | 402 | // 添加新菜单 403 | const addMenu = function (p) { 404 | // const p = JSON.parse(request.body); 405 | p.id = ++id_sequence; 406 | menus.push(p); 407 | return { status: 200, data: menus, message: "添加成功" }; 408 | }; 409 | // 修改菜单 410 | const upMenu = function (p) { 411 | // const p = JSON.parse(request.body); 412 | const oldIndex = menus.findIndex(function (item) { 413 | return item.id === p.id; 414 | }); 415 | if (oldIndex !== -1) { 416 | const news = Object.assign({}, menus[oldIndex], p); 417 | menus.splice(oldIndex, 1, news); 418 | return { status: 200, data: menus, message: "success" }; 419 | } else { 420 | return { status: 204, data: null, message: "未找到该条数据" }; 421 | } 422 | }; 423 | // 删除菜单 424 | const delMenu = function (p) { 425 | // const p = JSON.parse(request.body); 426 | const oldIndex = menus.findIndex(function (item) { 427 | return item.id === p.id; 428 | }); 429 | 430 | if (oldIndex !== -1) { 431 | const haveChild = menus.findIndex(function (item) { 432 | return item.parent === menus[oldIndex].id; 433 | }); 434 | if (haveChild === -1) { 435 | menus.splice(oldIndex, 1); 436 | return { status: 200, data: menus, message: "success" }; 437 | } else { 438 | return { status: 204, data: null, message: "该菜单下有子菜单,无法删除" }; 439 | } 440 | } else { 441 | return { status: 204, data: null, message: "未找到该条数据" }; 442 | } 443 | }; 444 | // 根据菜单ID查询其下权限 445 | const getPowerByMenuId = function (p) { 446 | // const p = JSON.parse(request.body); 447 | const menuId = Number(p.menuId); 448 | 449 | if (menuId) { 450 | return { 451 | status: 200, 452 | data: powers 453 | .filter(function (item) { 454 | return item.menu === menuId; 455 | }) 456 | .sort(function (a, b) { 457 | return a.sorts - b.sorts; 458 | }), 459 | message: "success", 460 | }; 461 | } else { 462 | return { status: 200, data: [], message: "success" }; 463 | } 464 | }; 465 | // 根据权限ID查询对应的权限 466 | const getPowerById = function (p) { 467 | // const p = JSON.parse(request.body); 468 | let res = []; 469 | if (p.id instanceof Array) { 470 | res = powers.filter(function (item) { 471 | return p.id.includes(item.id); 472 | }); 473 | } else { 474 | const t = powers.find(function (item) { 475 | return item.id === p.id; 476 | }); 477 | res.push(t); 478 | } 479 | return { status: 200, data: res, message: "success" }; 480 | }; 481 | // 添加权限 482 | const addPower = function (p) { 483 | // const p = JSON.parse(request.body); 484 | p.id = ++id_sequence; 485 | powers.push(p); 486 | return { status: 200, data: { id: p.id }, message: "success" }; 487 | }; 488 | // 修改权限 489 | const upPower = function (p) { 490 | // const p = JSON.parse(request.body); 491 | 492 | const oldIndex = powers.findIndex(function (item) { 493 | return item.id === p.id; 494 | }); 495 | if (oldIndex !== -1) { 496 | const news = Object.assign({}, powers[oldIndex], p); 497 | powers.splice(oldIndex, 1, news); 498 | return { status: 200, data: { id: p.id }, message: "success" }; 499 | } else { 500 | return { status: 204, data: null, message: "未找到该条数据" }; 501 | } 502 | }; 503 | // 删除权限 504 | const delPower = function (p) { 505 | const oldIndex = powers.findIndex(function (item) { 506 | return item.id === p.id; 507 | }); 508 | 509 | if (oldIndex !== -1) { 510 | powers.splice(oldIndex, 1); 511 | return { status: 200, data: null, message: "success" }; 512 | } else { 513 | return { status: 204, data: null, message: "未找到该条数据" }; 514 | } 515 | }; 516 | // 查询角色(分页,条件筛选) 517 | const getRoles = function (p) { 518 | const map = roles.filter(function (item) { 519 | let yeah = true; 520 | const title = decode(p.title); 521 | const conditions = Number(p.conditions); 522 | if (title && !item.title.includes(title)) { 523 | yeah = false; 524 | } 525 | if (conditions && item.conditions !== conditions) { 526 | yeah = false; 527 | } 528 | return yeah; 529 | }); 530 | const r = map.sort(function (a, b) { 531 | return a.sorts - b.sorts; 532 | }); 533 | const res = r.slice((p.pageNum - 1) * p.pageSize, p.pageNum * p.pageSize); 534 | return { 535 | status: 200, 536 | data: { list: res, total: map.length }, 537 | message: "success", 538 | }; 539 | }; 540 | // 查询角色(所有) 541 | const getAllRoles = function (p) { 542 | return { status: 200, data: roles, message: "success" }; 543 | }; 544 | // 查询角色(通过角色ID) 545 | const getRoleById = function (p) { 546 | // const p = JSON.parse(request.body); 547 | let res = []; 548 | if (p.id instanceof Array) { 549 | res = roles.filter(function (item) { 550 | return p.id.includes(item.id); 551 | }); 552 | } else { 553 | const t = roles.find(function (item) { 554 | return item.id === p.id; 555 | }); 556 | res.push(t); 557 | } 558 | return { status: 200, data: res, message: "success" }; 559 | }; 560 | // 添加角色 561 | const addRole = function (p) { 562 | // const p = JSON.parse(request.body); 563 | p.id = ++id_sequence; 564 | if (!p.menuAndPowers) { 565 | p.menuAndPowers = []; 566 | } 567 | roles.push(p); 568 | return { status: 200, data: null, message: "success" }; 569 | }; 570 | // 修改角色 571 | const upRole = function (p) { 572 | // const p = JSON.parse(request.body); 573 | const oldIndex = roles.findIndex(function (item) { 574 | return item.id === p.id; 575 | }); 576 | if (oldIndex !== -1) { 577 | const news = Object.assign({}, roles[oldIndex], p); 578 | roles.splice(oldIndex, 1, news); 579 | return { status: 200, data: null, message: "success" }; 580 | } else { 581 | return { status: 204, data: null, message: "未找到该条数据" }; 582 | } 583 | }; 584 | // 删除角色 585 | const delRole = function (p) { 586 | // const p = JSON.parse(request.body); 587 | const oldIndex = roles.findIndex(function (item) { 588 | return item.id === p.id; 589 | }); 590 | if (oldIndex !== -1) { 591 | roles.splice(oldIndex, 1); 592 | return { status: 200, data: null, message: "success" }; 593 | } else { 594 | return { status: 204, data: null, message: "未找到该条数据" }; 595 | } 596 | }; 597 | // 根据角色ID查询该角色所拥有的菜单和权限详细信息 598 | const findAllPowerByRoleId = function (p) { 599 | // const p = JSON.parse(request.body); 600 | const t = roles.find(function (item) { 601 | return item.id === p.id; 602 | }); 603 | if (t) { 604 | const res = t.powers.map(function (item, index) { 605 | const _menu = menus.find(function (v) { 606 | return v.id === item.menuId; 607 | }); 608 | const _powers = item.powers.map(function (v) { 609 | return powers.find(function (p) { 610 | return p.id === v; 611 | }); 612 | }); 613 | _menu.powers = _powers.filter(function (item) { 614 | return item.conditions === 1; 615 | }); 616 | return _menu; 617 | }); 618 | return { status: 200, data: res, message: "success" }; 619 | } else { 620 | return { status: 204, data: null, message: "未找到该角色" }; 621 | } 622 | }; 623 | // 获取所有的菜单及权限数据 - 为了构建PowerTree组件 624 | const getAllMenusAndPowers = function (p) { 625 | const res = menus.map(function (item) { 626 | const _menu = item; 627 | const _powers = powers.filter(function (v) { 628 | return v.menu === item.id && v.conditions === 1; 629 | }); 630 | _menu.powers = _powers; 631 | return _menu; 632 | }); 633 | return { status: 200, data: res, message: "success" }; 634 | }; 635 | // 给指定角色分配菜单和权限 636 | const setPowersByRoleId = function (p) { 637 | // const p = JSON.parse(request.body); 638 | const oldIndex = roles.findIndex(function (item) { 639 | return item.id === p.id; 640 | }); 641 | if (oldIndex !== -1) { 642 | const pow = p.menus.map(function (item) { 643 | return { menuId: item, powers: [] }; 644 | }); 645 | // 将每一个权限id归类到对应的菜单里 646 | p.powers.forEach(function (ppItem) { 647 | // 通过权限id查询该权限对象 648 | const thePowerData = powers.find(function (pItem) { 649 | return pItem.id === ppItem; 650 | }); 651 | if (thePowerData) { 652 | const theMenuId = thePowerData.menu; 653 | if (theMenuId) { 654 | const thePow = pow.find(function (powItem) { 655 | return powItem.menuId === theMenuId; 656 | }); 657 | if (thePow) { 658 | thePow.powers.push(ppItem); 659 | } 660 | } 661 | } 662 | }); 663 | 664 | roles[oldIndex].menuAndPowers = pow; 665 | return { status: 200, data: null, message: "success" }; 666 | } else { 667 | return { status: 204, data: null, message: "未找到该条数据" }; 668 | } 669 | }; 670 | 671 | // 给指定角色分配菜单和权限 672 | const setPowersByRoleIds = function (ps) { 673 | ps.forEach(function (p) { 674 | const oldIndex = roles.findIndex(function (item) { 675 | return item.id === p.id; 676 | }); 677 | if (oldIndex !== -1) { 678 | const pow = p.menus.map(function (item) { 679 | return { menuId: item, powers: [] }; 680 | }); 681 | // 将每一个权限id归类到对应的菜单里 682 | p.powers.forEach(function (ppItem) { 683 | // 通过权限id查询该权限对象 684 | const thePowerData = powers.find(function (pItem) { 685 | return pItem.id === ppItem; 686 | }); 687 | if (thePowerData) { 688 | const theMenuId = thePowerData.menu; 689 | if (theMenuId) { 690 | const thePow = pow.find(function (powItem) { 691 | return powItem.menuId === theMenuId; 692 | }); 693 | if (thePow) { 694 | thePow.powers.push(ppItem); 695 | } 696 | } 697 | } 698 | }); 699 | roles[oldIndex].menuAndPowers = pow; 700 | } 701 | }); 702 | return { status: 200, data: null, message: "success" }; 703 | }; 704 | 705 | // 条件分页查询用户列表 706 | const getUserList = function (p) { 707 | const map = users.filter(function (item) { 708 | let yeah = true; 709 | const username = decode(p.username); 710 | const conditions = Number(p.conditions); 711 | if (username && !item.username.includes(username)) { 712 | yeah = false; 713 | } 714 | if (conditions && item.conditions != conditions) { 715 | yeah = false; 716 | } 717 | return yeah; 718 | }); 719 | const pageNum = Number(p.pageNum); // 从第1页开始 720 | const pageSize = Number(p.pageSize); 721 | const res = map.slice((pageNum - 1) * pageSize, pageNum * pageSize); 722 | return { 723 | status: 200, 724 | data: { list: res, total: map.length }, 725 | message: "success", 726 | }; 727 | }; 728 | // 添加用户 729 | const addUser = function (p) { 730 | // const p = JSON.parse(request.body); 731 | p.id = ++id_sequence; 732 | users.push(p); 733 | return { status: 200, data: null, message: "success" }; 734 | }; 735 | // 修改用户 736 | const upUser = function (p) { 737 | // const p = JSON.parse(request.body); 738 | const oldIndex = users.findIndex(function (item) { 739 | return item.id === p.id; 740 | }); 741 | if (oldIndex !== -1) { 742 | const news = Object.assign({}, users[oldIndex], p); 743 | users.splice(oldIndex, 1, news); 744 | return { status: 200, data: null, message: "success" }; 745 | } else { 746 | return { status: 204, data: null, message: "未找到该条数据" }; 747 | } 748 | }; 749 | // 删除用户 750 | const delUser = function (p) { 751 | // const p = JSON.parse(request.body); 752 | const oldIndex = users.findIndex(function (item) { 753 | return item.id === p.id; 754 | }); 755 | if (oldIndex !== -1) { 756 | users.splice(oldIndex, 1); 757 | return { status: 200, data: null, message: "success" }; 758 | } else { 759 | return { status: 204, data: null, message: "未找到该条数据" }; 760 | } 761 | }; 762 | 763 | export default function (obj) { 764 | const url = obj.url; 765 | const body = obj.body; 766 | let params = typeof body === "string" ? JSON.parse(body) : body; 767 | let path = url; 768 | 769 | // 是get请求 解析参数 770 | if (url.includes("?")) { 771 | path = url.split("?")[0]; 772 | const s = url.split("?")[1].split("&"); // ['a=1','b=2'] 773 | params = {}; 774 | 775 | for (let i = 0; i < s.length; i++) { 776 | if (s[i]) { 777 | const ss = s[i].split("="); 778 | params[ss[0]] = ss[1]; 779 | } 780 | } 781 | } 782 | if (path.includes("http")) { 783 | path = path.replace( 784 | globalThis.location.protocol + "//" + globalThis.location.host, 785 | "" 786 | ); 787 | } 788 | console.info("请求接口:", path, params); 789 | switch (path) { 790 | case "/api/login": 791 | return onLogin(params); 792 | case "/api/getmenus": 793 | return getMenus(params); 794 | case "/api/getMenusById": 795 | return getMenusById(params); 796 | case "/api/addmenu": 797 | return addMenu(params); 798 | case "/api/upmenu": 799 | return upMenu(params); 800 | case "/api/delmenu": 801 | return delMenu(params); 802 | case "/api/getpowerbymenuid": 803 | return getPowerByMenuId(params); 804 | case "/api/getPowerById": 805 | return getPowerById(params); 806 | case "/api/addpower": 807 | return addPower(params); 808 | case "/api/uppower": 809 | return upPower(params); 810 | case "/api/delpower": 811 | return delPower(params); 812 | case "/api/getroles": 813 | return getRoles(params); 814 | case "/api/getAllRoles": 815 | return getAllRoles(params); 816 | case "/api/getRoleById": 817 | return getRoleById(params); 818 | case "/api/addrole": 819 | return addRole(params); 820 | case "/api/uprole": 821 | return upRole(params); 822 | case "/api/delrole": 823 | return delRole(params); 824 | case "/api/findAllPowerByRoleId": 825 | return findAllPowerByRoleId(params); 826 | case "/api/getAllMenusAndPowers": 827 | return getAllMenusAndPowers(params); 828 | case "/api/setPowersByRoleId": 829 | return setPowersByRoleId(params); 830 | case "/api/setPowersByRoleIds": 831 | return setPowersByRoleIds(params); 832 | case "/api/getUserList": 833 | return getUserList(params); 834 | case "/api/addUser": 835 | return addUser(params); 836 | case "/api/upUser": 837 | return upUser(params); 838 | case "/api/delUser": 839 | return delUser(params); 840 | default: 841 | return { status: 404, data: null, message: "api not found" }; 842 | } 843 | } 844 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-admin", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "prettier": "prettier --write \"{src,mock}/**/*.{js,css,less,ts,tsx}\"" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons": "^5.1.0", 14 | "@loadable/component": "^5.15.3", 15 | "@rematch/core": "^2.2.0", 16 | "antd": "4.x", 17 | "axios": "^1.4.0", 18 | "lodash": "^4.17.21", 19 | "normalize.css": "^8.0.1", 20 | "qs": "^6.11.2", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-redux": "^8.0.5", 24 | "react-router-dom": "^6.11.2", 25 | "react-use": "^17.4.0", 26 | "react-vcode": "1.0.11", 27 | "redux": "^4.2.1" 28 | }, 29 | "devDependencies": { 30 | "@types/loadable__component": "^5.13.4", 31 | "@types/lodash": "^4.14.194", 32 | "@types/mockjs": "^1.0.7", 33 | "@types/node": "^20.2.3", 34 | "@types/qs": "^6.9.7", 35 | "@types/react": "^18.2.6", 36 | "@types/react-dom": "^18.2.4", 37 | "@types/react-redux": "^7.1.25", 38 | "@types/react-router-dom": "^5.3.3", 39 | "@typescript-eslint/eslint-plugin": "^5.59.6", 40 | "@typescript-eslint/parser": "^5.59.6", 41 | "@vitejs/plugin-react-swc": "^3.3.1", 42 | "autoprefixer": "^10.4.14", 43 | "consola": "^3.1.0", 44 | "eslint": "^8.41.0", 45 | "eslint-config-prettier": "^8.8.0", 46 | "eslint-plugin-prettier": "^4.2.1", 47 | "eslint-plugin-react": "^7.32.2", 48 | "eslint-plugin-react-hooks": "^4.6.0", 49 | "less": "^4.1.3", 50 | "mockjs": "^1.1.0", 51 | "postcss": "^8.4.23", 52 | "prettier": "^2.8.8", 53 | "prop-types": "^15.8.1", 54 | "rc-tree": "^5.7.3", 55 | "typescript": "^5.0.4", 56 | "vite": "^4.3.8", 57 | "vite-plugin-eslint": "^1.8.1", 58 | "vite-plugin-style-import": "^2.0.0" 59 | } 60 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/error.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/assets/error.gif -------------------------------------------------------------------------------- /src/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/assets/loading.gif -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/nothing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/assets/nothing.png -------------------------------------------------------------------------------- /src/assets/react-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/assets/react-logo.jpg -------------------------------------------------------------------------------- /src/assets/snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/assets/snow.gif -------------------------------------------------------------------------------- /src/assets/styles/default.less: -------------------------------------------------------------------------------- 1 | /* 全局配置 */ 2 | 3 | * { 4 | -webkit-overflow-scrolling: touch; /* 允许独立滚动区域,解决IOS上的非body元素滚动条滚动时卡顿 */ 5 | -webkit-tap-highlight-color: transparent; /* 覆盖IOS上用户点击连接时的默认高亮颜色 */ 6 | outline: none; 7 | } 8 | body { 9 | min-width: 1200px; 10 | } 11 | ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style: none; 15 | } 16 | /* 禁止换行,末尾省略 */ 17 | .all_nowarp { 18 | white-space: nowrap; 19 | overflow: hidden; 20 | text-overflow: ellipsis; 21 | } 22 | /* 强制换行 */ 23 | .all_warp { 24 | word-break: break-all; 25 | word-wrap: break-word; 26 | } 27 | .all_center { 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | /* 美化滚动条 */ 33 | ::-webkit-scrollbar { 34 | width: 6px; 35 | height: 6px; 36 | background-color: transparent; 37 | } 38 | 39 | /*定义滑块 内阴影+圆角*/ 40 | ::-webkit-scrollbar-thumb { 41 | background-color: #333; 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/styles/global.less: -------------------------------------------------------------------------------- 1 | /* 一些全局都用到的样式 */ 2 | 3 | /* table基础样式 */ 4 | .diy-table { 5 | .control-btn { 6 | font-size: 14px; 7 | cursor: pointer; 8 | &.green { 9 | color: #00a854; 10 | } 11 | &.red { 12 | color: #ff3333; 13 | } 14 | &.blue { 15 | color: #0066cc; 16 | } 17 | } 18 | .ant-pagination { 19 | margin-right: 20px; 20 | } 21 | td { 22 | max-width: 300px; 23 | min-width: 50px; 24 | } 25 | } 26 | 27 | /* 全局search基础样式 */ 28 | .g-search { 29 | display: flex; 30 | align-items: center; 31 | margin-bottom: 8px; 32 | .ant-divider { 33 | margin: 0 20px; 34 | } 35 | .search-func { 36 | display: flex; 37 | } 38 | .search-ul { 39 | display: flex; 40 | & > li { 41 | padding: 2px; 42 | margin-right: 4px; 43 | & > span:first-child { 44 | min-width: 90px; 45 | text-align: right; 46 | display: inline-block; 47 | margin-right: 10px; 48 | } 49 | } 50 | &.btns { 51 | border-top: solid 1px #f0f0f0; 52 | padding-top: 4px; 53 | } 54 | } 55 | } 56 | .ant-tooltip { 57 | min-width: 30px; 58 | } 59 | .ant-input[disabled]{ 60 | color: rgba(0,0,0,.6) !important; 61 | } -------------------------------------------------------------------------------- /src/components/Bread/index.less: -------------------------------------------------------------------------------- 1 | .bread { 2 | padding: 16px; 3 | display: flex; 4 | align-items: center; 5 | .icon { 6 | flex: none; 7 | color: #22cc22; 8 | margin-right: 8px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/Bread/index.tsx: -------------------------------------------------------------------------------- 1 | /** 通用动态面包屑 **/ 2 | import React, { useMemo } from "react"; 3 | import { useLocation } from "react-router-dom"; 4 | import { Breadcrumb } from "antd"; 5 | import { EnvironmentOutlined } from "@ant-design/icons"; 6 | import "./index.less"; 7 | import { Menu } from "@/models/index.type"; 8 | 9 | interface Props { 10 | menus: Menu[]; 11 | } 12 | 13 | export default function BreadCom(props: Props): JSX.Element { 14 | const location = useLocation(); 15 | 16 | /** 根据当前location动态生成对应的面包屑 **/ 17 | const breads = useMemo(() => { 18 | const paths: string = location.pathname; 19 | const breads: JSX.Element[] = []; 20 | 21 | let parentId: number | null = null; 22 | do { 23 | const pathObj: Menu | undefined = props.menus.find( 24 | (v) => v.id === parentId || v.url === paths 25 | ); 26 | 27 | if (pathObj) { 28 | breads.push( 29 | {pathObj.title} 30 | ); 31 | parentId = pathObj.parent; 32 | } else { 33 | parentId = null; 34 | } 35 | } while (parentId); 36 | 37 | breads.reverse(); 38 | return breads; 39 | }, [location.pathname, props.menus]); 40 | 41 | return ( 42 |
43 | 44 | {breads} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/CanvasBack/index.less: -------------------------------------------------------------------------------- 1 | .canvas-back { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | overflow: hidden; 8 | canvas { 9 | position: absolute; 10 | display: block; 11 | top: 50%; 12 | left: 50%; 13 | height: 100%; 14 | width: 100%; 15 | transform: translate(-50%, -50%); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/CanvasBack/index.tsx: -------------------------------------------------------------------------------- 1 | /** Canvas背景效果,变化的线条 **/ 2 | import React, { useRef, useCallback } from "react"; 3 | import { useMount } from "react-use"; 4 | import "./index.less"; 5 | 6 | interface Props { 7 | col: number; // 纵向密度 8 | row: number; // 横向密度 9 | } 10 | interface BaseData { 11 | ctx: CanvasRenderingContext2D | null; 12 | dots: Dot[]; 13 | width: number; 14 | height: number; 15 | } 16 | interface Dot { 17 | x: number; // 原始坐标x 18 | y: number; // 原始坐标y 19 | sx: number; // 当前偏移量x 20 | sy: number; // 当前偏移量y 21 | dx: boolean; // 当前方向x 22 | dy: boolean; // 当前方向y 23 | color: number; // b通道颜色值 24 | dcolor: boolean; // 颜色改变向量 25 | } 26 | export default function CanvasBack(props: Props): JSX.Element { 27 | const myCanvas = useRef(null); 28 | const data = useRef({ 29 | ctx: null, 30 | dots: [], 31 | width: 0, 32 | height: 0, 33 | }); 34 | 35 | const animateTimer = useRef(null); 36 | useMount(() => { 37 | if (myCanvas.current) { 38 | data.current.ctx = myCanvas.current.getContext("2d"); 39 | if (data.current && data.current.ctx) { 40 | data.current.ctx.strokeStyle = "rgba(255,255,255,1)"; 41 | data.current.width = myCanvas.current.clientWidth; 42 | data.current.height = myCanvas.current.clientHeight; 43 | myCanvas.current.width = data.current.width; 44 | myCanvas.current.height = data.current.height; 45 | init(props.row, props.col, data.current.width, data.current.height); 46 | animate(); 47 | } 48 | } 49 | 50 | return (): void => { 51 | animateTimer.current && window.cancelAnimationFrame(animateTimer.current); 52 | }; 53 | }); 54 | 55 | /** 工具 - 获取范围随机数 **/ 56 | const random = (min: number, max: number): number => { 57 | return Math.random() * (max - min) + min; 58 | }; 59 | 60 | /** 初始化canvas **/ 61 | const init = ( 62 | row: number, 63 | col: number, 64 | width: number, 65 | height: number 66 | ): void => { 67 | const step_row: number = height / (row - 2); 68 | const step_col: number = width / (col - 2); 69 | for (let i = 0; i < row; i++) { 70 | for (let j = 0; j < col; j++) { 71 | const temp: Dot = { 72 | x: j * step_col - step_col / 2, // 原始坐标x 73 | y: i * step_row - step_row / 2, // 原始坐标y 74 | sx: random(-step_row / 2, step_row / 2), // 当前偏移量x 75 | sy: random(-step_col / 2, step_col / 2), // 当前偏移量y 76 | dx: !!Math.round(random(0, 1)), // 当前方向x 77 | dy: !!Math.round(random(0, 1)), // 当前方向y 78 | color: random(20, 70), // b通道颜色值 79 | dcolor: !!Math.round(random(0, 1)), // 颜色改变向量 80 | }; 81 | data.current.dots.push(temp); 82 | } 83 | } 84 | }; 85 | 86 | /** 绘制一帧 **/ 87 | const drow = useCallback( 88 | ( 89 | dots: Dot[], 90 | row: number, 91 | col: number, 92 | ctx: CanvasRenderingContext2D, 93 | width: number, 94 | height: number 95 | ): void => { 96 | ctx.fillRect(0, 0, width, height); 97 | 98 | for (let i = 0; i < row; i++) { 99 | for (let j = 0; j < col - 1; j++) { 100 | const k = i * col + j; 101 | const k1 = k + 1; 102 | const k2 = k + col; 103 | const k3 = k - col + 1; 104 | if (i <= row - 2) { 105 | ctx.beginPath(); 106 | ctx.moveTo(dots[k].x + dots[k].sx, dots[k].y + dots[k].sy); 107 | ctx.lineTo(dots[k1].x + dots[k1].sx, dots[k1].y + dots[k1].sy); 108 | ctx.lineTo(dots[k2].x + dots[k2].sx, dots[k2].y + dots[k2].sy); 109 | ctx.closePath(); 110 | const c = Math.round( 111 | (dots[k].color + dots[k1].color + dots[k2].color) / 3 112 | ); 113 | ctx.fillStyle = `rgb(6,${Math.round(c / 1.3)},${c})`; 114 | ctx.fill(); 115 | } 116 | if (i > 0) { 117 | ctx.beginPath(); 118 | ctx.moveTo(dots[k].x + dots[k].sx, dots[k].y + dots[k].sy); 119 | ctx.lineTo(dots[k1].x + dots[k1].sx, dots[k1].y + dots[k1].sy); 120 | ctx.lineTo(dots[k3].x + dots[k3].sx, dots[k3].y + dots[k3].sy); 121 | ctx.closePath(); 122 | const c: number = Math.round( 123 | (dots[k].color + dots[k1].color + dots[k3].color) / 3 124 | ); 125 | ctx.fillStyle = `rgb(6, ${Math.round(c / 1.3)},${c})`; 126 | ctx.fill(); 127 | } 128 | } 129 | } 130 | }, 131 | [] 132 | ); 133 | 134 | /** 动画函数 **/ 135 | const animate = (): void => { 136 | const row = props.row; 137 | const col = props.col; 138 | const width = data.current.width; 139 | const height = data.current.height; 140 | const step_row = height / (row - 2); 141 | const step_col = width / (col - 2); 142 | 143 | data.current.dots.forEach(function (item) { 144 | if (item.dx) { 145 | // 增 146 | if (item.sx < step_col / 3) { 147 | item.sx += 0.1; 148 | } else { 149 | item.dx = !item.dx; 150 | } 151 | } else { 152 | // 减 153 | if (item.sx > -(step_col / 3)) { 154 | item.sx -= 0.1; 155 | } else { 156 | item.dx = !item.dx; 157 | } 158 | } 159 | 160 | if (item.dy) { 161 | // 增 162 | if (item.sy < step_row / 3) { 163 | item.sy += 0.1; 164 | } else { 165 | item.dy = !item.dy; 166 | } 167 | } else { 168 | // 减 169 | if (item.sy > -(step_row / 3)) { 170 | item.sy -= 0.1; 171 | } else { 172 | item.dy = !item.dy; 173 | } 174 | } 175 | 176 | /** 处理颜色变化 **/ 177 | if (item.dcolor) { 178 | // 颜色变亮 179 | if (item.color < 80) { 180 | item.color += 0.4; 181 | } else { 182 | item.dcolor = !item.dcolor; 183 | } 184 | } else { 185 | if (item.color > 20) { 186 | item.color -= 0.4; 187 | } else { 188 | item.dcolor = !item.dcolor; 189 | } 190 | } 191 | }); 192 | 193 | drow( 194 | data.current.dots, 195 | row, 196 | col, 197 | data.current.ctx as CanvasRenderingContext2D, 198 | width, 199 | height 200 | ); 201 | animateTimer.current = requestAnimationFrame(animate); 202 | }; 203 | 204 | return ( 205 |
206 | 207 |
208 | ); 209 | } 210 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.less: -------------------------------------------------------------------------------- 1 | .error-boundary { 2 | text-align: center; 3 | padding: 50px; 4 | font-size: 14px; 5 | position: relative; 6 | margin: 0 auto; 7 | color: #aaa; 8 | .error-icon { 9 | font-size: 60px; 10 | margin-bottom: 20px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 子组件有任何报错都会传递到此 3 | * 用于页面异步加载出错时显示 4 | * 此组件只能用class的方式,因为hooks不支持getDerivedStateFromError 和 componentDidCatch 5 | */ 6 | import React from "react"; 7 | import { WarningOutlined } from "@ant-design/icons"; 8 | import "./index.less"; 9 | 10 | interface Props { 11 | location: Location; 12 | children: JSX.Element; 13 | } 14 | interface State { 15 | hasError: boolean; 16 | } 17 | export default class ErrorBoundary extends React.PureComponent { 18 | constructor(props: Props) { 19 | super(props); 20 | this.state = { 21 | hasError: false, 22 | }; 23 | } 24 | static getDerivedStateFromError() { 25 | return { hasError: true }; 26 | } 27 | 28 | componentDidUpdate(prevP: Props) { 29 | if (prevP.location !== this.props.location) { 30 | this.setState({ 31 | hasError: false, 32 | }); 33 | } 34 | } 35 | 36 | render() { 37 | if (this.state.hasError) { 38 | return ( 39 |
40 | 41 |
加载出错,请刷新页面
42 |
43 | ); 44 | } 45 | return this.props.children; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Footer/index.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | padding: 0 16px 16px 16px; 4 | flex: none; 5 | a:hover { 6 | text-decoration: underline; 7 | } 8 | &.user-layout { 9 | width: 100%; 10 | background-color: transparent; 11 | color: #ddd; 12 | font-size: 12px; 13 | z-index: 2; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | /* Footer 页面底部 */ 2 | import React from "react"; 3 | import { Layout } from "antd"; 4 | import "./index.less"; 5 | 6 | const { Footer } = Layout; 7 | 8 | interface Props { 9 | className?: string; 10 | } 11 | 12 | export default function FooterCom(props: Props) { 13 | return ( 14 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Header/index.less: -------------------------------------------------------------------------------- 1 | @backColor: #d1e3fa; 2 | .header { 3 | background-color: #fff; 4 | padding: 0; 5 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 6 | overflow: hidden; 7 | display: flex; 8 | align-items: center; 9 | .trigger { 10 | padding: 0 24px; 11 | font-size: 20px; 12 | cursor: pointer; 13 | line-height: 64px; 14 | transition: all 0.3s; 15 | &.fold { 16 | transform: rotate(180deg); 17 | } 18 | &:hover { 19 | background-color: @backColor; 20 | } 21 | } 22 | .rightBox { 23 | flex: auto; 24 | display: flex; 25 | justify-content: flex-end; 26 | .userhead { 27 | padding: 0 20px; 28 | margin-right: 4px; 29 | cursor: pointer; 30 | font-size: 20px; 31 | .username { 32 | font-size: 14px; 33 | margin-left: 4px; 34 | } 35 | &:hover { 36 | background-color: @backColor; 37 | } 38 | } 39 | } 40 | .full { 41 | padding: 0 24px; 42 | cursor: pointer; 43 | transition: all 0.3s; 44 | .ant-badge { 45 | display: inline !important; 46 | .ant-badge-count { 47 | top: -15px; 48 | } 49 | } 50 | .icon { 51 | font-size: 20px; 52 | padding: 4px; 53 | } 54 | &:hover { 55 | background-color: @backColor; 56 | } 57 | a { 58 | display: block; 59 | width: 100%; 60 | height: 100%; 61 | } 62 | } 63 | } 64 | .menu { 65 | .anticon { 66 | margin-right: 8px; 67 | } 68 | .ant-dropdown-menu-item { 69 | width: 160px; 70 | } 71 | } 72 | .headerPopover { 73 | .ant-popover-inner-content { 74 | padding: 0; 75 | } 76 | } 77 | .headerTabs { 78 | max-width: 400px; 79 | .ant-tabs-bar { 80 | padding: 12px 16px 0 16px; 81 | } 82 | .ant-tabs-tab { 83 | margin: 0 32px 0 0 !important; 84 | &:last-child { 85 | margin-right: 0 !important; 86 | } 87 | } 88 | .ant-tabs-content { 89 | min-height: 200px; 90 | } 91 | 92 | .link { 93 | display: block; 94 | width: 100%; 95 | height: 100%; 96 | padding: 12px; 97 | transition: all 0.3s; 98 | & + .link { 99 | border-top: solid 1px #f0f0f0; 100 | } 101 | &:hover { 102 | background-color: @backColor; 103 | } 104 | } 105 | .clear { 106 | width: 100%; 107 | color: #888; 108 | transition: all 0.3s; 109 | cursor: pointer; 110 | border: none; 111 | border-top: solid 1px #f0f0f0; 112 | &:hover { 113 | color: red; 114 | background-color: @backColor; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | /** 头部 **/ 2 | 3 | // ================== 4 | // 第三方库 5 | // ================== 6 | import React, { useState, useCallback } from "react"; 7 | import { Link } from "react-router-dom"; 8 | import { Layout, Tooltip, Dropdown } from "antd"; 9 | import { 10 | MenuFoldOutlined, 11 | FullscreenOutlined, 12 | FullscreenExitOutlined, 13 | GithubOutlined, 14 | ChromeOutlined, 15 | LogoutOutlined, 16 | SmileOutlined, 17 | } from "@ant-design/icons"; 18 | 19 | const { Header } = Layout; 20 | 21 | // ================== 22 | // 自定义的东西 23 | // ================== 24 | import "./index.less"; 25 | 26 | // ================== 27 | // 类型声明 28 | // ================== 29 | import type { MenuProps } from "antd"; 30 | import { UserInfo } from "@/models/index.type"; 31 | 32 | interface Element { 33 | webkitRequestFullscreen?: () => void; 34 | webkitExitFullscreen?: () => void; 35 | mozRequestFullScreen?: () => void; 36 | mozCancelFullScreen?: () => void; 37 | msRequestFullscreen?: () => void; 38 | msExitFullscreen?: () => void; 39 | } 40 | 41 | interface Props { 42 | collapsed: boolean; // 菜单的状态 43 | userinfo: UserInfo; // 用户信息 44 | onToggle: () => void; // 菜单收起与展开状态切换 45 | onLogout: () => void; // 退出登录 46 | } 47 | 48 | export default function HeaderCom(props: Props): JSX.Element { 49 | const [fullScreen, setFullScreen] = useState(false); // 当前是否是全屏状态 50 | // 进入全屏 51 | const requestFullScreen = useCallback(() => { 52 | const element: HTMLElement & Element = document.documentElement; 53 | // 判断各种浏览器,找到正确的方法 54 | const requestMethod = 55 | element.requestFullscreen || // W3C 56 | element.webkitRequestFullscreen || // Chrome等 57 | element.mozRequestFullScreen || // FireFox 58 | element.msRequestFullscreen; // IE11 59 | if (requestMethod) { 60 | requestMethod.call(element); 61 | } 62 | setFullScreen(true); 63 | }, []); 64 | 65 | // 退出全屏 66 | const exitFullScreen = useCallback(() => { 67 | // 判断各种浏览器,找到正确的方法 68 | const element: Document & Element = document; 69 | const exitMethod = 70 | element.exitFullscreen || // W3C 71 | element.mozCancelFullScreen || // firefox 72 | element.webkitExitFullscreen || // Chrome等 73 | element.msExitFullscreen; // IE11 74 | 75 | if (exitMethod) { 76 | exitMethod.call(document); 77 | } 78 | setFullScreen(false); 79 | }, []); 80 | 81 | // 退出登录 82 | const onMenuClick: MenuProps["onClick"] = (e) => { 83 | // 退出按钮被点击 84 | if (e.key === "logout") { 85 | props.onLogout(); 86 | } 87 | }; 88 | 89 | const u = props.userinfo.userBasicInfo; 90 | return ( 91 |
92 | props.onToggle()} 95 | /> 96 | 97 |
98 | 99 |
100 | {fullScreen ? ( 101 | 105 | ) : ( 106 | 110 | )} 111 |
112 |
113 | {u ? ( 114 | 127 | 128 | blog.isluo.com 129 | 130 | ), 131 | }, 132 | { 133 | key: "item-2", 134 | label: ( 135 | 140 | 141 | GitHub 142 | 143 | ), 144 | }, 145 | { 146 | type: "divider", 147 | }, 148 | { 149 | key: "logout", 150 | label: ( 151 | <> 152 | 153 | 退出登录 154 | 155 | ), 156 | }, 157 | ], 158 | }} 159 | placement="bottomRight" 160 | > 161 |
162 | 163 | {u.username} 164 |
165 |
166 | ) : ( 167 | 168 |
169 | 未登录 170 |
171 |
172 | )} 173 |
174 |
175 | ); 176 | } 177 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | /* 用于菜单的自定义图标 */ 2 | import React from "react"; 3 | import { createFromIconfontCN } from "@ant-design/icons"; 4 | 5 | const IconFont = createFromIconfontCN({ 6 | scriptUrl: "//at.alicdn.com/t/font_1688075_vwak21i2wxj.js", 7 | }); 8 | 9 | interface Props { 10 | type: string; 11 | } 12 | 13 | export default function Icon(props: Props): JSX.Element { 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Loading/index.less: -------------------------------------------------------------------------------- 1 | .loading { 2 | text-align: center; 3 | padding: 50px; 4 | font-size: 14px; 5 | position: relative; 6 | margin: 0 auto; 7 | color: #888; 8 | img { 9 | margin-bottom: 20px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Loading组件 3 | * 用于按需加载时过渡显示等 4 | */ 5 | import React from "react"; 6 | import "./index.less"; 7 | import ImgLoading from "@/assets/loading.gif"; 8 | 9 | export default function LoadingComponent(): JSX.Element { 10 | return ( 11 |
12 | 13 |
加载中...
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Menu/index.less: -------------------------------------------------------------------------------- 1 | .sider { 2 | box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); 3 | min-height: 100vh; 4 | .menuLogo { 5 | height: 64px; 6 | background-color: #001529; 7 | &.hide { 8 | a { 9 | justify-content: center; 10 | div { 11 | overflow: hidden; 12 | opacity: 0; 13 | width: 0; 14 | } 15 | } 16 | } 17 | a { 18 | display: flex; 19 | height: 100%; 20 | align-items: center; 21 | img { 22 | width: 40px; 23 | flex: none; 24 | margin-left: 12px; 25 | } 26 | div { 27 | color: #fff; 28 | width: 100%; 29 | font-size: 24px; 30 | padding-left: 10px; 31 | white-space: nowrap; 32 | transition: all 0.3s; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | /** 左侧导航 **/ 2 | 3 | // ================== 4 | // 第三方库 5 | // ================== 6 | import React, { useState, useEffect, useCallback, useMemo } from "react"; 7 | import { Layout, Menu as MenuAntd } from "antd"; 8 | import { Link, useNavigate, useLocation } from "react-router-dom"; 9 | import { cloneDeep } from "lodash"; 10 | 11 | const { Sider } = Layout; 12 | 13 | // ================== 14 | // 自定义的东西 15 | // ================== 16 | import "./index.less"; 17 | import ImgLogo from "@/assets/logo.png"; 18 | import Icon from "@/components/Icon"; 19 | 20 | // ================== 21 | // 类型声明 22 | // ================== 23 | import type { Menu } from "@/models/index.type"; 24 | import type { ItemType } from "antd/lib/menu/hooks/useItems"; 25 | 26 | interface Props { 27 | data: Menu[]; // 所有的菜单数据 28 | collapsed: boolean; // 菜单咱开还是收起 29 | } 30 | 31 | // ================== 32 | // 本组件 33 | // ================== 34 | export default function MenuCom(props: Props): JSX.Element { 35 | const navigate = useNavigate(); 36 | const location = useLocation(); 37 | const [chosedKey, setChosedKey] = useState([]); // 当前选中 38 | const [openKeys, setOpenKeys] = useState([]); // 当前需要被展开的项 39 | 40 | // 当页面路由跳转时,即location发生改变,则更新选中项 41 | useEffect(() => { 42 | const paths = location.pathname.split("/").filter((item) => !!item); 43 | setChosedKey([location.pathname]); 44 | setOpenKeys(paths.map((item) => `/${item}`)); 45 | }, [location]); 46 | 47 | // ================== 48 | // 私有方法 49 | // ================== 50 | 51 | // 菜单被选择 52 | const onSelect = (e: any) => { 53 | if (e?.key) { 54 | navigate(e.key); 55 | } 56 | }; 57 | 58 | // 工具 - 递归将扁平数据转换为层级数据 59 | const dataToJson = useCallback( 60 | (one: Menu | undefined, data: Menu[]): Menu[] | undefined => { 61 | let kids; 62 | if (!one) { 63 | // 第1次递归 64 | kids = data.filter((item: Menu) => !item.parent); 65 | } else { 66 | kids = data.filter((item: Menu) => item.parent === one.id); 67 | } 68 | kids.forEach((item: Menu) => (item.children = dataToJson(item, data))); 69 | return kids.length ? kids : undefined; 70 | }, 71 | [] 72 | ); 73 | 74 | // 构建树结构 75 | const makeTreeDom = useCallback((data: Menu[]): any => { 76 | return data.map((item: Menu) => { 77 | if (item.children) { 78 | return { 79 | key: item.url, 80 | label: 81 | !item.parent && item.icon ? ( 82 | 83 | 84 | {item.title} 85 | 86 | ) : ( 87 | item.title 88 | ), 89 | children: makeTreeDom(item.children), 90 | }; 91 | } else { 92 | return { 93 | label: ( 94 | <> 95 | {!item.parent && item.icon ? : null} 96 | {item.title} 97 | 98 | ), 99 | key: item.url, 100 | }; 101 | } 102 | }); 103 | }, []); 104 | 105 | // ================== 106 | // 计算属性 memo 107 | // ================== 108 | 109 | /** 处理原始数据,将原始数据处理为层级关系 **/ 110 | const treeDom: ItemType[] = useMemo(() => { 111 | const d: Menu[] = cloneDeep(props.data); 112 | // 按照sort排序 113 | d.sort((a, b) => { 114 | return a.sorts - b.sorts; 115 | }); 116 | const sourceData: Menu[] = dataToJson(undefined, d) || []; 117 | const treeDom = makeTreeDom(sourceData); 118 | return treeDom; 119 | }, [props.data, dataToJson, makeTreeDom]); 120 | 121 | return ( 122 | 129 |
130 | 131 | 132 |
React-Admin
133 | 134 |
135 | setOpenKeys(keys)} 142 | onSelect={onSelect} 143 | /> 144 |
145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /src/components/TreeChose/PowerTreeTable.tsx: -------------------------------------------------------------------------------- 1 | /** 权限Table树 **/ 2 | 3 | import React, { useState, useMemo, useEffect, useCallback } from "react"; 4 | import { Modal, Table, Checkbox, Spin } from "antd"; 5 | import { Power, PowerTree } from "@/models/index.type"; 6 | import { cloneDeep } from "lodash"; 7 | 8 | // ================== 9 | // 类型声明 10 | // ================== 11 | 12 | // 默认被选中的菜单和权限 13 | export type PowerTreeDefault = { 14 | menus: number[]; 15 | powers: number[]; 16 | }; 17 | 18 | export type PowerLevel = PowerTree & { 19 | parent?: PowerLevel; 20 | children?: PowerLevel[]; 21 | key?: number; 22 | }; 23 | 24 | interface Props { 25 | title: string; // 指定模态框标题 26 | data: PowerTree[]; // 所有的菜单&权限原始数据 27 | defaultChecked: PowerTreeDefault; // 需要默认选中的项 28 | modalShow: boolean; // 是否显示 29 | initloading?: boolean; // 初始化时,树是否处于加载中状态 30 | loading: boolean; // 提交表单时,树的确定按钮是否处于等待状态 31 | onClose: () => void; // 关闭模态框 32 | onOk: (res: PowerTreeDefault) => void; // 确定选择,将所选项信息返回上级 33 | } 34 | 35 | // ================== 36 | // 本组件 用于角色授权的树形表格 37 | // ================== 38 | export default function TreeTable(props: Props): JSX.Element { 39 | const [treeChecked, setTreeChecked] = useState([]); // 受控,所有被选中的表格行 40 | const [btnDtoChecked, setBtnDtoChecked] = useState([]); // 受控,所有被选中的权限数据 41 | 42 | // ================== 43 | // 副作用 44 | // ================== 45 | 46 | // 哪些需要被默认选中 47 | useEffect(() => { 48 | setTreeChecked(props.defaultChecked.menus || []); 49 | setBtnDtoChecked(props.defaultChecked.powers || []); 50 | }, [props.defaultChecked]); 51 | 52 | // ================== 53 | // 私有方法 54 | // ================== 55 | 56 | // 提交 57 | const onOk = useCallback(() => { 58 | props.onOk?.({ 59 | menus: treeChecked, 60 | powers: btnDtoChecked, 61 | }); 62 | }, [props, btnDtoChecked, treeChecked]); 63 | 64 | // 关闭模态框 65 | const onClose = useCallback(() => { 66 | props.onClose(); 67 | }, [props]); 68 | 69 | // 被选中的权限 受控 70 | const dtoIsChecked = useCallback( 71 | (id: number): boolean => { 72 | return !!btnDtoChecked.find((item) => item === id); 73 | }, 74 | [btnDtoChecked] 75 | ); 76 | 77 | // TABLE btn权限选中和取消选中,需要记录哪些被选中 id/title/powers 78 | const onBtnDtoChange = useCallback( 79 | (e: any, id: number, record: PowerLevel) => { 80 | console.log("哈?", record); 81 | const old = [...btnDtoChecked]; 82 | let treeCheckedTemp = [...treeChecked]; 83 | if (e.target.checked) { 84 | // 选中 85 | old.push(id); 86 | treeCheckedTemp = Array.from(new Set([record.id, ...treeChecked])); 87 | } else { 88 | // 取消选中 89 | old.splice(old.indexOf(id), 1); 90 | 91 | // 判断当前这一行的权限中是否还有被选中的,如果全都没有选中,那当前菜单也要取消选中 92 | const tempMap = record.powers.map((item: Power) => item.id); 93 | if ( 94 | !btnDtoChecked.some( 95 | (item) => item !== id && tempMap.indexOf(item) >= 0 96 | ) 97 | ) { 98 | treeCheckedTemp.splice(treeCheckedTemp.indexOf(record.id), 1); 99 | } 100 | } 101 | 102 | setBtnDtoChecked(old); 103 | setTreeChecked(treeCheckedTemp); 104 | }, 105 | [btnDtoChecked, treeChecked] 106 | ); 107 | 108 | // 工具 - 递归将扁平数据转换为层级数据 109 | const dataToJson = useCallback( 110 | (one: PowerLevel | undefined, data: PowerLevel[]) => { 111 | let kids; 112 | if (!one) { 113 | // 第1次递归 114 | kids = data.filter((item: PowerLevel) => !item.parent); 115 | } else { 116 | kids = data.filter((item: PowerLevel) => item.parent === one.id); 117 | } 118 | kids.forEach((item: PowerLevel) => { 119 | item.children = dataToJson(item, data); 120 | item.key = item.id; 121 | }); 122 | return kids.length ? kids : undefined; 123 | }, 124 | [] 125 | ); 126 | 127 | // ================== 128 | // 计算属性 memo 129 | // ================== 130 | 131 | // 工具 - 赋值Key 132 | const makeKey = useCallback((data: PowerTree[]) => { 133 | const newData: PowerLevel[] = []; 134 | for (let i = 0; i < data.length; i++) { 135 | const item: any = { ...data[i] }; 136 | if (item.children) { 137 | item.children = makeKey(item.children); 138 | } 139 | const treeItem: PowerLevel = { 140 | ...item, 141 | key: item.id, 142 | }; 143 | newData.push(treeItem); 144 | } 145 | return newData; 146 | }, []); 147 | 148 | // 处理原始数据,将原始数据处理为层级关系(菜单的层级关系) 149 | const sourceData = useMemo(() => { 150 | const powerData: PowerTree[] = cloneDeep(props.data); 151 | // 这应该递归,把children数据也赋值key 152 | const d: PowerLevel[] = makeKey(powerData); 153 | // 按照sort排序 154 | d.sort((a, b) => { 155 | return a.sorts - b.sorts; 156 | }); 157 | 158 | return dataToJson(undefined, d) || []; 159 | }, [props.data, dataToJson]); 160 | 161 | // TABLE 列表项前面是否有多选框,并配置行为 162 | type TableData = { 163 | id: number; 164 | }; 165 | 166 | const tableRowSelection = useMemo(() => { 167 | return { 168 | onChange: (selectedRowKeys: React.Key[]): void => { 169 | setTreeChecked(selectedRowKeys.map((item) => Number(item))); 170 | }, 171 | onSelect: (record: TableData, selected: boolean): void => { 172 | const t = props.data.find((item) => item.id === record.id); 173 | if (selected) { 174 | // 选中,连带其权限全部勾选 175 | if (t && Array.isArray(t.powers)) { 176 | const temp = Array.from( 177 | new Set([...t.powers.map((item) => item.id), ...btnDtoChecked]) 178 | ); 179 | setBtnDtoChecked(temp); 180 | } 181 | } else { 182 | // 取消选中,连带其权限全部取消勾选 183 | if (t && Array.isArray(t.powers)) { 184 | const mapTemp = t.powers.map((item) => item.id); 185 | const temp = btnDtoChecked.filter( 186 | (item) => mapTemp.indexOf(item) < 0 187 | ); 188 | setBtnDtoChecked(temp); 189 | } 190 | } 191 | }, 192 | onSelectAll: (selected: boolean) => { 193 | if (selected) { 194 | // 选中 195 | setBtnDtoChecked( 196 | props.data.reduce((v1, v2) => { 197 | return [...v1, ...v2.powers.map((k) => k.id)]; 198 | }, [] as number[]) 199 | ); 200 | } else { 201 | setBtnDtoChecked([]); 202 | } 203 | }, 204 | selectedRowKeys: treeChecked, 205 | }; 206 | }, [props.data, treeChecked, btnDtoChecked]); 207 | 208 | // TABLE 字段 209 | const tableColumns = useMemo(() => { 210 | return [ 211 | { 212 | title: "菜单", 213 | dataIndex: "title", 214 | key: "title", 215 | width: "30%", 216 | }, 217 | { 218 | title: "权限", 219 | dataIndex: "powers", 220 | key: "powers", 221 | width: "70%", 222 | render: (value: Power[], record: PowerLevel): JSX.Element[] | null => { 223 | if (value) { 224 | return value.map((item: Power, index: number) => { 225 | return ( 226 | onBtnDtoChange(e, item.id, record)} 230 | > 231 | {item.title} 232 | 233 | ); 234 | }); 235 | } 236 | return null; 237 | }, 238 | }, 239 | ]; 240 | }, [dtoIsChecked, onBtnDtoChange]); 241 | 242 | return ( 243 | 253 | {props.initloading ? ( 254 |
255 | 256 |
257 | ) : ( 258 | 265 | )} 266 | 267 | ); 268 | } 269 | -------------------------------------------------------------------------------- /src/components/TreeChose/RoleTree.tsx: -------------------------------------------------------------------------------- 1 | /* Tree选择 - 角色选择 - 多选 */ 2 | import React, { useState, useMemo, useEffect, useCallback } from "react"; 3 | import { Tree, Modal } from "antd"; 4 | import { cloneDeep } from "lodash"; 5 | import { Role } from "@/models/index.type"; 6 | 7 | // ================== 8 | // 类型声明 9 | // ================== 10 | 11 | type RoleLevel = Role & { 12 | key: string | number; 13 | parent?: RoleLevel; 14 | children?: RoleLevel[]; 15 | }; 16 | 17 | interface Props { 18 | title: string; // 标题 19 | data: Role[]; // 原始数据 20 | defaultKeys: number[]; // 当前默认选中的key们 21 | visible: boolean; // 是否显示 22 | loading: boolean; // 确定按钮是否在等待中状态 23 | onOk: (keys: string[], role: Role[]) => Promise; // 确定 24 | onClose: () => void; // 关闭 25 | } 26 | 27 | // ================== 28 | // 本组件 29 | // ================== 30 | export default function RoleTreeComponent(props: Props): JSX.Element { 31 | const [nowKeys, setNowKeys] = useState([]); 32 | 33 | useEffect(() => { 34 | setNowKeys(props.defaultKeys.map((item) => `${item}`)); 35 | }, [props.defaultKeys]); 36 | 37 | // 工具 - 递归将扁平数据转换为层级数据 38 | const dataToJson = useCallback( 39 | (one: RoleLevel | undefined, data: RoleLevel[]) => { 40 | let kids; 41 | if (!one) { 42 | // 第1次递归 43 | kids = data.filter((item: RoleLevel) => !item.parent); 44 | } else { 45 | kids = data.filter((item: RoleLevel) => item.parent?.id === one.id); 46 | } 47 | kids.forEach( 48 | (item: RoleLevel) => (item.children = dataToJson(item, data)) 49 | ); 50 | return kids.length ? kids : undefined; 51 | }, 52 | [] 53 | ); 54 | 55 | // 点击确定时触发 56 | const onOk = useCallback(() => { 57 | // 通过key返回指定的数据 58 | const res = props.data.filter((item) => { 59 | return nowKeys.includes(`${item.id}`); 60 | }); 61 | // 返回选中的keys和选中的具体数据 62 | props.onOk && props.onOk(nowKeys, res); 63 | }, [props, nowKeys]); 64 | 65 | // 点击关闭时触发 66 | const onClose = useCallback(() => { 67 | props.onClose(); 68 | }, [props]); 69 | 70 | // 选中或取消选中时触发 71 | const onCheck = useCallback((keys: any) => { 72 | setNowKeys(keys); 73 | }, []); 74 | 75 | // ================== 76 | // 计算属性 memo 77 | // ================== 78 | 79 | // 工具 - 赋值Key 80 | const makeKey = useCallback((data: Role[]) => { 81 | const newData: RoleLevel[] = []; 82 | for (let i = 0; i < data.length; i++) { 83 | const item: any = { ...data[i] }; 84 | if (item.children) { 85 | item.children = makeKey(item.children); 86 | } 87 | const treeItem: RoleLevel = { 88 | ...(item as RoleLevel), 89 | key: item.id, 90 | }; 91 | newData.push(treeItem); 92 | } 93 | return newData; 94 | }, []); 95 | 96 | // 处理原始数据,将原始数据处理为层级关系 97 | const sourceData = useMemo(() => { 98 | const roleData: Role[] = cloneDeep(props.data); 99 | 100 | // 这应该递归,把children数据也赋值key 101 | const d: RoleLevel[] = makeKey(roleData); 102 | 103 | d.forEach((item) => { 104 | item.key = String(item.id); 105 | }); 106 | return dataToJson(undefined, d) || []; 107 | }, [props.data, dataToJson]); 108 | 109 | return ( 110 | 118 | 125 | 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // const env = process.env.NODE_ENV; // development / production 2 | 3 | export const baseUrl = `${location.protocol}//${location.host}`; 4 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.less: -------------------------------------------------------------------------------- 1 | .page-basic { 2 | width: 100%; 3 | min-height: 100vh; 4 | .content { 5 | margin: 0 16px 16px 16px; 6 | padding: 16px; 7 | background: #fff; 8 | height: 100%; 9 | min-height: 280px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/layouts/BasicLayout.tsx: -------------------------------------------------------------------------------- 1 | /** 基础页面结构 - 有头部、底部、侧边导航 **/ 2 | 3 | // ================== 4 | // 第三方库 5 | // ================== 6 | import React, { useState } from "react"; 7 | import { useSelector, useDispatch } from "react-redux"; 8 | import { useNavigate, Outlet } from "react-router-dom"; 9 | import { Layout, message } from "antd"; 10 | 11 | // ================== 12 | // 自定义的东西 13 | // ================== 14 | import "./BasicLayout.less"; 15 | 16 | // ================== 17 | // 组件 18 | // ================== 19 | import Header from "@/components/Header"; 20 | import MenuCom from "@/components/Menu"; 21 | import Footer from "@/components/Footer"; 22 | import Bread from "@/components/Bread"; 23 | 24 | const { Content } = Layout; 25 | 26 | // ================== 27 | // 类型声明 28 | // ================== 29 | import type { RootState, Dispatch } from "@/store"; 30 | 31 | // ================== 32 | // 本组件 33 | // ================== 34 | function BasicLayoutCom(): JSX.Element { 35 | const dispatch = useDispatch(); 36 | const navigate = useNavigate(); 37 | const userinfo = useSelector((state: RootState) => state.app.userinfo); 38 | const [collapsed, setCollapsed] = useState(false); // 菜单栏是否收起 39 | 40 | // 退出登录 41 | const onLogout = () => { 42 | dispatch.app.onLogout().then(() => { 43 | message.success("退出成功"); 44 | navigate("/user/login"); 45 | }); 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 |
setCollapsed(!collapsed)} 57 | onLogout={onLogout} 58 | /> 59 | 60 | 61 | 62 | 63 |
64 | 65 | 66 | ); 67 | } 68 | 69 | export default BasicLayoutCom; 70 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.less: -------------------------------------------------------------------------------- 1 | .page-user { 2 | width: 100%; 3 | min-height: 100vh; 4 | } 5 | -------------------------------------------------------------------------------- /src/layouts/UserLayout.tsx: -------------------------------------------------------------------------------- 1 | /** 基础页面结构 - 有头部,有底部,有侧边导航 **/ 2 | 3 | // ================== 4 | // 所需的第三方库 5 | // ================== 6 | import React from "react"; 7 | import { Outlet } from "react-router-dom"; 8 | import { Layout } from "antd"; 9 | 10 | // ================== 11 | // 自定义的东西 12 | // ================== 13 | import "./UserLayout.less"; 14 | 15 | // ================== 16 | // 组件 17 | // ================== 18 | 19 | import Footer from "../components/Footer"; 20 | 21 | const { Content } = Layout; 22 | 23 | // ================== 24 | // 本组件 25 | // ================== 26 | export default function AppContainer(): JSX.Element { 27 | return ( 28 | 29 | 30 | 31 | 32 |
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { HashRouter } from "react-router-dom"; 4 | import { Provider } from "react-redux"; 5 | import store from "./store"; 6 | import Router from "./router"; 7 | 8 | import "normalize.css"; 9 | import "@/assets/styles/default.less"; 10 | import "@/assets/styles/global.less"; 11 | 12 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/models/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础model 3 | * 在src/store/index.js 中被挂载到store上,命名为 app 4 | * **/ 5 | 6 | import axios from "@/util/axios"; // 自己写的工具函数,封装了请求数据的通用接口 7 | import { message } from "antd"; 8 | import { Dispatch, RootState } from "@/store"; 9 | import { 10 | Menu, 11 | Role, 12 | Power, 13 | MenuAndPower, 14 | UserInfo, 15 | AppState, 16 | Res, 17 | } from "./index.type"; 18 | 19 | const defaultState: AppState = { 20 | userinfo: { 21 | roles: [], // 当前用户拥有的角色 22 | menus: [], // 当前用户拥有的已授权的菜单 23 | powers: [], // 当前用户拥有的权限数据 24 | userBasicInfo: null, // 用户的基础信息,id,用户名... 25 | }, // 当前用户基本信息 26 | powersCode: [], // 当前用户拥有的权限code列表(仅保留了code),页面中的按钮的权限控制将根据此数据源判断 27 | }; 28 | export default { 29 | state: defaultState, 30 | reducers: { 31 | reducerUserInfo(state: AppState, payload: UserInfo) { 32 | return { 33 | ...state, 34 | userinfo: payload, 35 | powersCode: payload.powers.map((item) => item.code), 36 | }; 37 | }, 38 | reducerLogout(state: AppState) { 39 | return { 40 | ...state, 41 | userinfo: { 42 | menus: [], 43 | roles: [], 44 | powers: [], 45 | }, 46 | }; 47 | }, 48 | }, 49 | 50 | effects: (dispatch: Dispatch) => ({ 51 | /** 52 | * 登录 53 | * @param { username, password } params 54 | * */ 55 | async onLogin(params: { username: string; password: string }) { 56 | try { 57 | const res: Res = await axios.post("/api/login", params); 58 | return res; 59 | } catch (err) { 60 | message.error("网络错误,请重试"); 61 | } 62 | return; 63 | }, 64 | /** 65 | * 退出登录 66 | * @param null 67 | * **/ 68 | async onLogout() { 69 | try { 70 | // 同 dispatch.app.reducerLogout(); 71 | 72 | dispatch({ type: "app/reducerLogout", payload: null }); 73 | sessionStorage.removeItem("userinfo"); 74 | return "success"; 75 | } catch (err) { 76 | message.error("网络错误,请重试"); 77 | } 78 | return; 79 | }, 80 | /** 81 | * 设置用户信息 82 | * @param: {*} params 83 | * **/ 84 | async setUserInfo(params: UserInfo) { 85 | dispatch.app.reducerUserInfo(params); 86 | return "success"; 87 | }, 88 | 89 | /** 修改了角色/菜单/权限信息后需要更新用户的roles,menus,powers数据 **/ 90 | async updateUserInfo(payload: null, rootState: RootState): Promise { 91 | /** 2.重新查询角色信息 **/ 92 | const userinfo: UserInfo = rootState.app.userinfo; 93 | 94 | const res2: Res | undefined = await dispatch.sys.getRoleById({ 95 | id: userinfo.roles.map((item) => item.id), 96 | }); 97 | if (!res2 || res2.status !== 200) { 98 | // 角色查询失败 99 | return res2; 100 | } 101 | 102 | const roles: Role[] = res2.data.filter( 103 | (item: Role) => item.conditions === 1 104 | ); 105 | 106 | /** 3.根据菜单id 获取菜单信息 **/ 107 | const menuAndPowers = roles.reduce( 108 | (a, b) => [...a, ...b.menuAndPowers], 109 | [] as MenuAndPower[] 110 | ); 111 | const res3: Res | undefined = await dispatch.sys.getMenusById({ 112 | id: Array.from(new Set(menuAndPowers.map((item) => item.menuId))), 113 | }); 114 | if (!res3 || res3.status !== 200) { 115 | // 查询菜单信息失败 116 | return res3; 117 | } 118 | const menus: Menu[] = res3.data.filter( 119 | (item: Menu) => item.conditions === 1 120 | ); 121 | 122 | /** 4.根据权限id,获取权限信息 **/ 123 | const res4: Res | undefined = await dispatch.sys.getPowerById({ 124 | id: Array.from( 125 | new Set( 126 | menuAndPowers.reduce((a, b) => [...a, ...b.powers], [] as number[]) 127 | ) 128 | ), 129 | }); 130 | if (!res4 || res4.status !== 200) { 131 | // 权限查询失败 132 | return res4; 133 | } 134 | const powers: Power[] = res4.data.filter( 135 | (item: Power) => item.conditions === 1 136 | ); 137 | this.setUserInfo({ 138 | ...userinfo, 139 | roles, 140 | menus, 141 | powers, 142 | }); 143 | return; 144 | }, 145 | }), 146 | }; 147 | -------------------------------------------------------------------------------- /src/models/index.type.ts: -------------------------------------------------------------------------------- 1 | // 菜单添加,修改时的参数类型 2 | export interface MenuParam { 3 | id?: number; // ID,添加时可以没有id 4 | title: string; // 标题 5 | icon: string; // 图标 6 | url: string; // 链接路径 7 | parent: number | null; // 父级ID 8 | desc: string; // 描述 9 | sorts: number; // 排序编号 10 | conditions: number; // 状态,1启用,-1禁用 11 | children?: Menu[]; // 子菜单 12 | } 13 | 14 | // 菜单对象 15 | export interface Menu extends MenuParam { 16 | id: number; // ID 17 | } 18 | 19 | // 菜单id和权限id 20 | export interface MenuAndPower { 21 | menuId: number; // 菜单ID 22 | powers: number[]; // 该菜单拥有的所有权限ID 23 | } 24 | 25 | // 角色添加和修改时的参数类型 26 | export interface RoleParam { 27 | id?: number; // ID,添加时可以不传id 28 | title: string; // 角色名 29 | desc: string; // 描述 30 | sorts: number; // 排序编号 31 | conditions: number; // 状态,1启用,-1禁用 32 | menuAndPowers?: MenuAndPower[]; // 添加时可以不传菜单和权限 33 | } 34 | 35 | // 角色对象 36 | export interface Role extends RoleParam { 37 | id: number; // ID 38 | menuAndPowers: MenuAndPower[]; // 当前角色拥有的菜单id和权限id 39 | } 40 | 41 | // 权限添加修改时的参数类型 42 | export interface PowerParam { 43 | id?: number; // ID, 添加时可以没有id 44 | menu: number; // 所属的菜单 45 | title: string; // 标题 46 | code: string; // CODE 47 | desc: string; // 描述 48 | sorts: number; // 排序 49 | conditions: number; // 状态 1启用,-1禁用 50 | } 51 | 52 | // 权限对象 53 | export interface Power extends PowerParam { 54 | id: number; // ID 55 | } 56 | 57 | // 用户数据类型 58 | export interface UserInfo { 59 | userBasicInfo: UserBasicInfo | null; // 用户的基本信息 60 | menus: Menu[]; // 拥有的所有菜单对象 61 | roles: Role[]; // 拥有的所有角色对象 62 | powers: Power[]; // 拥有的所有权限对象 63 | } 64 | 65 | // 用户的基本信息 66 | export interface UserBasicInfo { 67 | id: number; // ID 68 | username: string; // 用户名 69 | password: string | number; // 密码 70 | phone: string | number; // 手机 71 | email: string; // 邮箱 72 | desc: string; // 描述 73 | conditions: number; // 状态 1启用,-1禁用 74 | roles: number[]; // 拥有的所有角色ID 75 | } 76 | 77 | // 添加修改用户时参数的数据类型 78 | export interface UserBasicInfoParam { 79 | id?: number; // ID 80 | username: string; // 用户名 81 | password: string | number; // 密码 82 | phone?: string | number; // 手机 83 | email?: string; // 邮箱 84 | desc?: string; // 描述 85 | conditions?: number; // 状态 1启用,-1禁用 86 | } 87 | 88 | export interface PowerTree extends Menu { 89 | powers: Power[]; 90 | } 91 | 92 | // ./app.js的state类型 93 | export interface AppState { 94 | userinfo: UserInfo; 95 | powersCode: string[]; 96 | } 97 | 98 | // ./sys.js的state类型 99 | export interface SysState { 100 | menus: Menu[]; 101 | roles: Role[]; 102 | powerTreeData: PowerTree[]; 103 | } 104 | 105 | // 接口的返回值类型 106 | export type Res = 107 | | { 108 | status: number; // 状态,200成功 109 | data?: any; // 返回的数据 110 | message?: string; // 返回的消息 111 | } 112 | | undefined; 113 | -------------------------------------------------------------------------------- /src/models/sys.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础model,系统权限相关功能 3 | * 在src/store/index.js 中被挂载到store上,命名为 sys 4 | * **/ 5 | 6 | import axios from "@/util/axios"; // 自己写的工具函数,封装了请求数据的通用接口 7 | import qs from "qs"; 8 | import { message } from "antd"; 9 | import { Dispatch } from "@/store"; 10 | 11 | import { 12 | Menu, 13 | Role, 14 | MenuParam, 15 | PowerParam, 16 | PowerTree, 17 | RoleParam, 18 | SysState, 19 | Res, 20 | UserBasicInfoParam, 21 | } from "./index.type"; 22 | 23 | const defaultState: SysState = { 24 | menus: [], // 所有的菜单信息(用于菜单管理,无视权限) 25 | roles: [], // 所有的角色信息(用于Model赋予项,无视权限) 26 | powerTreeData: [], // 分配权限treeTable组件所需原始数据 27 | }; 28 | 29 | export default { 30 | state: defaultState, 31 | reducers: { 32 | // 保存所有菜单数据 33 | reducerSetMenus(state: SysState, payload: Menu[]): SysState { 34 | return { ...state, menus: payload }; 35 | }, 36 | // 保存所有角色数据 37 | reducerSetRoles(state: SysState, payload: Role[]): SysState { 38 | return { ...state, roles: payload }; 39 | }, 40 | 41 | // 保存所有权限数据 42 | reducerSetAllPowers(state: SysState, payload: PowerTree[]): SysState { 43 | return { ...state, powerTreeData: payload }; 44 | }, 45 | }, 46 | 47 | effects: (dispatch: Dispatch) => ({ 48 | /** 49 | * 获取所有菜单 50 | * **/ 51 | async getMenus(): Promise { 52 | try { 53 | const res: Res = await axios.get("/api/getmenus"); 54 | if (res && res.status === 200) { 55 | dispatch.sys.reducerSetMenus(res.data); 56 | } 57 | return res; 58 | } catch (err) { 59 | message.error("网络错误,请重试"); 60 | } 61 | return; 62 | }, 63 | /** 64 | * 根据菜单ID获取对应的菜单信息 65 | * @param {number} id 可以是一个数字也可以是一个数组 66 | * **/ 67 | async getMenusById(params: { id: number | number[] }) { 68 | try { 69 | const res: Res = await axios.post(`/api/getMenusById`, params); 70 | return res; 71 | } catch (err) { 72 | message.error("网络错误,请重试"); 73 | } 74 | return; 75 | }, 76 | 77 | /** 78 | * 添加菜单 79 | * @param params MenuParam 80 | */ 81 | async addMenu(params: MenuParam) { 82 | try { 83 | const res: Res = await axios.post("/api/addmenu", params); 84 | return res; 85 | } catch (err) { 86 | message.error("网络错误,请重试"); 87 | } 88 | return; 89 | }, 90 | /** 91 | * 修改菜单 92 | * **/ 93 | async upMenu(params: MenuParam) { 94 | try { 95 | const res: Res = await axios.post("/api/upmenu", params); 96 | return res; 97 | } catch (err) { 98 | message.error("网络错误,请重试"); 99 | } 100 | return; 101 | }, 102 | /** 103 | * 删除菜单 104 | * **/ 105 | async delMenu(params: { id: number }) { 106 | try { 107 | const res: Res = await axios.post("/api/delmenu", params); 108 | return res; 109 | } catch (err) { 110 | message.error("网络错误,请重试"); 111 | } 112 | return; 113 | }, 114 | 115 | /** 116 | * 根据菜单ID查询其下的权限数据 117 | * **/ 118 | async getPowerDataByMenuId(params: { menuId: number | null }) { 119 | try { 120 | const res: Res = await axios.get( 121 | `/api/getpowerbymenuid?${qs.stringify(params)}` 122 | ); 123 | return res; 124 | } catch (err) { 125 | message.error("网络错误,请重试"); 126 | } 127 | return; 128 | }, 129 | 130 | /** 131 | * 根据权限ID查询对应的权限数据 132 | * @param id 可以是一个数字也可以是一个数组 133 | * **/ 134 | async getPowerById(params: { id: number | number[] }) { 135 | try { 136 | const res: Res = await axios.post(`/api/getPowerById`, params); 137 | return res; 138 | } catch (err) { 139 | message.error("网络错误,请重试"); 140 | } 141 | return; 142 | }, 143 | 144 | /** 获取所有角色 **/ 145 | async getAllRoles(): Promise { 146 | try { 147 | const res: Res = await axios.get("/api/getAllRoles"); 148 | if (res && res.status === 200) { 149 | dispatch.sys.reducerSetRoles(res.data); 150 | } 151 | return res; 152 | } catch (err) { 153 | message.error("网络错误,请重试"); 154 | } 155 | return; 156 | }, 157 | /** 158 | * 添加权限 159 | * **/ 160 | async addPower(params: PowerParam) { 161 | try { 162 | const res: Res = await axios.post("/api/addpower", params); 163 | return res; 164 | } catch (err) { 165 | message.error("网络错误,请重试"); 166 | } 167 | return; 168 | }, 169 | 170 | /** 171 | * 修改权限 172 | * **/ 173 | async upPower(params: PowerParam) { 174 | try { 175 | const res: Res = await axios.post("/api/uppower", params); 176 | return res; 177 | } catch (err) { 178 | message.error("网络错误,请重试"); 179 | } 180 | return; 181 | }, 182 | 183 | /** 184 | * 删除权限 185 | * **/ 186 | async delPower(params: { id: number }) { 187 | try { 188 | const res: Res = await axios.post("/api/delpower", params); 189 | return res; 190 | } catch (err) { 191 | message.error("网络错误,请重试"); 192 | } 193 | return; 194 | }, 195 | 196 | /** 197 | * 分页查询角色数据 198 | * **/ 199 | async getRoles(params: { 200 | pageNum: number; 201 | pageSize: number; 202 | title?: string; 203 | conditions?: number; 204 | }) { 205 | try { 206 | const res: Res = await axios.get( 207 | `/api/getroles?${qs.stringify(params)}` 208 | ); 209 | return res; 210 | } catch (err) { 211 | message.error("网络错误,请重试"); 212 | } 213 | return; 214 | }, 215 | 216 | /** 217 | * 通过角色ID查询对应的角色数据 218 | * @param id 可以是一个数字,也可以是一个数组 219 | * @return 返回值是数组 220 | * **/ 221 | async getRoleById(params: { id: number | number[] }) { 222 | try { 223 | const res: Res = await axios.post(`/api/getRoleById`, params); 224 | return res; 225 | } catch (err) { 226 | message.error("网络错误,请重试"); 227 | } 228 | return; 229 | }, 230 | 231 | /** 232 | * 添加角色 233 | * **/ 234 | async addRole(params: RoleParam) { 235 | try { 236 | const res: Res = await axios.post("/api/addrole", params); 237 | return res; 238 | } catch (err) { 239 | message.error("网络错误,请重试"); 240 | } 241 | return; 242 | }, 243 | /** 244 | * 修改角色 245 | * **/ 246 | async upRole(params: RoleParam) { 247 | try { 248 | const res: Res = await axios.post("/api/uprole", params); 249 | return res; 250 | } catch (err) { 251 | message.error("网络错误,请重试"); 252 | } 253 | return; 254 | }, 255 | 256 | /** 257 | * 删除角色 258 | * **/ 259 | async delRole(params: { id: number }) { 260 | try { 261 | const res: Res = await axios.post("/api/delrole", params); 262 | return res; 263 | } catch (err) { 264 | message.error("网络错误,请重试"); 265 | } 266 | return; 267 | }, 268 | 269 | /** 270 | * 通过角色ID查询该角色拥有的所有菜单和权限详细信息 271 | * **/ 272 | async findAllPowerByRoleId(params: { id: number }) { 273 | try { 274 | const res: Res = await axios.get( 275 | `/api/findAllPowerByRoleId?${qs.stringify(params)}` 276 | ); 277 | return res; 278 | } catch (err) { 279 | message.error("网络错误,请重试"); 280 | } 281 | return; 282 | }, 283 | 284 | /** 285 | * 获取所有的菜单及权限详细信息 286 | * 如果你在sys.ts中引用了sys本身,则需要显式的注明返回值的类型 287 | * **/ 288 | async getAllMenusAndPowers(): Promise { 289 | try { 290 | const res: Res = await axios.get(`/api/getAllMenusAndPowers`); 291 | if (res && res.status === 200) { 292 | dispatch.sys.reducerSetAllPowers(res.data); 293 | } 294 | return res; 295 | } catch (err) { 296 | message.error("网络错误,请重试"); 297 | } 298 | return; 299 | }, 300 | 301 | /** 302 | * 通过角色ID给指定角色设置菜单及权限 303 | * **/ 304 | async setPowersByRoleId(params: { 305 | id: number; 306 | menus: number[]; 307 | powers: number[]; 308 | }) { 309 | try { 310 | const res: Res = await axios.post("/api/setPowersByRoleId", params); 311 | return res; 312 | } catch (err) { 313 | message.error("网络错误,请重试"); 314 | } 315 | return; 316 | }, 317 | 318 | /** 319 | * (批量)通过角色ID给指定角色设置菜单及权限 320 | * @param params [{id,menus,powers},...] 321 | * */ 322 | async setPowersByRoleIds( 323 | params: { 324 | id: number; 325 | menus: number[]; 326 | powers: number[]; 327 | }[] 328 | ) { 329 | try { 330 | const res: Res = await axios.post("/api/setPowersByRoleIds", params); 331 | return res; 332 | } catch (err) { 333 | message.error("网络错误,请重试"); 334 | } 335 | return; 336 | }, 337 | 338 | /** 339 | * 条件分页查询用户列表 340 | * **/ 341 | async getUserList(params: { 342 | pageNum: number; 343 | pageSize: number; 344 | username?: string; 345 | conditions?: number; 346 | }) { 347 | try { 348 | const res: Res = await axios.get( 349 | `/api/getUserList?${qs.stringify(params)}` 350 | ); 351 | return res; 352 | } catch (err) { 353 | message.error("网络错误,请重试"); 354 | } 355 | return; 356 | }, 357 | 358 | /** 359 | * 添加用户 360 | * **/ 361 | async addUser(params: UserBasicInfoParam) { 362 | try { 363 | const res: Res = await axios.post("/api/addUser", params); 364 | return res; 365 | } catch (err) { 366 | message.error("网络错误,请重试"); 367 | } 368 | return; 369 | }, 370 | 371 | /** 372 | * 修改用户 373 | * **/ 374 | async upUser(params: UserBasicInfoParam) { 375 | try { 376 | const res: Res = await axios.post("/api/upUser", params); 377 | return res; 378 | } catch (err) { 379 | message.error("网络错误,请重试"); 380 | } 381 | return; 382 | }, 383 | 384 | /** 385 | * 删除用户 386 | * **/ 387 | async delUser(params: { id: number }) { 388 | try { 389 | const res: Res = await axios.post("/api/delUser", params); 390 | return res; 391 | } catch (err) { 392 | message.error("网络错误,请重试"); 393 | } 394 | return; 395 | }, 396 | 397 | /** 398 | * 给用户分配角色 399 | * 用的也是upUser接口 400 | * **/ 401 | async setUserRoles(params: { id: number; roles: number[] }) { 402 | try { 403 | const res: Res = await axios.post("/api/upUser", params); 404 | return res; 405 | } catch (err) { 406 | message.error("网络错误,请重试"); 407 | } 408 | return; 409 | }, 410 | }), 411 | }; 412 | -------------------------------------------------------------------------------- /src/pages/ErrorPages/401.tsx: -------------------------------------------------------------------------------- 1 | /* 401 没有权限 */ 2 | 3 | import React from "react"; 4 | import { Button } from "antd"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | import "./index.less"; 8 | import Img from "@/assets/error.gif"; 9 | 10 | export default function NoPowerContainer(): JSX.Element { 11 | const navigate = useNavigate(); 12 | const gotoHome = (): void => { 13 | navigate("/", { replace: true }); 14 | }; 15 | 16 | return ( 17 |
18 |
19 |
401
20 |
你没有访问该页面的权限
21 |
请联系你的管理员
22 | 25 |
26 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/ErrorPages/404.tsx: -------------------------------------------------------------------------------- 1 | /* 404 NotFound */ 2 | 3 | import React from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | import { Button } from "antd"; 6 | import Img from "@/assets/error.gif"; 7 | 8 | import "./index.less"; 9 | 10 | export default function NotFoundContainer(): JSX.Element { 11 | const navigate = useNavigate(); 12 | const gotoHome = (): void => { 13 | navigate("/", { replace: true }); 14 | }; 15 | return ( 16 |
17 |
18 |
404
19 |
Oh dear
20 |
这里什么也没有
21 | 24 |
25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/ErrorPages/index.less: -------------------------------------------------------------------------------- 1 | .page-error { 2 | position: relative; 3 | height: 100%; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | background-color: #fff; 8 | letter-spacing: 1px; 9 | .title { 10 | font-size: 100px; 11 | font-weight: bold; 12 | color: #888; 13 | @supports (-webkit-background-clip: text) { 14 | -webkit-background-clip: text; 15 | -webkit-text-fill-color: transparent; 16 | background-image: url(../../assets/snow.gif); 17 | background-repeat: repeat; 18 | opacity: 0.5; 19 | } 20 | } 21 | .info { 22 | font-size: 20px; 23 | color: #888; 24 | } 25 | .backBtn { 26 | margin-top: 10px; 27 | } 28 | img { 29 | margin-left: 100px; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/Home/index.less: -------------------------------------------------------------------------------- 1 | .page-home { 2 | padding: 50px 0 0 0; 3 | height: 100%; 4 | .box { 5 | margin: 0 auto; 6 | text-align: center; 7 | .title { 8 | font-size: 24px; 9 | } 10 | .info { 11 | margin-top: 10px; 12 | color: #888; 13 | } 14 | .link { 15 | margin-top: 40px; 16 | font-size: 12px; 17 | color: #888; 18 | a { 19 | color: #888; 20 | text-decoration: none; 21 | &:hover { 22 | text-decoration: underline; 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | /* 主页 */ 2 | 3 | import React from "react"; 4 | import ImgLogo from "@/assets/react-logo.jpg"; 5 | 6 | import "./index.less"; 7 | 8 | export default function HomePageContainer(): JSX.Element { 9 | return ( 10 |
11 |
12 | 13 |
React-admin
14 |
15 | 标准后台管理系统解决方案,react18、router6、rematch、antd4、vite4、ES6+ 16 |
17 |
动态菜单配置,权限精确到按钮
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/Login/index.less: -------------------------------------------------------------------------------- 1 | .page-login { 2 | height: calc(100vh - 53px); 3 | min-height: 400px; 4 | display: flex; 5 | justify-content: center; 6 | align-items: flex-start; 7 | .canvasBox { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | z-index: 0; 14 | } 15 | .loginBox { 16 | position: relative; 17 | margin-top: 10vh; 18 | flex: none; 19 | width: 400px; 20 | padding: 20px; 21 | border-radius: 4px; 22 | transform: scale(1.2, 1.2); 23 | opacity: 0; 24 | transition: all 300ms; 25 | &.show { 26 | transform: scale(1, 1); 27 | opacity: 1; 28 | } 29 | .title { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | margin-bottom: 40px; 34 | font-size: 36px; 35 | font-weight: 600; 36 | color: #fff; 37 | letter-spacing: 1px; 38 | img { 39 | height: 56px; 40 | margin-right: 10px; 41 | } 42 | } 43 | .vcode { 44 | float: right; 45 | border: solid 1px #d9d9d9; 46 | border-radius: 4px; 47 | } 48 | .remember { 49 | color: #fff; 50 | } 51 | .submit-btn { 52 | float: right; 53 | width: 270px; 54 | } 55 | } 56 | .login-back { 57 | width: 100%; 58 | height: 100%; 59 | position: fixed; 60 | bottom: 0; 61 | z-index: -1; 62 | canvas { 63 | width: 100%; 64 | height: 100%; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/pages/Login/index.tsx: -------------------------------------------------------------------------------- 1 | /** 登录页 **/ 2 | 3 | // ================== 4 | // 所需的各种插件 5 | // ================== 6 | import React, { useState, useEffect, useCallback } from "react"; 7 | import { useDispatch } from "react-redux"; 8 | import { useNavigate } from "react-router-dom"; 9 | import tools from "@/util/tools"; 10 | 11 | // ================== 12 | // 所需的所有组件 13 | // ================== 14 | import Vcode from "react-vcode"; 15 | import { Form, Input, Button, Checkbox, message } from "antd"; 16 | import { UserOutlined, KeyOutlined } from "@ant-design/icons"; 17 | import CanvasBack from "@/components/CanvasBack"; 18 | import LogoImg from "@/assets/logo.png"; 19 | 20 | // ================== 21 | // 类型声明 22 | // ================== 23 | import { Dispatch } from "@/store"; 24 | import { 25 | Role, 26 | Menu, 27 | Power, 28 | UserBasicInfo, 29 | Res, 30 | MenuAndPower, 31 | } from "@/models/index.type"; 32 | import { CheckboxChangeEvent } from "antd/lib/checkbox"; 33 | 34 | // ================== 35 | // CSS 36 | // ================== 37 | import "./index.less"; 38 | 39 | // ================== 40 | // 本组件 41 | // ================== 42 | function LoginContainer(): JSX.Element { 43 | const dispatch = useDispatch(); 44 | 45 | const navigate = useNavigate(); 46 | const [form] = Form.useForm(); 47 | const [loading, setLoading] = useState(false); // 是否正在登录中 48 | const [rememberPassword, setRememberPassword] = useState(false); // 是否记住密码 49 | const [codeValue, setCodeValue] = useState("00000"); // 当前验证码的值 50 | const [show, setShow] = useState(false); // 加载完毕时触发动画 51 | 52 | // 进入登陆页时,判断之前是否保存了用户名和密码 53 | useEffect(() => { 54 | const userLoginInfo = localStorage.getItem("userLoginInfo"); 55 | if (userLoginInfo) { 56 | const userLoginInfoObj = JSON.parse(userLoginInfo); 57 | setRememberPassword(true); 58 | 59 | form.setFieldsValue({ 60 | username: userLoginInfoObj.username, 61 | password: tools.uncompile(userLoginInfoObj.password), 62 | }); 63 | } 64 | if (!userLoginInfo) { 65 | document.getElementById("username")?.focus(); 66 | } else { 67 | document.getElementById("vcode")?.focus(); 68 | } 69 | setShow(true); 70 | }, [form]); 71 | 72 | /** 73 | * 执行登录 74 | * 这里模拟: 75 | * 1.登录,得到用户信息 76 | * 2.通过用户信息获取其拥有的所有角色信息 77 | * 3.通过角色信息获取其拥有的所有权限信息 78 | * **/ 79 | const loginIn = useCallback( 80 | async (username: string, password: string) => { 81 | let userBasicInfo: UserBasicInfo | null = null; 82 | let roles: Role[] = []; 83 | let menus: Menu[] = []; 84 | let powers: Power[] = []; 85 | 86 | /** 1.登录 (返回信息中有该用户拥有的角色id) **/ 87 | const res1: Res | undefined = await dispatch.app.onLogin({ 88 | username, 89 | password, 90 | }); 91 | if (!res1 || res1.status !== 200 || !res1.data) { 92 | // 登录失败 93 | return res1; 94 | } 95 | 96 | userBasicInfo = res1.data; 97 | 98 | /** 2.根据角色id获取角色信息 (角色信息中有该角色拥有的菜单id和权限id) **/ 99 | const res2 = await dispatch.sys.getRoleById({ 100 | id: (userBasicInfo as UserBasicInfo).roles, 101 | }); 102 | if (!res2 || res2.status !== 200) { 103 | // 角色查询失败 104 | return res2; 105 | } 106 | 107 | roles = res2.data.filter((item: Role) => item.conditions === 1); // conditions: 1启用 -1禁用 108 | 109 | /** 3.根据菜单id 获取菜单信息 **/ 110 | const menuAndPowers = roles.reduce( 111 | (a, b) => [...a, ...b.menuAndPowers], 112 | [] as MenuAndPower[] 113 | ); 114 | const res3 = await dispatch.sys.getMenusById({ 115 | id: Array.from(new Set(menuAndPowers.map((item) => item.menuId))), 116 | }); 117 | if (!res3 || res3.status !== 200) { 118 | // 查询菜单信息失败 119 | return res3; 120 | } 121 | 122 | menus = res3.data.filter((item: Menu) => item.conditions === 1); 123 | 124 | /** 4.根据权限id,获取权限信息 **/ 125 | const res4 = await dispatch.sys.getPowerById({ 126 | id: Array.from( 127 | new Set( 128 | menuAndPowers.reduce( 129 | (a, b: MenuAndPower) => [...a, ...b.powers], 130 | [] as number[] 131 | ) 132 | ) 133 | ), 134 | }); 135 | if (!res4 || res4.status !== 200) { 136 | // 权限查询失败 137 | return res4; 138 | } 139 | powers = res4.data.filter((item: Power) => item.conditions === 1); 140 | return { status: 200, data: { userBasicInfo, roles, menus, powers } }; 141 | }, 142 | [dispatch.sys, dispatch.app] 143 | ); 144 | 145 | // 用户提交登录 146 | const onSubmit = async (): Promise => { 147 | try { 148 | const values = await form.validateFields(); 149 | setLoading(true); 150 | const res = await loginIn(values.username, values.password); 151 | if (res && res.status === 200) { 152 | message.success("登录成功"); 153 | if (rememberPassword) { 154 | localStorage.setItem( 155 | "userLoginInfo", 156 | JSON.stringify({ 157 | username: values.username, 158 | password: tools.compile(values.password), // 密码简单加密一下再存到localStorage 159 | }) 160 | ); // 保存用户名和密码 161 | } else { 162 | localStorage.removeItem("userLoginInfo"); 163 | } 164 | /** 将这些信息加密后存入sessionStorage,并存入store **/ 165 | sessionStorage.setItem( 166 | "userinfo", 167 | tools.compile(JSON.stringify(res.data)) 168 | ); 169 | await dispatch.app.setUserInfo(res.data); 170 | navigate("/"); // 跳转到主页 171 | } else { 172 | message.error(res?.message ?? "登录失败"); 173 | setLoading(false); 174 | } 175 | } catch (e) { 176 | // 验证未通过 177 | } 178 | }; 179 | 180 | // 记住密码按钮点击 181 | const onRemember = (e: CheckboxChangeEvent): void => { 182 | setRememberPassword(e.target.checked); 183 | }; 184 | 185 | // 验证码改变时触发 186 | const onVcodeChange = (code: string | null): void => { 187 | form.setFieldsValue({ 188 | vcode: code, // 开发模式自动赋值验证码,正式环境,这里应该赋值'' 189 | }); 190 | setCodeValue(code || ""); 191 | }; 192 | 193 | return ( 194 |
195 |
196 | 197 |
198 |
199 |
200 |
201 | logo 202 | React-Admin 203 |
204 |
205 | 216 | } 218 | size="large" 219 | id="username" // 为了获取焦点 220 | placeholder="admin/user" 221 | onPressEnter={onSubmit} 222 | /> 223 | 224 | 231 | } 233 | size="large" 234 | type="password" 235 | placeholder="123456/123456" 236 | onPressEnter={onSubmit} 237 | /> 238 | 239 | 240 | ({ 245 | validator: (rule: any, value: string): Promise => { 246 | const v = tools.trim(value); 247 | if (v) { 248 | if (v.length > 4) { 249 | return Promise.reject("验证码为4位字符"); 250 | } else if ( 251 | v.toLowerCase() !== codeValue.toLowerCase() 252 | ) { 253 | return Promise.reject("验证码错误"); 254 | } else { 255 | return Promise.resolve(); 256 | } 257 | } else { 258 | return Promise.reject("请输入验证码"); 259 | } 260 | }, 261 | }), 262 | ]} 263 | > 264 | 271 | 272 | 282 | 283 |
284 | 289 | 记住密码 290 | 291 | 300 |
301 |
302 | 303 |
304 |
305 | ); 306 | } 307 | 308 | export default LoginContainer; 309 | -------------------------------------------------------------------------------- /src/pages/System/MenuAdmin/index.less: -------------------------------------------------------------------------------- 1 | .page-menu-admin { 2 | display: flex; 3 | .l { 4 | width: 256px; 5 | border: solid 1px #f0f0f0; 6 | flex: none; 7 | .title { 8 | height: 44px; 9 | line-height: 44px; 10 | box-sizing: border-box; 11 | font-size: 14px; 12 | padding: 0 10px; 13 | border-bottom: 1px solid #f0f0f0; 14 | } 15 | } 16 | .r { 17 | border: solid 1px #f0f0f0; 18 | margin-left: 16px; 19 | flex: auto; 20 | .searchBox { 21 | padding: 8px; 22 | & > ul { 23 | display: flex; 24 | & > li + li { 25 | margin-left: 4px; 26 | flex: none; 27 | } 28 | } 29 | } 30 | .ant-table-pagination.ant-pagination { 31 | margin-right: 10px; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/System/MenuAdmin/index.tsx: -------------------------------------------------------------------------------- 1 | /** 菜单管理页 **/ 2 | 3 | // ================== 4 | // 第三方库 5 | // ================== 6 | import React, { useState, useCallback, useMemo } from "react"; 7 | import { useSetState, useMount } from "react-use"; 8 | import { useSelector, useDispatch } from "react-redux"; 9 | import { 10 | Tree, 11 | Button, 12 | Table, 13 | Tooltip, 14 | Popconfirm, 15 | Modal, 16 | Form, 17 | Select, 18 | Input, 19 | InputNumber, 20 | message, 21 | Divider, 22 | } from "antd"; 23 | import { 24 | EyeOutlined, 25 | ToolOutlined, 26 | DeleteOutlined, 27 | PlusCircleOutlined, 28 | } from "@ant-design/icons"; 29 | import { cloneDeep } from "lodash"; 30 | 31 | // ================== 32 | // 组件 33 | // ================== 34 | import { IconsData } from "@/util/json"; 35 | import Icon from "@/components/Icon"; 36 | 37 | const { Option } = Select; 38 | const { TextArea } = Input; 39 | 40 | const formItemLayout = { 41 | labelCol: { 42 | xs: { span: 24 }, 43 | sm: { span: 4 }, 44 | }, 45 | wrapperCol: { 46 | xs: { span: 24 }, 47 | sm: { span: 19 }, 48 | }, 49 | }; 50 | 51 | // ================== 52 | // 类型声明 53 | // ================== 54 | import { 55 | TableRecordData, 56 | Menu, 57 | ModalType, 58 | operateType, 59 | MenuParam, 60 | TreeSourceData, 61 | } from "./index.type"; 62 | import { RootState, Dispatch } from "@/store"; 63 | import type { EventDataNode, DataNode } from "rc-tree/lib/interface"; 64 | 65 | // ================== 66 | // CSS 67 | // ================== 68 | import "./index.less"; 69 | 70 | // ================== 71 | // 本组件 72 | // ================== 73 | function MenuAdminContainer() { 74 | const p = useSelector((state: RootState) => state.app.powersCode); 75 | const dispatch = useDispatch(); 76 | 77 | const [form] = Form.useForm(); 78 | const [data, setData] = useState([]); // 所有的菜单数据(未分层级) 79 | const [loading, setLoading] = useState(false); // 数据是否正在加载中 80 | 81 | // 模态框相关参数控制 82 | const [modal, setModal] = useSetState({ 83 | operateType: "add", 84 | nowData: null, 85 | modalShow: false, 86 | modalLoading: false, 87 | }); 88 | 89 | const [treeSelect, setTreeSelect] = useState<{ title?: string; id?: number }>( 90 | {} 91 | ); 92 | 93 | // 生命周期 - 首次加载组件时触发 94 | useMount(() => { 95 | getData(); 96 | }); 97 | 98 | // 获取本页面所需数据 99 | const getData = async () => { 100 | if (!p.includes("menu:query")) { 101 | return; 102 | } 103 | setLoading(true); 104 | try { 105 | const res = await dispatch.sys.getMenus(); 106 | if (res && res.status === 200) { 107 | setData(res.data); 108 | } 109 | } finally { 110 | setLoading(false); 111 | } 112 | }; 113 | 114 | /** 工具 - 递归将扁平数据转换为层级数据 **/ 115 | const dataToJson = useCallback( 116 | (one: TreeSourceData | null, data: TreeSourceData[]) => { 117 | let kids: TreeSourceData[]; 118 | if (!one) { 119 | // 第1次递归 120 | kids = data.filter((item: TreeSourceData) => !item.parent); 121 | } else { 122 | kids = data.filter((item: TreeSourceData) => item.parent === one.id); 123 | } 124 | kids.forEach( 125 | (item: TreeSourceData) => (item.children = dataToJson(item, data)) 126 | ); 127 | return kids.length ? kids : undefined; 128 | }, 129 | [] 130 | ); 131 | 132 | // 工具 - 赋值Key 133 | const makeKey = useCallback((data: Menu[]) => { 134 | const newData: TreeSourceData[] = []; 135 | for (let i = 0; i < data.length; i++) { 136 | const item: any = { ...data[i] }; 137 | if (item.children) { 138 | item.children = makeKey(item.children); 139 | } 140 | const treeItem: TreeSourceData = { 141 | ...(item as TreeSourceData), 142 | key: item.id, 143 | }; 144 | newData.push(treeItem); 145 | } 146 | return newData; 147 | }, []); 148 | 149 | /** 点击树目录时触发 **/ 150 | const onTreeSelect = useCallback( 151 | ( 152 | keys: React.Key[], 153 | info: { 154 | event: "select"; 155 | selected: boolean; 156 | node: EventDataNode & { id: number; title: string }; 157 | selectedNodes: DataNode[]; 158 | nativeEvent: MouseEvent; 159 | } 160 | ) => { 161 | let treeSelect = {}; 162 | if (info.selected) { 163 | // 选中 164 | treeSelect = { title: info.node.title, id: info.node.id }; 165 | } 166 | setTreeSelect(treeSelect); 167 | }, 168 | [] 169 | ); 170 | 171 | /** 工具 - 根据parentID返回parentName **/ 172 | const getNameByParentId = (id: number | null) => { 173 | const p = data.find((item) => item.id === id); 174 | return p ? p.title : undefined; 175 | }; 176 | 177 | /** 新增&修改 模态框出现 **/ 178 | const onModalShow = (data: TableRecordData | null, type: operateType) => { 179 | setModal({ 180 | modalShow: true, 181 | nowData: data, 182 | operateType: type, 183 | }); 184 | 185 | setTimeout(() => { 186 | if (type === "add") { 187 | form.resetFields(); 188 | } else { 189 | if (data) { 190 | form.setFieldsValue({ 191 | formConditions: data.conditions, 192 | formDesc: data.desc, 193 | formIcon: data.icon, 194 | formSorts: data.sorts, 195 | formTitle: data.title, 196 | formUrl: data.url, 197 | }); 198 | } 199 | } 200 | }); 201 | }; 202 | 203 | /** 新增&修改 模态框关闭 **/ 204 | const onClose = () => { 205 | setModal({ 206 | modalShow: false, 207 | }); 208 | }; 209 | 210 | /** 新增&修改 提交 **/ 211 | const onOk = async () => { 212 | if (modal.operateType === "see") { 213 | onClose(); 214 | return; 215 | } 216 | try { 217 | const values = await form.validateFields(); 218 | 219 | const params: MenuParam = { 220 | title: values.formTitle, 221 | url: values.formUrl, 222 | icon: values.formIcon, 223 | parent: Number(treeSelect.id) || null, 224 | sorts: values.formSorts, 225 | desc: values.formDesc, 226 | conditions: values.formConditions, 227 | }; 228 | setModal({ 229 | modalLoading: true, 230 | }); 231 | if (modal.operateType === "add") { 232 | try { 233 | const res = await dispatch.sys.addMenu(params); 234 | if (res && res.status === 200) { 235 | message.success("添加成功"); 236 | getData(); 237 | onClose(); 238 | dispatch.app.updateUserInfo(null); 239 | } else { 240 | message.error("添加失败"); 241 | } 242 | } finally { 243 | setModal({ 244 | modalLoading: false, 245 | }); 246 | } 247 | } else { 248 | try { 249 | params.id = modal?.nowData?.id; 250 | const res = await dispatch.sys.upMenu(params); 251 | if (res && res.status === 200) { 252 | message.success("修改成功"); 253 | getData(); 254 | onClose(); 255 | dispatch.app.updateUserInfo(null); 256 | } else { 257 | message.error("修改失败"); 258 | } 259 | } finally { 260 | setModal({ 261 | modalLoading: false, 262 | }); 263 | } 264 | } 265 | } catch { 266 | // 未通过校验 267 | } 268 | }; 269 | 270 | /** 删除一条数据 **/ 271 | const onDel = async (record: TableRecordData) => { 272 | const params = { id: record.id }; 273 | const res = await dispatch.sys.delMenu(params); 274 | if (res && res.status === 200) { 275 | getData(); 276 | dispatch.app.updateUserInfo(null); 277 | message.success("删除成功"); 278 | } else { 279 | message.error(res?.message ?? "操作失败"); 280 | } 281 | }; 282 | 283 | // ================== 284 | // 属性 和 memo 285 | // ================== 286 | 287 | /** 处理原始数据,将原始数据处理为层级关系 **/ 288 | const sourceData = useMemo(() => { 289 | const menuData: Menu[] = cloneDeep(data); 290 | // 这应该递归,把children数据也赋值key 291 | const d: TreeSourceData[] = makeKey(menuData); 292 | 293 | // 按照sort排序 294 | d.sort((a, b) => { 295 | return a.sorts - b.sorts; 296 | }); 297 | return dataToJson(null, d) || []; 298 | }, [data, dataToJson]); 299 | 300 | /** 构建表格字段 **/ 301 | const tableColumns = [ 302 | { 303 | title: "序号", 304 | dataIndex: "serial", 305 | key: "serial", 306 | }, 307 | { 308 | title: "图标", 309 | dataIndex: "icon", 310 | key: "icon", 311 | render: (v: string | null) => { 312 | return v ? : ""; 313 | }, 314 | }, 315 | { 316 | title: "菜单名称", 317 | dataIndex: "title", 318 | key: "title", 319 | }, 320 | { 321 | title: "路径", 322 | dataIndex: "url", 323 | key: "url", 324 | render: (v: string | null) => { 325 | return v ? `/${v.replace(/^\//, "")}` : ""; 326 | }, 327 | }, 328 | { 329 | title: "描述", 330 | dataIndex: "desc", 331 | key: "desc", 332 | }, 333 | { 334 | title: "父级", 335 | dataIndex: "parent", 336 | key: "parent", 337 | render: (v: number | null) => getNameByParentId(v), 338 | }, 339 | { 340 | title: "状态", 341 | dataIndex: "conditions", 342 | key: "conditions", 343 | render: (v: number) => 344 | v === 1 ? ( 345 | 启用 346 | ) : ( 347 | 禁用 348 | ), 349 | }, 350 | { 351 | title: "操作", 352 | key: "control", 353 | width: 120, 354 | render: (v: number, record: TableRecordData) => { 355 | const controls = []; 356 | 357 | p.includes("menu:query") && 358 | controls.push( 359 | onModalShow(record, "see")} 363 | > 364 | 365 | 366 | 367 | 368 | ); 369 | p.includes("menu:up") && 370 | controls.push( 371 | onModalShow(record, "up")} 375 | > 376 | 377 | 378 | 379 | 380 | ); 381 | p.includes("menu:del") && 382 | controls.push( 383 | onDel(record)} 389 | > 390 | 391 | 392 | 393 | 394 | 395 | 396 | ); 397 | const result: JSX.Element[] = []; 398 | controls.forEach((item, index) => { 399 | if (index) { 400 | result.push(); 401 | } 402 | result.push(item); 403 | }); 404 | return result; 405 | }, 406 | }, 407 | ]; 408 | 409 | /** 构建表格数据 **/ 410 | const tableData = useMemo(() => { 411 | return data 412 | .filter((item: Menu) => item.parent === (Number(treeSelect.id) || null)) 413 | .map((item, index) => { 414 | return { 415 | key: index, 416 | id: item.id, 417 | icon: item.icon, 418 | parent: item.parent, 419 | title: item.title, 420 | url: item.url, 421 | desc: item.desc, 422 | sorts: item.sorts, 423 | conditions: item.conditions, 424 | serial: index + 1, 425 | control: item.id, 426 | }; 427 | }); 428 | }, [data, treeSelect.id]); 429 | 430 | return ( 431 |
432 |
433 |
目录结构
434 |
435 | 436 |
437 |
438 |
439 |
440 |
    441 |
  • 442 | 450 |
  • 451 |
452 |
453 |
`共 ${total} 条数据`, 461 | }} 462 | /> 463 | 464 | 465 | 472 |
473 | 482 | 486 | 487 | 493 | 497 | 498 | 499 | 511 | 512 | 518 |