├── .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 | ![01_登录](./resources/01_登录.png) 76 | 77 | ![02_注册](./resources/02_注册.png) 78 | 79 | ![03_控制台](./resources/03_控制台.png) 80 | 81 | ![04_用户列表](./resources/04_用户列表.png) 82 | 83 | ![05_用户添加修改](./resources/05_用户添加修改.png) 84 | 85 | ![06_部门列表](./resources/06_部门列表.png) 86 | 87 | ![07_添加部门](./resources/07_添加部门.png) 88 | 89 | ![08_职位列表](./resources/08_职位列表.png) 90 | 91 | ![09_添加职位](./resources/09_添加职位.png) 92 | 93 | ![10_职员列表](./resources/10_职员列表.png) 94 | 95 | ![11_职员添加](./resources/11_职员添加.png) 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 |
39 | 44 |
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 | Min_Width ? pathToList(pathname) : []}> 60 | {props?.routes?.map((route) => ( 61 | route.children && route.children.length > 0 ? renderSubMenu(route) : renderMenu(route) 62 | ))} 63 | 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 ? avatar : 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 |
53 | 54 | 55 | 56 | 57 | 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 |
112 | 113 | 114 | 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 |
56 | ({ value: item.id, text: item.name }))} 59 | loading={departmentAll?.loading} /> 60 | 61 | 62 | 63 | 64 | 65 | 66 | 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 |
112 | 113 | 114 | 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 |
58 | 59 | 60 | 61 | 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 |
48 | 49 | 50 | 51 | 52 | 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 |
82 | 基础信息 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 其他信息 98 | ({ text: item.jobName, value: item.jobId }))} 101 | form={form} /> 102 | ({ text: item.name, value: item.id }))} 105 | form={form} /> 106 | {/* */} 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 |
116 | 117 | 118 | 119 | 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 |
112 | 113 | 114 | 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 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | ({ text: item.label, value: item.value }))} /> 78 | 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 |
127 |
128 | 129 | 130 | 131 |
132 |
133 | { setAddVisible(true) }}> 139 | 添加用户 140 | 141 | )} /> 142 |
143 |
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 | --------------------------------------------------------------------------------