├── 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 | welcome 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 | logo 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 | login 15 |
16 |
17 |
18 | logo-icon 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /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 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ({ label: item.roleName, value: item.id }))} /> 67 | 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 |
54 | 55 | } /> 56 | 57 | 58 | } /> 59 | 60 | 61 | 64 | 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 |
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 | --------------------------------------------------------------------------------