├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── config ├── antd-variables.less └── index.ts ├── index.html ├── jest.config.js ├── mock ├── department.ts ├── index.ts ├── login.ts ├── menu.ts ├── remoteSearch.ts ├── role.ts └── user.ts ├── package-lock.json ├── package.json ├── src ├── App.tsx ├── api │ ├── department.ts │ ├── index.ts │ ├── login.ts │ ├── menu.ts │ ├── models │ │ ├── departmentModel.ts │ │ ├── menuModel.ts │ │ ├── roleModel.ts │ │ └── userModel.ts │ ├── remoteSearch.ts │ ├── role.ts │ └── user.ts ├── assets │ └── images │ │ ├── 404.png │ │ ├── avatar.jpg │ │ ├── bg-xw.jpg │ │ ├── bg.jpg │ │ ├── githubCorner.png │ │ ├── logo.png │ │ ├── logo.svg │ │ └── vite.svg ├── components │ ├── Breadcrumb │ │ ├── index.less │ │ └── index.tsx │ ├── ErrorBoundary │ │ ├── core.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── FullScreen │ │ ├── index.less │ │ └── index.tsx │ ├── Hamburger │ │ ├── index.less │ │ └── index.tsx │ ├── Icon │ │ ├── Icon │ │ │ └── index.tsx │ │ ├── IconPicker │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── data │ │ │ ├── AntIcons.tsx │ │ │ └── IconPicker.ts │ │ └── index.tsx │ ├── IntlDropdown │ │ └── index.tsx │ ├── Loading │ │ ├── AppLoading │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── PageLoading │ │ │ └── index.tsx │ │ └── index.tsx │ ├── Mallki │ │ ├── index.less │ │ └── index.tsx │ ├── PageBox │ │ ├── index.less │ │ └── index.tsx │ ├── PanThumb │ │ ├── index.less │ │ └── index.tsx │ ├── Setting │ │ └── index.tsx │ ├── TreeTable │ │ ├── index.less │ │ └── index.tsx │ ├── TypingCard │ │ └── index.tsx │ ├── UserAvatarDropdown │ │ └── index.tsx │ ├── VBasicDrawerForm │ │ ├── index.less │ │ └── index.tsx │ ├── VBasicFormV2 │ │ ├── components │ │ │ └── FromItem │ │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── VBasicModal │ │ ├── index.less │ │ └── index.tsx │ ├── VBasicModalForm │ │ ├── index.less │ │ └── index.tsx │ ├── VBasicTable │ │ ├── components │ │ │ ├── VBasicTableHeader │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── VBasicTableTitle │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ └── VBasicTableToolbar │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── default.ts │ │ ├── hooks │ │ │ └── useTableScrollY.ts │ │ ├── index.less │ │ └── index.tsx │ ├── VBasicTree │ │ ├── index.less │ │ ├── index.tsx │ │ └── utils │ │ │ └── index.tsx │ └── vBasicForm │ │ ├── components │ │ ├── FormIconPicker │ │ │ └── index.tsx │ │ ├── FormTree │ │ │ └── index.tsx │ │ ├── FormTreeSelect │ │ │ └── index.tsx │ │ └── FromItem │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx ├── config │ ├── menuConfig.tsx │ └── routeMap.ts ├── core │ └── bootstrap.ts ├── defaultSetting.ts ├── demo.tsx ├── favicon.svg ├── hooks │ ├── useActions.tsx │ ├── useClickOutside.tsx │ ├── useDocumentSize.ts │ └── useShallowEqualSelector.tsx ├── layout │ ├── Content │ │ ├── index.less │ │ └── index.tsx │ ├── DrawerSider │ │ └── index.tsx │ ├── Header │ │ ├── index.less │ │ └── index.tsx │ ├── RightPanel │ │ ├── index.less │ │ └── index.tsx │ ├── Sider │ │ ├── Logo │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Menu │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── TagsView │ │ ├── components │ │ │ └── TagsList │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── index.less │ └── index.tsx ├── locales │ ├── index.tsx │ └── lang │ │ ├── en_US.ts │ │ ├── id_ID.ts │ │ └── zh_CN.ts ├── main.tsx ├── router │ └── index.tsx ├── store │ ├── action-types.ts │ ├── actions │ │ ├── app.ts │ │ ├── auth.ts │ │ ├── index.ts │ │ ├── settings.ts │ │ ├── tagsView.ts │ │ └── user.ts │ ├── index.ts │ └── reducers │ │ ├── app.ts │ │ ├── index.ts │ │ ├── settings.ts │ │ ├── tagsView.ts │ │ └── user.ts ├── styles │ ├── index.less │ ├── mixins.less │ ├── transition.less │ ├── utilities.less │ └── variables.module.less ├── utils │ ├── auth.ts │ ├── clipboard.ts │ ├── index.ts │ ├── request.ts │ ├── typing.ts │ ├── utils.ts │ └── validate.ts └── views │ ├── about │ └── index.tsx │ ├── account │ ├── center │ │ ├── index.less │ │ └── index.tsx │ └── setting │ │ ├── index.less │ │ └── index.tsx │ ├── component │ ├── form │ │ └── index.tsx │ └── table │ │ └── index.tsx │ ├── dashboard │ ├── components │ │ ├── BarChart │ │ │ └── index.tsx │ │ ├── BoxCard │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── LineChart │ │ │ └── index.tsx │ │ ├── PanelGroup │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── PieChart │ │ │ └── index.tsx │ │ ├── RadarChart │ │ │ └── index.tsx │ │ └── TransactionTable │ │ │ └── index.tsx │ ├── index.less │ ├── index.tsx │ └── models │ │ └── index.ts │ ├── doc │ └── index.tsx │ ├── error │ └── 404 │ │ ├── index.less │ │ └── index.tsx │ ├── index.ts │ ├── login │ ├── index.less │ └── index.tsx │ ├── nested │ └── menu1 │ │ ├── menu1-1 │ │ └── index.tsx │ │ └── menu1-2 │ │ └── menu1-2-1 │ │ └── index.tsx │ ├── permission │ ├── admin.tsx │ ├── editor.tsx │ ├── guest.tsx │ └── index.tsx │ └── system │ ├── account │ ├── index.less │ ├── index.tsx │ └── models │ │ ├── departmentModel.ts │ │ └── userModel.ts │ ├── changePassword │ ├── index.less │ └── index.tsx │ ├── department │ ├── index.less │ └── index.tsx │ ├── menu │ ├── index.less │ └── index.tsx │ └── role │ ├── index.less │ └── index.tsx ├── tests ├── foo.ts └── index.test.tsx ├── tsconfig.build.json ├── tsconfig.json └── vite.config.ts /.env.development: -------------------------------------------------------------------------------- 1 | VITE_APP_API_URL = http://localhost:3000/api -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 请求Api接口基础地址 2 | VITE_APP_API_URL = http://nestjs.cms.visionwu.top -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # src/serviceWorker.js 2 | # src/serviceWorker.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | node: true, 6 | es2021: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:react/recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:prettier/recommended', 13 | ], 14 | settings: { 15 | react: { 16 | pragma: 'React', 17 | version: 'detect', 18 | }, 19 | }, 20 | parser: '@typescript-eslint/parser', 21 | parserOptions: { 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | ecmaVersion: 2021, 26 | }, 27 | plugins: ['react', '@typescript-eslint'], 28 | rules: { 29 | 'react/prop-types': 0, 30 | 'no-empty-interface': 0, 31 | '@typescript-eslint/no-explicit-any': ['off'], 32 | '@typescript-eslint/explicit-module-boundary-types': 'off', 33 | 'no-useless-escape': 0, 34 | 'import/prefer-default-export': 0, 35 | '@typescript-eslint/no-empty-interface': 0, 36 | 'react/display-name': [2], 37 | '@typescript-eslint/no-non-null-assertion': 0, 38 | 'no-self-assign': 0, 39 | '@typescript-eslint/ban-ts-comment': 'off', 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | 7 | ui-admin 8 | ui-mobile 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | 5 | # Ignore all HTML files: 6 | *.html -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | semi: false, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | bracketSpacing: false, 7 | jsxBracketSameLine: false, 8 | arrowParens: 'avoid', 9 | insertPragma: false, 10 | tabWidth: 2, 11 | useTabs: false, 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | cache: 5 | directories: 6 | - node_modules 7 | env: 8 | - CI=true 9 | script: 10 | - npm run lint 11 | deploy: 12 | provider: pages 13 | skip_cleanup: true 14 | github_token: $GITHUB_TOKEN 15 | # local_dir: storybook-static 16 | on: 17 | branch: main 18 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | '@babel/preset-react', 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /config/antd-variables.less: -------------------------------------------------------------------------------- 1 | // 自定义覆盖 ============================================================= 2 | @primary-color: '#0960BD'; // 全局主题 3 | // 下面你可以各种写一些覆盖的样式,这里就简单覆盖一个主题色的样式,我们改为深蓝 4 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | export type EnvName = 'development' | 'beta' | 'production' | 'preview' 2 | 3 | interface BaseConfig { 4 | cdn?: string 5 | apiBaseUrl?: string 6 | } 7 | 8 | type Config = { 9 | [key in EnvName]?: BaseConfig 10 | } 11 | 12 | const config: Config = { 13 | // 开发环境配置 14 | development: { 15 | cdn: './', 16 | apiBaseUrl: 'http://localhost:5001', 17 | }, 18 | // 测试环境配置 19 | beta: { 20 | cdn: './', 21 | apiBaseUrl: 'http://nestjs.cms.visionwu.top', 22 | }, 23 | // 生产环境配置 24 | production: { 25 | cdn: './', 26 | apiBaseUrl: 'http://nestjs.cms.visionwu.top', 27 | }, 28 | // 预览环境配置 29 | preview: { 30 | cdn: './', 31 | apiBaseUrl: 'http://nestjs.cms.visionwu.top', 32 | }, 33 | } 34 | 35 | export default config 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.[jt]sx?$': 'babel-jest', 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | moduleNameMapper: { 7 | '^@/(.*)$': '/src/$1', 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | import {MockMethod} from 'vite-plugin-mock' 2 | import {users, TokenName} from './user' 3 | /** 自定义http响应状态码 */ 4 | export enum HttpStatusCode { 5 | OK = 20000, 6 | USERNAME_ERROR = 60204, 7 | PASSWORD_ERROR = 60203, 8 | BAD_REQUEST = 40000, 9 | ILLEGAL_UNAUTHORIZED = 50008, 10 | } 11 | 12 | /** httpResponse响应数据格式 */ 13 | export interface ResponseData { 14 | code: number 15 | message?: string 16 | data: any 17 | } 18 | 19 | /** 校验是否有权限 */ 20 | export function checkPermission(token: TokenName): boolean { 21 | const user = users[token] 22 | if (!user) { 23 | return false 24 | } 25 | return true 26 | } 27 | 28 | export default [] as MockMethod[] 29 | -------------------------------------------------------------------------------- /mock/login.ts: -------------------------------------------------------------------------------- 1 | import {MockMethod} from 'vite-plugin-mock' 2 | import {users, tokens, UserProps} from './user' 3 | import {ResponseData, HttpStatusCode} from './index' 4 | 5 | /** mock login api urls */ 6 | enum LoginUrls { 7 | userLogin = '/user/login', 8 | userLogout = '/user/logout', 9 | } 10 | 11 | export default [ 12 | // user login 13 | { 14 | url: LoginUrls.userLogin, 15 | method: 'post', 16 | response: ({body}) => { 17 | const {username, password} = body 18 | const userToken = tokens[username] 19 | if (!userToken) { 20 | return { 21 | code: HttpStatusCode.USERNAME_ERROR, 22 | message: '用户名不正确', 23 | } as ResponseData 24 | } 25 | 26 | const token = userToken.token 27 | 28 | const user = users[token] as UserProps 29 | 30 | if (user.password !== password) { 31 | return { 32 | code: HttpStatusCode.PASSWORD_ERROR, 33 | message: '密码错误', 34 | } as ResponseData 35 | } 36 | 37 | return { 38 | code: HttpStatusCode.OK, 39 | message: 'success', 40 | data: token, 41 | } as ResponseData 42 | }, 43 | }, 44 | // user logout 45 | { 46 | url: LoginUrls.userLogout, 47 | method: 'post', 48 | response: () => { 49 | return { 50 | code: HttpStatusCode.OK, 51 | message: 'success', 52 | } as ResponseData 53 | }, 54 | }, 55 | ] as MockMethod[] 56 | -------------------------------------------------------------------------------- /mock/remoteSearch.ts: -------------------------------------------------------------------------------- 1 | import {MockMethod} from 'vite-plugin-mock' 2 | import {ResponseData} from './index' 3 | 4 | type TagType = 'success' | 'pending' 5 | 6 | /** api urls */ 7 | enum TransactionUrls { 8 | transactionListUrl = '/transaction/list', 9 | } 10 | 11 | export interface TransactionProps { 12 | key: number 13 | order_no: string 14 | price?: number 15 | tag?: TagType 16 | } 17 | 18 | const list: TransactionProps[] = [ 19 | { 20 | key: 2204, 21 | order_no: 'D28CA823-C7bf-9b9a-fbbF-124dfDFB52B2', 22 | price: 9123.55, 23 | tag: 'success', 24 | }, 25 | { 26 | key: 2205, 27 | order_no: 'dbce34dc-Fd85-5672-bC9f-4BCb3b3fbdEc', 28 | price: 14311.4, 29 | tag: 'success', 30 | }, 31 | { 32 | key: 2206, 33 | order_no: 'Dadc4FcE-706F-ffCF-EE69-79f79dC1e4F7', 34 | price: 11124.6, 35 | tag: 'success', 36 | }, 37 | { 38 | key: 2207, 39 | order_no: 'AB1D14fE-F35F-84a3-A1BE-75e85ef69861', 40 | price: 5299.87, 41 | tag: 'pending', 42 | }, 43 | { 44 | key: 2208, 45 | order_no: '70D8cfeD-1e6C-184F-eAee-Cc8517bcc8E0', 46 | price: 12419, 47 | tag: 'pending', 48 | }, 49 | { 50 | key: 2209, 51 | order_no: '3Ef48E38-F714-6D5B-C22b-aCF2dA4cB6D7', 52 | price: 11994.18, 53 | tag: 'pending', 54 | }, 55 | { 56 | key: 2210, 57 | order_no: '29d8EAcC-fDeC-7Abe-edC9-fAb02120DBA8', 58 | price: 12687.2, 59 | tag: 'pending', 60 | }, 61 | { 62 | key: 2211, 63 | order_no: 'bBdFAcd1-6FA9-271C-F24A-e7478d5Bf534', 64 | price: 4551.84, 65 | tag: 'pending', 66 | }, 67 | { 68 | key: 2212, 69 | order_no: 'ECc478B2-eaCE-A52C-440D-DAe7b123846c', 70 | price: 12230.54, 71 | tag: 'success', 72 | }, 73 | { 74 | key: 2213, 75 | order_no: 'cCe1E869-4D2F-2A9E-bc9b-f6b894baf8D4', 76 | price: 6468.46, 77 | tag: 'pending', 78 | }, 79 | { 80 | key: 2214, 81 | order_no: '8521C0df-b76B-bFBc-cB23-01c34AFf2Fd0', 82 | price: 10924.04, 83 | tag: 'pending', 84 | }, 85 | { 86 | key: 2215, 87 | order_no: 'fFeAd47f-42dc-06e7-6df8-811aB2fb3a1f', 88 | price: 12574.1, 89 | tag: 'success', 90 | }, 91 | { 92 | key: 2216, 93 | order_no: '3A7f248F-1E99-1a74-2Df9-458bfEBe1D55', 94 | price: 13821.17, 95 | tag: 'pending', 96 | }, 97 | { 98 | key: 2217, 99 | order_no: '3B939bfF-beCD-b39c-7b2F-cEbDfDF517B6', 100 | price: 5370.33, 101 | tag: 'success', 102 | }, 103 | { 104 | key: 2218, 105 | order_no: '4b685764-F745-6cF2-4Bc7-BEEC2A2Fb66F', 106 | price: 14661.3, 107 | tag: 'success', 108 | }, 109 | { 110 | key: 2219, 111 | order_no: '6D7AAB2B-F975-E0fA-7C0C-2f7C42f34b65', 112 | price: 9912.2, 113 | tag: 'success', 114 | }, 115 | { 116 | key: 2220, 117 | order_no: '772A8ED7-DCD1-fA97-cbBb-1dA8Aa35dE7B', 118 | price: 1109.7, 119 | tag: 'pending', 120 | }, 121 | { 122 | key: 2221, 123 | order_no: 'EC3d62ae-90c4-3E4f-72Ce-6A2FfDFBBcdD', 124 | price: 5093.5, 125 | tag: 'success', 126 | }, 127 | { 128 | key: 2222, 129 | order_no: '4bF63315-CdB9-8B7F-ED47-cBfC215A1F4E', 130 | price: 7769.2, 131 | tag: 'success', 132 | }, 133 | { 134 | key: 2223, 135 | order_no: 'cbdaF666-7D68-daE0-44fA-9EA31c5A5e6F', 136 | price: 11468, 137 | tag: 'pending', 138 | }, 139 | ] 140 | 141 | export default [ 142 | // mock transaction list 143 | { 144 | url: TransactionUrls.transactionListUrl, 145 | method: 'get', 146 | response: () => { 147 | return { 148 | code: 20000, 149 | data: list, 150 | } as ResponseData 151 | }, 152 | }, 153 | ] as MockMethod[] 154 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react' 2 | import Router from '@/router/index' 3 | import IntlPro from '@/locales' 4 | import useShallowEqualSelector from 'hooks/useShallowEqualSelector' 5 | // import Initializer from '@/core/bootstrap' 6 | 7 | interface IAppProps {} 8 | 9 | const App: React.FC = () => { 10 | const lang = useShallowEqualSelector(state => state.app.lang) 11 | const {weekMode, grayMode} = useShallowEqualSelector(state => state.settings) 12 | 13 | const setWeekMode = () => { 14 | if (weekMode) { 15 | document.documentElement.classList.add('week-mode') 16 | } else { 17 | document.documentElement.classList.remove('week-mode') 18 | } 19 | } 20 | 21 | const setGrayMode = () => { 22 | if (grayMode) { 23 | document.documentElement.classList.add('gray-mode') 24 | } else { 25 | document.documentElement.classList.remove('gray-mode') 26 | } 27 | } 28 | 29 | useEffect(() => { 30 | setWeekMode() 31 | }, [weekMode]) 32 | 33 | useEffect(() => { 34 | setGrayMode() 35 | }, [grayMode]) 36 | return ( 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | export default React.memo(App) 44 | -------------------------------------------------------------------------------- /src/api/department.ts: -------------------------------------------------------------------------------- 1 | import {AxiosResponse} from 'axios' 2 | import request from 'utils/request' 3 | import {DepartmentProps, DepartmentQueryParams} from './models/departmentModel' 4 | 5 | /** 部门请求urls */ 6 | enum DepartmentUrls { 7 | departmentListUrl = 'system/dept/list', 8 | addDepartmentUrl = 'system/dept/add', 9 | updateDepartmentUrl = 'system/dept', 10 | removeDepartmentUrl = 'system/dept', 11 | selectDepartmentUrl = 'system/dept/select', 12 | ownDeptTreeUrl = 'system/dept/ownDeptTree', 13 | roleDeptTreeSelectUrl = 'system/dept/roleDeptTree', 14 | } 15 | 16 | export function getDepartmentList( 17 | params: DepartmentQueryParams = {}, 18 | ): Promise> { 19 | return request({ 20 | url: DepartmentUrls.departmentListUrl, 21 | method: 'get', 22 | params, 23 | }) 24 | } 25 | 26 | export function addDepartment( 27 | department: DepartmentProps, 28 | ): Promise> { 29 | return request({ 30 | url: DepartmentUrls.addDepartmentUrl, 31 | method: 'post', 32 | data: department, 33 | }) 34 | } 35 | 36 | export function updateDepartment( 37 | id: string, 38 | department: DepartmentProps, 39 | ): Promise> { 40 | return request({ 41 | url: `${DepartmentUrls.updateDepartmentUrl}/${id}`, 42 | method: 'put', 43 | data: department, 44 | }) 45 | } 46 | 47 | export function removeDepartment( 48 | id: string | number, 49 | ): Promise> { 50 | return request({ 51 | url: `${DepartmentUrls.removeDepartmentUrl}/${id}`, 52 | method: 'delete', 53 | }) 54 | } 55 | 56 | export function selectDepartment(id: string): Promise> { 57 | return request({ 58 | url: `${DepartmentUrls.selectDepartmentUrl}/${id}`, 59 | method: 'get', 60 | }) 61 | } 62 | 63 | export function roleDeptTreeSelect(id: string): Promise> { 64 | return request({ 65 | url: `${DepartmentUrls.roleDeptTreeSelectUrl}/${id}`, 66 | method: 'get', 67 | }) 68 | } 69 | 70 | export function ownDeptTree(): Promise> { 71 | return request({ 72 | url: DepartmentUrls.ownDeptTreeUrl, 73 | method: 'get', 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | /** 自定义http响应状态码 */ 2 | export enum HttpStatusCode { 3 | OK = 20000, 4 | USERNAME_ERROR = 60204, 5 | PASSWORD_ERROR = 60203, 6 | BAD_REQUEST = 40000, 7 | ILLEGAL_UNAUTHORIZED = 50008, 8 | } 9 | 10 | /** httpResponse响应数据格式 */ 11 | export interface ResponseData { 12 | code: number 13 | message?: string 14 | data: any 15 | pageNumber?: number 16 | total?: number 17 | } 18 | -------------------------------------------------------------------------------- /src/api/login.ts: -------------------------------------------------------------------------------- 1 | import {AxiosResponse} from 'axios' 2 | import request from 'utils/request' 3 | 4 | /** 用户请求urls */ 5 | enum LoginUrls { 6 | userLogin = '/user/login', 7 | userLogout = '/user/logout', 8 | } 9 | 10 | interface UserProps { 11 | username: string 12 | password: string 13 | } 14 | 15 | export function reqLogin(data: UserProps): Promise> { 16 | return request({ 17 | url: LoginUrls.userLogin, 18 | method: 'post', 19 | data, 20 | }) 21 | } 22 | 23 | export function reqLogout(): Promise> { 24 | return request({ 25 | url: LoginUrls.userLogout, 26 | method: 'post', 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/api/menu.ts: -------------------------------------------------------------------------------- 1 | import {AxiosResponse} from 'axios' 2 | import request from 'utils/request' 3 | import {MenuProps, MenuQueryParams} from './models/menuModel' 4 | 5 | // 菜单请求的url 6 | enum MenuUrls { 7 | menuListUrl = '/system/menu/list', 8 | addMenuUrl = '/system/menu/add', 9 | updateMenuUrl = '/system/menu', 10 | removeMenuUrl = '/system/menu', 11 | getRoutesUrl = '/system/menu/getRoutes', 12 | menuTreeUrl = '/system/menu/tree', 13 | menuTreeSelectUrl = '/system/menu/treeSelect', 14 | roleMenuTreeSelectUrl = '/system/menu/roleMenuTreeSelect', 15 | } 16 | 17 | /** 请求菜单列表 */ 18 | export function getMenuList( 19 | params: MenuQueryParams, 20 | ): Promise> { 21 | return request({ 22 | url: MenuUrls.menuListUrl, 23 | method: 'get', 24 | params, 25 | }) 26 | } 27 | 28 | export function addMenu(menu: MenuProps): Promise> { 29 | return request({ 30 | url: MenuUrls.addMenuUrl, 31 | method: 'post', 32 | data: menu, 33 | }) 34 | } 35 | 36 | export function updateMenu( 37 | id: string | number, 38 | menu: MenuProps, 39 | ): Promise> { 40 | return request({ 41 | url: `${MenuUrls.updateMenuUrl}/${id}`, 42 | method: 'put', 43 | data: menu, 44 | }) 45 | } 46 | 47 | export function removeMenu(id: string | number): Promise> { 48 | return request({ 49 | url: `${MenuUrls.removeMenuUrl}/${id}`, 50 | method: 'delete', 51 | }) 52 | } 53 | 54 | export function findRoutes(): Promise> { 55 | return request({ 56 | url: MenuUrls.getRoutesUrl, 57 | method: 'get', 58 | }) 59 | } 60 | 61 | export function getMenuTree(): Promise> { 62 | return request({ 63 | url: MenuUrls.menuTreeUrl, 64 | method: 'get', 65 | }) 66 | } 67 | 68 | export function getTreeSelect(): Promise> { 69 | return request({ 70 | url: MenuUrls.menuTreeSelectUrl, 71 | method: 'get', 72 | }) 73 | } 74 | 75 | export function getRoleMenuTreeSelect(): Promise> { 76 | return request({ 77 | url: MenuUrls.roleMenuTreeSelectUrl, 78 | method: 'get', 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /src/api/models/departmentModel.ts: -------------------------------------------------------------------------------- 1 | /** 部门类型定义 */ 2 | export interface DepartmentProps { 3 | _id: string 4 | parentId: string 5 | name: string 6 | orderId: number 7 | leader: string 8 | email?: string 9 | mobile?: string 10 | status?: DepartmentStatus 11 | } 12 | 13 | /** 部门状态 */ 14 | export enum DepartmentStatus { 15 | /** 启用 */ 16 | enable = 1, 17 | /** 禁用 */ 18 | disable = 0, 19 | } 20 | 21 | /** 部门列表查询参数 */ 22 | export interface DepartmentQueryParams { 23 | name?: string 24 | status?: DepartmentStatus | string 25 | } 26 | -------------------------------------------------------------------------------- /src/api/models/menuModel.ts: -------------------------------------------------------------------------------- 1 | /** 菜单类型 */ 2 | export enum MenuType { 3 | /** 目录 */ 4 | M = 2, 5 | /** 菜单 */ 6 | C = 1, 7 | /** 按钮 */ 8 | B = 0, 9 | } 10 | 11 | /** 菜单状态 */ 12 | export enum MenuStatus { 13 | /** 禁用 */ 14 | disable = 0, 15 | /** 启用 */ 16 | enable = 1, 17 | } 18 | 19 | /** 菜单项数据类型定义 */ 20 | export interface MenuProps { 21 | _id: string 22 | name: string 23 | icon?: string 24 | path?: string 25 | orderId: string | number 26 | parentId: string 27 | type: MenuType 28 | status: MenuStatus 29 | } 30 | 31 | /** 菜单列表查询参数 */ 32 | export interface MenuQueryParams { 33 | name?: string 34 | status?: MenuStatus | string 35 | } 36 | -------------------------------------------------------------------------------- /src/api/models/roleModel.ts: -------------------------------------------------------------------------------- 1 | /** 角色状态 */ 2 | export enum RoleStatus { 3 | /** 禁用 */ 4 | disable = 0, 5 | /** 启用 */ 6 | enable = 1, 7 | } 8 | 9 | /** 角色类型 */ 10 | export enum RoleType { 11 | /** 普通角色 */ 12 | common = 0, 13 | /** 超级管理员角色 */ 14 | admin = 1, 15 | } 16 | 17 | /** 角色数据类型定义 */ 18 | export interface RoleProps { 19 | _id: string 20 | roleName: string 21 | roleKeys: string 22 | menuIdList: string[] | Record 23 | orderId: string | number 24 | remark?: string 25 | deptId?: string[] 26 | status: RoleStatus 27 | type: RoleType 28 | } 29 | 30 | /** 角色列表查询参数 */ 31 | export interface RoleQueryParams { 32 | name?: string 33 | status?: RoleStatus | string 34 | pageNumber?: number 35 | pageSize?: number 36 | } 37 | -------------------------------------------------------------------------------- /src/api/models/userModel.ts: -------------------------------------------------------------------------------- 1 | /** 性别枚举 */ 2 | export enum SexEnum { 3 | // 男 4 | male = 1, 5 | // 女 6 | female = 2, 7 | // 未知 8 | unknown = 3, 9 | } 10 | 11 | /** 用户状态枚举 */ 12 | export enum UserStatus { 13 | // 禁用 14 | disable = 0, 15 | // 启用 16 | enable = 1, 17 | } 18 | 19 | /** 用户类型 */ 20 | export enum UserType { 21 | /** 普通用户 */ 22 | common = 0, 23 | /** 超级管理员 */ 24 | admin = 1, 25 | } 26 | 27 | /** 用户类型 */ 28 | export interface UserProps { 29 | _id: string 30 | username: string 31 | password?: string 32 | nickname?: string 33 | roles: string[] 34 | avatar?: string 35 | mobile?: string 36 | email?: string 37 | sex?: SexEnum 38 | type?: UserType 39 | remark?: string 40 | status?: UserStatus 41 | } 42 | 43 | export interface UserQueryParams { 44 | /** 用户名 */ 45 | username?: string 46 | /** 昵称 */ 47 | nickname?: string 48 | /** 部门id */ 49 | deptId?: number | string 50 | /** 当前页 */ 51 | pageNumber?: number 52 | /** 分页数目 */ 53 | pageSize?: number 54 | } 55 | -------------------------------------------------------------------------------- /src/api/remoteSearch.ts: -------------------------------------------------------------------------------- 1 | import {AxiosResponse} from 'axios' 2 | import request from 'utils/request' 3 | 4 | enum RemoteSearch { 5 | remoteSearch = '/transaction/list', 6 | } 7 | 8 | export function remoteSearch(): Promise> { 9 | return request({ 10 | url: RemoteSearch.remoteSearch, 11 | method: 'get', 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/api/role.ts: -------------------------------------------------------------------------------- 1 | import request from 'utils/request' 2 | import {AxiosResponse} from 'axios' 3 | import {RoleProps, RoleQueryParams, RoleType} from './models/roleModel' 4 | import {RoleStatus} from 'root/mock/role' 5 | 6 | /** api请求urls */ 7 | enum RoleUrls { 8 | roleListUrl = 'system/role/list', 9 | addRoleUrl = 'system/role/add', 10 | updateRoleUrl = 'system/role', 11 | removeRoleUrl = 'system/role', 12 | selectRoleUrl = 'system/role/all', 13 | changeStatusUrl = 'system/role/changeStatus', 14 | dataScopeUrl = 'system/role/dataScope', 15 | } 16 | 17 | export function getRoleList( 18 | params: RoleQueryParams, 19 | ): Promise> { 20 | return request({ 21 | url: RoleUrls.roleListUrl, 22 | method: 'get', 23 | params, 24 | }) 25 | } 26 | 27 | export function addRole(role: RoleProps): Promise> { 28 | return request({ 29 | url: RoleUrls.addRoleUrl, 30 | method: 'post', 31 | data: role, 32 | }) 33 | } 34 | 35 | export function updateRole( 36 | id: string, 37 | role: RoleProps, 38 | ): Promise> { 39 | return request({ 40 | url: `${RoleUrls.updateRoleUrl}/${id}`, 41 | method: 'put', 42 | data: role, 43 | }) 44 | } 45 | 46 | export function removeRole(id: string): Promise> { 47 | return request({ 48 | url: `${RoleUrls.removeRoleUrl}/${id}`, 49 | method: 'delete', 50 | }) 51 | } 52 | 53 | export function selectRole(): Promise> { 54 | return request({ 55 | url: RoleUrls.selectRoleUrl, 56 | method: 'get', 57 | }) 58 | } 59 | 60 | export function changeRoleStatus( 61 | id: string, 62 | status: RoleStatus, 63 | type: RoleType, 64 | ): Promise> { 65 | return request({ 66 | url: RoleUrls.changeStatusUrl, 67 | method: 'patch', 68 | data: { 69 | id, 70 | status, 71 | type, 72 | }, 73 | }) 74 | } 75 | 76 | export function dataScope( 77 | id: string, 78 | deptIds: string[], 79 | ): Promise> { 80 | return request({ 81 | url: RoleUrls.dataScopeUrl, 82 | method: 'patch', 83 | data: { 84 | id, 85 | deptIds, 86 | }, 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import request from 'utils/request' 2 | import {AxiosResponse} from 'axios' 3 | import { 4 | UserProps, 5 | UserQueryParams, 6 | UserStatus, 7 | UserType, 8 | } from './models/userModel' 9 | 10 | /** api请求urls */ 11 | enum UserUrls { 12 | getUserInfo = 'user/info', 13 | getUserList = 'system/user/list', 14 | addUser = 'system/user/add', 15 | updateUser = 'system/user', 16 | removeUser = 'system/user', 17 | resetPwdUrl = '/system/user/resetPwd', 18 | changeStatusUrl = '/system/user/changeStatus', 19 | } 20 | 21 | export function getUserInfo(): Promise> { 22 | return request({ 23 | url: UserUrls.getUserInfo, 24 | method: 'get', 25 | }) 26 | } 27 | // eslint-disable-next-line 28 | export function getUserList( 29 | params: UserQueryParams, 30 | ): Promise> { 31 | return request({ 32 | url: UserUrls.getUserList, 33 | method: 'get', 34 | params, 35 | }) 36 | } 37 | 38 | export function addUser(user: UserProps): Promise> { 39 | return request({ 40 | url: UserUrls.addUser, 41 | method: 'post', 42 | data: user, 43 | }) 44 | } 45 | 46 | export function updateUser( 47 | id: string, 48 | user: UserProps, 49 | ): Promise> { 50 | return request({ 51 | url: `${UserUrls.updateUser}/${id}`, 52 | method: 'put', 53 | data: user, 54 | }) 55 | } 56 | 57 | export function removeUser(id: string): Promise> { 58 | return request({ 59 | url: `${UserUrls.removeUser}/${id}`, 60 | method: 'delete', 61 | }) 62 | } 63 | 64 | export function resetPwd( 65 | id: string, 66 | password: string, 67 | type: UserType, 68 | ): Promise> { 69 | return request({ 70 | url: UserUrls.resetPwdUrl, 71 | method: 'patch', 72 | data: { 73 | id, 74 | password, 75 | type, 76 | }, 77 | }) 78 | } 79 | 80 | export function changeUserStatus( 81 | id: string, 82 | status: UserStatus, 83 | type: UserType, 84 | ): Promise> { 85 | return request({ 86 | url: UserUrls.changeStatusUrl, 87 | method: 'patch', 88 | data: { 89 | id, 90 | status, 91 | type, 92 | }, 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/assets/images/avatar.jpg -------------------------------------------------------------------------------- /src/assets/images/bg-xw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/assets/images/bg-xw.jpg -------------------------------------------------------------------------------- /src/assets/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/assets/images/bg.jpg -------------------------------------------------------------------------------- /src/assets/images/githubCorner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/assets/images/githubCorner.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/images/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .breadcrumb-container { 3 | display: inline-block; 4 | vertical-align: 1px; 5 | margin: 0 8px; 6 | .ant-breadcrumb, 7 | .ant-breadcrumb-link { 8 | > span, 9 | a { 10 | color: @text-color-1 !important; 11 | } 12 | > span:hover, 13 | a:hover { 14 | color: @primary !important; 15 | } 16 | } 17 | &.is-visible { 18 | display: none !important; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import {Breadcrumb, Grid} from 'antd' 3 | import {useLocation} from 'react-router-dom' 4 | import MenuList, {MenuListProps} from '@/config/menuConfig' 5 | import {TransitionGroup, CSSTransition} from 'react-transition-group' 6 | import classnames from 'classnames' 7 | const {useBreakpoint} = Grid 8 | import './index.less' 9 | 10 | interface IBreadcrumbProps {} 11 | 12 | export const matchRoutes = (routes: MenuListProps[], pathname: string) => { 13 | const temppath: MenuListProps[] = [] 14 | try { 15 | // eslint-disable-next-line 16 | function getItemPath(item: MenuListProps) { 17 | const isMatch = pathname.indexOf(item.path) !== -1 18 | if (isMatch) { 19 | temppath.push(item) 20 | } 21 | if (item.path === pathname) { 22 | throw new Error('GOT IT!') 23 | } 24 | if (isMatch && item.children && item.children.length > 0) { 25 | for (let i = 0; i < item.children.length; i++) { 26 | getItemPath(item.children[i]) 27 | } 28 | } 29 | } 30 | for (let i = 0; i < routes.length; i++) { 31 | getItemPath(routes[i]) 32 | } 33 | } catch (error) { 34 | return temppath 35 | } 36 | } 37 | 38 | const BreadcrumbContainer: React.FC = () => { 39 | const location = useLocation() 40 | const screens = useBreakpoint() 41 | 42 | const classes = classnames('breadcrumb-container', { 43 | 'is-visible': !screens.lg, 44 | }) 45 | 46 | let paths = matchRoutes(MenuList, location.pathname) 47 | 48 | const first = paths && paths[0] 49 | 50 | if (first && first.title.trim() !== '首页') { 51 | if (paths) { 52 | paths = [{path: '/dashboard', title: '首页'}].concat(paths) 53 | } 54 | } 55 | 56 | return ( 57 |
58 | 59 | 60 | {paths && 61 | paths.map(item => { 62 | const isHome = item.title === '首页' 63 | const type = isHome ? 'a' : 'span' 64 | const props = isHome ? {href: item.path} : null 65 | return ( 66 | 72 | 73 | {React.createElement(type, props, item.title)} 74 | 75 | 76 | ) 77 | })} 78 | 79 | 80 |
81 | ) 82 | } 83 | 84 | export default memo(BreadcrumbContainer) 85 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/core.tsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import './index.less' 3 | 4 | interface ErrorBoundaryProps { 5 | /** 监听错误的回调函数 */ 6 | onError?: (error: Error, errorInfo: any) => void 7 | 8 | /** 降级备用渲染 React Element */ 9 | fallback?: React.ReactElement 10 | 11 | /** 降级备用渲染 React 组件 */ 12 | FallbackComponent?: any 13 | 14 | fallbackRender: (args: any) => any 15 | 16 | /** 重置错误组件的状态函数 */ 17 | onReset?: () => void 18 | } 19 | 20 | interface ErrorBoundaryState { 21 | /** 是否有错误 */ 22 | error: boolean 23 | } 24 | 25 | const initialState = {error: false} 26 | 27 | /** 错误边界组件 */ 28 | export default class ErrorBoundary extends Component< 29 | ErrorBoundaryProps, 30 | ErrorBoundaryState 31 | > { 32 | state = initialState 33 | 34 | static getDerivedStateFromError(error: Error) { 35 | return {error} 36 | } 37 | 38 | componentDidCatch(error: Error, errorInfo: any) { 39 | if (this.props.onError) { 40 | this.props.onError(error, errorInfo) 41 | } 42 | } 43 | 44 | // 执行自定义重置逻辑,并重置组件状态 45 | resetErrorBoundary = () => { 46 | if (this.props.onReset) this.props.onReset() 47 | this.setState(initialState) 48 | } 49 | 50 | render() { 51 | const {fallback, FallbackComponent, fallbackRender} = this.props 52 | const {error} = this.state 53 | 54 | if (error) { 55 | const fallbackProps = { 56 | error, 57 | resetErrorBoundary: this.resetErrorBoundary, 58 | } 59 | 60 | if (React.isValidElement(fallback)) return fallback 61 | 62 | if (typeof fallbackRender === 'function') 63 | return fallbackRender(fallbackProps) 64 | 65 | if (FallbackComponent) return 66 | 67 | throw new Error('ErrorBoundary 组件需要传入 fallback') 68 | } 69 | 70 | return this.props.children 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.less: -------------------------------------------------------------------------------- 1 | .error-boundary-container { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | padding-bottom: 10%; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import DefaultErrorBoundary from './core' 3 | const catchreacterror = (Boundary: any = DefaultErrorBoundary) => ( 4 | InnerComponent: any, 5 | // eslint-disable-next-line 6 | ) => (props: any) => { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default catchreacterror 15 | -------------------------------------------------------------------------------- /src/components/FullScreen/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .fullscreen-container { 3 | display: inline-block; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/FullScreen/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState, RefObject, memo} from 'react' 2 | import screenfull, {Screenfull} from 'screenfull' 3 | import {message, Tooltip} from 'antd' 4 | import classnames from 'classnames' 5 | import {isElement} from 'utils/utils' 6 | import Icon from '../Icon' 7 | import variables from 'styles/variables.module.less' 8 | import './index.less' 9 | 10 | interface IFullScreenProps { 11 | /** 外层容器内联样式 */ 12 | wrapperStyle?: React.CSSProperties 13 | /** 外层容器自定义 Css class */ 14 | wrapperClassName?: string 15 | /** 图标内联样式 */ 16 | style?: React.CSSProperties 17 | /** 图标自定义 Css class */ 18 | className?: string 19 | /** 图标字体大小 */ 20 | size?: number 21 | /** 图标字体颜色 */ 22 | color?: string 23 | /** 全屏元素 */ 24 | target?: Element | RefObject 25 | } 26 | 27 | const ScreenFull: Screenfull = screenfull as Screenfull 28 | 29 | const FullScreen: React.FC = props => { 30 | const { 31 | className, 32 | style, 33 | wrapperClassName, 34 | wrapperStyle, 35 | size, 36 | color, 37 | target, 38 | } = props 39 | 40 | const [isfullScreen, setIsFullScreen] = useState(false) 41 | 42 | const wrapClasses = classnames('fullscreen-container', wrapperClassName) 43 | 44 | const classes = classnames(className) 45 | 46 | const iconStyle: React.CSSProperties = { 47 | fontSize: `${size}px`, 48 | color: color, 49 | fill: color, 50 | ...style, 51 | } 52 | 53 | const toggleFullScreen = () => { 54 | if (!ScreenFull.isEnabled) { 55 | message.warning('Failed to enable fullscreen') 56 | return false 57 | } 58 | if (isElement(target)) { 59 | ScreenFull.toggle(target) 60 | } else { 61 | if (target) { 62 | ScreenFull.toggle(target.current as Element) 63 | } 64 | } 65 | } 66 | 67 | const onChange = () => { 68 | setIsFullScreen(ScreenFull.isFullscreen) 69 | } 70 | 71 | useEffect(() => { 72 | ScreenFull.isEnabled && ScreenFull.on('change', onChange) 73 | return () => { 74 | ScreenFull.off('change', onChange) 75 | } 76 | }) 77 | 78 | const title = isfullScreen ? '取消全屏' : '全屏' 79 | 80 | return ( 81 |
e.stopPropagation()} 85 | > 86 | 87 | {isfullScreen ? ( 88 | 94 | ) : ( 95 | 101 | )} 102 | 103 |
104 | ) 105 | } 106 | 107 | FullScreen.defaultProps = { 108 | size: 16, 109 | color: variables['primary-text'], 110 | target: document.documentElement, 111 | } 112 | 113 | export default memo(FullScreen) 114 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .hamburger-container { 3 | display: inline-block; 4 | padding: 0 10px; 5 | cursor: pointer; 6 | transition: background 0.3s ease; 7 | &:hover { 8 | background-color: @gray-1; 9 | } 10 | .anticon { 11 | font-size: 16px !important; 12 | color: @black!important; 13 | fill: @black !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Hamburger/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo, useState} from 'react' 2 | import Icon from '../Icon' 3 | import './index.less' 4 | 5 | interface IHamburgerProps { 6 | /** 折叠状态 */ 7 | collapse?: boolean 8 | /** 点击触发的回调 */ 9 | onClick?: () => void 10 | } 11 | 12 | const Hamburger: React.FC = props => { 13 | const {onClick, collapse} = props 14 | const [innerCollapse, setInnerCollapse] = useState(collapse) 15 | 16 | const icon = innerCollapse ? 'MenuUnfoldOutlined' : 'MenuFoldOutlined' 17 | 18 | const handleClick = () => { 19 | setInnerCollapse(c => !c) 20 | onClick && onClick() 21 | } 22 | 23 | return ( 24 |
25 | {} 26 |
27 | ) 28 | } 29 | 30 | Hamburger.defaultProps = { 31 | collapse: false, 32 | } 33 | 34 | export default memo(Hamburger) 35 | -------------------------------------------------------------------------------- /src/components/Icon/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties} from 'react' 2 | import AntIcons, {IconName} from '../data/AntIcons' 3 | 4 | /** Icon组件属性类型定义 */ 5 | interface IIconProps extends React.HTMLProps { 6 | /** 设置图标的样式,例如 fontSize 和 color */ 7 | style?: CSSProperties 8 | /** 设置图标的样式名 */ 9 | className?: string 10 | /** 图标名称 */ 11 | icon: IconName 12 | /** 图标颜色 */ 13 | color?: string 14 | /** 图标大小 */ 15 | size?: number 16 | /** 图标是否有旋转动画 */ 17 | spin?: boolean 18 | /** 图标旋转角度 */ 19 | rotate?: number 20 | /** 设置双色图标的颜色 */ 21 | twoToneColor?: string 22 | } 23 | 24 | const Icon: React.FC = props => { 25 | const {icon, color, size, style, ...restProps} = props 26 | 27 | const iconStyle: CSSProperties = { 28 | color: color, 29 | fontSize: size + 'px', 30 | fill: color, 31 | ...style, 32 | } 33 | 34 | const component = AntIcons[icon] 35 | 36 | if (!component) return null 37 | 38 | return component.type.render({ 39 | style: iconStyle, 40 | ...restProps, 41 | }) 42 | } 43 | 44 | Icon.defaultProps = { 45 | size: 14, 46 | spin: false, 47 | } 48 | 49 | export default Icon 50 | -------------------------------------------------------------------------------- /src/components/Icon/IconPicker/index.less: -------------------------------------------------------------------------------- 1 | .icon-picker-modal { 2 | .ant-modal-body { 3 | padding: 22px !important; 4 | } 5 | .icon-list { 6 | display: flex; 7 | flex-wrap: wrap; 8 | justify-content: space-between; 9 | width: 100%; 10 | padding: 0; 11 | margin: 0; 12 | margin-top: 10px; 13 | list-style: none; 14 | .icon-item { 15 | display: flex; 16 | padding: 8px; 17 | justify-content: center; 18 | align-items: center; 19 | flex-direction: column; 20 | border: 1px solid #ccc; 21 | border-radius: 2px; 22 | width: 32%; 23 | margin-bottom: 8px; 24 | cursor: pointer; 25 | .icon-name { 26 | width: 100%; 27 | font-size: 14px; 28 | margin: 5px 0; 29 | white-space: nowrap; 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | text-align: center; 33 | } 34 | &:hover, 35 | &.is-active { 36 | border-color: #0960bd; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Icon/data/AntIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | DashboardOutlined, 4 | FileOutlined, 5 | SearchOutlined, 6 | LockOutlined, 7 | ClusterOutlined, 8 | UserOutlined, 9 | HomeOutlined, 10 | SettingOutlined, 11 | UpCircleTwoTone, 12 | CaretDownOutlined, 13 | QuestionCircleOutlined, 14 | GlobalOutlined, 15 | CopyOutlined, 16 | NotificationOutlined, 17 | MoreOutlined, 18 | RightOutlined, 19 | DownOutlined, 20 | RedoOutlined, 21 | ColumnHeightOutlined, 22 | FullscreenExitOutlined, 23 | FullscreenOutlined, 24 | CloseOutlined, 25 | DeleteOutlined, 26 | FormOutlined, 27 | MenuFoldOutlined, 28 | MenuUnfoldOutlined, 29 | MessageOutlined, 30 | PayCircleOutlined, 31 | ShoppingCartOutlined, 32 | } from '@ant-design/icons' 33 | 34 | export type IconName = 35 | | 'HomeOutlined' 36 | | 'DashboardOutlined' 37 | | 'FileOutlined' 38 | | 'SearchOutlined' 39 | | 'LockOutlined' 40 | | 'ClusterOutlined' 41 | | 'ClusterOutlined' 42 | | 'UserOutlined' 43 | | 'SettingOutlined' 44 | | 'UpCircleTwoTone' 45 | | 'CaretDownOutlined' 46 | | 'QuestionCircleOutlined' 47 | | 'GlobalOutlined' 48 | | 'CopyOutlined' 49 | | 'NotificationOutlined' 50 | | 'MoreOutlined' 51 | | 'RightOutlined' 52 | | 'DownOutlined' 53 | | 'RedoOutlined' 54 | | 'ColumnHeightOutlined' 55 | | 'FullscreenExitOutlined' 56 | | 'FullscreenOutlined' 57 | | 'CloseOutlined' 58 | | 'DeleteOutlined' 59 | | 'FormOutlined' 60 | | 'MenuFoldOutlined' 61 | | 'MenuUnfoldOutlined' 62 | | 'MessageOutlined' 63 | | 'PayCircleOutlined' 64 | | 'ShoppingCartOutlined' 65 | 66 | type AntdIconsProps = { 67 | [key in IconName]: any 68 | } 69 | 70 | const AntdIcons: AntdIconsProps = { 71 | HomeOutlined: , 72 | DashboardOutlined: , 73 | FileOutlined: , 74 | SearchOutlined: , 75 | LockOutlined: , 76 | ClusterOutlined: , 77 | UserOutlined: , 78 | SettingOutlined: , 79 | UpCircleTwoTone: , 80 | CaretDownOutlined: , 81 | QuestionCircleOutlined: , 82 | GlobalOutlined: , 83 | CopyOutlined: , 84 | NotificationOutlined: , 85 | MoreOutlined: , 86 | RightOutlined: , 87 | DownOutlined: , 88 | RedoOutlined: , 89 | ColumnHeightOutlined: , 90 | FullscreenExitOutlined: , 91 | FullscreenOutlined: , 92 | CloseOutlined: , 93 | DeleteOutlined: , 94 | FormOutlined: , 95 | MenuFoldOutlined: , 96 | MenuUnfoldOutlined: , 97 | MessageOutlined: , 98 | PayCircleOutlined: , 99 | ShoppingCartOutlined: , 100 | } 101 | 102 | export default AntdIcons 103 | -------------------------------------------------------------------------------- /src/components/Icon/data/IconPicker.ts: -------------------------------------------------------------------------------- 1 | /** 图标库组件名称数组 */ 2 | export type IconPickerName = 3 | | 'HomeOutlined' 4 | | 'DashboardOutlined' 5 | | 'FileOutlined' 6 | | 'SearchOutlined' 7 | | 'LockOutlined' 8 | | 'ClusterOutlined' 9 | | 'ClusterOutlined' 10 | | 'UserOutlined' 11 | | 'SettingOutlined' 12 | | 'UpCircleTwoTone' 13 | | 'CaretDownOutlined' 14 | | 'QuestionCircleOutlined' 15 | | 'GlobalOutlined' 16 | | 'CopyOutlined' 17 | | 'NotificationOutlined' 18 | | 'MoreOutlined' 19 | | 'RightOutlined' 20 | | 'DownOutlined' 21 | | 'RedoOutlined' 22 | | 'ColumnHeightOutlined' 23 | | 'FullscreenExitOutlined' 24 | | 'FullscreenOutlined' 25 | | 'CloseOutlined' 26 | | 'DeleteOutlined' 27 | | 'FormOutlined' 28 | | 'MessageOutlined' 29 | | 'PayCircleOutlined' 30 | | 'ShoppingCartOutlined' 31 | 32 | export default [ 33 | 'HomeOutlined', 34 | 'DashboardOutlined', 35 | 'FileOutlined', 36 | 'SearchOutlined', 37 | 'LockOutlined', 38 | 'ClusterOutlined', 39 | 'UserOutlined', 40 | 'SettingOutlined', 41 | 'UpCircleTwoTone', 42 | 'CaretDownOutlined', 43 | 'QuestionCircleOutlined', 44 | 'GlobalOutlined', 45 | 'CopyOutlined', 46 | 'NotificationOutlined', 47 | 'MoreOutlined', 48 | 'RightOutlined', 49 | 'DownOutlined', 50 | 'RedoOutlined', 51 | 'ColumnHeightOutlined', 52 | 'FullscreenExitOutlined', 53 | 'FullscreenOutlined', 54 | 'CloseOutlined', 55 | 'DeleteOutlined', 56 | 'FormOutlined', 57 | 'MessageOutlined', 58 | 'PayCircleOutlined', 59 | 'ShoppingCartOutlined', 60 | ] as IconPickerName[] 61 | -------------------------------------------------------------------------------- /src/components/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import Icon from './Icon' 2 | import IconPicker from './IconPicker' 3 | 4 | export {Icon, IconPicker} 5 | 6 | export default Icon 7 | -------------------------------------------------------------------------------- /src/components/IntlDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, memo, CSSProperties} from 'react' 2 | import {Dropdown, Menu} from 'antd' 3 | import Icon from 'comps/Icon' 4 | import {toggleLang} from 'store/actions' 5 | import {useAppDispatch, useAppSelector} from 'store/index' 6 | import {MenuClickEventHandler} from 'rc-menu/lib/interface' 7 | import {Language} from 'store/reducers/app' 8 | import variables from 'styles/variables.module.less' 9 | import {asyncIntlLoadingToggle, intlLoadingToggle} from 'store/actions' 10 | 11 | interface IIntlDropdownProps {} 12 | 13 | const IntlDropdown: React.FC = () => { 14 | const {lang} = useAppSelector(state => state.app) 15 | const [selectedKeys, setSelectedKeys] = useState( 16 | lang ? [lang] : [], 17 | ) 18 | 19 | const style: CSSProperties = { 20 | display: 'inline-block', 21 | } 22 | 23 | const dispatch = useAppDispatch() 24 | 25 | const handleToggleLang: MenuClickEventHandler = info => { 26 | const {key} = info 27 | if (key === 'zh') { 28 | dispatch(toggleLang(key)) 29 | setSelectedKeys([key]) 30 | } else if (key === 'en') { 31 | dispatch(toggleLang(key)) 32 | setSelectedKeys([key]) 33 | } else if (key === 'id') { 34 | dispatch(toggleLang(key)) 35 | setSelectedKeys([key]) 36 | } 37 | console.log('切换语言动画开始') 38 | dispatch(asyncIntlLoadingToggle() as any).then(() => { 39 | dispatch(intlLoadingToggle()) 40 | console.log('切换语言动画完成') 41 | }) 42 | } 43 | 44 | const langMenu = ( 45 | 46 | 47 | 简体中文 48 | 49 | 50 | English 51 | 52 | 53 | 印尼文 54 | 55 | 56 | ) 57 | 58 | return ( 59 | 60 |
61 | 66 |
67 |
68 | ) 69 | } 70 | 71 | export default memo(IntlDropdown) 72 | -------------------------------------------------------------------------------- /src/components/Loading/AppLoading/index.less: -------------------------------------------------------------------------------- 1 | .app-loading { 2 | &-container { 3 | background-color: #fff; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | position: fixed; 9 | left: 0; 10 | right: 0; 11 | bottom: 0; 12 | top: 0; 13 | margin: auto; 14 | z-index: 100000; 15 | } 16 | &-logo { 17 | width: 110px; 18 | } 19 | &-spin-icon { 20 | margin: 35px 0; 21 | .ant-spin .ant-spin-dot { 22 | font-size: 50px !important; 23 | } 24 | .ant-spin .ant-spin-dot i { 25 | width: 20px; 26 | height: 20px; 27 | } 28 | } 29 | &-title { 30 | font-size: 30px; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/Loading/AppLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import {Spin} from 'antd' 3 | import viteSvg from 'assets/images/vite.svg' 4 | import './index.less' 5 | 6 | interface IAppLoadingProps {} 7 | 8 | const AppLoading: React.FC = () => { 9 | return ( 10 |
11 | app-loading 12 |
13 | 14 |
15 |

橙晨燕

16 |
17 | ) 18 | } 19 | 20 | export default memo(AppLoading) 21 | -------------------------------------------------------------------------------- /src/components/Loading/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, CSSProperties, memo} from 'react' 2 | import {Spin} from 'antd' 3 | import NProgress from 'nprogress' 4 | import 'nprogress/nprogress.css' 5 | NProgress.configure({showSpinner: false}) 6 | 7 | interface IPageLoadingProps {} 8 | 9 | const PageLoading: React.FC = () => { 10 | useEffect(() => { 11 | NProgress.start() 12 | return () => { 13 | NProgress.done() 14 | } 15 | }, []) 16 | 17 | const style: CSSProperties = { 18 | display: 'flex', 19 | alignItems: 'center', 20 | justifyContent: 'center', 21 | flexDirection: 'column', 22 | minHeight: '300px', 23 | } 24 | 25 | return ( 26 |
27 | 28 |
29 | ) 30 | } 31 | 32 | export default memo(PageLoading) 33 | -------------------------------------------------------------------------------- /src/components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import PageLoading from './PageLoading' 2 | import AppLoading from './AppLoading' 3 | 4 | export {AppLoading, PageLoading} 5 | 6 | export default PageLoading 7 | -------------------------------------------------------------------------------- /src/components/Mallki/index.less: -------------------------------------------------------------------------------- 1 | .mallki { 2 | font-weight: 800; 3 | color: #4dd9d5; 4 | font-family: 'Dosis', sans-serif; 5 | -webkit-transition: color 0.5s 0.25s; 6 | transition: color 0.5s 0.25s; 7 | overflow: hidden; 8 | position: relative; 9 | display: inline-block; 10 | line-height: 1; 11 | outline: none; 12 | text-decoration: none; 13 | } 14 | 15 | .mallki:hover { 16 | -webkit-transition: none; 17 | transition: none; 18 | color: transparent; 19 | } 20 | 21 | .mallki::before { 22 | content: ''; 23 | width: 100%; 24 | height: 6px; 25 | margin: -3px 0 0 0; 26 | background: #3888fa; 27 | position: absolute; 28 | left: 0; 29 | top: 50%; 30 | -webkit-transform: translate3d(-100%, 0, 0); 31 | transform: translate3d(-100%, 0, 0); 32 | -webkit-transition: -webkit-transform 0.4s; 33 | transition: transform 0.4s; 34 | -webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); 35 | transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1); 36 | } 37 | 38 | .mallki:hover::before { 39 | -webkit-transform: translate3d(100%, 0, 0); 40 | transform: translate3d(100%, 0, 0); 41 | } 42 | 43 | .mallki span { 44 | position: absolute; 45 | height: 50%; 46 | width: 100%; 47 | left: 0; 48 | top: 0; 49 | overflow: hidden; 50 | } 51 | 52 | .mallki span::before { 53 | content: attr(data-letters); 54 | color: red; 55 | position: absolute; 56 | left: 0; 57 | width: 100%; 58 | color: #3888fa; 59 | -webkit-transition: -webkit-transform 0.5s; 60 | transition: transform 0.5s; 61 | } 62 | 63 | .mallki span:nth-child(2) { 64 | top: 50%; 65 | } 66 | 67 | .mallki span:first-child::before { 68 | top: 0; 69 | -webkit-transform: translate3d(0, 100%, 0); 70 | transform: translate3d(0, 100%, 0); 71 | } 72 | 73 | .mallki span:nth-child(2)::before { 74 | bottom: 0; 75 | -webkit-transform: translate3d(0, -100%, 0); 76 | transform: translate3d(0, -100%, 0); 77 | } 78 | 79 | .mallki:hover span::before { 80 | -webkit-transition-delay: 0.3s; 81 | transition-delay: 0.3s; 82 | -webkit-transform: translate3d(0, 0, 0); 83 | transform: translate3d(0, 0, 0); 84 | -webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1); 85 | transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1); 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Mallki/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import './index.less' 3 | 4 | interface IMallkiProps { 5 | className?: string 6 | text?: string 7 | } 8 | 9 | const Mallki: React.FC = props => { 10 | const {className, text} = props 11 | return ( 12 | 13 | {text} 14 | 15 | 16 | 17 | ) 18 | } 19 | Mallki.defaultProps = { 20 | className: '', 21 | text: 'React-Antd-Admin', 22 | } 23 | 24 | export default memo(Mallki) 25 | -------------------------------------------------------------------------------- /src/components/PageBox/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/components/PageBox/index.less -------------------------------------------------------------------------------- /src/components/PageBox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import './index.less' 3 | 4 | interface IPageBoxProps { 5 | style?: React.CSSProperties 6 | className?: string 7 | } 8 | 9 | const PageBox: React.FC = () => { 10 | return
11 | } 12 | 13 | export default memo(PageBox) 14 | -------------------------------------------------------------------------------- /src/components/PanThumb/index.less: -------------------------------------------------------------------------------- 1 | .pan-item { 2 | width: 200px; 3 | height: 200px; 4 | border-radius: 50%; 5 | display: inline-block; 6 | position: relative; 7 | cursor: default; 8 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); 9 | } 10 | 11 | .pan-info-roles-container { 12 | padding: 20px; 13 | text-align: center; 14 | } 15 | 16 | .pan-thumb { 17 | width: 100%; 18 | height: 100%; 19 | background-size: 100%; 20 | border-radius: 50%; 21 | overflow: hidden; 22 | position: absolute; 23 | transform-origin: 95% 40%; 24 | transition: all 0.3s ease-in-out; 25 | } 26 | 27 | .pan-thumb:after { 28 | content: ''; 29 | width: 8px; 30 | height: 8px; 31 | position: absolute; 32 | border-radius: 50%; 33 | top: 40%; 34 | left: 95%; 35 | margin: -4px 0 0 -4px; 36 | background: radial-gradient( 37 | ellipse at center, 38 | rgba(14, 14, 14, 1) 0%, 39 | rgba(125, 126, 125, 1) 100% 40 | ); 41 | box-shadow: 0 0 1px rgba(255, 255, 255, 0.9); 42 | } 43 | 44 | .pan-info { 45 | position: absolute; 46 | width: inherit; 47 | height: inherit; 48 | border-radius: 50%; 49 | overflow: hidden; 50 | box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05); 51 | } 52 | 53 | .pan-info h3 { 54 | color: #fff; 55 | text-transform: uppercase; 56 | position: relative; 57 | letter-spacing: 2px; 58 | font-size: 18px; 59 | margin: 0 60px; 60 | padding: 22px 0 0 0; 61 | height: 85px; 62 | font-family: 'Open Sans', Arial, sans-serif; 63 | text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3); 64 | } 65 | 66 | .pan-info p { 67 | color: #fff; 68 | padding: 10px 5px; 69 | font-style: italic; 70 | margin: 0 30px; 71 | font-size: 12px; 72 | border-top: 1px solid rgba(255, 255, 255, 0.5); 73 | } 74 | 75 | .pan-info p a { 76 | display: block; 77 | color: #333; 78 | width: 80px; 79 | height: 80px; 80 | background: rgba(255, 255, 255, 0.3); 81 | border-radius: 50%; 82 | color: #fff; 83 | font-style: normal; 84 | font-weight: 700; 85 | text-transform: uppercase; 86 | font-size: 9px; 87 | letter-spacing: 1px; 88 | padding-top: 24px; 89 | margin: 7px auto 0; 90 | font-family: 'Open Sans', Arial, sans-serif; 91 | opacity: 0; 92 | transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, 93 | background 0.2s linear 0s; 94 | transform: translateX(60px) rotate(90deg); 95 | } 96 | 97 | .pan-info p a:hover { 98 | background: rgba(255, 255, 255, 0.5); 99 | } 100 | 101 | .pan-item:hover .pan-thumb { 102 | transform: rotate(-110deg); 103 | } 104 | 105 | .pan-item:hover .pan-info p a { 106 | opacity: 1; 107 | transform: translateX(0px) rotate(0deg); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/PanThumb/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import './index.less' 3 | 4 | interface IPanThumbProps { 5 | image: string 6 | zIndex?: number 7 | width?: string 8 | height?: string 9 | className?: string 10 | } 11 | 12 | const PanThumb: React.FC = props => { 13 | const {image, zIndex, width, height, className} = props 14 | return ( 15 |
23 |
24 |
{props.children}
25 |
26 | 27 |
28 | ) 29 | } 30 | 31 | PanThumb.defaultProps = { 32 | width: '150px', 33 | height: '150px', 34 | zIndex: 1, 35 | className: '', 36 | } 37 | 38 | export default memo(PanThumb) 39 | -------------------------------------------------------------------------------- /src/components/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import Icon from 'comps/Icon' 3 | import {Tooltip} from 'antd' 4 | import {showRightPanelToggle} from 'store/actions' 5 | import {useAppDispatch} from 'root/src/store' 6 | import variables from 'styles/variables.module.less' 7 | 8 | interface ISettingProps {} 9 | 10 | const Setting: React.FC = () => { 11 | const dispatch = useAppDispatch() 12 | const action = () => { 13 | dispatch(showRightPanelToggle()) 14 | } 15 | 16 | return ( 17 | 18 |
19 | 25 |
26 |
27 | ) 28 | } 29 | 30 | export default memo(Setting) 31 | -------------------------------------------------------------------------------- /src/components/TreeTable/index.less: -------------------------------------------------------------------------------- 1 | .rowBgColor { 2 | background-color: #fafafa; 3 | } 4 | 5 | .rowHover { 6 | &:hover { 7 | > td { 8 | background-color: #e3f4fc !important; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/TreeTable/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, memo} from 'react' 2 | import {Table, TableProps} from 'antd' 3 | import { 4 | RenderExpandIcon, 5 | ExpandableConfig, 6 | RowClassName, 7 | } from 'rc-table/lib/interface' 8 | import {RightOutlined, DownOutlined} from '@ant-design/icons' 9 | import classnames from 'classnames' 10 | 11 | type ITreeTableProps = TableProps 12 | 13 | const TreeTable: React.FC = props => { 14 | const {dataSource, columns} = props 15 | 16 | const [expandedRowKeys, setExpandedRowKeys] = useState([]) 17 | 18 | useEffect(() => { 19 | const keyArr: any[] = [] 20 | if (dataSource) { 21 | dataSource.map(item => { 22 | //这里就可以把要展开的key加进来记住必须是唯一的 23 | keyArr.push(item.key) 24 | }) 25 | } 26 | setExpandedRowKeys(keyArr) 27 | }, []) 28 | 29 | const onExpand = (expanded: any, record: any) => { 30 | //expanded是否展开 record每一项的值 31 | const keys = expandedRowKeys 32 | if (expanded) { 33 | const arr = keys 34 | arr.push(record.key) 35 | setExpandedRowKeys(arr) 36 | } else { 37 | let arr2 = [] 38 | if (keys.length > 0 && record.key) { 39 | arr2 = keys.filter(key => { 40 | return key !== record.key 41 | }) 42 | } 43 | setExpandedRowKeys(arr2) 44 | } 45 | } 46 | 47 | const expandedIcon: RenderExpandIcon = ({ 48 | expanded, 49 | onExpand, 50 | record, 51 | }) => { 52 | //expanded-是否可展开, onExpand-展开事件默认, record-每一项的值 设置自定义图标 53 | if (record.children && record.children.length != 0) { 54 | if (expanded) { 55 | //根据是否可以展开判断 56 | return ( 57 | onExpand(record, e)} /> 58 | ) 59 | } else { 60 | return ( 61 | onExpand(record, e)} /> 62 | ) 63 | } 64 | } else { 65 | return '' 66 | } 67 | } 68 | 69 | const expandable: ExpandableConfig = { 70 | expandIcon: expandedIcon, 71 | expandedRowKeys: expandedRowKeys, 72 | onExpand: onExpand, 73 | } 74 | 75 | console.log(classnames('1')) 76 | 77 | const classNameFn: RowClassName = (record, index) => { 78 | let className = 'rowHover' 79 | if (record && record.leve === 1) { 80 | className = classnames(className, { 81 | rowBgColor: index % 2 !== 0, 82 | }) 83 | } 84 | return className 85 | } 86 | 87 | return ( 88 |
89 | 95 | 96 | ) 97 | } 98 | 99 | export default memo(TreeTable) 100 | -------------------------------------------------------------------------------- /src/components/TypingCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef, useEffect, memo} from 'react' 2 | import {Card} from 'antd' 3 | import Typing from 'utils/typing' 4 | 5 | interface ITypingCardProps { 6 | title: string 7 | source: string 8 | } 9 | 10 | const TypingCard: React.FC = props => { 11 | const {title, source} = props 12 | 13 | const sourceEl = useRef(null) 14 | const outputEl = useRef(null) 15 | 16 | useEffect(() => { 17 | const typing = new Typing({ 18 | source: sourceEl.current, 19 | output: outputEl.current, 20 | delay: 30, 21 | }) 22 | typing.start() 23 | }, []) 24 | return ( 25 | 26 |
31 |
32 | 33 | ) 34 | } 35 | 36 | TypingCard.defaultProps = { 37 | title: '', 38 | source: '', 39 | } 40 | 41 | export default memo(TypingCard) 42 | -------------------------------------------------------------------------------- /src/components/UserAvatarDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties, memo} from 'react' 2 | import {Link} from 'react-router-dom' 3 | import {Dropdown, Avatar, Modal, Menu, Typography} from 'antd' 4 | import Icon from 'comps/Icon' 5 | import store, {useAppSelector} from 'store/index' 6 | import {logout} from 'store/actions' 7 | import avatarImg from '@/assets/images/avatar.jpg' 8 | const {confirm} = Modal 9 | const {Text} = Typography 10 | 11 | interface IUserAvatarDorpdownProps {} 12 | 13 | function handleLogout() { 14 | confirm({ 15 | title: '注销', 16 | icon: , 17 | content: '确定要退出系统吗?', 18 | cancelText: '取消', 19 | okText: '确认', 20 | visible: false, 21 | onOk: () => { 22 | store.dispatch(logout()) 23 | }, 24 | }) 25 | } 26 | 27 | const menu = ( 28 | 29 | 30 | 首页 31 | 32 | 33 | 34 | 项目地址 35 | 36 | 37 | 38 | 注销 39 | 40 | ) 41 | 42 | const UserAvatarDorpdown: React.FC = () => { 43 | const username = useAppSelector(state => state.user.username) 44 | 45 | const textStyle: CSSProperties = { 46 | fontSize: '14px', 47 | } 48 | 49 | return ( 50 | 51 |
52 | 53 | 54 | {username} 55 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export default memo(UserAvatarDorpdown) 62 | -------------------------------------------------------------------------------- /src/components/VBasicDrawerForm/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/mixins.less'; 2 | .vbasic-drawer-form { 3 | .ant-drawer-body { 4 | .scrollbar(); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/VBasicFormV2/components/FromItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react' 2 | import {Form, Input, Select, InputNumber, Checkbox, Radio} from 'antd' 3 | import {FormContext} from '../../index' 4 | import {Rule} from 'rc-field-form/lib/interface' 5 | 6 | /** 表单项类型 */ 7 | export type FormItemType = 8 | | 'input' 9 | | 'password' 10 | | 'select' 11 | | 'textarea' 12 | | 'upload' 13 | | 'number' 14 | | 'checkbox' 15 | | 'radio' 16 | | 'checkbox-group' 17 | | 'radio-group' 18 | 19 | export interface IFormItemProps { 20 | /** 表单项类型 */ 21 | type: FormItemType 22 | /** label标签文本 */ 23 | label: string 24 | /** 表单提交项的名称 */ 25 | name: string 26 | /** 表单项的值 */ 27 | value: any 28 | /** 表单项额外参数 */ 29 | payload?: any 30 | /** 表单项验证规则 */ 31 | rules?: Rule[] 32 | } 33 | 34 | const FormItem: React.FC = props => { 35 | const {type, label, name, payload, rules} = props 36 | 37 | /** 获取表单context提供的表单实例 */ 38 | const {form} = useContext(FormContext) 39 | 40 | let valuePropName = undefined 41 | 42 | /** 表单项内容 */ 43 | let formItemContent: React.ReactNode = null 44 | /** */ 45 | const placeholder = `请${type === 'select' ? '选择' : '输入'}${label}` 46 | switch (type) { 47 | case 'textarea': 48 | formItemContent = 49 | break 50 | case 'password': 51 | formItemContent = 52 | break 53 | case 'number': 54 | formItemContent = 55 | break 56 | case 'checkbox': 57 | valuePropName = 'checked' 58 | formItemContent = {payload} 59 | break 60 | case 'checkbox-group': 61 | formItemContent = 62 | break 63 | case 'radio': 64 | valuePropName = 'checked' 65 | formItemContent = {payload} 66 | break 67 | case 'radio-group': 68 | formItemContent = 69 | break 70 | case 'select': 71 | formItemContent = payload ? ( 72 | 73 | ) : null 74 | break 75 | default: 76 | formItemContent = 77 | break 78 | } 79 | 80 | return ( 81 | 87 | {formItemContent} 88 | 89 | ) 90 | } 91 | 92 | export default FormItem 93 | -------------------------------------------------------------------------------- /src/components/VBasicFormV2/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/components/VBasicFormV2/index.less -------------------------------------------------------------------------------- /src/components/VBasicFormV2/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | memo, 4 | useMemo, 5 | useImperativeHandle, 6 | useRef, 7 | useEffect, 8 | createContext, 9 | } from 'react' 10 | import {Button, Form, FormInstance} from 'antd' 11 | import VBasicFormItem, {IFormItemProps} from './components/FromItem' 12 | import classnames from 'classnames' 13 | import './index.less' 14 | 15 | /** 将组件内部属性暴露到外面 */ 16 | export interface FromImperativeProps { 17 | onFinish: (callback: (values: any) => void) => void 18 | onReset: (callback: () => void) => void 19 | } 20 | 21 | /** 外部传递给表单属性 */ 22 | interface IVBasicFormV2Props { 23 | ref: any 24 | className?: string 25 | style?: React.CSSProperties 26 | formFooter?: React.ReactNode 27 | formFields: IFormItemProps[] 28 | } 29 | 30 | /** 表单context属性 */ 31 | interface IFormContext { 32 | form: FormInstance 33 | } 34 | 35 | /** 提供给表单子组件的context */ 36 | export const FormContext = createContext({} as IFormContext) 37 | // eslint-disable-next-line 38 | const VBasicFormV2: React.FC = forwardRef((props, ref) => { 39 | // eslint-disable-next-line 40 | const submitFnRef = useRef((values: any) => {}) 41 | const resetFnRef = useRef<() => void>(() => null) 42 | /** 获取表单组件实例 */ 43 | const [form] = Form.useForm() 44 | /** 解构组件属性 */ 45 | const {className, style, formFooter, formFields} = props 46 | /** 自定义Css class */ 47 | const classes = classnames('vbasic-form', className) 48 | /** 初始化表单值函数 */ 49 | const initFormValue = useMemo(() => { 50 | return () => { 51 | formFields.forEach(field => { 52 | const {name, value} = field 53 | form.setFieldsValue({[name]: value}) 54 | }) 55 | } 56 | }, [formFields]) 57 | /** 初始化表单的值 */ 58 | useEffect(() => { 59 | initFormValue() 60 | }, [initFormValue]) 61 | /** 将组件内部属性暴露到外面 */ 62 | useImperativeHandle( 63 | ref, 64 | () => { 65 | return { 66 | onFinish: cb => { 67 | console.log(222) 68 | 69 | submitFnRef.current = cb 70 | }, 71 | onReset: cb => { 72 | resetFnRef.current = cb 73 | }, 74 | } 75 | }, 76 | [submitFnRef, resetFnRef], 77 | ) 78 | /** 处理表单提交 */ 79 | const handleSubmit = useMemo(() => { 80 | return (values: any) => { 81 | submitFnRef.current(values) 82 | } 83 | }, [submitFnRef]) 84 | /** 处理表单重置 */ 85 | const handleReset = () => { 86 | form.resetFields() 87 | resetFnRef.current() 88 | } 89 | /** 生成所有FormItem */ 90 | const gereratorFormItems = () => { 91 | return formFields.map(field => ( 92 | 93 | )) 94 | } 95 | /** 生成表单底部内容 */ 96 | const generatorFormFooter = useMemo(() => { 97 | return ( 98 | formFooter || ( 99 | 100 | 103 | 106 | 107 | ) 108 | ) 109 | }, [formFooter]) 110 | 111 | return ( 112 |
113 | 118 |
119 | {/* FormItem */} 120 | {gereratorFormItems()} 121 | {/* 表单底部 */} 122 | {generatorFormFooter} 123 | 124 |
125 |
126 | ) 127 | }) 128 | 129 | export default memo(VBasicFormV2) 130 | -------------------------------------------------------------------------------- /src/components/VBasicModal/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/components/VBasicModal/index.less -------------------------------------------------------------------------------- /src/components/VBasicModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useState, 4 | useImperativeHandle, 5 | forwardRef, 6 | useRef, 7 | memo, 8 | useMemo, 9 | } from 'react' 10 | import {Button, Modal, ModalProps} from 'antd' 11 | import classnames from 'classnames' 12 | import './index.less' 13 | 14 | /** 点击确认按钮的回调函数 */ 15 | interface OnOK { 16 | (): void 17 | } 18 | 19 | /** 将组件内部的一些属性或方法暴露到组件外面 */ 20 | export interface ModalImperativeProps { 21 | /** 打开弹框的函数 */ 22 | openModal: (onOk: () => void, content?: string | React.ReactNode) => void 23 | /** 关闭弹框的函数 */ 24 | closeModal: () => void 25 | } 26 | 27 | /** 从ModalProps剔除一些属性 */ 28 | type ModalWithoutProps = 'onOk' | 'onCancel' | 'title' | 'content' | 'footer' 29 | 30 | interface IVBasicModalProps extends Omit { 31 | /** basicModal组件引用实例 */ 32 | ref: any 33 | /** 弹框标题 */ 34 | title?: React.ReactNode | string 35 | /** 弹框提示内容 */ 36 | content?: React.ReactNode | string 37 | /** 自定义弹框底部内容 */ 38 | customFooter?: React.ReactNode 39 | /** 自定义 Css class */ 40 | className?: string 41 | /** 自定义Css 内联样式 */ 42 | style?: React.CSSProperties 43 | children?: React.ReactNode 44 | } 45 | // eslint-disable-next-line 46 | const VBasicModal: React.FC = forwardRef((props, ref) => { 47 | /** 解构组件的属性 */ 48 | const { 49 | title, 50 | className, 51 | style, 52 | content, 53 | customFooter, 54 | children, 55 | ...restProps 56 | } = props 57 | const contentRef = useRef() 58 | /** 保存外部传递的确认回调函数 */ 59 | const handleOkRef = useRef(() => null) 60 | /** 组件内部向外抛出的数据 */ 61 | useImperativeHandle(ref, () => ({ 62 | openModal: (onOk, content) => { 63 | setVisiable(true) 64 | contentRef.current = content 65 | handleOkRef.current = onOk 66 | }, 67 | closeModal: () => { 68 | setVisiable(false) 69 | }, 70 | })) 71 | /** 组件的class类 */ 72 | const classes = classnames('vbasic-modal', className) 73 | /** 弹框显示/隐藏状态 */ 74 | const [visiable, setVisiable] = useState(false) 75 | 76 | /** 内部关闭弹框 */ 77 | const handleCancelModal = useCallback(() => { 78 | setVisiable(false) 79 | }, []) 80 | /** 弹框确认 */ 81 | const handleOkModal = useCallback(() => { 82 | handleOkRef.current() 83 | }, []) 84 | 85 | /** 生成弹框底部内容 */ 86 | const generatorFooter = useMemo(() => { 87 | return ( 88 | customFooter || ( 89 |
90 | 91 | 94 |
95 | ) 96 | ) 97 | }, [customFooter]) 98 | 99 | return ( 100 | 110 | {children || contentRef.current || content} 111 | 112 | ) 113 | }) 114 | 115 | VBasicModal.defaultProps = { 116 | title: '基础通用弹框', 117 | content: '确认执行该操作?', 118 | } 119 | 120 | export default memo(VBasicModal) 121 | -------------------------------------------------------------------------------- /src/components/VBasicModalForm/index.less: -------------------------------------------------------------------------------- 1 | .ant-modal-close:hover { 2 | .fullscreen-icon { 3 | svg { 4 | color: #8c8c8c !important; 5 | } 6 | } 7 | } 8 | // 修改icon颜色字体大小 9 | .close-icon-wrapper { 10 | .fullscreen-icon { 11 | svg { 12 | color: #8c8c8c !important; 13 | font-size: 14px !important; 14 | fill: #8c8c8c !important; 15 | transition: all 0.3s ease !important; 16 | } 17 | } 18 | .fullscreen-icon:hover { 19 | svg { 20 | color: #404040 !important; 21 | fill: #404040 !important; 22 | } 23 | } 24 | } 25 | 26 | // 全屏下将弹框样式 27 | .fullscreen-modal .ant-modal { 28 | top: 0 !important; 29 | right: 0 !important; 30 | bottom: 0 !important; 31 | left: 0 !important; 32 | width: 100% !important; 33 | height: 100%; 34 | margin: auto 0 !important; 35 | padding-bottom: 0 !important; 36 | max-width: 100% !important; 37 | } 38 | 39 | // 全屏下将弹框内容高度设置为100% 40 | .fullscreen-modal .ant-modal-content { 41 | height: 100%; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/VBasicTable/components/VBasicTableHeader/index.less: -------------------------------------------------------------------------------- 1 | .vbasic-table-header { 2 | display: flex; 3 | justify-content: space-between; 4 | .ant-table-title { 5 | padding: 0; 6 | padding-bottom: 8px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/VBasicTable/components/VBasicTableHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {MouseEvent} from 'react' 2 | import VBasicTableTitle from '../VBasicTableTitle' 3 | import VBasicTableToolbar from '../VBasicTableToolbar' 4 | import './index.less' 5 | 6 | interface IVBasicTableHeaderProps { 7 | title: string 8 | addBtnText: string 9 | addBtnClick(e: MouseEvent): void 10 | } 11 | 12 | const VBasicTableHeader: React.FC = props => { 13 | const {title, addBtnText, addBtnClick} = props 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 |
21 | ) 22 | } 23 | 24 | export default React.memo(VBasicTableHeader) 25 | -------------------------------------------------------------------------------- /src/components/VBasicTable/components/VBasicTableTitle/index.less: -------------------------------------------------------------------------------- 1 | .vbasic-table-title { 2 | font-size: 16px; 3 | font-weight: bold; 4 | color: #000; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/VBasicTable/components/VBasicTableTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './index.less' 3 | 4 | interface IVBasicTableTitleProps { 5 | title: string 6 | } 7 | 8 | const VBasicTableTitle: React.FC = props => { 9 | return
{props.title}
10 | } 11 | 12 | export default React.memo(VBasicTableTitle) 13 | -------------------------------------------------------------------------------- /src/components/VBasicTable/components/VBasicTableToolbar/index.less: -------------------------------------------------------------------------------- 1 | .vbasic-table-toolbar { 2 | .anticon { 3 | font-size: 18px !important; 4 | color: #000 !important; 5 | font-weight: bold; 6 | margin: 0.4rem; 7 | cursor: pointer; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/VBasicTable/components/VBasicTableToolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {MouseEvent, useContext} from 'react' 2 | import {Button, Divider, Tooltip, Dropdown, Menu} from 'antd' 3 | import Icon from 'comps/Icon' 4 | import {ToolbarContext} from '../../index' 5 | import './index.less' 6 | 7 | interface IVBasicTableToolbarProps { 8 | addBtnText: string 9 | addBtnClick(e: MouseEvent): void 10 | } 11 | 12 | const VBasicTableToolbar: React.FC = props => { 13 | const {addBtnText, addBtnClick} = props 14 | 15 | const {size, onRefresh, dispatch} = useContext(ToolbarContext) 16 | 17 | const dispatchSize = (key: 'large' | 'middle' | 'small') => { 18 | switch (key) { 19 | case 'large': 20 | dispatch && dispatch({type: 'updateSize', value: 'large'}) 21 | break 22 | case 'middle': 23 | dispatch && dispatch({type: 'updateSize', value: 'middle'}) 24 | break 25 | case 'small': 26 | dispatch && dispatch({type: 'updateSize', value: 'small'}) 27 | break 28 | default: 29 | break 30 | } 31 | } 32 | 33 | const SizeOverlay = ( 34 | 35 | { 38 | dispatchSize('large') 39 | }} 40 | > 41 | 大号 42 | 43 | { 46 | dispatchSize('middle') 47 | }} 48 | > 49 | 中等 50 | 51 | { 54 | dispatchSize('small') 55 | }} 56 | > 57 | 紧凑 58 | 59 | 60 | ) 61 | 62 | return ( 63 |
64 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | ) 93 | } 94 | 95 | export default React.memo(VBasicTableToolbar) 96 | -------------------------------------------------------------------------------- /src/components/VBasicTable/default.ts: -------------------------------------------------------------------------------- 1 | import {TablePaginationConfig} from 'antd/lib/table/interface' 2 | 3 | export const defaultPagination: TablePaginationConfig = { 4 | total: 0, 5 | showSizeChanger: true, 6 | showQuickJumper: true, 7 | showTotal: total => `共 ${total} 条数据`, 8 | showTitle: true, 9 | } 10 | -------------------------------------------------------------------------------- /src/components/VBasicTable/hooks/useTableScrollY.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react' 2 | import {SizeType} from 'antd/lib/config-provider/SizeContext' 3 | import {debounce} from 'utils/index' 4 | 5 | export default function useTableScrollY(othersHeight = 0, size: SizeType) { 6 | const initScrollY = document.documentElement.clientHeight - othersHeight || 0 7 | 8 | const [scrollY, setScrollY] = useState(initScrollY) 9 | 10 | function getPosition() { 11 | setScrollY(document.documentElement.clientHeight - othersHeight) 12 | } 13 | const res = debounce(50, getPosition) 14 | 15 | useEffect(() => { 16 | res() 17 | }, [size, othersHeight]) 18 | 19 | useEffect(() => { 20 | const listener = () => { 21 | res() 22 | } 23 | window.addEventListener('resize', listener) 24 | return () => { 25 | window.removeEventListener('reset', listener) 26 | } 27 | }) 28 | 29 | return scrollY 30 | } 31 | -------------------------------------------------------------------------------- /src/components/VBasicTable/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/mixins.less'; 2 | .vbasic-table { 3 | padding: 8px; 4 | background-color: #fff; 5 | // 表格体滚动条样式 6 | .ant-table-body, 7 | .ant-table-content { 8 | .scrollbar(); 9 | } 10 | .rowBgColor { 11 | background-color: #fafafa; 12 | } 13 | 14 | .rowHover { 15 | &:hover { 16 | > td { 17 | background-color: #e3f4fc !important; 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/VBasicTree/index.less: -------------------------------------------------------------------------------- 1 | .vbasic-tree { 2 | height: 100%; 3 | &-header { 4 | .ant-dropdown-trigger { 5 | vertical-align: middle; 6 | margin-top: -2px; 7 | margin-left: 5px; 8 | color: #000; 9 | > svg { 10 | transform: scale(1.1); 11 | color: #000; 12 | font-weight: 900; 13 | } 14 | } 15 | } 16 | &-title { 17 | font-size: 16px; 18 | font-weight: bold; 19 | color: #000; 20 | } 21 | &-body { 22 | padding: 8px 0; 23 | } 24 | .site-tree-search-value { 25 | color: rgb(230, 209, 198) !important; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/VBasicTree/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import {DataNode} from 'rc-tree/lib/interface' 2 | /** 3 | * 生成一个扁平化数据 4 | * @param treeData 5 | * @param dataList 6 | */ 7 | export const generateList = (treeData: DataNode[], dataList: any[] = []) => { 8 | for (let i = 0; i < treeData.length; i++) { 9 | const node = treeData[i] 10 | const {key, title} = node 11 | dataList.push({...node, key, title}) 12 | if (node.children) { 13 | generateList(node.children, dataList) 14 | } 15 | } 16 | } 17 | 18 | /** 19 | * 通过key在树中查找对应的节点的父节点的key 20 | * @param key 21 | * @param tree 22 | * @returns 23 | */ 24 | export const getParentKey = (key: string, tree: any[]): any => { 25 | let parentKey 26 | for (let i = 0; i < tree.length; i++) { 27 | const node = tree[i] 28 | if (node.children) { 29 | if (node.children.some((item: any) => item.key === key)) { 30 | parentKey = node.key 31 | } else if (getParentKey(key, node.children)) { 32 | parentKey = getParentKey(key, node.children) 33 | } 34 | } 35 | } 36 | return parentKey 37 | } 38 | 39 | /** 40 | * 根据输入的值获取要展开的keys 41 | * @param value 搜索框字符串 42 | * @param dataList 扁平化的treeData 43 | * @param treeData tree组件所需树形数据 44 | */ 45 | export const getExpandeKeysInDataListByValue = ( 46 | value: any, 47 | dataList: any[], 48 | treeData: DataNode[], 49 | ) => { 50 | return dataList 51 | .map(item => { 52 | if (item.title.indexOf(value) > -1) { 53 | return getParentKey(item.key, treeData) 54 | } 55 | return null 56 | }) 57 | .filter((item, i, self) => item && self.indexOf(item) === i) 58 | } 59 | -------------------------------------------------------------------------------- /src/components/vBasicForm/components/FormIconPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useMemo, useRef, useState} from 'react' 2 | import {Input} from 'antd' 3 | import Icon, {IconPicker} from 'comps/Icon' 4 | import {IconPrickerImperativeProps} from 'comps/Icon/IconPicker' 5 | import {FormContext} from '../../index' 6 | 7 | interface IFormIconPickerProps { 8 | /** 表单字段名 */ 9 | name: string 10 | /** 表单值 */ 11 | selectedIcon: string 12 | /** 触发选中的事件 */ 13 | onSelectedIcon: (iconName: string) => void 14 | } 15 | 16 | const FormIconPicker: React.FC = props => { 17 | const {name, selectedIcon, onSelectedIcon} = props 18 | const [innerVal, setInnerVal] = useState(selectedIcon) 19 | const iconPickerRef = useRef(null) 20 | 21 | /** 从context中获取表单实例 */ 22 | const {form, closeFlag} = useContext(FormContext) 23 | 24 | /** 用户监听iconName值发生改变重新设置字段值的effect */ 25 | useEffect(() => { 26 | form.setFieldsValue({[name]: innerVal}) 27 | }, [form, innerVal]) 28 | 29 | /** 弹框关闭重置FormItem字段值,输入框值的effect */ 30 | useEffect(() => { 31 | /** 拥有初始值 */ 32 | if (selectedIcon) { 33 | return setInnerVal(selectedIcon) 34 | } 35 | setInnerVal('') 36 | }, [closeFlag]) 37 | 38 | /** 打开图标库,选取图标 */ 39 | const handleOpenIconPicker = () => { 40 | const {openIconPicker} = iconPickerRef.current! 41 | openIconPicker(iconName => { 42 | onSelectedIcon(iconName) 43 | setInnerVal(iconName) 44 | }, innerVal) 45 | } 46 | 47 | /** 渲染Input组件后缀,用作展示从图标库选中的图标 */ 48 | const renderAddoAfter = useMemo(() => { 49 | return innerVal ? ( 50 | 51 | ) : ( 52 | 选择图标 53 | ) 54 | }, [innerVal]) 55 | 56 | return ( 57 |
58 | 59 | 60 | 61 |
62 | ) 63 | } 64 | 65 | export default FormIconPicker 66 | -------------------------------------------------------------------------------- /src/components/vBasicForm/components/FormTree/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react' 2 | import {Input} from 'antd' 3 | import VBasictree, {IVBasicTreeProps} from '../../../VBasicTree' 4 | import {FormContext} from '../../index' 5 | import {deepMerge} from 'utils/utils' 6 | import {Key} from 'rc-tree/lib/interface' 7 | 8 | /** 剔除一些属性 */ 9 | export type filterProps = 10 | | 'checkedKeys' 11 | | 'onCheck' 12 | | 'onSelect' 13 | | 'selectedKeys' 14 | 15 | export type FormTreeOuterProps = Partial> 16 | 17 | /** 提供FormTree组件内部的属性 */ 18 | interface IFormTreeProps extends FormTreeOuterProps { 19 | /** 表单字段名称 */ 20 | name: string 21 | /** 表单值 */ 22 | selectValue: Key[] 23 | /** 选中节点值发生改变的回调 */ 24 | onFormTreeChange: (keys: Key[], type: 'check' | 'select') => void 25 | } 26 | 27 | /** 默认基础树形控件配置项 */ 28 | const defaultBasicTreeOptions: IVBasicTreeProps = { 29 | /** 组件上方标题 */ 30 | title: 'BasicTitle', 31 | treeData: [], 32 | /** 是否可选中 */ 33 | selectable: false, 34 | /** 是否显示搜索框 */ 35 | showSearch: false, 36 | /** 节点前添加 Checkbox 复选框 */ 37 | checkable: true, 38 | /** 是否展示 TreeNode title 前的图标 */ 39 | showIcon: true, 40 | /** checkable 状态下节点选择完全受控(父子节点选中状态不再关联) */ 41 | checkStrictly: true, 42 | } 43 | 44 | type onSelectFnType = (selectedKeys: Key[]) => void 45 | 46 | type onCheckFnType = (checked: Key[]) => void 47 | 48 | const FormTree: React.FC = props => { 49 | /** 解构组件的props */ 50 | const { 51 | name, 52 | selectValue, 53 | onFormTreeChange, 54 | checkable, 55 | selectable, 56 | ...restProps 57 | } = props 58 | /** 选中复选框选中的值 */ 59 | const [checkedKeys, setCheckedKeys] = useState(selectValue || []) 60 | /** 点击选中树节点的值 */ 61 | const [selectedKeys, setSelectedKeys] = useState(selectValue || []) 62 | 63 | /** FormTree容器内联样式 */ 64 | const style: React.CSSProperties = { 65 | height: '250px', 66 | minHeight: '250px', 67 | } 68 | 69 | /** 与默认配置项合并 */ 70 | const options = deepMerge(defaultBasicTreeOptions, restProps) 71 | 72 | /** FormContext */ 73 | const {form, closeFlag} = useContext(FormContext) 74 | 75 | /** 处理点击树节点触发的回调 */ 76 | const handleSelect: onSelectFnType = keys => { 77 | if (!selectable) return 78 | form.setFieldsValue({[name]: keys}) 79 | setSelectedKeys(keys) 80 | onFormTreeChange(keys, 'select') 81 | } 82 | 83 | /** 点击复选框触发的回调 */ 84 | const handleCheck: onCheckFnType = keys => { 85 | if (!checkable) return 86 | form.setFieldsValue({[name]: keys}) 87 | setCheckedKeys(keys) 88 | onFormTreeChange(keys, 'check') 89 | } 90 | 91 | /** 监听modal是否关闭,如果关闭就重置树的数据 */ 92 | useEffect(() => { 93 | if (selectValue && selectValue.length) { 94 | return setCheckedKeys(selectValue) 95 | } 96 | 97 | if (checkable) { 98 | setCheckedKeys([]) 99 | } 100 | if (selectable) { 101 | setSelectedKeys([]) 102 | } 103 | }, [closeFlag]) 104 | 105 | return ( 106 |
107 | 108 | 117 |
118 | ) 119 | } 120 | 121 | FormTree.defaultProps = defaultBasicTreeOptions 122 | 123 | export default FormTree 124 | -------------------------------------------------------------------------------- /src/components/vBasicForm/components/FormTreeSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useContext, useEffect, useState} from 'react' 2 | import {message, TreeSelect} from 'antd' 3 | import {DataNode} from 'rc-tree-select/lib/interface' 4 | import {isPromise} from 'utils/utils' 5 | import {FormContext} from '../../index' 6 | 7 | /** 基础表单树形选择控件属性 */ 8 | interface IFormTreeSelectProps { 9 | /** 表单字段名 */ 10 | name: string 11 | /** 将要选中的值 */ 12 | selectValue: any 13 | /** 选中树节点的回调 */ 14 | onSelect: (value: any) => void 15 | /** 树形选择控件的值 */ 16 | treeData: Promise | DataNode[] 17 | } 18 | 19 | const FormTreeSelect: React.FC = props => { 20 | /** 解构props */ 21 | const {name, selectValue, onSelect} = props 22 | let treeData = props.treeData 23 | /** 通过context获取form实例 */ 24 | const {form, closeFlag} = useContext(FormContext) 25 | 26 | const [data, setData] = useState([]) 27 | 28 | const [value, setValue] = useState() 29 | 30 | /** 打开抽屉时设置初始值,关闭清空值 */ 31 | useEffect(() => { 32 | if (closeFlag) { 33 | setValue('') 34 | } else { 35 | setValue(selectValue) 36 | } 37 | }, [closeFlag, selectValue]) 38 | 39 | if (Array.isArray(treeData)) { 40 | treeData = Promise.resolve(treeData) 41 | } else if (isPromise(treeData)) { 42 | treeData = treeData 43 | } else { 44 | message.warning('只能传入Promise 或 DataNode[]类型的数组') 45 | treeData = Promise.resolve([]) 46 | } 47 | 48 | /** 设置树形选择器的值 */ 49 | useEffect(() => { 50 | ;(treeData as Promise).then(res => { 51 | setData(res) 52 | }) 53 | }, [treeData]) 54 | 55 | /** 更新表单字段值 */ 56 | useEffect(() => { 57 | form.setFieldsValue({[name]: value}) 58 | }, [value]) 59 | 60 | const handleSelect = useCallback( 61 | (value: any) => { 62 | setValue(value) 63 | onSelect && onSelect(value) 64 | }, 65 | [onSelect, setValue], 66 | ) 67 | const handleClear = useCallback(() => { 68 | setValue('') 69 | }, [selectValue]) 70 | return ( 71 | 78 | ) 79 | } 80 | 81 | export default FormTreeSelect 82 | -------------------------------------------------------------------------------- /src/components/vBasicForm/index.less: -------------------------------------------------------------------------------- 1 | .vbasic-form { 2 | margin-bottom: 16px !important; 3 | background-color: #fff !important; 4 | border-radius: 4px; 5 | padding: 12px 10px 18px 10px !important; 6 | .ant-form-item-label { 7 | width: 120px; 8 | } 9 | .ant-form-item-control { 10 | width: calc(100% - 120px); 11 | } 12 | .reset-btn { 13 | margin-right: 0.5rem !important; 14 | } 15 | .ant-row { 16 | width: 100%; 17 | } 18 | .vbasic-form-row { 19 | > .ant-col { 20 | margin-bottom: 16px; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/vBasicForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useMemo, createContext} from 'react' 2 | import {Row, Col, Button, Form} from 'antd' 3 | import {FormLayout, FormInstance} from 'antd/lib/form/Form' 4 | import VBasicFormItem, {IFormItemProps} from './components/FromItem' 5 | import classnames from 'classnames' 6 | import './index.less' 7 | 8 | export interface IVBasicFormProps { 9 | layout?: FormLayout 10 | style?: React.CSSProperties 11 | className?: string 12 | onFinish: (values: any) => void 13 | onReset: () => void 14 | formItems: IFormItemProps[] 15 | loading?: boolean 16 | } 17 | 18 | export interface VBasicFormContext { 19 | form: FormInstance 20 | closeFlag?: boolean 21 | } 22 | 23 | export const FormContext = createContext( 24 | {} as VBasicFormContext, 25 | ) 26 | 27 | const VBasicForm: React.FC = props => { 28 | /** 解构组件的props */ 29 | const { 30 | className, 31 | style, 32 | layout, 33 | onFinish, 34 | onReset, 35 | formItems, 36 | loading, 37 | } = props 38 | 39 | /** 获取表单实例 */ 40 | const [form] = Form.useForm() 41 | 42 | /** 自定义Css class */ 43 | const classes = classnames('vbasic-form-wrapper', className) 44 | 45 | /** 处理表单提交 */ 46 | const handleSubmit = (values: any) => { 47 | onFinish && onFinish!(values) 48 | } 49 | 50 | /** 处理表单重置 */ 51 | const handleReset = () => { 52 | form.resetFields() 53 | onReset && onReset() 54 | } 55 | 56 | /** 初始化表单值 */ 57 | const initFormValue = useMemo(() => { 58 | return () => { 59 | formItems.forEach(field => { 60 | const {name, value} = field 61 | form.setFieldsValue({[name]: value !== undefined ? value : ''}) 62 | }) 63 | } 64 | }, [formItems, form]) 65 | 66 | useEffect(() => { 67 | initFormValue() 68 | }, [initFormValue]) 69 | 70 | /** 生成FormItem */ 71 | const generatorFormItems = useMemo(() => { 72 | return formItems.map((item, index) => { 73 | return ( 74 |
75 | 76 | 77 | ) 78 | }) 79 | }, [formItems]) 80 | 81 | /** 自定义表单底部内容 */ 82 | const gereratorFormFooter = useMemo(() => { 83 | return ( 84 | 85 | 86 | 89 | 92 | 93 | 94 | ) 95 | }, [loading, handleReset]) 96 | 97 | return ( 98 |
99 | 104 |
111 | 112 | {generatorFormItems} 113 | {gereratorFormFooter} 114 | 115 | 116 |
117 |
118 | ) 119 | } 120 | 121 | VBasicForm.defaultProps = { 122 | layout: 'inline', 123 | loading: false, 124 | } 125 | 126 | export default React.memo(VBasicForm) 127 | -------------------------------------------------------------------------------- /src/config/routeMap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Login, 3 | Dashboard, 4 | Doc, 5 | About, 6 | Explanation, 7 | Editor, 8 | Admin, 9 | Guest, 10 | Account, 11 | Role, 12 | Menu, 13 | ChangePassword, 14 | Department, 15 | Menu1_2_1, 16 | Menu1_1, 17 | NotFound, 18 | AccountCenter, 19 | AccountSetting, 20 | TableCom, 21 | FormCom, 22 | } from '../views' 23 | export type RoleName = 'admin' | 'editor' | 'guest' 24 | export interface routeProps { 25 | path: string 26 | name?: string 27 | noExtra?: boolean 28 | // eslint-disable-next-line 29 | component: React.LazyExoticComponent 30 | roles?: Array 31 | notAuth?: boolean 32 | } 33 | 34 | /** 35 | * 全局路由,无需嵌套sidebar,header 36 | */ 37 | export const globalRoutes: Array = [ 38 | { 39 | path: '/login', 40 | name: 'Login', 41 | component: Login, 42 | }, 43 | ] 44 | 45 | const routes: Array = [ 46 | { 47 | path: '/dashboard', 48 | name: 'Dashboard', 49 | component: Dashboard, 50 | notAuth: true, 51 | }, 52 | { 53 | path: '/doc', 54 | name: 'Doc', 55 | component: Doc, 56 | notAuth: true, 57 | }, 58 | { 59 | path: '/permission/explanation', 60 | name: 'Explanation', 61 | component: Explanation, 62 | notAuth: true, 63 | }, 64 | { 65 | path: '/permission/admin', 66 | name: 'Admin', 67 | component: Admin, 68 | }, 69 | { 70 | path: '/permission/editor', 71 | name: 'Editor', 72 | component: Editor, 73 | }, 74 | { 75 | path: '/permission/guest', 76 | name: 'Guest', 77 | component: Guest, 78 | }, 79 | { 80 | path: '/system/account', 81 | name: 'Account', 82 | component: Account, 83 | }, 84 | { 85 | path: '/system/role', 86 | name: 'Role', 87 | component: Role, 88 | }, 89 | { 90 | path: '/system/menu', 91 | name: 'Menu', 92 | component: Menu, 93 | }, 94 | { 95 | path: '/system/dept', 96 | name: 'Department', 97 | component: Department, 98 | }, 99 | { 100 | path: '/system/changePassword', 101 | name: 'ChangePassword', 102 | component: ChangePassword, 103 | }, 104 | { 105 | path: '/component/table', 106 | name: 'TableCom', 107 | component: TableCom, 108 | }, 109 | { 110 | path: '/component/form', 111 | name: 'FormCom', 112 | component: FormCom, 113 | }, 114 | { 115 | path: '/nested/menu1/menu1-1', 116 | name: 'Menu1_1', 117 | component: Menu1_1, 118 | }, 119 | { 120 | path: '/nested/menu1/menu1-2/menu1-2-1', 121 | name: 'Menu1_2_1', 122 | component: Menu1_2_1, 123 | }, 124 | { 125 | path: '/account/center', 126 | name: 'AccountCenter', 127 | component: AccountCenter, 128 | notAuth: true, 129 | }, 130 | { 131 | path: '/account/setting', 132 | name: 'AccountSetting', 133 | component: AccountSetting, 134 | notAuth: true, 135 | }, 136 | { 137 | path: '/about', 138 | name: 'About', 139 | component: About, 140 | notAuth: true, 141 | }, 142 | { 143 | path: '/error/404', 144 | name: '404', 145 | component: NotFound, 146 | notAuth: true, 147 | }, 148 | ] 149 | 150 | export default routes 151 | -------------------------------------------------------------------------------- /src/core/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react' 2 | export default function Initializer() { 3 | useEffect(() => { 4 | console.log('Initializer') 5 | }, []) 6 | } 7 | -------------------------------------------------------------------------------- /src/defaultSetting.ts: -------------------------------------------------------------------------------- 1 | import {Language} from './store/reducers/app' 2 | 3 | /** 菜单折叠按钮显示位置 */ 4 | export enum CollapsedMenuBtnPosition { 5 | hide = 'hide', 6 | top = 'top', 7 | bottom = 'bottom', 8 | } 9 | 10 | export interface DefaultSettings { 11 | iconfontUrl: string 12 | showLogo: boolean 13 | fixHeader: boolean 14 | fixSidebar: boolean 15 | openTagsView: boolean 16 | showSidebar: boolean 17 | showBreadcrumb: boolean 18 | sidebarCollapsed: boolean 19 | title: string 20 | collapsedMenuBtnPosition: CollapsedMenuBtnPosition 21 | lang: Language 22 | weekMode: boolean 23 | grayMode: boolean 24 | } 25 | 26 | const defaultSettings: DefaultSettings = { 27 | // Your custom iconfont Symbol script Url 28 | // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js 29 | // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理 30 | // Usage: https://github.com/ant-design/ant-design-pro/pull/3517 31 | iconfontUrl: '', 32 | 33 | // 是否显示侧边Logo 34 | showLogo: true, 35 | 36 | // 是否固定header 37 | fixHeader: false, 38 | 39 | // 是否固定sidebar 40 | fixSidebar: true, 41 | 42 | // 是否开启tags-view 43 | openTagsView: true, 44 | 45 | // 是否显示侧边栏 46 | showSidebar: true, 47 | 48 | // 是否显示面包屑导航 49 | showBreadcrumb: true, 50 | 51 | // 侧边栏是否折叠 52 | sidebarCollapsed: false, 53 | 54 | // 菜单折叠按钮显示位置 55 | collapsedMenuBtnPosition: CollapsedMenuBtnPosition.top, 56 | 57 | // 色弱模式 58 | weekMode: false, 59 | 60 | // 灰度模式 61 | grayMode: false, 62 | 63 | // 页面title 64 | title: 'React-Ts-Antd-Pro', 65 | 66 | // 系统默认语言 67 | lang: 'zh', 68 | } 69 | 70 | export default defaultSettings 71 | -------------------------------------------------------------------------------- /src/demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface IDemoProps {} 4 | 5 | const Demo: React.FC = () => { 6 | return
Demo Page
7 | } 8 | 9 | export default Demo 10 | -------------------------------------------------------------------------------- /src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/hooks/useActions.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | bindActionCreators, 3 | ActionCreator, 4 | ActionCreatorsMapObject, 5 | AnyAction, 6 | } from 'redux' 7 | import {useDispatch} from 'react-redux' 8 | import {useMemo} from 'react' 9 | import { 10 | CommonActionCreator, 11 | CommonAsyncActionCreator, 12 | RootActionsCreator, 13 | } from '../store/actions' 14 | 15 | /** redux中的ActionCreator */ 16 | type ReduxActionCreator = 17 | | ActionCreator 18 | | ActionCreator[] 19 | | ActionCreatorsMapObject 20 | 21 | /** 当前应用的ActionCreator */ 22 | type AppRootActionCreator = 23 | | RootActionsCreator 24 | | RootActionsCreator[] 25 | | {[key: string]: RootActionsCreator} 26 | 27 | /** 自定义通用的ActionCreator */ 28 | type CombineCommonActionCreator = CommonActionCreator | CommonAsyncActionCreator 29 | type CommonActionCreators = 30 | | CombineCommonActionCreator 31 | | CombineCommonActionCreator[] 32 | | {[key: string]: CombineCommonActionCreator} 33 | 34 | /** useActions hooks第一个参数actions的类型 */ 35 | type actionsType = 36 | | AppRootActionCreator 37 | | ReduxActionCreator 38 | | CommonActionCreators 39 | 40 | export default function useActions(actions: actionsType, deps: any[]): any { 41 | const dispatch = useDispatch() 42 | return useMemo( 43 | () => { 44 | if (Array.isArray(actions)) { 45 | return actions.map((a: any) => bindActionCreators(a, dispatch)) 46 | } 47 | if (typeof actions === 'object') { 48 | return bindActionCreators(actions, dispatch) 49 | } 50 | return bindActionCreators(actions, dispatch) 51 | }, 52 | deps ? [dispatch, ...deps] : [dispatch], 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/hooks/useClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, RefObject} from 'react' 2 | 3 | export default function useClickOutside( 4 | ref: RefObject, 5 | handler: (e: MouseEvent) => void, 6 | ): void { 7 | useEffect(() => { 8 | const listener = (e: MouseEvent) => { 9 | if ( 10 | !ref.current || 11 | ref.current.contains(e.target as HTMLElement) || 12 | !handler 13 | ) { 14 | return 15 | } 16 | handler(e) 17 | } 18 | 19 | document.addEventListener('click', listener, false) 20 | 21 | return () => { 22 | document.removeEventListener('click', listener, false) 23 | } 24 | }, [ref, handler]) 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useDocumentSize.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react' 2 | 3 | // eslint-disable-next-line 4 | export default function useDocumentSize(callback?: Function) { 5 | const [documentSize, setDocumentSize] = useState({ 6 | width: document.documentElement.clientWidth, 7 | height: document.documentElement.clientHeight, 8 | }) 9 | 10 | const listener = () => { 11 | const width = document.documentElement.clientWidth || window.innerWidth 12 | const height = document.documentElement.clientHeight || window.innerHeight 13 | const size = {width, height} 14 | callback && callback(size) 15 | setDocumentSize(size) 16 | } 17 | 18 | useEffect(() => { 19 | window.addEventListener('resize', listener, false) 20 | return () => { 21 | window.removeEventListener('resize', listener, false) 22 | } 23 | }, []) 24 | 25 | return documentSize 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/useShallowEqualSelector.tsx: -------------------------------------------------------------------------------- 1 | import {useSelector, shallowEqual, TypedUseSelectorHook} from 'react-redux' 2 | import {StoreStateProps} from '../store/reducers' 3 | 4 | interface ISelector { 5 | (sRate: StoreStateProps): R 6 | } 7 | 8 | const useStoreSelector: TypedUseSelectorHook = useSelector 9 | 10 | export default function useShallowEqualSelecto( 11 | selector: ISelector, 12 | ) { 13 | return useStoreSelector(selector, shallowEqual) 14 | } 15 | -------------------------------------------------------------------------------- /src/layout/Content/index.less: -------------------------------------------------------------------------------- 1 | .layout-content-container { 2 | position: relative; 3 | flex: 1; 4 | // overflow: hidden; 5 | > div { 6 | height: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/layout/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo, Suspense, useContext} from 'react' 2 | import {Layout} from 'antd' 3 | import {Switch, Route, Redirect, useLocation} from 'react-router-dom' 4 | import routesConfig, {routeProps} from '@/config/routeMap' 5 | import {CSSTransition, TransitionGroup} from 'react-transition-group' 6 | import Loading from 'comps/Loading' 7 | import {useAppSelector} from 'store/index' 8 | import DocumentTitle from 'react-document-title' 9 | import MenuList from '@/config/menuConfig' 10 | import {getMenuItemInMenuListByProperty} from 'utils/index' 11 | import defaultSettings from '@/defaultSetting' 12 | import {LayoutContext} from '../index' 13 | const {Content} = Layout 14 | import './index.less' 15 | 16 | interface ILayoutContentProps {} 17 | 18 | const getPageTitle = (value: string): string => { 19 | const item = getMenuItemInMenuListByProperty(MenuList, 'path', value) 20 | if (item) { 21 | return item.title + ' - ' + defaultSettings.title 22 | } 23 | return defaultSettings.title 24 | } 25 | 26 | const LayoutContent: React.FC = () => { 27 | const location = useLocation() 28 | const {permissions} = useAppSelector(state => state.user) 29 | const {routes} = useContext(LayoutContext) 30 | 31 | const superdmin_permission = '*:*:*' 32 | 33 | /** 34 | * 用户拥有的路由(routes),与路由表(routesConfig)中进行比对筛选 35 | * 1. 无需权限 36 | * 2. 拥有超级管理员权限 '*:*:*' 37 | * 3. 用户拥有当前路由 38 | */ 39 | const filterRoute = (route: routeProps) => { 40 | return ( 41 | route.notAuth || 42 | permissions.includes(superdmin_permission) || 43 | (routes && 44 | routes.find(item => { 45 | return item.path === route.path 46 | })) 47 | ) 48 | } 49 | 50 | /** 异步获取到路由前先不渲染页面 */ 51 | if (!routes) { 52 | return null 53 | } 54 | 55 | return ( 56 | 57 | 58 | }> 59 | 60 | 66 | 67 | {routesConfig.map((route, index) => { 68 | return filterRoute(route) ? ( 69 | 75 | ) : null 76 | })} 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ) 86 | } 87 | 88 | export default memo(LayoutContent) 89 | -------------------------------------------------------------------------------- /src/layout/DrawerSider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties, useContext} from 'react' 2 | import Menu from '../Sider/Menu' 3 | import Logo from '../Sider/Logo' 4 | import {Drawer} from 'antd' 5 | import {LayoutContext} from '../index' 6 | 7 | interface IDrawerSiderProps {} 8 | 9 | const DrawerSider: React.FC = () => { 10 | const {drawerVisible, setDrawerVisible} = useContext(LayoutContext) 11 | const bodyStyle: CSSProperties = { 12 | padding: '0', 13 | background: '#001529', 14 | } 15 | 16 | return ( 17 | setDrawerVisible(false)} 23 | bodyStyle={bodyStyle} 24 | > 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | export default DrawerSider 32 | -------------------------------------------------------------------------------- /src/layout/Header/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .layout-header { 3 | transition: width 0.2s; 4 | z-index: @layout-header-zIndex; 5 | padding: 0; 6 | height: @layout-header-h; 7 | // line-height: @layout-header-top-h; 8 | background-color: @white !important; 9 | &-top { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | height: @layout-header-top-h; 14 | line-height: @layout-header-top-h; 15 | padding: 0; 16 | background-color: @white !important; 17 | box-shadow: @layout-header-box-shadow; 18 | .left-menu { 19 | display: flex; 20 | align-items: center; 21 | } 22 | .right-menu { 23 | > div { 24 | display: inline-block; 25 | padding: 0 10px; 26 | cursor: pointer; 27 | transition: background 0.3s ease; 28 | &:hover { 29 | background-color: @gray-1; 30 | } 31 | } 32 | } 33 | } 34 | &.is-fixed { 35 | position: fixed; 36 | top: 0; 37 | right: 0; 38 | z-index: @layout-fix-header-zIndex; 39 | } 40 | .user-avatar-dropdown { 41 | .anticon-caret-down { 42 | vertical-align: bottom; 43 | padding: 0 0 12px 6px; 44 | } 45 | } 46 | &.hide-tags { 47 | height: @layout-header-top-h; 48 | line-height: @layout-header-top-h; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/layout/RightPanel/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/mixins.less'; 2 | .right-panel-wrapper { 3 | .ant-drawer-content-wrapper { 4 | z-index: 10000 !important; 5 | .ant-drawer-body { 6 | padding: 0; 7 | } 8 | } 9 | .scrollbar-view { 10 | padding: 20px; 11 | } 12 | .ant-row { 13 | margin: 16px 0 !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/layout/Sider/Logo/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .sidebar-logo-container { 3 | line-height: @layout-header-top-h; 4 | height: @layout-header-top-h; 5 | background-color: #001529; 6 | overflow: hidden; 7 | text-align: center; 8 | .sidebar-logo { 9 | display: inline-block; 10 | width: @layout-header-top-h; 11 | height: @layout-header-top-h; 12 | vertical-align: middle; 13 | animation: sidebar-logo-spin 10s linear infinite; 14 | } 15 | .sidebar-title { 16 | display: inline-block; 17 | vertical-align: middle; 18 | margin-bottom: 0; 19 | color: @white; 20 | font-size: 14px; 21 | font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; 22 | font-weight: bold; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/layout/Sider/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {CSSProperties, memo} from 'react' 2 | import {Link} from 'react-router-dom' 3 | import logo from '@/assets/images/logo.svg' 4 | import classnames from 'classnames' 5 | import './index.less' 6 | 7 | interface ILogoProps { 8 | style?: CSSProperties 9 | classNames?: string 10 | showTitle?: boolean 11 | } 12 | 13 | const Logo: React.FC = props => { 14 | const {showTitle, classNames, style} = props 15 | const classes = classnames('sidebar-logo-container', classNames) 16 | return ( 17 |
18 | 19 | logo 20 | {showTitle &&

橙晨燕

} 21 | 22 |
23 | ) 24 | } 25 | 26 | Logo.defaultProps = { 27 | showTitle: true, 28 | } 29 | 30 | export default memo(Logo) 31 | -------------------------------------------------------------------------------- /src/layout/Sider/Menu/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/layout/Sider/Menu/index.less -------------------------------------------------------------------------------- /src/layout/Sider/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .layout-sidebar-container { 3 | .ant-menu { 4 | width: @layout-sidebar-w; 5 | } 6 | &.is-collapsed { 7 | .ant-menu { 8 | width: @layout-sidebar-collapsed-w; 9 | } 10 | } 11 | &.is-fixed { 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | height: 100%; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/layout/Sider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo, useCallback} from 'react' 2 | import {useAppDispatch, useAppSelector} from 'store/index' 3 | import {sidebarCollapsedToggle} from 'store/actions' 4 | import {Layout} from 'antd' 5 | import Logo from './Logo' 6 | import Menu from './Menu' 7 | import {CSSTransition} from 'react-transition-group' 8 | import variables from 'styles/variables.module.less' 9 | import classnames from 'classnames' 10 | const {Sider} = Layout 11 | import {CollapsedMenuBtnPosition} from 'root/src/defaultSetting' 12 | import './index.less' 13 | 14 | interface ILayoutSiderProps {} 15 | 16 | const LayoutSider: React.FC = () => { 17 | const sidebarCollapsed = useAppSelector(state => state.app.sidebarCollapsed) 18 | const {showLogo, fixSidebar, collapsedMenuBtnPosition} = useAppSelector( 19 | state => state.settings, 20 | ) 21 | const collapsedWidth = parseInt(variables['layout-sidebar-collapsed-w']) 22 | const width = parseInt(variables['layout-sidebar-w']) 23 | const dispatch = useAppDispatch() 24 | /** 占位的sider Css 类 */ 25 | const classes = classnames('layout-sidebar-container', { 26 | 'is-collapsed': sidebarCollapsed, 27 | }) 28 | 29 | /** 显示的sider Css 类*/ 30 | const sidebarClasses = classnames(classes, { 31 | 'is-fixed': fixSidebar, 32 | }) 33 | 34 | const sidebarProps: any = { 35 | collapsed: sidebarCollapsed, 36 | collapsedWidth: collapsedWidth, 37 | width: width, 38 | } 39 | 40 | const collapsed = collapsedMenuBtnPosition === CollapsedMenuBtnPosition.bottom 41 | 42 | const handleCollapse = useCallback(() => { 43 | dispatch(sidebarCollapsedToggle()) 44 | }, [dispatch, sidebarCollapsedToggle]) 45 | 46 | return ( 47 | <> 48 | {/* sidebar展位 */} 49 | {fixSidebar ? : null} 50 | 56 | 63 | 64 | 65 | 66 | 67 | 68 | ) 69 | } 70 | 71 | export default memo(LayoutSider) 72 | -------------------------------------------------------------------------------- /src/layout/TagsView/components/TagsList/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/layout/TagsView/components/TagsList/index.less -------------------------------------------------------------------------------- /src/layout/TagsView/components/TagsList/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './index.less' 3 | 4 | interface ITagsListProps {} 5 | 6 | const TagsList: React.FC = () => { 7 | return
8 | } 9 | 10 | export default TagsList 11 | -------------------------------------------------------------------------------- /src/layout/TagsView/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/variables.module.less'; 2 | .tagsView-container { 3 | height: @layout-header-tags-h; 4 | line-height: @layout-header-tags-h; 5 | white-space: nowrap; 6 | box-shadow: 0 1px 3px 0 rgb(0 0 0 / 8%), 0 0 3px 0 rgb(0 0 0 / 4%); 7 | background: @white; 8 | > div { 9 | height: @layout-header-tags-h; 10 | } 11 | .scrollbar-container { 12 | .tags-wrap { 13 | list-style: none; 14 | padding: 0; 15 | margin: 0; 16 | > li { 17 | display: inline-block; 18 | &:first-child { 19 | margin-left: 15px; 20 | } 21 | span { 22 | cursor: pointer; 23 | } 24 | } 25 | } 26 | } 27 | .contextMenu { 28 | position: absolute; 29 | z-index: 9999; 30 | width: 80px; 31 | margin: 0; 32 | padding: 5px 0; 33 | list-style: none; 34 | font-size: 12px; 35 | color: #333; 36 | font-weight: 400; 37 | background-color: #fff; 38 | border-radius: 4px; 39 | box-shadow: 2px 2px 3px 0 rgb(0 0 0 / 30%); 40 | > li { 41 | margin: 0; 42 | text-align: center; 43 | line-height: 30px; 44 | height: 30px; 45 | cursor: pointer; 46 | &:hover { 47 | background-color: #eee; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/layout/index.less: -------------------------------------------------------------------------------- 1 | @import 'styles/mixins.less'; 2 | .ant-layout.ant-layout-has-sider > .ant-layout { 3 | overflow-x: hidden; 4 | } 5 | -------------------------------------------------------------------------------- /src/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, memo, useEffect, useState} from 'react' 2 | import {Layout, Grid, BackTop} from 'antd' 3 | import Content from './Content' 4 | import Sider from './Sider' 5 | import Header from './Header' 6 | import RightPanel from './RightPanel' 7 | import DrawerSider from './DrawerSider' 8 | import {useAppDispatch, useAppSelector} from '../store' 9 | import {setShowSidebar} from '../store/actions' 10 | import {AppLoading} from 'comps/Loading' 11 | import {treeDataTranslate} from '../utils' 12 | import {findRoutes} from '../api/menu' 13 | import {MenuListProps} from '../config/menuConfig' 14 | const {useBreakpoint} = Grid 15 | import './index.less' 16 | 17 | /** 布局Context */ 18 | interface LayoutContext { 19 | /** 大屏断点 */ 20 | lg: boolean | undefined 21 | /** 是否显示抽屉菜单 */ 22 | drawerVisible: boolean 23 | setDrawerVisible: React.Dispatch> 24 | /** 侧边栏菜单 */ 25 | menus: MenuListProps[] 26 | /** 系统路由 */ 27 | routes: any[] | null 28 | } 29 | 30 | export const LayoutContext = createContext({} as LayoutContext) 31 | 32 | interface IBaseLayoutProps {} 33 | 34 | const BaseLayout: React.FC = () => { 35 | const showSidebar = useAppSelector(state => state.settings.showSidebar) 36 | const intlLoading = useAppSelector(state => state.app.intlLoading) 37 | const [drawerVisible, setDrawerVisible] = useState(true) 38 | const [menus, setMenus] = useState([]) 39 | const [routes, setRoutes] = useState(null) 40 | const {lg} = useBreakpoint() 41 | const dispatch = useAppDispatch() 42 | 43 | useEffect(() => { 44 | dispatch(setShowSidebar(!!lg)) 45 | }, [lg]) 46 | 47 | /** 获取系统路由,菜单 */ 48 | useEffect(() => { 49 | findRoutes().then(res => { 50 | const {data} = res 51 | if (data.code === 20000) { 52 | const list = data.data 53 | const treeMenus: any = treeDataTranslate(list) 54 | setRoutes(list) 55 | setMenus(treeMenus) 56 | } 57 | }) 58 | }, []) 59 | 60 | return ( 61 | <> 62 | {/* 切换语言加载动画 */} 63 | {intlLoading ? : null} 64 | {/* 系统返回顶部组件 */} 65 | document.body} /> 66 | 69 | 70 | {showSidebar ? : null} 71 | {!lg ? : null} 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | ) 81 | } 82 | 83 | export default memo(BaseLayout) 84 | -------------------------------------------------------------------------------- /src/locales/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Cookies from 'js-cookie' 3 | import {IntlProvider} from 'react-intl' 4 | import {ConfigProvider} from 'antd' 5 | import {Language} from 'store/reducers/app' 6 | import en_US from './lang/en_US' 7 | import zh_CN from './lang/zh_CN' 8 | import id_ID from './lang//id_ID' 9 | import moment from 'moment' 10 | // import 'moment/locale/zh-cn' 11 | // import 'moment/locale/id' 12 | 13 | /** 默认语言 */ 14 | export const defaultLang = 'zh' || navigator.language.split('-')[0] 15 | 16 | /** 存储在cookie中locale的键 */ 17 | export const LOCALE_KEY = 'LOCALE' 18 | 19 | /** 获取cookie中的locale */ 20 | export function getLocale() { 21 | return Cookies.get(LOCALE_KEY) as Language 22 | } 23 | 24 | /** 设置locale */ 25 | export function setLocale(locale: Language) { 26 | return Cookies.set(LOCALE_KEY, locale) as Language 27 | } 28 | 29 | /** 删除locale */ 30 | export function removeLocale(): void { 31 | Cookies.remove(LOCALE_KEY) 32 | } 33 | 34 | /** 自定义的国际化语言翻译 */ 35 | const localeMessages: any = { 36 | en: en_US, 37 | zh: zh_CN, 38 | id: id_ID, 39 | } 40 | 41 | /** 封装的国际化组件属性定义 */ 42 | export interface IIntlProProps { 43 | locale: Language 44 | children: React.ReactNode 45 | } 46 | 47 | class IntlPro extends React.Component { 48 | static defaultProps = { 49 | locale: defaultLang, 50 | } 51 | constructor(props: any) { 52 | super(props) 53 | } 54 | render() { 55 | const {children, locale} = this.props 56 | const currLocale = getLocale() || locale 57 | const {antdLocale, momentName, momentLocale, ...message} = localeMessages[ 58 | currLocale 59 | ] 60 | 61 | moment.updateLocale(momentName, momentLocale) 62 | 63 | return ( 64 | 70 | {children} 71 | 72 | ) 73 | } 74 | } 75 | 76 | export default IntlPro 77 | -------------------------------------------------------------------------------- /src/locales/lang/en_US.ts: -------------------------------------------------------------------------------- 1 | import antdEnUS from 'antd/lib/locale/en_US' 2 | // @ts-ignore 3 | import momentEU from 'moment/locale/eu' 4 | 5 | const components = { 6 | antdLocale: antdEnUS, 7 | momentName: 'en', 8 | momentLocale: momentEU, 9 | } 10 | 11 | export default { 12 | hello: 'hello world', 13 | ...components, 14 | } 15 | -------------------------------------------------------------------------------- /src/locales/lang/id_ID.ts: -------------------------------------------------------------------------------- 1 | import antdIdID from 'antd/lib/locale/id_ID' 2 | // @ts-ignore 3 | import momentID from 'moment/locale/id' 4 | 5 | const components = { 6 | antdLocale: antdIdID, 7 | momentName: 'id', 8 | momentLocale: momentID, 9 | } 10 | 11 | export default { 12 | hello: 'Selamat datang di Genie', 13 | ...components, 14 | } 15 | -------------------------------------------------------------------------------- /src/locales/lang/zh_CN.ts: -------------------------------------------------------------------------------- 1 | import antdZhCN from 'antd/lib/locale/zh_CN' 2 | // @ts-ignore 3 | import momentCN from 'moment/locale/zh-cn' 4 | 5 | export const components = { 6 | antdLocale: antdZhCN, 7 | momentName: 'zh-cn', 8 | momentLocale: momentCN, 9 | } 10 | 11 | export default { 12 | hello: '你好', 13 | ...components, 14 | } 15 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import {Provider} from 'react-redux' 5 | import store from 'store/index' 6 | import 'styles/index.less' 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ) 14 | -------------------------------------------------------------------------------- /src/router/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {Suspense} from 'react' 2 | import {Redirect, Route, Switch, HashRouter} from 'react-router-dom' 3 | import {Login} from 'views/index' 4 | import Layout from '@/layout' 5 | import {connect} from 'react-redux' 6 | import {bindActionCreators, Dispatch} from 'redux' 7 | import {StoreStateProps} from 'store/reducers' 8 | import {getUserinfo, toggleAppEnterLoading} from 'store/actions' 9 | import {AppLoading} from 'comps/Loading' 10 | 11 | type IRouterProps = IProps 12 | 13 | const Router: React.FC = props => { 14 | const { 15 | token, 16 | roleNames, 17 | getUserinfo, 18 | appEnterLoading, 19 | toggleAppEnterLoading, 20 | } = props 21 | 22 | return ( 23 | 24 | }> 25 | 26 | 27 | { 30 | // 不存在token 31 | if (!token) { 32 | return 33 | } 34 | 35 | // 未登录,或者没有权限 36 | if (!roleNames) { 37 | toggleAppEnterLoading(true) 38 | ;(getUserinfo() as any) 39 | .then(() => { 40 | return 41 | }) 42 | .catch(() => { 43 | props.history.push('/login') 44 | }) 45 | .finally(() => { 46 | toggleAppEnterLoading(false) 47 | }) 48 | } else { 49 | // 用户已登录,并且具有权限,进入系统 50 | return 51 | } 52 | 53 | // 应用初始时的加载动画 54 | if (appEnterLoading) { 55 | return 56 | } 57 | }} 58 | /> 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | // 映射state到组件的props上 66 | const mapStateToProps = (state: StoreStateProps) => ({ 67 | token: state.user.token, 68 | roleNames: state.user.roleNames, 69 | appEnterLoading: state.app.appEnterLoading, 70 | }) 71 | 72 | // 映射dispatch到组件的props上 73 | const mapDispatchToProps = (dispatch: Dispatch) => 74 | bindActionCreators( 75 | { 76 | getUserinfo, 77 | toggleAppEnterLoading, 78 | }, 79 | dispatch, 80 | ) 81 | 82 | type IProps = ReturnType & 83 | ReturnType 84 | 85 | export default connect(mapStateToProps, mapDispatchToProps)(React.memo(Router)) 86 | -------------------------------------------------------------------------------- /src/store/actions/app.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {Language} from '../reducers/app' 3 | import {Dispatch} from 'redux' 4 | 5 | export interface ISidebarCollapsedToggleAction { 6 | type: types.SIDEBAR_COLLAPSED_TOGGLE_TYPE 7 | } 8 | 9 | export interface IShowRightPanelAction { 10 | type: types.SHOW_RIGHT_PANEL_TOGGLE_TYPE 11 | } 12 | 13 | export interface IToggleLangAction { 14 | type: types.TOGGLE_LANG_TYPE 15 | payload: Language 16 | } 17 | 18 | export interface IToggleAppEnterLoadingAction { 19 | type: types.APP_ENTER_LOADING_TYPE 20 | payload: boolean 21 | } 22 | 23 | export interface IIntlLoadingToggleAction { 24 | type: types.INTL_LOADING_TOGGLE_TYPE 25 | } 26 | 27 | export const sidebarCollapsedToggle = (): ISidebarCollapsedToggleAction => { 28 | return { 29 | type: types.SIDEBAR_COLLAPSED_TOGGLE, 30 | } 31 | } 32 | 33 | export const showRightPanelToggle = (): IShowRightPanelAction => { 34 | return { 35 | type: types.SHOW_RIGHT_PANEL_TOGGLE, 36 | } 37 | } 38 | 39 | export const toggleLang = (payload: Language): IToggleLangAction => { 40 | return { 41 | type: types.TOGGLE_LANG, 42 | payload, 43 | } 44 | } 45 | 46 | export const toggleAppEnterLoading = ( 47 | payload: boolean, 48 | ): IToggleAppEnterLoadingAction => { 49 | return { 50 | type: types.APP_ENTER_LOADING, 51 | payload, 52 | } 53 | } 54 | 55 | export const intlLoadingToggle = (): IIntlLoadingToggleAction => { 56 | return { 57 | type: types.INTL_LOADING_TOGGLE, 58 | } 59 | } 60 | 61 | export const asyncIntlLoadingToggle = () => ( 62 | dispatch: Dispatch, 63 | ): Promise => { 64 | return new Promise(resolve => { 65 | dispatch(intlLoadingToggle()) 66 | setTimeout(() => { 67 | resolve(true) 68 | }, 600) 69 | }) 70 | } 71 | 72 | export type AppAction = 73 | | ISidebarCollapsedToggleAction 74 | | IShowRightPanelAction 75 | | IToggleLangAction 76 | | IToggleAppEnterLoadingAction 77 | | IIntlLoadingToggleAction 78 | -------------------------------------------------------------------------------- /src/store/actions/auth.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {Dispatch} from 'redux' 3 | import {resetUser, setUserinfo, setUserToken} from './user' 4 | import {reqLogin, reqLogout} from 'apis/login' 5 | import {HttpStatusCode, ResponseData} from 'mock/index' 6 | import {setToken, removeToken} from 'utils/auth' 7 | 8 | export interface LoginAction { 9 | type: types.AUTH_LOGIN_TYPE 10 | } 11 | 12 | export interface LogoutAction { 13 | type: types.AUTH_LOGOUT_TYPE 14 | } 15 | 16 | export type AsyncLoginAction = ReturnType 17 | 18 | export type AsyncLogoutAction = ReturnType 19 | 20 | export const login = (username: string, password: string) => ( 21 | dispatch: Dispatch, 22 | ): Promise => { 23 | return new Promise((resolve, reject) => { 24 | reqLogin({username, password}) 25 | .then(response => { 26 | const data = response.data 27 | if (data.code === HttpStatusCode.OK) { 28 | const {token, userInfo} = data.data 29 | const { 30 | _id, 31 | roleIdList, 32 | roles, 33 | type, 34 | createdAt, 35 | updatedAt, 36 | __v, 37 | ...restProps 38 | } = userInfo 39 | dispatch(setUserToken(token)) 40 | dispatch(setUserinfo(restProps)) 41 | setToken(token) 42 | resolve(data) 43 | } else { 44 | reject(data.message) 45 | } 46 | }) 47 | .catch(error => { 48 | reject(error) 49 | }) 50 | }) 51 | } 52 | 53 | export const logout = () => (dispatch: Dispatch): Promise => { 54 | return new Promise((resolve, reject) => { 55 | reqLogout() 56 | .then(response => { 57 | const data = response.data as ResponseData 58 | if (data.code === 20000) { 59 | dispatch(resetUser()) 60 | removeToken() 61 | resolve(data) 62 | } else { 63 | reject(data.message) 64 | } 65 | }) 66 | .catch(error => { 67 | reject(error) 68 | }) 69 | }) 70 | } 71 | 72 | export type AuthAction = LoginAction | LogoutAction 73 | -------------------------------------------------------------------------------- /src/store/actions/index.ts: -------------------------------------------------------------------------------- 1 | import {Types} from '../action-types' 2 | import {Dispatch} from 'react' 3 | import {Action, AnyAction} from 'redux' 4 | import { 5 | sidebarCollapsedToggle, 6 | showRightPanelToggle, 7 | AppAction, 8 | toggleLang, 9 | toggleAppEnterLoading, 10 | asyncIntlLoadingToggle, 11 | intlLoadingToggle, 12 | } from './app' 13 | import { 14 | fixHeaderToggle, 15 | fixSidebarToggle, 16 | showLogoToggle, 17 | openTagsViewToggle, 18 | SettingsAction, 19 | setShowSidebar, 20 | breadcrumbToggle, 21 | setCollapsedMenuBtnPosition, 22 | weekModeToggle, 23 | grayModeToggle, 24 | } from './settings' 25 | import {setUserToken, UserAction, getUserinfo, getRoutes} from './user' 26 | import {login, logout, AuthAction} from './auth' 27 | import { 28 | addTagsView, 29 | removeTagsView, 30 | closeAllTagsView, 31 | closeOthersTagView, 32 | TagsViewActions, 33 | } from './tagsView' 34 | 35 | /** 自定义通用同步Action */ 36 | export interface CommonAction { 37 | type: Types 38 | payload?: T 39 | } 40 | 41 | /** 自定义通用同步ActionCreator */ 42 | export interface CommonActionCreator { 43 | (options?: T): CommonAction 44 | } 45 | 46 | /** 自定义通用异步ActionCreator*/ 47 | export interface CommonAsyncActionCreator< 48 | T = any, 49 | R = any, 50 | A extends Action = AnyAction 51 | > { 52 | (options?: T): (dispatch: Dispatch) => R 53 | } 54 | 55 | /** 应用ActionCreator */ 56 | export interface RootActionsCreator { 57 | (): RootActions 58 | } 59 | 60 | /** 应用Actions */ 61 | export type RootActions = 62 | | AppAction 63 | | SettingsAction 64 | | UserAction 65 | | AuthAction 66 | | TagsViewActions 67 | 68 | export { 69 | setUserToken, 70 | sidebarCollapsedToggle, 71 | showLogoToggle, 72 | showRightPanelToggle, 73 | fixHeaderToggle, 74 | fixSidebarToggle, 75 | openTagsViewToggle, 76 | login, 77 | logout, 78 | getUserinfo, 79 | addTagsView, 80 | removeTagsView, 81 | closeOthersTagView, 82 | closeAllTagsView, 83 | toggleLang, 84 | toggleAppEnterLoading, 85 | setShowSidebar, 86 | breadcrumbToggle, 87 | setCollapsedMenuBtnPosition, 88 | weekModeToggle, 89 | grayModeToggle, 90 | asyncIntlLoadingToggle, 91 | intlLoadingToggle, 92 | getRoutes, 93 | } 94 | -------------------------------------------------------------------------------- /src/store/actions/settings.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {CollapsedMenuBtnPosition} from 'root/src/defaultSetting' 3 | 4 | export interface IShowLogoAction { 5 | type: types.SHOW_LOGO_TOGGLE_TYPE 6 | } 7 | 8 | export interface IFixHeaderAction { 9 | type: types.FIX_HEADER_TOGGLE_TYPE 10 | } 11 | 12 | export interface IOpenTagsViewAction { 13 | type: types.OPEN_TAGS_VIEW_TYPE 14 | } 15 | 16 | export interface ISetShowSidebarAction { 17 | type: types.SET_SHOW_SIDEBAR_TYPE 18 | payload: boolean 19 | } 20 | 21 | export interface IToggleBreadcrumbAction { 22 | type: types.BREADCRUMB_TOGGLE_TYPE 23 | } 24 | 25 | export interface IFixSidebarAction { 26 | type: types.FIX_SIDEBAR_TOGGLE_TYPE 27 | } 28 | 29 | export interface ISetCollapsedMenuBtnPositionAction { 30 | type: types.COLLAPSED_MENU_BTN_POSITION_TYPE 31 | payload: CollapsedMenuBtnPosition 32 | } 33 | 34 | export interface IWeekModeToggleAction { 35 | type: types.WEEK_MODE_TOGGLE_TYPE 36 | } 37 | 38 | export interface IGrayModeToggleAction { 39 | type: types.GRAY_MODE_TOGGLE_TYPE 40 | } 41 | 42 | export const showLogoToggle = (): IShowLogoAction => { 43 | return { 44 | type: types.SHOW_LOGO_TOGGLE, 45 | } 46 | } 47 | 48 | export const fixHeaderToggle = (): IFixHeaderAction => { 49 | return { 50 | type: types.FIX_HEADER_TOGGLE, 51 | } 52 | } 53 | 54 | export const fixSidebarToggle = (): IFixSidebarAction => { 55 | return { 56 | type: types.FIX_SIDEBAR_TOGGLE, 57 | } 58 | } 59 | 60 | export const openTagsViewToggle = (): IOpenTagsViewAction => { 61 | return { 62 | type: types.OPEN_TAGS_VIEW, 63 | } 64 | } 65 | 66 | export const setShowSidebar = (payload: boolean): ISetShowSidebarAction => { 67 | return { 68 | type: types.SET_SHOW_SIDEBAR, 69 | payload, 70 | } 71 | } 72 | 73 | export const breadcrumbToggle = (): IToggleBreadcrumbAction => { 74 | return { 75 | type: types.BREADCRUMB_TOGGLE, 76 | } 77 | } 78 | 79 | export const setCollapsedMenuBtnPosition = ( 80 | payload: CollapsedMenuBtnPosition, 81 | ): ISetCollapsedMenuBtnPositionAction => { 82 | return { 83 | type: types.COLLAPSED_MENU_BTN_POSITION, 84 | payload, 85 | } 86 | } 87 | 88 | export const weekModeToggle = (): IWeekModeToggleAction => { 89 | return { 90 | type: types.WEEK_MODE_TOGGLE, 91 | } 92 | } 93 | 94 | export const grayModeToggle = (): IGrayModeToggleAction => { 95 | return { 96 | type: types.GRAY_MODE_TOGGLE, 97 | } 98 | } 99 | 100 | export type SettingsAction = 101 | | IShowLogoAction 102 | | IFixHeaderAction 103 | | IFixSidebarAction 104 | | IOpenTagsViewAction 105 | | ISetShowSidebarAction 106 | | IToggleBreadcrumbAction 107 | | ISetCollapsedMenuBtnPositionAction 108 | | IWeekModeToggleAction 109 | | IGrayModeToggleAction 110 | -------------------------------------------------------------------------------- /src/store/actions/tagsView.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {TagViewProps} from '../reducers/tagsView' 3 | 4 | interface AddTagsViewAction { 5 | type: types.ADD_TAGS_VIEW_TYPE 6 | payload: TagViewProps 7 | } 8 | 9 | interface RemoveTagsViewAction { 10 | type: types.REMOVE_TAGS_VIEW_TYPE 11 | payload: TagViewProps 12 | } 13 | 14 | interface CloseAllTagsViewAction { 15 | type: types.CLOSE_ALL_TAGS_VIEW_TYPE 16 | } 17 | 18 | interface CloseOthersTagViewAction { 19 | type: types.CLOSE_OTHERS_TAG_VIEW_TYPE 20 | payload: TagViewProps 21 | } 22 | 23 | export const addTagsView = (payload: TagViewProps): AddTagsViewAction => { 24 | return { 25 | type: types.ADD_TAGS_VIEW, 26 | payload, 27 | } 28 | } 29 | 30 | export const removeTagsView = (payload: TagViewProps): RemoveTagsViewAction => { 31 | return { 32 | type: types.REMOVE_TAGS_VIEW, 33 | payload, 34 | } 35 | } 36 | 37 | export const closeAllTagsView = (): CloseAllTagsViewAction => { 38 | return { 39 | type: types.CLOSE_ALL_TAGS_VIEW, 40 | } 41 | } 42 | 43 | export const closeOthersTagView = ( 44 | payload: TagViewProps, 45 | ): CloseOthersTagViewAction => { 46 | return { 47 | type: types.CLOSE_OTHERS_TAG_VIEW, 48 | payload, 49 | } 50 | } 51 | 52 | export type TagsViewActions = 53 | | AddTagsViewAction 54 | | RemoveTagsViewAction 55 | | CloseAllTagsViewAction 56 | | CloseOthersTagViewAction 57 | -------------------------------------------------------------------------------- /src/store/actions/user.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {getUserInfo} from 'apis/user' 3 | import {findRoutes} from 'apis/menu' 4 | import {Dispatch} from 'redux' 5 | import {ResponseData} from 'mock' 6 | import {UserStateProps} from '../reducers/user' 7 | 8 | export interface ISetTokenAction { 9 | type: types.SET_TOKEN_TYPE 10 | payload: string 11 | } 12 | 13 | export interface IResetUserAction { 14 | type: types.RESET_USER_TYPE 15 | } 16 | 17 | export interface IGetUserinfoAction { 18 | type: types.GET_USERINFO_TYPE 19 | } 20 | 21 | export interface ISetUserinfoAction { 22 | type: types.SET_USERINFO_TYPE 23 | payload: UserStateProps 24 | } 25 | 26 | export interface ISetRoutesAction { 27 | type: types.SET_ROUTES_TYPE 28 | payload: any[] 29 | } 30 | 31 | export const setUserToken = (payload: string): ISetTokenAction => { 32 | return { 33 | type: types.SET_TOKEN, 34 | payload, 35 | } 36 | } 37 | 38 | export const resetUser = (): IResetUserAction => { 39 | return { 40 | type: types.RESET_USER, 41 | } 42 | } 43 | 44 | export const setUserinfo = (userinfo: UserStateProps): ISetUserinfoAction => { 45 | return { 46 | type: types.SET_USERINFO, 47 | payload: userinfo, 48 | } 49 | } 50 | 51 | export const setRoutes = (routes: any[]): ISetRoutesAction => { 52 | return { 53 | type: types.SET_ROUTES, 54 | payload: routes, 55 | } 56 | } 57 | 58 | export const getUserinfo = () => (dispatch: Dispatch): Promise => { 59 | return new Promise((resolve, reject) => { 60 | getUserInfo() 61 | .then(response => { 62 | const data = response.data as ResponseData 63 | if (data.code === 20000) { 64 | setTimeout(() => { 65 | const userinfo = data.data 66 | const { 67 | _id, 68 | roleIdList, 69 | roles, 70 | type, 71 | createdAt, 72 | updatedAt, 73 | __v, 74 | ...restProps 75 | } = userinfo 76 | dispatch(setUserinfo(restProps)) 77 | resolve(data) 78 | }, 300) 79 | } else { 80 | reject(data.message) 81 | } 82 | }) 83 | .catch(error => { 84 | reject(error) 85 | }) 86 | }) 87 | } 88 | 89 | export const getRoutes = () => (dispatch: Dispatch): Promise => { 90 | return new Promise((resolve, reject) => { 91 | findRoutes() 92 | .then(response => { 93 | const data = response.data as ResponseData 94 | if (data.code === 20000) { 95 | const routes = data.data 96 | dispatch(setRoutes(routes)) 97 | resolve(data) 98 | } else { 99 | reject(data.message) 100 | } 101 | }) 102 | .catch(error => { 103 | reject(error) 104 | }) 105 | }) 106 | } 107 | 108 | export type UserAction = 109 | | ISetTokenAction 110 | | IResetUserAction 111 | | IGetUserinfoAction 112 | | ISetUserinfoAction 113 | | ISetRoutesAction 114 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import {useSelector, TypedUseSelectorHook, useDispatch} from 'react-redux' 2 | import { 3 | createStore, 4 | applyMiddleware, 5 | StoreEnhancer, 6 | StoreEnhancerStoreCreator, 7 | Store, 8 | } from 'redux' 9 | import thunk from 'redux-thunk' 10 | import reducer, {StoreActions, StoreStateProps} from './reducers' 11 | 12 | /** App应用的Dispatch类型 */ 13 | export type AppDispatch = typeof store.dispatch 14 | 15 | /** App应用的useSelector */ 16 | export const useAppSelector: TypedUseSelectorHook = useSelector 17 | 18 | /** App应用的useDispatch */ 19 | export const useAppDispatch = () => useDispatch() 20 | 21 | const storeEnhancer: StoreEnhancer = applyMiddleware(thunk) 22 | const storeEnhancerStoreCreator: StoreEnhancerStoreCreator = storeEnhancer( 23 | createStore, 24 | ) 25 | 26 | const store: Store = storeEnhancerStoreCreator( 27 | reducer, 28 | ) 29 | 30 | export default store 31 | -------------------------------------------------------------------------------- /src/store/reducers/app.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {RootActions} from '../actions' 3 | import {getLocale, setLocale} from '@/locales' 4 | import defaultSettings from '@/defaultSetting' 5 | 6 | export type Language = 'zh' | 'en' | 'id' 7 | 8 | export interface AppStateProps { 9 | sidebarCollapsed: boolean 10 | showRightPanel: boolean 11 | lang: Language 12 | appEnterLoading: boolean 13 | intlLoading: boolean 14 | } 15 | 16 | const initialState: AppStateProps = { 17 | sidebarCollapsed: defaultSettings.sidebarCollapsed, 18 | showRightPanel: false, 19 | lang: getLocale() || defaultSettings.lang, 20 | appEnterLoading: true, 21 | intlLoading: false, 22 | } 23 | 24 | export default (state = initialState, action: RootActions): AppStateProps => { 25 | switch (action.type) { 26 | case types.SIDEBAR_COLLAPSED_TOGGLE: 27 | return { 28 | ...state, 29 | sidebarCollapsed: !state.sidebarCollapsed, 30 | } 31 | case types.SHOW_RIGHT_PANEL_TOGGLE: 32 | return { 33 | ...state, 34 | showRightPanel: !state.showRightPanel, 35 | } 36 | case types.TOGGLE_LANG: 37 | setLocale(action.payload) 38 | return { 39 | ...state, 40 | lang: action.payload, 41 | } 42 | case types.APP_ENTER_LOADING: 43 | return { 44 | ...state, 45 | appEnterLoading: action.payload, 46 | } 47 | case types.INTL_LOADING_TOGGLE: 48 | return { 49 | ...state, 50 | intlLoading: !state.intlLoading, 51 | } 52 | default: 53 | return state 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import {AnyAction, combineReducers, ReducersMapObject, Reducer} from 'redux' 2 | import app, {AppStateProps} from './app' 3 | import user, {UserStateProps} from './user' 4 | import settings, {SettingsStateProps} from './settings' 5 | import tagsView, {TagsViewStateProps} from './tagsView' 6 | import {RootActions} from '../actions' 7 | 8 | export type StoreActions = AnyAction | RootActions 9 | 10 | export interface StoreStateProps { 11 | app: AppStateProps 12 | user: UserStateProps 13 | settings: SettingsStateProps 14 | tagsView: TagsViewStateProps 15 | } 16 | 17 | const reducers: ReducersMapObject = { 18 | app, 19 | user, 20 | settings, 21 | tagsView, 22 | } 23 | 24 | const reducer: Reducer = combineReducers( 25 | reducers, 26 | ) 27 | 28 | export default reducer 29 | -------------------------------------------------------------------------------- /src/store/reducers/settings.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {RootActions} from '../actions' 3 | import defaultSettings, {CollapsedMenuBtnPosition} from '@/defaultSetting' 4 | 5 | export interface SettingsStateProps { 6 | showLogo: boolean 7 | fixHeader: boolean 8 | fixSidebar: boolean 9 | openTagsView: boolean 10 | showSidebar: boolean 11 | showBreadcrumb: boolean 12 | collapsedMenuBtnPosition: CollapsedMenuBtnPosition 13 | weekMode: boolean 14 | grayMode: boolean 15 | } 16 | 17 | const initialState: SettingsStateProps = { 18 | showLogo: defaultSettings.showLogo, 19 | fixHeader: defaultSettings.fixHeader, 20 | fixSidebar: defaultSettings.fixSidebar, 21 | openTagsView: defaultSettings.openTagsView, 22 | showSidebar: defaultSettings.showSidebar, 23 | showBreadcrumb: defaultSettings.showBreadcrumb, 24 | collapsedMenuBtnPosition: defaultSettings.collapsedMenuBtnPosition, 25 | weekMode: defaultSettings.weekMode, 26 | grayMode: defaultSettings.weekMode, 27 | } 28 | 29 | export default ( 30 | state = initialState, 31 | action: RootActions, 32 | ): SettingsStateProps => { 33 | switch (action.type) { 34 | case types.SHOW_LOGO_TOGGLE: 35 | return { 36 | ...state, 37 | showLogo: !state.showLogo, 38 | } 39 | case types.FIX_HEADER_TOGGLE: 40 | return { 41 | ...state, 42 | fixHeader: !state.fixHeader, 43 | } 44 | case types.FIX_SIDEBAR_TOGGLE: 45 | return { 46 | ...state, 47 | fixSidebar: !state.fixSidebar, 48 | } 49 | case types.OPEN_TAGS_VIEW: 50 | return { 51 | ...state, 52 | openTagsView: !state.openTagsView, 53 | } 54 | case types.SET_SHOW_SIDEBAR: 55 | return { 56 | ...state, 57 | showSidebar: action.payload, 58 | } 59 | case types.BREADCRUMB_TOGGLE: 60 | return { 61 | ...state, 62 | showBreadcrumb: !state.showBreadcrumb, 63 | } 64 | case types.COLLAPSED_MENU_BTN_POSITION: 65 | return { 66 | ...state, 67 | collapsedMenuBtnPosition: action.payload, 68 | } 69 | case types.WEEK_MODE_TOGGLE: 70 | return { 71 | ...state, 72 | weekMode: !state.weekMode, 73 | } 74 | case types.GRAY_MODE_TOGGLE: 75 | return { 76 | ...state, 77 | grayMode: !state.grayMode, 78 | } 79 | default: 80 | return state 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/store/reducers/tagsView.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {RootActions} from '../actions' 3 | 4 | export interface TagViewProps { 5 | title: string 6 | path: string 7 | } 8 | 9 | export interface TagsViewStateProps { 10 | tagsList: Array 11 | } 12 | 13 | const initialState: TagsViewStateProps = { 14 | tagsList: [], 15 | } 16 | 17 | export default ( 18 | state = initialState, 19 | action: RootActions, 20 | ): TagsViewStateProps => { 21 | let currentTagView: TagViewProps 22 | let hasTagView: TagViewProps | undefined 23 | switch (action.type) { 24 | case types.ADD_TAGS_VIEW: 25 | currentTagView = action.payload 26 | hasTagView = state.tagsList.find(tag => tag.path === currentTagView.path) 27 | return { 28 | ...state, 29 | tagsList: hasTagView 30 | ? [...state.tagsList] 31 | : [...state.tagsList, currentTagView], 32 | } 33 | case types.REMOVE_TAGS_VIEW: 34 | currentTagView = action.payload 35 | return { 36 | ...state, 37 | tagsList: state.tagsList.filter( 38 | tagView => 39 | tagView.title !== currentTagView.title && 40 | tagView.path !== currentTagView.path, 41 | ), 42 | } 43 | case types.CLOSE_ALL_TAGS_VIEW: 44 | return { 45 | ...state, 46 | tagsList: [], 47 | } 48 | case types.CLOSE_OTHERS_TAG_VIEW: 49 | currentTagView = action.payload 50 | return { 51 | ...state, 52 | tagsList: state.tagsList.filter( 53 | tagView => 54 | tagView.path === '/dashboard' || 55 | tagView.path === currentTagView.path, 56 | ), 57 | } 58 | default: 59 | return state 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/store/reducers/user.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../action-types' 2 | import {getToken} from 'utils/auth' 3 | import {RootActions} from '../actions' 4 | 5 | export interface UserStateProps { 6 | username: string 7 | email?: string 8 | mobile?: string 9 | nickname?: string 10 | avatar: string 11 | sex?: number 12 | token?: string 13 | roleNames: string[] | null 14 | permissions: string[] 15 | routes: any[] 16 | remark?: string 17 | status?: number 18 | } 19 | 20 | const initialState: UserStateProps = { 21 | username: 'admin', 22 | avatar: 'https://s1.ax1x.com/2020/04/28/J5hUaT.jpg', 23 | token: getToken() || 'admin-token', 24 | roleNames: null, 25 | permissions: [], 26 | routes: [], 27 | } 28 | 29 | export default (state = initialState, action: RootActions): UserStateProps => { 30 | switch (action.type) { 31 | case types.SET_TOKEN: 32 | return { 33 | ...state, 34 | token: action.payload, 35 | } 36 | case types.RESET_USER: 37 | return { 38 | ...state, 39 | username: '', 40 | token: '', 41 | avatar: '', 42 | roleNames: [], 43 | } 44 | case types.SET_USERINFO: 45 | return { 46 | ...state, 47 | ...action.payload, 48 | } 49 | case types.SET_ROUTES: 50 | return { 51 | ...state, 52 | routes: action.payload, 53 | } 54 | default: 55 | return state 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import './variables.module.less'; 2 | @import './transition.less'; 3 | @import './utilities.less'; 4 | @import './mixins.less'; 5 | html, 6 | body { 7 | width: 100% !important; 8 | height: 100% !important; 9 | overflow: visible !important; 10 | overflow-x: hidden !important; 11 | } 12 | 13 | body { 14 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 15 | 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 16 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 17 | font-variant: tabular-nums; 18 | line-height: 1.5715; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | color: @primary-text; 22 | margin: 0; 23 | box-sizing: border-box; 24 | .scrollbar(); 25 | } 26 | 27 | #root { 28 | width: 100%; 29 | height: 100%; 30 | } 31 | 32 | code { 33 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 34 | monospace; 35 | } 36 | 37 | .app-container { 38 | background-color: #f0f2f5; 39 | position: relative; 40 | padding: 20px; 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/mixins.less: -------------------------------------------------------------------------------- 1 | // 美化滚动条 2 | .scrollbar( 3 | @thumbBg: #c8c9cB, 4 | @trackBg: #e6e6e6, 5 | @scrollWidth: 7px, 6 | @scrollHeight: 7px, 7 | ) { 8 | //滚动条整体部分 9 | &::-webkit-scrollbar { 10 | width: @scrollWidth; //y轴滚动条粗细 11 | height: @scrollHeight; //x轴滚动条粗细 12 | } 13 | 14 | //滚动条里面的小方块,能上下左右移动(取决于是垂直滚动条还是水平滚动条) 15 | &::-webkit-scrollbar-thumb { 16 | border-radius: 4px; 17 | background: @thumbBg; 18 | margin-bottom: 10px; 19 | } 20 | // 鼠标hover上去的小方块颜色 21 | &::-webkit-scrollbar-thumb { 22 | &:hover { 23 | background: darken(#c8c9cb, 10%); 24 | } 25 | } 26 | //滚动条的轨道(里面装有thumb)滚动槽 27 | &::-webkit-scrollbar-track { 28 | border-radius: 0; 29 | background: @trackBg; //滚动槽背景色 30 | } 31 | } 32 | 33 | // 全局色弱 34 | .week-mode { 35 | filter: invert(80%); 36 | } 37 | 38 | // 灰度模式 39 | .gray-mode { 40 | filter: grayscale(100%); 41 | } 42 | 43 | // mixins for clearfix 44 | // ------------------------ 45 | .clearfix() { 46 | zoom: 1; 47 | &::before, 48 | &::after { 49 | display: table; 50 | content: ' '; 51 | } 52 | &::after { 53 | clear: both; 54 | height: 0; 55 | font-size: 0; 56 | visibility: hidden; 57 | } 58 | } 59 | 60 | // 单行溢出打点 61 | .textOverflow { 62 | overflow: hidden; 63 | white-space: nowrap; 64 | text-overflow: ellipsis; 65 | word-break: break-all; 66 | } 67 | 68 | // 多行溢出打点 69 | .textOverflowMulti(@line: 3, @bg: #fff) { 70 | position: relative; 71 | max-height: @line * 1.5em; 72 | margin-right: -1em; 73 | padding-right: 1em; 74 | overflow: hidden; 75 | line-height: 1.5em; 76 | text-align: justify; 77 | &::before { 78 | position: absolute; 79 | right: 14px; 80 | bottom: 0; 81 | padding: 0 1px; 82 | background: @bg; 83 | content: '...'; 84 | } 85 | &::after { 86 | position: absolute; 87 | right: 14px; 88 | width: 1em; 89 | height: 1em; 90 | margin-top: 0.2em; 91 | background: white; 92 | content: ''; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/styles/transition.less: -------------------------------------------------------------------------------- 1 | // 路由转场动画 2 | .fadeInLeft-enter { 3 | opacity: 0; 4 | transform: translateX(-30px); 5 | } 6 | 7 | .fadeInLeft-enter-active, 8 | .fadeInLeft-exit-active { 9 | transition: all 0.5s ease-out; 10 | opacity: 1; 11 | transform: translateX(0); 12 | } 13 | 14 | .fadeInLeft-exit { 15 | opacity: 0; 16 | transform: translateX(30px); 17 | } 18 | 19 | // 旋转动画 20 | @keyframes sidebar-logo-spin { 21 | from { 22 | transform-origin: center; 23 | transform: rotate(0); 24 | } 25 | to { 26 | transform-origin: center; 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | // 向里面缩放 32 | .scaleIn-enter { 33 | transform: scale(0.8); 34 | opacity: 0; 35 | } 36 | 37 | .scaleIn-enter-active { 38 | transition: all 0.1s ease-out; 39 | opacity: 1; 40 | transform: scale(1); 41 | } 42 | 43 | .scaleIn-exit-active { 44 | transition: all 0.2s ease-out; 45 | opacity: 1; 46 | transform: scale(1); 47 | } 48 | 49 | .scaleIn-exit { 50 | opacity: 0; 51 | transform: scale(0); 52 | } 53 | 54 | // 淡入 55 | .fadeIn-enter { 56 | opacity: 0; 57 | } 58 | 59 | .fadeIn-enter-active, 60 | .fadeIn-exit-active { 61 | transition: all 0.5s ease-out; 62 | opacity: 1; 63 | } 64 | 65 | .fadeIn-exit { 66 | opacity: 0; 67 | } 68 | -------------------------------------------------------------------------------- /src/styles/variables.module.less: -------------------------------------------------------------------------------- 1 | // 中性色 2 | @white: #ffffff; 3 | @gray-1: #f6f6f6; 4 | @black: #000000; 5 | 6 | // 自然色彩 (红、橙、黄、绿、蓝、淀、紫) 7 | @red: #f56c6c; 8 | @green: #85ce61; 9 | @blue: #409eff; 10 | @yellow: #e6a23c; 11 | @gray: #909399; 12 | 13 | // 品牌色 14 | @primary: #0960bd; 15 | 16 | // 中性色,用于文本,背景,边框,分割线 17 | @title: rgba(@black, 0.85); 18 | @primary-text: rgba(@black, 0.85); 19 | @secondary: rgba(@black, 0.45); 20 | @disable: rgba(@black, 0.25); 21 | @border: rgba(@black, 0.15); 22 | @dividers: rgba(@black, 0.06); 23 | @background: rgba(@black, 0.04); 24 | @table-header: rgba(@black, 0.02); 25 | 26 | // 布局变量 27 | @layout-sidebar-w: 200px; 28 | @layout-sidebar-collapsed-w: 48px; 29 | 30 | @layout-header-h: 80px; 31 | @layout-header-top-h: 48px; 32 | @layout-header-tags-h: 32px; 33 | @layout-header-zIndex: 999; 34 | @layout-fix-header-zIndex: 1000; 35 | @layout-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 36 | 37 | // 文本颜色 38 | @text-color-1: rgba(@black, 0.65); 39 | 40 | // 导出的颜色变量 41 | :export { 42 | white: @white; 43 | gray-1: @gray-1; 44 | black: @black; 45 | 46 | red: @red; 47 | 48 | primary: @primary; 49 | secondary: @secondary; 50 | 51 | // 布局 52 | layout-sidebar-w: @layout-sidebar-w; 53 | layout-sidebar-collapsed-w: @layout-sidebar-collapsed-w; 54 | layout-header-h: @layout-header-h; 55 | layout-header-top-h: @layout-header-top-h; 56 | layout-header-tags-h: @layout-header-tags-h; 57 | layout-header-zIndex: @layout-header-zIndex; 58 | layout-fix-header-zIndex: @layout-fix-header-zIndex; 59 | layout-header-box-shadow: @layout-header-box-shadow; 60 | 61 | // 文本 62 | primary-text: @primary-text; 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | export const TOKEN = 'TOKEN' 4 | 5 | export function setToken(token: string): string | undefined { 6 | return Cookies.set(TOKEN, token) 7 | } 8 | 9 | export function removeToken(): void { 10 | return Cookies.remove(TOKEN) 11 | } 12 | 13 | export function getToken(): string | undefined { 14 | return Cookies.get(TOKEN) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | import {message} from 'antd' 2 | import ClipboardJS from 'clipboard' 3 | 4 | function clipboardSuccess() { 5 | message.success('复制成功') 6 | } 7 | 8 | function clipboardError() { 9 | message.error('复制失败') 10 | } 11 | 12 | interface ClipboardProps { 13 | onClick?: (e: MouseEvent) => void 14 | } 15 | 16 | export default function handleClipboard(text: string, event: any): void { 17 | const clipboard: ClipboardJS & ClipboardProps = new ClipboardJS( 18 | event.target, 19 | { 20 | text: () => text, 21 | }, 22 | ) 23 | clipboard.on('success', () => { 24 | clipboardSuccess() 25 | clipboard.destroy() 26 | }) 27 | clipboard.on('error', () => { 28 | clipboardError() 29 | clipboard.destroy() 30 | }) 31 | clipboard.onClick && clipboard.onClick(event) 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios' 2 | import {message, Modal} from 'antd' 3 | import store from 'store/index' 4 | import {logout} from 'store/actions' 5 | import config, {EnvName} from 'root/config' 6 | 7 | function dispatchLogout() { 8 | Modal.confirm({ 9 | title: '确认登出?', 10 | content: '你已被登出,可以取消继续留在该页面,或者重新登录', 11 | okText: '重新登录', 12 | cancelText: '取消', 13 | onOk: () => { 14 | // 清除登录信息 15 | store.dispatch(logout()) 16 | }, 17 | onCancel: () => { 18 | console.log('Cancel') 19 | }, 20 | }) 21 | } 22 | 23 | /** 环境变量 */ 24 | const MODE = import.meta.env.MODE as EnvName 25 | 26 | const base = config[MODE] 27 | 28 | const defaultUrl = 29 | MODE === 'development' 30 | ? 'http://localhost:5001' 31 | : process.env.VITE_APP_API_URL 32 | 33 | // 创建axios的实例 34 | const service = axios.create({ 35 | baseURL: base ? base.apiBaseUrl : defaultUrl, 36 | timeout: 5000, 37 | }) 38 | 39 | // 请求拦截器 40 | service.interceptors.request.use( 41 | config => { 42 | // 请求携带凭证 43 | const token = store.getState().user.token 44 | if (token) { 45 | config.headers.Authorization = 'Bearer ' + token 46 | } 47 | return config 48 | }, 49 | error => Promise.reject(error), 50 | ) 51 | 52 | // 响应拦截器 53 | service.interceptors.response.use( 54 | response => { 55 | const res = response.data 56 | 57 | if (res.code !== 20000) { 58 | message.error(res.message || 'Error', 5) 59 | // 50008:非法token,50012:其他客户端登录,50014:token失效了 60 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 61 | // 尝试重新登录 62 | dispatchLogout() 63 | } 64 | return Promise.reject(res.message || 'Error') 65 | } else { 66 | return response 67 | } 68 | }, 69 | (error: AxiosError) => { 70 | const response = error.response 71 | const data = response && response.data 72 | if (response) { 73 | switch (response.status) { 74 | case 400: 75 | error.message = data.message || '错误请求' 76 | break 77 | case 401: 78 | error.message = data.message || 'token失效,请重新登录' 79 | break 80 | case 403: 81 | error.message = data.message || '非法token,拒绝访问' 82 | dispatchLogout() 83 | break 84 | case 404: 85 | error.message = data.message || '请求错误,资源找不到了' 86 | break 87 | case 408: 88 | error.message = data.message || '请求超时' 89 | break 90 | case 500: 91 | error.message = data.message || '服务器错误' 92 | break 93 | default: 94 | error.message = data.message || '连接错误' 95 | } 96 | } else { 97 | if (!window.navigator.onLine) { 98 | error.message = '网络中断' 99 | } 100 | } 101 | message.error(error.message) 102 | return Promise.reject(error) 103 | }, 104 | ) 105 | 106 | /** 封装request请求方法 */ 107 | function request(config: AxiosRequestConfig) { 108 | return service.request>(config) 109 | } 110 | 111 | export default request 112 | -------------------------------------------------------------------------------- /src/utils/typing.ts: -------------------------------------------------------------------------------- 1 | export interface TypingOptions { 2 | source: HTMLElement 3 | output: HTMLElement 4 | delay: number 5 | done?: () => void 6 | } 7 | 8 | export interface TypingStatic { 9 | new (opts: TypingOptions): TypingClass 10 | } 11 | 12 | export interface TypingClass { 13 | opts: TypingOptions 14 | init(): void 15 | convert(dom: HTMLElement, arr: Array): Array 16 | print(dom: HTMLElement, val: any, callback: () => void): void 17 | play(ele: HTMLElement): void 18 | start(): void 19 | } 20 | 21 | interface ChainProps { 22 | parent: ChainProps | null 23 | dom: HTMLElement 24 | val: any[] 25 | } 26 | 27 | class Typing { 28 | private opts: TypingOptions 29 | source: HTMLElement 30 | output: HTMLElement 31 | delay: number 32 | chain: ChainProps 33 | 34 | constructor(opts: TypingOptions) { 35 | this.opts = opts || {} 36 | this.source = opts.source 37 | this.output = opts.output 38 | this.delay = opts.delay || 120 39 | this.chain = { 40 | parent: null, 41 | dom: this.output, 42 | val: [], 43 | } 44 | if (!(typeof this.opts.done === 'function')) { 45 | this.opts.done = function () { 46 | return 47 | } 48 | } 49 | } 50 | 51 | init() { 52 | //初始化函数 53 | this.chain.val = this.convert(this.source, this.chain.val) 54 | } 55 | 56 | convert(dom: HTMLElement | ChildNode, arr: any[]) { 57 | //将dom节点的子节点转换成数组, 58 | const children = Array.from(dom.childNodes) 59 | for (let i = 0; i < children.length; i++) { 60 | const node = children[i] 61 | if (node.nodeType === 3) { 62 | arr = arr.concat((node.nodeValue as string).split('')) //将字符串转换成字符串数组,后面打印时才会一个一个的打印 63 | } else if (node.nodeType === 1) { 64 | let val: any[] = [] 65 | val = this.convert(node, val) 66 | arr.push({ 67 | dom: node, 68 | val: val, 69 | }) 70 | } 71 | } 72 | return arr 73 | } 74 | 75 | print(dom: HTMLElement, val: any, callback: () => void) { 76 | setTimeout(function () { 77 | dom.appendChild(document.createTextNode(val)) 78 | callback() 79 | }, this.delay) 80 | } 81 | 82 | play(ele: ChainProps) { 83 | //当打印最后一个字符时,动画完毕,执行done 84 | if (!ele.val.length) { 85 | if (ele.parent) this.play(ele.parent) 86 | else this.opts.done && this.opts.done() 87 | return 88 | } 89 | const current = ele.val.shift() //获取第一个元素,同时删除数组中的第一个元素 90 | if (typeof current === 'string') { 91 | this.print(ele.dom, current, () => { 92 | this.play(ele) //继续打印下一个字符 93 | }) 94 | } else { 95 | const dom = current.dom.cloneNode() //克隆节点,不克隆节点的子节点,所以不用加参数true 96 | ele.dom.appendChild(dom) 97 | this.play({ 98 | parent: ele, 99 | dom, 100 | val: current.val, 101 | }) 102 | } 103 | } 104 | 105 | start() { 106 | this.init() 107 | this.play(this.chain) 108 | } 109 | } 110 | 111 | export default Typing 112 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/ 2 | 3 | export const isUrl = (path: string): boolean => reg.test(path) 4 | /** 判断是否是图片链接 */ 5 | export const isImg = (path: string) => { 6 | return /\w.(png|jpg|jpeg|svg|webp|gif|bmp)$/i.test(path) 7 | } 8 | 9 | const objPtoToString = Object.prototype.toString 10 | 11 | export function isPromise(val: any): val is Promise { 12 | return objPtoToString.call(val) === '[object Promise]' 13 | } 14 | 15 | export function isDate(val: any): val is Date { 16 | return objPtoToString.call(val) === '[object Date]' 17 | } 18 | // eslint-disable-next-line 19 | export function isObject(val: any): val is Object { 20 | return val !== null && typeof val === 'object' 21 | } 22 | // eslint-disable-next-line 23 | export function isPlainObject(val: any): val is Object { 24 | return objPtoToString.call(val) === '[object Object]' 25 | } 26 | 27 | export function isElement(val: any): val is Element { 28 | return val instanceof Element 29 | } 30 | 31 | /** 32 | * 深拷贝工具函数 33 | * @param objs 34 | * @returns 35 | */ 36 | export function deepMerge(...objs: any[]): any { 37 | const result = Object.create(null) 38 | 39 | objs.forEach(obj => { 40 | if (obj) { 41 | Object.keys(obj).forEach(key => { 42 | const val = obj[key] 43 | if (isPlainObject(val)) { 44 | // 第二次遍历时发现仍让是一个普通的object,而且存在result中那么就是合并这俩个对象 45 | if (isPlainObject(result[key])) { 46 | result[key] = deepMerge(result[key], val) 47 | } else { 48 | // 第一次就是深拷贝这个对象,并赋值 49 | result[key] = deepMerge({}, val) 50 | } 51 | } else { 52 | // 第一就是普通赋值,第二次这个属性如果不存在就赋值存在覆盖 53 | result[key] = val 54 | } 55 | }) 56 | } 57 | }) 58 | 59 | return result 60 | } 61 | 62 | /** 63 | * @description 将时间戳转换为年-月-日-时-分-秒格式 64 | * @param {String} timestamp 65 | * @returns {String} 年-月-日-时-分-秒 66 | */ 67 | 68 | export function timestampToTime(timestamp: string) { 69 | const date = new Date(timestamp) 70 | const Y = date.getFullYear() + '-' 71 | const M = 72 | (date.getMonth() + 1 < 10 73 | ? '0' + (date.getMonth() + 1) 74 | : date.getMonth() + 1) + '-' 75 | const D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' ' 76 | const h = 77 | (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':' 78 | const m = 79 | (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':' 80 | const s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() 81 | 82 | const strDate = Y + M + D + h + m + s 83 | return strDate 84 | } 85 | 86 | /** 87 | * 删除对象的一些字段 88 | * @param obj 89 | * @param keys 90 | * @returns 91 | */ 92 | // eslint-disable-next-line 93 | export function filterObjFields(obj: object, keys: string[]) { 94 | return Object.keys(obj).reduce((o, key) => { 95 | if (!keys.includes(key)) { 96 | o[key] = (obj as any)[key] 97 | } 98 | return o 99 | }, {} as {[key: string]: any}) 100 | } 101 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} path 3 | * @returns {Boolean} 4 | */ 5 | export function isExternal(path: string): boolean { 6 | return /^(https?:|mailto:|tel:)/.test(path) 7 | } 8 | 9 | /** 10 | * @param {string} str 11 | * @returns {Boolean} 12 | */ 13 | export function validUsername(str: string): boolean { 14 | const valid_map = ['admin', 'editor'] 15 | return valid_map.indexOf(str.trim()) >= 0 16 | } 17 | 18 | /** 19 | * @param {string} url 20 | * @returns {Boolean} 21 | */ 22 | export function validURL(url: string): boolean { 23 | const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/ 24 | return reg.test(url) 25 | } 26 | 27 | /** 28 | * @param {string} str 29 | * @returns {Boolean} 30 | */ 31 | export function validLowerCase(str: string): boolean { 32 | const reg = /^[a-z]+$/ 33 | return reg.test(str) 34 | } 35 | 36 | /** 37 | * @param {string} str 38 | * @returns {Boolean} 39 | */ 40 | export function validUpperCase(str: string): boolean { 41 | const reg = /^[A-Z]+$/ 42 | return reg.test(str) 43 | } 44 | 45 | /** 46 | * @param {string} str 47 | * @returns {Boolean} 48 | */ 49 | export function validAlphabets(str: string): boolean { 50 | const reg = /^[A-Za-z]+$/ 51 | return reg.test(str) 52 | } 53 | 54 | /** 55 | * @param {string} email 56 | * @returns {Boolean} 57 | */ 58 | export function validEmail(email: string): boolean { 59 | const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ 60 | return reg.test(email) 61 | } 62 | 63 | /** 64 | * @param {string} str 65 | * @returns {Boolean} 66 | */ 67 | export function isString(str: any): boolean { 68 | if (typeof str === 'string' || str instanceof String) { 69 | return true 70 | } 71 | return false 72 | } 73 | 74 | /** 75 | * @param {Array} arg 76 | * @returns {Boolean} 77 | */ 78 | export function isArray(arg: any): boolean { 79 | if (typeof Array.isArray === 'undefined') { 80 | return Object.prototype.toString.call(arg) === '[object Array]' 81 | } 82 | return Array.isArray(arg) 83 | } 84 | 85 | /** 86 | * 87 | * @param {String} phone 88 | * @returns {Boolean} 89 | */ 90 | export function isPhone(phone: string) { 91 | return /^1[3456789]\d{9}$/.test(phone) 92 | } 93 | -------------------------------------------------------------------------------- /src/views/about/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TypingCard from 'comps/TypingCard' 3 | 4 | interface IHomeProps {} 5 | 6 | const Home: React.FC = () => { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | 14 | export default Home 15 | -------------------------------------------------------------------------------- /src/views/account/center/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/views/account/center/index.less -------------------------------------------------------------------------------- /src/views/account/center/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './index.less' 3 | 4 | interface IAccountCenterProps {} 5 | 6 | const AccountCenter: React.FC = props => { 7 | return
个人中心
8 | } 9 | 10 | export default AccountCenter 11 | -------------------------------------------------------------------------------- /src/views/account/setting/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/views/account/setting/index.less -------------------------------------------------------------------------------- /src/views/account/setting/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './index.less' 3 | 4 | interface IAccountSettingProps {} 5 | 6 | const AccountSetting: React.FC = props => { 7 | return
个人设置
8 | } 9 | 10 | export default AccountSetting 11 | -------------------------------------------------------------------------------- /src/views/component/form/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface IFormComProps {} 4 | 5 | const FormCom: React.FC = () => { 6 | return
表单组件展示
7 | } 8 | 9 | export default FormCom 10 | -------------------------------------------------------------------------------- /src/views/component/table/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface ITableComProps {} 4 | 5 | const TableCom: React.FC = () => { 6 | return
表格组件展示
7 | } 8 | 9 | export default TableCom 10 | -------------------------------------------------------------------------------- /src/views/dashboard/components/BarChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Column} from '@ant-design/charts' 3 | 4 | const BarChart: React.FC = () => { 5 | const data = [ 6 | { 7 | name: 'London', 8 | month: 'Jan.', 9 | value: 12.9, 10 | type: '语文', 11 | }, 12 | { 13 | name: 'London', 14 | month: 'Jan.', 15 | value: 10.9, 16 | type: '数学', 17 | }, 18 | { 19 | name: 'London', 20 | month: 'Jan.', 21 | value: 120.9, 22 | type: '英语', 23 | }, 24 | { 25 | name: 'Berlin', 26 | month: 'Jan.', 27 | value: 12.4, 28 | type: '美术', 29 | }, 30 | { 31 | name: 'Berlin', 32 | month: 'Jan.', 33 | value: 12.4, 34 | type: '线性代数', 35 | }, 36 | { 37 | name: 'beijing', 38 | month: 'Jan.', 39 | value: 12.4, 40 | type: '高数', 41 | }, 42 | { 43 | name: 'London', 44 | month: 'Feb.', 45 | value: 2.9, 46 | type: '语文', 47 | }, 48 | { 49 | name: 'London', 50 | month: 'Feb.', 51 | value: 5.9, 52 | type: '数学', 53 | }, 54 | { 55 | name: 'London', 56 | month: 'Feb.', 57 | value: 10.9, 58 | type: '英语', 59 | }, 60 | { 61 | name: 'Berlin', 62 | month: 'Feb.', 63 | value: 22.4, 64 | type: '美术', 65 | }, 66 | { 67 | name: 'Berlin', 68 | month: 'Feb.', 69 | value: 32.4, 70 | type: '线性代数', 71 | }, 72 | { 73 | name: 'Berlin', 74 | month: 'Feb.', 75 | value: 62.4, 76 | type: '线性代数-上', 77 | }, 78 | { 79 | name: 'beijing', 80 | month: 'Feb.', 81 | value: 42.4, 82 | type: '高数', 83 | }, 84 | { 85 | name: 'London', 86 | month: 'Mar.', 87 | value: 2.9, 88 | type: '语文', 89 | }, 90 | { 91 | name: 'London', 92 | month: 'Mar.', 93 | value: 5.9, 94 | type: '数学', 95 | }, 96 | { 97 | name: 'Berlin', 98 | month: 'Mar.', 99 | value: 22.4, 100 | type: '美术', 101 | }, 102 | { 103 | name: 'Berlin', 104 | month: 'Mar.', 105 | value: 32.4, 106 | type: '线性代数', 107 | }, 108 | { 109 | name: 'beijing', 110 | month: 'Mar.', 111 | value: 42.4, 112 | type: '高数', 113 | }, 114 | { 115 | name: 'beijing', 116 | month: 'Mar.', 117 | value: 42.4, 118 | type: '高数-上', 119 | }, 120 | ] 121 | const config = { 122 | data: data, 123 | xField: 'month', 124 | yField: 'value', 125 | isGroup: true, 126 | isStack: true, 127 | seriesField: 'type', 128 | groupField: 'name', 129 | } 130 | return 131 | } 132 | 133 | export default BarChart 134 | -------------------------------------------------------------------------------- /src/views/dashboard/components/BoxCard/index.less: -------------------------------------------------------------------------------- 1 | .box-card-component { 2 | position: relative; 3 | .mallki-text { 4 | position: absolute; 5 | top: 0px; 6 | right: 0px; 7 | font-size: 25px; 8 | font-weight: bold; 9 | } 10 | .panThumb { 11 | z-index: 100; 12 | height: 70px !important; 13 | width: 70px !important; 14 | position: absolute !important; 15 | top: -45px; 16 | left: 0px; 17 | border: 5px solid #ffffff; 18 | background-color: #fff; 19 | margin: auto; 20 | box-shadow: none !important; 21 | } 22 | .progress-item { 23 | margin-bottom: 10px; 24 | font-size: 14px; 25 | } 26 | @media only screen and (max-width: 1510px) { 27 | .mallki-text { 28 | display: none; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/views/dashboard/components/BoxCard/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import {Card, Progress} from 'antd' 3 | import PanThumb from 'comps/PanThumb' 4 | import Mallki from 'comps/Mallki' 5 | import avatarImg from 'assets/images/avatar.jpg' 6 | import './index.less' 7 | 8 | interface IBoxCardProps {} 9 | 10 | const BoxCard: React.FC = () => { 11 | return ( 12 |
13 | 19 | } 20 | > 21 |
22 | 23 | 24 | 25 |
26 | ReactJs 27 | 28 |
29 | 30 |
31 | TypeScript 32 | 33 |
34 | 35 |
36 | Less 37 | 38 |
39 | 40 |
41 | Vite 42 | 43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | 50 | export default BoxCard 51 | -------------------------------------------------------------------------------- /src/views/dashboard/components/LineChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Line} from '@ant-design/charts' 3 | 4 | const style: React.CSSProperties = { 5 | padding: '12px', 6 | backgroundColor: '#fff', 7 | marginBottom: '25px', 8 | } 9 | 10 | interface ILineChartProps { 11 | chartData: any 12 | } 13 | 14 | const LineChart: React.FC = props => { 15 | const {chartData} = props 16 | const config: any = { 17 | width: '100%', 18 | height: 350, 19 | data: chartData, 20 | xField: 'year', 21 | yField: 'gdp', 22 | seriesField: 'name', 23 | yAxis: { 24 | label: { 25 | formatter: function formatter(v: any) { 26 | return ''.concat((v / 1000000000).toFixed(1), ' B') 27 | }, 28 | }, 29 | }, 30 | legend: {position: 'top'}, 31 | smooth: true, 32 | animation: { 33 | appear: { 34 | animation: 'path-in', 35 | duration: 1500, 36 | }, 37 | }, 38 | } 39 | return ( 40 |
41 | 42 |
43 | ) 44 | } 45 | 46 | export default LineChart 47 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PanelGroup/index.less: -------------------------------------------------------------------------------- 1 | .panel-group-container { 2 | .panel-group { 3 | margin-left: -20px !important; 4 | margin-right: -20px !important; 5 | } 6 | .card-panel-col { 7 | padding-left: 20px !important; 8 | padding-right: 20px !important; 9 | } 10 | .card-panel-card { 11 | background-color: #fff; 12 | margin-bottom: 20px; 13 | display: flex; 14 | width: 100%; 15 | height: 108px; 16 | justify-content: space-between; 17 | cursor: pointer; 18 | &:hover { 19 | .card-panel-icon-wrap { 20 | background-color: #cccccc; 21 | } 22 | } 23 | } 24 | .card-panel-icon-wrap { 25 | box-sizing: border-box; 26 | font-size: 55px !important; 27 | width: 80px; 28 | height: 80px; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | padding: 16px; 33 | transition: all 0.38s ease-out; 34 | border-radius: 6px; 35 | margin: 14px 0 0 14px; 36 | background-color: transparent; 37 | } 38 | .card-panel-description { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | margin-right: 25px; 43 | .card-panel-text { 44 | color: #8c8c8c; 45 | font-size: 16px; 46 | font-weight: bold; 47 | margin-bottom: 10px; 48 | } 49 | .card-panel-number { 50 | color: #666666; 51 | font-weight: bold; 52 | font-size: 18px; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PanelGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Row, Col} from 'antd' 3 | import Icon from 'comps/Icon' 4 | import './index.less' 5 | 6 | interface IPanelGroupProps { 7 | handleSetLineChartData: (type: string) => void 8 | } 9 | 10 | interface ChartItemProps { 11 | type: string 12 | icon: string 13 | num: number 14 | color: string 15 | size: number 16 | } 17 | 18 | const chartList: ChartItemProps[] = [ 19 | { 20 | type: 'NewVisits', 21 | icon: 'UserOutlined', 22 | num: 102400, 23 | color: '#40c9c6', 24 | size: 55, 25 | }, 26 | { 27 | type: 'Messages', 28 | icon: 'MessageOutlined', 29 | num: 81212, 30 | color: '#36a3f7', 31 | size: 55, 32 | }, 33 | { 34 | type: 'Purchases', 35 | icon: 'PayCircleOutlined', 36 | num: 9280, 37 | color: '#f4516c', 38 | size: 55, 39 | }, 40 | { 41 | type: 'Shoppings', 42 | icon: 'ShoppingCartOutlined', 43 | num: 13600, 44 | color: '#f6ab40', 45 | size: 55, 46 | }, 47 | ] 48 | 49 | const PanelGroup: React.FC = props => { 50 | const {handleSetLineChartData} = props 51 | return ( 52 |
53 | 54 | {chartList.map((chart, i) => { 55 | return ( 56 |
65 |
66 |
67 | 72 |
73 |
74 |

{chart.type}

75 | {chart.num} 76 |
77 |
78 | 79 | ) 80 | })} 81 | 82 | 83 | ) 84 | } 85 | 86 | export default PanelGroup 87 | -------------------------------------------------------------------------------- /src/views/dashboard/components/PieChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Pie} from '@ant-design/charts' 3 | 4 | const PieChart: React.FC = () => { 5 | const data = [ 6 | { 7 | type: '分类一', 8 | value: 27, 9 | }, 10 | { 11 | type: '分类二', 12 | value: 25, 13 | }, 14 | { 15 | type: '分类三', 16 | value: 18, 17 | }, 18 | { 19 | type: '分类四', 20 | value: 15, 21 | }, 22 | { 23 | type: '分类五', 24 | value: 10, 25 | }, 26 | { 27 | type: '其他', 28 | value: 5, 29 | }, 30 | ] 31 | const config = { 32 | appendPadding: 10, 33 | data: data, 34 | angleField: 'value', 35 | colorField: 'type', 36 | radius: 0.8, 37 | label: { 38 | type: 'spider', 39 | labelHeight: 28, 40 | content: '{name}\n{percentage}', 41 | }, 42 | interactions: [{type: 'element-selected'}, {type: 'element-active'}], 43 | } 44 | return 45 | } 46 | 47 | export default PieChart 48 | -------------------------------------------------------------------------------- /src/views/dashboard/components/RadarChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Radar} from '@ant-design/charts' 3 | 4 | const RadarChart: React.FC = () => { 5 | const data = [ 6 | {name: 'G2', star: 10178}, 7 | {name: 'G6', star: 7077}, 8 | {name: 'F2', star: 7345}, 9 | {name: 'L7', star: 2029}, 10 | {name: 'X6', star: 298}, 11 | {name: 'AVA', star: 806}, 12 | ] 13 | const config = { 14 | data: data.map(d => ({...d, star: Math.log(d.star).toFixed(2)})), 15 | xField: 'name', 16 | yField: 'star', 17 | meta: { 18 | star: { 19 | alias: '分数', 20 | min: 0, 21 | nice: true, 22 | }, 23 | }, 24 | xAxis: { 25 | line: null, 26 | tickLine: null, 27 | }, 28 | yAxis: { 29 | label: false, 30 | grid: { 31 | alternateColor: 'rgba(0, 0, 0, 0.04)', 32 | }, 33 | }, 34 | // 开启辅助点 35 | point: {}, 36 | area: {}, 37 | } 38 | return 39 | } 40 | 41 | export default RadarChart 42 | -------------------------------------------------------------------------------- /src/views/dashboard/components/TransactionTable/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo, useEffect, useState} from 'react' 2 | import {Table, Tag} from 'antd' 3 | import {ColumnsType} from 'antd/es/table' 4 | import {TransactionProps} from 'mock/remoteSearch' 5 | import {remoteSearch} from 'apis/remoteSearch' 6 | import {ResponseData} from 'mock/index' 7 | 8 | const columns: ColumnsType = [ 9 | { 10 | title: 'Order_No', 11 | dataIndex: 'order_no', 12 | key: 'order_no', 13 | width: 200, 14 | align: 'center', 15 | }, 16 | { 17 | title: 'Price', 18 | dataIndex: 'price', 19 | key: 'price', 20 | width: 195, 21 | align: 'center', 22 | render: (text: any) => `$${text}`, 23 | }, 24 | { 25 | title: 'Status', 26 | key: 'tag', 27 | dataIndex: 'tag', 28 | align: 'center', 29 | width: 100, 30 | // eslint-disable-next-line 31 | render: function (tag: any) { 32 | const color = tag === 'pending' ? 'magenta' : 'green' 33 | return ( 34 | 35 | {tag} 36 | 37 | ) 38 | }, 39 | }, 40 | ] 41 | 42 | const TransactionTable: React.FC = () => { 43 | const [tableList, setTableList] = useState([]) 44 | 45 | useEffect(() => { 46 | // remoteSearch().then(response => { 47 | // const data = response.data as ResponseData 48 | // if (data.code === 20000) { 49 | // const list = data.data as TransactionProps[] 50 | // setTableList(list.slice(0, 12)) 51 | // } 52 | // }) 53 | }, []) 54 | 55 | return ( 56 |
57 |
58 | 59 | ) 60 | } 61 | 62 | export default memo(TransactionTable) 63 | -------------------------------------------------------------------------------- /src/views/dashboard/index.less: -------------------------------------------------------------------------------- 1 | .app-container { 2 | .chart-wrapper { 3 | background: #fff; 4 | padding: 16px 16px 0; 5 | margin-bottom: 32px; 6 | } 7 | } 8 | .github-corner { 9 | position: absolute; 10 | z-index: 99; 11 | width: 120px; 12 | height: 120px; 13 | right: 0; 14 | top: 0; 15 | background: url('../../assets/images//githubCorner.png'); 16 | background-size: 100% 100%; 17 | } 18 | .progress-item { 19 | margin-bottom: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /src/views/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react' 2 | import {Row, Col} from 'antd' 3 | import PanelGroup from './components/PanelGroup' 4 | import LineChart from './components/LineChart' 5 | import RadarChart from './components/RadarChart' 6 | import PieChart from './components/PieChart' 7 | import BarChart from './components/BarChart' 8 | import TransactionTable from './components/TransactionTable' 9 | import BoxCard from './components/BoxCard' 10 | import {NewVisits, Messages, Purchases, Shoppings} from './models' 11 | import './index.less' 12 | 13 | export interface IDashboardProps {} 14 | 15 | const lineChartDefaultData: any = { 16 | NewVisits, 17 | Messages, 18 | Purchases, 19 | Shoppings, 20 | } 21 | 22 | const Dashboard: React.FC = () => { 23 | const [lineChartData, setLineChartData] = useState( 24 | lineChartDefaultData['NewVisits'], 25 | ) 26 | const handleSetLineChartData = (type: any) => { 27 | setLineChartData(lineChartDefaultData[type]) 28 | } 29 | 30 | return ( 31 |
32 | 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 | 64 | 72 | 73 | 74 | 82 | 83 | 84 | 85 | 86 | ) 87 | } 88 | 89 | export default Dashboard 90 | -------------------------------------------------------------------------------- /src/views/doc/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import TypingCard from 'comps/TypingCard' 3 | 4 | interface IDocProps {} 5 | 6 | const Doc: React.FC = () => { 7 | const source = React.useMemo(() => { 8 | return `开发文档请戳这里 vite-vue3-docs 开发文档。 目前正在编写完善中...` 9 | }, []) 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | 18 | export default memo(Doc) 19 | -------------------------------------------------------------------------------- /src/views/error/404/index.less: -------------------------------------------------------------------------------- 1 | .not-found-page { 2 | .right { 3 | padding-left: 50px; 4 | margin-top: 150px; 5 | h1 { 6 | font-size: 35px; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/views/error/404/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {memo} from 'react' 2 | import {Row, Col, Button} from 'antd' 3 | import notFound from 'assets/images/404.png' 4 | import {useHistory} from 'react-router' 5 | import './index.less' 6 | 7 | interface INotFoundProps {} 8 | 9 | const NotFound: React.FC = () => { 10 | const history = useHistory() 11 | return ( 12 |
13 | 14 |
15 | 404 16 | 17 | 18 |

404

19 |

抱歉,你访问的页面不存在

20 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default memo(NotFound) 30 | -------------------------------------------------------------------------------- /src/views/index.ts: -------------------------------------------------------------------------------- 1 | import {lazy} from 'react' 2 | 3 | const About = lazy(() => import('./about')) 4 | const Login = lazy(() => import('./login')) 5 | const Dashboard = lazy(() => import('./dashboard')) 6 | const Doc = lazy(() => import('./doc')) 7 | const Explanation = lazy(() => import('./permission')) 8 | const Admin = lazy(() => import('./permission/admin')) 9 | const Editor = lazy(() => import('./permission/editor')) 10 | const Guest = lazy(() => import('./permission/guest')) 11 | const Account = lazy(() => import('./system/account')) 12 | const Role = lazy(() => import('./system/role')) 13 | const Menu = lazy(() => import('./system/menu')) 14 | const Department = lazy(() => import('./system/department')) 15 | const ChangePassword = lazy(() => import('./system/changePassword')) 16 | const Menu1_1 = lazy(() => import('./nested/menu1/menu1-1')) 17 | const Menu1_2_1 = lazy(() => import('./nested/menu1/menu1-2/menu1-2-1')) 18 | const NotFound = lazy(() => import('./error/404')) 19 | const AccountCenter = lazy(() => import('./account/center')) 20 | const AccountSetting = lazy(() => import('./account/setting')) 21 | const TableCom = lazy(() => import('./component/table')) 22 | const FormCom = lazy(() => import('./component/form')) 23 | 24 | export { 25 | About, 26 | Login, 27 | Dashboard, 28 | Doc, 29 | Explanation, 30 | Admin, 31 | Editor, 32 | Guest, 33 | Account, 34 | Role, 35 | Menu, 36 | ChangePassword, 37 | Department, 38 | Menu1_1, 39 | Menu1_2_1, 40 | NotFound, 41 | AccountCenter, 42 | AccountSetting, 43 | TableCom, 44 | FormCom, 45 | } 46 | -------------------------------------------------------------------------------- /src/views/login/index.less: -------------------------------------------------------------------------------- 1 | .login-container { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100vw; 6 | height: 100vh; 7 | background-color: #fff; 8 | background: url('../../assets/images/bg-xw.jpg') no-repeat center center; 9 | background-size: cover; 10 | background-position-x: 15% !important; 11 | .login-form-container { 12 | width: 320px; 13 | background: #fff; 14 | padding: 30px; 15 | box-sizing: border-box; 16 | box-shadow: 0 0 10px 2px rgba(40, 138, 204, 0.16); 17 | border-radius: 3px; 18 | transform: translateY(-10%); 19 | .login-title { 20 | text-align: center; 21 | margin-bottom: 30px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/views/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Form, Button, Input, Spin, notification} from 'antd' 3 | import {login} from 'store/actions' 4 | import {connect} from 'react-redux' 5 | import {RouteComponentProps, withRouter} from 'react-router' 6 | import Icon from 'comps/Icon' 7 | import {HttpStatusCode} from 'root/mock' 8 | import './index.less' 9 | 10 | type ILoginProps = RouteComponentProps 11 | 12 | const Login: React.FC = props => { 13 | const {login, history} = props 14 | 15 | const formSubmit = (values: any) => { 16 | login(values.username.trim(), values.password.trim()).then((data: any) => { 17 | if (data.code === HttpStatusCode.OK) { 18 | notification.success({ 19 | message: '登录成功', 20 | description: `欢迎回来: ${values.username}`, 21 | duration: 3, 22 | }) 23 | history.push('/dashboard') 24 | } 25 | }) 26 | } 27 | 28 | return ( 29 |
30 |
31 |

用户登录

32 | 33 |
34 | 44 | } 46 | placeholder="用户名" 47 | /> 48 | 49 | 59 | } 62 | placeholder="密码" 63 | /> 64 | 65 | 66 | 69 | 70 | 71 |

账号 : admin 密码 : 123456

72 |

账号 : editor 密码 : 123456

73 |

账号 : guest 密码 : 123456

74 |
75 | 76 |
77 |
78 |
79 | ) 80 | } 81 | 82 | interface ActionProps { 83 | login: any 84 | } 85 | 86 | export default connect(null, {login})(withRouter(Login)) 87 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-1/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface IMenu1_1Props {} 4 | 5 | const Menu1_1: React.FC = () => { 6 | return ( 7 |
8 | Menu1_1 9 |
10 | ) 11 | } 12 | 13 | export default Menu1_1 14 | -------------------------------------------------------------------------------- /src/views/nested/menu1/menu1-2/menu1-2-1/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface IMenu1_2_1Props {} 4 | 5 | const Menu1_2_1: React.FC = () => { 6 | return
Menu1_2_1
7 | } 8 | 9 | export default Menu1_2_1 10 | -------------------------------------------------------------------------------- /src/views/permission/admin.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from 'react' 2 | import TypingCard from 'comps/TypingCard' 3 | import {Button} from 'antd' 4 | import VBasicDrawerForm, {DrawerFormImperative} from 'comps/VBasicDrawerForm' 5 | import {IFormItemProps} from 'comps/vBasicForm/components/FromItem' 6 | 7 | interface IAdminProps {} 8 | 9 | const Admin: React.FC = () => { 10 | const drawerFormRef = useRef(null) 11 | const formFields: IFormItemProps[] = [ 12 | { 13 | type: 'input', 14 | label: '用户名', 15 | name: 'username', 16 | value: '', 17 | rules: [ 18 | { 19 | required: true, 20 | message: '请输入用户名', 21 | }, 22 | ], 23 | }, 24 | { 25 | type: 'password', 26 | label: '密码', 27 | name: 'password', 28 | value: '', 29 | rules: [ 30 | { 31 | required: true, 32 | message: '请输入密码', 33 | }, 34 | ], 35 | }, 36 | { 37 | type: 'input', 38 | label: '昵称', 39 | name: 'nickname', 40 | value: '', 41 | rules: [ 42 | { 43 | required: true, 44 | message: '请输入昵称', 45 | }, 46 | ], 47 | }, 48 | { 49 | type: 'icon-picker', 50 | name: 'menuIcon', 51 | label: '菜单图标', 52 | value: '', 53 | rules: [ 54 | { 55 | required: true, 56 | message: '请选择图标', 57 | }, 58 | ], 59 | }, 60 | { 61 | type: 'textarea', 62 | label: '描述', 63 | name: 'description', 64 | value: '', 65 | rules: [ 66 | { 67 | required: true, 68 | message: '请输入描述', 69 | }, 70 | ], 71 | }, 72 | ] 73 | 74 | const handleClick = () => { 75 | const {openFormDrawer, closeFormDrawer} = drawerFormRef.current! 76 | openFormDrawer(() => { 77 | console.log('value') 78 | }) 79 | } 80 | 81 | return ( 82 |
83 | 87 | 88 | 89 | 90 | 95 |
96 | ) 97 | } 98 | 99 | export default Admin 100 | -------------------------------------------------------------------------------- /src/views/permission/editor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TypingCard from 'comps/TypingCard' 3 | 4 | interface IEditorProps {} 5 | 6 | const Editor: React.FC = () => { 7 | return ( 8 |
9 | 13 |
14 | ) 15 | } 16 | 17 | export default Editor 18 | -------------------------------------------------------------------------------- /src/views/permission/guest.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TypingCard from 'comps/TypingCard' 3 | 4 | interface IGuestProps {} 5 | 6 | const Guest: React.FC = () => { 7 | return ( 8 |
9 | 13 |
14 | ) 15 | } 16 | 17 | export default Guest 18 | -------------------------------------------------------------------------------- /src/views/permission/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import TypingCard from 'comps/TypingCard' 3 | 4 | interface IExplanationProps {} 5 | 6 | const Explanation: React.FC = () => { 7 | const source = React.useMemo(() => { 8 | return ` 9 |

本项目中的菜单权限和路由权限都是基于用户所属角色来分配的,本项目中内置了三种角色,分别是:

10 |
    11 |
  • 管理员 admin:该角色拥有系统内所有菜单和路由的权限。
  • 12 |
  • 编辑员 editor:该角色拥有系统内除用户管理页之外的所有菜单和路由的权限。
  • 13 |
  • 游客 guest:该角色仅拥有Dashboard、开发文档、权限测试和关于作者三个页面的权限。
  • 14 |
15 |
你可以通过用户管理页面,动态的添加或删除用户,以及编辑某个已经存在的用户,例如修改其权限等操作。
16 | ` 17 | }, []) 18 | 19 | return ( 20 |
21 | 22 |
23 | ) 24 | } 25 | 26 | export default Explanation 27 | -------------------------------------------------------------------------------- /src/views/system/account/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/views/system/account/index.less -------------------------------------------------------------------------------- /src/views/system/account/models/departmentModel.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/views/system/account/models/userModel.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/views/system/changePassword/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/views/system/changePassword/index.less -------------------------------------------------------------------------------- /src/views/system/changePassword/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {PageHeader, Form, Input, Button} from 'antd' 3 | import './index.less' 4 | 5 | interface IChangePasswordProps {} 6 | 7 | const ChangePassword: React.FC = () => { 8 | return ( 9 |
10 | 11 |

修改成功后会自动退出当前登录!

12 |
13 |
14 |
18 |
24 | 29 | 30 | 31 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 |
55 |
56 |
57 | ) 58 | } 59 | 60 | export default ChangePassword 61 | -------------------------------------------------------------------------------- /src/views/system/department/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/views/system/department/index.less -------------------------------------------------------------------------------- /src/views/system/menu/index.less: -------------------------------------------------------------------------------- 1 | .leve1 { 2 | background: burlywood; 3 | } 4 | .leve2 { 5 | background: blue; 6 | } 7 | .leve3 { 8 | background: gray; 9 | } 10 | .leve4 { 11 | background: rgb(45, 223, 0); 12 | } 13 | 14 | .rowBgColor { 15 | background-color: #fafafa; 16 | } 17 | 18 | .rowHover { 19 | &:hover { 20 | > td { 21 | background-color: #e3f4fc !important; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/views/system/role/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/src/views/system/role/index.less -------------------------------------------------------------------------------- /tests/foo.ts: -------------------------------------------------------------------------------- 1 | export default function (): void { 2 | console.log('foo') 3 | } 4 | -------------------------------------------------------------------------------- /tests/index.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '@testing-library/jest-dom' 3 | import {render} from '@testing-library/react' 4 | import Demo from '@/demo' 5 | import foo from './foo' 6 | 7 | describe('test', () => { 8 | foo() 9 | it('1 + 1', () => { 10 | expect(1 + 2).toBe(3) 11 | }) 12 | 13 | it('test demo page', () => { 14 | const {getByText} = render() 15 | // const element = getByTestId('demo') 16 | const element = getByText('Demo Page').parentElement as HTMLDivElement 17 | expect(element).toBeInTheDocument() 18 | expect(element.tagName).toEqual('DIV') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visionwuwu/vite-react-ts-admin/8b8115c77199d16b7f01a9a828403390de516d0d/tsconfig.build.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "types": ["vite/client", "jest"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react", 18 | "baseUrl": ".", 19 | "paths": { 20 | "root/*": ["*"], 21 | "@/*": ["src/*"], 22 | "utils/*": ["src/utils/*"], 23 | "comps/*": ["src/components/*"], 24 | "views/*": ["src/views/*"], 25 | "store/*": ["src/store/*"], 26 | "hooks/*": ["src/hooks/*"], 27 | "assets/*": ["src/assets/*"], 28 | "styles/*": ["src/styles/*"], 29 | "apis/*": ["src/api/*"], 30 | "mock/*": ["mock/*"] 31 | } 32 | }, 33 | "include": ["./src", "mock/index.ts", "tests"] 34 | } 35 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {UserConfigExport, ConfigEnv} from 'vite' 2 | import reactRefresh from '@vitejs/plugin-react-refresh' 3 | import path from 'path' 4 | import styleImport from 'vite-plugin-style-import' 5 | // import {viteMockServe} from 'vite-plugin-mock' 6 | import config, {EnvName} from './config' 7 | import lessToJS from 'less-vars-to-js' 8 | import fs from 'fs' 9 | 10 | /** 获取环境变量 */ 11 | const env: EnvName = 12 | (process.argv[process.argv.length - 1] as EnvName) || 'development' 13 | 14 | /** 当前环境基础配置 */ 15 | const base = config[env] 16 | 17 | /** 自定义antd主题 */ 18 | const themeVariables = lessToJS( 19 | fs.readFileSync( 20 | path.resolve(__dirname, './config/antd-variables.less'), 21 | 'utf8', 22 | ), 23 | ) 24 | 25 | // https://vitejs.dev/config/ 26 | export default ({command}: ConfigEnv): UserConfigExport => { 27 | return { 28 | base: base ? base.cdn : './', 29 | resolve: { 30 | alias: { 31 | root: path.resolve(__dirname, './'), 32 | '@': path.resolve(__dirname, './src'), 33 | views: path.resolve(__dirname, './src/views'), 34 | store: path.resolve(__dirname, './src/store'), 35 | utils: path.resolve(__dirname, './src/utils'), 36 | hooks: path.resolve(__dirname, './src/hooks'), 37 | assets: path.resolve(__dirname, './src/assets'), 38 | styles: path.resolve(__dirname, './src/styles'), 39 | apis: path.resolve(__dirname, './src/api'), 40 | comps: path.resolve(__dirname, './src/components'), 41 | mock: path.resolve(__dirname, './mock'), 42 | }, 43 | }, 44 | css: { 45 | preprocessorOptions: { 46 | less: { 47 | modifyVars: themeVariables, 48 | javascriptEnabled: true, 49 | }, 50 | }, 51 | }, 52 | // server: { 53 | // proxy: { 54 | // // 开发代理 55 | // '/api': { 56 | // target: 'http://localhost:5001/', 57 | // changeOrigin: true, 58 | // rewrite: path => path.replace(/^\/api/, ''), 59 | // }, 60 | // }, 61 | // }, 62 | plugins: [ 63 | reactRefresh(), 64 | // viteMockServe({ 65 | // supportTs: true, 66 | // localEnabled: command === 'serve', 67 | // }), 68 | styleImport({ 69 | libs: [ 70 | { 71 | libraryName: 'antd', 72 | esModule: true, 73 | resolveStyle: name => { 74 | return `antd/es/${name}/style/index` 75 | }, 76 | }, 77 | ], 78 | }), 79 | ], 80 | } 81 | } 82 | --------------------------------------------------------------------------------