├── .editorconfig
├── .env
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── commitlint.config.js
├── config-overrides.js
├── package.json
├── paths.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── assets
│ └── images
│ │ └── logo.svg
├── base
│ └── GlobalLoading
│ │ ├── index.scss
│ │ └── index.tsx
├── components
│ ├── ErrorBoundary
│ │ └── index.tsx
│ └── RenderRouter
│ │ └── index.tsx
├── config
│ ├── antd.ts
│ ├── index.ts
│ ├── layout.ts
│ └── menu.tsx
├── hooks
│ └── useBoolean
│ │ └── index.ts
├── index.tsx
├── layout
│ ├── AuthorityLayout.scss
│ ├── AuthorityLayout.tsx
│ ├── NormalLayout.scss
│ ├── NormalLayout.tsx
│ └── components
│ │ ├── AuthorityHeader
│ │ ├── index.scss
│ │ └── index.tsx
│ │ ├── AuthoritySider
│ │ ├── index.scss
│ │ └── index.tsx
│ │ └── Footer
│ │ └── index.tsx
├── pages
│ ├── 404
│ │ └── index.tsx
│ ├── home
│ │ └── index.tsx
│ └── login
│ │ ├── index.scss
│ │ └── index.tsx
├── react-app-env.d.ts
├── routes
│ ├── history.ts
│ └── index.ts
├── serviceWorker.ts
├── setupTests.ts
├── store
│ └── login.ts
├── styles
│ ├── global.scss
│ ├── index.scss
│ └── reset.scss
└── utils
│ ├── dom.ts
│ ├── filter.ts
│ ├── index.ts
│ ├── request
│ ├── axios.ts
│ └── index.ts
│ ├── storage
│ ├── index.ts
│ └── storage.ts
│ └── type.ts
├── tsconfig.json
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | PORT=9527
2 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // 运行环境
3 | env: {
4 | browser: true,
5 | es2020: true
6 | },
7 | // 继承的规则 / 插件
8 | extends: [
9 | 'plugin:react/recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'prettier'
12 | ],
13 | // 解析器
14 | parser: '@typescript-eslint/parser',
15 | // 解析器配置
16 | parserOptions: {
17 | ecmaFeatures: {
18 | jsx: true
19 | },
20 | ecmaVersion: 11,
21 | sourceType: 'module'
22 | },
23 | // 插件
24 | plugins: ['@typescript-eslint', 'react', 'react-hooks'],
25 | settings: {
26 | // 自动检测 React 的版本
27 | react: {
28 | version: 'detect'
29 | }
30 | },
31 | // 规则
32 | rules: {
33 | 'react/prop-types': 0,
34 | 'react/forbid-prop-types': 0,
35 | 'react/require-default-props': 0,
36 | 'react/default-props-match-prop-types': 0,
37 | 'react/jsx-indent': 0,
38 | 'react/jsx-filename-extension': 0,
39 | 'react/display-name': 0,
40 | 'react/button-has-type': 0,
41 | '@typescript-eslint/no-non-null-assertion': 0,
42 | '@typescript-eslint/no-explicit-any': 0,
43 | '@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_' }],
44 | '@typescript-eslint/no-var-requires': 0,
45 | '@typescript-eslint/explicit-module-boundary-types': 0
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .idea
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmmirror.com
2 | sass_binary_site=https://npmmirror.com/mirrors/node-sass
3 | phantomjs_cdnurl=https://npmmirror.com/mirrors/phantomjs
4 | electron_mirror=https://npmmirror.com/mirrors/electron
5 | profiler_binary_host_mirror=https://npmmirror.com/mirrors/node-inspector
6 | chromedriver_cdnurl=https://npmmirror.com/mirrors/chromedriver
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.svg
2 | *.html
3 | package.json
4 | tsconfig.json
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "overrides": [
7 | {
8 | "files": ".prettierrc",
9 | "options": { "parser": "json" }
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-admin-template
2 |
3 | React 管理后台基础模板
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional']
3 | }
4 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const {
2 | override,
3 | fixBabelImports,
4 | addWebpackAlias,
5 | addWebpackExternals,
6 | addWebpackPlugin
7 | } = require('customize-cra')
8 | const path = require('path')
9 | const { DefinePlugin } = require('webpack')
10 | const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin')
11 |
12 | const isEnvDevelopment = process.env.NODE_ENV === 'development'
13 | const isEnvProduction = process.env.NODE_ENV === 'production'
14 |
15 | const customWebpackConfig = (config) => {
16 | /* 开发环境相关配置 */
17 | if (isEnvDevelopment) {
18 | // 增加 module rules 配置
19 | config.module.rules.push({
20 | test: /\.[jt]sx?$/,
21 | loader: 'react-dev-inspector/plugins/webpack/inspector-loader'
22 | })
23 | }
24 |
25 | /* 生产环境相关配置 */
26 | if (isEnvProduction) {
27 | // 修改 HtmlWebpackPlugin 配置
28 | config.plugins = config.plugins.map((plugin) => {
29 | if (plugin.constructor.name === 'HtmlWebpackPlugin') {
30 | plugin.userOptions.cdn = [
31 | 'https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js',
32 | 'https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js',
33 | 'https://cdn.jsdelivr.net/npm/react-router-dom@5.2.0/umd/react-router-dom.min.js',
34 | 'https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js'
35 | ]
36 | }
37 | return plugin
38 | })
39 | }
40 |
41 | return config
42 | }
43 |
44 | module.exports = override(
45 | // 配置别名
46 | addWebpackAlias({
47 | '@': path.resolve(__dirname, 'src')
48 | }),
49 | // 配置 antd 按需加载
50 | fixBabelImports('import', {
51 | libraryName: 'antd',
52 | libraryDirectory: 'es',
53 | style: 'css'
54 | }),
55 | addWebpackPlugin(
56 | // 添加全局变量
57 | new DefinePlugin({
58 | 'process.env.PWD': JSON.stringify(process.env.PWD)
59 | }),
60 | // 配置 antd dayjs
61 | new AntdDayjsWebpackPlugin()
62 | ),
63 | // 配置 externals
64 | isEnvProduction &&
65 | addWebpackExternals({
66 | react: 'React',
67 | 'react-dom': 'ReactDOM',
68 | 'react-router-dom': 'ReactRouterDOM',
69 | axios: 'axios'
70 | }),
71 | // 自定义 webpack 配置
72 | customWebpackConfig
73 | )
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-admin-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "author": "maomao1996 <1714487678@qq.com>",
6 | "bugs": {
7 | "url": "https://github.com/maomao1996/react-admin-template/issues"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/maomao1996/react-admin-template"
12 | },
13 | "dependencies": {
14 | "@testing-library/jest-dom": "^5.11.0",
15 | "@testing-library/react": "^10.4.3",
16 | "@testing-library/user-event": "^12.0.11",
17 | "@types/jest": "^26.0.3",
18 | "@types/node": "^14.0.14",
19 | "@types/react": "^16.9.41",
20 | "@types/react-dom": "^16.9.8",
21 | "ahooks": "^2.6.0",
22 | "antd": "4.6.4",
23 | "axios": "0.19.2",
24 | "dayjs": "^1.10.4",
25 | "path-to-regexp": "^6.1.0",
26 | "react": "16.13.1",
27 | "react-dom": "16.13.1",
28 | "react-router-dom": "5.2.0",
29 | "react-scripts": "5.0.0",
30 | "typescript": "~4.5.4",
31 | "unstated-next": "^1.1.0"
32 | },
33 | "scripts": {
34 | "start": "react-app-rewired start",
35 | "build": "react-app-rewired build",
36 | "analyze": "source-map-explorer build/static/js/*.js",
37 | "test": "react-app-rewired test",
38 | "commit": "git-cz",
39 | "eject": "react-scripts eject",
40 | "lint": "eslint --ext js,ts,tsx src",
41 | "fix": "prettier --write ./src"
42 | },
43 | "eslintConfig": {
44 | "extends": "react-app"
45 | },
46 | "browserslist": {
47 | "production": [
48 | ">0.2%",
49 | "not dead",
50 | "not op_mini all"
51 | ],
52 | "development": [
53 | "last 1 chrome version",
54 | "last 1 firefox version",
55 | "last 1 safari version"
56 | ]
57 | },
58 | "devDependencies": {
59 | "@commitlint/cli": "^9.0.1",
60 | "@commitlint/config-conventional": "^9.0.1",
61 | "@types/path-to-regexp": "^1.7.0",
62 | "@types/react-router-dom": "^5.1.5",
63 | "@typescript-eslint/eslint-plugin": "^5.9.1",
64 | "@typescript-eslint/parser": "^5.9.1",
65 | "antd-dayjs-webpack-plugin": "^1.0.6",
66 | "babel-plugin-import": "^1.13.0",
67 | "commitizen": "^4.1.2",
68 | "customize-cra": "^0.9.1",
69 | "cz-conventional-changelog": "^3.2.0",
70 | "eslint-config-prettier": "^6.11.0",
71 | "eslint-plugin-prettier": "^3.1.4",
72 | "eslint-plugin-react": "^7.20.3",
73 | "eslint-plugin-react-hooks": "^4.0.5",
74 | "husky": "^4.2.5",
75 | "lint-staged": "^10.2.11",
76 | "prettier": "^2.0.5",
77 | "react-app-rewired": "^2.1.6",
78 | "react-dev-inspector": "^1.1.1",
79 | "sass": "^1.26.9",
80 | "source-map-explorer": "^2.4.2"
81 | },
82 | "husky": {
83 | "hooks": {
84 | "pre-commit": "lint-staged",
85 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
86 | }
87 | },
88 | "lint-staged": {
89 | "src/**/*.{js,ts,tsx}": [
90 | "eslint --fix",
91 | "prettier --write"
92 | ]
93 | },
94 | "config": {
95 | "commitizen": {
96 | "path": "cz-conventional-changelog"
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/paths.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["src/*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-admin-template/1f5e7f0999f7ea6f97af070d44c6e24eb1717695/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 | <% htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.forEach(src => { %>
33 |
34 | <% }) %>
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-admin-template/1f5e7f0999f7ea6f97af070d44c6e24eb1717695/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maomao1996/react-admin-template/1f5e7f0999f7ea6f97af070d44c6e24eb1717695/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import { Router } from 'react-router-dom'
3 | import { ConfigProvider } from 'antd'
4 |
5 | import RenderRouter from '@/components/RenderRouter'
6 | import { authorizedRoutes, normalRoutes } from '@/routes'
7 | import history from '@/routes/history'
8 | import LoginContainer from '@/store/login'
9 | import { antdConfig } from './config'
10 |
11 | const App: React.FC = () => {
12 | const { isLogin } = LoginContainer.useContainer()
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default App
26 |
--------------------------------------------------------------------------------
/src/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/base/GlobalLoading/index.scss:
--------------------------------------------------------------------------------
1 | .global-loading {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | position: fixed;
6 | top: 0;
7 | right: 0;
8 | bottom: 0;
9 | left: 0;
10 | z-index: 1996;
11 | background-color: rgba(#000, 0.35);
12 | }
13 |
--------------------------------------------------------------------------------
/src/base/GlobalLoading/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Spin } from 'antd'
4 | import { SpinProps } from 'antd/es/spin'
5 |
6 | import { isHidden } from '@/utils'
7 |
8 | import './index.scss'
9 |
10 | export const Loading: React.FC = (props) => (
11 |
12 |
13 |
14 | )
15 |
16 | let dom: HTMLElement | null
17 | const GlobalLoading = {
18 | open(props: React.ComponentProps = {}): void {
19 | if (!dom) {
20 | dom = document.createElement('div')
21 | ReactDOM.render(, dom)
22 | document.body.appendChild(dom)
23 | }
24 | if (isHidden(dom)) {
25 | dom.style.display = ''
26 | }
27 | },
28 | close(): void {
29 | dom!.style.display = 'none'
30 | },
31 | remove(): void {
32 | ReactDOM.unmountComponentAtNode(dom!)
33 | document.body.removeChild(dom!)
34 | dom = null
35 | }
36 | }
37 |
38 | export default GlobalLoading
39 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Result } from 'antd'
3 |
4 | // 用于捕获渲染时错误的组件
5 |
6 | class ErrorBoundary extends Component {
7 | state = {
8 | error: null
9 | }
10 |
11 | static getDerivedStateFromError(error: unknown): unknown {
12 | return { error }
13 | }
14 |
15 | render(): React.ReactNode {
16 | if (this.state.error) {
17 | return (
18 |
23 | )
24 | }
25 | return this.props.children
26 | }
27 | }
28 |
29 | export default ErrorBoundary
30 |
--------------------------------------------------------------------------------
/src/components/RenderRouter/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, SuspenseProps } from 'react'
2 | import { Switch, Route, RouteProps, Redirect } from 'react-router-dom'
3 |
4 | // 路由渲染组件
5 |
6 | export interface RouteItem extends Omit {
7 | redirect?: string
8 | icon?: React.ReactNode
9 | routes?: RouteItem[]
10 | }
11 |
12 | const renderRedirectRoute = (route: RouteItem) => {
13 | return (
14 | }
18 | />
19 | )
20 | }
21 |
22 | const renderRoute = (route: RouteItem) => {
23 | if (route.redirect) {
24 | return renderRedirectRoute(route)
25 | }
26 | const { component: Component, ...rest } = route
27 | return (
28 |
32 | Component && (
33 |
34 |
35 |
36 | )
37 | }
38 | />
39 | )
40 | }
41 |
42 | interface RenderRouterProps {
43 | routes: RouteItem[]
44 | fallback?: SuspenseProps['fallback']
45 | }
46 |
47 | const RenderRouter: React.FC = ({ routes, fallback = null }) => {
48 | if (!routes.length) {
49 | return null
50 | }
51 | return (
52 |
53 | {routes.map((route) => renderRoute(route))}
54 |
55 | )
56 | }
57 |
58 | export default RenderRouter
59 |
--------------------------------------------------------------------------------
/src/config/antd.ts:
--------------------------------------------------------------------------------
1 | import zhCN from 'antd/es/locale/zh_CN'
2 | import { ConfigProviderProps } from 'antd/es/config-provider'
3 |
4 | import dayjs from 'dayjs'
5 | import 'dayjs/locale/zh-cn'
6 |
7 | dayjs.locale('zh-cn')
8 |
9 | /**
10 | * antd 全局配置
11 | * https://ant.design/components/config-provider-cn/#API
12 | */
13 |
14 | export const antdConfig: ConfigProviderProps = {
15 | // 组件大小
16 | componentSize: 'middle',
17 | // 语言包配置
18 | locale: zhCN
19 | }
20 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from './antd'
2 |
3 | export * from './layout'
4 |
--------------------------------------------------------------------------------
/src/config/layout.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * layout 配置
3 | */
4 |
5 | /**
6 | * 侧边栏最大宽度
7 | */
8 | export const SIDER_MAX_WIDTH = 260
9 |
10 | /**
11 | * 侧边栏自动收缩宽度
12 | */
13 | export const SIDER_AUTO_SHRINK_WIDTH = 1000
14 |
--------------------------------------------------------------------------------
/src/config/menu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { DatabaseOutlined, WarningOutlined } from '@ant-design/icons'
3 |
4 | import { MenuProps } from '@/layout/components/AuthoritySider'
5 |
6 | /**
7 | * 侧边栏配置
8 | * 图标查询地址 https://ant.design/components/icon-cn/
9 | */
10 |
11 | export const menuConfig: MenuProps[] = [
12 | {
13 | title: '首页',
14 | path: 'home',
15 | icon:
16 | },
17 | {
18 | title: '404',
19 | path: '404',
20 | icon:
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/src/hooks/useBoolean/index.ts:
--------------------------------------------------------------------------------
1 | import { useState, useMemo } from 'react'
2 |
3 | /* 用于管理 boolean 值的 Hook */
4 |
5 | export interface Actions {
6 | setTrue: () => void
7 | setFalse: () => void
8 | toggle: () => void
9 | }
10 |
11 | const useBoolean = (defaultValue = false): [boolean, Actions] => {
12 | const [state, setState] = useState(defaultValue)
13 |
14 | const actions: Actions = useMemo(() => {
15 | const setTrue = () => setState(true)
16 | const setFalse = () => setState(false)
17 | // 取反
18 | const toggle = () => setState((v) => !v)
19 | return { toggle, setTrue, setFalse }
20 | }, [])
21 |
22 | return [state, actions]
23 | }
24 |
25 | export default useBoolean
26 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Inspector } from 'react-dev-inspector'
4 |
5 | import ErrorBoundary from '@/components/ErrorBoundary'
6 | import App from './App'
7 | import LoginContainer from '@/store/login'
8 |
9 | import * as serviceWorker from './serviceWorker'
10 |
11 | import '@/styles/index.scss'
12 |
13 | const InspectorWrapper = process.env.NODE_ENV === 'development' ? Inspector : Fragment
14 |
15 | ReactDOM.render(
16 |
17 |
18 |
19 |
20 |
21 |
22 | ,
23 | document.getElementById('root')
24 | )
25 |
26 | // If you want your app to work offline and load faster, you can change
27 | // unregister() to register() below. Note this comes with some pitfalls.
28 | // Learn more about service workers: https://bit.ly/CRA-PWA
29 | serviceWorker.unregister()
30 |
--------------------------------------------------------------------------------
/src/layout/AuthorityLayout.scss:
--------------------------------------------------------------------------------
1 | .m-authoritylayout {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | min-height: 100%;
6 | &-content {
7 | margin: 20px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/layout/AuthorityLayout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { Layout } from 'antd'
3 |
4 | import Header from './components/AuthorityHeader'
5 | import Sider from './components/AuthoritySider'
6 | import Footer from './components/Footer'
7 |
8 | import { SIDER_MAX_WIDTH, SIDER_AUTO_SHRINK_WIDTH } from '@/config'
9 |
10 | import './AuthorityLayout.scss'
11 |
12 | const { Content } = Layout
13 |
14 | const AuthorityLayout: React.FC = (props) => {
15 | const { children } = props
16 |
17 | const [collapsed, setCollapsed] = useState(
18 | () => window.innerWidth < SIDER_AUTO_SHRINK_WIDTH
19 | )
20 |
21 | return (
22 |
23 |
24 |
30 |
31 | {children}
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default AuthorityLayout
39 |
--------------------------------------------------------------------------------
/src/layout/NormalLayout.scss:
--------------------------------------------------------------------------------
1 | .m-normallayout {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | min-height: 100%;
6 | &-content {
7 | display: flex;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/layout/NormalLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Layout } from 'antd'
3 |
4 | import Footer from './components/Footer'
5 |
6 | import './NormalLayout.scss'
7 |
8 | const { Content } = Layout
9 |
10 | const NormalLayout: React.FC = (props) => {
11 | const { children } = props
12 | return (
13 |
14 | {children}
15 |
16 |
17 | )
18 | }
19 |
20 | export default NormalLayout
21 |
--------------------------------------------------------------------------------
/src/layout/components/AuthorityHeader/index.scss:
--------------------------------------------------------------------------------
1 | .ant-layout-header.m-header {
2 | padding: 0px;
3 | background-color: #fff;
4 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
5 | }
6 |
7 | .m-header {
8 | &-r {
9 | display: flex;
10 | height: 100%;
11 | }
12 | &-item {
13 | display: flex;
14 | align-items: center;
15 | padding: 0 12px;
16 | cursor: pointer;
17 | transition: all 0.3s;
18 | &:hover {
19 | background: rgba(0, 0, 0, 0.025);
20 | }
21 | }
22 | .username {
23 | margin-left: 8px;
24 | height: 100%;
25 | color: rgba(0, 0, 0, 0.65);
26 | vertical-align: middle;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/layout/components/AuthorityHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useHistory } from 'react-router-dom'
3 | import { Layout, Dropdown, Avatar, Menu, Modal } from 'antd'
4 | import { MenuInfo } from 'rc-menu/es/interface'
5 | import {
6 | LogoutOutlined,
7 | GithubOutlined,
8 | FullscreenExitOutlined,
9 | FullscreenOutlined
10 | } from '@ant-design/icons'
11 | import { useFullscreen } from 'ahooks'
12 |
13 | import LoginContainer from '@/store/login'
14 |
15 | import Logo from '@/assets/images/logo.svg'
16 |
17 | import './index.scss'
18 |
19 | const AuthorityHeader: React.FC = () => {
20 | const { logout, userInfo } = LoginContainer.useContainer()
21 |
22 | const [isFullscreen, { toggleFull }] = useFullscreen(document.body)
23 |
24 | const history = useHistory()
25 |
26 | const onMenuClick = ({ key }: MenuInfo) => {
27 | switch (key) {
28 | case 'logout':
29 | Modal.confirm({
30 | title: '注销',
31 | content: '确定要退出系统吗?',
32 | okText: '确定',
33 | cancelText: '取消',
34 | onOk: () => {
35 | logout().then(() => {
36 | history.push('/login')
37 | })
38 | }
39 | })
40 | break
41 | case 'github':
42 | window.open('https://github.com/maomao1996/react-admin-template')
43 | break
44 | default:
45 | break
46 | }
47 | }
48 |
49 | const menu = (
50 |
60 | )
61 |
62 | return (
63 |
64 |
65 |
66 | {isFullscreen ? : }
67 |
68 |
69 |
70 |
71 |
72 | {userInfo?.nickname}
73 |
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | export default AuthorityHeader
81 |
--------------------------------------------------------------------------------
/src/layout/components/AuthoritySider/index.scss:
--------------------------------------------------------------------------------
1 | .ant-layout-sider.m-sider {
2 | position: fixed !important;
3 | top: 0;
4 | left: 0;
5 | height: 100%;
6 | overflow: auto;
7 | box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
8 | }
9 |
10 | .m-sider {
11 | &-head {
12 | display: flex;
13 | align-items: center;
14 | position: relative;
15 | padding: 0 24px;
16 | overflow: hidden;
17 | height: 60px;
18 | background: #001529;
19 | cursor: pointer;
20 | }
21 | &-logo {
22 | height: 32px;
23 | vertical-align: middle;
24 | }
25 | &-title {
26 | flex: 1;
27 | margin-left: 12px;
28 | color: #fff;
29 | font-weight: 600;
30 | font-size: 20px;
31 | vertical-align: middle;
32 | white-space: nowrap;
33 | }
34 | &-content {
35 | padding: 16px 0;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/layout/components/AuthoritySider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback, useMemo } from 'react'
2 | import { Link, useHistory } from 'react-router-dom'
3 | import { Layout, Menu } from 'antd'
4 | import { SiderProps } from 'antd/es/layout'
5 | import { pathToRegexp } from 'path-to-regexp'
6 |
7 | import { menuConfig } from '@/config/menu'
8 |
9 | import Logo from '@/assets/images/logo.svg'
10 |
11 | import './index.scss'
12 |
13 | const { Sider } = Layout
14 | const { SubMenu, Item } = Menu
15 |
16 | export type MenuProps = {
17 | title: string
18 | path: string
19 | children?: MenuProps[]
20 | } & React.ComponentProps
21 |
22 | // 格式化菜单 path
23 | function formatMenuPath(menuData: MenuProps[], parentPath = '/'): MenuProps[] {
24 | return menuData.map((item) => {
25 | const result = {
26 | ...item,
27 | path: `${parentPath}${item.path}`
28 | }
29 | if (item.children) {
30 | result.children = formatMenuPath(item.children, `${parentPath}${item.path}/`)
31 | }
32 | return result
33 | })
34 | }
35 |
36 | function getFlatMenuKeys(menuData: MenuProps[]): string[] {
37 | return menuData.reduce((keys: string[], item) => {
38 | keys.push(item.path)
39 | if (item.children) {
40 | return keys.concat(getFlatMenuKeys(item.children))
41 | }
42 | return keys
43 | }, [])
44 | }
45 |
46 | function urlToArray(url: string): string[] {
47 | const arr = url.split('/').filter((i) => i)
48 | return arr.map((v, i) => `/${arr.slice(0, i + 1).join('/')}`)
49 | }
50 |
51 | function matchKeys(flatMenuKeys: string[], paths: string[]): string[] {
52 | return paths.reduce(
53 | (arr: string[], path) =>
54 | arr.concat(flatMenuKeys.filter((item) => pathToRegexp(item).test(path))),
55 | []
56 | )
57 | }
58 |
59 | const renderMenu = (data: MenuProps[]) =>
60 | data.map((item) => {
61 | if (item.children) {
62 | return (
63 |
68 | {item.title}
69 |
70 | }
71 | >
72 | {renderMenu(item.children)}
73 |
74 | )
75 | }
76 |
77 | return (
78 | -
79 |
80 | {item.title}
81 |
82 |
83 | )
84 | })
85 |
86 | const AuthoritySider: React.FC = (props) => {
87 | const {
88 | location: { pathname }
89 | } = useHistory()
90 | const fullPathMenuData = useMemo(() => formatMenuPath(menuConfig), [])
91 | const menuKeys = useMemo(() => getFlatMenuKeys(fullPathMenuData), [fullPathMenuData])
92 |
93 | const selectedKeys = useCallback(() => {
94 | return matchKeys(menuKeys, urlToArray(pathname))
95 | }, [menuKeys, pathname])
96 |
97 | const [openKeys, setOpenKeys] = useState(selectedKeys())
98 |
99 | return (
100 |
101 |
102 |
103 | {!props.collapsed && 管理后台模板
}
104 |
105 |
106 |
115 |
116 |
117 | )
118 | }
119 |
120 | export default AuthoritySider
121 |
--------------------------------------------------------------------------------
/src/layout/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { CopyrightOutlined } from '@ant-design/icons'
3 | import { Layout } from 'antd'
4 |
5 | const Footer: React.FC = () => {
6 | return (
7 |
8 | Copyright 2020 maomao1996 制作
9 |
10 | )
11 | }
12 |
13 | export default memo(Footer)
14 |
--------------------------------------------------------------------------------
/src/pages/404/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Result } from 'antd'
3 |
4 | const NotFound: React.FC = () => (
5 |
6 | )
7 |
8 | export default NotFound
9 |
--------------------------------------------------------------------------------
/src/pages/home/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | // 首页
4 |
5 | const Home: React.FC = () => {
6 | return 首页
7 | }
8 |
9 | export default Home
10 |
--------------------------------------------------------------------------------
/src/pages/login/index.scss:
--------------------------------------------------------------------------------
1 | .login {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | flex: 1;
7 | background: #f0f2f5;
8 | }
9 |
10 | .header {
11 | height: 44px;
12 | line-height: 44px;
13 | a {
14 | text-decoration: none;
15 | }
16 | }
17 |
18 | .logo {
19 | margin-right: 16px;
20 | height: 40px;
21 | vertical-align: top;
22 | }
23 |
24 | .title {
25 | font-weight: 600;
26 | font-size: 30px;
27 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
28 | color: rgba(0, 0, 0, 0.85);
29 | }
30 |
31 | .desc {
32 | margin-top: 12px;
33 | color: rgba(0, 0, 0, 0.45);
34 | }
35 |
36 | .content {
37 | padding: 32px 0;
38 | width: 320px;
39 | }
40 |
41 | .login-prefix-icon {
42 | font-size: 14px;
43 | color: #1890ff;
44 | }
45 |
--------------------------------------------------------------------------------
/src/pages/login/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useHistory } from 'react-router-dom'
3 | import { Form, Input, Button } from 'antd'
4 | import { UserOutlined, LockTwoTone } from '@ant-design/icons'
5 |
6 | import LoginContainer from '@/store/login'
7 |
8 | import logo from '@/assets/images/logo.svg'
9 |
10 | import './index.scss'
11 |
12 | export interface LoginFormData {
13 | username: string
14 | password: string
15 | }
16 |
17 | // 登录页
18 |
19 | const Login: React.FC = () => {
20 | const login = LoginContainer.useContainer()
21 | const history = useHistory()
22 |
23 | const handleSubmit = (values: LoginFormData) => {
24 | console.log(values)
25 | login.login().then(() => {
26 | history.push('/home')
27 | })
28 | }
29 |
30 | return (
31 |
32 |
33 |

34 |
React 后台应用
35 |
36 |
管理系统 React 应用模版
37 |
38 |
53 | }
55 | placeholder="请输入用户名"
56 | />
57 |
58 |
67 | }
69 | placeholder="请输入密码"
70 | />
71 |
72 |
73 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 |
83 | export default Login
84 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/routes/history.ts:
--------------------------------------------------------------------------------
1 | import { createBrowserHistory } from 'history'
2 |
3 | export default createBrowserHistory()
4 |
--------------------------------------------------------------------------------
/src/routes/index.ts:
--------------------------------------------------------------------------------
1 | import { lazy } from 'react'
2 |
3 | import { RouteItem } from '@/components/RenderRouter'
4 |
5 | // 默认路由
6 | export const normalRoutes: RouteItem[] = [
7 | {
8 | path: '/',
9 | component: lazy(() => import('@/layout/NormalLayout')),
10 | routes: [
11 | {
12 | path: '/',
13 | exact: true,
14 | redirect: '/login'
15 | },
16 | {
17 | path: '/login',
18 | component: lazy(() => import('@/pages/login'))
19 | },
20 | {
21 | path: '*',
22 | redirect: '/login'
23 | }
24 | ]
25 | }
26 | ]
27 |
28 | // 权限路由
29 | export const authorizedRoutes: RouteItem[] = [
30 | {
31 | path: '/',
32 | component: lazy(() => import('@/layout/AuthorityLayout')),
33 | routes: [
34 | {
35 | path: '/',
36 | exact: true,
37 | redirect: '/home'
38 | },
39 | {
40 | path: '/home',
41 | component: lazy(() => import('@/pages/home'))
42 | },
43 | {
44 | path: '/404',
45 | component: lazy(() => import('@/pages/404'))
46 | },
47 | {
48 | path: '*',
49 | redirect: '/404'
50 | }
51 | ]
52 | }
53 | ]
54 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
19 | )
20 |
21 | type Config = {
22 | onSuccess?: (registration: ServiceWorkerRegistration) => void
23 | onUpdate?: (registration: ServiceWorkerRegistration) => void
24 | }
25 |
26 | export function register(config?: Config): void {
27 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
28 | // The URL constructor is available in all browsers that support SW.
29 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
30 | if (publicUrl.origin !== window.location.origin) {
31 | // Our service worker won't work if PUBLIC_URL is on a different origin
32 | // from what our page is served on. This might happen if a CDN is used to
33 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
34 | return
35 | }
36 |
37 | window.addEventListener('load', () => {
38 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
39 |
40 | if (isLocalhost) {
41 | // This is running on localhost. Let's check if a service worker still exists or not.
42 | checkValidServiceWorker(swUrl, config)
43 |
44 | // Add some additional logging to localhost, pointing developers to the
45 | // service worker/PWA documentation.
46 | navigator.serviceWorker.ready.then(() => {
47 | console.log(
48 | 'This web app is being served cache-first by a service ' +
49 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
50 | )
51 | })
52 | } else {
53 | // Is not localhost. Just register service worker
54 | registerValidSW(swUrl, config)
55 | }
56 | })
57 | }
58 | }
59 |
60 | function registerValidSW(swUrl: string, config?: Config) {
61 | navigator.serviceWorker
62 | .register(swUrl)
63 | .then((registration) => {
64 | registration.onupdatefound = () => {
65 | const installingWorker = registration.installing
66 | if (installingWorker == null) {
67 | return
68 | }
69 | installingWorker.onstatechange = () => {
70 | if (installingWorker.state === 'installed') {
71 | if (navigator.serviceWorker.controller) {
72 | // At this point, the updated precached content has been fetched,
73 | // but the previous service worker will still serve the older
74 | // content until all client tabs are closed.
75 | console.log(
76 | 'New content is available and will be used when all ' +
77 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
78 | )
79 |
80 | // Execute callback
81 | if (config && config.onUpdate) {
82 | config.onUpdate(registration)
83 | }
84 | } else {
85 | // At this point, everything has been precached.
86 | // It's the perfect time to display a
87 | // "Content is cached for offline use." message.
88 | console.log('Content is cached for offline use.')
89 |
90 | // Execute callback
91 | if (config && config.onSuccess) {
92 | config.onSuccess(registration)
93 | }
94 | }
95 | }
96 | }
97 | }
98 | })
99 | .catch((error) => {
100 | console.error('Error during service worker registration:', error)
101 | })
102 | }
103 |
104 | function checkValidServiceWorker(swUrl: string, config?: Config) {
105 | // Check if the service worker can be found. If it can't reload the page.
106 | fetch(swUrl, {
107 | headers: { 'Service-Worker': 'script' }
108 | })
109 | .then((response) => {
110 | // Ensure service worker exists, and that we really are getting a JS file.
111 | const contentType = response.headers.get('content-type')
112 | if (
113 | response.status === 404 ||
114 | (contentType != null && contentType.indexOf('javascript') === -1)
115 | ) {
116 | // No service worker found. Probably a different app. Reload the page.
117 | navigator.serviceWorker.ready.then((registration) => {
118 | registration.unregister().then(() => {
119 | window.location.reload()
120 | })
121 | })
122 | } else {
123 | // Service worker found. Proceed as normal.
124 | registerValidSW(swUrl, config)
125 | }
126 | })
127 | .catch(() => {
128 | console.log('No internet connection found. App is running in offline mode.')
129 | })
130 | }
131 |
132 | export function unregister(): void {
133 | if ('serviceWorker' in navigator) {
134 | navigator.serviceWorker.ready
135 | .then((registration) => {
136 | registration.unregister()
137 | })
138 | .catch((error) => {
139 | console.error(error.message)
140 | })
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect'
6 |
--------------------------------------------------------------------------------
/src/store/login.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react'
2 | import { createContainer } from 'unstated-next'
3 |
4 | import {
5 | getStorageToken,
6 | setStorageToken,
7 | removeStorageToken,
8 | getStorageUser,
9 | setStorageUser,
10 | removeStorageUser
11 | } from '@/utils'
12 |
13 | export type User = {
14 | userId?: number
15 | nickname?: string
16 | }
17 |
18 | const user: User = {
19 | userId: 1996,
20 | nickname: 'maomao1996'
21 | }
22 |
23 | function useLogin() {
24 | const [isLogin, setIsLogin] = useState(() => !!getStorageToken())
25 | const [userInfo, setUserInfo] = useState(() => getStorageUser())
26 |
27 | // 登录
28 | const login = useCallback(
29 | (): Promise =>
30 | new Promise((resolve) => {
31 | setIsLogin(true)
32 | setStorageToken('maomao1996')
33 |
34 | setUserInfo(user)
35 | setStorageUser(user)
36 |
37 | resolve()
38 | }),
39 | []
40 | )
41 | // 注销
42 | const logout = useCallback(
43 | (): Promise =>
44 | new Promise((resolve) => {
45 | setIsLogin(false)
46 | removeStorageToken()
47 |
48 | setUserInfo({})
49 | removeStorageUser()
50 |
51 | resolve()
52 | }),
53 | []
54 | )
55 |
56 | return { isLogin, userInfo, login, logout }
57 | }
58 |
59 | const LoginContainer = createContainer(useLogin)
60 |
61 | export default LoginContainer
62 |
--------------------------------------------------------------------------------
/src/styles/global.scss:
--------------------------------------------------------------------------------
1 | // 全局样式类
2 |
3 | @each $key in (center, left, right) {
4 | .is-#{$key} {
5 | text-align: $key;
6 | }
7 | }
8 |
9 | .pointer {
10 | cursor: pointer;
11 | }
12 |
13 | .fl {
14 | float: left;
15 | }
16 |
17 | .fr {
18 | float: right;
19 | }
20 |
21 | .w-full {
22 | width: 100%;
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | @import 'reset.scss';
2 | @import 'global.scss';
3 |
4 | html,
5 | body {
6 | font-family: Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial,
7 | sans-serif;
8 | }
9 |
10 | #root,
11 | body,
12 | html {
13 | height: 100%;
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/reset.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | -webkit-tap-highlight-color: transparent;
89 | }
90 |
91 | /* HTML5 display-role reset for older browsers */
92 | article,
93 | aside,
94 | details,
95 | figcaption,
96 | figure,
97 | footer,
98 | header,
99 | hgroup,
100 | menu,
101 | nav,
102 | section {
103 | display: block;
104 | }
105 |
106 | body {
107 | line-height: 1;
108 | }
109 |
110 | ol,
111 | ul {
112 | list-style: none;
113 | }
114 |
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 |
120 | blockquote::before,
121 | blockquote::after,
122 | q::before,
123 | q::after {
124 | content: '';
125 | content: none;
126 | }
127 |
128 | table {
129 | border-collapse: collapse;
130 | border-spacing: 0;
131 | }
132 |
133 | a {
134 | text-decoration: none;
135 | color: inherit;
136 | }
137 |
138 | input[type='number']::-webkit-inner-spin-button,
139 | input[type='number']::-webkit-outer-spin-button {
140 | -webkit-appearance: none;
141 | }
142 |
--------------------------------------------------------------------------------
/src/utils/dom.ts:
--------------------------------------------------------------------------------
1 | export function isHidden(el: HTMLElement): boolean {
2 | const style = window.getComputedStyle(el)
3 | return style.display === 'none'
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/filter.ts:
--------------------------------------------------------------------------------
1 | import { isPlainObject } from './type'
2 |
3 | const DEFAULT_OMITS = [undefined, null, '']
4 | // 过滤对象中的基础属性 默认为 undefined null ''
5 | export const filterObject = (value: unknown, omits: any[] = DEFAULT_OMITS): any => {
6 | if (!isPlainObject(value)) {
7 | return value
8 | }
9 | return Object.keys(value as any).reduce((obj: any, key) => {
10 | const current = (value as any)[key]
11 | if (!omits.includes(current)) {
12 | obj[key] = isPlainObject(current) ? filterObject(current) : current
13 | }
14 | return obj
15 | }, {})
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './request'
2 |
3 | export * from './storage'
4 |
5 | export * from './dom'
6 |
7 | export * from './filter'
8 |
9 | export * from './type'
10 |
--------------------------------------------------------------------------------
/src/utils/request/axios.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
2 |
3 | import GlobalLoading from '@/base/GlobalLoading'
4 | import { filterObject } from '../filter'
5 |
6 | // 处理请求 loading
7 | let loadingCount = 0
8 | function loadingInterceptors(instance: AxiosInstance): void {
9 | // 打开 loading
10 | const openLoading = (config: AxiosRequestConfig): AxiosRequestConfig => {
11 | GlobalLoading.open()
12 | loadingCount++
13 | return config
14 | }
15 | // 关闭 loading
16 | const closeLoading = () => {
17 | loadingCount--
18 | if (loadingCount < 0) {
19 | loadingCount = 0
20 | }
21 | loadingCount === 0 && GlobalLoading.close()
22 | }
23 |
24 | instance.interceptors.request.use(openLoading)
25 | instance.interceptors.response.use(
26 | (response) => {
27 | closeLoading()
28 | return response
29 | },
30 | (e) => {
31 | closeLoading()
32 | throw e
33 | }
34 | )
35 | }
36 |
37 | // 过滤请求参数中的 null undefined ''
38 | function filterInterceptors(instance: AxiosInstance): void {
39 | const filter = (config: AxiosRequestConfig): AxiosRequestConfig => {
40 | if (config.data) {
41 | config.data = filterObject(config.data)
42 | }
43 | if (config.params) {
44 | config.params = filterObject(config.params)
45 | }
46 | return config
47 | }
48 | instance.interceptors.request.use(filter)
49 | }
50 |
51 | export default function createAxiosInstance(baseURL = '', isLoading = false): AxiosInstance {
52 | const instance = axios.create({ baseURL })
53 | filterInterceptors(instance)
54 | isLoading && loadingInterceptors(instance)
55 | return instance
56 | }
57 |
--------------------------------------------------------------------------------
/src/utils/request/index.ts:
--------------------------------------------------------------------------------
1 | import createAxiosInstance from './axios'
2 |
3 | export const request = createAxiosInstance('', true)
4 |
--------------------------------------------------------------------------------
/src/utils/storage/index.ts:
--------------------------------------------------------------------------------
1 | import storage from './storage'
2 | import { User } from '@/store/login'
3 |
4 | const getKey = (key: string): string => `__MM_ADMIN_${key.toUpperCase()}__`
5 |
6 | // token 相关
7 | const TOKEN_KEY: string = getKey('token')
8 | export const getStorageToken = (): string => storage.get(TOKEN_KEY, '') as string
9 | export const setStorageToken = (value: string): void => storage.set(TOKEN_KEY, value)
10 | export const removeStorageToken = (): void => storage.remove(TOKEN_KEY)
11 |
12 | // user 相关
13 | const USER_KEY: string = getKey('user')
14 | export const getStorageUser = (): User => storage.get(USER_KEY, {}) as User
15 | export const setStorageUser = (value: unknown): void => storage.set(USER_KEY, value)
16 | export const removeStorageUser = (): void => storage.remove(USER_KEY)
17 |
--------------------------------------------------------------------------------
/src/utils/storage/storage.ts:
--------------------------------------------------------------------------------
1 | import { isString } from '../type'
2 |
3 | const STORAGE = window.localStorage
4 |
5 | function serialize(value: unknown): string {
6 | return JSON.stringify(value)
7 | }
8 |
9 | function deserialize(value: string | null) {
10 | if (!isString(value)) {
11 | return undefined
12 | }
13 | try {
14 | return JSON.parse(value)
15 | } catch (error) {
16 | return value || undefined
17 | }
18 | }
19 |
20 | // 定义 storage 方法
21 | export default {
22 | set(key: string, value: unknown): void {
23 | STORAGE.setItem(key, serialize(value))
24 | },
25 | get(key: string, defaultValue: unknown): unknown {
26 | const value = deserialize(STORAGE.getItem(key))
27 | return value === undefined ? defaultValue : value
28 | },
29 | remove(key: string): void {
30 | STORAGE.removeItem(key)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/utils/type.ts:
--------------------------------------------------------------------------------
1 | export const isString = (value: unknown): value is string => typeof value === 'string'
2 |
3 | export const objectToString = Object.prototype.toString
4 | export const toTypeString = (value: unknown): string => objectToString.call(value)
5 |
6 | export const isPlainObject = (value: unknown): value is Record =>
7 | toTypeString(value) === '[object Object]'
8 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./paths.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react",
22 | "noFallthroughCasesInSwitch": true
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------