├── src
├── style
│ ├── var.scss
│ ├── theme
│ │ ├── index.scss
│ │ ├── theme.default.scss
│ │ └── theme.dark.scss
│ ├── reset.scss
│ └── common.scss
├── assets
│ ├── fonts
│ │ ├── DIN.otf
│ │ ├── MetroDF.ttf
│ │ ├── YouSheBiaoTiHei.ttf
│ │ └── font.scss
│ ├── images
│ │ ├── logo.png
│ │ ├── avatar.png
│ │ ├── welcome.png
│ │ ├── login_left.png
│ │ ├── welcome01.png
│ │ ├── login_left1.png
│ │ ├── login_left2.png
│ │ ├── login_left3.png
│ │ ├── login_left4.png
│ │ └── login_bg.svg
│ └── iconfont
│ │ ├── iconfont.ttf
│ │ └── iconfont.scss
├── views
│ ├── news-manage
│ │ └── news-list
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ ├── role-manage
│ │ ├── role-list
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ └── rights-list
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ ├── user-manage
│ │ └── user-list
│ │ │ ├── style.js
│ │ │ ├── components
│ │ │ └── UserFormModel.jsx
│ │ │ └── index.jsx
│ ├── home
│ │ ├── style.js
│ │ └── index.jsx
│ ├── error-page
│ │ ├── style.js
│ │ ├── 500.jsx
│ │ ├── 403.jsx
│ │ └── 404.jsx
│ └── login
│ │ ├── index.jsx
│ │ ├── style.js
│ │ └── components
│ │ └── LoginForm.jsx
├── store
│ ├── modules
│ │ ├── index.js
│ │ ├── auth.js
│ │ ├── menu.js
│ │ └── global.js
│ └── index.js
├── layout
│ ├── AuthLayout.jsx
│ ├── style.js
│ ├── components
│ │ ├── LayoutFooter
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ ├── LayoutHeader
│ │ │ ├── components
│ │ │ │ ├── CollapseIcon.jsx
│ │ │ │ ├── BreadcrumbNav.jsx
│ │ │ │ ├── Fullscreen.jsx
│ │ │ │ ├── Language.jsx
│ │ │ │ ├── AssemblySize.jsx
│ │ │ │ ├── Theme.jsx
│ │ │ │ └── AvatarIcon.jsx
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ │ └── LayoutMenu
│ │ │ ├── components
│ │ │ └── Logo.jsx
│ │ │ ├── style.js
│ │ │ └── index.jsx
│ └── index.jsx
├── config
│ └── nprogress.js
├── api
│ ├── index.js
│ └── modules
│ │ └── user.js
├── components
│ ├── Loading
│ │ └── index.jsx
│ └── SwitchDark
│ │ └── index.jsx
├── hooks
│ ├── useTheme.js
│ └── useAuthRoues.js
├── language
│ ├── modules
│ │ ├── zh.js
│ │ └── en.js
│ └── index.js
├── index.js
├── hoc
│ └── AuthRouter.jsx
├── utils
│ ├── request.js
│ └── utils.js
├── App.jsx
└── router
│ └── index.js
├── .env.development
├── public
├── favicon.ico
└── index.html
├── .prettierignore
├── .env.production
├── .env
├── .eslintignore
├── jsconfig.json
├── .editorconfig
├── .gitignore
├── README.md
├── craco.config.js
├── .prettierrc.js
├── .eslintrc.js
└── package.json
/src/style/var.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --p-color: #1890ff
3 | }
4 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | # 接口地址
2 | REACT_APP_API_URL = 'http://localhost:8088/api'
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/fonts/DIN.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/fonts/DIN.otf
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /dist/*
2 | .local
3 | /node_modules/**
4 |
5 | **/*.svg
6 | **/*.sh
7 |
8 | /public/*
9 |
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/assets/fonts/MetroDF.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/fonts/MetroDF.ttf
--------------------------------------------------------------------------------
/src/assets/images/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/avatar.png
--------------------------------------------------------------------------------
/src/assets/images/welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/welcome.png
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | # 公共路径
2 | PUBLIC_URL = '/'
3 |
4 | # 接口地址
5 | REACT_APP_API_URL = 'http://localhost:8088/api'
6 |
--------------------------------------------------------------------------------
/src/assets/iconfont/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/iconfont/iconfont.ttf
--------------------------------------------------------------------------------
/src/assets/images/login_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/login_left.png
--------------------------------------------------------------------------------
/src/assets/images/welcome01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/welcome01.png
--------------------------------------------------------------------------------
/src/assets/images/login_left1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/login_left1.png
--------------------------------------------------------------------------------
/src/assets/images/login_left2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/login_left2.png
--------------------------------------------------------------------------------
/src/assets/images/login_left3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/login_left3.png
--------------------------------------------------------------------------------
/src/assets/images/login_left4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/images/login_left4.png
--------------------------------------------------------------------------------
/src/assets/fonts/YouSheBiaoTiHei.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zzz412/react-admin/HEAD/src/assets/fonts/YouSheBiaoTiHei.ttf
--------------------------------------------------------------------------------
/src/views/news-manage/news-list/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const ListWrapper = styled.div``
4 |
--------------------------------------------------------------------------------
/src/views/role-manage/role-list/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const RoleWrapper = styled.div``
4 |
--------------------------------------------------------------------------------
/src/views/user-manage/user-list/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const ListWrapper = styled.div``
4 |
--------------------------------------------------------------------------------
/src/views/role-manage/rights-list/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const RightsWrapper = styled.div``
4 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # 公共路径
2 | PUBLIC_URL = '/'
3 |
4 | # title
5 | REACT_APP_GLOB_APP_TITLE = 'React-Admin'
6 |
7 | # port
8 | REACT_APP_PORT = 3010
9 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.sh
2 | node_modules
3 | *.md
4 | *.woff
5 | *.ttf
6 | .vscode
7 | .idea
8 | dist
9 | /public
10 | /docs
11 | .husky
12 | .local
13 | /bin
14 | .eslintrc.js
15 | .prettierrc.js
16 | /src/mock/*
17 |
18 |
--------------------------------------------------------------------------------
/src/views/home/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const HomeWrapper = styled.div`
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | height: 100%;
8 | img {
9 | width: 70%;
10 | }
11 | `
12 |
--------------------------------------------------------------------------------
/src/store/modules/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from '@reduxjs/toolkit'
2 | import auth from './auth'
3 | import global from './global'
4 | import menu from './menu'
5 |
6 | const reducers = combineReducers({ auth, global, menu })
7 |
8 | export default reducers
9 |
--------------------------------------------------------------------------------
/src/assets/fonts/font.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: YouSheBiaoTiHei;
3 | src: url("./YouSheBiaoTiHei.ttf");
4 | }
5 | @font-face {
6 | font-family: MetroDF;
7 | src: url("./MetroDF.ttf");
8 | }
9 | @font-face {
10 | font-family: DIN;
11 | src: url("./DIN.otf");
12 | }
13 |
--------------------------------------------------------------------------------
/src/views/news-manage/news-list/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { ListWrapper } from './style'
3 |
4 | const NewsList = memo(() => {
5 | return NewsList
6 | })
7 |
8 | NewsList.displayName = 'NewsList'
9 |
10 | export default NewsList
11 |
--------------------------------------------------------------------------------
/src/views/role-manage/role-list/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { RoleWrapper } from './style'
3 |
4 | const RoleList = memo(() => {
5 | return RoleList
6 | })
7 |
8 | RoleList.displayName = 'RoleList'
9 |
10 | export default RoleList
11 |
--------------------------------------------------------------------------------
/src/layout/AuthLayout.jsx:
--------------------------------------------------------------------------------
1 | import AuthRouter from '@/hoc/AuthRouter'
2 | import React, { memo } from 'react'
3 | import LayoutIndex from '.'
4 |
5 | const AuthLayout = memo(() => {
6 | return (
7 |
8 |
9 |
10 | )
11 | })
12 |
13 | export default AuthLayout
14 |
--------------------------------------------------------------------------------
/src/views/role-manage/rights-list/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { RightsWrapper } from './style'
3 |
4 | const RightsList = memo(() => {
5 | return RightsList
6 | })
7 |
8 | RightsList.displayName = 'RightsList'
9 |
10 | export default RightsList
11 |
--------------------------------------------------------------------------------
/src/config/nprogress.js:
--------------------------------------------------------------------------------
1 | import NProgress from 'nprogress'
2 | import 'nprogress/nprogress.css'
3 |
4 | NProgress.configure({
5 | easing: 'ease', // 动画方式
6 | speed: 500, // 递增进度条速度
7 | showSpinner: false, // 是否显示加载icon
8 | trickleSpeed: 200, // 自动递增间隔
9 | minimum: 0.3 // 初始化百分比
10 | })
11 |
12 | export default NProgress
13 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import RequestHttp from '@/utils/request'
2 |
3 | const config = {
4 | // 默认地址请求地址,可在 .env 开头文件中修改
5 | baseURL: process.env.REACT_APP_API_URL,
6 | // 设置超时时间(10s)
7 | timeout: 10000,
8 | // 跨域时候是否允许携带凭证
9 | withCredentials: false
10 | }
11 |
12 | const http = new RequestHttp(config)
13 |
14 | export default http
15 |
--------------------------------------------------------------------------------
/src/views/error-page/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const NotFoundWrapper = styled.div`
4 | .ant-result {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: center;
9 | height: 100%;
10 | .ant-result-image {
11 | margin: 0;
12 | }
13 | }
14 | `
15 |
--------------------------------------------------------------------------------
/src/views/home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { HomeWrapper } from './style'
3 | import welcome from '@/assets/images/welcome01.png'
4 |
5 | const Home = memo(() => {
6 | return (
7 |
8 |
9 |
10 | )
11 | })
12 |
13 | Home.displayName = 'Home'
14 |
15 | export default Home
16 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "baseUrl": "./",
6 | "moduleResolution": "node",
7 | "paths": {
8 | "@/*": ["src/*"],
9 | "@c/*": ["src/components/*"],
10 | "@u/*": ["src/utils/*"]
11 | },
12 | "jsx": "preserve",
13 | "lib": [
14 | "esnext",
15 | "dom",
16 | "dom.iterable",
17 | "scripthost"
18 | ]
19 | }
20 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # @see: http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*] # 表示所有文件适用
6 | charset = utf-8 # 设置文件字符集为 utf-8
7 | end_of_line = lf # 控制换行类型(lf | cr | crlf)
8 | insert_final_newline = true # 始终在文件末尾插入一个新行
9 | indent_style = tab # 缩进风格(tab | space)
10 | indent_size = 2 # 缩进大小
11 | max_line_length = 130 # 最大行长度
12 |
13 | [*.md] # 表示仅 md 文件适用以下规则
14 | max_line_length = off # 关闭最大行长度限制
15 | trim_trailing_whitespace = false # 关闭末尾空格修剪
16 |
--------------------------------------------------------------------------------
/src/components/Loading/index.jsx:
--------------------------------------------------------------------------------
1 | import { Spin } from 'antd'
2 | import React, { memo } from 'react'
3 | import styled from 'styled-components'
4 |
5 | const LoadingWrapper = styled.div`
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | height: 100%;
10 | `
11 |
12 | const Loading = memo(() => {
13 | return (
14 |
15 |
16 |
17 | )
18 | })
19 |
20 | Loading.displayName = 'Loading'
21 |
22 | export default Loading
23 |
--------------------------------------------------------------------------------
/src/store/modules/auth.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit'
2 |
3 | const AuthSlice = createSlice({
4 | name: 'auth',
5 | initialState: {
6 | authRouter: [],
7 | authButtons: []
8 | },
9 | reducers: {
10 | setAuthRouter(state, { payload }) {
11 | state.authRouter = payload
12 | },
13 | setAuthButtons(state, { payload }) {
14 | state.authButtons = payload
15 | }
16 | }
17 | })
18 |
19 | export const { setAuthRouter, setAuthButtons } = AuthSlice.actions
20 |
21 | export default AuthSlice.reducer
22 |
--------------------------------------------------------------------------------
/src/layout/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const LayoutWrapper = styled.div`
4 | display: flex;
5 | min-width: 950px;
6 | height: 100%;
7 |
8 | .ant-layout {
9 | background-color: #fff;
10 |
11 | /* 侧边栏 */
12 | .ant-layout-sider {
13 | border-right: 1px solid #e4e7ed;
14 | box-sizing: border-box;
15 | }
16 |
17 | /* 内容 */
18 | .ant-layout-content {
19 | box-sizing: border-box;
20 | flex: 1;
21 | padding: 10px 12px;
22 | /* overflow-x: hidden; */
23 | }
24 | }
25 | `
26 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutFooter/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const FooterWrapper = styled.div`
4 | /* 底部 */
5 | .ant-layout-footer {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | background-color: #fff;
10 | height: 30px;
11 | border-top: 1px solid #e4e7ed;
12 | padding: 0;
13 | a {
14 | font-size: 14px;
15 | color: rgba(0, 0, 0, 0.85);
16 | text-decoration: none;
17 | letter-spacing: 0.5px;
18 | white-space: nowrap;
19 | }
20 | }
21 | `
22 |
--------------------------------------------------------------------------------
/src/hooks/useTheme.js:
--------------------------------------------------------------------------------
1 | import '@/style/theme/index.scss'
2 |
3 | import { theme } from 'antd'
4 | const useTheme = themeConfig => {
5 | const { isDark } = themeConfig
6 |
7 | // 切换暗黑模式
8 | const body = document.querySelector('body')
9 | body.className = isDark ? 'dark' : ''
10 |
11 | // antd模式切换
12 | const themes = {
13 | algorithm: themeConfig.isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
14 | token: {
15 | colorPrimary: themeConfig.primary
16 | }
17 | }
18 |
19 | return themes
20 | }
21 |
22 | export default useTheme
23 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | React Admin
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/hooks/useAuthRoues.js:
--------------------------------------------------------------------------------
1 | import { filterRouteList } from '@/utils/utils'
2 | import { useMemo } from 'react'
3 | import { useSelector } from 'react-redux'
4 |
5 | export let routes = []
6 |
7 | const useAuthRoutes = (base, async) => {
8 | const authRouter = useSelector(({ auth }) => auth.authRouter)
9 |
10 | const routeList = useMemo(() => {
11 | const dynamicRouter = filterRouteList(async, authRouter)
12 | routes = base.concat(dynamicRouter)
13 | return routes
14 | }, [authRouter])
15 |
16 | return routeList
17 | }
18 |
19 | export default useAuthRoutes
20 |
--------------------------------------------------------------------------------
/src/language/modules/zh.js:
--------------------------------------------------------------------------------
1 | export default {
2 | login: {
3 | confirm: '登录',
4 | reset: '重置'
5 | },
6 | home: {
7 | welcome: '欢迎使用'
8 | },
9 | tabs: {
10 | more: '更多',
11 | closeCurrent: '关闭当前',
12 | closeOther: '关闭其它',
13 | closeAll: '关闭所有'
14 | },
15 | header: {
16 | componentSize: '组件大小',
17 | language: '语言',
18 | theme: '主题',
19 | themeSetting: '主题设置',
20 | darkMode: '暗黑模式',
21 | lightMode: '浅色模式',
22 | fullScreen: '全屏',
23 | exitFullScreen: '退出全屏',
24 | personalData: '个人资料',
25 | changePassword: '修改密码',
26 | logout: '退出登录'
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/language/index.js:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next'
2 | import enUsTrans from './modules/en'
3 | import zhCnTrans from './modules/zh'
4 |
5 | import { initReactI18next } from 'react-i18next'
6 |
7 | i18next.use(initReactI18next).init({
8 | resources: {
9 | en: {
10 | translation: enUsTrans
11 | },
12 | zh: {
13 | translation: zhCnTrans
14 | }
15 | },
16 | // 选择默认语言,选择内容为上述配置中的 key,即 en/zh
17 | fallbackLng: 'zh',
18 | debug: false,
19 | interpolation: {
20 | escapeValue: false // not needed for react as it escapes by default
21 | }
22 | })
23 |
24 | export default i18next
25 |
--------------------------------------------------------------------------------
/.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 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | # Editor directories and files
26 | .vscode/*
27 | !.vscode/extensions.json
28 | !.vscode/settings.json
29 | .idea
30 | .DS_Store
31 | *.suo
32 | *.ntvs*
33 | *.njsproj
34 | *.sln
35 | *.sw?
36 |
--------------------------------------------------------------------------------
/src/assets/iconfont/iconfont.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: iconfont;
3 | src: url('iconfont.ttf?t=1648886414212') format('truetype');
4 | }
5 | .iconfont {
6 | font-family: iconfont !important;
7 | font-size: 16px;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | font-style: normal;
11 | }
12 | .icon-zhongyingwen::before {
13 | content: '\e605';
14 | }
15 | .icon-suoxiao::before {
16 | content: '\e641';
17 | }
18 | .icon-fangda::before {
19 | content: '\e826';
20 | }
21 | .icon-contentright::before {
22 | content: '\e8c9';
23 | }
24 | .icon-zhuti::before {
25 | content: '\e62b';
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client'
2 | import '@/style/reset.scss'
3 | import '@/assets/iconfont/iconfont.scss'
4 | import '@/assets/fonts/font.scss'
5 | import '@/style/common.scss'
6 | import '@/language'
7 |
8 | import { Provider } from 'react-redux'
9 | import { PersistGate } from 'redux-persist/integration/react'
10 | import store, { persistor } from './store'
11 | import App from './App'
12 |
13 | const root = ReactDOM.createRoot(document.querySelector('#root'))
14 |
15 | root.render(
16 |
17 |
18 |
19 |
20 |
21 | )
22 |
--------------------------------------------------------------------------------
/src/style/theme/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | --main-bg-color: #f0f2f5;
3 | --bg-color: #ffffff;
4 | --border-color: #e4e7ed;
5 | --border-header-color: #f6f6f6;
6 | --text-color: rgba(0, 0, 0, 0.85);
7 | --shadow-color: 0 0 12px #0000000d;
8 | --scrollbar-bg-color: #dddee0;
9 | &.dark {
10 | --main-bg-color: #141414;
11 | --bg-color: #1f1f1f;
12 | --border-color: #414243;
13 | --border-header-color: #414243;
14 | --text-color: #d9d9d9;
15 | --shadow-color: 5px 5px 15px rgba(255, 255, 255, 0.2);
16 | --scrollbar-bg-color: #686868;
17 | }
18 | }
19 |
20 | @import url('./theme.dark.scss');
21 | @import url('./theme.default.scss');
22 |
--------------------------------------------------------------------------------
/src/store/modules/menu.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit'
2 |
3 | const MenuSlice = createSlice({
4 | name: 'menu',
5 | initialState: {
6 | isCollapse: false,
7 | menuList: [],
8 | breadcrumbList: []
9 | },
10 | reducers: {
11 | changeCollapse(state, { payload }) {
12 | state.isCollapse = payload
13 | },
14 | setMenuListAction(state, { payload }) {
15 | state.menuList = payload
16 | },
17 | setBreadcrumbList(state, { payload }) {
18 | state.breadcrumbList = payload
19 | }
20 | }
21 | })
22 |
23 | export const { changeCollapse, setMenuListAction, setBreadcrumbList } = MenuSlice.actions
24 |
25 | export default MenuSlice.reducer
26 |
--------------------------------------------------------------------------------
/src/language/modules/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | login: {
3 | confirm: 'Login',
4 | reset: 'Reset'
5 | },
6 | home: {
7 | welcome: 'Welcome'
8 | },
9 | tabs: {
10 | more: 'More',
11 | closeCurrent: 'Current',
12 | closeOther: 'Other',
13 | closeAll: 'All'
14 | },
15 | header: {
16 | componentSize: 'Component Size',
17 | language: 'Language',
18 | theme: 'theme',
19 | themeSetting: 'Theme setting',
20 | darkMode: 'Dark Mode',
21 | lightMode: 'Light Mode',
22 | fullScreen: 'Full Screen',
23 | exitFullScreen: 'Exit Full Screen',
24 | personalData: 'Personal Data',
25 | changePassword: 'Change Password',
26 | logout: 'Logout'
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/SwitchDark/index.jsx:
--------------------------------------------------------------------------------
1 | import { setThemeConfig } from '@/store/modules/global'
2 | import { Switch } from 'antd'
3 | import React, { memo } from 'react'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 |
6 | const SwitchDark = memo(() => {
7 | const { themeConfig } = useSelector(({ global }) => ({ themeConfig: global.themeConfig }), shallowEqual)
8 |
9 | const dispatch = useDispatch()
10 | const onChange = checked => {
11 | dispatch(setThemeConfig({ isDark: checked }))
12 | }
13 |
14 | return
15 | })
16 |
17 | export default SwitchDark
18 |
--------------------------------------------------------------------------------
/src/views/error-page/500.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { NotFoundWrapper } from './style'
3 | import { Button, Result } from 'antd'
4 | import { useNavigate } from 'react-router-dom'
5 |
6 | const NotFound = memo(() => {
7 | const navigate = useNavigate()
8 | const goHome = () => {
9 | navigate('/home')
10 | }
11 |
12 | return (
13 |
14 |
20 | 返回主页
21 |
22 | }
23 | />
24 |
25 | )
26 | })
27 |
28 | NotFound.displayName = 'NotFound'
29 |
30 | export default NotFound
31 |
--------------------------------------------------------------------------------
/src/views/error-page/403.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { NotFoundWrapper } from './style'
3 | import { Button, Result } from 'antd'
4 | import { useNavigate } from 'react-router-dom'
5 |
6 | const NotFound = memo(() => {
7 | const navigate = useNavigate()
8 | const goHome = () => {
9 | navigate('/home')
10 | }
11 |
12 | return (
13 |
14 |
20 | 返回主页
21 |
22 | }
23 | />
24 |
25 | )
26 | })
27 |
28 | NotFound.displayName = 'NotFound'
29 |
30 | export default NotFound
31 |
--------------------------------------------------------------------------------
/src/views/error-page/404.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { NotFoundWrapper } from './style'
3 | import { Button, Result } from 'antd'
4 | import { useNavigate } from 'react-router-dom'
5 |
6 | const NotFound = memo(() => {
7 | const navigate = useNavigate()
8 | const goHome = () => {
9 | navigate('/home')
10 | }
11 |
12 | return (
13 |
14 |
20 | 返回主页
21 |
22 | }
23 | />
24 |
25 | )
26 | })
27 |
28 | NotFound.displayName = 'NotFound'
29 |
30 | export default NotFound
31 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/CollapseIcon.jsx:
--------------------------------------------------------------------------------
1 | import { changeCollapse } from '@/store/modules/menu'
2 | import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
3 | import React, { memo } from 'react'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 |
6 | const CollapseIcon = memo(() => {
7 | const { isCollapse } = useSelector(({ menu }) => ({ isCollapse: menu.isCollapse }), shallowEqual)
8 | const dispatch = useDispatch()
9 |
10 | return (
11 | dispatch(changeCollapse(!isCollapse))}>
12 | {isCollapse ? : }
13 |
14 | )
15 | })
16 |
17 | export default CollapseIcon
18 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutMenu/components/Logo.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import logo from '@/assets/images/avatar.png'
3 | import { shallowEqual, useSelector } from 'react-redux'
4 | import { useNavigate } from 'react-router-dom'
5 |
6 | const Logo = memo(() => {
7 | const { isCollapse } = useSelector(({ menu }) => ({ isCollapse: menu.isCollapse }), shallowEqual)
8 |
9 | const navigate = useNavigate()
10 | const goHome = () => {
11 | navigate('/home')
12 | }
13 |
14 | return (
15 |
16 |

17 | {!isCollapse &&
React Admin
}
18 |
19 | )
20 | })
21 |
22 | export default Logo
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-Admin
2 | ## 1. 简介
3 | :facepunch: **React-Admin** 基于React18、React-RouterV6、Redux/RTK、CRA脚手架、AntD5开源的一套后台管理模板。
4 |
5 | 后续可直接通过`clone`项目进行二次开发
6 |
7 |
8 |
9 | ## 2. 项目开发文档
10 |
11 | 制作中 -- :sob::sob:
12 |
13 |
14 |
15 | ## 3. 预览地址
16 |
17 | + link `http://it.zeng.pub/react-admin`
18 |
19 |
20 |
21 | ## 4. :secret: 项目功能
22 |
23 | + 采用最新技术找开发:React18、React-Router v6、React-Hooks、RTK
24 | + 项目基于CRA脚手架构建 CRACO作为项目自定义配置
25 | + 使用RTK作为状态管理工具 并集成了数据持久化
26 | + 支持Antd组件大小切换、暗黑模式、i18n国际化
27 | + 使用HOC进行路由权限拦截、动态路由生成、动态菜单配置
28 | + 支持ReactRouterV6路由懒加载、伸缩菜单、无限极菜单、面包屑导航
29 | + 使用 Prettier 统一格式化代码,集成 Eslint代码校验规范(**可自行更改配置**)
30 |
31 |
32 |
33 | ## 5. 快速开始
34 |
35 | 1. clone项目
36 | 2. install依赖 `npm i `
37 | 3. 启动项目 `npm run dev`
--------------------------------------------------------------------------------
/src/layout/components/LayoutFooter/index.jsx:
--------------------------------------------------------------------------------
1 | import { Layout } from 'antd'
2 | import React, { memo } from 'react'
3 | import { useSelector } from 'react-redux'
4 | import { FooterWrapper } from './style'
5 |
6 | const LayoutFooter = memo(() => {
7 | const { Footer } = Layout
8 | const isShow = useSelector(({ global }) => global.themeConfig.footer)
9 |
10 | return (
11 | <>
12 | {isShow && (
13 |
14 |
19 |
20 | )}
21 | >
22 | )
23 | })
24 |
25 | LayoutFooter.displayName = 'LayoutFooter'
26 |
27 | export default LayoutFooter
28 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from 'redux-persist'
3 | import storage from 'redux-persist/lib/storage'
4 | import reducers from './modules'
5 |
6 | const persistConfig = { key: 'root', version: 1, storage }
7 |
8 | const persistedReducer = persistReducer(persistConfig, reducers)
9 |
10 | const store = configureStore({
11 | reducer: persistedReducer,
12 | middleware: getDefaultMiddleware =>
13 | getDefaultMiddleware({
14 | serializableCheck: {
15 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
16 | }
17 | })
18 | })
19 |
20 | export const persistor = persistStore(store)
21 |
22 | export default store
23 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | require('dotenv').config()
3 |
4 | const resolve = pathname => path.resolve(__dirname, pathname)
5 |
6 | module.exports = {
7 | devServer: {
8 | port: process.env.REACT_APP_PORT,
9 | // 本地服务的响应头设置
10 | headers: {
11 | // 允许跨域
12 | 'Access-Control-Allow-Origin': '*'
13 | }
14 | },
15 | webpack: {
16 | alias: {
17 | '@': resolve('src'),
18 | '@c': resolve('src/components'),
19 | '@u': resolve('src/utils')
20 | },
21 | configure(webpackConfig) {
22 | // 配置扩展扩展名
23 | webpackConfig.resolve.extensions = [...webpackConfig.resolve.extensions, ...['.scss', '.css']]
24 | // 配置公共路径
25 | webpackConfig.output.publicPath = process.env.PUBLIC_URL
26 | return webpackConfig
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/BreadcrumbNav.jsx:
--------------------------------------------------------------------------------
1 | import { Breadcrumb } from 'antd'
2 | import React, { memo } from 'react'
3 | import { useMemo } from 'react'
4 | import { useSelector } from 'react-redux'
5 | import { Link, useLocation } from 'react-router-dom'
6 |
7 | const BreadcrumbNav = memo(() => {
8 | const { pathname } = useLocation()
9 | const isShow = useSelector(({ global }) => global.themeConfig.breadcrumb)
10 | const breadcrumbList = useSelector(({ menu }) => menu.breadcrumbList[pathname])
11 |
12 | const items = useMemo(() => {
13 | const list = breadcrumbList.map(item => ({ title: item !== '首页' ? item : null }))
14 | list.unshift({ title: 首页 })
15 | return list
16 | }, [breadcrumbList])
17 |
18 | return <>{isShow && }>
19 | })
20 |
21 | export default BreadcrumbNav
22 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/Fullscreen.jsx:
--------------------------------------------------------------------------------
1 | import screenfull from 'screenfull'
2 | import classNames from 'classnames'
3 | import React, { memo, useEffect, useState } from 'react'
4 | import { message } from 'antd'
5 |
6 | const Fullscreen = memo(() => {
7 | const [fullScreen, setFullScreen] = useState(screenfull.isFullscreen)
8 |
9 | useEffect(() => {
10 | screenfull.on('change', () => {
11 | setFullScreen(screenfull.isFullscreen)
12 | })
13 | return () => screenfull.off('change')
14 | }, [])
15 |
16 | const handleFullScreen = () => {
17 | if (!screenfull.isEnabled) message.warning('当前您的浏览器不支持全屏 ❌')
18 | screenfull.toggle()
19 | }
20 |
21 | return (
22 |
23 | )
24 | })
25 |
26 | export default Fullscreen
27 |
--------------------------------------------------------------------------------
/src/api/modules/user.js:
--------------------------------------------------------------------------------
1 | // * 用户模块
2 | import http from '..'
3 |
4 | // 用户登录
5 | export const loginApi = params => http.get('/login', params)
6 |
7 | // 获取菜单列表
8 | export const menuListApi = () => http.get('/rights')
9 |
10 | // 获取用户列表
11 | export const userListApi = () => http.get('/users', { _expand: 'role' })
12 |
13 | // 修改用户状态
14 | export const userStateApi = (id, state) => http.patch(`/users/${id}`, { roleState: state })
15 |
16 | // 获取区域列表
17 | export const regionsListApi = () => http.get(`/regions`)
18 |
19 | // 获取角色列表
20 | export const roleListApi = () => http.get(`/roles`)
21 |
22 | // 新增用户
23 | export const addUserApi = value => http.post('/users', { ...value, roleState: true, default: false })
24 |
25 | // 更新用户
26 | export const updateUserApi = (id, value) => http.patch(`/users/${id}`, value)
27 |
28 | // 删除用户
29 | export const removeUserApi = id => http.delete(`/users/${id}`)
30 |
--------------------------------------------------------------------------------
/src/views/login/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import loginLeft from '@/assets/images/login_left.png'
3 | import logo from '@/assets/images/logo.png'
4 | import { LoginWrapper } from './style'
5 | import LoginForm from './components/LoginForm'
6 | import SwitchDark from '@/components/SwitchDark'
7 |
8 | const Login = memo(() => {
9 | return (
10 |
11 |
12 |
13 |
14 |

15 |
16 |
17 |
18 |

19 |
React-Admin
20 |
21 |
22 |
23 |
24 |
25 | )
26 | })
27 |
28 | Login.displayName = 'Login'
29 |
30 | export default Login
31 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/Language.jsx:
--------------------------------------------------------------------------------
1 | import { setLanguage } from '@/store/modules/global'
2 | import { Dropdown } from 'antd'
3 | import React, { memo } from 'react'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 |
6 | const Language = memo(() => {
7 | const { language } = useSelector(({ global }) => ({ language: global.language }), shallowEqual)
8 | const dispatch = useDispatch()
9 |
10 | const items = [
11 | {
12 | key: 'zh',
13 | label: 简体中文,
14 | disabled: language === 'zh'
15 | },
16 | {
17 | key: 'en',
18 | label: English,
19 | disabled: language === 'en'
20 | }
21 | ]
22 |
23 | const onClick = ({ key }) => {
24 | dispatch(setLanguage(key))
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 | )
32 | })
33 |
34 | export default Language
35 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const HeaderWrapper = styled.div`
4 | /* 头部 */
5 | .ant-layout-header {
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 | border-bottom: 1px solid #f6f6f6;
10 | height: 55px;
11 | padding: 0 40px 0 20px;
12 | background-color: #fff;
13 | .header-lf {
14 | display: flex;
15 | align-items: center;
16 |
17 | .collapsed {
18 | margin-right: 20px;
19 | font-size: 18px;
20 | cursor: pointer;
21 | transition: color 0.3s;
22 | }
23 |
24 | .ant-breadcrumb a {
25 | color: inherit;
26 | }
27 | }
28 |
29 | .header-ri {
30 | display: flex;
31 | align-items: center;
32 |
33 | .icon-style {
34 | margin-right: 22px;
35 | font-size: 19px;
36 | line-height: 19px;
37 | cursor: pointer;
38 | }
39 |
40 | .username {
41 | margin: 0 20px 0 0;
42 | font-size: 15px;
43 | }
44 |
45 | .ant-avatar {
46 | cursor: pointer;
47 | }
48 | }
49 | }
50 | `
51 |
--------------------------------------------------------------------------------
/src/store/modules/global.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit'
2 |
3 | const GLobalSlice = createSlice({
4 | name: 'global',
5 | initialState: {
6 | token: '',
7 | userinfo: '',
8 | language: 'zh',
9 | assemblySize: 'middle',
10 | themeConfig: {
11 | // 默认主题颜色
12 | primary: '#1890ff',
13 | // 深色模式
14 | isDark: false,
15 | // 面包屑导航
16 | breadcrumb: true,
17 | // 页脚
18 | footer: true
19 | }
20 | },
21 | reducers: {
22 | setToken(state, { payload }) {
23 | state.token = payload
24 | },
25 | setUserinfo(state, { payload }) {
26 | state.userinfo = payload
27 | },
28 | setAssemblySize(state, { payload }) {
29 | state.assemblySize = payload
30 | },
31 | setLanguage(state, { payload }) {
32 | state.language = payload
33 | },
34 | setThemeConfig(state, { payload }) {
35 | state.themeConfig = { ...state.themeConfig, ...payload }
36 | }
37 | }
38 | })
39 |
40 | export const { setToken, setUserinfo, setAssemblySize, setLanguage, setThemeConfig } = GLobalSlice.actions
41 |
42 | export default GLobalSlice.reducer
43 |
--------------------------------------------------------------------------------
/src/hoc/AuthRouter.jsx:
--------------------------------------------------------------------------------
1 | import { routes } from '@/hooks/useAuthRoues'
2 | import store from '@/store'
3 | import { searchRoute } from '@/utils/utils'
4 | import React, { memo } from 'react'
5 | import { Navigate, useLocation } from 'react-router-dom'
6 |
7 | const AuthRouter = memo(({ children }) => {
8 | const { pathname } = useLocation()
9 |
10 | const route = searchRoute(pathname, routes)
11 |
12 | // * 判断当前路由是否需要权限(不需要直接放行)
13 | if (!route?.meta?.requiresAuth) return children
14 |
15 | // * 判断是否有Token
16 | const token = store.getState().global.token
17 | if (!token) return
18 |
19 | // // * 动态路由(当前用户的能访问的路由)
20 | // const dynamicRouter = store.getState().auth.authRouter
21 | // // * 静态路由(静态路由【默认的页面】)
22 | // const stateRouter = ['/home']
23 | // const routerList = dynamicRouter.concat(stateRouter)
24 | // // * 判断访问地址是否在路由表中
25 | // if (!routerList.includes(pathname)) return
26 |
27 | // * 当前账号有权限 正常访问页面
28 | return children
29 | })
30 |
31 | AuthRouter.displayName = 'AuthRouter'
32 |
33 | export default AuthRouter
34 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/AssemblySize.jsx:
--------------------------------------------------------------------------------
1 | import { setAssemblySize } from '@/store/modules/global'
2 | import { Dropdown } from 'antd'
3 | import React, { memo } from 'react'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 |
6 | const AssemblySize = memo(() => {
7 | const { assemblySize } = useSelector(({ global }) => ({ assemblySize: global.assemblySize }), shallowEqual)
8 | const dispatch = useDispatch()
9 |
10 | const items = [
11 | {
12 | key: 'middle',
13 | disabled: assemblySize === 'middle',
14 | label: 默认
15 | },
16 | {
17 | disabled: assemblySize === 'large',
18 | key: 'large',
19 | label: 大型
20 | },
21 | {
22 | disabled: assemblySize === 'small',
23 | key: 'small',
24 | label: 小型
25 | }
26 | ]
27 |
28 | const onClick = ({ key }) => {
29 | dispatch(setAssemblySize(key))
30 | }
31 |
32 | return (
33 |
34 |
35 |
36 | )
37 | })
38 |
39 | export default AssemblySize
40 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/index.jsx:
--------------------------------------------------------------------------------
1 | import { Layout } from 'antd'
2 | import React, { memo } from 'react'
3 | import { useSelector } from 'react-redux'
4 | import AssemblySize from './components/AssemblySize'
5 | import AvatarIcon from './components/AvatarIcon'
6 | import BreadcrumbNav from './components/BreadcrumbNav'
7 | import CollapseIcon from './components/CollapseIcon'
8 | import Fullscreen from './components/Fullscreen'
9 | import Language from './components/Language'
10 | import Theme from './components/Theme'
11 | import { HeaderWrapper } from './style'
12 |
13 | const LayoutHeader = memo(() => {
14 | const { username } = useSelector(({ global }) => global.userinfo)
15 | const { Header } = Layout
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
{username}
29 |
30 |
31 |
32 |
33 | )
34 | })
35 |
36 | LayoutHeader.displayName = 'LayoutHeader'
37 |
38 | export default LayoutHeader
39 |
--------------------------------------------------------------------------------
/src/layout/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react'
2 | import { Layout } from 'antd'
3 | import { LayoutWrapper } from './style'
4 | import LayoutMenu from './components/LayoutMenu'
5 | import LayoutHeader from './components/LayoutHeader'
6 | import { Outlet } from 'react-router-dom'
7 | import LayoutFooter from './components/LayoutFooter'
8 | import { shallowEqual, useSelector } from 'react-redux'
9 | import { Suspense } from 'react'
10 | import Loading from '@/components/Loading'
11 |
12 | const LayoutIndex = memo(() => {
13 | const { Sider, Content } = Layout
14 | const { isCollapse } = useSelector(({ menu }) => ({ isCollapse: menu.isCollapse }), shallowEqual)
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | }>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | )
34 | })
35 |
36 | LayoutIndex.displayName = 'Layout'
37 |
38 | export default LayoutIndex
39 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | // @see: https://www.prettier.cn
2 |
3 | module.exports = {
4 | // 超过最大值换行
5 | printWidth: 130,
6 | // 缩进字节数
7 | tabWidth: 2,
8 | // 使用制表符而不是空格缩进行
9 | useTabs: true,
10 | // 结尾不用分号(true有,false没有)
11 | semi: false,
12 | // 使用单引号(true单双引号,false双引号)
13 | singleQuote: true,
14 | // 更改引用对象属性的时间 可选值""
15 | quoteProps: "as-needed",
16 | // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
17 | bracketSpacing: true,
18 | // 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"",默认none
19 | trailingComma: "none",
20 | // 在JSX中使用单引号而不是双引号
21 | jsxSingleQuote: true,
22 | // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 ,always:不省略括号
23 | arrowParens: "avoid",
24 | // 如果文件顶部已经有一个 doclock,这个选项将新建一行注释,并打上@format标记。
25 | insertPragma: false,
26 | // 指定要使用的解析器,不需要写文件开头的 @prettier
27 | requirePragma: false,
28 | // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
29 | proseWrap: "preserve",
30 | // 在html中空格是否是敏感的 "css" - 遵守CSS显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的
31 | htmlWhitespaceSensitivity: "css",
32 | // 换行符使用 lf 结尾是 可选值""
33 | endOfLine: "auto",
34 | // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
35 | rangeStart: 0,
36 | rangeEnd: Infinity,
37 | // Vue文件脚本和样式标签缩进
38 | vueIndentScriptAndStyle: false
39 | };
40 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // @see: http://eslint.cn
2 | module.exports = {
3 | settings: {
4 | react: {
5 | version: 'detect'
6 | }
7 | },
8 | root: true,
9 | env: {
10 | browser: true,
11 | node: true,
12 | es6: true
13 | },
14 | /* 优先级低于 parse 的语法解析配置 */
15 | parserOptions: {
16 | ecmaVersion: 2020,
17 | sourceType: 'module',
18 | jsxPragma: 'React',
19 | ecmaFeatures: {
20 | jsx: true
21 | }
22 | },
23 | // plugins: ['react', 'react-hooks', 'prettier'],
24 | /* 继承某些已有的规则 */
25 | extends: ['react-app', 'plugin:prettier/recommended'],
26 | /*
27 | * "off" 或 0 ==> 关闭规则
28 | * "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行)
29 | * "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错)
30 | */
31 | rules: {
32 | // eslint (http://eslint.cn/docs/rules)
33 | 'no-var': 'error', // 要求使用 let 或 const 而不是 var
34 | 'no-multiple-empty-lines': ['error', { max: 1 }], // 不允许多个空行
35 | 'no-use-before-define': 'off', // 禁止在 函数/类/变量 定义之前使用它们
36 | 'prefer-const': 'off', // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const
37 | 'no-irregular-whitespace': 'off', // 禁止不规则的空白
38 | 'import/no-anonymous-default-export': 'off', // 禁止单行默认导出
39 |
40 | // react (https://github.com/jsx-eslint/eslint-plugin-react)
41 | 'react-hooks/rules-of-hooks': 'off',
42 | 'react-hooks/exhaustive-deps': 'off',
43 |
44 | // prettierrc配置
45 | 'prettier/prettier': [
46 | 'error',
47 | {
48 | endOfLine: 'auto',
49 | semi: false,
50 | singleQuote: true,
51 | vueIndentScriptAndStyle: true,
52 | arrowParens: 'avoid',
53 | trailingComma: 'none'
54 | }
55 | ]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutMenu/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const MenuWrapper = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | height: 100%;
7 |
8 | .logo-box {
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | height: 55px;
13 | border-bottom: 1px solid #010b14;
14 | cursor: pointer;
15 |
16 | .logo-img {
17 | width: 30px;
18 | margin: 0;
19 | border-radius: 4px;
20 | overflow: hidden;
21 | }
22 |
23 | .logo-text {
24 | margin: 0 0 0 10px;
25 | font-size: 24px;
26 | font-weight: bold;
27 | color: #dadada;
28 | white-space: nowrap;
29 | }
30 | }
31 |
32 | /* menu菜单 */
33 | .ant-menu-root {
34 | flex: 1;
35 | overflow-x: hidden;
36 | overflow-y: auto;
37 | }
38 |
39 | .ant-menu {
40 | border-right: 0;
41 |
42 | .ant-menu-submenu-title,
43 | .ant-menu-item {
44 | border-radius: 0;
45 | margin: 0;
46 | height: 48px;
47 | line-height: 48px;
48 | width: 100%;
49 | }
50 |
51 | &::-webkit-scrollbar {
52 | background-color: #001529;
53 | }
54 | &::-webkit-scrollbar-thumb {
55 | background-color: #41444b;
56 | }
57 | }
58 |
59 | /* 去除菜单 Loading 遮罩层 */
60 | .ant-spin-nested-loading,
61 | .ant-spin-container {
62 | display: flex;
63 | flex-direction: column;
64 | height: 100%;
65 | .ant-spin {
66 | max-height: 100% !important;
67 | }
68 | .ant-spin-container::after {
69 | background: transparent !important;
70 | }
71 | .ant-spin-blur {
72 | overflow: auto !important;
73 | clear: none !important;
74 | opacity: 1 !important;
75 | }
76 | }
77 | `
78 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import NProgress from '@/config/nprogress'
2 | import { message } from 'antd'
3 | import axios from 'axios'
4 |
5 | class RequestHttp {
6 | constructor(config) {
7 | // 实例化axios
8 | this.service = axios.create(config)
9 |
10 | // 请求拦截器
11 | this.service.interceptors.request.use(
12 | config => {
13 | NProgress.start()
14 | return config
15 | },
16 | error => {
17 | return Promise.reject(error)
18 | }
19 | )
20 |
21 | // 响应拦截器
22 | this.service.interceptors.response.use(
23 | res => {
24 | const { data } = res
25 | NProgress.done()
26 | // * 错误信息拦截 【没有code 或者 code不为200】
27 | if (data.code && data.code !== 200) {
28 | message.error(data.msg)
29 | return Promise.reject(data)
30 | }
31 | // * 成功请求
32 | return data
33 | },
34 | error => {
35 | // const { response } = error
36 | NProgress.done()
37 | // 请求超时单独判断,请求超时没有 response
38 | if (error.message.indexOf('timeout') !== -1) message.error('请求超时,请稍后再试')
39 | // 服务器结果都没有返回(可能服务器错误可能客户端断网) 断网处理:可以跳转到断网页面
40 | if (!window.navigator.onLine) window.location.hash = '/500'
41 | return Promise.reject(error)
42 | }
43 | )
44 | }
45 |
46 | get(url, params, config) {
47 | return this.service.get(url, { params, ...config })
48 | }
49 |
50 | post(url, params, config) {
51 | return this.service.post(url, params, config)
52 | }
53 |
54 | put(url, params, config) {
55 | return this.service.put(url, params, config)
56 | }
57 |
58 | delete(url, params, config) {
59 | return this.service.delete(url, { params, ...config })
60 | }
61 |
62 | patch(url, params, config) {
63 | return this.service.patch(url, params, config)
64 | }
65 | }
66 |
67 | export default RequestHttp
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-admin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@ant-design/icons": "^5.0.1",
7 | "@reduxjs/toolkit": "^1.9.3",
8 | "@testing-library/jest-dom": "^5.16.5",
9 | "@testing-library/react": "^13.4.0",
10 | "@testing-library/user-event": "^13.5.0",
11 | "antd": "^5.3.2",
12 | "axios": "^1.3.4",
13 | "classnames": "^2.3.2",
14 | "dotenv": "^16.0.3",
15 | "i18next": "^22.4.14",
16 | "immer": "^9.0.21",
17 | "nprogress": "^0.2.0",
18 | "react": "^18.2.0",
19 | "react-dom": "^18.2.0",
20 | "react-i18next": "^12.2.0",
21 | "react-redux": "^8.0.5",
22 | "react-router-dom": "^6.9.0",
23 | "react-scripts": "5.0.1",
24 | "redux-persist": "^6.0.0",
25 | "screenfull": "^6.0.2",
26 | "styled-components": "^5.3.9",
27 | "web-vitals": "^2.1.4"
28 | },
29 | "scripts": {
30 | "dev": "craco start",
31 | "build": "craco build",
32 | "test": "craco test",
33 | "eject": "react-scripts eject"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | },
53 | "devDependencies": {
54 | "@craco/craco": "^7.1.0",
55 | "eslint-config-prettier": "^8.7.0",
56 | "eslint-plugin-prettier": "^4.2.1",
57 | "eslint-plugin-react": "^7.32.2",
58 | "eslint-plugin-react-hooks": "^4.6.0",
59 | "prettier": "^2.8.5",
60 | "sass": "^1.59.3"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useState } from 'react'
2 | import { RouterProvider } from 'react-router-dom'
3 | import { ConfigProvider, App as AppProvider } from 'antd'
4 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
5 | import Router, { baseRoutes, asyncRoutes } from '@/router'
6 | import zhCN from 'antd/lib/locale/zh_CN'
7 | import enUS from 'antd/lib/locale/en_US'
8 | import useTheme from './hooks/useTheme'
9 | import useAuthRoutes from './hooks/useAuthRoues'
10 | import { Suspense } from 'react'
11 | import Loading from './components/Loading'
12 | import { setLanguage } from '@/store/modules/global'
13 | import i18next from 'i18next'
14 |
15 | const App = memo(() => {
16 | const { assemblySize, themeConfig, language } = useSelector(({ global }) => global, shallowEqual)
17 | const [i18nLocale, setI18nLocale] = useState(zhCN)
18 |
19 | const themes = useTheme(themeConfig)
20 | const routeList = useAuthRoutes(baseRoutes, asyncRoutes)
21 |
22 | // 设置 antd 语言国际化
23 | const setAntdLanguage = () => {
24 | // 如果 redux 中有默认语言就设置成 redux 的默认语言,没有默认语言就设置成浏览器默认语言
25 | if (language && language === 'zh') return setI18nLocale(zhCN)
26 | if (language && language === 'en') return setI18nLocale(enUS)
27 | }
28 | const dispatch = useDispatch()
29 | useEffect(() => {
30 | i18next.changeLanguage(language)
31 | dispatch(setLanguage(language))
32 | setAntdLanguage()
33 | }, [language])
34 |
35 | return (
36 |
37 |
38 | }>
39 |
40 |
41 |
42 |
43 | )
44 | })
45 |
46 | export default App
47 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/Theme.jsx:
--------------------------------------------------------------------------------
1 | import SwitchDark from '@/components/SwitchDark'
2 | import { setThemeConfig } from '@/store/modules/global'
3 | import { FireOutlined, SettingOutlined } from '@ant-design/icons'
4 | import { Divider, Drawer, Switch } from 'antd'
5 | import React, { memo, useState } from 'react'
6 | import { shallowEqual, useDispatch, useSelector } from 'react-redux'
7 |
8 | const Theme = memo(() => {
9 | const [open, setOpen] = useState(false)
10 | const { breadcrumb, footer } = useSelector(({ global }) => global.themeConfig, shallowEqual)
11 | const dispatch = useDispatch()
12 |
13 | const onClose = () => {
14 | setOpen(false)
15 | }
16 | // Todo: 界面设置
17 | return (
18 | <>
19 | setOpen(true)}>
20 |
21 |
22 |
23 | 全局主题
24 |
25 |
26 | 暗黑模式
27 |
28 |
29 |
30 |
31 | 界面设置
32 |
33 |
34 | 面包屑导航
35 | dispatch(setThemeConfig({ breadcrumb: checked }))} />
36 |
37 |
38 | 标签栏
39 |
40 |
41 |
42 | 页脚
43 | dispatch(setThemeConfig({ footer: checked }))} />
44 |
45 |
46 | >
47 | )
48 | })
49 |
50 | export default Theme
51 |
--------------------------------------------------------------------------------
/src/style/reset.scss:
--------------------------------------------------------------------------------
1 | /* Reset style sheet */
2 | html,
3 | body,
4 | div,
5 | span,
6 | applet,
7 | object,
8 | iframe,
9 | h1,
10 | h2,
11 | h3,
12 | h4,
13 | h5,
14 | h6,
15 | p,
16 | blockquote,
17 | pre,
18 | a,
19 | abbr,
20 | acronym,
21 | address,
22 | big,
23 | cite,
24 | code,
25 | del,
26 | dfn,
27 | em,
28 | img,
29 | ins,
30 | kbd,
31 | q,
32 | s,
33 | samp,
34 | small,
35 | strike,
36 | strong,
37 | sub,
38 | sup,
39 | tt,
40 | var,
41 | b,
42 | u,
43 | i,
44 | center,
45 | dl,
46 | dt,
47 | dd,
48 | ol,
49 | ul,
50 | li,
51 | fieldset,
52 | form,
53 | label,
54 | legend,
55 | table,
56 | caption,
57 | tbody,
58 | tfoot,
59 | thead,
60 | tr,
61 | th,
62 | td,
63 | article,
64 | aside,
65 | canvas,
66 | details,
67 | embed,
68 | figure,
69 | figcaption,
70 | footer,
71 | header,
72 | hgroup,
73 | menu,
74 | nav,
75 | output,
76 | ruby,
77 | section,
78 | summary,
79 | time,
80 | mark,
81 | audio,
82 | video {
83 | padding: 0;
84 | margin: 0;
85 | font: inherit;
86 | font-size: 100%;
87 | vertical-align: baseline;
88 | border: 0;
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 | body {
106 | padding: 0;
107 | margin: 0;
108 | }
109 | ol,
110 | ul {
111 | list-style: none;
112 | }
113 | blockquote,
114 | q {
115 | quotes: none;
116 | }
117 | blockquote::before,
118 | blockquote::after,
119 | q::before,
120 | q::after {
121 | content: '';
122 | content: none;
123 | }
124 | table {
125 | border-spacing: 0;
126 | border-collapse: collapse;
127 | }
128 | html,
129 | body,
130 | #root {
131 | width: 100%;
132 | height: 100%;
133 | }
134 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutHeader/components/AvatarIcon.jsx:
--------------------------------------------------------------------------------
1 | import { App, Avatar, Dropdown } from 'antd'
2 | import React, { memo } from 'react'
3 | import avatar from '@/assets/images/avatar.png'
4 | import { useNavigate } from 'react-router-dom'
5 | import { ExclamationCircleOutlined } from '@ant-design/icons'
6 | import { setToken, setUserinfo } from '@/store/modules/global'
7 | import { useDispatch } from 'react-redux'
8 | import { setAuthRouter } from '@/store/modules/auth'
9 |
10 | const AvatarIcon = memo(() => {
11 | const items = [
12 | {
13 | key: '1',
14 | label: 首页
15 | },
16 | {
17 | key: '2',
18 | label: 个人信息
19 | },
20 | {
21 | key: '3',
22 | label: 修改密码
23 | },
24 | {
25 | type: 'divider'
26 | },
27 | {
28 | key: '4',
29 | label: 退出登录
30 | }
31 | ]
32 |
33 | // 退出登录
34 | const { modal, message } = App.useApp()
35 | const dispatch = useDispatch()
36 | const logout = () => {
37 | modal.confirm({
38 | title: '温馨提示 🧡',
39 | icon: ,
40 | content: '是否确认退出登录?',
41 | okText: '确认',
42 | cancelText: '取消',
43 | onOk: () => {
44 | dispatch(setToken(''))
45 | dispatch(setUserinfo(''))
46 | dispatch(setAuthRouter([]))
47 | message.success('退出登录成功!')
48 | navigate('/login')
49 | }
50 | })
51 | }
52 |
53 | const navigate = useNavigate()
54 | const onClick = ({ key }) => {
55 | switch (key) {
56 | case '1':
57 | return navigate('/')
58 | case '2':
59 | return
60 | case '3':
61 | return
62 | case '4':
63 | return logout()
64 | default:
65 | return
66 | }
67 | }
68 |
69 | return (
70 |
71 |
72 |
73 | )
74 | })
75 |
76 | export default AvatarIcon
77 |
--------------------------------------------------------------------------------
/src/views/login/style.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import logo from '@/assets/images/login_bg.svg'
3 |
4 | export const LoginWrapper = styled.div`
5 | position: relative;
6 | min-width: 550px;
7 | height: 100%;
8 | min-height: 500px;
9 | background-image: url(${logo});
10 | background-position: 50%;
11 | background-size: cover;
12 | .dark {
13 | position: absolute;
14 | top: 5%;
15 | right: 3.2%;
16 | }
17 |
18 | .login-box {
19 | width: 96%;
20 | height: 94%;
21 | padding: 0 4% 0 20px;
22 | box-sizing: border-box;
23 | overflow: hidden;
24 | border-radius: 10px;
25 | background-color: rgba(255, 255, 255, 0.8);
26 | justify-content: space-around;
27 |
28 | .login-left {
29 | width: 750px;
30 | img {
31 | width: 100%;
32 | height: 100%;
33 | }
34 | }
35 |
36 | .login-form {
37 | padding: 40px 45px 25px;
38 | border-radius: 10px;
39 | background-color: transparent;
40 | box-shadow: 2px 3px 7px rgba(0, 0, 0, 0.2);
41 |
42 | .login-logo {
43 | margin-bottom: 40px;
44 | .login-icon {
45 | width: 70px;
46 | }
47 | .logo-text {
48 | padding-left: 25px;
49 | font-size: 48px;
50 | font-weight: bold;
51 | white-space: nowrap;
52 | color: #475768;
53 | }
54 | }
55 |
56 | .ant-form-item {
57 | height: 75px;
58 | margin-bottom: 0;
59 |
60 | .ant-input-prefix {
61 | margin-right: 10px;
62 | }
63 |
64 | .ant-input-affix-wrapper-lg {
65 | padding: 8.3px 11px;
66 | }
67 |
68 | .ant-input-affix-wrapper,
69 | .ant-input-lg {
70 | font-size: 14px;
71 | }
72 |
73 | .ant-input-affix-wrapper {
74 | color: #bfbfbf;
75 | }
76 | }
77 |
78 | .login-btn {
79 | width: 100%;
80 | margin-top: 10px;
81 | white-space: nowrap;
82 |
83 | .ant-form-item-control-input-content {
84 | display: flex;
85 | justify-content: space-between;
86 |
87 | .ant-btn {
88 | width: 180px;
89 | span {
90 | font-size: 14px;
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 | `
98 |
--------------------------------------------------------------------------------
/src/assets/images/login_bg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/views/user-manage/user-list/components/UserFormModel.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useMemo, useState } from 'react'
2 | import { Form, Input, Modal, Select } from 'antd'
3 | import { addUserApi, updateUserApi, roleListApi, regionsListApi } from '@/api/modules/user'
4 |
5 | const UserFormModel = memo(({ open, onCancel, onOk, defaultValues = {} }) => {
6 | const [form] = Form.useForm()
7 | const [loading, setLoading] = useState(false)
8 | const [roleList, setRoleList] = useState([])
9 | const [regionsList, setRegionsList] = useState([])
10 |
11 | // 初始化函数
12 | const initData = async () => {
13 | const [res1, res2] = await Promise.all([roleListApi(), regionsListApi()])
14 | setRoleList(res1.data)
15 | setRegionsList(res2.data)
16 | }
17 |
18 | useEffect(() => {
19 | initData()
20 | }, [])
21 |
22 | useEffect(() => {
23 | if (open && defaultValues.id) {
24 | form.setFieldsValue(defaultValues)
25 | }
26 | }, [open])
27 |
28 | const formOk = async () => {
29 | try {
30 | const values = await form.validateFields()
31 | setLoading(true)
32 | defaultValues.id ? await updateUserApi(defaultValues.id, values) : await addUserApi(values)
33 | form.resetFields()
34 | onOk && onOk()
35 | } catch (e) {
36 | console.log('校验失败')
37 | } finally {
38 | setLoading(false)
39 | }
40 | }
41 |
42 | const cancel = () => {
43 | form.resetFields()
44 | onCancel && onCancel()
45 | }
46 |
47 | return (
48 |
55 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
65 |
66 |
68 |
69 |
70 | )
71 | })
72 |
73 | UserFormModel.displayName = 'UserFormModel'
74 |
75 | export default UserFormModel
76 |
--------------------------------------------------------------------------------
/src/views/login/components/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { loginApi } from '@/api/modules/user'
2 | import { setAuthRouter } from '@/store/modules/auth'
3 | import { setToken, setUserinfo } from '@/store/modules/global'
4 | import { CloseCircleOutlined, LockOutlined, UserOutlined } from '@ant-design/icons'
5 | import { Button, Form, Input, App } from 'antd'
6 | import React, { memo, useState } from 'react'
7 | import { useDispatch } from 'react-redux'
8 | import { useNavigate } from 'react-router-dom'
9 | import { useTranslation } from 'react-i18next'
10 |
11 | const LoginForm = memo(() => {
12 | const { t } = useTranslation()
13 | const [form] = Form.useForm()
14 | const [loading, setLoading] = useState(false)
15 | const initialValues = { username: 'admin', password: '123456' }
16 | const { message } = App.useApp()
17 |
18 | const navigate = useNavigate()
19 | const dispatch = useDispatch()
20 | // 提交【验证成功】
21 | const onFinish = async loginForm => {
22 | try {
23 | setLoading(true)
24 | const { data } = await loginApi(loginForm)
25 | if (!data.length) return message.error('用户名与密码不匹配')
26 |
27 | const user = data[0]
28 | dispatch(setToken(JSON.stringify(user)))
29 | dispatch(setUserinfo(user))
30 | dispatch(setAuthRouter(user.role.rights))
31 | message.success('登录成功!')
32 | navigate('/home')
33 | } finally {
34 | setLoading(false)
35 | }
36 | }
37 |
38 | // 提交【验证失败】
39 | const onFinishFailed = errorInfo => {
40 | console.log('Failed', errorInfo)
41 | }
42 |
43 | return (
44 |
55 | } />
56 |
57 |
58 | } />
59 |
60 |
61 | } onClick={() => form.resetFields()}>
62 | {t('login.reset')}
63 |
64 | }>
65 | {t('login.confirm')}
66 |
67 |
68 |
69 | )
70 | })
71 |
72 | export default LoginForm
73 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createBrowserRouter, Navigate } from 'react-router-dom'
2 | import Login from '@/views/login'
3 | import NotFound from '@/views/error-page/404'
4 | import Layout from '@/layout/AuthLayout'
5 | import { lazy } from 'react'
6 |
7 | export const baseRoutes = [
8 | { path: '/', element: },
9 | { path: '/login', element: , meta: { title: '登录页' } },
10 | { path: '/404', element: , meta: { title: '404页面' } },
11 | { path: '/403', element: , meta: { title: '403页面' } },
12 | { path: '/500', element: , meta: { title: '500页面' } }
13 | ]
14 |
15 | // 需要权限验证的路由 添加meta属性requiresAuth=true
16 | export const asyncRoutes = [
17 | // 主页
18 | {
19 | element: ,
20 | children: [{ path: '/home', Component: lazy(() => import('@/views/home')), meta: { title: '主页', requiresAuth: true } }]
21 | },
22 | // 用户管理 -> 用户列表
23 | {
24 | element: ,
25 | meta: { title: '用户管理', requiresAuth: true },
26 | children: [
27 | { path: '/user-manage', element: },
28 | {
29 | path: '/user-manage/user-list',
30 | Component: lazy(() => import('@/views/user-manage/user-list')),
31 | meta: { title: '用户列表', requiresAuth: true }
32 | }
33 | ]
34 | },
35 | // 权限管理 -> 角色列表 权限列表
36 | {
37 | element: ,
38 | meta: { title: '权限管理', requiresAuth: true },
39 | children: [
40 | { path: '/right-manage', element: },
41 | {
42 | path: '/right-manage/role-list',
43 | Component: lazy(() => import('@/views/role-manage/role-list')),
44 | meta: { title: '角色列表', requiresAuth: true }
45 | },
46 | {
47 | path: '/right-manage/rights-list',
48 | Component: lazy(() => import('@/views/role-manage/rights-list')),
49 | meta: { title: '权限列表', requiresAuth: true }
50 | }
51 | ]
52 | },
53 | // 新闻管理 -> 新闻列表
54 | {
55 | element: ,
56 | meta: { title: '新闻管理', requiresAuth: true },
57 | children: [
58 | { path: '/news-manage', element: },
59 | {
60 | path: '/news-manage/news-list',
61 | Component: lazy(() => import('@/views/news-manage/news-list')),
62 | meta: { title: '新闻列表', requiresAuth: true }
63 | }
64 | ]
65 | },
66 | { path: '*', element: }
67 | ]
68 |
69 | const Router = routes => {
70 | const router = createBrowserRouter(routes, { basename: process.env.PUBLIC_URL })
71 | return router
72 | }
73 |
74 | export default Router
75 |
--------------------------------------------------------------------------------
/src/style/theme/theme.default.scss:
--------------------------------------------------------------------------------
1 | // 需要自定义覆盖的样式
2 | body {
3 | background-color: var(--main-bg-color);
4 | color: var(--text-color);
5 | #driver-highlighted-element-stage {
6 | background-color: #fff;
7 | }
8 | }
9 |
10 | /* login container 登录样式 */
11 | .login-container {
12 | background-color: #eeeeee !important;
13 | .login-box {
14 | background-color: rgba(255, 255, 255, 0.8) !important;
15 | .login-form {
16 | background-color: transparent !important;
17 | box-shadow: 2px 3px 7px rgba(0, 0, 0, 0.2) !important;
18 | .login-logo {
19 | .logo-text {
20 | color: #475768 !important;
21 | }
22 | }
23 | .login-btn {
24 | .ant-btn-default {
25 | color: #606266 !important;
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
32 | // container
33 | .container {
34 | // sider
35 | .ant-layout-sider {
36 | border-right: 1px solid var(--border-color) !important;
37 | .ant-menu {
38 | &::-webkit-scrollbar {
39 | background-color: #001529 !important;
40 | }
41 | &::-webkit-scrollbar-thumb {
42 | background-color: #41444b !important;
43 | }
44 | }
45 | .logo-box {
46 | border-bottom: 1px solid #010b14 !important;
47 | }
48 | }
49 |
50 | // layout
51 | .ant-layout {
52 | background-color: var(--main-bg-color) !important;
53 |
54 | // 通用
55 | .ant-layout-header,
56 | .tabs,
57 | .footer,
58 | .card {
59 | background-color: var(--bg-color) !important;
60 | border-color: var(--border-color) !important;
61 | }
62 |
63 | // 头部
64 | .ant-layout-header {
65 | background-color: var(--bg-color) !important;
66 | border-color: var(--border-header-color) !important;
67 | .icon-style,
68 | .username {
69 | color: var(--text-color) !important;
70 | }
71 | }
72 |
73 | // 底部
74 | .ant-layout-footer {
75 | background-color: var(--bg-color) !important;
76 | border-color: var(--border-color) !important;
77 | a {
78 | color: var(--text-color) !important;
79 | }
80 | }
81 |
82 | .card {
83 | box-shadow: var(--shadow-color) !important;
84 | .text {
85 | color: #585858 !important;
86 | }
87 | }
88 |
89 | // 内容
90 | .ant-layout-content {
91 | &::-webkit-scrollbar {
92 | background-color: var(--main-bg-color) !important;
93 | }
94 | &::-webkit-scrollbar-thumb {
95 | background-color: var(--scrollbar-bg-color) !important;
96 | }
97 | .card {
98 | &::-webkit-scrollbar {
99 | background-color: var(--bg-color) !important;
100 | }
101 | &::-webkit-scrollbar-thumb {
102 | background-color: var(--scrollbar-bg-color) !important;
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/style/theme/theme.dark.scss:
--------------------------------------------------------------------------------
1 | // 需要自定义覆盖的样式
2 | body.dark {
3 | background-color: var(--main-bg-color);
4 | color: var(--text-color);
5 | #driver-highlighted-element-stage {
6 | background-color: #525457;
7 | }
8 | }
9 |
10 | .dark {
11 | /* login container 登录样式 */
12 | .login-container {
13 | background-color: var(--main-bg-color) !important;
14 | .login-box {
15 | background-color: rgba(0, 0, 0, 0.8) !important;
16 | .login-form {
17 | background-color: var(--main-bg-color) !important;
18 | box-shadow: var(--shadow-color) !important;
19 | .login-logo {
20 | .logo-text {
21 | color: var(--text-color) !important;
22 | }
23 | }
24 | .login-btn {
25 | .ant-btn-default {
26 | color: var(--text-color) !important;
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
33 | // container
34 | .container {
35 | // sider
36 | .ant-layout-sider {
37 | border-right: 1px solid var(--border-color) !important;
38 | background: var(--bg-color);
39 | .ant-menu {
40 | background: var(--bg-color);
41 | &::-webkit-scrollbar {
42 | background-color: var(--bg-color) !important;
43 | }
44 | &::-webkit-scrollbar-thumb {
45 | background-color: var(--scrollbar-bg-color) !important;
46 | }
47 | }
48 | .logo-box {
49 | border-bottom: 1px solid var(--border-color) !important;
50 | }
51 | }
52 |
53 | // layout
54 | .ant-layout {
55 | background-color: var(--main-bg-color) !important;
56 |
57 | // 通用
58 | .ant-layout-header,
59 | .tabs,
60 | .footer,
61 | .card {
62 | background-color: var(--bg-color) !important;
63 | border-color: var(--border-color) !important;
64 | .text {
65 | color: var(--text-color) !important;
66 | }
67 | }
68 |
69 | // 头部
70 | .ant-layout-header {
71 | .icon-style,
72 | .username {
73 | color: var(--text-color) !important;
74 | }
75 | }
76 |
77 | // 底部
78 | .ant-layout-footer {
79 | background-color: var(--bg-color) !important;
80 | border-color: var(--border-color) !important;
81 | a {
82 | color: var(--text-color) !important;
83 | }
84 | }
85 |
86 | // 内容
87 | .ant-layout-content {
88 | &::-webkit-scrollbar {
89 | background-color: var(--main-bg-color) !important;
90 | }
91 | &::-webkit-scrollbar-thumb {
92 | background-color: var(--scrollbar-bg-color) !important;
93 | }
94 | .card {
95 | &::-webkit-scrollbar {
96 | background-color: var(--bg-color) !important;
97 | }
98 | &::-webkit-scrollbar-thumb {
99 | background-color: var(--scrollbar-bg-color) !important;
100 | }
101 | }
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/style/common.scss:
--------------------------------------------------------------------------------
1 | @import url('./var.scss');
2 |
3 | /* 常用 flex */
4 | .flx-c {
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | }
9 | .flx-j {
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | }
14 | .flx-a {
15 | display: flex;
16 | align-items: center;
17 | }
18 |
19 | /* 清除浮动 */
20 | .clearfix::after {
21 | display: block;
22 | height: 0;
23 | overflow: hidden;
24 | clear: both;
25 | content: '';
26 | }
27 |
28 | /* 文字单行省略号 */
29 | .sle {
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | white-space: nowrap;
33 | }
34 |
35 | /* 文字多行省略号 */
36 | .mle {
37 | display: -webkit-box;
38 | overflow: hidden;
39 | -webkit-box-orient: vertical;
40 | -webkit-line-clamp: 2;
41 | }
42 |
43 | /* 文字多了自動換行 */
44 | .break-word {
45 | word-break: break-all;
46 | word-wrap: break-word;
47 | }
48 |
49 | /* page switch animation */
50 | .fade-enter {
51 | opacity: 0;
52 | transform: translateX(-30px);
53 | }
54 | .fade-enter-active,
55 | .fade-exit-active {
56 | opacity: 1;
57 | transition: all 0.2s ease-out;
58 | transform: translateX(0);
59 | }
60 | .fade-exit {
61 | opacity: 0;
62 | transform: translateX(30px);
63 | }
64 |
65 | /* scroll bar */
66 | ::-webkit-scrollbar {
67 | width: 8px;
68 | height: 8px;
69 | background-color: #ffffff;
70 | }
71 | ::-webkit-scrollbar-thumb {
72 | background-color: #dddee0;
73 | border-radius: 20px;
74 | box-shadow: inset 0 0 0 #ffffff;
75 | }
76 |
77 | .mb10 {
78 | margin-bottom: 10px;
79 | }
80 |
81 | .mg10 {
82 | margin: 0 5px;
83 | }
84 |
85 | /* card 卡片样式 */
86 | .card {
87 | width: 100%;
88 | height: 100%;
89 | box-sizing: border-box;
90 | padding: 20px;
91 | overflow-x: hidden;
92 | border: 1px solid #e4e7ed;
93 | border-radius: 4px;
94 | }
95 |
96 | /* content-box */
97 | .content-box {
98 | display: flex;
99 | flex-direction: column;
100 | width: 100%;
101 | height: 100%;
102 | .text {
103 | margin: 30px 0;
104 | font-size: 23px;
105 | font-weight: 700;
106 | text-align: center;
107 | a {
108 | text-decoration: underline !important;
109 | }
110 | }
111 | }
112 |
113 | // 抽屉主题设置
114 | .ant-drawer {
115 | .theme-item {
116 | display: flex;
117 | align-items: center;
118 | justify-content: space-between;
119 | margin: 25px 0;
120 | span {
121 | font-size: 14px;
122 | }
123 | .ant-switch {
124 | width: 46px;
125 | }
126 | }
127 | .divider {
128 | margin: 0 0 22px;
129 | font-size: 15px;
130 | .anticon {
131 | margin-right: 10px;
132 | }
133 | }
134 | .ant-divider-with-text::before,
135 | .ant-divider-with-text::after {
136 | border-top: 1px solid #dcdfe6;
137 | }
138 | }
139 |
140 | .ant-app {
141 | height: 100%;
142 | }
143 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | // * 检查是否有权限
2 | export const checkPagePermission = (item, rights) => item.key !== '/login' && rights.includes(item.key) && item.pagePermission
3 |
4 | /**
5 | * 根据权限递归处理menu列表 【被权限筛选后的路由列表】
6 | */
7 | export const filterMenuList = (menuList, rights) => {
8 | const newArr = []
9 | menuList.forEach(item => {
10 | // 是否为页面级权限
11 | if (checkPagePermission(item, rights)) {
12 | // 无子级
13 | if (!item?.children?.length) return newArr.push(item)
14 | // 有子级
15 | newArr.push({ ...item, children: filterMenuList(item.children, rights) })
16 | }
17 | })
18 | return newArr
19 | }
20 |
21 | /**
22 | * 获取需要展开的 subMenu
23 | */
24 | export const getOpenKeys = path => {
25 | let newStr = ''
26 | let newArr = []
27 | let arr = path.split('/').map(i => '/' + i)
28 | for (let i = 1; i < arr.length - 1; i++) {
29 | newStr += arr[i]
30 | newArr.push(newStr)
31 | }
32 | return newArr
33 | }
34 |
35 | /**
36 | * 递归查询对应路由【根据path获取route配置项】
37 | */
38 | export const searchRoute = (path, routes) => {
39 | let result = {}
40 | for (let item of routes) {
41 | if (item.path === path) return item
42 | if (item.children) {
43 | const res = searchRoute(path, item.children)
44 | if (Object.keys(res).length) result = res
45 | }
46 | }
47 | return result
48 | }
49 |
50 | /**
51 | * 根据权限路由映射路由表信息
52 | */
53 | export const hasPermission = (route, rights) => {
54 | if (route.meta?.requiresAuth && route.path) {
55 | return rights.includes(route.path)
56 | }
57 | return true
58 | }
59 |
60 | export const filterRouteList = (routes, rights) => {
61 | let newArr = []
62 | routes.forEach(route => {
63 | const item = { ...route }
64 | if (hasPermission(item, rights)) {
65 | if (item.children) {
66 | item.children = filterRouteList(item.children, rights)
67 | }
68 | newArr.push(item)
69 | }
70 | })
71 | return newArr
72 | }
73 |
74 | /**
75 | * 筛选所有最低级菜单生成面包屑导航
76 | */
77 | export const findAllBreadcrumb = menuList => {
78 | const handleBreadcrumbList = {}
79 | const loop = item => {
80 | if (item?.children?.length) item.children.forEach(loop)
81 | else {
82 | handleBreadcrumbList[item.key] = getBreadcrumbList(item.key, menuList)
83 | }
84 | }
85 | menuList.forEach(loop)
86 | return handleBreadcrumbList
87 | }
88 |
89 | export const getBreadcrumbList = (key, menuList) => {
90 | const matchMenu = menuList.find(menu => key.includes(menu.key))
91 | function deepChildren(menu, arr = []) {
92 | arr.push({ key: menu.key, label: menu.label })
93 | if (menu.children?.length) {
94 | menu.children.forEach(item => deepChildren(item, arr))
95 | }
96 | return arr
97 | }
98 | return deepChildren(matchMenu)
99 | .filter(item => key.includes(item.key))
100 | .map(item => item.label)
101 | }
102 |
--------------------------------------------------------------------------------
/src/views/user-manage/user-list/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useEffect, useMemo, useState } from 'react'
2 | import { ListWrapper } from './style'
3 | import { App, Button, Switch, Table } from 'antd'
4 | import { removeUserApi, userListApi, userStateApi } from '@/api/modules/user'
5 | import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
6 | import produce from 'immer'
7 | import UserFormModel from '@/views/user-manage/user-list/components/UserFormModel'
8 |
9 | const UserList = memo(() => {
10 | const { message } = App.useApp()
11 | const [data, setData] = useState([])
12 | // 用户表单状态
13 | const [userVisible, setUserVisible] = useState(false)
14 | const [currentItem, setCurrentItem] = useState({})
15 |
16 | const columns = useMemo(
17 | () => [
18 | { title: '区域', dataIndex: 'region', render: region => {region === '' ? '全国' : region} },
19 | { title: '角色名称', dataIndex: 'role', render: role => role?.roleName },
20 | { title: '用户名', dataIndex: 'username' },
21 | {
22 | title: '用户状态',
23 | dataIndex: 'roleState',
24 | render: (roleState, item, index) => (
25 | changeState(checked, index)} />
26 | )
27 | },
28 | {
29 | title: '操作',
30 | render: item => (
31 |
32 | }
38 | onClick={() => handleDelete(item)}
39 | />
40 | }
46 | onClick={() => handleUpdate(item)}
47 | />
48 |
49 | )
50 | }
51 | ],
52 | [data]
53 | )
54 |
55 | // * 拉取表格数据
56 | const fetchData = async () => {
57 | const { data } = await userListApi()
58 | setData(data)
59 | }
60 |
61 | useEffect(() => {
62 | fetchData()
63 | }, [])
64 |
65 | // * 相关用户操作
66 | // 状态、更新、删除、新增
67 | const changeState = async (checked, index) => {
68 | setData(
69 | produce(data => {
70 | data[index].roleState = checked
71 | })
72 | )
73 | await userStateApi(data[index].id, checked)
74 | message.success('操作成功')
75 | }
76 |
77 | const handleUpdate = item => {
78 | setCurrentItem({ ...item })
79 | setUserVisible(true)
80 | }
81 |
82 | const handleDelete = async item => {
83 | await removeUserApi(item.id)
84 | message.success('操作成功')
85 | await fetchData()
86 | }
87 |
88 | // * 弹框回调操作
89 | const [onOk, onCancel] = useMemo(() => {
90 | const cancel = () => {
91 | setUserVisible(false)
92 | setCurrentItem({})
93 | }
94 | const ok = () => {
95 | fetchData()
96 | cancel()
97 | }
98 | return [ok, cancel]
99 | }, [])
100 |
101 | return (
102 |
103 |
106 | item.id} />
107 |
108 | {/*user操作弹窗*/}
109 |
110 |
111 | )
112 | })
113 |
114 | UserList.displayName = 'UserList'
115 |
116 | export default UserList
117 |
--------------------------------------------------------------------------------
/src/layout/components/LayoutMenu/index.jsx:
--------------------------------------------------------------------------------
1 | import { Menu, Spin } from 'antd'
2 | import React, { memo, useEffect, useState } from 'react'
3 | import Logo from './components/Logo'
4 | import { MenuWrapper } from './style'
5 | import { useLocation, useNavigate } from 'react-router-dom'
6 | import { menuListApi } from '@/api/modules/user'
7 | import * as Icons from '@ant-design/icons'
8 | import { filterMenuList, findAllBreadcrumb, getOpenKeys, searchRoute } from '@/utils/utils'
9 | import { useDispatch, useSelector } from 'react-redux'
10 | import { setBreadcrumbList, setMenuListAction } from '@/store/modules/menu'
11 |
12 | const LayoutMenu = memo(() => {
13 | const { pathname } = useLocation()
14 | const [selectedKeys, setSelectedKeys] = useState([pathname])
15 | const isCollapse = useSelector(({ menu }) => menu.isCollapse)
16 |
17 | const [openKeys, setOpenKeys] = useState([])
18 | useEffect(() => {
19 | setSelectedKeys([pathname])
20 | !isCollapse && setOpenKeys(getOpenKeys(pathname))
21 | }, [pathname, isCollapse])
22 |
23 | // 设置当前展开的 subMenu
24 | const onOpenChange = openKeys => {
25 | if (openKeys.length === 0 || openKeys.length === 1) return setOpenKeys(openKeys)
26 | const lastOpenKey = openKeys[openKeys.length - 1]
27 | if (lastOpenKey.includes(openKeys[0])) return setOpenKeys(openKeys)
28 | setOpenKeys([lastOpenKey])
29 | }
30 |
31 | // 点击菜单跳转页面
32 | const navigate = useNavigate()
33 | const clickMenu = ({ key }) => {
34 | const route = searchRoute(pathname, menuList)
35 | if (route.isLink) window.open(route.isLink, '_blank')
36 | navigate(key)
37 | }
38 |
39 | // 生成MenuItem
40 | const menuItem = (label, key, icon, children) => ({ label, key, icon, children })
41 | // 生成Icon图标
42 | const iconItem = name => name && React.createElement(Icons[name])
43 | // 处理后台返回数据匹配菜单key
44 | const deepLoopFloat = menuList => {
45 | const newArr = []
46 | menuList.forEach(item => {
47 | // 无子级
48 | if (!item?.children?.length) return newArr.push(menuItem(item.title, item.key, iconItem(item.icon)))
49 | // 有子级
50 | newArr.push(menuItem(item.title, item.key, iconItem(item.icon), deepLoopFloat(item.children)))
51 | })
52 | return newArr
53 | }
54 |
55 | // 获取菜单列表并处理成 antd menu 需要的格式
56 | const dispatch = useDispatch()
57 | const [menuList, setMenuList] = useState([])
58 | const [loading, setLoading] = useState(false)
59 | const { role } = useSelector(({ global }) => global.userinfo)
60 | const getMenuData = async () => {
61 | setLoading(true)
62 | try {
63 | const { data } = await menuListApi()
64 | if (!data) return
65 | // 1. 根据用户权限筛选列表
66 | const filterData = filterMenuList(data, role.rights)
67 | // 3. 映射 -> menu菜单格式
68 | const menuData = deepLoopFloat(filterData)
69 | // 4. 设置到菜单上
70 | setMenuList(menuData)
71 | // 5. 设置到面包屑中
72 | dispatch(setBreadcrumbList(findAllBreadcrumb(menuData)))
73 | // 5. 设置到redux中
74 | dispatch(setMenuListAction(filterData))
75 | } finally {
76 | setLoading(false)
77 | }
78 | }
79 |
80 | useEffect(() => {
81 | getMenuData()
82 | }, [])
83 |
84 | return (
85 |
86 |
87 |
88 |
98 |
99 |
100 | )
101 | })
102 |
103 | LayoutMenu.displayName = 'LayoutMenu'
104 |
105 | export default LayoutMenu
106 |
--------------------------------------------------------------------------------