基于react18.x+vite4+arco-design自研中后台管理系统解决方案ReactAdmin。
react-vite-admin 基于vite4搭建react18.x后台管理项目。使用了react18 hooks+arco.design+zustand+bizcharts等技术实现权限管理模板框架。支持暗黑/亮色主题、i18n国际化、动态权限鉴定、3种布局模板、tab路由标签栏等功能。
React18Admin管理系统是首创自研的轻量级中后台框架,构建运行速度快,支持dark/light主题模式。
技术栈
- 编辑器:Vscode
- 框架技术:react18+vite4+react-router+zustand+axios
- 组件库:arco-design (字节前端react组件库)
- 路由管理:react-router-dom^6.16.0
- 状态管理:zustand^4.4.1
- 模拟数据:mockjs^1.1.0
- 模拟请求:axios^1.5.1
- 图表库:bizcharts^4.1.22
- 编辑器组件:@wangeditor/editor-for-react^1.0.6
- markdown编辑器:@uiw/react-md-editor^3.23.6
- 请求进度插件:nprogress^0.2.0
react-admin采用字节出品的react桌面端组件库arco.design。
特性
- 基于vite4.x构建react18后台,轻/快/小
- 使用最新前端技术栈react18、zustand、bizcharts、react-router、axios
- 搭配清新react组件库arco.design
- 支持中英文/繁体国际化语言
- 支持动态路由权限验证
- 支持动态tabs标签栏控制
- 内置多种模板布局样式
项目结构目录
采用标准化的react目录结构,整个项目使用react18 function语法编码开发。
构建预览图
wangeditor-react图文编辑器使用的是wangeditor的react版本,支持dark/light主题。
react-md-editor基于react的markdown编辑器,支持dark/light主题。
react18-scrollbar项目中使用的虚拟滚动条基于react18自定义组件实现功能。
// 引入滚动条组件 import RScroll from '@/components/rscroll' <RScroll autohide maxHeight={100}> 包裹需要滚动的内容块。。。 </RScroll>
React18-Admin布局模板
如上图:支持分栏+垂直+水平3种通用布局模板。也可以定制化模板样式。
/** * 主布局模板 * @author Hs Q:282310962 */ import { useMemo } from 'react' import { appStore } from '@/store/app' import Columns from './template/columns' import Vertical from './template/vertical' import Transverse from './template/transverse' function Layout() { const { config: { skin, layout } } = appStore() // 布局模板 const LayoutComponent = useMemo(() => { switch(layout) { case 'columns': return Columns case 'vertical': return Vertical case 'transverse': return Transverse default: return Columns } }, [layout]) return ( <div className="radmin__container"> <LayoutComponent /> </div> ) } export default Layout
主模板Main.jsx动态Permission鉴权验证。
import './index.scss' import { Outlet } from 'react-router-dom' import RScroll from '@/components/rscroll' import Permission from '@/components/Permission' import Forbidden from '@/views/error/forbidden' import { useRoute } from '@/hooks/useRoutes' export default function Main() { const route = useRoute() return ( <> <RScroll> <div className="ra__layout-main__wrapper"> {/* 鉴权组件 */} <Permission roles={route?.meta?.roles} content={<Forbidden />} > <Outlet /> </Permission> </div> </RScroll> </> ) }
react-router路由配置
/** * @title react-router-dom v6路由配置管理 * @author andy */ import { useRoutes, Navigate } from 'react-router-dom' import Error from '@views/error/404' // 批量导入modules路由 const modules = import.meta.glob('./modules/*.jsx', { eager: true }) const patchRoutes = Object.keys(modules).map(key => modules[key].default).flat() // useRoutes集中式路由配置 export const routes = [ { path: '/', element: <Navigate to="/home" replace={true} />, meta: { isWhite: true // 路由白名单 } }, ...patchRoutes, // 404模块 path="*"不能省略 { path: '*', element: <Error />, meta: { isWhite: true } } ] const Router = () => useRoutes(routes) export default Router
lazyload.jsx配置
/** * 延迟加载提示 */ import { Suspense } from 'react' import { Spin } from '@arco-design/web-react' import NprogressLoading from './nprogress' // 加载提示 const SpinLoading = () => { return ( <Spin tip='loading...' style={{ width: '100%' }} /> ) } // 延迟加载 const lazyload = LazyComponent => { // React 16.6 新增了<Suspense>组件,懒加载的模式需要我们给他加上一层 Loading的提示加载组件 // return <Suspense fallback={<SpinLoading />}><LazyComponent /></Suspense> return <Suspense fallback={<NprogressLoading />}><LazyComponent /></Suspense> } export default lazyload
NProgress.jsx配置
/** * 加载进度条NProgress */ import { Component } from 'react' import NProgress from 'nprogress' import 'nprogress/nprogress.css' export default class NprogressLoading extends Component { constructor(props) { super(props) NProgress.set(.4) NProgress.start() } componentDidMount() { NProgress.done() } render() { return <div /> } }
主路由main.jsx配置
/** * 主路由配置 * @author Hs */ import { lazy } from 'react' import { IconHome, IconDashboard, IconLink, IconCommand, IconUserGroup, IconLock, IconMenu, IconSafe, IconBug, IconHighlight, IconUnorderedList, IconStop } from '@arco-design/web-react/icon' import Layout from '@/layouts' import Blank from '@/layouts/blank' import lazyload from '../lazyload' export default [ /*首页模块*/ { path: '/home', key: '/home', // 用于Menu组件跳转路由地址 element: <Layout />, meta: { // icon: 've-icon-home', // 菜单图标 icon: <IconHome />, name: 'layout__main-menu__home', // i18n国际化标题 title: '主页', isAuth: true, // 需要鉴权 isHidden: false, // 是否隐藏菜单 isAffix: true // 固定tabview标签栏(不可关闭) }, children: [ { key: '/home', index: true, element: lazyload(lazy(() => import('@views/home'))), meta: { // icon: 've-icon-home', icon: <IconHome />, name: 'layout__main-menu__home-index', title: '首页', isAuth: true } }, // 工作台 { path: 'dashboard', key: '/home/dashboard', element: lazyload(lazy(() => import('@views/home/dashboard'))), meta: { // icon: 've-icon-computer', icon: <IconDashboard />, name: 'layout__main-menu__home-workplace', title: '工作台', isAuth: true } }, // 外部链接 { path: 'https://react.dev/', key: 'https://react.dev/', meta: { // icon: 've-icon-clip', icon: <IconLink />, name: 'layout__main-menu__home-apidocs', title: 'react.js官方文档', rootRoute: '/home' } } ] }, /*组件模块*/ { ... }, /*用户管理模块*/ { ... }, /*权限模块*/ { ... }, /*错误模块*/ { ... } ]
备注:路由菜单参数配置说明。
/** * @description 路由参数说明 * @param path ==> 路由地址标识 * @param key ==> 用于Menu组件跳转路由地址 * @param redirect ==> 重定向地址 * @param element ==> 视图页面路径 * 菜单信息(meta) * @param meta.icon ==> 菜单图标 * @param meta.title ==> 菜单标题 * @param meta.name ==> i18n国际化标题 * @param meta.roles ==> 页面权限 ['admin', 'dev', 'test'] * @param meta.isAuth ==> 是否需要验证 * @param meta.isHidden ==> 是否隐藏页面 * @param meta.isAffix ==> 是否固定标签(tabs标签栏不能关闭) * */
react自定义路由菜单Menu
基于arco.design组件库提供的Menu组件封装三种不同的路由菜单。
<RouteMenu /> <RouteMenu rootRouteEnable /> <RouteMenu rootRouteEnable mode="horizontal" />
RouteMenu路由菜单模板
/** * 路由菜单模板 */ import './index.scss' import { useState, useMemo, useEffect } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { Menu } from '@arco-design/web-react' import Icon from '@components/Icon' import RouteSubMenu from './submenu' import { routes } from '@/routers' import { getCurrentRootRoute, findParentRoute } from '@/hooks/useRoutes' import Locales from '@/locales' export default function RouteMenu(props) { const { // 菜单类型(垂直vertical 水平菜单horizontal 弹出pop) mode = 'vertical', // 菜单风格('light' | 'dark') theme = 'light', // 是否开启一级路由菜单 rootRouteEnable = false, style = {} } = props const navigate = useNavigate() const { pathname } = useLocation() const t = Locales() const [openKeys, setOpenKeys] = useState([]) const rootRoute = getCurrentRootRoute() const filterRoutes = routes.filter(item => !item?.meta?.isWhite) const menuRoutes = useMemo(() => { if(rootRouteEnable) { return filterRoutes } // 过滤一级菜单 return filterRoutes.find(item => item.path == rootRoute && item.children)?.children }, [pathname]) useEffect(() => { setOpenKeys(getKeys(pathname)) }, [pathname]) // 获取选中菜单路由keys数组 const getKeys = (key) => { return findParentRoute(menuRoutes, key)?.map(item => item?.key) } const handleNavigate = (key) => { const reg = /[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/ if(reg.test(key)) { window.open(key) }else { navigate(key) } } return ( <Menu className="ra__menus" mode={mode} theme={theme} selectedKeys={[pathname]} openKeys={openKeys} levelIndent={28} style={{ ...style }} onClickMenuItem={handleNavigate} onClickSubMenu={(_, openKeys) => { setOpenKeys(openKeys) }} > { menuRoutes.map(item => { if(item?.children) { return RouteSubMenu(item, t) } return ( !item?.meta?.isHidden && <Menu.Item className="ra__menuItem" key={item.redirect || item.key}> { item?.meta?.icon && <Icon name={item.meta.icon} size={18} style={{marginRight: 10}} /> } { item?.meta?.name && <span>{t[item.meta.name]}</span> } </Menu.Item> ) })} </Menu> ) }
react18状态管理zustand
Zustand新一代react状态管理工具,内置多种插件,支持persist本地存储服务。
/** * react18状态管理库Zustand4,中间件persist本地持久化存储 */ import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' import { generate, getRgbStr } from '@arco-design/color' export const appStore = create( persist( (set, get) => ({ // 语言(中文zh-CN 英文en 繁体字zh-TW) lang: 'zh-CN', // 角色类型 roles: ['admin'] / roles: ['admin', 'dev'] / roles: ['dev', test'] roles: ["dev"], // 配置信息 config: { // 布局(分栏columns 纵向vertical 横向transverse) layout: 'columns', // 模式(亮色light - 暗黑dark) mode: 'light', // 主题色 theme: '#3491FA', // 是否折叠菜单 collapsed: false, // 开启面包屑导航 breadcrumb: true, // 开启标签栏 tabsview: true, tabRoutes: [], // 显示搜索 showSearch: true, // 显示全屏 showFullscreen: true, // 显示语言 showLang: true, // 显示公告 showNotice: true, // 显示底部 showFooter: false }, // 更新配置 updateConfig: (key, value) => set({ config: { ...get().config, [key]: value } }), // 设置角色 setRoles: (roles) => set({roles}), // 设置多语言 setLang: (lang) => set({lang}), // 设置主题模式 setMode: (mode) => { if(mode == 'dark') { // 设置为暗黑主题 document.body.setAttribute('arco-theme', 'dark') }else { // 恢复亮色主题 document.body.removeAttribute('arco-theme') } get().updateConfig('mode', mode) }, // 设置主题样式 setTheme: (theme) => { const colors = generate(theme, { list: true }) colors.map((item, index) => { const rgbStr = getRgbStr(item) document.body.style.setProperty(`--arcoblue-${index + 1}`, rgbStr) }) get().updateConfig('theme', theme) } }), { name: 'appState', // name: 'app-store', // name of the item in the storage (must be unique) // storage: createJSONStorage(() => sessionStorage), // by default, 'localStorage' } ) )
react18国际化配置i18n
/** * 国际化配置 * @author YXY */ import { appStore } from '@/store/app' // 引入语言配置 import enUS from './en-US' import zhCN from './zh-CN' import zhTW from './zh-TW' export const locales = { 'en': enUS, 'zh-CN': zhCN, 'zh-TW': zhTW } export default (locale) => { const appState = appStore() const lang = appState.lang || 'zh-CN' return (locale || locales)[lang] || {} }
App.jsx引入arco.design组件库语言包。
/** * 入口模板 * @author Hs */ import { useEffect, useMemo } from 'react' import { HashRouter } from 'react-router-dom' // 通过 ConfigProvider 组件实现国际化 import { ConfigProvider } from '@arco-design/web-react' // 引入语言包 import enUS from '@arco-design/web-react/es/locale/en-US' import zhCN from '@arco-design/web-react/es/locale/zh-CN' import zhTW from '@arco-design/web-react/es/locale/zh-TW' import { AuthRouter } from '@/hooks/useRoutes' import { appStore } from '@/store/app' // 引入路由配置 import Router from './routers' function App() { const { lang, config: { mode, theme }, setMode, setTheme } = appStore() const locale = useMemo(() => { switch(lang) { case 'en': return enUS case 'zh-CN': return zhCN case 'zh-TW': return zhTW default: return zhCN } }, [lang]) useEffect(() => { setMode(mode) setTheme(theme) }, []) return ( <ConfigProvider locale={locale}> <HashRouter> <AuthRouter> <Router /> </AuthRouter> </HashRouter> </ConfigProvider> ) } export default App
Lang.jsx配置
import { Dropdown, Menu, Button } from '@arco-design/web-react' import Icon from '@components/Icon' import { appStore } from '@/store/app' export default function Lang() { const { lang, setLang } = appStore() const handleLang = val => { setLang(val) } return ( <Dropdown position="bottom" droplist={ <Menu className="radmin__dropdownLang" defaultSelectedKeys={[lang]} onClickMenuItem={handleLang}> <Menu.Item key='zh-CN'>简体中文 <span>zh-CN</span></Menu.Item> <Menu.Item key="zh-TW">繁体字 <span>zh-TW</span></Menu.Item> <Menu.Item key="en">英文 <span>en</span></Menu.Item> </Menu> } > <Button shape="circle" size="small" icon={<Icon name="ve-icon-lang" />} /> </Dropdown> ) }
Tabs.jsx动态路由栏
项目中动态路由栏tabs采用arco.design组件库Tabs组件自定义实现功能。
<Tabs activeTab={pathname} editable showAddButton={false} onDeleteTab={key => delTabs(key)} > { tabRoutes.map(item => ( <Tabs.TabPane closable={!item?.meta?.isAffix} key={item?.redirect || item?.key} title={ <Dropdown trigger='contextMenu' position='bl' droplist={ <Menu className="ra__dropdownContext" onClickMenuItem={(key, e) => handleClickMenuItem(key, e, item)}> <Menu.Item key="close" disabled={item?.meta?.isAffix}><Icon name="ve-icon-close" />{t['tabview__contextmenu-close']}</Menu.Item> <Menu.Item key="closeLeft" disabled={isFirstTab()}><Icon name="ve-icon-prev" />{t['tabview__contextmenu-closeleft']}</Menu.Item> <Menu.Item key="closeRight" disabled={isLastTab()}><Icon name="ve-icon-next" />{t['tabview__contextmenu-closeright']}</Menu.Item> <Menu.Item key="closeOther"><Icon name="ve-icon-reset" />{t['tabview__contextmenu-closeother']}</Menu.Item> <Menu.Item key="closeAll"><Icon name="ve-icon-close-circle-o" />{t['tabview__contextmenu-closeall']}</Menu.Item> </Menu> } onVisibleChange={visible=>handleOpenContextMenu(visible, item)} > <span className="ra__tabsview-title" onClick={() => navigate(item?.redirect || item?.key)}> <TabIcon path={item?.key} /> { t[item?.meta?.name] } </span> </Dropdown> } /> ))} </Tabs>
export default function TabsView() { const { pathname } = useLocation() const navigate = useNavigate() const [selectedTab, setSelectedTab] = useState() const { config: { tabRoutes }, updateConfig } = appStore() const route = useRoute() const t = Locales() useEffect(() => { addTabs() }, [pathname]) // 添加 const addTabs = () => { const tabIndex = tabRoutes.findIndex(item => item?.key === pathname) let newTabs = tabRoutes if(tabIndex == -1) { newTabs.push(route) } newTabs.map(item => { item.isActive = false if(item?.key === pathname) { item.isActive = true } }) updateConfig('tabRoutes', newTabs) } // 删除 const delTabs = (path) => { const tabIndex = tabRoutes.findIndex(item => item?.key === path) let newTabs = tabRoutes if(tabIndex > -1) { newTabs.splice(tabIndex, 1) updateConfig('tabRoutes', newTabs) updateTabs(newTabs) } } // 删除左侧标签 const delLeftTabs = (path) => { const tabIndex = tabRoutes.findIndex(item => item?.key === path) let newTabs = tabRoutes if(tabIndex > -1) { newTabs = newTabs.filter((item, i) => item?.meta?.isAffix || i >= tabIndex) updateConfig('tabRoutes', newTabs) updateTabs(newTabs) } } // 删除右侧标签 const delRightTabs = (path) => { const tabIndex = tabRoutes.findIndex(item => item?.key === path) let newTabs = tabRoutes if(tabIndex > -1) { newTabs = newTabs.filter((item, i) => item?.meta?.isAffix || i <= tabIndex) updateConfig('tabRoutes', newTabs) updateTabs(newTabs) } } // 删除其它 const delOtherTabs = (path) => { let newTabs = tabRoutes.filter(item => item?.meta?.isAffix || item?.key === path) updateConfig('tabRoutes', newTabs) updateTabs(newTabs) } // 删除所有 const delAllTabs = () => { let newTabs = tabRoutes.filter(item => item?.meta?.isAffix) updateConfig('tabRoutes', newTabs) updateTabs(newTabs) } // 更新跳转选项卡 const updateTabs = (tabs) => { const nextTab = tabs[tabs.length + 1] || tabs[tabs.length - 1] if(!nextTab) return navigate(nextTab?.redirect || nextTab?.key) } // 是否第一个标签 const isFirstTab = () => { return selectedTab?.key === tabRoutes[0]?.key || selectedTab?.key === '/home' } // 是否最后一个标签 const isLastTab = () => { return selectedTab?.key === tabRoutes[tabRoutes.length - 1]?.key } // 打开右键菜单 const handleOpenContextMenu = (visible, item) => { if(visible) { setSelectedTab(item) } } // 点击右键菜单项 const handleClickMenuItem = (key, e, item) => { e.stopPropagation() const path = item?.key switch(key) { case 'close': delTabs(path) break case 'closeLeft': delLeftTabs(path) break case 'closeRight': delRightTabs(path) break case 'closeOther': delOtherTabs(path) break case 'closeAll': delAllTabs() break } } return ( ... ) }
OK,以上就是react18+arco开发后台系统的一些分享,希望对大家有所帮助~