├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── README.md
├── index.html
├── package.json
├── resources
├── 01_登录.png
├── 02_注册.png
├── 03_控制台.png
├── 04_用户列表.png
├── 05_用户添加修改.png
├── 06_部门列表.png
├── 07_添加部门.png
├── 08_职位列表.png
├── 09_添加职位.png
├── 10_职员列表.png
└── 11_职员添加.png
├── src
├── api
│ ├── auth.ts
│ ├── department.ts
│ ├── job.ts
│ ├── staff.ts
│ ├── types
│ │ ├── auth.ts
│ │ ├── department.ts
│ │ ├── index.ts
│ │ ├── job.ts
│ │ ├── staff.ts
│ │ └── user.ts
│ ├── upload.ts
│ └── user.ts
├── assets
│ ├── data.ts
│ ├── images
│ │ └── avatar.png
│ └── normalize.less
├── components
│ ├── AuthWrapper
│ │ └── index.tsx
│ ├── BasisTable
│ │ └── index.tsx
│ ├── Breadcrumbs
│ │ ├── index.less
│ │ └── index.tsx
│ ├── Captcha
│ │ └── index.tsx
│ ├── Editor
│ │ ├── config.ts
│ │ └── index.tsx
│ ├── FormItem
│ │ ├── DepartmentItem
│ │ │ └── index.tsx
│ │ ├── JobItem
│ │ │ └── index.tsx
│ │ ├── LoginItem
│ │ │ └── index.tsx
│ │ ├── StaffItem
│ │ │ ├── config.ts
│ │ │ └── index.tsx
│ │ ├── UserItem
│ │ │ └── index.tsx
│ │ ├── searchItem.tsx
│ │ └── type.ts
│ ├── FromInput
│ │ └── input.tsx
│ ├── Layout
│ │ ├── AsyncRoutes
│ │ │ └── AsyncRoutes.tsx
│ │ ├── Auth
│ │ │ └── index.tsx
│ │ ├── Header
│ │ │ ├── UserInfo
│ │ │ │ ├── index.less
│ │ │ │ └── index.tsx
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── Main
│ │ │ ├── index.less
│ │ │ └── index.tsx
│ │ ├── MainRoutes
│ │ │ └── index.tsx
│ │ ├── Sider
│ │ │ └── index.tsx
│ │ ├── index.less
│ │ └── index.tsx
│ ├── SiderMenu
│ │ └── index.tsx
│ ├── TransitionMain
│ │ ├── index.less
│ │ └── index.tsx
│ ├── UploadPic
│ │ └── index.tsx
│ └── UserLayout
│ │ ├── index.less
│ │ └── index.tsx
├── constants
│ ├── app.ts
│ ├── lottiePath.ts
│ ├── server.ts
│ └── validate.ts
├── main.tsx
├── router
│ ├── index.ts
│ ├── type.ts
│ └── utils.ts
├── store
│ ├── index.ts
│ ├── module
│ │ ├── app.ts
│ │ └── user.ts
│ └── type.ts
├── styles
│ ├── global.less
│ ├── mixin.less
│ └── variables.less
├── typings
│ └── global.d.ts
├── utils
│ ├── auth.ts
│ ├── filter.ts
│ ├── lazyImport.ts
│ ├── prase.ts
│ ├── request.ts
│ └── weather.ts
└── views
│ ├── App.tsx
│ ├── Dashboard
│ ├── CardGroup.tsx
│ ├── Header.tsx
│ ├── header.less
│ ├── index.less
│ └── index.tsx
│ ├── Department
│ ├── Add
│ │ └── index.tsx
│ └── List
│ │ └── index.tsx
│ ├── Error
│ ├── 403.tsx
│ └── 404.tsx
│ ├── Job
│ ├── Add
│ │ └── index.tsx
│ └── List
│ │ └── index.tsx
│ ├── Login
│ ├── Form
│ │ ├── loginForm.tsx
│ │ └── registerForm.tsx
│ ├── index.less
│ └── index.tsx
│ ├── Staff
│ ├── Add
│ │ ├── index.less
│ │ └── index.tsx
│ └── List
│ │ └── index.tsx
│ ├── Success
│ └── index.tsx
│ └── User
│ └── List
│ ├── AddModal.tsx
│ ├── index.less
│ └── index.tsx
├── tsconfig.json
├── vite.config.ts
├── yarn-error.log
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .vscode/
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2020: true,
5 | },
6 | extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'airbnb', 'plugin:react-hooks/recommended'],
7 | parser: '@typescript-eslint/parser',
8 | parserOptions: {
9 | ecmaFeatures: {
10 | jsx: true,
11 | },
12 | ecmaVersion: 11,
13 | sourceType: 'module',
14 | },
15 | plugins: ['react', 'react-hooks', '@typescript-eslint'],
16 | rules: {
17 | 'max-len': ['error', 150, 2], // 一行的字符不能超过100
18 |
19 | 'linebreak-style': ['error', 'windows'],
20 | quotes: ['error', 'single'],
21 | 'jsx-quotes': ['error', 'prefer-single'],
22 | semi: [0, 'never'],
23 | // 禁止缩进错误
24 | indent: 'off',
25 | // checkAttributes: false,
26 | // 关闭不允许使用 no-tabs
27 | 'no-tabs': 'off',
28 | 'no-console': 1,
29 | // 设置不冲突 underscore 库
30 | 'no-underscore-dangle': 0,
31 | // 箭头函数直接返回的时候不需要 大括号 {}
32 | 'arrow-body-style': [2, 'as-needed'],
33 | 'no-alert': 'error',
34 | // 可以传递props
35 | 'react/jsx-props-no-spreading': 'off',
36 |
37 | // 设置是否可以重新改变参数的值
38 | 'no-param-reassign': 0,
39 | // 允许使用 for in
40 | 'no-restricted-syntax': 0,
41 | 'guard-for-in': 0,
42 | // 不需要每次都有返回
43 | 'consistent-return': 0,
44 | // 允许使用 arguments
45 | 'prefer-rest-params': 0,
46 | // 允许返回 await
47 | 'no-return-await': 0,
48 | // 不必在使用前定义 函数
49 | 'no-use-before-define': 0,
50 | // 允许代码后面空白
51 | 'no-trailing-spaces': 0,
52 | // 允许变量定义了未使用
53 | 'no-unused-vars': 'off',
54 | // 允许重复声明
55 | 'no-shadow': 0,
56 | // 允许Ts变量定义了未使用
57 | '@typescript-eslint/no-unused-vars': 'off',
58 | // 允许函数不声明返回类型
59 | '@typescript-eslint/explicit-module-boundary-types': 0,
60 |
61 | // 有一些 event 的时候,不需要 role 属性,不需要其他解释
62 | 'jsx-a11y/no-static-element-interactions': 0,
63 | 'jsx-a11y/click-events-have-key-events': 0,
64 | // 类成员之间空行问题
65 | 'lines-between-class-members': 0,
66 |
67 | // 变量名可以使用下划线
68 | camelcase: [0, { properties: 'never' }],
69 |
70 | // 不区分是否在 despendencies
71 | 'import/no-extraneous-dependencies': 0,
72 | // 引用时候根据根目录基础
73 | 'import/no-unresolved': 0,
74 |
75 | 'react/prop-types': 'off',
76 |
77 | /* 配置hooks的eslint */
78 | /* 'react-hooks/rule-of-hooks': 'error',
79 |
80 | 'react-hooks/exhaustive-deps': 'warn', */
81 |
82 | // 允许不使用默认值
83 | 'react/destructuring-assignment': 1,
84 |
85 | 'react/jsx-indent': ['error', 4],
86 | // jsx > 紧跟着属性
87 | 'react/jsx-closing-bracket-location': [1, 'after-props'],
88 | // 不区分是否是 无状态组件
89 | 'import/extensions': ['off', 'always', {
90 | js: 'never',
91 | ts: 'never',
92 | tsx: 'never',
93 | vue: 'never',
94 | }],
95 | 'react/jsx-filename-extension': [1, {
96 | extensions: ['.js', '.jsx', '.tsx'],
97 | }],
98 |
99 | },
100 | }
101 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Admin
2 |
3 | React Admin——一个基于React、TypeScript、Vite的编写的管理系统。
4 |
5 | 新建用户权限默认为admin,需要在用户管理中添加不同权限的用户。同时新建的用户也没有数据,需要自行添加。
6 |
7 | ## 项目体验地址
8 |
9 | http://icloudmusic.top/reactAdmin
10 |
11 |
12 | ## 视频教学地址
13 |
14 | https://www.bilibili.com/video/BV1Hg4y167v6?from=search&seid=14801539303278423807
15 |
16 |
17 | ## 使用
18 |
19 | ```
20 | $ git clone https://github.com/chengzhenguo1/react-admin.git
21 | $ cd react-admin
22 | $ yarn install
23 | $ yarn run dev
24 | ```
25 |
26 | ## 功能列表
27 |
28 | - 登录/注册
29 |
30 | 控制台
31 |
32 | - 基础信息
33 | - 人数统计
34 |
35 | - 用户管理
36 |
37 | - 添加用户
38 | - 编辑用户
39 | - 分配角色
40 |
41 | - 部门管理
42 |
43 | - 部门列表
44 | - 添加部门
45 |
46 | - 职位管理
47 |
48 | - 职位列表
49 | - 添加职位
50 |
51 | - 职员管理
52 |
53 | - 职员列表
54 | - 职员添加
55 |
56 | - 权限功能
57 |
58 | - 根据用户角色权限生成路由以及侧边栏导航
59 | - 根据权限来校验组件渲染
60 |
61 | ## 技术栈
62 |
63 | - React,使用Redux做状态管理。
64 | - Hook,react-use库(很方便)。
65 | - TypeScript。
66 | - antd 组件库。
67 | - Less。
68 | - Vite。
69 | - Eslint做代码检查。
70 |
71 |
72 |
73 | ## 图片预览
74 |
75 | 
76 |
77 | 
78 |
79 | 
80 |
81 | 
82 |
83 | 
84 |
85 | 
86 |
87 | 
88 |
89 | 
90 |
91 | 
92 |
93 | 
94 |
95 | 
96 |
97 | ## API接口
98 |
99 | [api (web-jshtml.cn)](http://apidoc.web-jshtml.cn/#/home)
100 | ` 请求该地址即可http://old.web-jshtml.cn/api/react`
101 |
102 | ## 感谢
103 |
104 | 感谢**A总**老师的教学视频[手把手撸码前端 React 企业人事后台管理系统开发,由0到1自主搭建管理后台,学习React全家桶知识、Ant Design组件UI_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili](https://www.bilibili.com/video/BV1Hg4y167v6?from=search&seid=14801539303278423807)
105 |
106 | 另附上官网手把手撸码前端 (web-jshtml.cn)](http://school.web-jshtml.cn/#/),欢迎来一起学习
107 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-jira",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite --mode development",
6 | "build:beta": "vite build --mode beta",
7 | "build:release": "vite build --mode release",
8 | "serve": "vite preview",
9 | "eslint": "eslint src --ext .js,.jsx,.ts,.tsx --fix"
10 | },
11 | "dependencies": {
12 | "@lottiefiles/react-lottie-player": "^3.1.2",
13 | "@tinymce/tinymce-react": "^3.12.2",
14 | "@types/crypto-js": "^4.0.1",
15 | "@types/react-router-dom": "^5.1.7",
16 | "@types/react-transition-group": "^4.4.1",
17 | "@types/redux-logger": "^3.0.8",
18 | "@types/store": "^2.0.2",
19 | "antd": "^4.15.0",
20 | "axios": "^0.21.1",
21 | "classnames": "^2.3.1",
22 | "crypto-js": "^4.0.0",
23 | "less-vars-to-js": "^1.3.0",
24 | "react": "^17.0.0",
25 | "react-dom": "^17.0.0",
26 | "react-redux": "^7.2.3",
27 | "react-router-dom": "^5.2.0",
28 | "react-transition-group": "^4.4.1",
29 | "react-use": "^17.2.3",
30 | "redux": "^4.0.5",
31 | "redux-logger": "^3.0.6",
32 | "redux-thunk": "^2.3.0",
33 | "store": "^2.0.12",
34 | "vite-plugin-imp": "^2.0.5"
35 | },
36 | "devDependencies": {
37 | "@types/node": "^14.14.37",
38 | "@types/react": "^17.0.0",
39 | "@types/react-dom": "^17.0.0",
40 | "@typescript-eslint/eslint-plugin": "^4.18.0",
41 | "@typescript-eslint/parser": "^4.18.0",
42 | "@vitejs/plugin-react-refresh": "^1.3.1",
43 | "babel-eslint": "^10.1.0",
44 | "babel-plugin-import": "^1.13.3",
45 | "eslint": "^7.20.0",
46 | "eslint-config-airbnb": "^18.2.1",
47 | "eslint-config-prettier": "^7.2.0",
48 | "eslint-plugin-import": "^2.22.1",
49 | "eslint-plugin-jsx-a11y": "^6.4.1",
50 | "eslint-plugin-prettier": "^3.3.1",
51 | "eslint-plugin-react": "^7.22.0",
52 | "eslint-plugin-react-hooks": "^4.2.0",
53 | "less": "^4.1.1",
54 | "typescript": "^4.1.2",
55 | "vite": "^2.1.5",
56 | "vite-plugin-style-import": "^0.10.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/resources/01_登录.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/01_登录.png
--------------------------------------------------------------------------------
/resources/02_注册.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/02_注册.png
--------------------------------------------------------------------------------
/resources/03_控制台.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/03_控制台.png
--------------------------------------------------------------------------------
/resources/04_用户列表.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/04_用户列表.png
--------------------------------------------------------------------------------
/resources/05_用户添加修改.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/05_用户添加修改.png
--------------------------------------------------------------------------------
/resources/06_部门列表.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/06_部门列表.png
--------------------------------------------------------------------------------
/resources/07_添加部门.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/07_添加部门.png
--------------------------------------------------------------------------------
/resources/08_职位列表.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/08_职位列表.png
--------------------------------------------------------------------------------
/resources/09_添加职位.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/09_添加职位.png
--------------------------------------------------------------------------------
/resources/10_职员列表.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/10_职员列表.png
--------------------------------------------------------------------------------
/resources/11_职员添加.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/resources/11_职员添加.png
--------------------------------------------------------------------------------
/src/api/auth.ts:
--------------------------------------------------------------------------------
1 | import axios from '@src/utils/request'
2 | import type { IUser, IParam } from './types/auth'
3 |
4 | type LoginFn = (data: IParam) => Promise
5 | type GetSmsFn = (data: {username: string, module: 'login' | 'register'}) => Promise<{message: string}>
6 | type RegisterFn = (data: IParam) => Promise<{message: string}>
7 | type GetRoleFn = () => Promise<{label: string, value: string}[]>
8 |
9 | const login: LoginFn = (data) => axios({
10 | url: '/login/',
11 | method: 'POST',
12 | data,
13 | })
14 |
15 | const register: RegisterFn = (data) => axios({
16 | url: '/register/',
17 | method: 'POST',
18 | data,
19 | })
20 |
21 | const getSms: GetSmsFn = (data) => axios({
22 | url: '/getSms/',
23 | method: 'POST',
24 | data,
25 | })
26 |
27 | const getRole: GetRoleFn = async () => {
28 | const res = await axios({
29 | url: '/role/',
30 | method: 'POST',
31 | })
32 | return res.data
33 | }
34 |
35 | export default {
36 | login,
37 | register,
38 | getSms,
39 | getRole,
40 | }
41 |
--------------------------------------------------------------------------------
/src/api/department.ts:
--------------------------------------------------------------------------------
1 | import axios from '@src/utils/request'
2 | import { IGetParam, IList } from './types'
3 | import type { IDepartment, IDepartmentParams } from './types/department'
4 |
5 | type GetDepartmentListFn = (data: IGetParam)=> Promise>
6 | type GetDepartmentListAllFn = ()=> Promise
7 | type GetDepartmentDetailedFn = (id: string)=> Promise
8 | type AddOrEditDepartmentFn = (data: IDepartmentParams)=> Promise
9 | type SetDepartmentStatusFn = (id: string, status: boolean)=> Promise
10 | type DeleteDepartmentFn = (id: string)=> Promise
11 |
12 | /* 获取部门列表 */
13 | const getDepartmentList: GetDepartmentListFn = async (data = { pageNumber: 1, pageSize: 10 }) => {
14 | const res = await axios({
15 | url: '/department/list/',
16 | method: 'POST',
17 | data,
18 | })
19 | return res
20 | }
21 |
22 | /* 获取全部部门 */
23 | const getDepartmentListAll: GetDepartmentListAllFn = async () => {
24 | const res = await axios({
25 | url: '/department/listAll/',
26 | method: 'POST',
27 | })
28 | return res.data.data
29 | }
30 |
31 | /* 获取部门详情 */
32 | const getDepartmentDetailed: GetDepartmentDetailedFn = async (id) => {
33 | const res = await axios({
34 | url: '/department/detailed/',
35 | method: 'POST',
36 | data: {
37 | id,
38 | },
39 | })
40 | return res.data
41 | }
42 |
43 | /* 新增或者编辑部门 */
44 | const addOrEditDepartment: AddOrEditDepartmentFn = async (data) => {
45 | const path = data?.id ? 'edit' : 'add'
46 | const res = await axios({
47 | url: `/department/${path}/`,
48 | method: 'POST',
49 | data,
50 | })
51 | return res.message
52 | }
53 |
54 | /* 部门禁启用 */
55 | const setDepartmentStatus: SetDepartmentStatusFn = async (id, status) => {
56 | const res = await axios({
57 | url: '/department/status/',
58 | method: 'POST',
59 | data: {
60 | id,
61 | status,
62 | },
63 | })
64 | return res.message
65 | }
66 |
67 | /* 删除部门 */
68 | const deleteDepartment: DeleteDepartmentFn = async (id) => {
69 | const res = await axios({
70 | url: '/department/delete/',
71 | method: 'POST',
72 | data: {
73 | id,
74 | },
75 | })
76 | return res.message
77 | }
78 |
79 | export default {
80 | getDepartmentList,
81 | getDepartmentDetailed,
82 | getDepartmentListAll,
83 | addOrEditDepartment,
84 | setDepartmentStatus,
85 | deleteDepartment,
86 | }
87 |
--------------------------------------------------------------------------------
/src/api/job.ts:
--------------------------------------------------------------------------------
1 | import axios from '@src/utils/request'
2 | import type { IJobDeatil, IJob } from '../api/types/job'
3 | import { IGetParam, IList } from './types'
4 |
5 | type JobAddOrEditFn = (data: IJob & {content: string, parentId?: string})=> Promise<{message: string}>
6 | type GetJobListFn = (data: IGetParam)=> Promise>
7 | type JobListAllFn = ()=> Promise
8 | type JobDetailFn = (id: number | string)=> Promise
9 | type SetJobStatusFn = (id: number, status: boolean)=> Promise<{message: string}>
10 | type JobDeleteFn = (id:string)=> Promise<{message: string}>
11 |
12 | /* 新增或者编辑添加 */
13 | const jobAddOrEdit: JobAddOrEditFn = async (data) => {
14 | const path = data?.jobId ? 'edit' : 'add'
15 | const res = await axios({
16 | url: `/job/${path}/`,
17 | method: 'POST',
18 | data,
19 | })
20 | return res
21 | }
22 |
23 | /* 职位列表 */
24 | const getJobList: GetJobListFn = async (data) => {
25 | const res = await axios({
26 | url: '/job/list/',
27 | method: 'POST',
28 | data,
29 | })
30 | return res
31 | }
32 |
33 | /* 职位列表 / 全部列表 */
34 | const getJobAllList: JobListAllFn = async () => {
35 | const res = await axios({
36 | url: '/job/listAll/',
37 | method: 'POST',
38 | })
39 | return res.data.data
40 | }
41 |
42 | /* 职位详情 */
43 | const jobDetail: JobDetailFn = async (id) => {
44 | const res = await axios({
45 | url: '/job/detailed/',
46 | method: 'POST',
47 | data: {
48 | id,
49 | },
50 | })
51 | return res.data
52 | }
53 |
54 | /* 职位禁启用 */
55 | const setJobStatus : SetJobStatusFn = (id, status) => axios({
56 | url: '/job/status/',
57 | method: 'POST',
58 | data: {
59 | id,
60 | status,
61 | },
62 | })
63 |
64 | /* 职位删除 */
65 | const jobDelete: JobDeleteFn = (id) => axios({
66 | url: '/job/delete/',
67 | method: 'POST',
68 | data: {
69 | id,
70 | },
71 | })
72 |
73 | export default {
74 | jobAddOrEdit,
75 | getJobList,
76 | getJobAllList,
77 | jobDetail,
78 | setJobStatus,
79 | jobDelete,
80 | }
81 |
--------------------------------------------------------------------------------
/src/api/staff.ts:
--------------------------------------------------------------------------------
1 | import axios from '@src/utils/request'
2 | import { IGetParam, IList } from './types'
3 | import type { IStaffAdd, IStaff } from './types/staff'
4 |
5 | type AddOrEditStaffFn = (data: IStaffAdd & {id?: string})=> Promise
6 | type GetStaffListFn = (data: IGetParam)=> Promise>
7 | type GetStaffDetailFn = (id: string)=> Promise
8 | type SetStaffStatusFn = (id: string, status: boolean)=> Promise
9 | type DeleteStaffFn = (id: string)=> Promise
10 |
11 | const getstaffList: GetStaffListFn = async (data) => {
12 | const res = await axios({
13 | url: '/staff/list/',
14 | method: 'POST',
15 | data,
16 | })
17 | return res
18 | }
19 |
20 | const getstaffDetail: GetStaffDetailFn = async (id) => {
21 | const res = await axios({
22 | url: '/staff/detailed/',
23 | method: 'POST',
24 | data: {
25 | id,
26 | },
27 | })
28 | return res.data
29 | }
30 |
31 | const addOrEditStaff: AddOrEditStaffFn = async (data) => {
32 | const path = data.id ? 'edit' : 'add'
33 | const res = await axios({
34 | url: `/staff/${path}/`,
35 | method: 'POST',
36 | data,
37 | })
38 | return res.message
39 | }
40 |
41 | const setstaffStatus: SetStaffStatusFn = async (id, status) => {
42 | const res = await axios({
43 | url: '/staff/status/',
44 | method: 'POST',
45 | data: {
46 | id,
47 | status,
48 | },
49 | })
50 | return res.message
51 | }
52 |
53 | const deleteStaff: DeleteStaffFn = async (id) => {
54 | const res = await axios({
55 | url: '/staff/delete/',
56 | method: 'POST',
57 | data: {
58 | id,
59 | },
60 | })
61 | return res
62 | }
63 |
64 | export default {
65 | getstaffDetail,
66 | getstaffList,
67 | addOrEditStaff,
68 | setstaffStatus,
69 | deleteStaff,
70 | }
71 |
--------------------------------------------------------------------------------
/src/api/types/auth.ts:
--------------------------------------------------------------------------------
1 | import { UserState } from '@src/store/module/user'
2 |
3 | /* 登录返回数据 */
4 | export interface IUser{
5 | message: string
6 | data: UserState
7 | }
8 |
9 | /* 登录/注册参数列表 */
10 | export interface IParam {
11 | username: string
12 | password: string
13 | code: string
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/types/department.ts:
--------------------------------------------------------------------------------
1 | export interface IDepartmentParams extends IDepartment {
2 | content: string
3 | }
4 | export interface IDepartment {
5 | id?: string
6 | name: string
7 | number: number
8 | status: true
9 | }
10 |
--------------------------------------------------------------------------------
/src/api/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface IGetParam {
2 | pageNumber: number
3 | pageSize: number
4 | name?: string
5 | status?: boolean
6 | }
7 |
8 | export interface IList {
9 | data: {
10 | data: T[]
11 | total: number
12 | }
13 | message: string
14 | }
15 |
--------------------------------------------------------------------------------
/src/api/types/job.ts:
--------------------------------------------------------------------------------
1 | export interface IJobDeatil{
2 | data: DataDeatil[]
3 | resCode: number
4 | total: number
5 | message: string
6 | }
7 |
8 | export interface IJob {
9 | jobId: string
10 | jobName: string
11 | status: boolean
12 | }
13 |
14 | export interface DataDeatil extends IJob {
15 | parentId: string
16 | }
17 |
--------------------------------------------------------------------------------
/src/api/types/staff.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Key Required Type Default Describe
3 | name true String 姓名
4 | sex true Boolean 性别(男:true,女:false)
5 | face_img false String 头像
6 | card_id true String 身份证
7 | diploma_img false String 毕业证
8 | birthday false String 出生年月
9 | phone true String 手机号
10 | nation true String 民族
11 | political false String 政治面貌
12 | school false String 毕业院校
13 | education false String 学历
14 | major false String 专业
15 | wechat false String 微信号
16 | email false String 个人邮箱
17 | job_id true String 职位
18 | departmen_id true Date 部门
19 | job_status_date true String
20 | job_status true String 职位状态(在线:online,休假:vacation, 离职:quit)
21 | company_email true String 公司邮箱
22 | introduce false String 描述
23 | status true Boolean 禁启用(启用:true,禁用:false)
24 | job_entry_date false Date 入职日期
25 | job_formal_date false Date 转正日期
26 | job_quit_date false Date 离职日期
27 | */
28 | export interface IStaffAdd {
29 | name: string
30 | sex: boolean
31 | face_img?: string
32 | card_id: string
33 | diploma_img?: string
34 | birthday?: string
35 | phone: string
36 | nation: string
37 | political?: string
38 | school?: string
39 | education?: string
40 | major?: string
41 | wechat?: string
42 | email?: string
43 | job_id: string
44 | departmen_id: Date
45 | job_status_date: string
46 | job_status: 'online' | 'vacation' | 'quit'
47 | company_email: string
48 | introduce?: string
49 | status: boolean
50 | job_entry_date?: Date
51 | job_formal_date?: Date
52 | job_quit_date?: Date
53 | }
54 |
55 | export interface IStaff {
56 | company_email: string
57 | full_name: string
58 | jobName: string
59 | job_entry_date: string
60 | job_formal_date: string
61 | job_quit_date: string
62 | name: string
63 | phone: string
64 | staff_id: string
65 | status: boolean
66 | }
67 |
--------------------------------------------------------------------------------
/src/api/types/user.ts:
--------------------------------------------------------------------------------
1 | export interface FormParam {
2 | username: string
3 | truename: string
4 | password: string
5 | status: boolean
6 | phone: string
7 | role: string
8 | }
9 |
10 | export interface User extends IUserDeatil{
11 | role_str: string
12 | }
13 |
14 | export interface IUserDeatil {
15 | id: string
16 | username: string
17 | truename: string
18 | phone: string
19 | status: boolean
20 | role: string
21 | }
22 |
--------------------------------------------------------------------------------
/src/api/upload.ts:
--------------------------------------------------------------------------------
1 | import axios from '@src/utils/request'
2 |
3 | const uploadToken = async (ak: string, sk: string, buckety: string) => {
4 | const res = await axios({
5 | url: '/uploadIToken/',
6 | method: 'POST',
7 | data: {
8 | ak,
9 | sk,
10 | buckety,
11 | },
12 | })
13 |
14 | return res.data.token
15 | }
16 |
17 | export default {
18 | uploadToken,
19 | }
20 |
--------------------------------------------------------------------------------
/src/api/user.ts:
--------------------------------------------------------------------------------
1 | import axios from '@src/utils/request'
2 | import { IList, IGetParam } from './types'
3 | import { User, FormParam, IUserDeatil } from './types/user'
4 |
5 | type GetUserListFn = (page: IGetParam)=>Promise>
6 | type AddOrEditUserFn = (data: FormParam & {id?: string})=> Promise<{message: string}>
7 | type SetUserStatusFn = (id: string, status: boolean)=> Promise<{message: string}>
8 | type DeleteUserFn = (id: string)=> Promise<{message: string}>
9 | type GetUserDetailFn = (id: string)=> Promise
10 |
11 | /* 获取用户列表 / 全部 */
12 | const getUserList: GetUserListFn = async (data) => {
13 | const res = await axios({
14 | url: '/user/list/',
15 | method: 'POST',
16 | data,
17 | })
18 | return res
19 | }
20 |
21 | /* 用户编辑或添加 */
22 | const addOrEditUser: AddOrEditUserFn = (data) => {
23 | const path = data?.id ? 'edit' : 'add'
24 | return axios({
25 | url: `/user/${path}/`,
26 | method: 'POST',
27 | data,
28 | })
29 | }
30 | /* 用户禁启用 */
31 | const setUserStatus: SetUserStatusFn = (id, status) => axios({
32 | url: '/user/status/',
33 | method: 'POST',
34 | data: {
35 | id,
36 | status,
37 | },
38 | })
39 |
40 | /* 用户删除 */
41 | const deleteUser: DeleteUserFn = (id) => axios({
42 | url: '/user/delete/',
43 | method: 'POST',
44 | data: {
45 | id,
46 | },
47 | })
48 |
49 | /* 用户详情 */
50 | const getUserDetail: GetUserDetailFn = async (id) => {
51 | const res = await axios({
52 | url: '/user/detailed/',
53 | method: 'POST',
54 | data: {
55 | id,
56 | },
57 | })
58 | return res.data
59 | }
60 |
61 | export default {
62 | getUserList,
63 | addOrEditUser,
64 | deleteUser,
65 | setUserStatus,
66 | getUserDetail,
67 | }
68 |
--------------------------------------------------------------------------------
/src/assets/data.ts:
--------------------------------------------------------------------------------
1 | export const nation = [
2 | {
3 | text: '汉族',
4 | value: 'HA',
5 | num: '01',
6 | },
7 | {
8 | text: '蒙古族',
9 | value: 'MG',
10 | num: '02',
11 | },
12 | {
13 | text: '回族',
14 | value: 'HU',
15 | num: '03',
16 | },
17 | {
18 | text: '藏族',
19 | value: 'ZA',
20 | num: '04',
21 | },
22 | {
23 | text: '维吾尔族',
24 | value: 'UG',
25 | num: '05',
26 | },
27 | {
28 | text: '苗族',
29 | value: 'MH',
30 | num: '06',
31 | },
32 | {
33 | text: '彝族',
34 | value: 'YI',
35 | num: '07',
36 | },
37 | {
38 | text: '壮族',
39 | value: 'ZH',
40 | num: '08',
41 | },
42 | {
43 | text: '布依族',
44 | value: 'BY',
45 | num: '09',
46 | },
47 | {
48 | text: '朝鲜族',
49 | value: 'CS',
50 | num: '10',
51 | },
52 | {
53 | text: '满族',
54 | value: 'MA',
55 | num: '11',
56 | },
57 | {
58 | text: '侗族',
59 | value: 'DO',
60 | num: '12',
61 | },
62 | {
63 | text: '瑶族',
64 | value: 'YA',
65 | num: '13',
66 | },
67 | {
68 | text: '白族',
69 | value: 'BA',
70 | num: '14',
71 | },
72 | {
73 | text: '土家族',
74 | value: 'TJ',
75 | num: '15',
76 | },
77 | {
78 | text: '哈尼族',
79 | value: 'HN',
80 | num: '16',
81 | },
82 | {
83 | text: '哈萨克族',
84 | value: 'KZ',
85 | num: '17',
86 | },
87 | {
88 | text: '傣族',
89 | value: 'DA',
90 | num: '18',
91 | },
92 | {
93 | text: '黎族',
94 | value: 'LI',
95 | num: '19',
96 | },
97 | {
98 | text: '傈僳族',
99 | value: 'LS',
100 | num: '20',
101 | },
102 | {
103 | text: '佤族',
104 | value: 'VA',
105 | num: '21',
106 | },
107 | {
108 | text: '畲族',
109 | value: 'SH',
110 | num: '22',
111 | },
112 | {
113 | text: '高山族',
114 | value: 'GS',
115 | num: '23',
116 | },
117 | {
118 | text: '拉祜族',
119 | value: 'LH',
120 | num: '24',
121 | },
122 | {
123 | text: '水族',
124 | value: 'SU',
125 | num: '25',
126 | },
127 | {
128 | text: '东乡族',
129 | value: 'DX',
130 | num: '26',
131 | },
132 | {
133 | text: '纳西族',
134 | value: 'NX',
135 | num: '27',
136 | },
137 | {
138 | text: '景颇族',
139 | value: 'JP',
140 | num: '28',
141 | },
142 | {
143 | text: '阿昌族',
144 | value: 'AC',
145 | num: '29',
146 | },
147 | {
148 | text: '柯尔克孜族',
149 | value: 'KG',
150 | num: '29',
151 | },
152 | {
153 | text: '土族',
154 | value: 'TU',
155 | num: '30',
156 | },
157 | {
158 | text: '达斡尔族',
159 | value: 'DU',
160 | num: '31',
161 | },
162 | {
163 | text: '仫佬族',
164 | value: 'ML',
165 | num: '32',
166 | },
167 | {
168 | text: '羌族',
169 | value: 'QI',
170 | num: '33',
171 | },
172 | {
173 | text: '布朗族',
174 | value: 'BL',
175 | num: '34',
176 | },
177 | {
178 | text: '撒拉族',
179 | value: 'SL',
180 | num: '35',
181 | },
182 | {
183 | text: '毛南族',
184 | value: 'MN',
185 | num: '36',
186 | },
187 | {
188 | text: '仡佬族',
189 | value: 'GL',
190 | num: '37',
191 | },
192 | {
193 | text: '锡伯族',
194 | value: 'XB',
195 | num: '38',
196 | },
197 | {
198 | text: '普米族',
199 | value: 'PM',
200 | num: '40',
201 | },
202 | {
203 | text: '塔吉克族',
204 | value: 'TA',
205 | num: '41',
206 | },
207 | {
208 | text: '怒族',
209 | value: 'NU',
210 | num: '42',
211 | },
212 | {
213 | text: '乌兹别克族',
214 | value: 'UZ',
215 | num: '43',
216 | },
217 | {
218 | text: '俄罗斯族',
219 | value: 'RS',
220 | num: '44',
221 | },
222 | {
223 | text: '鄂温克族',
224 | value: 'EW',
225 | num: '45',
226 | },
227 | {
228 | text: '德昂族',
229 | value: 'DE',
230 | num: '46',
231 | },
232 | {
233 | text: '保安族',
234 | value: 'BN',
235 | num: '47',
236 | },
237 | {
238 | text: '裕固族',
239 | value: 'YG',
240 | num: '48',
241 | },
242 | {
243 | text: '京族',
244 | value: 'GI',
245 | num: '49',
246 | },
247 | {
248 | text: '塔塔尔族',
249 | value: 'TT',
250 | num: '50',
251 | },
252 | {
253 | text: '独龙族',
254 | value: 'DR',
255 | num: '51',
256 | },
257 | {
258 | text: '鄂伦春族',
259 | value: 'OR',
260 | num: '52',
261 | },
262 | {
263 | text: '赫哲族',
264 | value: 'HZ',
265 | num: '53',
266 | },
267 | {
268 | text: '门巴族',
269 | value: 'MB',
270 | num: '54',
271 | },
272 | {
273 | text: '珞巴族',
274 | value: 'LB',
275 | num: '55',
276 | },
277 | {
278 | text: '基诺族',
279 | value: 'JN',
280 | num: '56',
281 | },
282 | ]
283 |
284 | export const face = [
285 | { value: '01', text: '中共党员' },
286 | { value: '02', text: '中共预备党员' },
287 | { value: '03', text: '共青团员' },
288 | { value: '04', text: '民革会员' },
289 | { value: '05', text: '民盟盟员' },
290 | { value: '06', text: '民建会员' },
291 | { value: '07', text: '民进会员' },
292 | { value: '08', text: '农工党党员' },
293 | { value: '09', text: '致公党党员' },
294 | { value: '10', text: '九三学社社员' },
295 | { value: '11', text: '台盟盟员' },
296 | { value: '12', text: '无党派民主人士' },
297 | { value: '13', text: '群众' },
298 | ]
299 |
300 | export const education = [
301 | { text: '小学', value: 'primary_school' },
302 | { text: '初中', value: 'junior_middle' },
303 | { text: '中专/高中', value: 'secondary_specialized' },
304 | { text: '专科', value: 'specialty' },
305 | { text: '本科', value: 'undergraduate' },
306 | { text: '硕士', value: 'master' },
307 | { text: '博士', value: 'doctor' },
308 | ]
309 |
310 | export const StatusText = [
311 | {
312 | value: false,
313 | text: '禁用',
314 | },
315 | {
316 | value: true,
317 | text: '启用',
318 | },
319 | ]
320 |
321 | export const JobStatus = [
322 | {
323 | value: 'online',
324 | text: '在职',
325 | },
326 | {
327 | value: 'vacation',
328 | text: '休假',
329 | },
330 | {
331 | value: 'quit',
332 | text: '离职',
333 | },
334 | ]
335 |
--------------------------------------------------------------------------------
/src/assets/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/src/assets/images/avatar.png
--------------------------------------------------------------------------------
/src/assets/normalize.less:
--------------------------------------------------------------------------------
1 | html,body{height:100%;}
2 | html,body,h1,h2,h3,h4,h5,h6,div,dl,dt,dd,ul,ol,li,p,blockquote,pre,hr,figure,table,caption,th,td,form,fieldset,legend,input,button,textarea,menu{margin:0;padding:0;}
3 | header,footer,section,article,aside,nav,hgroup,address,figure,figcaption,menu,details{display:block;}
4 | table{border-collapse:collapse;border-spacing:0;}
5 | caption,th{text-align:left;font-weight:normal;}
6 | html,body,fieldset,img,iframe,abbr{border:0;}
7 | i,cite,em,var,address,dfn{font-style:normal;}
8 | [hidefocus],summary{outline:0;}
9 | li{list-style:none;}
10 | h1,h2,h3,h4,h5,h6,small{font-size:100%;}
11 | sup,sub{font-size:83%;}
12 | pre,code,kbd,samp{font-family:inherit;}
13 | q:before,q:after{content:none;}
14 | textarea{overflow:auto;resize:none;}
15 | label,summary{cursor:default;}
16 | a,button{cursor:pointer;}
17 | h1,h2,h3,h4,h5,h6,em,strong,b{font-weight:bold;}
18 | del,ins,u,s,a,a:hover{text-decoration:none;}
19 | body,textarea,input,button,select,keygen,legend{font:12px/1.14 Microsoft YaHei,arial,\5b8b\4f53;color:#333;outline:0;}
20 | body{background:#fff;}
21 | a,a:hover{color:#333;}
22 | *{box-sizing: border-box;}
--------------------------------------------------------------------------------
/src/components/AuthWrapper/index.tsx:
--------------------------------------------------------------------------------
1 | import { Roles } from '@src/router/type'
2 | import { IStoreState } from '@src/store/type'
3 | import React, { memo } from 'react'
4 | import { connect } from 'react-redux'
5 |
6 | interface IProps {
7 | roles?: Roles[]
8 | role?: Roles
9 | component: any
10 | noMatch?: React.ReactNode | null // 不匹配后的结果
11 | }
12 |
13 | export const checkAuth = (roles?: Roles[], auth?: Roles) => {
14 | if (!roles || auth === 'admin') {
15 | return true
16 | }
17 | /* 判断用户是否在校验表中,条件渲染组件 */
18 | return !!roles.includes(auth as never)
19 | }
20 |
21 | const AuthWrapper: React.FC = memo(({
22 | roles, role, component: Component, noMatch = null,
23 | }) => (
24 | checkAuth(roles, role) ? Component : noMatch
25 | ))
26 |
27 | export default connect(({ user: { role } }: IStoreState) => ({ role }), null)(AuthWrapper)
28 |
--------------------------------------------------------------------------------
/src/components/BasisTable/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { PaginationProps, Table, TableProps } from 'antd'
3 |
4 | interface IProps extends TableProps{
5 | // eslint-disable-next-line react/require-default-props
6 | data?: T[]
7 | // eslint-disable-next-line react/require-default-props
8 | total?: number
9 | children: React.ReactNode
10 | onChange: (pageParams: PaginationProps)=>void
11 | }
12 |
13 | function BasisTable(props: IProps) {
14 | const {
15 | data, total, loading, onChange, children, ...resetProps
16 | } = props
17 | const [pagination, setPagination] = useState({
18 | defaultCurrent: 1,
19 | defaultPageSize: 10,
20 | showQuickJumper: true,
21 | })
22 |
23 | const onTableChange = (pageParams: PaginationProps) => {
24 | setPagination(pageParams)
25 | onChange(pageParams)
26 | }
27 |
28 | return (
29 |
30 | {...resetProps}
31 | loading={loading}
32 | dataSource={data && data}
33 | bordered
34 | rowKey={(record) => (record?.id || record?.jobId || record?.staff_id || '')}
35 | pagination={{
36 | ...pagination,
37 | total: total && total,
38 | showTotal: (total) => `总共${total}条`,
39 | }}
40 | onChange={onTableChange}>
41 | {children}
42 |
43 | )
44 | }
45 |
46 | export default BasisTable
47 |
--------------------------------------------------------------------------------
/src/components/Breadcrumbs/index.less:
--------------------------------------------------------------------------------
1 | .breadcrumb-container {
2 | display: flex;
3 | align-items: center;
4 |
5 | .ant-breadcrumb {
6 | margin-left: 8px;
7 | font-size: 14px;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/components/Breadcrumbs/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useState } from 'react'
2 | import { Breadcrumb } from 'antd'
3 | import { getBreadcrumbs } from '@src/router/utils'
4 | import { IRoute } from '@src/router/type'
5 | import { Link, useLocation } from 'react-router-dom'
6 | import './index.less'
7 |
8 | const Breadcrumbs: React.FC = memo(() => {
9 | const [breadcrumbs, setBreadcrumbs] = useState([])
10 | const { pathname } = useLocation()
11 |
12 | useEffect(() => {
13 | setBreadcrumbs(getBreadcrumbs(pathname))
14 | }, [pathname])
15 |
16 | return (
17 |
18 |
19 | {breadcrumbs.map((item, index) => (
20 | index === breadcrumbs.length - 1 ? {item.meta?.title}
21 | : (
22 |
23 | {item.meta?.title}
24 | {/*
25 | {item.meta.title}
26 | */}
27 |
28 | )))}
29 |
30 |
31 | )
32 | })
33 |
34 | export default Breadcrumbs
35 |
--------------------------------------------------------------------------------
/src/components/Captcha/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | memo, useState, useEffect, useRef,
3 | } from 'react'
4 | import { IDictionary } from '@src/typings/global'
5 | import { Button, FormInstance, message } from 'antd'
6 | import authApi from '@src/api/auth'
7 | import { useAsyncFn, useCounter } from 'react-use'
8 |
9 | const COUNT_STATIC = 60
10 | interface IProps {
11 | module: 'login' | 'register'
12 | form: FormInstance
13 | }
14 |
15 | interface BtnState {
16 | state: boolean
17 | info: string
18 | }
19 |
20 | const currentState: IDictionary = {
21 | init: {
22 | info: '获取验证码',
23 | state: false,
24 | },
25 | sending: {
26 | info: '发送中',
27 | state: true,
28 | },
29 | countDown: {
30 | info: 'S',
31 | state: true,
32 | },
33 | Error: {
34 | info: '重新获取',
35 | state: false,
36 | },
37 | }
38 |
39 | const Captcha: React.FC = memo(({ module, form }) => {
40 | const [state, setState] = useState(currentState.init)
41 | const [, { get, set, reset }] = useCounter(COUNT_STATIC)
42 | const timer = useRef(null)
43 | const [, getSmsFn] = useAsyncFn(authApi.getSms)
44 |
45 | /* 清理定时器 */
46 | useEffect(() => () => {
47 | if (timer.current)clearInterval(timer.current)
48 | }, [])
49 |
50 | /* 按钮状态 */
51 | const handleGetCaptcha = async () => {
52 | form.validateFields(['username']).then((res) => {
53 | setState(currentState.sending)
54 | getSmsFn({ username: res.username, module }).then((data) => {
55 | /* 校验通过,开始倒计时 */
56 | message.success(data.message)
57 | timer.current = window.setInterval(() => {
58 | set((value) => value - 1)
59 | setState({
60 | info: `${get()}${currentState.countDown.info}`,
61 | state: currentState.countDown.state,
62 | })
63 |
64 | if (get() <= 0) {
65 | clearInterval(Number(timer.current))
66 | setState(currentState.Error)
67 | reset()
68 | }
69 | }, 1000)
70 | }).catch(() => {
71 | /* 校验失败,需重新获取验证码 */
72 | setState(currentState.Error)
73 | reset()
74 | })
75 | })
76 | }
77 |
78 | return (
79 |
82 | )
83 | })
84 |
85 | export default Captcha
86 |
--------------------------------------------------------------------------------
/src/components/Editor/config.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | height: '500px',
3 | width: '100%',
4 | language: 'zh_CN',
5 | plugins: 'table lists link image preview code',
6 | toolbar: `formatselect | code | preview | bold italic strikethrough forecolor backcolor |
7 | link image | alignleft aligncenter alignright alignjustify |
8 | numlist bullist outdent indent`,
9 | relative_urls: false,
10 | file_picker_types: 'image',
11 | images_upload_url: 'http',
12 | image_advtab: true,
13 | image_uploadtab: true,
14 | images_upload_handler: (blobInfo: any, success: any, failure: any) => {
15 | let formData;
16 | const file = blobInfo.blob() // 转化为易于理解的file对象
17 | // eslint-disable-next-line prefer-const
18 | formData = new FormData()
19 | formData.append('file', file, file.name)// 此处与源文档不一样
20 | /* Upload(formData).then((response: any) => {
21 | const data = response.data.data.url
22 | success(data)
23 | }).catch((error: any) => {
24 | const { data } = error
25 | failure(data.message)
26 | }) */
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Editor/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { Editor } from '@tinymce/tinymce-react'
3 | import config from './config'
4 |
5 | interface IProps {
6 | value?: any
7 | onChange?: (value: string)=>void
8 | }
9 |
10 | const MyEditor: React.FC = memo(({ value, onChange }) => (
11 | onChange && onChange(value.level.content)}
15 | initialValue={value}
16 | init={{ ...config }} />
17 | ))
18 |
19 | export default MyEditor
20 |
--------------------------------------------------------------------------------
/src/components/FormItem/DepartmentItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { StatusText } from '@src/assets/data'
3 | import FormInput from '../../FromInput/input'
4 | import { formProps, ItemConfig, ItemProps } from '../type'
5 |
6 | interface JobItemType {
7 | Name: React.FC
8 | Number: React.FC
9 | Status: React.FC
10 | Content: React.FC
11 | }
12 |
13 | const config: {[key in keyof JobItemType]: ItemConfig} = {
14 | Name: {
15 | name: 'name',
16 | label: '部门名称',
17 | rules: [{ required: true, message: '请输入部门名称!' }],
18 | inputProps: {
19 | placeholder: '部门名称',
20 | type: 'text',
21 | },
22 | },
23 | Number: {
24 | name: 'number',
25 | label: '人员数量',
26 | rules: [{ required: true, message: '请选择人员数量!' }],
27 | inputProps: {
28 | placeholder: '部门名称',
29 | type: 'number',
30 | min: 1,
31 | max: 100,
32 | },
33 | },
34 | Status: {
35 | name: 'status',
36 | label: '禁启用',
37 | inputProps: {
38 | type: 'radio',
39 | },
40 | optionItem: StatusText,
41 | },
42 | Content: {
43 | name: 'content',
44 | label: '描述',
45 | rows: 10,
46 | inputProps: {
47 | type: 'textArea',
48 | },
49 | },
50 | }
51 |
52 | function Name(props: ItemProps) {
53 | return (
54 |
58 | )
59 | }
60 |
61 | function Number(props: ItemProps) {
62 | return (
63 |
67 | )
68 | }
69 |
70 | function Status(props: ItemProps) {
71 | return (
72 |
76 | )
77 | }
78 |
79 | function Content(props: ItemProps) {
80 | return (
81 |
85 | )
86 | }
87 |
88 | const DepartmentItem: JobItemType = {
89 | Name: memo(Name),
90 | Number: memo(Number),
91 | Status: memo(Status),
92 | Content: memo(Content),
93 | }
94 |
95 | export default DepartmentItem
96 |
--------------------------------------------------------------------------------
/src/components/FormItem/JobItem/index.tsx:
--------------------------------------------------------------------------------
1 | import { StatusText } from '@src/assets/data'
2 | import React, { memo } from 'react'
3 | import FormInput from '../../FromInput/input'
4 | import { formProps, ItemConfig, ItemProps } from '../type'
5 |
6 | interface JobItemType {
7 | Name: React.FC
8 | JobName: React.FC
9 | Status: React.FC
10 | Content: React.FC
11 | }
12 |
13 | const config: {[key in keyof JobItemType]: ItemConfig} = {
14 | Name: {
15 | name: 'parentId',
16 | label: '部门',
17 | rules: [{ required: true, message: '请选择部门名称!' }],
18 | inputProps: {
19 | placeholder: '请选择',
20 | type: 'select',
21 | },
22 | },
23 | JobName: {
24 | name: 'jobName',
25 | label: '职位名称',
26 | rules: [{ required: true, message: '请输入职位名称!' }],
27 | inputProps: {
28 | placeholder: '职位名称',
29 | type: 'text',
30 | },
31 | },
32 | Status: {
33 | name: 'status',
34 | label: '禁启用',
35 | inputProps: {
36 | type: 'radio',
37 | },
38 | optionItem: StatusText,
39 | },
40 | Content: {
41 | name: 'content',
42 | label: '描述',
43 | inputProps: {
44 | type: 'textArea',
45 | },
46 | rows: 10,
47 | },
48 | }
49 |
50 | function Name(props: ItemProps) {
51 | return (
52 |
56 | )
57 | }
58 |
59 | function JobName(props: ItemProps) {
60 | return (
61 |
65 | )
66 | }
67 |
68 | function Status(props: ItemProps) {
69 | return (
70 |
74 | )
75 | }
76 |
77 | function Content(props: ItemProps) {
78 | return (
79 |
83 | )
84 | }
85 |
86 | const JobItem: JobItemType = {
87 | Name: memo(Name),
88 | JobName: memo(JobName),
89 | Status: memo(Status),
90 | Content: memo(Content),
91 | }
92 |
93 | export default JobItem
94 |
--------------------------------------------------------------------------------
/src/components/FormItem/LoginItem/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CaptchaRule, ConfirmRule, PassWordRule, UserNameRule,
3 | } from '@src/constants/validate'
4 | import { FormItemProps } from 'antd'
5 | import React, { memo } from 'react'
6 | import { UserOutlined, LockOutlined, CreditCardOutlined } from '@ant-design/icons'
7 | import FormInput from '../../FromInput/input'
8 | import { formProps, ItemConfig, ItemProps } from '../type'
9 |
10 | interface LoginItemType {
11 | UserName: React.FC
12 | PassWord: React.FC
13 | Confirm: React.FC
14 | Code: React.FC
15 | }
16 |
17 | const config: {[key in keyof LoginItemType]: ItemConfig} = {
18 | UserName: {
19 | name: 'username',
20 | rules: UserNameRule,
21 | inputProps: {
22 | prefix: ,
23 | placeholder: '用户名',
24 | type: 'text',
25 | },
26 | },
27 | PassWord: {
28 | name: 'password',
29 | rules: PassWordRule,
30 | inputProps: {
31 | prefix: ,
32 | placeholder: '密码',
33 | type: 'password',
34 | /* visibilityToggle: true, */
35 | },
36 | },
37 | Confirm: {
38 | name: 'cpassword',
39 | rules: ConfirmRule,
40 | inputProps: {
41 | prefix: ,
42 | placeholder: '重复密码',
43 | type: 'password',
44 | /* visibilityToggle: true, */
45 | },
46 | },
47 | Code: {
48 | name: 'code',
49 | rules: CaptchaRule,
50 | inputProps: {
51 | prefix: ,
52 | placeholder: '验证码',
53 | type: 'code',
54 | },
55 | },
56 | }
57 |
58 | interface LoginItemProps extends ItemProps {
59 | // eslint-disable-next-line react/require-default-props
60 | module?: 'register' | 'login'
61 | }
62 |
63 | function UserName(props: LoginItemProps) {
64 | return (
65 |
69 | )
70 | }
71 |
72 | function PassWord(props: LoginItemProps) {
73 | return (
74 |
78 | )
79 | }
80 |
81 | function Confirm(props: LoginItemProps) {
82 | return (
83 |
87 | )
88 | }
89 |
90 | function Code(props: LoginItemProps) {
91 | return (
92 |
96 | )
97 | }
98 |
99 | const LoginItem: LoginItemType = {
100 | UserName: memo(UserName),
101 | PassWord: memo(PassWord),
102 | Confirm: memo(Confirm),
103 | Code: memo(Code),
104 | }
105 |
106 | export default LoginItem
107 |
--------------------------------------------------------------------------------
/src/components/FormItem/StaffItem/config.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | education, face, JobStatus, nation, StatusText,
4 | } from '@src/assets/data'
5 | import { CARDREG, PHONEREG } from '@src/constants/validate'
6 | import { ItemConfig, ItemProps } from '../type'
7 |
8 | export interface StaffItemType {
9 | Name: React.FC
10 | Sex: React.FC
11 | FaceImg: React.FC
12 | CardId: React.FC
13 | DiplomaImg: React.FC
14 | Birthday: React.FC
15 | Phone: React.FC
16 | Nation: React.FC
17 | Political: React.FC
18 | School: React.FC
19 | Education: React.FC
20 | Major: React.FC
21 | Wechat: React.FC
22 | Email: React.FC
23 | JobId: React.FC
24 | DepartmenId: React.FC
25 | JobStatusDate: React.FC
26 | JobStatus: React.FC
27 | CompanyEmail: React.FC
28 | Introduce: React.FC
29 | Status: React.FC
30 | JobEntryDate: React.FC
31 | JobFormalDate: React.FC
32 | JobQuitDate: React.FC
33 | }
34 |
35 | export const config: {[key in keyof StaffItemType]: ItemConfig} = {
36 | Name: {
37 | name: 'name',
38 | rules: [{ required: true, message: '请输入姓名' }],
39 | label: '姓名',
40 | inputProps: {
41 | placeholder: '用户名',
42 | type: 'text',
43 | },
44 | },
45 | Sex: {
46 | name: 'sex',
47 | rules: [{ required: true, message: '请选择性别' }],
48 | label: '性别',
49 | optionItem: [
50 | {
51 | value: true,
52 | text: '男',
53 | },
54 | {
55 | value: false,
56 | text: '女',
57 | },
58 | ],
59 | inputProps: {
60 | type: 'radio',
61 | },
62 | },
63 | FaceImg: {
64 | name: 'face_img',
65 | label: '头像',
66 | inputProps: {
67 | type: 'file',
68 | },
69 | },
70 | CardId: {
71 | name: 'card_id',
72 | label: '身份证号',
73 | rules: [{ pattern: CARDREG, message: '请输入合法的身份证' }],
74 | inputProps: {
75 | placeholder: '身份证',
76 | type: 'text',
77 | },
78 | },
79 | DiplomaImg: {
80 | name: 'diploma_img',
81 | label: '毕业证',
82 | inputProps: {
83 | placeholder: '毕业证',
84 | type: 'file',
85 | },
86 | },
87 | Birthday: {
88 | name: 'birthday',
89 | label: '出生年月',
90 | format: 'YYYY/MM',
91 | inputProps: {
92 | placeholder: '出生年月',
93 | type: 'month',
94 | },
95 | },
96 | Phone: {
97 | name: 'phone',
98 | label: '手机',
99 | rules: [{ pattern: PHONEREG, message: '请输入合法的手机号' }],
100 | inputProps: {
101 | placeholder: '手机号',
102 | type: 'text',
103 | },
104 | },
105 | Nation: {
106 | name: 'nation',
107 | label: '民族',
108 | rules: [{ required: true, message: '请选择民族' }],
109 | inputProps: {
110 | type: 'select',
111 | },
112 | optionItem: nation,
113 | },
114 | Political: {
115 | name: 'political',
116 | label: '政治面貌',
117 | rules: [{ required: true, message: '请选择政治面貌' }],
118 | inputProps: {
119 | type: 'select',
120 | },
121 | optionItem: face,
122 | },
123 | School: {
124 | name: 'school',
125 | label: '毕业院校',
126 | inputProps: {
127 | type: 'text',
128 | },
129 | },
130 | Education: {
131 | name: 'education',
132 | label: '学历',
133 | inputProps: {
134 | type: 'select',
135 | },
136 | optionItem: education,
137 | },
138 | Major: {
139 | name: 'major',
140 | label: '专业',
141 | inputProps: {
142 | type: 'text',
143 | },
144 | },
145 | Wechat: {
146 | name: 'wechat',
147 | label: '微信号',
148 | inputProps: {
149 | type: 'text',
150 | },
151 | },
152 | Email: {
153 | name: 'email',
154 | label: '个人邮箱',
155 | rules: [{ type: 'email', message: '请输入合法的邮箱' }],
156 | inputProps: {
157 | type: 'text',
158 | },
159 | },
160 | JobId: {
161 | name: 'job_id',
162 | label: '职位',
163 | rules: [{ required: true, message: '请选择职位' }],
164 | inputProps: {
165 | type: 'select',
166 | },
167 | },
168 | DepartmenId: {
169 | name: 'departmen_id',
170 | label: '部门',
171 | rules: [{ required: true, message: '请选择部门' }],
172 | inputProps: {
173 | type: 'select',
174 | },
175 | },
176 | JobStatusDate: {
177 | name: 'job_status_date',
178 | label: '工作时间',
179 | rules: [{ required: true, message: '请选择工作时间' }],
180 | format: 'DD/MM/YYYY',
181 | picker: 'date',
182 | inputProps: {
183 | type: 'month',
184 | placeholder: '工作时间',
185 | },
186 | optionItem: nation,
187 | },
188 | JobStatus: {
189 | name: 'job_status',
190 | label: '职位状态',
191 | rules: [{ required: true, message: '请选择职位状态' }],
192 | inputProps: {
193 | type: 'radio',
194 | },
195 | optionItem: JobStatus,
196 | },
197 | CompanyEmail: {
198 | name: 'company_email',
199 | label: '公司邮箱',
200 | rules: [{ type: 'email', message: '公司邮箱不符合规范' }],
201 | inputProps: {
202 | type: 'text',
203 | },
204 | },
205 | Status: {
206 | name: 'status',
207 | label: '禁启用',
208 | rules: [{ required: true, message: '请选择禁启用' }],
209 | inputProps: {
210 | type: 'radio',
211 | },
212 | optionItem: StatusText,
213 | },
214 | JobEntryDate: {
215 | name: 'job_entry_date',
216 | label: '入职日期',
217 | rules: [{ required: true, message: '请选择入职日期' }],
218 | inputProps: {
219 | type: 'month',
220 | },
221 | },
222 | JobFormalDate: {
223 | name: 'job_formal_date',
224 | label: '转正日期',
225 | rules: [{ required: true, message: '请选择转正日期' }],
226 | inputProps: {
227 | type: 'month',
228 | },
229 | },
230 | JobQuitDate: {
231 | name: 'job_quit_date',
232 | label: '离职日期',
233 | rules: [{ required: true, message: '请选择离职日期' }],
234 | inputProps: {
235 | type: 'month',
236 | },
237 | },
238 | Introduce: {
239 | name: 'introduce',
240 | label: '描述',
241 | inputProps: {
242 | type: 'editor',
243 | },
244 | },
245 | }
246 |
--------------------------------------------------------------------------------
/src/components/FormItem/StaffItem/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import FormInput from '../../FromInput/input'
3 | import { formProps, ItemProps } from '../type'
4 | import { config, StaffItemType } from './config'
5 |
6 | function Name(props: ItemProps) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | function Sex(props: ItemProps) {
16 | return (
17 |
21 | )
22 | }
23 |
24 | function FaceImg(props: ItemProps) {
25 | return (
26 |
30 | )
31 | }
32 |
33 | function CardId(props: ItemProps) {
34 | return (
35 |
39 | )
40 | }
41 |
42 | function DiplomaImg(props: ItemProps) {
43 | return (
44 |
48 | )
49 | }
50 |
51 | function Birthday(props: ItemProps) {
52 | return (
53 |
57 | )
58 | }
59 |
60 | function Phone(props: ItemProps) {
61 | return (
62 |
66 | )
67 | }
68 |
69 | function Nation(props: ItemProps) {
70 | return (
71 |
75 | )
76 | }
77 |
78 | function Political(props: ItemProps) {
79 | return (
80 |
84 | )
85 | }
86 |
87 | function School(props: ItemProps) {
88 | return (
89 |
93 | )
94 | }
95 |
96 | function Education(props: ItemProps) {
97 | return (
98 |
102 | )
103 | }
104 |
105 | function Major(props: ItemProps) {
106 | return (
107 |
111 | )
112 | }
113 |
114 | function Wechat(props: ItemProps) {
115 | return (
116 |
120 | )
121 | }
122 |
123 | function Email(props: ItemProps) {
124 | return (
125 |
129 | )
130 | }
131 |
132 | function JobId(props: ItemProps) {
133 | return (
134 |
138 | )
139 | }
140 |
141 | function DepartmenId(props: ItemProps) {
142 | return (
143 |
147 | )
148 | }
149 |
150 | function JobStatusDate(props: ItemProps) {
151 | return (
152 |
156 | )
157 | }
158 |
159 | function JobStatus(props: ItemProps) {
160 | return (
161 |
165 | )
166 | }
167 |
168 | function CompanyEmail(props: ItemProps) {
169 | return (
170 |
174 | )
175 | }
176 |
177 | function Introduce(props: ItemProps) {
178 | return (
179 |
183 | )
184 | }
185 |
186 | function Status(props: ItemProps) {
187 | return (
188 |
192 | )
193 | }
194 |
195 | function JobEntryDate(props: ItemProps) {
196 | return (
197 |
201 | )
202 | }
203 |
204 | function JobFormalDate(props: ItemProps) {
205 | return (
206 |
210 | )
211 | }
212 |
213 | function JobQuitDate(props: ItemProps) {
214 | return (
215 |
219 | )
220 | }
221 |
222 | const StaffItem: StaffItemType = {
223 | Name: memo(Name),
224 | Sex: memo(Sex),
225 | FaceImg: memo(FaceImg),
226 | CardId: memo(CardId),
227 | DiplomaImg: memo(DiplomaImg),
228 | Birthday: memo(Birthday),
229 | Phone: memo(Phone),
230 | Nation: memo(Nation),
231 | Political: memo(Political),
232 | School: memo(School),
233 | Education: memo(Education),
234 | Major: memo(Major),
235 | Wechat: memo(Wechat),
236 | Email: memo(Email),
237 | JobId: memo(JobId),
238 | DepartmenId: memo(DepartmenId),
239 | JobStatusDate: memo(JobStatusDate),
240 | JobStatus: memo(JobStatus),
241 | CompanyEmail: memo(CompanyEmail),
242 | Introduce: memo(Introduce),
243 | Status: memo(Status),
244 | JobEntryDate: memo(JobEntryDate),
245 | JobFormalDate: memo(JobFormalDate),
246 | JobQuitDate: memo(JobQuitDate),
247 | }
248 |
249 | export default StaffItem
250 |
--------------------------------------------------------------------------------
/src/components/FormItem/UserItem/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ConfirmPassWord,
3 | ConfirmRule, PASSWORDREG, PassWordRule, PHONEREG, UserNameRule,
4 | } from '@src/constants/validate'
5 | import React, { memo } from 'react'
6 | import { StatusText } from '@src/assets/data'
7 | import FormInput from '../../FromInput/input'
8 | import { formProps, ItemConfig, ItemProps } from '../type'
9 |
10 | interface UserItemType {
11 | UserName: React.FC
12 | Truename: React.FC
13 | PassWord: React.FC
14 | Confirm: React.FC
15 | Status: React.FC
16 | Phone: React.FC
17 | Role: React.FC
18 | }
19 |
20 | const config: {[key in keyof UserItemType]: ItemConfig} = {
21 | UserName: {
22 | name: 'username',
23 | label: '用户名',
24 | rules: UserNameRule,
25 | inputProps: {
26 | placeholder: '用户名',
27 | type: 'text',
28 | },
29 | },
30 | Truename: {
31 | name: 'truename',
32 | label: '真实名称',
33 | rules: [{ required: true, message: '请输入真实名称' }],
34 | inputProps: {
35 | placeholder: '真实名称',
36 | type: 'text',
37 | },
38 | },
39 | PassWord: {
40 | name: 'password',
41 | label: '密码',
42 | rules: [{ pattern: PASSWORDREG, message: '输入的密码不合规范!' }],
43 | inputProps: {
44 | placeholder: '密码',
45 | type: 'password',
46 | },
47 | },
48 | Confirm: {
49 | name: 'cpassword',
50 | label: '重复密码',
51 | rules: [ConfirmPassWord],
52 | inputProps: {
53 | placeholder: '重复密码',
54 | type: 'password',
55 | },
56 | },
57 | Status: {
58 | name: 'status',
59 | label: '禁启用',
60 | rules: [{ required: true, message: '请选择' }],
61 | inputProps: {
62 | type: 'radio',
63 | },
64 | optionItem: StatusText,
65 | },
66 | Phone: {
67 | name: 'phone',
68 | label: '手机号',
69 | rules: [{ required: true, message: '请输入手机号' }, { pattern: PHONEREG, message: '请输入合法的手机号' }],
70 | inputProps: {
71 | type: 'text',
72 | },
73 | },
74 | Role: {
75 | name: 'role',
76 | label: '角色',
77 | rules: [{ required: true, message: '请分配角色' }],
78 | inputProps: {
79 | type: 'radio',
80 | },
81 | },
82 | }
83 |
84 | interface IsRequiredProps extends ItemProps {
85 | // eslint-disable-next-line react/require-default-props
86 | isRequired?: boolean
87 | }
88 |
89 | function UserName(props: ItemProps) {
90 | return (
91 |
95 | )
96 | }
97 |
98 | function Truename(props: ItemProps) {
99 | return (
100 |
104 | )
105 | }
106 |
107 | function PassWord(props: IsRequiredProps) {
108 | const { isRequired } = props
109 | return (
110 |
115 | )
116 | }
117 |
118 | function Confirm(props: IsRequiredProps) {
119 | const { isRequired } = props
120 | return (
121 |
126 | )
127 | }
128 |
129 | function Status(props: ItemProps) {
130 | return (
131 |
135 | )
136 | }
137 |
138 | function Phone(props: ItemProps) {
139 | return (
140 |
144 | )
145 | }
146 |
147 | function Role(props: ItemProps) {
148 | return (
149 |
153 | )
154 | }
155 |
156 | const UserItem: UserItemType = {
157 | UserName: memo(UserName),
158 | Truename: memo(Truename),
159 | PassWord: memo(PassWord),
160 | Confirm: memo(Confirm),
161 | Status: memo(Status),
162 | Phone: memo(Phone),
163 | Role: memo(Role),
164 | }
165 |
166 | export default UserItem
167 |
--------------------------------------------------------------------------------
/src/components/FormItem/searchItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { StatusText } from '@src/assets/data'
3 | import { ItemConfig, ItemProps } from './type'
4 | import FormInput from '../FromInput/input'
5 |
6 | interface SearchItemType {
7 | SearchName: React.FC,
8 | SearchStatus: React.FC,
9 | }
10 |
11 | export interface SearchParam {
12 | name?: string
13 | status?: boolean | undefined
14 | }
15 |
16 | const config: {[key in keyof SearchItemType]: ItemConfig} = {
17 | SearchName: {
18 | name: 'name',
19 | label: '名称',
20 | inputProps: {
21 | placeholder: '名称',
22 | type: 'text',
23 | },
24 | },
25 | SearchStatus: {
26 | name: 'status',
27 | label: '禁启用',
28 | inputProps: {
29 | type: 'select',
30 | },
31 | width: 100,
32 | optionItem: StatusText,
33 | },
34 | }
35 |
36 | function SearchName(props: ItemProps) {
37 | return (
38 |
42 | )
43 | }
44 |
45 | function SearchStatus(props: ItemProps) {
46 | return (
47 |
51 | )
52 | }
53 |
54 | const SearchItem: SearchItemType = {
55 | SearchName: memo(SearchName),
56 | SearchStatus: memo(SearchStatus),
57 | }
58 |
59 | export default SearchItem
60 |
--------------------------------------------------------------------------------
/src/components/FormItem/type.ts:
--------------------------------------------------------------------------------
1 | import { FormInstance, InputProps } from 'antd'
2 | import { FormItemProps, Rule } from 'antd/lib/form'
3 |
4 | export const formProps: FormItemProps = {
5 | hasFeedback: true,
6 | children: null,
7 | }
8 | export interface ItemConfig {
9 | name: string
10 | rules?: Rule[]
11 | label?: string
12 | inputProps: InputProps
13 | rows?: number // 富文本行数
14 | cols?: number
15 | optionItem? : OptionItemType
16 | format?: string // 格式化时间
17 | picker?: 'time' | 'date' | 'month' | 'week' | 'quarter' | 'year' | undefined
18 | loading?: boolean
19 | width?: number
20 | height?: number
21 | module?: 'register' | 'login'
22 | [params: string]: any
23 | }
24 |
25 | export interface ItemProps {
26 | form: FormInstance
27 | optionItem? : OptionItemType
28 | loading?: boolean
29 | label?: string
30 | [params: string]: any
31 | }
32 |
33 | export type OptionItemType = { value: any, text: string, [item: string]: any}[]
34 |
--------------------------------------------------------------------------------
/src/components/FromInput/input.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback } from 'react'
2 | import {
3 | Col, Input, Row, Form, Radio, InputNumber, Select, DatePicker, Upload,
4 | } from 'antd'
5 | import { FormInstance, FormItemProps } from 'antd/lib/form'
6 | import { SelectValue } from 'antd/lib/select'
7 | import Captcha from '../Captcha'
8 | import { ItemConfig } from '../FormItem/type'
9 | import UpLoadPic from '../UploadPic'
10 | import MyEditor from '../Editor'
11 |
12 | export interface FormInputProps extends ItemConfig {
13 | formProps: FormItemProps
14 | form: FormInstance
15 | onSelect?: (value: SelectValue)=> void
16 | }
17 |
18 | const FormInput: React.FC = memo((props) => {
19 | const onHandleSelect = useCallback((value: SelectValue) => {
20 | if (props.onSelect) {
21 | props?.onSelect(value)
22 | }
23 | }, [])
24 |
25 | return (
26 |
27 | {(() => {
28 | switch (props.inputProps.type) {
29 | case 'password':
30 | return
31 | case 'code':
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | case 'textArea':
43 | return (
44 |
45 | )
46 | case 'radio':
47 | return (
48 |
49 | {props.optionItem?.map((item) => {item.text})}
50 |
51 | )
52 | case 'number':
53 | return (
54 |
55 | )
56 | case 'select':
57 | return (
58 |
67 | )
68 | case 'month':
69 | return (
70 |
74 | )
75 | case 'file':
76 | return (
77 |
78 | )
79 | case 'editor':
80 | return (
81 |
82 | )
83 | default:
84 | return
85 | }
86 | })()}
87 |
88 | )
89 | })
90 |
91 | export default FormInput
92 |
--------------------------------------------------------------------------------
/src/components/Layout/AsyncRoutes/AsyncRoutes.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { Spin } from 'antd'
3 | import { connect } from 'react-redux'
4 | import { IRoute, Roles } from '@src/router/type'
5 | import { IStoreState } from '@src/store/type'
6 | import { setSideBarRoutes } from '@src/store/module/app'
7 | import TransitionMain from '@src/components/TransitionMain'
8 | import { checkAuth } from '@src/components/AuthWrapper'
9 | import { menuRouteList } from '@src/router/utils'
10 |
11 | interface AsyncRoutesProps {
12 | children: React.ReactNode
13 | init: boolean
14 | role: Roles
15 | setSideBarRoutes: (routes: IRoute[]) => void
16 | }
17 |
18 | function formatMenuToRoute(menus: IRoute[], role: Roles): IRoute[] {
19 | const result: IRoute[] = []
20 | menus.forEach((menu) => {
21 | /* 查看当前路由表是否有权限 */
22 | if (((menu?.path && checkAuth(menu.roles, role)) && !menu.hidden)) {
23 | const route: IRoute = {
24 | path: menu.path,
25 | meta: {
26 | title: menu.meta?.title || '未知',
27 | icon: menu.meta?.icon,
28 | },
29 | }
30 | if (menu.children) {
31 | route.children = formatMenuToRoute(menu.children, role)
32 | }
33 | result.push(route)
34 | }
35 | })
36 | return result
37 | }
38 |
39 | const AsyncRoutes: React.FC = (props) => {
40 | /* 需要登录后再初始化路由, 如果后端返回的路由表则不需要校验是否登录 */
41 | if (!props.init && props.role) {
42 | /*
43 | 进行侧边栏筛选渲染,查看当前路由是否有该权限
44 | 可以进行异步请求后端路由表,根据后端存储的路由进行渲染,
45 | 同时存储到Redux中,然后在Auth组件改变校验方式,也就是注释上的
46 | 如
47 | apiGetMenuList()
48 | .then(({ data }) => {
49 | props.setSideBarRoutes(formatMenuToRoute(data.list));
50 | })
51 | .catch(() => {})
52 | */
53 | props.setSideBarRoutes(formatMenuToRoute(menuRouteList, props.role))
54 |
55 | return
56 | }
57 | return {props.children}
58 | }
59 |
60 | export default connect(({ app, user: { role } }: IStoreState) => ({ init: app.init, role }), { setSideBarRoutes })(
61 | memo(AsyncRoutes),
62 | )
63 |
--------------------------------------------------------------------------------
/src/components/Layout/Auth/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { Redirect, RouteComponentProps } from 'react-router-dom'
3 | import store from '@src/store/index'
4 | import { businessRouteList } from '@src/router/utils'
5 | import { getToken } from '@src/utils/auth'
6 | import { Roles, IRoute } from '@src/router/type'
7 |
8 | interface AuthProps extends RouteComponentProps {
9 | route: IRoute
10 | children: React.ReactNode
11 | }
12 |
13 | function checkAuth(location: RouteComponentProps['location']): boolean {
14 | // redux 中的 routes 同时负责渲染 sidebar
15 | /* const { flattenRoutes } = store.getState().app */
16 |
17 | const { role } = store.getState().user
18 |
19 | // 判断当前访问路由是否在系统路由中, 不存在直接走最后默认的 404 路由
20 | const route = businessRouteList.find((child) => child.path === location.pathname)
21 |
22 | // 当前路由不存在直接返回
23 | if (!route) {
24 | return true
25 | }
26 |
27 | // 当前路由存在跳转也返回true
28 | if (route.redirect) {
29 | return true
30 | }
31 |
32 | // 不需要检验也返回true
33 | if (!route.roles) {
34 | return true
35 | }
36 |
37 | /* // 查看当前路由是否存在系统中,该用户是否有此路由权限,后端返回路由表的时候使用该方法
38 | if (!flattenRoutes.find((child) => child.path === location.pathname)) {
39 | return false
40 | } */
41 |
42 | /* 当前用户角色是否有该路由权限, admin直接放行 */
43 | if (!route.roles?.includes(role as Roles) && role !== 'admin') {
44 | return false
45 | }
46 |
47 | return true
48 | }
49 |
50 | function Auth(props: AuthProps) {
51 | // 判断是否登录
52 | if (!getToken()) {
53 | return (
54 |
60 | )
61 | }
62 |
63 | // 检查是否有权限
64 | if (!checkAuth(props.location)) {
65 | return
66 | }
67 |
68 | // 检查是否有跳转
69 | if (props.route.redirect) {
70 | return
71 | }
72 |
73 | // 进行渲染 route
74 | return <>{props.children}>
75 | }
76 |
77 | export default memo(Auth)
78 |
--------------------------------------------------------------------------------
/src/components/Layout/Header/UserInfo/index.less:
--------------------------------------------------------------------------------
1 | @import '@src/styles/mixin.less';
2 |
3 | .userinfo {
4 | display: flex;
5 | align-items: center;
6 | height: 100%;
7 |
8 | .name {
9 | font-size: 14px;
10 | padding: 0 10px;
11 | font-weight: 500;
12 | .article-container(80%);
13 | @media (max-width: 768px){
14 | .article-container(80px);
15 | }
16 | }
17 |
18 | .close {
19 | display: flex;
20 | justify-content: center;
21 | width: @header-height;
22 | align-items: center;
23 | height: 100%;
24 | border-left: 1px solid #F1F1F1;
25 | cursor: pointer;
26 | /* .anticon-close-circle {
27 | font-size: 18px;
28 | cursor: pointer;
29 | } */
30 | }
31 | }
--------------------------------------------------------------------------------
/src/components/Layout/Header/UserInfo/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback } from 'react'
2 | import { connect } from 'react-redux'
3 | import { useHistory } from 'react-router-dom'
4 | import { Popconfirm, Avatar } from 'antd'
5 | import { Player } from '@lottiefiles/react-lottie-player'
6 | import { ClosePath } from '@src/constants/lottiePath'
7 | import { logout, UserState } from '@src/store/module/user'
8 | import { clearSideBarRoutes } from '@src/store/module/app'
9 | import { IStoreState } from '@src/store/type'
10 | import avatarImg from '@src/assets/images/avatar.png'
11 | import './index.less'
12 |
13 | interface IProps {
14 | logout: () => void
15 | clearSideBarRoutes: () => void
16 | username: UserState['username']
17 | }
18 |
19 | const UserInfo: React.FC = memo((props) => {
20 | const { replace } = useHistory()
21 |
22 | const logOut = useCallback(() => {
23 | props.logout()
24 | props.clearSideBarRoutes()
25 | replace('/system/login')
26 | }, [])
27 |
28 | return (
29 |
30 |
31 |
{props.username}
32 |
38 |
45 |
46 |
47 | )
48 | })
49 |
50 | export default connect(({ user: { username } }: IStoreState) => ({ username }), {
51 | logout, clearSideBarRoutes,
52 | })(UserInfo)
53 |
--------------------------------------------------------------------------------
/src/components/Layout/Header/index.less:
--------------------------------------------------------------------------------
1 | .header {
2 | height: @header-height !important;
3 | background-color: @main-bgcolor !important;
4 | display: flex;
5 | align-items: center;
6 | justify-content: space-between;
7 | padding: 0 !important;
8 |
9 | .anticon {
10 | color: @text-gray;
11 | &:hover {
12 | color: #000;
13 | }
14 | }
15 |
16 | .anticon-menu-fold, .anticon-menu-unfold {
17 | font-size: 16px;
18 | padding-left: 30px;
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/src/components/Layout/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { useWindowSize, useDebounce } from 'react-use'
3 | import { Layout } from 'antd'
4 | import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
5 | import { AppState, updateSideBar } from '@src/store/module/app'
6 | import { Min_Width } from '@src/constants/app'
7 | import { connect } from 'react-redux'
8 | import UserInfo from './UserInfo'
9 | import './index.less'
10 |
11 | interface IProps {
12 | updateSideBar: (sidebar: AppState['sidebar']) => void
13 | sidebar: AppState['sidebar']
14 | }
15 |
16 | const Header: React.FC = memo(({ sidebar, updateSideBar }) => {
17 | const { width } = useWindowSize()
18 |
19 | useDebounce(
20 | () => {
21 | if (width < Min_Width) {
22 | updateSideBar({ ...sidebar, opened: true })
23 | }
24 | },
25 | 100,
26 | [width],
27 | )
28 |
29 | const handleToggle = () => {
30 | updateSideBar({ ...sidebar, opened: !sidebar.opened })
31 | }
32 |
33 | return (
34 |
35 | {/* 左侧切换 */}
36 | {sidebar.opened
37 | ?
38 | : }
39 | {/* 右侧,如果宽度大于460或者菜单栏不展开时显示 */}
40 | { (sidebar.opened || width > 460) && }
41 |
42 | )
43 | })
44 |
45 | export default connect(() => ({}), { updateSideBar })(Header)
46 |
--------------------------------------------------------------------------------
/src/components/Layout/Main/index.less:
--------------------------------------------------------------------------------
1 | .main {
2 | background-color: #F7F7F7;
3 | padding: 20px 20px 0 20px;
4 | .main-cover {
5 | background-color: #fff;
6 | padding: 25px;
7 | height: calc(100vh - @header-height - 64px);
8 | overflow: auto;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/components/Layout/Main/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import Breadcrumbs from '@src/components/Breadcrumbs'
3 | import { Layout } from 'antd'
4 | import './index.less'
5 |
6 | const Main: React.FC = memo(({ children }) => (
7 |
8 |
9 |
10 | {children}
11 |
12 |
13 | ))
14 |
15 | export default Main
16 |
--------------------------------------------------------------------------------
/src/components/Layout/MainRoutes/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo } from 'react'
2 | import { IRoute } from '@src/router/type'
3 | import { businessRouteList } from '@src/router/utils'
4 | import { Route } from 'react-router-dom'
5 | import Auth from '../Auth'
6 | import AsyncRoutes from '../AsyncRoutes/AsyncRoutes'
7 |
8 | function renderRoute(route: IRoute) {
9 | const { component: Component } = route
10 |
11 | return (
12 | (
17 |
18 |
19 |
20 | )} />
21 | )
22 | }
23 |
24 | /* 条件渲染/下的路由列表 */
25 | function renderRouteList(): React.ReactNode[] {
26 | const result: React.ReactNode[] = []
27 |
28 | businessRouteList.forEach((child: IRoute) => {
29 | result.push(renderRoute(child))
30 | })
31 |
32 | return result
33 | }
34 |
35 | const MainRoutes: React.FC = memo(() => {
36 | const routeList = useMemo(() => renderRouteList(), [])
37 |
38 | return (
39 | {routeList}
40 | )
41 | })
42 |
43 | export default MainRoutes
44 |
--------------------------------------------------------------------------------
/src/components/Layout/Sider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 |
3 | import { Layout } from 'antd'
4 | import SiderMenu from '@src/components/SiderMenu'
5 |
6 | interface IProps {
7 | collapsed: boolean
8 | }
9 |
10 | const Sider: React.FC = memo(({ collapsed }) => (
11 |
12 |
13 | {/* 菜单栏 */}
14 |
15 |
16 | ))
17 |
18 | export default Sider
19 |
--------------------------------------------------------------------------------
/src/components/Layout/index.less:
--------------------------------------------------------------------------------
1 | .logo {
2 | height: 32px;
3 | margin: 16px;
4 | background: rgba(255, 255, 255, 0.3);
5 | }
6 |
7 | .site-layout .site-layout-background {
8 | background: @main-bgcolor;
9 | }
--------------------------------------------------------------------------------
/src/components/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { connect } from 'react-redux'
3 | import { Layout } from 'antd'
4 | import { IStoreState } from '@src/store/type'
5 | import { AppState } from '@src/store/module/app'
6 | import Sider from './Sider'
7 | import Header from './Header'
8 | import Main from './Main'
9 | import MainRoutes from './MainRoutes'
10 | import './index.less'
11 |
12 | interface IProps {
13 | sidebar: AppState['sidebar']
14 | }
15 |
16 | const LayOut: React.FC = memo(({ sidebar }) => (
17 |
18 | {/* 侧边栏 */}
19 |
20 |
21 | {/* 顶部 */}
22 |
23 | {/* 中心区域 */}
24 |
25 |
26 |
27 |
28 |
29 | ))
30 |
31 | export default connect(({ app: { sidebar } }:IStoreState) => ({ sidebar }), null)(LayOut)
32 |
--------------------------------------------------------------------------------
/src/components/SiderMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useMemo } from 'react'
2 | import { Link, useLocation } from 'react-router-dom'
3 | import { connect } from 'react-redux'
4 | import { Menu } from 'antd'
5 | import { IRoute } from '@src/router/type'
6 | import { pathToList } from '@src/router/utils'
7 | import { IDictionary } from '@src/typings/global'
8 | import {
9 | UserOutlined, AppstoreOutlined, WechatOutlined, UsergroupAddOutlined, AuditOutlined, MailOutlined, MehOutlined, FrownOutlined,
10 | } from '@ant-design/icons'
11 | import { IStoreState } from '@src/store/type'
12 | import { Min_Width } from '@src/constants/app'
13 |
14 | const { SubMenu } = Menu
15 |
16 | interface IProps {
17 | routes?: IRoute[]
18 | }
19 |
20 | const IconMap: IDictionary = {
21 | UserOutlined: ,
22 | AppstoreOutlined: ,
23 | WechatOutlined: ,
24 | AuditOutlined: ,
25 | UsergroupAddOutlined: ,
26 | MailOutlined: ,
27 | MehOutlined: ,
28 | FrownOutlined: ,
29 | }
30 |
31 | /* 无极菜单 */
32 | const renderMenu = ({ path, meta }: IRoute) => (
33 |
34 |
35 | {meta?.title}
36 |
37 |
38 | )
39 |
40 | /* 子级菜单处理 */
41 | const renderSubMenu = ({ children, path, meta }: IRoute) => (
42 |
43 | {children?.map((item) => (
44 | item.children && item.children.length > 0 ? renderSubMenu(item) : renderMenu(item)
45 | ))}
46 |
47 | )
48 |
49 | const SiderMenu: React.FC = memo((props) => {
50 | const { pathname } = useLocation()
51 |
52 | const width = useMemo(() => document.body.clientWidth, [])
53 |
54 | return (
55 |
64 | )
65 | })
66 |
67 | export default connect(({ app: { routes } }: IStoreState) => ({ routes }), null)(SiderMenu)
68 |
--------------------------------------------------------------------------------
/src/components/TransitionMain/index.less:
--------------------------------------------------------------------------------
1 | /*入场动画开始*/
2 | .fade-enter {
3 | opacity: 0;
4 | }
5 |
6 | /*入场动画过程*/
7 | .fade-enter-active {
8 | opacity: 1;
9 | transition: opacity 1s ease-in;
10 | }
11 |
12 | /*入场动画结束*/
13 | .fade-enter-done {
14 | opacity: 1;
15 | }
16 |
17 | /*离场动画开始*/
18 | .fade-exit {
19 | opacity: 1;
20 | }
21 |
22 | /*离场动画过程*/
23 | .fade-exit-active {
24 | opacity: 0;
25 | transition: opacity 1s ease-in;
26 | }
27 |
28 | /*离场动画结束*/
29 | .fade-exit-done {
30 | opacity: 0;
31 | }
32 |
33 | /*页面第一次加载时的开始状态*/
34 | .fade-appear {
35 | opacity: 0;
36 | }
37 |
38 | /*页面第一次加载时的动画过程*/
39 | .fade-appear-active {
40 | opacity: 1;
41 | transition: opacity 1s ease-in;
42 | }
--------------------------------------------------------------------------------
/src/components/TransitionMain/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { TransitionGroup, CSSTransition } from 'react-transition-group'
3 | import { Route, Switch } from 'react-router-dom'
4 | import './index.less'
5 | import { Spin } from 'antd'
6 |
7 | interface TransitionMainProps {
8 | children: React.ReactNode
9 | }
10 |
11 | function TransitionMain({ children }: TransitionMainProps) {
12 | return (
13 | }>
14 | (
16 |
17 |
21 | {children}
22 |
23 |
24 | )} />
25 |
26 | )
27 | }
28 |
29 | export default TransitionMain;
30 |
--------------------------------------------------------------------------------
/src/components/UploadPic/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | memo, useState, useCallback, useEffect,
3 | } from 'react'
4 | import { useAsyncFn, useLocalStorage } from 'react-use'
5 | import store from 'store'
6 | import uploadApi from '@src/api/upload'
7 | import { Upload, message } from 'antd'
8 | import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'
9 | import { RcFile } from 'antd/lib/upload'
10 |
11 | interface IProps {
12 | onChange?: (value: any)=> void
13 | token: string
14 | }
15 |
16 | export const UPLOAD_TOKEN = 'admin_upload_token'
17 |
18 | const UpLoadPic: React.FC = memo(({ token, onChange }) => {
19 | const [loading, setLoading] = useState(false)
20 | const [imgUrl, setImgUrl] = useState('')
21 | const [uploadKey, setUploadKey] = useState<{token?:string, key?: string}>()
22 |
23 | const [, uploadtokenFn] = useAsyncFn(uploadApi.uploadToken)
24 |
25 | useEffect(() => {
26 | if (!token) {
27 | uploadtokenFn('dfawTwXxmuWJywb6LFiAn1a_xU8qz58dl3v7Bp74', 'gynIo9E-zyKeKBrPqeWmmgeA4DQSsl8gpuyYl9dT', 'bigbigtime').then((res) => {
28 | store.set(UPLOAD_TOKEN, res)
29 | })
30 | }
31 | }, [token])
32 |
33 | const beforeUpload = useCallback((file: RcFile) => {
34 | const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
35 | if (!isJpgOrPng) {
36 | message.error('You can only upload JPG/PNG file!')
37 | }
38 | const isLt2M = file.size / 1024 / 1024 < 2
39 | if (!isLt2M) {
40 | message.error('Image must smaller than 2MB!')
41 | }
42 | const key = encodeURI(`${file.name}`)
43 | setUploadKey({ token, key })
44 | return isJpgOrPng && isLt2M
45 | }, [])
46 |
47 | const handleChange = useCallback((info) => {
48 | if (info.file.status === 'uploading') {
49 | setLoading(true)
50 | return
51 | }
52 | if (info.file.status === 'done') {
53 | const { key } = info.file.response
54 | setImgUrl(`http://qkronr45u.hn-bkt.clouddn.com/${key}`)
55 | setLoading(false)
56 | }
57 | if (onChange) { onChange(info) }
58 | }, [])
59 |
60 | const uploadButton = (
61 |
62 | {loading ?
:
}
63 |
Upload
64 |
65 | )
66 |
67 | return (
68 |
76 | {imgUrl ?
: uploadButton}
77 |
78 | )
79 | })
80 |
81 | export default UpLoadPic
82 |
--------------------------------------------------------------------------------
/src/components/UserLayout/index.less:
--------------------------------------------------------------------------------
1 | .container {
2 | background-color: @bg-color;
3 | width: 100vw;
4 | height: 100vh;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | flex-direction: column;
9 | }
10 |
11 | .bubble-bgwall {
12 | left: 0;
13 | top: 0;
14 | overflow: hidden;
15 | position: absolute;
16 | width: 100vw;
17 | height: 100vh;
18 | z-index: 0;
19 |
20 | li {
21 | display: flex;
22 | position: absolute;
23 | bottom: -200px;
24 | justify-content: center;
25 | align-items: center;
26 | border-radius: 10px;
27 | width: 50px;
28 | height: 50px;
29 | background-color: rgba(#fff, .15);
30 | color: #ccc;
31 | animation: bubble 15s infinite;
32 | pointer-events: none;
33 |
34 | &:nth-child(1) {
35 | left: 10%;
36 | }
37 |
38 | &:nth-child(2) {
39 | left: 20%;
40 | width: 90px;
41 | height: 90px;
42 | animation-duration: 7s;
43 | animation-delay: 2s;
44 | }
45 |
46 | &:nth-child(3) {
47 | left: 25%;
48 | animation-delay: 4s;
49 | }
50 |
51 | &:nth-child(4) {
52 | left: 40%;
53 | width: 60px;
54 | height: 60px;
55 | background-color: rgba(#fff, .3);
56 | animation-duration: 8s;
57 | }
58 |
59 | &:nth-child(5) {
60 | left: 70%;
61 | }
62 |
63 | &:nth-child(6) {
64 | left: 80%;
65 | width: 120px;
66 | height: 120px;
67 | background-color: rgba(#fff, .2);
68 | animation-delay: 3s;
69 | }
70 |
71 | &:nth-child(7) {
72 | left: 32%;
73 | width: 160px;
74 | height: 160px;
75 | animation-delay: 2s;
76 | }
77 |
78 | &:nth-child(8) {
79 | left: 55%;
80 | width: 40px;
81 | height: 40px;
82 | font-size: 12px;
83 | animation-duration: 15s;
84 | animation-delay: 4s;
85 | }
86 |
87 | &:nth-child(9) {
88 | left: 25%;
89 | width: 40px;
90 | height: 40px;
91 | background-color: rgba(#fff, .3);
92 | font-size: 12px;
93 | animation-duration: 12s;
94 | animation-delay: 2s;
95 | }
96 |
97 | &:nth-child(10) {
98 | left: 85%;
99 | width: 160px;
100 | height: 160px;
101 | animation-delay: 5s;
102 | }
103 | }
104 | }
105 |
106 | @keyframes bubble {
107 | 0% {
108 | opacity: .5;
109 | transform: translateY(0) rotate(45deg);
110 | }
111 |
112 | 25% {
113 | opacity: .75;
114 | transform: translateY(-400px) rotate(90deg);
115 | }
116 |
117 | 50% {
118 | opacity: 1;
119 | transform: translateY(-600px) rotate(135deg);
120 | }
121 |
122 | 100% {
123 | opacity: 0;
124 | transform: translateY(-1000px) rotate(180deg);
125 | }
126 | }
--------------------------------------------------------------------------------
/src/components/UserLayout/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { Route, Switch, Link } from 'react-router-dom'
3 | import {
4 | Spin, Result, Button, Typography,
5 | } from 'antd'
6 | import { IRoute } from '@src/router/type'
7 | import './index.less'
8 | import { systemRouteList } from '@src/router/utils'
9 |
10 | interface UserLayoutState {
11 | isError: boolean
12 | }
13 |
14 | class UserLayout extends React.PureComponent {
15 | // eslint-disable-next-line react/state-in-constructor
16 | state: UserLayoutState = {
17 | isError: false,
18 | }
19 |
20 | static getDerivedStateFromError() {
21 | return { isError: true };
22 | }
23 |
24 | componentDidCatch() {
25 | // 上报错误
26 | }
27 |
28 | render() {
29 | if (this.state.isError) {
30 | return (
31 |
36 | Go Contact
37 |
38 | )} />
39 | );
40 | }
41 |
42 | return (
43 | <>
44 |
45 |
46 | {/* eslint-disable-next-line react/no-array-index-key */}
47 | {new Array(10).fill(0).map((item, index) => - Admin
)}
48 |
49 |
50 |
51 |
52 |
53 | React Ant Admin
54 |
55 |
56 |
57 |
}>
58 |
59 | {systemRouteList.map((menu: IRoute) => (
60 |
61 | ))}
62 |
63 |
64 |
65 |
66 | >
67 | )
68 | }
69 | }
70 |
71 | export default UserLayout
72 |
--------------------------------------------------------------------------------
/src/constants/app.ts:
--------------------------------------------------------------------------------
1 | import { FormProps } from 'antd';
2 |
3 | export const LayoutCol: FormProps = {
4 | labelCol: { span: 4 },
5 | wrapperCol: { span: 16 },
6 | }
7 |
8 | export const Min_Width = 460
9 |
--------------------------------------------------------------------------------
/src/constants/lottiePath.ts:
--------------------------------------------------------------------------------
1 | export const ClosePath = 'https://assets10.lottiefiles.com/temp/lf20_1m8bBV.json'
2 |
3 | export const LoginPath = 'https://assets3.lottiefiles.com/private_files/lf30_irb2v6ie.json'
4 |
--------------------------------------------------------------------------------
/src/constants/server.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/prefer-default-export
2 | export const BASE = process.env.NODE_ENV === 'development' ? '/api' : '/adminApi/'
3 |
--------------------------------------------------------------------------------
/src/constants/validate.ts:
--------------------------------------------------------------------------------
1 | import { Rule } from 'antd/lib/form'
2 |
3 | export const PASSWORDREG = /^[a-zA-Z]\w{5,17}$/
4 | export const CARDREG = /(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/
5 | export const PHONEREG = /^(?:(?:\+|00)86)?1\d{10}$/
6 |
7 | export const ConfirmPassWord = ({ getFieldValue }: any) => ({
8 | validator(_: any, value: any) {
9 | if (!value || getFieldValue('password') === value) {
10 | return Promise.resolve()
11 | }
12 | return Promise.reject(new Error('您输入的两个密码不匹配!'))
13 | },
14 | })
15 |
16 | /* 邮箱验证 */
17 | export const UserNameRule: Rule[] = [{ required: true, message: '请输入用户名!' }, { type: 'email', message: '请输入正确的邮箱格式!' }]
18 |
19 | /* 密码验证 */
20 | export const PassWordRule: Rule[] = [{ required: true, message: '请输入密码!' }, { pattern: PASSWORDREG, message: '输入的密码不合规范!' }]
21 |
22 | /* 确认密码 */
23 | export const ConfirmRule: Rule[] = [{ required: true, message: '请输入重复密码!' }, ConfirmPassWord]
24 |
25 | /* 验证码 */
26 | export const CaptchaRule : Rule[] = [{ required: true, message: '请输入验证码!' }, { len: 6, message: '请检查验证码长度!' }]
27 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 | import { ConfigProvider } from 'antd'
5 | import zhCN from 'antd/es/locale/zh_CN'
6 | import App from './views/App'
7 | import store from './store'
8 | import './assets/normalize.less'
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root'),
17 | )
18 |
--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react'
2 | import { lazyImport } from '@src/utils/lazyImport'
3 | import { IRoute } from './type'
4 |
5 | const System = lazy(() => import('../components/UserLayout'))
6 | const Layout = lazy(() => import('../components/Layout'))
7 |
8 | const Login = lazy(() => import('../views/Login'))
9 |
10 | const Dashboard = lazy(() => import('../views/Dashboard'))
11 | /* const UserAdd = lazyImport('../views/User/Add') */
12 | const UserList = lazy(() => import('../views/User/List'))
13 | const DepartmentList = lazy(() => import('../views/Department/List'))
14 | const DepartmentAdd = lazy(() => import('../views/Department/Add'))
15 | const JobList = lazy(() => import('../views/Job/List'))
16 | const JobAdd = lazy(() => import('../views/Job/Add'))
17 | const StaffList = lazy(() => import('../views/Staff/List'))
18 | const StaffAdd = lazy(() => import('../views/Staff/Add'))
19 |
20 | export const routes: IRoute[] = [
21 | {
22 | path: '/system',
23 | redirect: '/system/login',
24 | component: System,
25 | meta: {
26 | title: '系统',
27 | },
28 | children: [
29 | {
30 | path: '/system/login',
31 | exact: true,
32 | component: Login,
33 | meta: {
34 | title: '登录',
35 | },
36 | },
37 | ],
38 | },
39 | {
40 | path: '/',
41 | redirect: '/dashboard',
42 | component: Layout,
43 | meta: {
44 | title: '系统',
45 | },
46 | children: [
47 | {
48 | path: '/dashboard',
49 | exact: true,
50 | component: Dashboard,
51 | meta: {
52 | title: '控制台',
53 | icon: 'AppstoreOutlined',
54 | },
55 | },
56 | {
57 | path: '/user',
58 | redirect: '/user/add',
59 | roles: ['user', 'admin'],
60 | meta: {
61 | title: '用户管理',
62 | icon: 'UserOutlined',
63 | },
64 | children: [
65 | /* {
66 | path: '/user/add',
67 | component: UserAdd,
68 | meta: {
69 | title: '用户添加',
70 | },
71 | roles: ['admin'],
72 | }, */
73 | {
74 | path: '/user/list',
75 | component: UserList,
76 | meta: {
77 | title: '用户列表',
78 | },
79 | },
80 | ],
81 | },
82 | {
83 | path: '/department',
84 | redirect: '/department/list',
85 | roles: ['product', 'information'],
86 | meta: {
87 | title: '部门管理',
88 | icon: 'WechatOutlined',
89 | },
90 | children: [
91 | {
92 | path: '/department/list',
93 | component: DepartmentList,
94 | roles: ['product'],
95 | meta: {
96 | title: '部门列表',
97 | },
98 | },
99 | {
100 | path: '/department/add',
101 | roles: ['information'],
102 | component: DepartmentAdd,
103 | meta: {
104 | title: '添加部门',
105 | },
106 | },
107 | ],
108 | },
109 | {
110 | path: '/job',
111 | redirect: '/job/list',
112 | meta: {
113 | title: '职位管理',
114 | icon: 'AuditOutlined',
115 | },
116 | children: [
117 | {
118 | path: '/job/list',
119 | component: JobList,
120 | meta: {
121 | title: '职位列表',
122 | },
123 | },
124 | {
125 | path: '/job/add',
126 | component: JobAdd,
127 | meta: {
128 | title: '添加职位',
129 | },
130 | },
131 | ],
132 | },
133 | {
134 | path: '/staff',
135 | redirect: '/staff/list',
136 | meta: {
137 | title: '职员管理',
138 | icon: 'UsergroupAddOutlined',
139 | },
140 | children: [
141 | {
142 | path: '/staff/list',
143 | component: StaffList,
144 | meta: {
145 | title: '职员列表',
146 | },
147 | },
148 | {
149 | path: '/staff/add',
150 | component: StaffAdd,
151 | meta: {
152 | title: '职员添加',
153 | },
154 | },
155 | ],
156 | },
157 | /*
158 | {
159 | path: '/a',
160 | meta: {
161 | title: '嵌套路由A',
162 | },
163 | redirect: '/a/a',
164 | children: [
165 | {
166 | path: '/a/a',
167 | meta: {
168 | title: '嵌套路由AA',
169 | },
170 | children: [
171 | {
172 | path: '/a/a/a',
173 | meta: {
174 | title: '嵌套路由AAA',
175 | },
176 | },
177 | {
178 | path: '/a/a/b',
179 | meta: {
180 | title: '嵌套路由AAB',
181 | },
182 | },
183 | ],
184 | },
185 | {
186 | path: '/a/b',
187 | meta: {
188 | title: '嵌套路由AB',
189 | },
190 | children: [
191 | {
192 | path: '/a/b/a',
193 | meta: {
194 | title: '嵌套路由ABA',
195 | },
196 | },
197 | ],
198 | },
199 | ],
200 | },
201 | {
202 | path: '/askforleave',
203 | roles: ['admin', 'user'],
204 | exact: true,
205 | meta: {
206 | title: '请假',
207 | icon: 'MehOutlined',
208 | },
209 | },
210 | {
211 | path: '/vertime',
212 | exact: true,
213 | meta: {
214 | title: '加班',
215 | icon: 'FrownOutlined',
216 | },
217 | }, */
218 | /* 错误页面 */
219 | {
220 | path: '/success',
221 | hidden: true,
222 | component: lazy(() => import('../views/Success')),
223 | },
224 | {
225 | path: '/error',
226 | meta: {
227 | title: '错误页面',
228 | },
229 | hidden: true,
230 | redirect: '/error/404',
231 | children: [
232 | {
233 | path: '/error/404',
234 | component: lazy(() => import('../views/Error/404')),
235 | meta: {
236 | title: '页面不存在',
237 | },
238 | },
239 | {
240 | path: '/error/403',
241 | component: lazy(() => import('../views/Error/403')),
242 | meta: {
243 | title: '暂无权限',
244 | },
245 | },
246 | ],
247 | },
248 | {
249 | path: '/*',
250 | hidden: true,
251 | meta: {
252 | title: '错误页面',
253 | },
254 | redirect: '/error/404',
255 | },
256 | ],
257 | },
258 | ]
259 |
260 | export default routes
261 |
--------------------------------------------------------------------------------
/src/router/type.ts:
--------------------------------------------------------------------------------
1 | import { RouteProps } from 'react-router-dom'
2 |
3 | export type Roles = ('admin'| 'user' | 'information' | 'product')
4 |
5 | export interface IRoute extends RouteProps{
6 | /** 组件 */
7 | component?: any
8 | /** 重定向 */
9 | redirect?: string
10 | // 子路由
11 | children?: IRoute[]
12 | // roles: ['admin', 'user' , 'information' , 'product'] 权限校验 将控制页面角色(允许设置多个角色) 子路由会继承父路由的 roles 属性
13 | roles?: Roles[]
14 | /** true为不渲染在侧边栏上 */
15 | hidden?: boolean
16 | /** 路由元信息 */
17 | meta?: IRouteMeta
18 | }
19 |
20 | export interface IRouteMeta {
21 | title: string
22 | icon?: string
23 | }
24 |
--------------------------------------------------------------------------------
/src/router/utils.ts:
--------------------------------------------------------------------------------
1 | import { routes } from './index'
2 | import { IRoute } from './type'
3 |
4 | /*
5 | 动态生成路由流程
6 | 1.首先遍历生成最外层的布局路由,也就是layoutRouteList属性
7 | 在app.tsx中以下代码
8 | {layoutRouteList.map((route: IRoute) => (
9 |
14 | ))}
15 | 2.在layout中遍历 /下的所有路由,进行条件校验渲染(没有权限跳到403,重定向。。。),businessRouteList属性
16 | 在Layout/MainRoutes/index.tsx
17 | 2.1使用auth组件包裹进行条件渲染,渲染出有权限的的路由,没有权限的进行403跳转
18 | 有权限但有redirect属性的进行重定向
19 | 2.2使用AsyncRoutes包裹剩下条件渲染后的组件。authRoutes属性动态生成侧边栏以及添加跳转动画
20 | 3.在UserLayout中遍历 /system下的路由,即登录,注册等。及systemRouteList属性下的路由
21 | 在UserLayout/index.tsx
22 | 3.1进行循环生成以及布局和捕获错误
23 |
24 | */
25 |
26 | /* 解析当前path路径, /a/a/a 解析成[/a],[/a/a],[/a/a/a] */
27 | export const pathToList = (path: string): string[] => {
28 | const pathList = path.split('/').filter((item) => item)
29 | return pathList.map((item, index) => `/${pathList.slice(0, index + 1).join('/')}`)
30 | }
31 |
32 | /* 递归查找当前path路径中的路由,[/a/a/a],将a,aa,aaa加入进去 */
33 | function findRoutesByPaths(pathList: string[], routes: IRoute[]): IRoute[] {
34 | const res: IRoute[] = []
35 | routes.forEach(
36 | (child: IRoute) => {
37 | if (pathList.indexOf(`${child.path}`) !== -1) {
38 | res.push(child)
39 | }
40 | if (child.children && child.children.length > 1) {
41 | res.push(...findRoutesByPaths(pathList, child.children))
42 | }
43 | },
44 | )
45 | return res
46 | }
47 |
48 | /**
49 | *
50 | * 将路由转换为一维数组
51 | * @param routeList 路由
52 | * @param deep 是否深层转化
53 | */
54 |
55 | export function flattenRoute(routeList: IRoute[], deep: boolean): IRoute[] {
56 | const result: IRoute[] = []
57 |
58 | for (let i = 0; i < routeList.length; i += 1) {
59 | const route = routeList[i]
60 |
61 | result.push({
62 | ...route,
63 | })
64 |
65 | if (route.children && deep) {
66 | result.push(...flattenRoute(route.children, deep))
67 | }
68 | }
69 |
70 | return result
71 | }
72 |
73 | function getLayoutRouteList(): IRoute[] {
74 | return flattenRoute(routes, false)
75 | }
76 |
77 | function getBusinessRouteList(): IRoute[] {
78 | const routeList = routes.filter((route) => route.path === '/')
79 | if (routeList.length > 0) {
80 | return flattenRoute(routeList, true)
81 | }
82 | return []
83 | }
84 |
85 | function getSystemRouteList(): IRoute[] {
86 | const routeList = routes.filter((route) => route.path === '/system');
87 |
88 | if (routeList.length > 0) {
89 | return flattenRoute(routeList, true).slice(1)
90 | }
91 | return []
92 | }
93 |
94 | function getMenuRouteList(): IRoute[] {
95 | const routeList = routes.filter((route) => route.path === '/');
96 | if (routeList.length > 0) {
97 | return routeList[0].children || []
98 | }
99 | return []
100 | }
101 |
102 | /**
103 | * 这里会将 config 中所有路由解析成三个数组
104 | * 第一个: 最外层的路由,例如 Layout UserLayout ...
105 | * 第二个: 系统路由, 例如 Login Register RegisterResult
106 | * 第三个: 业务路由,为 /system 路由下的业务路由
107 | * 第四个: 菜单列表,为 / 路由下的路由
108 | */
109 |
110 | export const layoutRouteList = getLayoutRouteList()
111 | export const businessRouteList = getBusinessRouteList()
112 | export const systemRouteList = getSystemRouteList()
113 | export const menuRouteList = getMenuRouteList()
114 |
115 | /* 页面标题 */
116 | export function getPageTitle(routeList: IRoute[]): string {
117 | const route = routeList.find((child) => child.path === window.location.pathname)
118 | return route ? (route.meta?.title || 'React Admin') : ''
119 | }
120 |
121 | /* 面包屑 */
122 | export function getBreadcrumbs(path: string): IRoute[] {
123 | return findRoutesByPaths(pathToList(path), routes)
124 | }
125 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createStore, Reducer, combineReducers, Middleware, compose, applyMiddleware,
3 | } from 'redux'
4 | import reduxThunk from 'redux-thunk'
5 | import reduxLogger from 'redux-logger'
6 | import { IAction, IStoreState } from './type'
7 | import userReducer from './module/user'
8 | import appReducer from './module/app'
9 |
10 | const reducers: Reducer> = combineReducers({
11 | user: userReducer,
12 | app: appReducer,
13 | })
14 |
15 | const middleware: Middleware[] = [reduxThunk]
16 |
17 | if (process.env.NODE_ENV === 'development') {
18 | middleware.push(reduxLogger)
19 | }
20 |
21 | function createMyStore() {
22 | const store = window.__REDUX_DEVTOOLS_EXTENSION__
23 | ? createStore(
24 | reducers,
25 | compose(applyMiddleware(...middleware), window.__REDUX_DEVTOOLS_EXTENSION__({})),
26 | )
27 | : createStore(reducers, applyMiddleware(...middleware))
28 |
29 | return store
30 | }
31 |
32 | const store = createMyStore()
33 |
34 | export default store
35 |
--------------------------------------------------------------------------------
/src/store/module/app.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux'
2 | import { IRoute } from '@src/router/type'
3 | import store from 'store'
4 | import { flattenRoute } from '@src/router/utils';
5 | import { IAction } from '../type'
6 |
7 | export interface AppState {
8 | sidebar: {
9 | opened: boolean
10 | }
11 | routes: IRoute[]
12 | /* flattenRoutes: IRoute[], */
13 | init: boolean
14 | }
15 |
16 | const SIDEBAR_KEY = 'React-ant-Admin-SideBar-Opened';
17 |
18 | const opened = store.get(SIDEBAR_KEY, true)
19 |
20 | const defaultApp: AppState = {
21 | sidebar: {
22 | opened: typeof opened === 'boolean' ? opened : true,
23 | },
24 | routes: [], // 过滤后的侧边栏
25 | /* flattenRoutes: [], // 路由表进行打平后的结果,用来进行校验使用 */
26 | init: false, // 进行判断是否初始化
27 | }
28 |
29 | const SET_SIDE_BAR_OPENED = 'SET_SIDE_BAR_OPENED'
30 | const SET_SIDE_BAR_ROUTES = 'SET_SIDE_BAR_ROUTES'
31 | const RMOVE_SIDE_BAR_ROUTES = 'RMOVE_SIDE_BAR_ROUTES'
32 |
33 | export const updateSideBar = (sidebar: AppState['sidebar']) => ({
34 | type: SET_SIDE_BAR_OPENED,
35 | payload: sidebar,
36 | })
37 |
38 | export const setSideBarRoutes = (routes: IRoute[]) => ({
39 | type: SET_SIDE_BAR_ROUTES,
40 | payload: routes,
41 | })
42 |
43 | export const clearSideBarRoutes = () => ({
44 | type: RMOVE_SIDE_BAR_ROUTES,
45 | payload: null,
46 | })
47 |
48 | const appReducer: Reducer> = (state = defaultApp, action: IAction) => {
49 | const { type, payload } = action
50 |
51 | switch (type) {
52 | case SET_SIDE_BAR_OPENED:
53 | store.set(SIDEBAR_KEY, (payload as AppState['sidebar']).opened)
54 |
55 | return {
56 | ...state,
57 | sidebar: payload,
58 | }
59 |
60 | /* 路由 */
61 | case SET_SIDE_BAR_ROUTES:
62 | return {
63 | ...state,
64 | routes: payload,
65 | /* flattenRoutes: flattenRoute(payload, true), */
66 | init: true,
67 | }
68 | /* 移除路由 */
69 | case RMOVE_SIDE_BAR_ROUTES:
70 | return {
71 | ...state,
72 | routes: [],
73 | /* flattenRoutes: [], */
74 | init: false,
75 | }
76 |
77 | default:
78 | return {
79 | ...state,
80 | }
81 | }
82 | }
83 |
84 | export default appReducer
85 |
--------------------------------------------------------------------------------
/src/store/module/user.ts:
--------------------------------------------------------------------------------
1 | import { Reducer } from 'redux'
2 | import {
3 | getRole, getToken, localSetUserInfo, localRemoveUserInfo, getUser,
4 | } from '@src/utils/auth'
5 | import { Roles } from '@src/router/type'
6 | import { IAction } from '../type'
7 |
8 | export interface UserState {
9 | username: string
10 | token: string
11 | role: Roles
12 | }
13 |
14 | const defaultUser: UserState = {
15 | username: getUser(),
16 | token: getToken(),
17 | role: getRole() as Roles,
18 | }
19 |
20 | const SET_USER_INFO = 'SET_USER_INFO'
21 |
22 | const SET_USER_LOGOUT = 'SET_USER_LOGOUT'
23 |
24 | export const setUserInfo: (user: UserState) => IAction = (user) => ({
25 | type: SET_USER_INFO,
26 | payload: user,
27 | })
28 |
29 | export const logout: ()=> IAction = () => ({
30 | type: SET_USER_LOGOUT,
31 | payload: null,
32 | })
33 |
34 | const userReducer: Reducer> = (state = defaultUser, action: IAction) => {
35 | const { type, payload } = action
36 | switch (type) {
37 | case SET_USER_INFO:
38 | localSetUserInfo(payload)
39 | return {
40 | ...payload,
41 | }
42 | case SET_USER_LOGOUT:
43 | localRemoveUserInfo()
44 | return {
45 | ...defaultUser,
46 | }
47 | default:
48 | return state
49 | }
50 | }
51 |
52 | export default userReducer
53 |
--------------------------------------------------------------------------------
/src/store/type.ts:
--------------------------------------------------------------------------------
1 | import { AppState } from './module/app';
2 | import { UserState } from './module/user'
3 |
4 | export interface IStoreState{
5 | user: UserState
6 | app: AppState
7 | }
8 |
9 | export interface IAction {
10 | type: string
11 | payload: T
12 | }
13 |
--------------------------------------------------------------------------------
/src/styles/global.less:
--------------------------------------------------------------------------------
1 | .table-btn-group {
2 | button + button {
3 | margin-left: 15px;
4 | }
5 | }
6 |
7 | .mb-20 {
8 | margin-bottom: 20px !important;
9 | }
10 |
11 | .lazy_loading {
12 | position: absolute;
13 | top: 50%;
14 | left: 50%;
15 | transform: translate(-50%,-50%);
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/mixin.less:
--------------------------------------------------------------------------------
1 | /* 文字超出显示省略号 */
2 | .article-container(@width: 50px) {
3 | width: @width;
4 | overflow: hidden;
5 | text-overflow: ellipsis;
6 | white-space: nowrap;
7 | }
--------------------------------------------------------------------------------
/src/styles/variables.less:
--------------------------------------------------------------------------------
1 | @bg-color: #00152A;
2 |
3 | @text-gray: #4F5761;
4 |
5 | @main-bgcolor: #fff;
6 |
7 | @header-height: 60px;
8 |
--------------------------------------------------------------------------------
/src/typings/global.d.ts:
--------------------------------------------------------------------------------
1 | export interface IDictionary {
2 | [key: string]: T
3 | }
4 |
5 | export interface PageResponseData {
6 | dataTotal?: number;
7 | pageTotal?: number;
8 | page?: number;
9 | size?: number;
10 | }
11 |
12 | declare global {
13 | interface Window {
14 | // eslint-disable-next-line @typescript-eslint/ban-types
15 | __REDUX_DEVTOOLS_EXTENSION__: Function
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import { UserState } from '@src/store/module/user'
2 | import store from 'store'
3 |
4 | const authToken = 'admin_Token'
5 | const authUser = 'admin_User'
6 | const authRole = 'admin_Role'
7 |
8 | export const getToken = ():string => (store.get(authToken) || '')
9 |
10 | export const setToken = (token: string): void => store.set(authToken, token)
11 |
12 | export const removeToken = ():void => (store.remove(authToken))
13 |
14 | export const getUser = ():string => (store.get(authUser) || '')
15 |
16 | export const setUser = (user: string): void => store.set(authUser, user)
17 |
18 | export const removeUser = ():void => (store.remove(authUser))
19 |
20 | export const getRole = ():string => (store.get(authRole) || '')
21 |
22 | export const setRole = (role: string): void => store.set(authRole, role)
23 |
24 | export const removeRole = ():void => (store.remove(authRole))
25 |
26 | export const localSetUserInfo = (user: UserState):void => {
27 | setToken(user.token)
28 | setUser(user.username)
29 | setRole(user.role)
30 | }
31 |
32 | export const localRemoveUserInfo = ():void => {
33 | removeRole()
34 | removeToken()
35 | removeUser()
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/filter.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chengzhenguo1/react-admin/c5d0108c253ef00d8dd13ff98518cb277feaaa37/src/utils/filter.ts
--------------------------------------------------------------------------------
/src/utils/lazyImport.ts:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react'
2 |
3 | // eslint-disable-next-line import/prefer-default-export
4 | export function lazyImport(url: string, timer = 300) {
5 | return lazy(() => new Promise((resolve) => {
6 | setTimeout(() => resolve(import(/* @vite-ignore */url)), timer)
7 | }))
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/prase.ts:
--------------------------------------------------------------------------------
1 | import { FC } from 'react'
2 |
3 | /* 不需要批量引入的路由 */
4 | const filterRoutes: string[] = ['Login', 'App']
5 |
6 | /* 动态批量引入路由 */
7 | const renderDynamicImport = (modules : Record) =>
8 | // eslint-disable-next-line implicit-arrow-linebreak
9 | Object.keys(modules).map((path): { path: string, Component: FC } | undefined => {
10 | /* 过滤不需要引入的路由 */
11 | if (!filterRoutes.some((route) => path.includes(route))) {
12 | /* 解析路径,返回path和组件 */
13 | const arr = path.split('/')
14 | const iPath = arr.slice(1, arr.length - 1).join('/').toLowerCase()
15 | return {
16 | path: `/${iPath}`,
17 | Component: modules[path].default,
18 | }
19 | }
20 | return undefined
21 | })
22 |
23 | export default {
24 | renderDynamicImport,
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/request.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosRequestConfig, ResponseType, AxiosInstance } from 'axios'
2 | import { message as Message, Modal } from 'antd'
3 | import { IDictionary } from '@src/typings/global'
4 | import { BASE } from '@src/constants/server'
5 | import { clearSideBarRoutes } from '@src/store/module/app'
6 | import { logout } from '@src/store/module/user'
7 | import store from '../store'
8 | import { getToken, getUser } from './auth'
9 |
10 | const TIMEOUT = 40000
11 |
12 | const MIME_TYPE: IDictionary = {
13 | JSON: 'json',
14 | }
15 |
16 | const createInstance = () => {
17 | const instance = axios.create({
18 | baseURL: BASE,
19 | withCredentials: true,
20 | timeout: TIMEOUT,
21 | responseType: MIME_TYPE.JSON,
22 | })
23 | return instance
24 | }
25 |
26 | const toastError = (error: any) => {
27 | const { response, message } = error
28 |
29 | Message.error(response?.data?.message || message)
30 |
31 | return Promise.reject(error)
32 | }
33 |
34 | interface Instance extends AxiosInstance {
35 | (config: AxiosRequestConfig): Promise
36 | }
37 |
38 | export const requestWithoutErrorToast: Instance = createInstance()
39 |
40 | const request: Instance = createInstance()
41 |
42 | /* 请求拦截器 */
43 | request.interceptors.request.use((config) => {
44 | config.headers.Token = getToken()
45 | config.headers.Username = getUser()
46 | return config
47 | }, toastError)
48 |
49 | /* 响应拦截器 */
50 | request.interceptors.response.use((res:any):any => {
51 | if (res?.data?.resCode !== 0) {
52 | /* token过期 */
53 | if (res?.data?.resCode === 1040) {
54 | Modal.confirm({
55 | title: '系统提示',
56 | content: res?.data?.message,
57 | okText: '重新登录',
58 | cancelText: '取消',
59 | onOk() {
60 | store.dispatch(clearSideBarRoutes())
61 | store.dispatch(logout())
62 | window.location.href = `${
63 | window.location.origin
64 | }/reactAdmin/#/system/login?redirectURL=${encodeURIComponent(window.location.href)}`
65 | },
66 | })
67 | }
68 | Message.error(res?.data?.message || '网络错误')
69 | return Promise.reject()
70 | }
71 |
72 | return Promise.resolve(res.data)
73 | }, toastError)
74 |
75 | export default request
76 |
--------------------------------------------------------------------------------
/src/utils/weather.ts:
--------------------------------------------------------------------------------
1 | /* 天气 */
2 | (function (d) {
3 | window.WIDGET = {
4 | CONFIG: {
5 | layout: '1',
6 | width: '450',
7 | height: '150',
8 | background: '1',
9 | dataColor: 'FFFFFF',
10 | key: '4552b93460d347ff811417c67e167092',
11 | },
12 | }
13 | const c = d.createElement('link')
14 | c.rel = 'stylesheet'
15 | c.href = 'https://widget.heweather.net/standard/static/css/he-standard.css?v=1.4.0'
16 | const s = d.createElement('script')
17 | s.src = 'https://widget.heweather.net/standard/static/js/he-standard.js?v=1.4.0'
18 | const sn = d.getElementsByTagName('script')[0]
19 | sn.parentNode!.insertBefore(c, sn)
20 | sn.parentNode!.insertBefore(s, sn)
21 | }(document))
22 |
--------------------------------------------------------------------------------
/src/views/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { Switch, HashRouter, Route } from 'react-router-dom'
3 | import { layoutRouteList } from '@src/router/utils'
4 | import { Spin } from 'antd'
5 | import { IRoute } from '@src/router/type'
6 | import 'antd/dist/antd.css'
7 | /* import '@src/styles/global.less' */
8 |
9 | const App = function () {
10 | return (
11 | }>
12 |
13 |
14 | {layoutRouteList.map((route: IRoute) => (
15 |
20 | ))}
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default App
28 |
--------------------------------------------------------------------------------
/src/views/Dashboard/CardGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { Card, Col } from 'antd'
3 |
4 | interface IProps {
5 | total: number
6 | title: string
7 | loading: boolean
8 | }
9 |
10 | const CardGroup: React.FC = memo(({ total, loading, title }) => (
11 |
12 |
13 |
16 |
17 |
18 | ))
19 |
20 | export default CardGroup
21 |
--------------------------------------------------------------------------------
/src/views/Dashboard/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { connect } from 'react-redux'
3 | import { Image, Statistic, Space } from 'antd'
4 | import avatarImg from '@src/assets/images/avatar.png'
5 | import './header.less'
6 | import { IStoreState } from '@src/store/type'
7 | import { UserState } from '@src/store/module/user'
8 |
9 | interface IProps {
10 | username?: UserState['username']
11 | }
12 |
13 | const DashboardHeader: React.FC = memo(({ username }) => (
14 |
15 |
16 | 控制台
17 |
18 |
19 |
20 |
26 |
27 |
28 | 早安,
29 | {username}
30 | ,祝你快乐一整天!
31 |
32 |
33 | 前端萌新 | 邯郸学院 — 软件工程专业 — 某某年级
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ))
45 |
46 | export default connect(({ user: { username } }: IStoreState) => ({ username }), null)(DashboardHeader)
47 |
--------------------------------------------------------------------------------
/src/views/Dashboard/header.less:
--------------------------------------------------------------------------------
1 | .dashboard-header {
2 | .header-title {
3 | font-weight: 600;
4 | font-size: 20px;
5 | margin-bottom: 10px;
6 | }
7 | }
8 |
9 | .header-content {
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | padding: 20px 0;
14 | @media (max-width: 768px) {
15 | display: block;
16 | }
17 | .content-left {
18 | display: flex;
19 | align-items: center;
20 |
21 | @media (max-width: 768px) {
22 | display: block;
23 | }
24 |
25 | & > div + div{
26 | margin-left: 10px;
27 | }
28 | .content-info {
29 | color: rgba(0,0,0,.85);
30 | font-size: 20px;
31 | margin-bottom: 15px;
32 | }
33 | .content-id {
34 | color: rgba(0,0,0,.45);
35 | }
36 | }
37 | .content-count {
38 | // display: flex;
39 | @media (max-width: 768px) {
40 | margin-top: 10px;
41 | }
42 | }
43 |
44 | }
--------------------------------------------------------------------------------
/src/views/Dashboard/index.less:
--------------------------------------------------------------------------------
1 | .site-card-border-less-wrapper {
2 | padding: 30px;
3 | background: #ececec;
4 | @media (max-width: 768px) {
5 | padding: 5px;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/views/Dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useState } from 'react'
2 | import { useAsyncFn } from 'react-use'
3 | import { PaginationProps, Row, Divider } from 'antd'
4 | import departmentApi from '@src/api/department'
5 | import jobApi from '@src/api/job'
6 | import staffApi from '@src/api/staff'
7 | import userApi from '@src/api/user'
8 | import Header from './Header'
9 | import CardGroup from './CardGroup'
10 | import './index.less'
11 |
12 | const Dashboard: React.FC = memo(() => {
13 | const [page, setPage] = useState({
14 | pageSize: 10,
15 | current: 1,
16 | })
17 |
18 | const [departmentList, getDepartmentList] = useAsyncFn(departmentApi.getDepartmentList)
19 | const [userList, getUserList] = useAsyncFn(userApi.getUserList)
20 | const [jobList, getJobList] = useAsyncFn(jobApi.getJobList)
21 | const [staffList, getStaffList] = useAsyncFn(staffApi.getstaffList)
22 |
23 | useEffect(() => {
24 | const pageSize = page.pageSize || 10
25 | const pageNumber = page.current || 1
26 | getDepartmentList({ pageSize, pageNumber })
27 | getUserList({ pageSize, pageNumber })
28 | getJobList({ pageSize, pageNumber })
29 | getStaffList({ pageSize, pageNumber })
30 | }, [])
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | )
46 | })
47 |
48 | export default Dashboard
49 |
--------------------------------------------------------------------------------
/src/views/Department/Add/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect } from 'react'
2 | import { useAsyncFn } from 'react-use'
3 | import { useHistory, useLocation } from 'react-router-dom'
4 | import { Button, Form, message } from 'antd'
5 | import { IDepartmentParams } from '@src/api/types/department'
6 | import { LayoutCol } from '@src/constants/app'
7 | import departmentApi from '@src/api/department'
8 | import DepartmentItem from '@src/components/FormItem/DepartmentItem'
9 |
10 | const DepartmentAdd: React.FC = memo(() => {
11 | const { state } = useLocation<{id: string}>()
12 | const [form] = Form.useForm()
13 | const { goBack } = useHistory()
14 |
15 | const [, addOrEditDepartmentFn] = useAsyncFn(departmentApi.addOrEditDepartment)
16 | const [, getDepartmentDetailedFn] = useAsyncFn(departmentApi.getDepartmentDetailed)
17 |
18 | /* 部门列表跳转判断是否有id */
19 | useEffect(() => {
20 | if (state?.id) {
21 | getDepartmentDetailedFn(state.id).then((res) => {
22 | form.setFieldsValue(res)
23 | })
24 | }
25 | }, [])
26 |
27 | /* 编辑或者删除 */
28 | const onHandleSubmit = useCallback(
29 | () => {
30 | form.validateFields().then((res: IDepartmentParams) => {
31 | const id = state?.id ? state.id : undefined
32 | const values = { id, ...res }
33 | addOrEditDepartmentFn(values).then((mes) => {
34 | message.success(mes)
35 | form.resetFields()
36 | goBack()
37 | })
38 | })
39 | },
40 | [],
41 | )
42 |
43 | return (
44 |
58 |
61 |
62 |
63 | )
64 | })
65 |
66 | export default DepartmentAdd
67 |
--------------------------------------------------------------------------------
/src/views/Department/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | memo, useCallback, useEffect, useState,
3 | } from 'react'
4 | import { useHistory } from 'react-router-dom'
5 | import { useAsyncFn } from 'react-use'
6 | import {
7 | Button, Table, Popconfirm, message, PaginationProps, Form, Switch, Modal,
8 | } from 'antd'
9 | import { IDepartment } from '@src/api/types/department'
10 | import BasisTable from '@src/components/BasisTable'
11 | import departmentApi from '@src/api/department'
12 | import SearchItem, { SearchParam } from '@src/components/FormItem/SearchItem'
13 |
14 | const DepartList: React.FC = memo(() => {
15 | const [search, setSearch] = useState()
16 | const [page, setPage] = useState({
17 | current: 1,
18 | pageSize: 10,
19 | })
20 | const [selectedRowKeys, setSelectedRowKeys] = useState([])
21 |
22 | const [form] = Form.useForm()
23 | const { push } = useHistory()
24 |
25 | const [departmentList, getDepartmentListFn] = useAsyncFn(departmentApi.getDepartmentList)
26 | const [deleteDepartment, deleteDepartmentFn] = useAsyncFn(departmentApi.deleteDepartment)
27 | const [, setDepartmentStatusFn] = useAsyncFn(departmentApi.setDepartmentStatus)
28 |
29 | useEffect(() => {
30 | getDepartmentData()
31 | }, [page, search])
32 |
33 | /* 页码改变 */
34 | const onPageChange = useCallback(
35 | (page: PaginationProps) => {
36 | setPage(page)
37 | },
38 | [],
39 | )
40 |
41 | /* 多选 */
42 | const onSelectChange = useCallback(
43 | (selectedRowKeys) => {
44 | setSelectedRowKeys(selectedRowKeys)
45 | },
46 | [],
47 | )
48 |
49 | /* 切换状态 */
50 | const onHandleChangeStatus = useCallback(
51 | (id, status) => {
52 | setDepartmentStatusFn(id, status)
53 | /* 立马修改后,服务器反应慢 */
54 | setTimeout(() => {
55 | getDepartmentData()
56 | }, 10)
57 | },
58 | [],
59 | )
60 |
61 | /* 批量删除 */
62 | const onHandleDelete = useCallback(
63 | () => {
64 | if (selectedRowKeys.length < 1) {
65 | return false
66 | }
67 | Modal.confirm({
68 | title: '是否删除所选部门?',
69 | okText: '确定',
70 | cancelText: '取消',
71 | onOk() {
72 | onDeleteModal(selectedRowKeys.join())
73 | setSelectedRowKeys([])
74 | },
75 | })
76 | },
77 | [selectedRowKeys],
78 | )
79 |
80 | /* 删除部门 */
81 | const onDeleteModal = useCallback(
82 | (id:string) => {
83 | deleteDepartmentFn(id).then((res) => {
84 | message.success(res)
85 | getDepartmentData()
86 | })
87 | },
88 | [],
89 | )
90 |
91 | /* 点击搜索 */
92 | const onHandleSearch = useCallback(
93 | (res: SearchParam) => {
94 | setSearch(res)
95 | },
96 | [],
97 | )
98 |
99 | const getDepartmentData = () => {
100 | getDepartmentListFn({
101 | name: search?.name, status: search?.status, pageNumber: page.current || 1, pageSize: page.pageSize || 10,
102 | })
103 | }
104 |
105 | return (
106 |
107 |
115 |
116 |
117 |
118 |
119 | loading={departmentList.loading}
120 | data={departmentList.value?.data.data}
121 | total={departmentList.value?.data.total}
122 | rowSelection={{ selectedRowKeys, onChange: onSelectChange }}
123 | onChange={onPageChange}
124 | footer={() => }>
125 |
126 | title='部门名称'
127 | dataIndex='name' />
128 |
129 | title='禁启用'
130 | dataIndex='status'
131 | align='center'
132 | width={100}
133 | render={(_, record) => (
134 | onHandleChangeStatus(record.id, checked)} />
139 | )} />
140 |
141 | title='人员数量'
142 | dataIndex='number' />
143 |
144 | title='操作'
145 | align='center'
146 | width={200}
147 | render={(_, record) => (
148 |
149 |
154 |
onDeleteModal(record.id!)}
157 | okText='确认'
158 | cancelText='取消'>
159 |
160 |
161 |
162 | )} />
163 |
164 |
165 | )
166 | })
167 |
168 | export default DepartList
169 |
--------------------------------------------------------------------------------
/src/views/Error/403.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { Button, Result } from 'antd'
3 | import { Link } from 'react-router-dom'
4 |
5 | const Noauthorized: React.FC = memo(() => {
6 | console.log('403')
7 | return (
8 | 返回主页} />
13 | )
14 | })
15 |
16 | export default Noauthorized
17 |
--------------------------------------------------------------------------------
/src/views/Error/404.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Link } from 'react-router-dom'
3 |
4 | import { Button, Result } from 'antd'
5 |
6 | const NotFount: React.FC = () => (
7 | 返回主页} />
12 | )
13 |
14 | export default NotFount
15 |
--------------------------------------------------------------------------------
/src/views/Job/Add/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect } from 'react'
2 | import { useLocation } from 'react-router-dom'
3 | import { useAsyncFn } from 'react-use'
4 | import { Button, Form, message } from 'antd'
5 | import jobApi from '@src/api/job'
6 | import departmentApi from '@src/api/department'
7 | import JobItem from '@src/components/FormItem/JobItem'
8 | import { LayoutCol } from '@src/constants/app'
9 |
10 | const JobAdd: React.FC = memo(() => {
11 | const { state } = useLocation<{jobId: string}>()
12 |
13 | const [form] = Form.useForm()
14 |
15 | const [{ loading }, jobAddOrEditFn] = useAsyncFn(jobApi.jobAddOrEdit)
16 | const [, getJobDetailedFn] = useAsyncFn(jobApi.jobDetail)
17 | const [departmentAll, getDepartmentAllFn] = useAsyncFn(departmentApi.getDepartmentListAll)
18 |
19 | /* 职位列表跳转判断是否有id */
20 | useEffect(() => {
21 | getDepartmentAllFn()
22 | if (state?.jobId) {
23 | getJobDetailedFn(state.jobId).then((res) => {
24 | form.setFieldsValue(res)
25 | })
26 | }
27 | }, [])
28 |
29 | /* 编辑或者删除 */
30 | const onHandleDartmentAddOrEdit = useCallback(
31 | () => {
32 | // 需要修复修改后id还存在的问题
33 | form.validateFields().then((res) => {
34 | let jobId = state?.jobId
35 | const values = { ...res, jobId }
36 | jobAddOrEditFn(values).then((data) => {
37 | jobId = ''
38 | message.success(data.message)
39 | form.resetFields()
40 | })
41 | })
42 | },
43 | [],
44 | )
45 |
46 | return (
47 |
67 | )
68 | })
69 |
70 | export default JobAdd
71 |
--------------------------------------------------------------------------------
/src/views/Job/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | memo, useCallback, useEffect, useState,
3 | } from 'react'
4 | import { useHistory } from 'react-router-dom'
5 | import { useAsyncFn } from 'react-use'
6 | import {
7 | Button, Table, Popconfirm, message, PaginationProps, Form, Switch, Modal,
8 | } from 'antd'
9 | import jobApi from '@src/api/job'
10 | import BasisTable from '@src/components/BasisTable'
11 | import { IJob } from '@src/api/types/job'
12 | import SearchItem, { SearchParam } from '@src/components/FormItem/SearchItem'
13 |
14 | const JobList: React.FC = memo(() => {
15 | const [search, setSearch] = useState()
16 | const [page, setPage] = useState({
17 | current: 1,
18 | pageSize: 10,
19 | })
20 | const [selectedRowKeys, setSelectedRowKeys] = useState([])
21 |
22 | const [form] = Form.useForm()
23 | const { push } = useHistory()
24 |
25 | const [jobList, getJobListFn] = useAsyncFn(jobApi.getJobList)
26 | const [, deleteJobFn] = useAsyncFn(jobApi.jobDelete)
27 | const [, setJobStatusFn] = useAsyncFn(jobApi.setJobStatus)
28 |
29 | useEffect(() => {
30 | getJobData()
31 | }, [page, search])
32 |
33 | /* 页码改变 */
34 | const onPageChange = useCallback(
35 | (page: PaginationProps) => {
36 | setPage(page)
37 | },
38 | [],
39 | )
40 |
41 | /* 多选 */
42 | const onSelectChange = useCallback(
43 | (selectedRowKeys) => {
44 | setSelectedRowKeys(selectedRowKeys)
45 | },
46 | [],
47 | )
48 |
49 | /* 切换状态 */
50 | const onHandleChangeStatus = useCallback(
51 | (id, status) => {
52 | setJobStatusFn(id, status)
53 | /* 立马修改后,服务器反应慢 */
54 | setTimeout(() => {
55 | getJobData()
56 | }, 10)
57 | },
58 | [],
59 | )
60 |
61 | /* 批量删除 */
62 | const onHandleDelete = useCallback(
63 | () => {
64 | if (selectedRowKeys.length < 1) {
65 | return false
66 | }
67 | Modal.confirm({
68 | title: '是否删除所选部门?',
69 | okText: '确定',
70 | cancelText: '取消',
71 | onOk() {
72 | onDeleteModal(selectedRowKeys.join())
73 | setSelectedRowKeys([])
74 | },
75 | })
76 | },
77 | [selectedRowKeys],
78 | )
79 |
80 | /* 删除职位 */
81 | const onDeleteModal = useCallback(
82 | (id:string) => {
83 | deleteJobFn(id).then((res) => {
84 | message.success(res.message)
85 | getJobData()
86 | })
87 | },
88 | [],
89 | )
90 |
91 | /* 点击搜索 */
92 | const onSearch = useCallback(
93 | (value: SearchParam) => {
94 | setSearch(value)
95 | },
96 | [],
97 | )
98 |
99 | const getJobData = () => {
100 | getJobListFn({
101 | name: search?.name, status: search?.status, pageNumber: page.current || 1, pageSize: page.pageSize || 10,
102 | })
103 | }
104 |
105 | return (
106 |
107 |
115 |
116 |
117 |
118 |
119 | loading={jobList.loading}
120 | data={jobList.value?.data.data}
121 | total={jobList.value?.data.total}
122 | rowSelection={{ selectedRowKeys, onChange: onSelectChange }}
123 | onChange={onPageChange}
124 | footer={() => }>
125 |
126 | title='职位名称'
127 | dataIndex='name' />
128 |
129 | title='部门名称'
130 | dataIndex='jobName' />
131 |
132 | title='禁启用'
133 | dataIndex='status'
134 | align='center'
135 | width={100}
136 | render={(text, record) => (
137 | onHandleChangeStatus(record.jobId, checked)} />
142 | )} />
143 |
144 | title='操作'
145 | align='center'
146 | width={200}
147 | render={(text, record) => (
148 |
149 |
154 |
onDeleteModal(record.jobId)}
157 | okText='确认'
158 | cancelText='取消'>
159 |
160 |
161 |
162 | )} />
163 |
164 |
165 | )
166 | })
167 |
168 | export default JobList
169 |
--------------------------------------------------------------------------------
/src/views/Login/Form/loginForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback } from 'react'
2 | import { useAsyncFn, useKey } from 'react-use'
3 | import { useHistory } from 'react-router-dom'
4 | import { connect } from 'react-redux'
5 | import sha256 from 'crypto-js/sha256'
6 | import { Form, Button, message } from 'antd'
7 | import authApi from '@src/api/auth'
8 | import LoginItem from '@src/components/FormItem/LoginItem'
9 | import type { IParam } from '@src/api/types/auth'
10 | import { UserState, setUserInfo } from '@src/store/module/user'
11 |
12 | interface IProps {
13 | setUserInfo: (user: UserState)=> void
14 | }
15 |
16 | const LoginForm: React.FC = memo((props) => {
17 | const { replace } = useHistory()
18 | const [form] = Form.useForm()
19 |
20 | const [{ loading }, loginFn] = useAsyncFn(authApi.login)
21 |
22 | const next = () => {
23 | const params = new URLSearchParams(window.location.search)
24 | const redirectURL = params.get('redirectURL')
25 | if (redirectURL) {
26 | window.location.href = redirectURL
27 | return
28 | }
29 | replace('/')
30 | }
31 |
32 | const onLogin = useCallback(
33 | () => {
34 | form.validateFields().then(async (res) => {
35 | const values = res as IParam
36 | const { data, message: mes } = await loginFn({
37 | username: values.username,
38 | password: sha256(values.password).toString(),
39 | code: values.code,
40 | })
41 | if (data) {
42 | message.success(mes)
43 | props.setUserInfo(data)
44 | next()
45 | }
46 | })
47 | },
48 | [],
49 | )
50 |
51 | /* 回车登录 */
52 | useKey('Enter', onLogin)
53 |
54 | return (
55 |
62 |
65 |
66 |
67 | )
68 | })
69 |
70 | export default connect(() => ({}), {
71 | setUserInfo,
72 | })(LoginForm)
73 |
--------------------------------------------------------------------------------
/src/views/Login/Form/registerForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback } from 'react'
2 | import sha256 from 'crypto-js/sha256'
3 | import { useAsyncFn, useKey } from 'react-use'
4 | import { Form, Button, message } from 'antd'
5 | import { IParam } from '@src/api/types/auth'
6 | import authApi from '@src/api/auth'
7 | import LoginItem from '@src/components/FormItem/LoginItem'
8 |
9 | interface IProps {
10 | toggleState: ()=> void
11 | }
12 |
13 | interface FormProp extends IParam{
14 | cpassword: string
15 | }
16 |
17 | const RegisterForm: React.FC = memo(({ toggleState }) => {
18 | const [form] = Form.useForm()
19 |
20 | const [{ loading }, registerFn] = useAsyncFn(authApi.register)
21 |
22 | const onRegister = useCallback(
23 | () => {
24 | form.validateFields().then(async (res) => {
25 | const values = res as FormProp
26 | const { cpassword, ...user } = { ...values }
27 | const data = await registerFn({
28 | username: user.username,
29 | password: sha256(user.password).toString(),
30 | code: user.code,
31 | })
32 |
33 | if (data.message) {
34 | message.success(data.message)
35 | toggleState()
36 | }
37 | })
38 | },
39 | [],
40 | )
41 |
42 | useKey('Enter', onRegister)
43 |
44 | return (
45 |
53 |
56 |
57 |
58 | )
59 | })
60 |
61 | export default RegisterForm
62 |
--------------------------------------------------------------------------------
/src/views/Login/index.less:
--------------------------------------------------------------------------------
1 | .login-wrap {
2 | display: flex;
3 | align-items: center;
4 | /* justify-content: center; */
5 | flex-direction: column;
6 | &-card {
7 | width: 300px;
8 | }
9 | .login-card {
10 | width: 300px;
11 | }
12 | .login-header {
13 | display: flex;
14 | justify-content: space-between;
15 | align-items: center;
16 | padding: 20px 0;
17 | .login-title {
18 | font-weight: 600;
19 | font-size: 20px;
20 | color: #fff;
21 | }
22 |
23 | .login-toggle {
24 | font-size: 12px;
25 | color: @text-gray;
26 | z-index: 1;
27 |
28 | &:hover {
29 | color: #fff;
30 | cursor: pointer;
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/views/Login/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useState } from 'react'
2 | import { useHistory } from 'react-router'
3 | import { getToken } from '@src/utils/auth'
4 | import { Player } from '@lottiefiles/react-lottie-player'
5 | import { LoginPath } from '@src/constants/lottiePath'
6 | import LoginForm from './Form/loginForm'
7 | import RegisterForm from './Form/registerForm'
8 | import './index.less'
9 |
10 | interface State {
11 | title: string
12 | toggleText: string
13 | }
14 |
15 | interface FormTabType {
16 | LOGIN: State
17 | REGISTER: State
18 | }
19 |
20 | const FormTab: { [key in keyof FormTabType]: State } = {
21 | LOGIN: {
22 | title: '登录',
23 | toggleText: '去注册',
24 | },
25 | REGISTER: {
26 | title: '注册',
27 | toggleText: '去登录',
28 | },
29 | }
30 |
31 | const Login: React.FC = memo(() => {
32 | const [state, setState] = useState(FormTab.LOGIN)
33 | const { replace } = useHistory()
34 |
35 | useEffect(() => {
36 | /* 登录后直接跳转到主页 */
37 | const token = getToken()
38 | if (token) {
39 | replace('/dashboard')
40 | }
41 | }, [])
42 |
43 | const handleToggleState = () => {
44 | if (state === FormTab.LOGIN) {
45 | setState(FormTab.REGISTER)
46 | } else {
47 | setState(FormTab.LOGIN)
48 | }
49 | }
50 |
51 | return (
52 |
53 |
59 |
60 |
61 |
62 | {state.title}
63 |
64 |
65 | {state.toggleText}
66 |
67 |
68 | {state === FormTab.LOGIN ?
:
}
69 |
70 |
71 | )
72 | })
73 |
74 | export default Login
75 |
--------------------------------------------------------------------------------
/src/views/Staff/Add/index.less:
--------------------------------------------------------------------------------
1 | .staff-form {
2 | display: flex;
3 | justify-content: space-between;
4 | flex-wrap: wrap;
5 | width: 100%;
6 | @media (max-width: 768px) {
7 | display: block;
8 | }
9 | &>div {
10 | flex-basis: 50%;
11 | }
12 | .form-editor {
13 | flex-basis: 100%;
14 | .ant-col-10 {
15 | max-width: 100% !important;
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/views/Staff/Add/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect } from 'react'
2 | import { useAsyncFn } from 'react-use'
3 | import { useLocation, useHistory } from 'react-router-dom'
4 | import moment from 'moment'
5 | import store from 'store'
6 | import { Button, Form, Divider } from 'antd'
7 | import departmentApi from '@src/api/department'
8 | import jobApi from '@src/api/job'
9 | import staffApi from '@src/api/staff'
10 | import StaffItem from '@src/components/FormItem/StaffItem'
11 | import './index.less'
12 | import { UPLOAD_TOKEN } from '@src/components/UploadPic'
13 |
14 | const token = store.get(UPLOAD_TOKEN)
15 |
16 | const StaffAdd: React.FC = memo(() => {
17 | const [form] = Form.useForm()
18 | const { state } = useLocation<{id: string}>()
19 | const { push } = useHistory()
20 |
21 | const [departmentAll, getDepartmentListAllFn] = useAsyncFn(departmentApi.getDepartmentListAll)
22 | const [jobAll, getJobListAllFn] = useAsyncFn(jobApi.getJobAllList)
23 | const [, getStaffDetailFn] = useAsyncFn(staffApi.getstaffDetail)
24 | const [, addOrEditStaffFn] = useAsyncFn(staffApi.addOrEditStaff)
25 |
26 | useEffect(() => {
27 | getDepartmentListAllFn()
28 | getJobListAllFn()
29 | if (state?.id) {
30 | getStaffDetailFn(state.id).then((res) => {
31 | const {
32 | birthday,
33 | job_entry_date,
34 | job_formal_date,
35 | job_status_date,
36 | job_quit_date,
37 | ...data
38 | } = res
39 |
40 | const basisDate = {
41 | birthday: birthday ? moment(birthday) : null,
42 | job_entry_date: job_entry_date ? moment(job_entry_date) : null,
43 | job_formal_date: job_formal_date ? moment(job_formal_date) : null,
44 | job_status_date: job_status_date ? moment(job_status_date) : null,
45 | job_quit_date: job_quit_date ? moment(job_quit_date) : null,
46 | }
47 |
48 | form.setFieldsValue({ ...data, ...basisDate })
49 | })
50 | }
51 | return () => {
52 | store.remove(UPLOAD_TOKEN)
53 | }
54 | }, [])
55 |
56 | const onSubmit = useCallback(
57 | () => {
58 | form.validateFields().then((res) => {
59 | const id = state?.id
60 | const values = { id, ...res }
61 | addOrEditStaffFn(values).then(() => {
62 | push({
63 | pathname: '/success',
64 | state: { title: `职员${id ? '修改' : '添加'}成功`, path: '/staff/list' },
65 | })
66 | })
67 | })
68 | },
69 | [],
70 | )
71 |
72 | return (
73 |
120 | )
121 | })
122 |
123 | export default StaffAdd
124 |
--------------------------------------------------------------------------------
/src/views/Staff/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | memo, useCallback, useEffect, useState,
3 | } from 'react'
4 | import { useHistory } from 'react-router-dom'
5 | import { useAsyncFn } from 'react-use'
6 | import {
7 | Button, Table, Popconfirm, message, PaginationProps, Form, Switch, Modal,
8 | } from 'antd'
9 | import staffApi from '@src/api/staff'
10 | import BasisTable from '@src/components/BasisTable'
11 | import SearchItem, { SearchParam } from '@src/components/FormItem/SearchItem'
12 | import { IStaff } from '@src/api/types/staff'
13 |
14 | const StaffList: React.FC = memo(() => {
15 | const [search, setSearch] = useState()
16 | const [page, setPage] = useState({
17 | current: 1,
18 | pageSize: 10,
19 | })
20 | const [selectedRowKeys, setSelectedRowKeys] = useState([])
21 |
22 | const { push } = useHistory()
23 | const [form] = Form.useForm()
24 |
25 | const [staffList, getStaffFn] = useAsyncFn(staffApi.getstaffList)
26 | const [, deleteStaffFn] = useAsyncFn(staffApi.deleteStaff)
27 | const [, setStatusFn] = useAsyncFn(staffApi.setstaffStatus)
28 |
29 | useEffect(() => {
30 | getStaffData()
31 | }, [page, search])
32 |
33 | /* 页码改变 */
34 | const onPageChange = useCallback(
35 | (page: PaginationProps) => {
36 | setPage(page)
37 | },
38 | [],
39 | )
40 |
41 | /* 多选 */
42 | const onSelectChange = useCallback(
43 | (selectedRowKeys) => {
44 | setSelectedRowKeys(selectedRowKeys)
45 | },
46 | [],
47 | )
48 |
49 | /* 切换状态 */
50 | const onHandleChangeStatus = useCallback(
51 | (id, status) => {
52 | setStatusFn(id, status)
53 | /* 立马修改后,服务器反应慢 */
54 | setTimeout(() => {
55 | getStaffData()
56 | }, 10)
57 | },
58 | [],
59 | )
60 |
61 | /* 批量删除 */
62 | const onHandleDelete = useCallback(
63 | () => {
64 | if (selectedRowKeys.length < 1) {
65 | return false
66 | }
67 | Modal.confirm({
68 | title: '是否删除所选职员?',
69 | okText: '确定',
70 | cancelText: '取消',
71 | onOk() {
72 | onDeleteModal(selectedRowKeys.join())
73 | setSelectedRowKeys([])
74 | },
75 | })
76 | },
77 | [selectedRowKeys],
78 | )
79 |
80 | /* 删除职位 */
81 | const onDeleteModal = useCallback(
82 | (id:string) => {
83 | deleteStaffFn(id).then((res) => {
84 | message.success(res.message)
85 | getStaffData()
86 | })
87 | },
88 | [],
89 | )
90 |
91 | /* 点击搜索 */
92 | const onSearch = useCallback(
93 | (value: SearchParam) => {
94 | setSearch(value)
95 | },
96 | [],
97 | )
98 |
99 | const getStaffData = () => {
100 | getStaffFn({
101 | name: search?.name, status: search?.status, pageNumber: page.current || 1, pageSize: page.pageSize || 10,
102 | })
103 | }
104 |
105 | return (
106 |
107 |
115 |
116 |
117 |
118 |
119 | loading={staffList.loading}
120 | data={staffList.value?.data.data}
121 | total={staffList.value?.data.total}
122 | rowSelection={{ selectedRowKeys, onChange: onSelectChange }}
123 | onChange={onPageChange}
124 | footer={() => }>
125 |
126 | title='职员名称'
127 | dataIndex='full_name' />
128 |
129 | title='部门名称'
130 | dataIndex='jobName' />
131 |
132 | title='禁启用'
133 | dataIndex='status'
134 | align='center'
135 | width={100}
136 | render={(text, record) => (
137 | onHandleChangeStatus(record.staff_id, checked)} />
142 | )} />
143 |
144 | title='操作'
145 | align='center'
146 | width={200}
147 | render={(text, record) => (
148 |
149 |
154 |
onDeleteModal(record.staff_id)}
157 | okText='确认'
158 | cancelText='取消'>
159 |
160 |
161 |
162 | )} />
163 |
164 |
165 | )
166 | })
167 |
168 | export default StaffList
169 |
--------------------------------------------------------------------------------
/src/views/Success/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { useHistory, useLocation } from 'react-router-dom'
3 | import { Button, Result } from 'antd'
4 |
5 | const SuccessPage: React.FC = memo(() => {
6 | const { state } = useLocation<{title: string, path: string}>()
7 | const { push, goBack } = useHistory()
8 | return (
9 | push(state.path || '/')}>
15 | 查看列表
16 | ,
17 | ,
18 | ]} />
19 | )
20 | })
21 |
22 | export default SuccessPage
23 |
--------------------------------------------------------------------------------
/src/views/User/List/AddModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect } from 'react'
2 | import { useAsyncFn } from 'react-use'
3 | import sha256 from 'crypto-js/sha256'
4 | import { Form, message, Modal } from 'antd'
5 | import { FormParam } from '@src/api/types/user'
6 | import userApi from '@src/api/user'
7 | import authApi from '@src/api/auth'
8 | import UserItem from '@src/components/FormItem/UserItem'
9 |
10 | interface IProps {
11 | visible: boolean
12 | id?: string
13 | onClose: () => void
14 | onConfirm: () => void
15 | }
16 |
17 | const AddModal: React.FC = memo(({
18 | visible, id, onClose, onConfirm,
19 | }) => {
20 | const [form] = Form.useForm()
21 |
22 | const [addOrEditUser, addOrEditUserFn] = useAsyncFn(userApi.addOrEditUser)
23 | const [getRole, getRoleFn] = useAsyncFn(authApi.getRole)
24 | const [, getUserDetailFn] = useAsyncFn(userApi.getUserDetail)
25 |
26 | useEffect(() => {
27 | if (id) {
28 | getUserDetailFn(id).then((res) => {
29 | form.setFieldsValue(res)
30 | })
31 | }
32 | }, [id])
33 |
34 | useEffect(() => {
35 | getRoleFn()
36 | }, [])
37 |
38 | /* 提交 */
39 | const onSubmit = useCallback(
40 | () => {
41 | form.validateFields().then((res) => {
42 | const { cpassword, ...values } = res as FormParam & {cpassword: string}
43 | const data = { id, ...values, password: sha256(values.password).toString() }
44 | addOrEditUserFn(data).then((res) => {
45 | message.success(res.message)
46 | onConfirm()
47 | })
48 | }).catch((err) => {
49 | message.error(err.errorFields[0].errors[0])
50 | })
51 | },
52 | [id],
53 | )
54 |
55 | return (
56 |
63 |
79 |
80 | )
81 | })
82 |
83 | export default AddModal
84 |
--------------------------------------------------------------------------------
/src/views/User/List/index.less:
--------------------------------------------------------------------------------
1 | .header-form {
2 | display: flex;
3 | justify-content: space-between;
4 | &>div {
5 | display: flex;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/views/User/List/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | memo, useCallback, useEffect, useState,
3 | } from 'react'
4 | import { useAsyncFn } from 'react-use'
5 | import {
6 | Button, Table, Popconfirm, message, PaginationProps, Form, Switch, Modal,
7 | } from 'antd'
8 | import userApi from '@src/api/user'
9 | import { User } from '@src/api/types/user'
10 | import BasisTable from '@src/components/BasisTable'
11 | import AuthWrapper from '@src/components/AuthWrapper'
12 | import SearchItem, { SearchParam } from '@src/components/FormItem/SearchItem'
13 | import AddModal from './AddModal'
14 | import './index.less'
15 |
16 | const UserList: React.FC = memo(() => {
17 | const [search, setSearch] = useState()
18 | const [id, setId] = useState('')
19 | const [addVisible, setAddVisible] = useState(false)
20 | const [page, setPage] = useState({
21 | current: 1,
22 | pageSize: 10,
23 | })
24 | const [selectedRowKeys, setSelectedRowKeys] = useState([])
25 |
26 | const [form] = Form.useForm()
27 |
28 | const [userList, getUserListFn] = useAsyncFn(userApi.getUserList)
29 | const [, deleteDepartmentFn] = useAsyncFn(userApi.deleteUser)
30 | const [, setDepartmentStatusFn] = useAsyncFn(userApi.setUserStatus)
31 |
32 | useEffect(() => {
33 | getUserData()
34 | }, [page, search])
35 |
36 | /* 关闭对话框 */
37 | const closeAddModal = useCallback(
38 | () => {
39 | setAddVisible(false)
40 | setId('')
41 | },
42 | [],
43 | )
44 |
45 | /* 页码改变 */
46 | const onPageChange = useCallback(
47 | (page: PaginationProps) => {
48 | setPage(page)
49 | },
50 | [],
51 | )
52 |
53 | /* 多选 */
54 | const onSelectChange = useCallback(
55 | (selectedRowKeys) => {
56 | setSelectedRowKeys(selectedRowKeys)
57 | },
58 | [],
59 | )
60 |
61 | /* 切换状态 */
62 | const onHandleChangeStatus = useCallback(
63 | (id, status) => {
64 | setDepartmentStatusFn(id, status)
65 | /* 立马修改后,服务器反应慢 */
66 | setTimeout(() => {
67 | getUserData()
68 | }, 10)
69 | },
70 | [],
71 | )
72 |
73 | /* 批量删除 */
74 | const onHandleDelete = useCallback(
75 | () => {
76 | if (selectedRowKeys.length < 1) {
77 | return false
78 | }
79 | Modal.confirm({
80 | title: '是否删除所选部门?',
81 | okText: '确定',
82 | cancelText: '取消',
83 | onOk() {
84 | onDeleteModal(selectedRowKeys.join())
85 | setSelectedRowKeys([])
86 | },
87 | })
88 | },
89 | [selectedRowKeys],
90 | )
91 |
92 | /* 删除 */
93 | const onDeleteModal = useCallback(
94 | (id:string) => {
95 | deleteDepartmentFn(id).then((res) => {
96 | message.success(res.message)
97 | getUserData()
98 | })
99 | },
100 | [],
101 | )
102 |
103 | /* 点击搜索 */
104 | const onHandleSearch = useCallback(
105 | (value: SearchParam) => {
106 | setSearch(value)
107 | },
108 | [],
109 | )
110 |
111 | const getUserData = () => {
112 | getUserListFn({
113 | name: search?.name,
114 | status: search?.status,
115 | pageNumber: page.current || 1,
116 | pageSize: page.pageSize || 10,
117 | })
118 | }
119 |
120 | return (
121 |
122 |
144 |
145 | loading={userList.loading}
146 | data={userList.value?.data.data}
147 | total={userList.value?.data.total}
148 | rowSelection={{ selectedRowKeys, onChange: onSelectChange }}
149 | onChange={onPageChange}
150 | footer={() => }>
151 |
152 | title='用户名'
153 | dataIndex='username' />
154 |
155 | title='真实姓名'
156 | dataIndex='truename' />
157 |
158 | title='手机号'
159 | dataIndex='phone' />
160 |
161 | title='禁启用'
162 | dataIndex='status'
163 | align='center'
164 | width={100}
165 | render={(text, record) => (
166 | onHandleChangeStatus(record.id, checked)} />
171 | )} />
172 |
173 | title='操作'
174 | align='center'
175 | width={200}
176 | render={(text, record) => (
177 |
178 |
183 |
onDeleteModal(record.id)}
186 | okText='确认'
187 | cancelText='取消'>
188 |
189 |
190 |
191 | )} />
192 |
193 | {/* 添加框 */}
194 |
{ closeAddModal() }}
198 | onConfirm={() => { closeAddModal() }} />
199 |
200 | )
201 | })
202 |
203 | export default UserList
204 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "DOM",
6 | "DOM.Iterable",
7 | "ESNext"
8 | ],
9 | "paths": {
10 | "@src/*": [
11 | "./src/*"
12 | ]
13 | },
14 | "types": [
15 | "vite/client",
16 | "node",
17 | ],
18 | "allowJs": false,
19 | "skipLibCheck": false,
20 | "esModuleInterop": false,
21 | "allowSyntheticDefaultImports": true,
22 | "strict": true,
23 | "forceConsistentCasingInFileNames": true,
24 | "module": "ESNext",
25 | "moduleResolution": "Node",
26 | "resolveJsonModule": true,
27 | "isolatedModules": true,
28 | "noEmit": true,
29 | "jsx": "react",
30 | "suppressImplicitAnyIndexErrors": true,
31 | },
32 | "include": [
33 | "./src",
34 | ]
35 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import reactRefresh from '@vitejs/plugin-react-refresh'
3 | import styleImport from 'vite-plugin-style-import'
4 | import path from 'path'
5 | import fs from 'fs'
6 | import lessToJS from 'less-vars-to-js'
7 |
8 | const themeVariables = lessToJS(
9 | fs.readFileSync(path.resolve(__dirname, './src/styles/variables.less'), 'utf8'),
10 | )
11 |
12 | // 获取环境变量
13 | /* const env = process.argv[process.argv.length - 1] */
14 |
15 | export default defineConfig({
16 | base: '/reactAdmin/',
17 | resolve: {
18 | // 路径别名
19 | alias: [
20 | { find: /^@src/, replacement: path.resolve(__dirname, 'src') },
21 | ],
22 | },
23 | /* 跨域转发 */
24 | server: {
25 | open: true,
26 | proxy: {
27 | '/api': {
28 | target: 'http://www.web-jshtml.cn/api/react',
29 | changeOrigin: true,
30 | rewrite: (path) => path.replace(/^\/api/, ''),
31 | },
32 | },
33 | },
34 | plugins: [
35 | reactRefresh(),
36 | styleImport({
37 | libs: [
38 | {
39 | libraryName: 'antd',
40 | esModule: true,
41 | resolveStyle: (name) => `antd/es/${name}/style/index`,
42 | },
43 | ],
44 | }),
45 | ],
46 | css: {
47 | preprocessorOptions: {
48 | less: {
49 | // 支持内联 JavaScript
50 | javascriptEnabled: true,
51 | // 配置变量,重写样式
52 | modifyVars: {
53 | ...themeVariables,
54 | },
55 | },
56 | },
57 | },
58 | })
59 |
--------------------------------------------------------------------------------