├── .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/) · 
2 |
3 | 标准后台管理系统解决方案
4 | 动态菜单配置,权限精确到按钮
5 |
6 | ## what's this?
7 |
8 | react+redux 后台管理系统脚手架
9 | react+redux+vite+antd
10 |
11 |
12 | 非服务端渲染
13 | 仿antd-pro外观,但没有使用dva和roadhog
14 |
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/) · 
2 |
3 | 标准后台管理系统解决方案
4 | 动态菜单配置,权限精确到按钮
5 |
6 | ## what's this?
7 |
8 | react+redux 后台管理系统脚手架
9 | react+redux+vite+antd
10 |
11 |
12 | 非服务端渲染
13 | 仿antd-pro外观,但没有使用dva和roadhog
14 |
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 |
23 | 返回首页
24 |
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 |
22 | 返回首页
23 |
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 |
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 | }
445 | onClick={() => onModalShow(null, "add")}
446 | disabled={!p.includes("menu:add")}
447 | >
448 | {`添加${treeSelect.title || "根级"}子菜单`}
449 |
450 |
451 |
452 |
453 |
`共 ${total} 条数据`,
461 | }}
462 | />
463 |
464 |
465 |
472 |
482 |
486 |
487 |
493 |
497 |
498 |
499 |
503 | {IconsData.map((item, index) => {
504 | return (
505 |
506 |
507 |
508 | );
509 | })}
510 |
511 |
512 |
518 |
523 |
524 |
530 |
536 |
537 |
543 |
544 |
545 | 启用
546 |
547 |
548 | 禁用
549 |
550 |
551 |
552 | {modal.operateType === "add" ? (
553 |
554 |
555 | 新增菜单后请前往角色管理将菜单授权给相关角色
556 |
557 |
558 | ) : null}
559 |
560 |
561 |
562 | );
563 | }
564 |
565 | export default MenuAdminContainer;
566 |
--------------------------------------------------------------------------------
/src/pages/System/MenuAdmin/index.type.ts:
--------------------------------------------------------------------------------
1 | /** 当前页面所需所有类型声明 **/
2 |
3 | import { Menu } from "@/models/index.type";
4 | export type { Menu, MenuParam } from "@/models/index.type";
5 |
6 | // 构建table所需数据
7 | export interface TableRecordData extends Menu {
8 | key: number;
9 | serial: number;
10 | control: number;
11 | }
12 | export type operateType = "add" | "see" | "up";
13 |
14 | export type ModalType = {
15 | operateType: operateType;
16 | nowData: TableRecordData | null;
17 | modalShow: boolean;
18 | modalLoading: boolean;
19 | };
20 |
21 | export interface TreeSourceData {
22 | id: number; // ID,添加时可以没有id
23 | key: string | number;
24 | title: string; // 标题
25 | icon: string; // 图标
26 | url: string; // 链接路径
27 | parent: number | null; // 父级ID
28 | desc: string; // 描述
29 | sorts: number; // 排序编号
30 | conditions: number; // 状态,1启用,-1禁用
31 | children?: TreeSourceData[]; // 子菜单
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/System/PowerAdmin/index.less:
--------------------------------------------------------------------------------
1 | .page-power-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/PowerAdmin/index.tsx:
--------------------------------------------------------------------------------
1 | /** 权限管理页 **/
2 |
3 | // ==================
4 | // 第三方库
5 | // ==================
6 | import React, { useState, useCallback, useMemo } from "react";
7 |
8 | import { useSetState, useMount } from "react-use";
9 | import { useSelector, useDispatch } from "react-redux";
10 | import {
11 | Tree,
12 | Button,
13 | Table,
14 | Tooltip,
15 | Popconfirm,
16 | Modal,
17 | Form,
18 | Select,
19 | Input,
20 | InputNumber,
21 | message,
22 | Divider,
23 | Checkbox,
24 | } from "antd";
25 | import {
26 | EyeOutlined,
27 | ToolOutlined,
28 | DeleteOutlined,
29 | PlusCircleOutlined,
30 | } from "@ant-design/icons";
31 | import { cloneDeep } from "lodash";
32 |
33 | // ==================
34 | // 自定义的东西
35 | // ==================
36 | const { Option } = Select;
37 | const { TextArea } = Input;
38 |
39 | const formItemLayout = {
40 | labelCol: {
41 | xs: { span: 24 },
42 | sm: { span: 4 },
43 | },
44 | wrapperCol: {
45 | xs: { span: 24 },
46 | sm: { span: 19 },
47 | },
48 | };
49 |
50 | // ==================
51 | // 类型声明
52 | // ==================
53 | import {
54 | TableRecordData,
55 | ModalType,
56 | operateType,
57 | Menu,
58 | Power,
59 | PowerParam,
60 | Res,
61 | TreeSourceData,
62 | } from "./index.type";
63 | import { RootState, Dispatch } from "@/store";
64 | import { CheckboxValueType } from "antd/lib/checkbox/Group";
65 | import type { EventDataNode, DataNode } from "rc-tree/lib/interface";
66 |
67 | // ==================
68 | // CSS
69 | // ==================
70 | import "./index.less";
71 |
72 | // ==================
73 | // 本组件
74 | // ==================
75 | function PowerAdminContainer() {
76 | const dispatch = useDispatch();
77 | const p = useSelector((state: RootState) => state.app.powersCode);
78 | const roles = useSelector((state: RootState) => state.sys.roles);
79 | const userinfo = useSelector((state: RootState) => state.app.userinfo);
80 |
81 | const [form] = Form.useForm();
82 | const [data, setData] = useState([]); // 当前所选菜单下的权限数据
83 | const [loading, setLoading] = useState(false); // 数据是否正在加载中
84 |
85 | // 模态框相关参数控制
86 | const [modal, setModal] = useSetState({
87 | operateType: "add",
88 | nowData: null,
89 | modalShow: false,
90 | modalLoading: false,
91 | });
92 | const [rolesCheckboxChose, setRolesCheckboxChose] = useState([]); // 表单 - 赋予项选中的值
93 |
94 | // 左侧菜单树相关参数 当前Menu树被选中的节点数据
95 | const [treeSelect, setTreeSelect] = useState<{ title?: string; id?: number }>(
96 | {}
97 | );
98 |
99 | // 生命周期 - 首次加载组件时触发
100 | useMount(() => {
101 | if (userinfo.menus.length === 0) {
102 | dispatch.sys.getMenus();
103 | }
104 | dispatch.sys.getAllRoles();
105 | getData();
106 | });
107 |
108 | // 根据所选菜单id获取其下权限数据
109 | const getData = async (menuId: string | number | null = null) => {
110 | if (!p.includes("power:query")) {
111 | return;
112 | }
113 |
114 | setLoading(true);
115 | const params = {
116 | menuId: Number(menuId) || null,
117 | };
118 |
119 | try {
120 | const res: Res = await dispatch.sys.getPowerDataByMenuId(params);
121 |
122 | if (res && res.status === 200) {
123 | setData(res.data);
124 | }
125 | } finally {
126 | setLoading(false);
127 | }
128 | };
129 |
130 | // 工具 - 递归将扁平数据转换为层级数据
131 | const dataToJson = useCallback(
132 | (one: TreeSourceData | null, data: TreeSourceData[]) => {
133 | let kids: TreeSourceData[];
134 | if (!one) {
135 | // 第1次递归
136 | kids = data.filter((item: TreeSourceData) => !item.parent);
137 | } else {
138 | kids = data.filter((item: TreeSourceData) => item.parent === one.id);
139 | }
140 | kids.forEach(
141 | (item: TreeSourceData) => (item.children = dataToJson(item, data))
142 | );
143 | return kids.length ? kids : undefined;
144 | },
145 | []
146 | );
147 |
148 | // 工具 - 赋值Key
149 | const makeKey = useCallback((data: Menu[]) => {
150 | const newData: TreeSourceData[] = [];
151 | for (let i = 0; i < data.length; i++) {
152 | const item: any = { ...data[i] };
153 | if (item.children) {
154 | item.children = makeKey(item.children);
155 | }
156 | const treeItem: TreeSourceData = {
157 | ...(item as TreeSourceData),
158 | key: item.id,
159 | };
160 | newData.push(treeItem);
161 | }
162 | return newData;
163 | }, []);
164 |
165 | // 点击树目录时触发
166 | const onTreeSelect = (
167 | keys: React.Key[],
168 | info: {
169 | event: "select";
170 | selected: boolean;
171 | node: EventDataNode & { id: number; title: string };
172 | selectedNodes: DataNode[];
173 | nativeEvent: MouseEvent;
174 | }
175 | ) => {
176 | if (info.selected) {
177 | // 选中时才触发
178 | getData(keys[0]);
179 | setTreeSelect({
180 | title: info.node.title,
181 | id: info.node.id,
182 | });
183 | } else {
184 | setTreeSelect({});
185 | setData([]);
186 | }
187 | };
188 |
189 | // 新增&修改 模态框出现
190 | const onModalShow = (data: TableRecordData | null, type: operateType) => {
191 | setModal({
192 | modalShow: true,
193 | nowData: data,
194 | operateType: type,
195 | });
196 | setRolesCheckboxChose(
197 | data && data.id
198 | ? roles
199 | .filter((item) => {
200 | const theMenuPower = item.menuAndPowers?.find(
201 | (item2) => item2.menuId === data.menu
202 | );
203 | if (theMenuPower) {
204 | return theMenuPower.powers.includes(data.id);
205 | }
206 | return false;
207 | })
208 | .map((item) => item.id)
209 | : []
210 | );
211 | setTimeout(() => {
212 | if (type === "add") {
213 | // 新增,需重置表单各控件的值
214 | form.resetFields();
215 | } else {
216 | // 查看或修改,需设置表单各控件的值为当前所选中行的数据
217 | form.setFieldsValue({
218 | formConditions: data?.conditions,
219 | formDesc: data?.desc,
220 | formCode: data?.code,
221 | formSorts: data?.sorts,
222 | formTitle: data?.title,
223 | });
224 | }
225 | });
226 | };
227 |
228 | // 新增&修改 模态框关闭
229 | const onClose = () => {
230 | setModal({
231 | modalShow: false,
232 | });
233 | };
234 |
235 | // 新增&修改 提交
236 | const onOk = async () => {
237 | if (modal.operateType === "see") {
238 | onClose();
239 | return;
240 | }
241 |
242 | try {
243 | const values = await form.validateFields();
244 | const params: PowerParam = {
245 | title: values.formTitle,
246 | code: values.formCode,
247 | menu: treeSelect.id || 0,
248 | sorts: values.formSorts,
249 | desc: values.formDesc,
250 | conditions: values.formConditions,
251 | };
252 | setModal({
253 | modalLoading: true,
254 | });
255 | if (modal.operateType === "add") {
256 | // 新增
257 | try {
258 | const res: Res = await dispatch.sys.addPower(params);
259 | if (res && res.status === 200) {
260 | message.success("添加成功");
261 | getData(treeSelect.id);
262 | onClose();
263 |
264 | await setPowersByRoleIds(res.data.id, rolesCheckboxChose);
265 | dispatch.app.updateUserInfo(null);
266 | dispatch.sys.getAllRoles();
267 | } else {
268 | message.error("添加失败");
269 | }
270 | } finally {
271 | setModal({
272 | modalLoading: false,
273 | });
274 | }
275 | } else {
276 | // 修改
277 | try {
278 | if (!modal?.nowData?.id) {
279 | message.error("该数据没有ID");
280 | return;
281 | }
282 | params.id = modal.nowData.id;
283 |
284 | const res: Res = await dispatch.sys.upPower(params);
285 | if (res && res.status === 200) {
286 | message.success("修改成功");
287 | getData(treeSelect.id);
288 | onClose();
289 |
290 | await setPowersByRoleIds(params.id, rolesCheckboxChose);
291 | dispatch.sys.getAllRoles();
292 | dispatch.app.updateUserInfo(null);
293 | } else {
294 | message.error("修改失败");
295 | }
296 | } finally {
297 | setModal({
298 | modalLoading: false,
299 | });
300 | }
301 | }
302 | } catch {
303 | // 未通过校验
304 | }
305 | };
306 |
307 | // 删除一条数据
308 | const onDel = async (record: TableRecordData) => {
309 | const params = { id: record.id };
310 | setLoading(true);
311 | const res = await dispatch.sys.delPower(params);
312 | if (res && res.status === 200) {
313 | getData(treeSelect.id);
314 | dispatch.app.updateUserInfo(null);
315 | message.success("删除成功");
316 | } else {
317 | message.error(res?.message ?? "操作失败");
318 | }
319 | };
320 |
321 | /**
322 | * 批量更新roles
323 | * @param id 当前这个权限的id
324 | * @param roleIds 选中的角色的id们,要把当前权限赋给这些角色
325 | * **/
326 | const setPowersByRoleIds = (id: number, roleIds: number[]) => {
327 | const params = roles.map((item) => {
328 | const powersTemp = new Set(
329 | item.menuAndPowers.reduce((a, b) => [...a, ...b.powers], [] as number[])
330 | );
331 | if (roleIds.includes(item.id)) {
332 | powersTemp.add(id);
333 | } else {
334 | powersTemp.delete(id);
335 | }
336 | return {
337 | id: item.id,
338 | menus: item.menuAndPowers.map((item) => item.menuId),
339 | powers: Array.from(powersTemp),
340 | };
341 | });
342 | dispatch.sys.setPowersByRoleIds(params);
343 | };
344 |
345 | // ==================
346 | // 属性 和 memo
347 | // ==================
348 |
349 | // 处理原始数据,将原始数据处理为层级关系
350 | const sourceData: TreeSourceData[] = useMemo(() => {
351 | const menuData: Menu[] = cloneDeep(userinfo.menus);
352 |
353 | // 这应该递归,把children数据也赋值key
354 | const d: TreeSourceData[] = makeKey(menuData);
355 |
356 | // 按照sort排序
357 | d.sort((a, b) => {
358 | return a.sorts - b.sorts;
359 | });
360 | return dataToJson(null, d) || ([] as TreeSourceData[]);
361 | }, [userinfo.menus, dataToJson]);
362 |
363 | // 构建表格字段
364 | const tableColumns = [
365 | {
366 | title: "序号",
367 | dataIndex: "serial",
368 | key: "serial",
369 | },
370 | {
371 | title: "权限名称",
372 | dataIndex: "title",
373 | key: "title",
374 | },
375 | {
376 | title: "Code",
377 | dataIndex: "code",
378 | key: "code",
379 | },
380 | {
381 | title: "描述",
382 | dataIndex: "desc",
383 | key: "desc",
384 | },
385 | {
386 | title: "状态",
387 | dataIndex: "conditions",
388 | key: "conditions",
389 | render: (v: number) =>
390 | v === 1 ? (
391 | 启用
392 | ) : (
393 | 禁用
394 | ),
395 | },
396 | {
397 | title: "操作",
398 | key: "control",
399 | width: 120,
400 | render: (v: number, record: TableRecordData) => {
401 | const controls = [];
402 | p.includes("power:query") &&
403 | controls.push(
404 | onModalShow(record, "see")}
408 | >
409 |
410 |
411 |
412 |
413 | );
414 | p.includes("power:up") &&
415 | controls.push(
416 | onModalShow(record, "up")}
420 | >
421 |
422 |
423 |
424 |
425 | );
426 | p.includes("power:del") &&
427 | controls.push(
428 | onDel(record)}
434 | >
435 |
436 |
437 |
438 |
439 |
440 |
441 | );
442 | const result: JSX.Element[] = [];
443 | controls.forEach((item, index) => {
444 | if (index) {
445 | result.push( );
446 | }
447 | result.push(item);
448 | });
449 | return result;
450 | },
451 | },
452 | ];
453 |
454 | // 构建表格数据
455 | const tableData = useMemo(() => {
456 | return data.map((item, index) => {
457 | return {
458 | key: index,
459 | id: item.id,
460 | menu: item.menu,
461 | title: item.title,
462 | code: item.code,
463 | desc: item.desc,
464 | sorts: item.sorts,
465 | conditions: item.conditions,
466 | serial: index + 1,
467 | control: item.id,
468 | };
469 | });
470 | }, [data]);
471 |
472 | // 新增或修改时 构建‘赋予’项数据
473 | const rolesCheckboxData = useMemo(() => {
474 | return roles.map((item) => ({
475 | label: item.title,
476 | value: item.id,
477 | }));
478 | }, [roles]);
479 |
480 | return (
481 |
482 |
483 |
目录结构
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 | }
495 | onClick={() => onModalShow(null, "add")}
496 | disabled={!(treeSelect.id && p.includes("power:add"))}
497 | >
498 | {`添加${treeSelect.title || ""}权限`}
499 |
500 |
501 |
502 |
503 |
`共 ${total} 条数据`,
511 | }}
512 | />
513 |
514 | {/** 查看&新增&修改用户模态框 **/}
515 | ${modal.nowData?.title ?? ""}`}
519 | open={modal.modalShow}
520 | onOk={onOk}
521 | onCancel={onClose}
522 | confirmLoading={modal.modalLoading}
523 | >
524 |
534 |
538 |
539 |
548 |
552 |
553 |
559 |
564 |
565 |
571 |
577 |
578 |
584 |
585 |
586 | 启用
587 |
588 |
589 | 禁用
590 |
591 |
592 |
593 |
594 |
599 | setRolesCheckboxChose(v as number[])
600 | }
601 | />
602 |
603 |
604 |
605 |
606 | );
607 | }
608 |
609 | export default PowerAdminContainer;
610 |
--------------------------------------------------------------------------------
/src/pages/System/PowerAdmin/index.type.ts:
--------------------------------------------------------------------------------
1 | /** 当前页面所需所有类型声明 **/
2 |
3 | import { PowerTreeDefault } from "@/components/TreeChose/PowerTreeTable";
4 | import { Power } from "@/models/index.type";
5 |
6 | export type {
7 | Menu,
8 | UserInfo,
9 | Role,
10 | Power,
11 | PowerParam,
12 | Res,
13 | } from "@/models/index.type";
14 |
15 | // 构建table所需数据
16 | export type TableRecordData = Power & {
17 | key: number;
18 | serial: number;
19 | control: number;
20 | };
21 | export type operateType = "add" | "see" | "up";
22 | export type ModalType = {
23 | operateType: operateType;
24 | nowData: TableRecordData | null;
25 | modalShow: boolean;
26 | modalLoading: boolean;
27 | };
28 | export type PowerTreeInfo = {
29 | treeOnOkLoading: boolean; // 是否正在分配权限
30 | powerTreeShow: boolean; // 权限树是否显示
31 | // 树默认需要选中的项
32 | powerTreeDefault: PowerTreeDefault;
33 | };
34 | export type SearchInfo = {
35 | title: string | undefined; // 用户名
36 | conditions: number | undefined; // 状态
37 | };
38 |
39 | export interface TreeSourceData {
40 | id: number; // ID,添加时可以没有id
41 | key: string | number;
42 | title: string; // 标题
43 | icon: string; // 图标
44 | url: string; // 链接路径
45 | parent: number | null; // 父级ID
46 | desc: string; // 描述
47 | sorts: number; // 排序编号
48 | conditions: number; // 状态,1启用,-1禁用
49 | children?: TreeSourceData[]; // 子菜单
50 | }
51 |
--------------------------------------------------------------------------------
/src/pages/System/RoleAdmin/index.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/pages/System/RoleAdmin/index.less
--------------------------------------------------------------------------------
/src/pages/System/RoleAdmin/index.tsx:
--------------------------------------------------------------------------------
1 | /** Role 系统管理/角色管理 **/
2 |
3 | // ==================
4 | // 第三方库
5 | // ==================
6 | import React, { useState, useMemo } from "react";
7 | import { useSelector, useDispatch } from "react-redux";
8 | import { useSetState, useMount } from "react-use";
9 | import {
10 | Form,
11 | Button,
12 | Input,
13 | Table,
14 | message,
15 | Popconfirm,
16 | Modal,
17 | Tooltip,
18 | Divider,
19 | Select,
20 | InputNumber,
21 | } from "antd";
22 | import {
23 | EyeOutlined,
24 | EditOutlined,
25 | ToolOutlined,
26 | DeleteOutlined,
27 | PlusCircleOutlined,
28 | SearchOutlined,
29 | } from "@ant-design/icons";
30 |
31 | // ==================
32 | // 自定义的东西
33 | // ==================
34 | import tools from "@/util/tools"; // 工具
35 |
36 | // ==================
37 | // 所需的组件
38 | // ==================
39 | import PowerTreeCom from "@/components/TreeChose/PowerTreeTable";
40 |
41 | const { TextArea } = Input;
42 | const { Option } = Select;
43 | const formItemLayout = {
44 | labelCol: {
45 | xs: { span: 24 },
46 | sm: { span: 4 },
47 | },
48 | wrapperCol: {
49 | xs: { span: 24 },
50 | sm: { span: 19 },
51 | },
52 | };
53 |
54 | // ==================
55 | // 类型声明
56 | // ==================
57 | import { RootState, Dispatch } from "@/store";
58 | import { PowerTreeDefault } from "@/components/TreeChose/PowerTreeTable";
59 | import {
60 | Page,
61 | TableRecordData,
62 | operateType,
63 | ModalType,
64 | PowerTreeInfo,
65 | SearchInfo,
66 | RoleParam,
67 | Role,
68 | Res,
69 | } from "./index.type";
70 |
71 | // ==================
72 | // CSS
73 | // ==================
74 | import "./index.less";
75 |
76 | // ==================
77 | // 本组件
78 | // ==================
79 | function RoleAdminContainer() {
80 | const dispatch = useDispatch();
81 | const p = useSelector((state: RootState) => state.app.powersCode);
82 | const powerTreeData = useSelector(
83 | (state: RootState) => state.sys.powerTreeData
84 | );
85 |
86 | const [form] = Form.useForm();
87 | const [data, setData] = useState([]); // 当前页面列表数据
88 | const [loading, setLoading] = useState(false); // 数据是否正在加载中
89 |
90 | // 分页相关参数控制
91 | const [page, setPage] = useSetState({
92 | pageNum: 1,
93 | pageSize: 10,
94 | total: 0,
95 | });
96 |
97 | // 模态框相关参数控制
98 | const [modal, setModal] = useSetState({
99 | operateType: "add",
100 | nowData: null,
101 | modalShow: false,
102 | modalLoading: false,
103 | });
104 |
105 | // 搜索相关参数
106 | const [searchInfo, setSearchInfo] = useSetState({
107 | title: undefined, // 角色名
108 | conditions: undefined, // 状态
109 | });
110 |
111 | // 权限树相关参数
112 | const [power, setPower] = useSetState({
113 | treeOnOkLoading: false,
114 | powerTreeShow: false,
115 | powerTreeDefault: { menus: [], powers: [] },
116 | });
117 |
118 | // 生命周期 - 首次加载组件时触发
119 | useMount(() => {
120 | getData(page);
121 | getPowerTreeData();
122 | });
123 |
124 | // 函数 - 获取所有的菜单权限数据,用于分配权限控件的原始数据
125 | const getPowerTreeData = () => {
126 | dispatch.sys.getAllMenusAndPowers();
127 | };
128 |
129 | // 函数- 查询当前页面所需列表数据
130 | const getData = async (page: { pageNum: number; pageSize: number }) => {
131 | if (!p.includes("role:query")) {
132 | return;
133 | }
134 | const params = {
135 | pageNum: page.pageNum,
136 | pageSize: page.pageSize,
137 | title: searchInfo.title,
138 | conditions: searchInfo.conditions,
139 | };
140 | setLoading(true);
141 | try {
142 | const res: Res = await dispatch.sys.getRoles(tools.clearNull(params));
143 | if (res && res.status === 200) {
144 | setData(res.data.list);
145 | setPage({
146 | total: res.data.total,
147 | pageNum: page.pageNum,
148 | pageSize: page.pageSize,
149 | });
150 | } else {
151 | message.error(res?.message ?? "获取失败");
152 | }
153 | } finally {
154 | setLoading(false);
155 | }
156 | };
157 |
158 | // 搜索 - 名称输入框值改变时触发
159 | const searchTitleChange = (e: React.ChangeEvent) => {
160 | if (e.target.value.length < 20) {
161 | setSearchInfo({ title: e.target.value });
162 | }
163 | };
164 |
165 | // 搜索 - 状态下拉框选择时触发
166 | const searchConditionsChange = (v: number) => {
167 | setSearchInfo({ conditions: v });
168 | };
169 |
170 | // 搜索
171 | const onSearch = () => {
172 | getData(page);
173 | };
174 |
175 | /**
176 | * 添加/修改/查看 模态框出现
177 | * @param data 当前选中的那条数据
178 | * @param type add添加/up修改/see查看
179 | * **/
180 | const onModalShow = (data: TableRecordData | null, type: operateType) => {
181 | setModal({
182 | modalShow: true,
183 | nowData: data,
184 | operateType: type,
185 | });
186 | setTimeout(() => {
187 | if (type === "add") {
188 | // 新增,需重置表单各控件的值
189 | form.resetFields();
190 | } else {
191 | // 查看或修改,需设置表单各控件的值为当前所选中行的数据
192 | form.setFieldsValue({
193 | formConditions: data?.conditions,
194 | formDesc: data?.desc,
195 | formSorts: data?.sorts,
196 | formTitle: data?.title,
197 | });
198 | }
199 | });
200 | };
201 |
202 | /** 模态框确定 **/
203 | const onOk = async () => {
204 | if (modal.operateType === "see") {
205 | onClose();
206 | return;
207 | }
208 |
209 | try {
210 | const values = await form.validateFields();
211 | setModal({
212 | modalLoading: true,
213 | });
214 | const params: RoleParam = {
215 | title: values.formTitle,
216 | desc: values.formDesc,
217 | sorts: values.formSorts,
218 | conditions: values.formConditions,
219 | };
220 | if (modal.operateType === "add") {
221 | // 新增
222 | try {
223 | const res: Res = await dispatch.sys.addRole(params);
224 | if (res && res.status === 200) {
225 | message.success("添加成功");
226 | getData(page);
227 | dispatch.app.updateUserInfo(null); // 角色信息有变化,立即更新当前用户信息
228 | onClose();
229 | }
230 | } finally {
231 | setModal({
232 | modalLoading: false,
233 | });
234 | }
235 | } else {
236 | // 修改
237 | params.id = modal?.nowData?.id;
238 | try {
239 | const res: Res = await dispatch.sys.upRole(params);
240 | if (res && res.status === 200) {
241 | message.success("修改成功");
242 | getData(page);
243 | dispatch.app.updateUserInfo(null);
244 | onClose();
245 | }
246 | } finally {
247 | setModal({
248 | modalLoading: false,
249 | });
250 | }
251 | }
252 | } catch {
253 | // 未通过校验
254 | }
255 | };
256 |
257 | // 删除某一条数据
258 | const onDel = async (id: number) => {
259 | setLoading(true);
260 | try {
261 | const res = await dispatch.sys.delRole({ id });
262 | if (res && res.status === 200) {
263 | message.success("删除成功");
264 | getData(page);
265 | dispatch.app.updateUserInfo(null);
266 | } else {
267 | message.error(res?.message ?? "操作失败");
268 | }
269 | } finally {
270 | setLoading(false);
271 | }
272 | };
273 |
274 | /** 模态框关闭 **/
275 | const onClose = () => {
276 | setModal({ modalShow: false });
277 | };
278 |
279 | /** 分配权限按钮点击,权限控件出现 **/
280 | const onAllotPowerClick = (record: TableRecordData) => {
281 | const menus = record.menuAndPowers.map((item) => item.menuId); // 需默认选中的菜单项ID
282 | // 需默认选中的权限ID
283 | const powers = record.menuAndPowers.reduce(
284 | (v1, v2) => [...v1, ...v2.powers],
285 | [] as number[]
286 | );
287 | setModal({ nowData: record });
288 | setPower({
289 | powerTreeShow: true,
290 | powerTreeDefault: { menus, powers },
291 | });
292 | };
293 |
294 | // 权限树确定 给角色分配菜单和权限
295 | const onPowerTreeOk = async (arr: PowerTreeDefault) => {
296 | if (!modal?.nowData?.id) {
297 | message.error("该数据没有ID");
298 | return;
299 | }
300 | const params = {
301 | id: modal.nowData.id,
302 | menus: arr.menus,
303 | powers: arr.powers,
304 | };
305 |
306 | setPower({ treeOnOkLoading: true });
307 | try {
308 | const res: Res = await dispatch.sys.setPowersByRoleId(params);
309 | if (res && res.status === 200) {
310 | getData(page);
311 | dispatch.app.updateUserInfo(null);
312 | onPowerTreeClose();
313 | } else {
314 | message.error(res?.message ?? "权限分配失败");
315 | }
316 | } finally {
317 | setPower({ treeOnOkLoading: false });
318 | }
319 | };
320 |
321 | // 关闭菜单树
322 | const onPowerTreeClose = () => {
323 | setPower({
324 | powerTreeShow: false,
325 | });
326 | };
327 |
328 | // 表单页码改变
329 | const onTablePageChange = (pageNum: number, pageSize: number | undefined) => {
330 | getData({ pageNum, pageSize: pageSize || page.pageSize });
331 | };
332 |
333 | // 构建字段
334 | const tableColumns = [
335 | {
336 | title: "序号",
337 | dataIndex: "serial",
338 | key: "serial",
339 | },
340 | {
341 | title: "角色名",
342 | dataIndex: "title",
343 | key: "title",
344 | },
345 | {
346 | title: "描述",
347 | dataIndex: "desc",
348 | key: "desc",
349 | },
350 | {
351 | title: "排序",
352 | dataIndex: "sorts",
353 | key: "sorts",
354 | },
355 | {
356 | title: "状态",
357 | dataIndex: "conditions",
358 | key: "conditions",
359 | render: (v: number) =>
360 | v === 1 ? (
361 | 启用
362 | ) : (
363 | 禁用
364 | ),
365 | },
366 | {
367 | title: "操作",
368 | key: "control",
369 | width: 200,
370 | render: (v: number, record: TableRecordData) => {
371 | const controls = [];
372 | p.includes("role:query") &&
373 | controls.push(
374 | onModalShow(record, "see")}
378 | >
379 |
380 |
381 |
382 |
383 | );
384 | p.includes("role:up") &&
385 | controls.push(
386 | onModalShow(record, "up")}
390 | >
391 |
392 |
393 |
394 |
395 | );
396 | p.includes("role:power") &&
397 | controls.push(
398 | onAllotPowerClick(record)}
402 | >
403 |
404 |
405 |
406 |
407 | );
408 | p.includes("role:del") &&
409 | controls.push(
410 | onDel(record.id)}
414 | okText="确定"
415 | cancelText="取消"
416 | >
417 |
418 |
419 |
420 |
421 |
422 |
423 | );
424 |
425 | const result: JSX.Element[] = [];
426 | controls.forEach((item, index) => {
427 | if (index) {
428 | result.push( );
429 | }
430 | result.push(item);
431 | });
432 | return result;
433 | },
434 | },
435 | ];
436 |
437 | const tableData = useMemo(() => {
438 | return data.map((item, index): TableRecordData => {
439 | return {
440 | key: index,
441 | id: item.id,
442 | serial: index + 1 + (page.pageNum - 1) * page.pageSize,
443 | title: item.title,
444 | desc: item.desc,
445 | sorts: item.sorts,
446 | conditions: item.conditions,
447 | control: item.id,
448 | menuAndPowers: item.menuAndPowers,
449 | };
450 | });
451 | }, [page, data]);
452 |
453 | return (
454 |
455 |
456 |
457 |
458 | }
461 | disabled={!p.includes("role:add")}
462 | onClick={() => onModalShow(null, "add")}
463 | >
464 | 添加角色
465 |
466 |
467 |
468 |
469 | {p.includes("role:query") && (
470 |
500 | )}
501 |
502 |
503 |
`共 ${total} 条数据`,
513 | onChange: (page, pageSize) => onTablePageChange(page, pageSize),
514 | }}
515 | />
516 |
517 | {/* 新增&修改&查看 模态框 */}
518 | onOk()}
522 | onCancel={() => onClose()}
523 | confirmLoading={modal.modalLoading}
524 | >
525 |
540 |
544 |
545 |
551 |
556 |
557 |
563 |
569 |
570 |
576 |
577 |
578 | 启用
579 |
580 |
581 | 禁用
582 |
583 |
584 |
585 |
586 |
587 |
596 |
597 | );
598 | }
599 |
600 | export default RoleAdminContainer;
601 |
--------------------------------------------------------------------------------
/src/pages/System/RoleAdmin/index.type.ts:
--------------------------------------------------------------------------------
1 | /** 当前页面所需所有类型声明 **/
2 |
3 | import { PowerTreeDefault } from "@/components/TreeChose/PowerTreeTable";
4 | import { Role } from "@/models/index.type";
5 | export type { PowerTree, RoleParam, Role, Res } from "@/models/index.type";
6 |
7 | // 分页相关参数控制
8 | export type Page = {
9 | pageNum: number; // 当前页码
10 | pageSize: number; // 每页显示多少条
11 | total: number; // 总共多少条数据
12 | };
13 |
14 | // 构建table所需数据
15 | export type TableRecordData = Role & {
16 | key: number;
17 | serial: number;
18 | control: number;
19 | };
20 |
21 | // 模态框打开的类型 see查看,add添加,up修改
22 | export type operateType = "see" | "add" | "up";
23 |
24 | // 模态框相关参数
25 | export type ModalType = {
26 | operateType: operateType;
27 | nowData: TableRecordData | null;
28 | modalShow: boolean;
29 | modalLoading: boolean;
30 | };
31 |
32 | // 权限树相关参数
33 | export type PowerTreeInfo = {
34 | treeOnOkLoading: boolean; // 是否正在分配权限
35 | powerTreeShow: boolean; // 权限树是否显示
36 | powerTreeDefault: PowerTreeDefault; // 树默认需要选中的项
37 | };
38 |
39 | // 搜索相关参数
40 | export type SearchInfo = {
41 | title: string | undefined; // 用户名
42 | conditions: number | undefined; // 状态
43 | };
44 |
--------------------------------------------------------------------------------
/src/pages/System/UserAdmin/index.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/javaLuo/react-admin/e40eaa88775c775ac83c3762eaef8e894f2a3886/src/pages/System/UserAdmin/index.less
--------------------------------------------------------------------------------
/src/pages/System/UserAdmin/index.tsx:
--------------------------------------------------------------------------------
1 | /** User 系统管理/用户管理 **/
2 |
3 | // ==================
4 | // 所需的第三方库
5 | // ==================
6 | import React, { useState, useMemo } from "react";
7 | import { useSetState, useMount } from "react-use";
8 | import { useSelector, useDispatch } from "react-redux";
9 | import {
10 | Form,
11 | Button,
12 | Input,
13 | Table,
14 | message,
15 | Popconfirm,
16 | Modal,
17 | Tooltip,
18 | Divider,
19 | Select,
20 | } from "antd";
21 | import {
22 | EyeOutlined,
23 | EditOutlined,
24 | ToolOutlined,
25 | DeleteOutlined,
26 | PlusCircleOutlined,
27 | SearchOutlined,
28 | } from "@ant-design/icons";
29 |
30 | // ==================
31 | // 所需的自定义的东西
32 | // ==================
33 | import tools from "@/util/tools"; // 工具函数
34 |
35 | const { TextArea } = Input;
36 | const { Option } = Select;
37 |
38 | const formItemLayout = {
39 | labelCol: {
40 | xs: { span: 24 },
41 | sm: { span: 4 },
42 | },
43 | wrapperCol: {
44 | xs: { span: 24 },
45 | sm: { span: 19 },
46 | },
47 | };
48 |
49 | // ==================
50 | // 所需的组件
51 | // ==================
52 | import RoleTree from "@/components/TreeChose/RoleTree";
53 |
54 | // ==================
55 | // 类型声明
56 | // ==================
57 | import {
58 | TableRecordData,
59 | Page,
60 | operateType,
61 | ModalType,
62 | SearchInfo,
63 | RoleTreeInfo,
64 | UserBasicInfoParam,
65 | Res,
66 | } from "./index.type";
67 | import { RootState, Dispatch } from "@/store";
68 |
69 | // ==================
70 | // CSS
71 | // ==================
72 | import "./index.less";
73 |
74 | // ==================
75 | // 本组件
76 | // ==================
77 | function UserAdminContainer(): JSX.Element {
78 | const dispatch = useDispatch();
79 | const userinfo = useSelector((state: RootState) => state.app.userinfo);
80 | const p = useSelector((state: RootState) => state.app.powersCode);
81 |
82 | const [form] = Form.useForm();
83 | const [data, setData] = useState([]); // 当前页面列表数据
84 | const [loading, setLoading] = useState(false); // 数据是否正在加载中
85 |
86 | // 分页相关参数
87 | const [page, setPage] = useSetState({
88 | pageNum: 1,
89 | pageSize: 10,
90 | total: 0,
91 | });
92 |
93 | // 模态框相关参数
94 | const [modal, setModal] = useSetState({
95 | operateType: "add", // see查看,add添加,up修改
96 | nowData: null,
97 | modalShow: false,
98 | modalLoading: false,
99 | });
100 |
101 | // 搜索相关参数
102 | const [searchInfo, setSearchInfo] = useSetState({
103 | username: undefined, // 用户名
104 | conditions: undefined, // 状态
105 | });
106 |
107 | // 角色树相关参数
108 | const [role, setRole] = useSetState({
109 | roleData: [],
110 | roleTreeLoading: false,
111 | roleTreeShow: false,
112 | roleTreeDefault: [],
113 | });
114 |
115 | // 生命周期 - 组件挂载时触发一次
116 | useMount(() => {
117 | onGetData(page);
118 | getAllRolesData();
119 | });
120 |
121 | // 函数 - 获取所有的角色数据,用于分配角色控件的原始数据
122 | const getAllRolesData = async (): Promise => {
123 | try {
124 | const res = await dispatch.sys.getAllRoles();
125 | if (res && res.status === 200) {
126 | setRole({
127 | roleData: res.data,
128 | });
129 | }
130 | } catch {
131 | //
132 | }
133 | };
134 |
135 | // 函数 - 查询当前页面所需列表数据
136 | async function onGetData(page: {
137 | pageNum: number;
138 | pageSize: number;
139 | }): Promise {
140 | if (!p.includes("user:query")) {
141 | return;
142 | }
143 |
144 | const params = {
145 | pageNum: page.pageNum,
146 | pageSize: page.pageSize,
147 | username: searchInfo.username,
148 | conditions: searchInfo.conditions,
149 | };
150 | setLoading(true);
151 | try {
152 | const res = await dispatch.sys.getUserList(tools.clearNull(params));
153 | if (res && res.status === 200) {
154 | setData(res.data.list);
155 | setPage({
156 | pageNum: page.pageNum,
157 | pageSize: page.pageSize,
158 | total: res.data.total,
159 | });
160 | } else {
161 | message.error(res?.message ?? "数据获取失败");
162 | }
163 | } finally {
164 | setLoading(false);
165 | }
166 | }
167 |
168 | // 搜索 - 名称输入框值改变时触发
169 | const searchUsernameChange = (
170 | e: React.ChangeEvent
171 | ): void => {
172 | if (e.target.value.length < 20) {
173 | setSearchInfo({ username: e.target.value });
174 | }
175 | };
176 |
177 | // 搜索 - 状态下拉框选择时触发
178 | const searchConditionsChange = (v: number): void => {
179 | setSearchInfo({ conditions: v });
180 | };
181 |
182 | // 搜索
183 | const onSearch = (): void => {
184 | onGetData(page);
185 | };
186 |
187 | /**
188 | * 添加/修改/查看 模态框出现
189 | * @param data 当前选中的那条数据
190 | * @param type add添加/up修改/see查看
191 | * **/
192 | const onModalShow = (
193 | data: TableRecordData | null,
194 | type: operateType
195 | ): void => {
196 | setModal({
197 | modalShow: true,
198 | nowData: data,
199 | operateType: type,
200 | });
201 | // 用setTimeout是因为首次让Modal出现时得等它挂载DOM,不然form对象还没来得及挂载到Form上
202 | setTimeout(() => {
203 | if (type === "add") {
204 | // 新增,需重置表单各控件的值
205 | form.resetFields();
206 | } else if (data) {
207 | // 查看或修改,需设置表单各控件的值为当前所选中行的数据
208 | form.setFieldsValue({
209 | ...data,
210 | });
211 | }
212 | });
213 | };
214 |
215 | /** 模态框确定 **/
216 | const onOk = async (): Promise => {
217 | if (modal.operateType === "see") {
218 | onClose();
219 | return;
220 | }
221 | try {
222 | const values = await form.validateFields();
223 | setModal({
224 | modalLoading: true,
225 | });
226 | const params: UserBasicInfoParam = {
227 | username: values.username,
228 | password: values.password,
229 | phone: values.phone,
230 | email: values.email,
231 | desc: values.desc,
232 | conditions: values.conditions,
233 | };
234 | if (modal.operateType === "add") {
235 | // 新增
236 | try {
237 | const res: Res | undefined = await dispatch.sys.addUser(params);
238 | if (res && res.status === 200) {
239 | message.success("添加成功");
240 | onGetData(page);
241 | onClose();
242 | } else {
243 | message.error(res?.message ?? "操作失败");
244 | }
245 | } finally {
246 | setModal({
247 | modalLoading: false,
248 | });
249 | }
250 | } else {
251 | // 修改
252 | params.id = modal.nowData?.id;
253 | try {
254 | const res: Res | undefined = await dispatch.sys.upUser(params);
255 | if (res && res.status === 200) {
256 | message.success("修改成功");
257 | onGetData(page);
258 | onClose();
259 | } else {
260 | message.error(res?.message ?? "操作失败");
261 | }
262 | } finally {
263 | setModal({
264 | modalLoading: false,
265 | });
266 | }
267 | }
268 | } catch {
269 | // 未通过校验
270 | }
271 | };
272 |
273 | // 删除某一条数据
274 | const onDel = async (id: number): Promise => {
275 | setLoading(true);
276 | try {
277 | const res = await dispatch.sys.delUser({ id });
278 | if (res && res.status === 200) {
279 | message.success("删除成功");
280 | onGetData(page);
281 | } else {
282 | message.error(res?.message ?? "操作失败");
283 | }
284 | } finally {
285 | setLoading(false);
286 | }
287 | };
288 |
289 | /** 模态框关闭 **/
290 | const onClose = () => {
291 | setModal({
292 | modalShow: false,
293 | });
294 | };
295 |
296 | /** 分配角色按钮点击,角色控件出现 **/
297 | const onTreeShowClick = (record: TableRecordData): void => {
298 | setModal({
299 | nowData: record,
300 | });
301 | setRole({
302 | roleTreeShow: true,
303 | roleTreeDefault: record.roles || [],
304 | });
305 | };
306 |
307 | // 分配角色确定
308 | const onRoleOk = async (keys: string[]): Promise => {
309 | if (!modal.nowData?.id) {
310 | message.error("未获取到该条数据id");
311 | return;
312 | }
313 | const params = {
314 | id: modal.nowData.id,
315 | roles: keys.map((item) => Number(item)),
316 | };
317 | setRole({
318 | roleTreeLoading: true,
319 | });
320 | try {
321 | const res: Res = await dispatch.sys.setUserRoles(params);
322 | if (res && res.status === 200) {
323 | message.success("分配成功");
324 | onGetData(page);
325 | onRoleClose();
326 | } else {
327 | message.error(res?.message ?? "操作失败");
328 | }
329 | } finally {
330 | setRole({
331 | roleTreeLoading: false,
332 | });
333 | }
334 | };
335 |
336 | // 分配角色树关闭
337 | const onRoleClose = (): void => {
338 | setRole({
339 | roleTreeShow: false,
340 | });
341 | };
342 |
343 | // 表格页码改变
344 | const onTablePageChange = (pageNum: number, pageSize: number): void => {
345 | onGetData({ pageNum, pageSize });
346 | };
347 |
348 | // ==================
349 | // 属性 和 memo
350 | // ==================
351 |
352 | // table字段
353 | const tableColumns = [
354 | {
355 | title: "序号",
356 | dataIndex: "serial",
357 | key: "serial",
358 | },
359 | {
360 | title: "用户名",
361 | dataIndex: "username",
362 | key: "username",
363 | },
364 | {
365 | title: "电话",
366 | dataIndex: "phone",
367 | key: "phone",
368 | },
369 | {
370 | title: "邮箱",
371 | dataIndex: "email",
372 | key: "email",
373 | },
374 | {
375 | title: "描述",
376 | dataIndex: "desc",
377 | key: "desc",
378 | },
379 | {
380 | title: "状态",
381 | dataIndex: "conditions",
382 | key: "conditions",
383 | render: (v: number): JSX.Element =>
384 | v === 1 ? (
385 | 启用
386 | ) : (
387 | 禁用
388 | ),
389 | },
390 | {
391 | title: "操作",
392 | key: "control",
393 | width: 200,
394 | render: (v: null, record: TableRecordData) => {
395 | const controls = [];
396 | const u = userinfo.userBasicInfo || { id: -1 };
397 | p.includes("user:query") &&
398 | controls.push(
399 | onModalShow(record, "see")}
403 | >
404 |
405 |
406 |
407 |
408 | );
409 | p.includes("user:up") &&
410 | controls.push(
411 | onModalShow(record, "up")}
415 | >
416 |
417 |
418 |
419 |
420 | );
421 | p.includes("user:role") &&
422 | controls.push(
423 | onTreeShowClick(record)}
427 | >
428 |
429 |
430 |
431 |
432 | );
433 |
434 | p.includes("user:del") &&
435 | u.id !== record.id &&
436 | controls.push(
437 | onDel(record.id)}
441 | okText="确定"
442 | cancelText="取消"
443 | >
444 |
445 |
446 |
447 |
448 |
449 |
450 | );
451 |
452 | const result: JSX.Element[] = [];
453 | controls.forEach((item, index) => {
454 | if (index) {
455 | result.push( );
456 | }
457 | result.push(item);
458 | });
459 | return result;
460 | },
461 | },
462 | ];
463 |
464 | // table列表所需数据
465 | const tableData = useMemo(() => {
466 | return data.map((item, index) => {
467 | return {
468 | key: index,
469 | id: item.id,
470 | serial: index + 1 + (page.pageNum - 1) * page.pageSize,
471 | username: item.username,
472 | password: item.password,
473 | phone: item.phone,
474 | email: item.email,
475 | desc: item.desc,
476 | conditions: item.conditions,
477 | control: item.id,
478 | roles: item.roles,
479 | };
480 | });
481 | }, [page, data]);
482 |
483 | return (
484 |
485 |
486 |
487 |
488 | }
491 | disabled={!p.includes("user:add")}
492 | onClick={() => onModalShow(null, "add")}
493 | >
494 | 添加用户
495 |
496 |
497 |
498 |
499 | {p.includes("user:query") && (
500 |
530 | )}
531 |
532 |
533 |
`共 ${t} 条数据`,
543 | onChange: onTablePageChange,
544 | }}
545 | />
546 |
547 |
548 | {/* 新增&修改&查看 模态框 */}
549 |
556 |
571 |
575 |
576 |
586 |
590 |
591 | ({
597 | validator: (rule, value) => {
598 | const v = value;
599 | if (v) {
600 | if (!tools.checkPhone(v)) {
601 | return Promise.reject("请输入有效的手机号码");
602 | }
603 | }
604 | return Promise.resolve();
605 | },
606 | }),
607 | ]}
608 | >
609 |
614 |
615 | ({
621 | validator: (rule, value) => {
622 | const v = value;
623 | if (v) {
624 | if (!tools.checkEmail(v)) {
625 | return Promise.reject("请输入有效的邮箱地址");
626 | }
627 | }
628 | return Promise.resolve();
629 | },
630 | }),
631 | ]}
632 | >
633 |
637 |
638 |
644 |
649 |
650 |
656 |
657 |
658 | 启用
659 |
660 |
661 | 禁用
662 |
663 |
664 |
665 |
666 |
667 |
668 |
677 |
678 | );
679 | }
680 |
681 | export default UserAdminContainer;
682 |
--------------------------------------------------------------------------------
/src/pages/System/UserAdmin/index.type.ts:
--------------------------------------------------------------------------------
1 | /** 当前页面所需所有类型声明 **/
2 |
3 | import { Role, UserBasicInfoParam } from "@/models/index.type";
4 |
5 | export type { UserBasicInfoParam, Res } from "@/models/index.type";
6 |
7 | // 列表table的数据类型
8 | export type TableRecordData = {
9 | key?: number;
10 | id: number;
11 | serial: number; // 序号
12 | username: string; // 用户名
13 | password: string; // 密码
14 | phone: string | number; // 手机
15 | email: string; // 邮箱
16 | desc: string; // 描述
17 | conditions: number; // 是否启用 1启用 -1禁用
18 | control?: number; // 控制,传入的ID
19 | roles?: number[]; // 拥有的所有权限ID
20 | };
21 |
22 | export type Page = {
23 | pageNum: number;
24 | pageSize: number;
25 | total: number;
26 | };
27 |
28 | export type operateType = "add" | "see" | "up";
29 |
30 | export type ModalType = {
31 | operateType: operateType;
32 | nowData: UserBasicInfoParam | null;
33 | modalShow: boolean;
34 | modalLoading: boolean;
35 | };
36 |
37 | export type SearchInfo = {
38 | username: string | undefined; // 用户名
39 | conditions: number | undefined; // 状态
40 | };
41 |
42 | export type RoleTreeInfo = {
43 | roleData: Role[]; // 所有的角色数据
44 | roleTreeLoading: boolean; // 控制树的loading状态,因为要先加载当前role的菜单,才能显示树
45 | roleTreeShow: boolean; // 角色树是否显示
46 | roleTreeDefault: number[]; // 用于角色树,默认需要选中的项
47 | };
48 |
--------------------------------------------------------------------------------
/src/router/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | // 路由守卫
2 |
3 | import React, { useMemo } from "react";
4 | import { Navigate, useLocation } from "react-router-dom";
5 | import { useSelector } from "react-redux";
6 | import { RootState } from "@/store";
7 |
8 | import type { Menu } from "@/models/index.type";
9 |
10 | import tools from "@/util/tools";
11 |
12 | interface Props {
13 | children: JSX.Element;
14 | }
15 |
16 | // 未登录的用户,重定向到登录页
17 | export function AuthNoLogin(props: Props) {
18 | const userinfo = useSelector((state: RootState) => state.app.userinfo);
19 |
20 | if (!userinfo.userBasicInfo) {
21 | return ;
22 | }
23 |
24 | return props.children;
25 | }
26 |
27 | // 已登录的用户,不应该进入login页,直接重定向到主页
28 | export function AuthWithLogin(props: Props) {
29 | const userinfo = useSelector((state: RootState) => state.app.userinfo);
30 |
31 | if (userinfo.userBasicInfo) {
32 | return ;
33 | }
34 | return props.children;
35 | }
36 |
37 | // 已登录,但没有权限访问当前页面,跳401
38 | export function AuthNoPower(props: Props) {
39 | const location = useLocation();
40 | const userinfo = useSelector((state: RootState) => state.app.userinfo);
41 |
42 | // 判断当前用户是否有该路由权限,如果没有就跳转至401页
43 | const isHavePower = useMemo(() => {
44 | let menus: Menu[] = [];
45 | if (userinfo.menus && userinfo.menus.length) {
46 | menus = userinfo.menus;
47 | } else if (sessionStorage.getItem("userinfo")) {
48 | menus = JSON.parse(
49 | tools.uncompile(sessionStorage.getItem("userinfo") || "[]")
50 | ).menus;
51 | }
52 | const m: string[] = menus.map((item) => item.url); // 当前用户拥有的所有菜单
53 |
54 | if (m.includes(location.pathname)) {
55 | return true;
56 | }
57 | return false;
58 | }, [userinfo, location.pathname]);
59 |
60 | console.log("auth:", userinfo, isHavePower, location.pathname);
61 |
62 | if (!isHavePower && location.pathname !== "/401") {
63 | return ;
64 | }
65 |
66 | return props.children;
67 | }
68 |
--------------------------------------------------------------------------------
/src/router/index.tsx:
--------------------------------------------------------------------------------
1 | /** 根路由 **/
2 |
3 | // ==================
4 | // 第三方库
5 | // ==================
6 | import React, { useEffect } from "react";
7 | import { Routes, Route, Navigate } from "react-router-dom";
8 | import { useSelector, useDispatch } from "react-redux";
9 | import { message } from "antd";
10 | import loadable from "@loadable/component";
11 |
12 | // ==================
13 | // 自定义的东西
14 | // ==================
15 | import tools from "@/util/tools";
16 |
17 | // ==================
18 | // 组件
19 | // ==================
20 | import { AuthNoLogin, AuthWithLogin, AuthNoPower } from "./AuthProvider";
21 | import Loading from "../components/Loading";
22 | import BasicLayout from "@/layouts/BasicLayout";
23 | import UserLayout from "@/layouts/UserLayout";
24 |
25 | // 全局提示只显示2秒
26 | message.config({
27 | duration: 2,
28 | });
29 |
30 | // ==================
31 | // 类型声明
32 | // ==================
33 | import { RootState, Dispatch } from "@/store";
34 |
35 | // ==================
36 | // 异步加载各路由模块
37 | // ==================
38 | const [
39 | NotFound,
40 | NoPower,
41 | Login,
42 | Home,
43 | MenuAdmin,
44 | PowerAdmin,
45 | RoleAdmin,
46 | UserAdmin,
47 | ] = [
48 | () => import("../pages/ErrorPages/404"),
49 | () => import("../pages/ErrorPages/401"),
50 | () => import("../pages/Login"),
51 | () => import("../pages/Home"),
52 | () => import("../pages/System/MenuAdmin"),
53 | () => import("../pages/System/PowerAdmin"),
54 | () => import("../pages/System/RoleAdmin"),
55 | () => import("../pages/System/UserAdmin"),
56 | ].map((item) => {
57 | return loadable(item as any, {
58 | fallback: ,
59 | });
60 | });
61 |
62 | // ==================
63 | // 本组件
64 | // ==================
65 | function RouterCom(): JSX.Element {
66 | const dispatch = useDispatch();
67 | const userinfo = useSelector((state: RootState) => state.app.userinfo);
68 |
69 | useEffect(() => {
70 | const userTemp = sessionStorage.getItem("userinfo");
71 | /**
72 | * sessionStorage中有user信息,但store中没有
73 | * 说明刷新了页面,需要重新同步user数据到store
74 | * **/
75 | if (userTemp && !userinfo.userBasicInfo) {
76 | dispatch.app.setUserInfo(JSON.parse(tools.uncompile(userTemp)));
77 | }
78 | }, [dispatch.app, userinfo.userBasicInfo]);
79 |
80 | return (
81 |
82 |
86 |
87 |
88 | }
89 | >
90 | }>
91 | }>
92 | } />
93 |
94 |
98 |
99 |
100 | }
101 | >
102 | } />
103 | } />
104 |
108 |
109 |
110 | }
111 | />
112 |
116 |
117 |
118 | }
119 | />
120 |
124 |
125 |
126 | }
127 | />
128 |
132 |
133 |
134 | }
135 | />
136 | } />
137 | } />
138 | } />
139 |
140 |
141 | );
142 | }
143 |
144 | export default RouterCom;
145 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | /** 全局唯一数据中心 **/
2 |
3 | import { init, Models, RematchDispatch, RematchRootState } from "@rematch/core";
4 |
5 | import app from "@/models/app";
6 | import sys from "@/models/sys";
7 |
8 | export interface RootModel extends Models {
9 | app: typeof app;
10 | sys: typeof sys;
11 | }
12 |
13 | const rootModel: RootModel = { app, sys };
14 | const store = init({
15 | models: rootModel,
16 | });
17 |
18 | export type Store = typeof store;
19 | export type Dispatch = RematchDispatch;
20 | export type RootState = RematchRootState;
21 |
22 | export default store;
23 |
--------------------------------------------------------------------------------
/src/util/axios.ts:
--------------------------------------------------------------------------------
1 | /** 对axios做一些配置 **/
2 |
3 | import { baseUrl } from "../config";
4 | import axios from "axios";
5 |
6 | /**
7 | * MOCK模拟数据
8 | * 不需要下面这些mock配置,仅本地用
9 | * 正式打包需要去掉
10 | * */
11 | import Mock from "mockjs";
12 | // @ts-ignore
13 | import mock from "../../mock/app-data.js";
14 | Mock.mock(/\/api.*/, (options: any) => {
15 | const res = mock(options);
16 | return res;
17 | });
18 |
19 | /**
20 | * 根据不同环境设置不同的请求地址
21 | * 把返回值赋给axios.defaults.baseURL即可
22 | */
23 | // function setBaseUrl(){
24 | // switch(process.env.NODE_ENV){
25 | // case 'development': return 'http://development.com';
26 | // case 'test': return 'http://test.com';
27 | // case 'production' : return 'https://production.com';
28 | // default : return baseUrl;
29 | // }
30 | // }
31 |
32 | // 默认基础请求地址
33 | axios.defaults.baseURL = baseUrl;
34 | // 请求是否带上cookie
35 | axios.defaults.withCredentials = false;
36 | // 对返回的结果做处理
37 | axios.interceptors.response.use((response) => {
38 | // const code = response?.data?.code ?? 200;
39 | // 没有权限,登录超时,登出,跳转登录
40 | // if (code === 3) {
41 | // message.error("登录超时,请重新登录");
42 | // sessionStorage.removeItem("userinfo");
43 | // setTimeout(() => {
44 | // window.location.href = "/";
45 | // }, 1500);
46 | // } else {
47 | // return response.data;
48 | // }
49 | return response.data;
50 | });
51 |
52 | export default axios;
53 |
--------------------------------------------------------------------------------
/src/util/json.ts:
--------------------------------------------------------------------------------
1 | /** 全局维护的统一数据 **/
2 |
3 | // 用于菜单选图标的图标数据
4 | export const IconsData: string[] = [
5 | "icon-Dollar",
6 | "icon-compass",
7 | "icon-EURO",
8 | "icon-time-circle",
9 | "icon-earth",
10 | "icon-YUAN",
11 | "icon-message",
12 | "icon-dashboard",
13 | "icon-piechart",
14 | "icon-setting",
15 | "icon-eye",
16 | "icon-save",
17 | "icon-appstore",
18 | "icon-control",
19 | "icon-detail",
20 | "icon-project",
21 | "icon-wallet",
22 | "icon-user",
23 | "icon-team",
24 | "icon-areachart",
25 | "icon-linechart",
26 | "icon-barchart",
27 | "icon-pointmap",
28 | "icon-database",
29 | "icon-reconciliation",
30 | "icon-securityscan",
31 | "icon-propertysafety",
32 | "icon-safetycertificate",
33 | "icon-alert",
34 | "icon-bulb",
35 | "icon-trophy",
36 | "icon-USB",
37 | "icon-home",
38 | "icon-like",
39 | "icon-unlock",
40 | "icon-lock",
41 | "icon-customerservice",
42 | "icon-flag",
43 | "icon-moneycollect",
44 | "icon-medicinebox",
45 | "icon-shop",
46 | "icon-rocket",
47 | "icon-shopping",
48 | "icon-folder",
49 | "icon-folder-open",
50 | "icon-sliders",
51 | "icon-cloud",
52 | "icon-crown",
53 | "icon-desktop",
54 | "icon-gift",
55 | ];
56 |
--------------------------------------------------------------------------------
/src/util/tools.ts:
--------------------------------------------------------------------------------
1 | /** 这个文件封装了一些常用的工具函数 **/
2 |
3 | const tools = {
4 | /**
5 | * 保留N位小数
6 | * 最终返回的是字符串
7 | * 若转换失败,返回参数原值
8 | * @param str - 数字或字符串
9 | * @param x - 保留几位小数点
10 | */
11 | pointX(str: string | number, x = 0): string | number {
12 | if (!str && str !== 0) {
13 | return str;
14 | }
15 | const temp = Number(str);
16 | if (temp === 0) {
17 | return temp.toFixed(x);
18 | }
19 | return temp ? temp.toFixed(x) : str;
20 | },
21 |
22 | /**
23 | * 去掉字符串两端空格
24 | * @param str - 待处理的字符串
25 | */
26 | trim(str: string): string {
27 | const reg = /^\s*|\s*$/g;
28 | return str.replace(reg, "");
29 | },
30 |
31 | /**
32 | * 给字符串打马赛克
33 | * 如:将123456转换为1****6,最多将字符串中间6个字符变成*
34 | * 如果字符串长度小于等于2,将不会有效果
35 | * @param str - 待处理的字符串
36 | */
37 | addMosaic(str: string): string {
38 | const s = String(str);
39 | const lenth = s.length;
40 | const howmuch = ((): number => {
41 | if (s.length <= 2) {
42 | return 0;
43 | }
44 | const l = s.length - 2;
45 | if (l <= 6) {
46 | return l;
47 | }
48 | return 6;
49 | })();
50 | const start = Math.floor((lenth - howmuch) / 2);
51 | const ret = s.split("").map((v, i) => {
52 | if (i >= start && i < start + howmuch) {
53 | return "*";
54 | }
55 | return v;
56 | });
57 | return ret.join("");
58 | },
59 |
60 | /**
61 | * 验证字符串
62 | * 只能为字母、数字、下划线
63 | * 可以为空
64 | * @param str - 待处理的字符串
65 | * **/
66 | checkStr(str: string): boolean {
67 | if (str === "") {
68 | return true;
69 | }
70 | const rex = /^[_a-zA-Z0-9]+$/;
71 | return rex.test(str);
72 | },
73 |
74 | /**
75 | * 验证字符串
76 | * 只能为数字
77 | * 可以为空
78 | * @param str - 待处理的字符串
79 | * **/
80 | checkNumber(str: string): boolean {
81 | if (!str) {
82 | return true;
83 | }
84 | const rex = /^\d*$/;
85 | return rex.test(str);
86 | },
87 |
88 | /**
89 | * 正则 手机号验证
90 | * @param str - 待处理的字符串或数字
91 | * **/
92 | checkPhone(str: string | number): boolean {
93 | const rex = /^1[34578]\d{9}$/;
94 | return rex.test(String(str));
95 | },
96 |
97 | /**
98 | * 正则 邮箱验证
99 | * @param str - 待处理的字符串
100 | * **/
101 | checkEmail(str: string): boolean {
102 | const rex =
103 | /^[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*@[a-zA-Z0-9]+([-_.][a-zA-Z0-9]+)*\.[a-z]{2,}$/;
104 | return rex.test(str);
105 | },
106 |
107 | /**
108 | * 字符串加密
109 | * 简单的加密方法
110 | * @param code - 待处理的字符串
111 | */
112 | compile(code: string): string {
113 | let c = String.fromCharCode(code.charCodeAt(0) + code.length);
114 | for (let i = 1; i < code.length; i++) {
115 | c += String.fromCharCode(code.charCodeAt(i) + code.charCodeAt(i - 1));
116 | }
117 | return c;
118 | },
119 |
120 | /**
121 | * 字符串解谜
122 | * 对应上面的字符串加密方法
123 | * @param code - 待处理的字符串
124 | */
125 | uncompile(code: string): string {
126 | let c = String.fromCharCode(code.charCodeAt(0) - code.length);
127 | for (let i = 1; i < code.length; i++) {
128 | c += String.fromCharCode(code.charCodeAt(i) - c.charCodeAt(i - 1));
129 | }
130 | return c;
131 | },
132 |
133 | /**
134 | * 清除一个对象中那些属性为空值的属性
135 | * 0 算有效值
136 | * @param {Object} obj 待处理的对象
137 | * **/
138 | clearNull(obj: T): T {
139 | const temp: any = { ...obj };
140 | for (const key in temp) {
141 | if (temp.hasOwnProperty(key)) {
142 | const value = temp[key];
143 | if (value === null || value === undefined) {
144 | delete temp[key];
145 | }
146 | }
147 | }
148 | return temp as T;
149 | },
150 | };
151 |
152 | export default tools;
153 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": "./",
19 | "paths": {
20 | "@/*": ["src/*"]
21 | }
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import eslintPlugin from "vite-plugin-eslint";
4 | import {createStyleImportPlugin, AntdResolve} from 'vite-plugin-style-import';
5 | import { resolve } from "path";
6 |
7 | function pathResolve(dir) {
8 | return resolve(process.cwd(), ".", dir);
9 | }
10 |
11 | // https://vitejs.dev/config/
12 | export default defineConfig({
13 | base: "./",
14 | plugins: [
15 | react(),
16 | eslintPlugin({
17 | cache: false,
18 | failOnError: false,
19 | include: ["src/**/*.js", "src/**/*.tsx", "src/**/*.ts"],
20 | }),
21 | createStyleImportPlugin({
22 | resolves: [AntdResolve()]
23 | })
24 | ],
25 | css: {
26 | preprocessorOptions: {
27 | less: {
28 | javascriptEnabled: true,
29 | },
30 | },
31 | postcss:{}
32 | },
33 | resolve: {
34 | alias: [
35 | {
36 | find: /@\//,
37 | replacement: `${pathResolve("src")}/`,
38 | },
39 | ],
40 | },
41 | })
42 |
--------------------------------------------------------------------------------