当前位置: 首页 > news >正文

React多功能管理平台项目开发全教程

​🌈个人主页:前端青山
🔥系列专栏: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-srcApp.cssApp.test.tsx App.tsx的测试文件  npm run test 查看测试结果App.tsxindex.cssindex.tsx react应用程序的入口文件logo.svg react-app-env.d.ts // 声明文件 // 指令声明对包的依赖关系reportWebVitals.ts // 测试性能seupTests.ts // 使用jest做为测试工具.gitignorepackage-lock.jsonpackage.jsonREADME.mdtsconfig.json

*.d.ts 代表ts的声明文件

2.改造目录结构

src
   apicomponentslayoutstorerouterutilsviewsApp.tsxindex.tsxlogo.svgreact-app-env.d.tsreportWebVitals.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" /><Menutheme="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><Contentstyle={{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" /><Menutheme="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 (<Contentstyle={{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 类型

虽然可以将RootStateandAppDispatch类型导入到每个组件中,但最好创建useDispatchand useSelectorhooks 的类型化版本以在您的应用程序中使用

// 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><Menutheme="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><Menutheme="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><ConfigProvidertheme = { {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 (<Contentstyle={{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 (<Contentstyle={{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 /accountif (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><Menutheme="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/searchconst { 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 /accountif (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><Menutheme="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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • C++ | Leetcode C++题解之第387题字符串中的第一个唯一字符
  • Go入门:gin框架极速搭建图书管理系统
  • MySQL:复合查询
  • 深度学习(二)
  • minio最新源码编译(处理安全扫描中跨域访问、.js.map等不安全问题)
  • SQLite3 数据类型深入全面讲解
  • 【PyQt】切换界面的实现
  • day-45 全排列 II
  • 【机器学习】循环神经网络(RNN)介绍
  • MySQL集群技术4——MySQL路由
  • 【大模型】Reflextion解读
  • P01-何谓Java方法
  • Nginx: 使用KeepAlived配置实现虚IP在多服务器节点漂移及Nginx高可用原理
  • macos 10.15 Catalina 可用docker最新版本 Docker Desktop 4.15.0 (93002) 下载地址与安装方法
  • 视觉辅助应用场景
  • Babel配置的不完全指南
  • Javascripit类型转换比较那点事儿,双等号(==)
  • JavaScript 无符号位移运算符 三个大于号 的使用方法
  • JavaScript异步流程控制的前世今生
  • Linux编程学习笔记 | Linux IO学习[1] - 文件IO
  • Linux中的硬链接与软链接
  • mysql_config not found
  • PHP 7 修改了什么呢 -- 2
  • vue自定义指令实现v-tap插件
  • XML已死 ?
  • 技术:超级实用的电脑小技巧
  • 聊聊redis的数据结构的应用
  • 那些年我们用过的显示性能指标
  • 微信开放平台全网发布【失败】的几点排查方法
  • 写代码的正确姿势
  • 学习使用ExpressJS 4.0中的新Router
  • FaaS 的简单实践
  • JavaScript 新语法详解:Class 的私有属性与私有方法 ...
  • 翻译 | The Principles of OOD 面向对象设计原则
  • 浅谈sql中的in与not in,exists与not exists的区别
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #我与Java虚拟机的故事#连载11: JVM学习之路
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • (pojstep1.3.1)1017(构造法模拟)
  • (初研) Sentence-embedding fine-tune notebook
  • .md即markdown文件的基本常用编写语法
  • .NET Core 网络数据采集 -- 使用AngleSharp做html解析
  • .Net Core中的内存缓存实现——Redis及MemoryCache(2个可选)方案的实现
  • .net MVC中使用angularJs刷新页面数据列表
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .Net插件开发开源框架
  • .NET国产化改造探索(三)、银河麒麟安装.NET 8环境
  • .NET开源快速、强大、免费的电子表格组件
  • .net连接MySQL的方法
  • .so文件(linux系统)
  • ??javascript里的变量问题
  • @EnableConfigurationProperties注解使用
  • [ 常用工具篇 ] POC-bomber 漏洞检测工具安装及使用详解
  • [2021]Zookeeper getAcl命令未授权访问漏洞概述与解决
  • [2669]2-2 Time类的定义