https://www.html.cn/create-react-app/
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版本:https://reactrouter.com/
如需使用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库
官网地址:https://ant.design/index-cn 5.2.0
国内官方镜像地址:https://ant-design.antgroup.com/index-cn
国内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 自定义主题
https://ant-design.antgroup.com/docs/react/use-in-typescript-cn
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 其他第三方工具包
https://www.lodashjs.com/
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.创建主布局文件
预览模板:https://pro.ant.design/zh-CN/
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来管理状态
http://cn.redux.js.org/
参考链接:http://cn.redux.js.org/tutorials/typescript-quick-start
6.1 定义State和Dispatch类型
// src/store/index.ts
// 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 官方文档
https://reactrouter.com/
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;
// 获取哪些项具有二级菜单