🌈个人主页:前端青山
🔥系列专栏:React篇
🔖人终将被年少不可得之物困其一生
依旧青山,本期给大家带来React篇专栏内容:React-综合应用开发教程:构建多功能管理平台
目录
1.创建项目
2.改造目录结构
3.安装一些必须的模块
3.1 配置预处理器
3.1.1 配置别名@
3.2安装状态管理器
3.3 路由
3.4 数据验证
3.5数据请求
3.6ui库
3.6.1 自定义主题
3.7 其他第三方工具包
4.创建主布局文件
5.拆分主界面
6.使用rtk来管理状态
6.1 定义State和Dispatch类型
6.2 定义 Hooks 类型
6.3 应用程序中使用
6.4 整合reducer
6.5 入口文件配置状态管理器
6.6 左侧菜单栏使用状态管理器
6.7 头部组件使用状态管理器
6.8保留用户习惯-可选
6.9 永久存储的 类 localStorage 的工具 store2
7.左侧菜单栏
7.1.设计左侧菜单栏的数据
7.2.渲染左侧菜单栏
7.3 低版本处理
7.4 菜单渲染优化
8.定义路由
8.1 官方文档
8.2 创建对应的页面
8.3 定义菜单路由信息
8.4.装载路由
8.5 定义路由组件
8.6 手动测试路由
8.7 设置404页面
9 切换路由
9.1 点击切换路由
9.2 刷新保持左侧菜单状态
10 设置面包屑导航
10.1 参考文档
10.2 设置面包屑导航
11.快捷切换页
11.1 准备组件
11.2 处理数据
11.3 监听路由添加数据
11.4 点击tab页切换路由,关闭效果
12.数据请求的封装
13 构建登录页面
13.1 参考组件库组件
13.2 构造登录接口API
13.3 创建登录的页面
13.4 创建登录路由
13.4 完善登录界面
14 执行登录
14.1 构建模块 admins
14.2 装载模块
14.3 登录实现
15.前端登录验证
16 .后端token校验
17.退出登录
17.1 实现退出登录
17.2 保留退出时的页面
18.隐藏左侧菜单项
19. 管理员管理
19.1.设计接口
19.2.展示管理员列表
19.3 优化表格滚动
19.4 优化表格的分页器
19.5 添加中文包
19.6删除管理员
19.7 如何批量删除管理员数据
19.9.添加管理员
19.9.1 设置添加管理员的抽屉效果(无树形控件)
19.9.2 修改菜单数据 添加了keyid字段
19.9.3 添加管理员时选择该管理员权限
19.9.4 添加管理员
19.10管理员修改
20 系统首页数据统计
21 左侧菜单栏的权限
21.1 思路
21.2 算法过程
21.3 算法实现
21.4 生成动态的左侧菜单项
22、页面权限
23、按钮权限
24、轮播图管理
24.1 封装接口
24.2 轮播图页面渲染
23.3 添加轮播图
25.产品管理
25.1 封装接口
25.2 产品列表
25.3 筛选列表
26.数据可视化
1.echarts
2.Highcharts
3.antv - g2
27.编辑器
1.富文本编辑器
2.markDown编辑器
28.导入以及导出
1.导出
2.导入
29.地图
30.项目打包发布
1.创建项目
# 现在
npx create-react-app react-admin-app --template typescript
熟悉目录结构
- react-admin-app
-node_modules
-public
-src
App.css
App.test.tsx App.tsx的测试文件 npm run test 查看测试结果
App.tsx
index.css
index.tsx react应用程序的入口文件
logo.svg
react-app-env.d.ts // 声明文件 // 指令声明对包的依赖关系
reportWebVitals.ts // 测试性能
seupTests.ts // 使用jest做为测试工具
.gitignore
package-lock.json
package.json
README.md
tsconfig.json
*.d.ts 代表ts的声明文件
2.改造目录结构
src
api
components
layout
store
router
utils
views
App.tsx
index.tsx
logo.svg
react-app-env.d.ts
reportWebVitals.ts
seupTests.ts
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
// src/App.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const App: FC<IAppProps> = (props) => {
return (
<>App</>
)
}
export default App
3.安装一些必须的模块
3.1 配置预处理器
两种方式:
-
抽离配置文件配置预处理器
-
不抽离配置文件craco进行预处理器配置
本项目推荐使用第二种方式
$ cnpm i @craco/craco @types/node -D
https://www.npmjs.com/package/@craco/craco
3.1.1 配置别名@
项目根目录创建 craco.config.js
,代码如下:
// craco.config.js
const path = require('path')
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}
为了使 TS 文件引入时的别名路径能够正常解析,需要配置 tsconifg.json
,在 compilerOptions
选项里添加 path 等属性。为了防止配置被覆盖,需要单独创建一个文件 tsconfig.path.json
,添加以下代码
// tsconfig.path.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": [
"node"
]
}
}
在 tsconifg.json
引入配置文件:
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"extends": "./tsconfig.path.json",
"include": [
"src"
]
}
修改 package.json
如下:
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test"
},
$ npm run start
3.2安装状态管理器
根据项目需求 任选其一即可
$ cnpm i redux -S
$ cnpm i redux react-redux -S
$ cnpm i redux react-redux redux-thunk -S
$ cnpm i redux react-redux redux-saga -S
$ cnpm i redux react-redux redux-thunk immutable redux-immutable -S
$ cnpm i redux react-redux redux-saga immutable redux-immutable -S
$ cnpm i mobx mobx-react -S
本项目不采用之前的状态管理模式,使用 rtk 技术
cnpm i @reduxjs/toolkit redux react-redux -S
3.3 路由
2021年11月4日 发布了 react-router-dom的v6.0.0版本:Home v6.26.1 | React Router
如需使用v5版本:https://v5.reactrouter.com/web/guides/quick-start cnpm i react-router-dom@5 -S
本项目采用 V6版本
cnpm i react-router-dom -S
3.4 数据验证
思考,有没有必要安装 prop-types ?
cnpm i prop-types -S
本项目其实没有必要安装,因为所有的数据都是基于ts,而ts需要指定类型注解
3.5数据请求
cnpm i axios -S
以前版本中 cnpm i @types/axios -S
Ts 中 @types/* 为声明文件
3.6ui库
官网地址:Ant Design - 一套企业级 UI 设计语言和 React 组件库 5.2.0
国内官方镜像地址:Ant Design - 一套企业级 UI 设计语言和 React 组件库
国内gitee镜像地址:https://ant-design.gitee.io/index-cn
cnpm i antd @ant-design/icons -S
src/index.tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
reportWebVitals();
测试组件库
// src/App.tsx
import React, { FC } from 'react';
import { Button } from 'antd';
interface IAppProps {
}
const App: FC<IAppProps> = (props) => {
return (
<>
App
<Button type="primary">
Primary
</Button>
</>
)
}
export default App
浏览器查看发现测试通过
3.6.1 自定义主题
404 Not Found - Ant Design
antd 内建了深色主题和紧凑主题,你可以参照 使用暗色主题和紧凑主题 进行接入。
可以定制的变量列表如下:
@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
@border-radius-base: 2px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
<React.StrictMode>
<ConfigProvider
theme = { {
token: {
colorPrimary: '#1890ff'
}
} }
>
<App />
</ConfigProvider>
</React.StrictMode>
);
reportWebVitals();
3.7 其他第三方工具包
Lodash 简介 | Lodash中文文档 | Lodash中文网
Lodash 工具包,项目必装,它提供了很多使用的函数
$ cnpm i lodash -S
$ cnpm i @types/lodash -D
import _ from 'lodash'
var users = [
{ 'user': 'barney', 'active': false },
{ 'user': 'fred', 'active': false },
{ 'user': 'pebbles', 'active': true }
];
console.log(_.findIndex(users, (item) => item.user === 'pebbles'))
console.log(users.findIndex((item) => item.user === 'pebbles'))
4.创建主布局文件
预览模板:开箱即用的中台前端/设计解决方案 - Ant Design Pro
src/layout/Index.tsx 作为后台管理系统的主页面布局(包含左侧的菜单栏,顶部,底部等)
https://ant-design.gitee.io/components/layout-cn/#components-layout-demo-custom-trigger
不要照着代码敲,直接复制即可,给 Layout 组件添加 id为
admin-app
// src/layout/Index.tsx
import React, { useState } from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu, theme } from 'antd';
const { Header, Sider, Content } = Layout;
const App: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<Layout id="components-layout-demo-custom-trigger">
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
</Sider>
<Layout className="site-layout">
<Header style={
{ padding: 0, background: colorBgContainer }}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
</Header>
<Content
style={
{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
}}
>
Content
</Content>
</Layout>
</Layout>
);
};
export default App;
主组件引入 主界面的布局文件
// src/App.tsx
import React, { FC } from 'react';
import Index from '@/layout/Index'
import './App.css'
interface IAppProps {
}
const App: FC<IAppProps> = (props) => {
return (
<>
<Index />
</>
)
}
export default App
查看浏览器,预览运行结果
发现页面并不是全屏。审查元素设置 root以及 components-layout-demo-custom-trigger 高度为 100%
/* src/App.css */
#root, #components-layout-demo-custom-trigger { height: 100%;}
#components-layout-demo-custom-trigger .trigger {
padding: 0 24px;
font-size: 18px;
line-height: 64px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}
5.拆分主界面
先拆分左侧的菜单栏组件
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import {
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
const { Sider } = Layout;
const App: React.FC = () => {
const [collapsed] = useState(false);
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
</Sider>
);
};
export default App;
// src/layout/components/AppHeader.tsx
import React, { useState } from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme } from 'antd';
const { Header } = Layout;
const App: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<Header style={
{ padding: 0, background: colorBgContainer }}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
</Header>
);
};
export default App;
// src/layout/components/AppMain.tsx
import React from 'react';
import { Layout, theme } from 'antd';
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<Content
style={
{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
}}
>
Content
</Content>
);
};
export default App;
整和组件资源
// src/layout/components/index.ts
export { default as SideBar } from './SideBar'
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
// src/layout/Index.tsx
import React from 'react';
import { Layout } from 'antd';
// import SideBar from './components/SideBar'
// import AppHeader from './components/AppHeader'
// import AppMain from './components/AppMain'
import { SideBar, AppHeader, AppMain } from './components'
const App: React.FC = () => {
return (
<Layout id="components-layout-demo-custom-trigger">
<SideBar />
<Layout className="site-layout">
<AppHeader />
<AppMain />
</Layout>
</Layout>
);
};
export default App;
此时点击头部的控制器,发现只有头部组件的 图标在切换,但是并没有影响左侧菜单的收缩
建议使用状态管理器管理控制的这个状态
6.使用rtk来管理状态
Redux 中文官网 - JavaScript 应用的状态容器,提供可预测的状态管理。 | Redux 中文官网
参考链接:TypeScript 快速开始 | Redux 中文官网
6.1 定义State和Dispatch类型
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {}
})
// 导出类型注解 // 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch
export default store
构建app的模块用于管理 头部和 左侧菜单的共同的状态
6.2 定义 Hooks 类型
虽然可以将RootState
andAppDispatch
类型导入到每个组件中,但最好创建useDispatch
and useSelector
hooks 的类型化版本以在您的应用程序中使用。
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './index'
// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
6.3 应用程序中使用
创建状态管理
// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
interface IAppState {
collapsed: boolean
}
const initialState: IAppState = {
collapsed: false
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
changeCollapsed (state) {
state.collapsed = !state.collapsed
}
}
})
export const { changeCollapsed } = appSlice.actions
export default appSlice.reducer
6.4 整合reducer
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import app from './modules/app'
const store = configureStore({
reducer: {
app
}
})
// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch
export default store
6.5 入口文件配置状态管理器
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store'
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
<React.StrictMode>
<ConfigProvider
theme = { {
token: {
colorPrimary: '#1890ff'
}
} }
>
<Provider store = { store }>
<App />
</Provider>
</ConfigProvider>
</React.StrictMode>
);
reportWebVitals();
6.6 左侧菜单栏使用状态管理器
// src/layout/components/SideBar.tsx
import React from 'react';
import {
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import { useAppSelector } from '@/store/hooks'
// import { useSelector } from 'react-redux'
// import type { RootState } from '@/store'
const { Sider } = Layout;
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// const collapsed = useSelector((state: RootState) => state.app.collapsed)
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={[
{
key: '1',
icon: <UserOutlined />,
label: 'nav 1',
},
{
key: '2',
icon: <VideoCameraOutlined />,
label: 'nav 2',
},
{
key: '3',
icon: <UploadOutlined />,
label: 'nav 3',
},
]}
/>
</Sider>
);
};
export default App;
6.7 头部组件使用状态管理器
// src/layout/components/AppHeader.tsx
import React from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
const { Header } = Layout;
const App: React.FC = () => {
// const [collapsed, setCollapsed] = useState(false);
const collapsed = useAppSelector(state => state.app.collapsed)
const dispatch = useAppDispatch()
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<Header style={
{ padding: 0, background: colorBgContainer }}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
// onClick: () => setCollapsed(!collapsed),
onClick: () => dispatch(changeCollapsed())
})}
</Header>
);
};
export default App;
6.8保留用户习惯-可选
永久存储 用户习惯
数据持久化: redux-persist
此时发现 头部的 按钮可以控制左侧菜单栏了,但是还没有满足需求
需求如下:保留用户的使用习惯
// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
interface IAppState {
collapsed: boolean
}
const initialState: IAppState = {
// collapsed: false
collapsed: localStorage.getItem('collapsed') === 'true'
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
changeCollapsed (state) {
state.collapsed = !state.collapsed
localStorage.setItem('collapsed', String(state.collapsed))
}
}
})
export const { changeCollapsed } = appSlice.actions
export default appSlice.reducer
6.9 永久存储的 类 localStorage 的工具 store2
$ cnpm i store2 -S
https://www.npmjs.com/package/store2
推荐一个好用的永久存储的 类 localStorage 的工具 store2
// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
import store2 from 'store2'
interface IAppState {
collapsed: boolean
}
const initialState: IAppState = {
// collapsed: false
// collapsed: localStorage.getItem('collapsed') === 'true'
collapsed: store2.get('collapsed') === 'true'
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
changeCollapsed (state) {
state.collapsed = !state.collapsed
// localStorage.setItem('collapsed', String(state.collapsed))
store2.set('collapsed', String(state.collapsed))
}
}
})
export const { changeCollapsed } = appSlice.actions
export default appSlice.reducer
7.左侧菜单栏
7.1.设计左侧菜单栏的数据
https://ant-design.gitee.io/components/menu-cn/#components-menu-demo-sider-current
Antd 4.20以上版本直接实现 递归
antd 4.20版本以下需要手动实现
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string // 多级菜单的默认地址
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
}
]
}
]
export default menus
7.2.渲染左侧菜单栏
左侧菜单栏的头部设定logo以及后台管理系统名称
// src/layout/components/SideBar.tsx
import React from 'react';
import { Layout, Menu, Image } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
const { Sider } = Layout;
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" style={ {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#fff'
}}>
<Image src = { logo } width="28px" height="28px" preview={ false }></Image>
{ !collapsed && <div style={
{
height: '32px',
overflow: 'hidden',
lineHeight: '32px'
}}>嗨购后台管理系统</div> }
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={ menus }
/>
</Sider>
);
};
export default App;
7.3 低版本处理
以上菜单项的设置在antd 4.20.0
版本以上好使,如果在4.20.0
版本以下,应该使用 递归组件实现
// src/layout/components/SideBar.tsx
import React from 'react';
import { Layout, Menu, Image } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
const { Sider } = Layout;
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// 自定义左侧菜单栏 - 递归
const renderMenus = (menus: any[]) => {
return menus.map(item => {
if (item.children) {
return (
<Menu.SubMenu title = { item.label } key = { item.key }>
{ renderMenus(item.children) }
</Menu.SubMenu>
)
} else {
return <Menu.Item key = { item.key }>{ item.label }</Menu.Item>
}
})
}
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" style={ {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#fff'
}}>
<Image src = { logo } width="28px" height="28px" preview={ false }></Image>
{ !collapsed && <div style={
{
height: '32px',
overflow: 'hidden',
lineHeight: '32px'
}}>嗨购后台管理系统</div> }
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
>
{
renderMenus(menus)
}
</Menu>
</Sider>
);
};
export default App;
组件形式渲染左侧菜单目前并不推荐使用
7.4 菜单渲染优化
如果左侧菜单栏数据过于庞大,每个管理项里又有很多项,需要只展开一个菜单项
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
const [openKeys, setOpenKeys] = useState(['sub1']);
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" style={ {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#fff'
}}>
<Image src = { logo } width="28px" height="28px" preview={ false }></Image>
{ !collapsed && <div style={
{
height: '32px',
overflow: 'hidden',
lineHeight: '32px'
}}>嗨购后台管理系统</div> }
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={ menus }
openKeys={openKeys}
onOpenChange={onOpenChange}
/>
</Sider>
);
};
export default App;
8.定义路由
8.1 官方文档
Home v6.26.1 | React Router
8.2 创建对应的页面
|-src | |- ... | |-views | |- banner | |- List.tsx #首页轮播图 | | |- Add.tsx #添加轮播图 | |- home | | |- Index.tsx #系统首页 | |- pro | | |- List.tsx #产品管理 | | |- Search.tsx #筛选列表 | |- account | | |- User.tsx #用户列表 | | |- Admin.tsx#管理员列表
// src/views/home/Index.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>系统首页</div>
)
}
export default Com
// src/views/account/Admin.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>管理员列表</div>
)
}
export default Com
// src/views/account/User.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>用户列表</div>
)
}
export default Com
// src/views/banner/Add.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>添加轮播图</div>
)
}
export default Com
// src/views/banner/List.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>轮播图列表</div>
)
}
export default Com
// src/views/pro/List.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>产品列表</div>
)
}
export default Com
// src/views/pro/Search.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>筛选列表</div>
)
}
export defa
8.3 定义菜单路由信息
v6的路由通过 element 属性定义匹配的组件
因此menus中可以添加一个 element 属性,值就为组件的引用即可
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />,
element: <Home />
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
element: <BannerList />
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
element: <BannerAdd />
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
element: <ProList />
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
element: <SearchList />
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
element: <UserList />
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
element: <AdminList />
}
]
}
]
export default menus
8.4.装载路由
在根组件添加 BrowserRouter
或者 HashRouter
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
<React.StrictMode>
<ConfigProvider
theme = { {
token: {
colorPrimary: '#1890ff'
}
} }
>
<Provider store = { store }>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</ConfigProvider>
</React.StrictMode>
);
reportWebVitals();
8.5 定义路由组件
在menu.tsx
里已经定义好了请求的路径(其实就是数据中key属性)和路径对应组件(其实就是数据中的element属性),剩下就是定义路由组件了
组件渲染的区域 AppMain
组件
// src/layout/components/AppMain.tsx
import React from 'react';
import { Layout, theme } from 'antd';
import { Routes, Route, Navigate } from 'react-router-dom';
// import BannerAdd from '@/views/banner/Add'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const renderRoute: any = (menus: IMyMenuItem[]) => {
return menus.map(item => {
if (item.children) {
// React.Fragment 也为空标签,可以设置 key 属性
// 实现 重定向
return (
<React.Fragment key = { item.path }>
<Route path = { item.path } element = { <Navigate to = { item.redirect! } />} />
{
renderRoute(item.children!)
}
</React.Fragment>
)
} else {
return <Route key = { item.path } path = { item.path } element = { item.element } />
}
})
}
return (
<Content
style={
{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
}}
>
<Routes>
{/* <Route path="/banner" element = { <Navigate to="/banner/add" /> } /> */}
{/* <Route path="/banner/add" element = { <BannerAdd /> } /> */}
{ renderRoute(menus) }
</Routes>
</Content>
);
};
export default App;
8.6 手动测试路由
可以在地址栏输入路径,测试是否正常
http://localhost:3000/ #系统首页
http://localhost:3000/banner #轮播图管理
http://localhost:3000/banner/list #轮播图列表
http://localhost:3000/banner/add #添加轮播图
http://localhost:3000/pro #产品管理
http://localhost:3000/pro/search #筛选列表
http://localhost:3000/pro/list #产品列表
http://localhost:3000/account #账户管理
http://localhost:3000/account/user #用户列表
http://localhost:3000/account/admin #管理员列表
8.7 设置404页面
// src/views/error/Page404.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC<IAppProps> = (props) => {
return (
<div>404</div>
)
}
export default Com
// src/layout/components/AppMain.tsx
import React from 'react';
import { Layout, theme } from 'antd';
import { Routes, Route, Navigate } from 'react-router-dom';
// import BannerAdd from '@/views/banner/Add'
import Page404 from '@/views/error/Page404'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const renderRoute: any = (menus: IMyMenuItem[]) => {
return menus.map(item => {
if (item.children) {
// React.Fragment 也为空标签,可以设置 key 属性
// 实现 重定向
return (
<React.Fragment key = { item.path }>
<Route path = { item.path } element = { <Navigate to = { item.redirect! } />} />
{
renderRoute(item.children!)
}
</React.Fragment>
)
} else {
return <Route key = { item.path } path = { item.path } element = { item.element } />
}
})
}
return (
<Content
style={
{
margin: '24px 16px',
padding: 24,
minHeight: 280,
background: colorBgContainer,
}}
>
<Routes>
{/* <Route path="/banner" element = { <Navigate to="/banner/add" /> } /> */}
{/* <Route path="/banner/add" element = { <BannerAdd /> } /> */}
{ renderRoute(menus) }
<Route path="*" element = { <Page404 /> } />
</Routes>
</Content>
);
};
export default App;
9 切换路由
上述项目中,切换路由都是手动输入的,实际上应该点击左侧菜单栏进行路由导航。
左侧菜单的逻辑交互,前面已经生成了(openKeys 以及 onOpenChanges 实现)
现在通过点击事件来切换导航
9.1 点击切换路由
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useNavigate } from 'react-router-dom';
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
const [openKeys, setOpenKeys] = useState(['']);
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// console.log('keys', keys)
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
// console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
const navigate = useNavigate()
const changeUrl = ({ key }: { key: string }) => {
console.log(key)
navigate(key)
}
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" style={ {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#fff'
}}>
<Image src = { logo } width="28px" height="28px" preview={ false }></Image>
{ !collapsed && <div style={
{
height: '32px',
overflow: 'hidden',
lineHeight: '32px'
}}>嗨购后台管理系统</div> }
</div>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['1']}
items={ menus }
openKeys={openKeys}
onOpenChange={onOpenChange}
onClick={changeUrl}
/>
</Sider>
);
};
export default App;
9.2 刷新保持左侧菜单状态
当页面刷新时,需要保证当前二级路由是展开的,且当前路由是被选中的状态
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// /pro/search
const { pathname } = useLocation() // /pro/search
// console.log(location)
const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// console.log('keys', keys)
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
// console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
const navigate = useNavigate()
const changeUrl = ({ key }: { key: string }) => {
// console.log(key)
navigate(key)
setSelectedKeys([key]) // 点击时需要告诉程序哪一项被选中
}
return (
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" style={ {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#fff'
}}>
<Image src = { logo } width="28px" height="28px" preview={ false }></Image>
{ !collapsed && <div style={
{
height: '32px',
overflow: 'hidden',
lineHeight: '32px'
}}>嗨购后台管理系统</div> }
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={ selectedKeys }
items={ menus }
openKeys={openKeys}
onOpenChange={onOpenChange}
onClick={changeUrl}
/>
</Sider>
);
};
export default App;
10 设置面包屑导航
10.1 参考文档
通过案例项目,得知 面包屑组件应该包含在 页面的头部 https://vvbin.cn/next/#/feat/breadcrumb/flat
参照组件库的面包屑 https://ant-design.g