├── .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 |
8 |
--------------------------------------------------------------------------------
/src/assets/images/vite.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |

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 |
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 |
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 |
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 |
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 |
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 |
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 |

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 |
29 |
30 |
31 |
TypeScript
32 |
33 |
34 |
35 |
39 |
40 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------