https://www.html.cn/create-react-app/

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.改造目录结构

srcapicomponentslayoutstorerouterutilsviewsApp.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版本: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><ConfigProvidertheme = { {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" /><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.tsexport { 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.dispatchexport 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.actionsexport 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.dispatchexport 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><ConfigProvidertheme = { {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" /><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;

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: falsecollapsed: localStorage.getItem('collapsed') === 'true'
}export const appSlice = createSlice({name: 'app',initialState,reducers: {changeCollapsed (state) {state.collapsed = !state.collapsedlocalStorage.setItem('collapsed', String(state.collapsed))}}
})export const { changeCollapsed } = appSlice.actionsexport 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.actionsexport 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><Menutheme="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 官方文档

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.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>系统首页</div>)
}export default Com
// src/views/account/Admin.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>管理员列表</div>)
}export default Com
// src/views/account/User.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>用户列表</div>)
}export default Com
// src/views/banner/Add.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>添加轮播图</div>)
}export default Com
// src/views/banner/List.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>轮播图列表</div>)
}export default Com
// src/views/pro/List.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>产品列表</div>)
}export default Com
// src/views/pro/Search.tsximport 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.tsximport 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.gitee.io/components/breadcrumb-cn/#components-breadcrumb-demo-react-router

10.2 设置面包屑导航

头部组件加入了面包屑导航组件,尽可能不动原来的布局

// src/layout/components/AppHeader.tsx
import React from 'react';
import {MenuFoldOutlined,MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { useLocation, Link } from 'react-router-dom'
import menus, { IMyMenuItem } from '@/router/menu'
const { Header } = Layout;// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}function getBreadcrumbNameMap (menus: any[]) {menus.forEach(item => {if (item.children) {breadcrumbNameMap[item.path] = item.labelgetBreadcrumbNameMap(item.children)} else {breadcrumbNameMap[item.path] = item.label}})
}
console.log(breadcrumbNameMap)getBreadcrumbNameMap(menus)
const App: React.FC = () => {// const [collapsed, setCollapsed] = useState(false);const collapsed = useAppSelector(state => state.app.collapsed)const dispatch = useAppDispatch()const {token: { colorBgContainer },} = theme.useToken();const location = useLocation(); // /pro/listconst pathSnippets = location.pathname.split('/').filter((i) => i);console.log(pathSnippets) // ['pro', 'list']const extraBreadcrumbItems = pathSnippets.map((_, index) => {const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;console.log(url) // /pro   /pro/listreturn (<Breadcrumb.Item key={url}><Link to={url}>{breadcrumbNameMap[url]}</Link></Breadcrumb.Item>);});const breadcrumbItems = [<Breadcrumb.Item key="home"><Link to="/">系统首页</Link></Breadcrumb.Item>,].concat(extraBreadcrumbItems);return (<Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {className: 'trigger',// onClick: () => setCollapsed(!collapsed),onClick: () => dispatch(changeCollapsed())})}<Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb></Header>);
};export default App;

随之而来的问题就是,当点击面包屑导航时,地址栏的路由已经发生了跳转,但是左侧菜单栏数据效果没有实时更新(左侧菜单栏组件早就创建完毕,选中和打开的选项已经做了固定, 点击面包屑没有引起左侧菜单栏组件的状态以及属性的更新,左侧菜单栏不会重新渲染)

此时可以在左侧菜单栏组件监听 路由的变化 – -useEffect

// src/layout/components/SideBar.tsx
import React, { useEffect, 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]) // 点击时需要告诉程序哪一项被选中}useEffect(() => { // ++++++++++++setSelectedKeys([pathname])setOpenKeys(['/' + pathname.split('/')[1] ])}, [pathname])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;

11.快捷切换页

https://panjiachen.gitee.io/vue-element-admin/#/charts/line

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upxVnLXz-1678063667812)(assets/image-20221025113910072.png)]

  • 系统默认路由为系统首页,所以第一个就为系统首页,且系统首页不可关闭
  • 切换路由,判断当前页面是否已存在,如果存在,找到列表项的索引值,设置该索引值选中效果,并且页面切换至该索引值
  • 如果当前路由对应的页面不存在,则在最后添加一项新的数据,并且设置最后一项为选中项

11.1 准备组件

// src/layout/components/AppTabs.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div style={{ height: 40, background: '#fff', borderTop: '1px solid #ccc' }}>AppTabs</div>)
}export default Com
// src/layout/components/index.tsexport { default as SideBar } from './SideBar'
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
export { default as AppTabs } from './AppTabs'
// 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, AppTabs } from './components'const App: React.FC = () => {return (<Layout id="components-layout-demo-custom-trigger"><SideBar /><Layout className="site-layout"><AppHeader /><AppTabs /><AppMain /></Layout></Layout>);
};export default App;

11.2 处理数据

后期 监听地址栏 从tabsArr 中提取数据

 const tabsArr = [{"label":"系统首页","key":"/"},{"label":"轮播图列表","key":"/banner/list"},{"label":"添加轮播图","key":"/banner/add"},{"label":"产品列表","key":"/pro/list"},{"label":"筛选列表","key":"/pro/search"},{"label":"用户列表","key":"/account/user"},{"label":"管理员列表","key":"/account/admin"}]

11.3 监听路由添加数据

11.4 点击tab页切换路由,关闭效果

// src/layout/components/AppTabs.tsximport React, { FC, useEffect, useState } from 'react';
import menus from '@/router/menu'
import { useLocation, useNavigate } from 'react-router-dom';
import { Tag } from 'antd'
interface IAppProps {
}
// 需要的原始数据
const tabAttr: { label: any; key: any; }[] = []
function getTabAttrs (menus: any[]) {menus.forEach(item => {if (item.children) {getTabAttrs(item.children)} else {tabAttr.push({label: item.label,key: item.key})}})
}
getTabAttrs(menus)
// console.log('tabAttr', tabAttr)
const Com: FC<IAppProps> = (props) => {// 当前地址栏的地址const { pathname } = useLocation()// 快捷导航的数组const [arr, setArr] = useState([{ label: '系统首页', key: '/' }])// 选中的索引值 - 加样式const [current, setCurrent] = useState(0)const [num, setNum] = useState(0) // 为了获取最新的数据useEffect(() => {// 判断当前的路由在不在快捷导航的数组中const index = arr.findIndex(item => item.key === pathname)if (index !== -1) {// 如果在,拿到索引值,添加样式setCurrent(index)} else {// 如果不在快捷导航数组中// 从原始数据中获取值const item = tabAttr.find(item => item.key === pathname)const newArr = arritem && newArr.push(item)// 修改状态setArr(newArr)setCurrent(arr.length - 1)}}, [pathname, arr, num]) // 一旦num发生变化 一定会获取到最新的数据const navigate = useNavigate()return (<div style={{ height: 40, background: '#fff', borderTop: '1px solid #ccc', paddingLeft: 16, display: 'flex', flexWrap: 'nowrap', overflowX: 'auto' }}>{arr && arr.map((item, index) => {return (<Tag style = {{ height: 26, lineHeight: '26px', marginTop: 7, borderRadius: 0, cursor: 'pointer'}}onClose={ () => {// 当前选中的这一项删除if (current === index) {// 选中上一个数据,跳转页面navigate(arr[index - 1].key)setCurrent(current - 1) } else {// 未选中删除if (index < current) { // 删除选中左边// 索引值减一setCurrent(current - 1)} else {// 让组件的状态发生改变// console.log('current', current)// console.log('arr', arr)// console.log('index', index)setNum(Math.random()) // 初始获取最新数据}}// 删除数据const deleteArr = arrdeleteArr.splice(index, 1)setArr(deleteArr)}} onClick = { () => {// console.log('test', index)navigate(arr[index].key)}}closable = { index !== 0 } key = { item.key } color = { current === index ? '#108ee9': '#ccc' }>{ item.label }</Tag>)})}</div>)
}export default Com

12.数据请求的封装

// src/utils/request.ts
import axios, { AxiosRequestConfig } from  'axios'
import store2 from 'store2'const isDev = process.env.NODE_ENV === 'development'const ins = axios.create({baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
})ins.interceptors.request.use(config => {config.headers!.token = store2.get('token') || ''return config
}, error => Promise.reject(error))ins.interceptors.response.use(response => {if (response.data.code === '10119') {store2.remove('token')store2.remove('adminname')window.location.href = "/login"}return response
}, error => Promise.reject(error))// 自定义各种常用的restful api的请求
// axios.get('url', { params: { key: value } })
// axios.post('url', { key: value })
// axios({ url: '', method: 'GET', params: { key: value }})
// axios({ url: '', method: 'POST', data: { key: value }})
export default function request( config: AxiosRequestConfig ) {// 接口请求 必须参数  url method  data  headersconst { url = '', method = 'GET', data = {}, headers = {} } = config// 区分不同的数据请求 为了执行时传入的数据请求方式统一性 GEt GeT get GETswitch (method.toUpperCase()) {case 'GET':return ins.get(url, { params: data })case 'POST': // 可能数据请求方式 表单提交  文件提交   默认json// 表单提交if (headers['content-type'] === 'application/x-www-form-url-encoded') {// 转换参数  URLSearchParams  / 第三方库 qsconst p = new URLSearchParams()for (const key in data) {p.append(key, data[key])}return ins.post(url, p, { headers })}// 文件提交if (headers['content-type'] === 'multipart/form-data') {const p = new FormData()for (const key in data) {p.append(key, data[key])}return ins.post(url, p, { headers })}// 默认 application/jsonreturn ins.post(url, data)// 修改数据 - 所有的数据的更新case 'PUT':return ins.put(url, data)// 删除数据case 'DELETE': return ins.delete(url, { data })  // 修改数据 - 部分的数据的更新case 'PATCH':return ins.patch(url, data)default:return ins(config)}
}

按照思维来看,此时需要请求以及渲染轮播图管理相关功能,但是查看后端接口,发现基本所有的借口都需要基于 token,那么需要首先完成登录功能

接口文档:http://121.89.205.189:3000/admindoc/

13 构建登录页面

13.1 参考组件库组件

https://ant-design.gitee.io/components/form-cn/#components-form-demo-normal-login

13.2 构造登录接口API

// src/api/admin.ts
import request from '@/utils/request'
export interface IAdminLoginParams {adminname: stringpassword: string
}
export function loginFn (params: IAdminLoginParams) {return request({url: '/admin/login',method: 'POST',data: params})
}

13.3 创建登录的页面

// src/views/login/Index.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>登录</div>)
}export default Com

13.4 创建登录路由

// src/App.tsx
import React, { FC } from 'react';import { Routes, Route } from 'react-router-dom'import Index from '@/layout/Index'
import Login from '@/views/login/Index'import './App.css'interface IAppProps {
}const App: FC<IAppProps> = (props) => {return (<Routes><Route path="/login" element = { <Login /> } /><Route path='/*' element = { <Index /> } />{/* <Index /> */}</Routes>)
}export default App

地址栏访问 http://localhost:3000/login 即可看到登录页面出现,其余路由还保持和之前一致

13.4 完善登录界面

/* src/views/login/login.module.css */.loginBox {width: 100%;height: 100%;background-color: #2d3a4b;display: flex;justify-content: center;align-items: center;
}.loginForm {width: 460px;height: 350px;/* background-color: #fff; */
}
.loginTitle {text-align: center;color: #fff;font-size: 26px;margin-bottom: 30px;
}
.myInput {height: 47px;background-color: #2d3a4b;
}
.myInput input {background-color: #2d3a4b;color: #fff;
}
.myInput input::-webkit-input-placeholder{color:#fff;
}
.loginBtn {height: 36px;
}
.tip {display: flex;color: #fff;width: 50%;
}
.tip div {flex: 1;
}
// src/views/login/Index.tsximport { IAdminLoginParams } from '@/api/admin';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import React, { FC } from 'react';import style from './login.module.css'
interface IAppProps {
}const Com: FC<IAppProps> = (props) => {const onFinish = (values: IAdminLoginParams) => {console.log('Received values of form: ', values);};return (<div className={ style.loginBox }><div className={ style.loginForm }><h1 className={ style.loginTitle }>系统登录</h1><Formname="normal_login"className="login-form"initialValues={ { adminname: 'admin', password: '123456' }}onFinish={onFinish}><Form.Itemname="adminname"rules={[{ required: true, message: '请输入管理员账户!' }]}><Input className={ style.myInput } style = {{ color: '#fff' }}prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账户" /></Form.Item><Form.Itemname="password"rules={[{ required: true, message: '请输入密码!' }]}><InputclassName={ style.myInput }style = {{ color: '#fff' }}prefix={<LockOutlined className="site-form-item-icon" />}type="password"placeholder="密码"/></Form.Item><Form.Item><Button className={ style.loginBtn } block type="primary" htmlType="submit" >登录</Button></Form.Item><div className={ style.tip }><div>账户:admin</div><div>密码:123456</div></div></Form></div></div>)
}export default Com

使用状态管理器,异步操作可以在组件,也可以在状态管理器

14 执行登录

使用状态管理器(RTK)管理登录信息。

14.1 构建模块 admins

// src/store/modules/admin.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import store2 from 'store2'
interface IAppState {loginState: booleanadminname: stringtoken: string
}const initialState: IAppState = {loginState: Boolean(store2.get('loginState')) || false,adminname: store2.get('adminname') || '',token: store2.get('token') || ''
}export const appSlice = createSlice({name: 'admin',initialState,reducers: {changeLoginState (state, action: PayloadAction<boolean>) {state.loginState = action.payloadstore2.set('loginState', action.payload)},changeAdminName (state, action: PayloadAction<string>) {state.adminname = action.payloadstore2.set('adminname', String(state.adminname))},changeToken (state, action: PayloadAction<string>) {state.token = action.payloadstore2.set('token', String(state.token))}}
})export const { changeLoginState, changeAdminName, changeToken } = appSlice.actionsexport default appSlice.reducer

14.2 装载模块

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'import app from './modules/app'
import admin from './modules/admin'const store = configureStore({reducer: {app,admin}
})// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatchexport default store

14.3 登录实现

// src/views/login/Index.tsximport { IAdminLoginParams, loginFn } from '@/api/admin';
import { useAppDispatch } from '@/store/hooks';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input, message } from 'antd';
import React, { FC } from 'react';
import { changeAdminName, changeLoginState, changeToken } from '@/store/modules/admin'
import style from './login.module.css'
import { useNavigate } from 'react-router-dom';
interface IAppProps {
}const Com: FC<IAppProps> = (props) => {const dispatch = useAppDispatch()const navigate = useNavigate()const onFinish = (values: IAdminLoginParams) => {console.log('Received values of form: ', values);loginFn(values).then(res => {if (res.data.code === '10003') {message.warning('密码输入错误')} else if (res.data.code === '10005') {message.error('账户不存在')} else {message.success('登录成功')console.log(res.data.data)dispatch(changeLoginState(true))dispatch(changeAdminName(res.data.data.adminname))dispatch(changeToken(res.data.data.token))navigate('/')}})};return (<div className={ style.loginBox }><div className={ style.loginForm }><h1 className={ style.loginTitle }>系统登录</h1><Formname="normal_login"className="login-form"initialValues={ { adminname: 'admin', password: '123456' }}onFinish={onFinish}><Form.Itemname="adminname"rules={[{ required: true, message: '请输入管理员账户!' }]}><Input className={ style.myInput } style = {{ color: '#fff' }}prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账户" /></Form.Item><Form.Itemname="password"rules={[{ required: true, message: '请输入密码!' }]}><InputclassName={ style.myInput }style = {{ color: '#fff' }}prefix={<LockOutlined className="site-form-item-icon" />}type="password"placeholder="密码"/></Form.Item><Form.Item><Button className={ style.loginBtn } block type="primary" htmlType="submit" >登录</Button></Form.Item><div className={ style.tip }><div>账户:admin</div><div>密码:123456</div></div></Form></div></div>)
}export default Com

15.前端登录验证

当前路由在登录页面,判断用户的登录状态,如果登录,则跳转到系统的首页,如果未登录,显示登录页面

当前路由在非登录页面,判断用户的登录状态,如果登录,则显示非登录页面,如果未登录,跳转到登录页面

// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate } from 'react-router-dom'import Index from '@/layout/Index'
import Login from '@/views/login/Index'import './App.css'
import { useAppSelector } from './store/hooks';interface IAppProps {
}const App: FC<IAppProps> = (props) => {const loginState = useAppSelector(state => state.admin.loginState)return (<Routes><Route path="/login" element = { loginState ? <Navigate to="/"/> :<Login /> } /><Route path='/*' element = { loginState ? <Index /> : <Navigate to="/login"/> } />{/* <Index /> */}</Routes>)
}export default App

16 .后端token校验

封装axios时已经实现 — 响应拦截器

后台管理系统都需要请求数据,而请求数据 都需要添加token字段

// src/utils/request.ts
import axios, { AxiosRequestConfig } from  'axios'
import store2 from 'store2'const isDev = process.env.NODE_ENV === 'development'const ins = axios.create({baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
})ins.interceptors.request.use(config => {config.headers!.token = store2.get('token') || ''return config
}, error => Promise.reject(error))ins.interceptors.response.use(response => {if (response.data.code === '10119') {store2.remove('token')store2.remove('adminname')store2.remove('loginState')window.location.href = "/login"}return response
}, error => Promise.reject(error))// 自定义各种常用的restful api的请求
// axios.get('url', { params: { key: value } })
// axios.post('url', { key: value })
// axios({ url: '', method: 'GET', params: { key: value }})
// axios({ url: '', method: 'POST', data: { key: value }})
export default function request(config: AxiosRequestConfig) {// 接口请求 必须参数  url method  data  headersconst { url = '', method = 'GET', data = {}, headers = {} } = config// 区分不同的数据请求 为了执行时传入的数据请求方式统一性 GEt GeT get GETswitch (method.toUpperCase()) {case 'GET':return ins.get(url, { params: data })case 'POST': // 可能数据请求方式 表单提交  文件提交   默认json// 表单提交if (headers['content-type'] === 'application/x-www-form-url-encoded') {// 转换参数  URLSearchParams  / 第三方库 qsconst p = new URLSearchParams()for (const key in data) {p.append(key, data[key])}return ins.post(url, p, { headers })}// 文件提交if (headers['content-type'] === 'multipart/form-data') {const p = new FormData()for (const key in data) {p.append(key, data[key])}return ins.post(url, p, { headers })}// 默认 application/jsonreturn ins.post(url, data)// 修改数据 - 所有的数据的更新case 'PUT':return ins.put(url, data)// 删除数据case 'DELETE': return ins.delete(url, { data })  // 修改数据 - 部分的数据的更新case 'PATCH':return ins.patch(url, data)default:return ins(config)}
}

17.退出登录

17.1 实现退出登录

https://ant-design.gitee.io/components/dropdown-cn/#components-dropdown-demo-trigger

// src/layout/components/AppHeader.tsx
import React from 'react';
import {DownOutlined,MenuFoldOutlined,MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}function getBreadcrumbNameMap (menus: any[]) {menus.forEach(item => {if (item.children) {breadcrumbNameMap[item.path] = item.labelgetBreadcrumbNameMap(item.children)} else {breadcrumbNameMap[item.path] = item.label}})
}
// console.log(breadcrumbNameMap)getBreadcrumbNameMap(menus)
const App: React.FC = () => {// const [collapsed, setCollapsed] = useState(false);const collapsed = useAppSelector(state => state.app.collapsed)const dispatch = useAppDispatch()const {token: { colorBgContainer },} = theme.useToken();const location = useLocation(); // /pro/listconst pathSnippets = location.pathname.split('/').filter((i) => i);// console.log(pathSnippets) // ['pro', 'list']const extraBreadcrumbItems = pathSnippets.map((_, index) => {const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;// console.log(url) // /pro   /pro/listreturn (<Breadcrumb.Item key={url}><Link to={url}>{breadcrumbNameMap[url]}</Link></Breadcrumb.Item>);});const breadcrumbItems = [<Breadcrumb.Item key="home"><Link to="/">系统首页</Link></Breadcrumb.Item>,].concat(extraBreadcrumbItems);const items: MenuProps['items'] = [{label: '个人中心',key: '/center',},{type: 'divider',},{label: '退出',key: '/logout',},];const navigate = useNavigate()const onClick: MenuProps['onClick'] = ({ key }) => {// console.log(key)// navigate(key)if (key === '/logout') {store2.remove('loginState')store2.remove('adminname')store2.remove('token')dispatch(changeLoginState(false)) // 只需要修改 loginStatenavigate('/login')}}return (<Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {className: 'trigger',// onClick: () => setCollapsed(!collapsed),onClick: () => dispatch(changeCollapsed())})}<Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb><div style={{ position: 'absolute', right: 16 }}><Dropdown menu={{ items, onClick }} trigger={['click']} ><span onClick={(e) => e.preventDefault()}><Space><Image preview = { false } style={{ width: 40, height: 40, borderRadius: '10px' }} src='https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80' /><DownOutlined /></Space></span></Dropdown></div></Header>);
};export default App;

17.2 保留退出时的页面

先获取退出登陆时 路由的地址

// src/layout/components/AppHeader.tsx
import React from 'react';
import {DownOutlined,MenuFoldOutlined,MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}function getBreadcrumbNameMap (menus: any[]) {menus.forEach(item => {if (item.children) {breadcrumbNameMap[item.path] = item.labelgetBreadcrumbNameMap(item.children)} else {breadcrumbNameMap[item.path] = item.label}})
}
// console.log(breadcrumbNameMap)getBreadcrumbNameMap(menus)
const App: React.FC = () => {// const [collapsed, setCollapsed] = useState(false);const collapsed = useAppSelector(state => state.app.collapsed)const dispatch = useAppDispatch()const {token: { colorBgContainer },} = theme.useToken();const location = useLocation(); // /pro/listconst pathSnippets = location.pathname.split('/').filter((i) => i);// console.log(pathSnippets) // ['pro', 'list']const extraBreadcrumbItems = pathSnippets.map((_, index) => {const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;// console.log(url) // /pro   /pro/listreturn (<Breadcrumb.Item key={url}><Link to={url}>{breadcrumbNameMap[url]}</Link></Breadcrumb.Item>);});const breadcrumbItems = [<Breadcrumb.Item key="home"><Link to="/">系统首页</Link></Breadcrumb.Item>,].concat(extraBreadcrumbItems);const items: MenuProps['items'] = [{label: '个人中心',key: '/center',},{type: 'divider',},{label: '退出',key: '/logout',},];const navigate = useNavigate()const { pathname } = useLocation()const onClick: MenuProps['onClick'] = ({ key }) => {// console.log(key)// navigate(key)if (key === '/logout') {store2.remove('loginState')store2.remove('adminname')store2.remove('token')dispatch(changeLoginState(false)) // 只需要修改 loginState// navigate('/login')navigate('/login?r=' + pathname)}}return (<Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {className: 'trigger',// onClick: () => setCollapsed(!collapsed),onClick: () => dispatch(changeCollapsed())})}<Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb><div style={{ position: 'absolute', right: 16 }}><Dropdown menu={{ items, onClick }} trigger={['click']} ><span onClick={(e) => e.preventDefault()}><Space><Image preview = { false } style={{ width: 40, height: 40, borderRadius: '10px' }} src='https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80' /><DownOutlined /></Space></span></Dropdown></div></Header>);
};export default App;

正常考虑问题思路是,在登陆时,登录成功之后 判断有没有退出时的记录地址,然后跳转

但实际上程序运行的思路是,当你登录成功之后,已经修改了登录状态,状态的改变引起视图的二次渲染,所以真正决定跳转地址的是App.tsx组件

// src/App.tsx
import React, { FC } from 'react';import { Routes, Route, Navigate, useSearchParams, useLocation } from 'react-router-dom'import Index from '@/layout/Index'
import Login from '@/views/login/Index'import './App.css'
import { useAppSelector } from './store/hooks';interface IAppProps {
}const App: FC<IAppProps> = (props) => {const loginState = useAppSelector(state => state.admin.loginState)// 1// const [params] = useSearchParams()// console.log('params', params.get('r') as string)// const url = params.get('r') as string// 2const location = useLocation()console.log('location', location.search)const url = location.search.split('?r=')[1]return (<Routes><Route path="/login" element = { loginState ? <Navigate to={ url ? url : "/" }/> :<Login /> } /><Route path='/*' element = { loginState ? <Index /> : <Navigate to="/login"/> } />{/* <Index /> */}</Routes>)
}export default App

18.隐藏左侧菜单项

添加一个设置页面

// src/views/set/Index.tsximport React, { FC } from 'react';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {return (<div>设置</div>)
}export default Com
// 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'import Set from '@/views/set/Index'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 />}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />},
]export default menus

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xNgpluG4-1678063667814)(assets/image-20230215160041756.png)]

给router/menu.tsx中不需要出现的 添加 hidden

给添加轮播图以及设置选项添加 hidden 属性

// 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'import Set from '@/views/set/Index'type MenuItem = Required<MenuProps>['items'][number];// 扩展固有的类型
export type IMyMenuItem = MenuItem & {path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性children?: IMyMenuItem[];redirect?: string; // 多级菜单的默认地址element?: ReactNode;hidden?: number
}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 />,hidden: 1}]},{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 />}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />,hidden: 1},
]export default menus

渲染左侧菜单栏数据时,可以过滤数据,将有hidden: 1子菜单删除掉

// src/layout/components/AppHeader.tsx
import React from 'react';
import {DownOutlined,MenuFoldOutlined,MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}function getBreadcrumbNameMap (menus: any[]) {menus.forEach(item => {if (item.children) {breadcrumbNameMap[item.path] = item.labelgetBreadcrumbNameMap(item.children)} else {breadcrumbNameMap[item.path] = item.label}})
}
// console.log(breadcrumbNameMap)getBreadcrumbNameMap(menus)
const App: React.FC = () => {// const [collapsed, setCollapsed] = useState(false);const collapsed = useAppSelector(state => state.app.collapsed)const dispatch = useAppDispatch()const {token: { colorBgContainer },} = theme.useToken();const location = useLocation(); // /pro/listconst pathSnippets = location.pathname.split('/').filter((i) => i);// console.log(pathSnippets) // ['pro', 'list']const extraBreadcrumbItems = pathSnippets.map((_, index) => {const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;// console.log(url) // /pro   /pro/listreturn (<Breadcrumb.Item key={url}><Link to={url}>{breadcrumbNameMap[url]}</Link></Breadcrumb.Item>);});const breadcrumbItems = [<Breadcrumb.Item key="home"><Link to="/">系统首页</Link></Breadcrumb.Item>,].concat(extraBreadcrumbItems);const items: MenuProps['items'] = [{label: '个人中心',key: '/center',},{label: '设置',key: '/set',},{type: 'divider',},{label: '退出',key: '/logout',},];const navigate = useNavigate()const { pathname } = useLocation()const onClick: MenuProps['onClick'] = ({ key }) => {// console.log(key)// navigate(key)if (key === '/logout') {store2.remove('loginState')store2.remove('adminname')store2.remove('token')dispatch(changeLoginState(false)) // 只需要修改 loginState// navigate('/login')navigate('/login?r=' + pathname)} else if (key === '/set') {navigate(key)}}return (<Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {className: 'trigger',// onClick: () => setCollapsed(!collapsed),onClick: () => dispatch(changeCollapsed())})}<Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb><div style={{ position: 'absolute', right: 16 }}><Dropdown menu={{ items, onClick }} trigger={['click']} ><span onClick={(e) => e.preventDefault()}><Space><Image preview = { false } style={{ width: 40, height: 40, borderRadius: '10px' }} src='https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80' /><DownOutlined /></Space></span></Dropdown></div></Header>);
};export default App;

隐藏子菜单使用 hidden 属性,如果使用的不是hidden属性,那么需要自行过滤数据

以下代码是过滤算法,本项目不需要

function getData (menus: IMenuProps[]) { // ++++++++++const items:IMenuProps[] = []menus.forEach(item => {if (item.children) {if (!item.hidden) {items.push({...item}) // 只提取二级菜单项中的第一层级}} else {if (!item.hidden) {items.push({...item}) // 一级菜单提取出来}}})items.forEach(item => { // 因为上面只提取了第一层级的数据if(item.children) {let a = getData(item.children)item.children = a}})return items
}

19. 管理员管理

19.1.设计接口

// src/api/admin.ts
import request from '@/utils/request'
export interface IAdminLoginParams {adminname: stringpassword: string
}
// 登录
export function loginFn (params: IAdminLoginParams) {return request({url: '/admin/login',method: 'POST',data: params,// headers: {//   'content-type': 'application/json'// }})
}
// 获取管理员列表数据
export function getAdminList () {return request({url: '/admin/list',// method: 'GET'})
}
// 获取管理员信息
export function getAdminDetail (params: { adminname: string }) {return request({url: '/admin/detail',// method: 'GET',data: params})
}
export interface IAddAdminParams {adminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
// 添加管理员
export function addAdmin (params: IAddAdminParams) {return request({url: '/admin/add',method: 'POST',data: params})
}
export interface IUpdateParams {adminname: stringrole: numbercheckedKeys: any[]
}// 修改管理员信息
export function updateAdmin (params: IUpdateParams) {return request({url: '/admin/update',method: 'POST',data: params})
}// 删除
export function deleteAdmin (params: { adminid: string }) {return request({url: '/admin/delete',method: 'POST',data: params})
}

19.2.展示管理员列表

// src/views/account/Admin.tsximport { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render () { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Button danger shape="circle" icon={<DeleteOutlined />} /></Space>)}}]const [adminList, setAdminList] = useState([])useEffect(() => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}, [])return (<><Table dataSource={ adminList }columns = { columns }rowKey="adminid"/></>)
}export default Com

19.3 优化表格滚动

如果屏幕比较小,默认展示的都是10条数据,就容易超出固定容器大小,此时可以通过 限制表格的滚动属性解决问题

height scroll

// src/views/account/Admin.tsximport { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render () { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Button danger shape="circle" icon={<DeleteOutlined />} /></Space>)}}]const [adminList, setAdminList] = useState([])useEffect(() => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度return (<Space direction='vertical' style = {{ width: '100%' }}><Button type='primary'>添加管理员</Button><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }/></Space>)
}export default Com

19.4 优化表格的分页器

优化数据表格(分页器优化 – 序号-分页之后需要要连贯)

// src/views/account/Admin.tsximport { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render () { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Button danger shape="circle" icon={<DeleteOutlined />} /></Space>)}}]const [adminList, setAdminList] = useState([])useEffect(() => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度return (<Space direction='vertical' style = {{ width: '100%' }}><Button type='primary'>添加管理员</Button><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }/></Space>)
}export default Com

19.5 添加中文包

由于 antd 组件的默认文案是英文,所以需要修改为中文

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wk2s3h3-1678063667815)(assets/image-20221026163808513.png)]

https://ant-design.gitee.io/docs/react/getting-started-cn

// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'import zhCN from 'antd/locale/zh_CN';import { BrowserRouter } from 'react-router-dom'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><ConfigProviderlocale={ zhCN }theme = { {token: {colorPrimary: '#1890ff'}} }><Provider store = { store }><BrowserRouter><App /></BrowserRouter></Provider></ConfigProvider></React.StrictMode>
);reportWebVitals()

19.6删除管理员

// src/views/account/Admin.tsximport { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Popconfirm, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度return (<Space direction='vertical' style = {{ width: '100%' }}><Button type='primary'>添加管理员</Button><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }/></Space>)
}export default Com

19.7 如何批量删除管理员数据

https://ant-design.gitee.io/components/table-cn/#components-table-demo-row-selection-custom

// src/views/account/Admin.tsximport { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Popconfirm, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);const onSelectChange = (newSelectedRowKeys: React.Key[]) => {console.log('selectedRowKeys changed: ', newSelectedRowKeys);setSelectedRowKeys(newSelectedRowKeys);};const rowSelection = {selectedRowKeys,onChange: onSelectChange,};const flag = useMemo(() => {return selectedRowKeys.length > 0}, [selectedRowKeys])const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const deleteMany = () => {// promise.allconst arr: any = []selectedRowKeys.forEach(item => {arr.push(deleteAdmin({ adminid: String(item) }))})Promise.all(arr).then(() => {getAdminListData()setSelectedRowKeys([])})}return (<Space direction='vertical' style = {{ width: '100%' }}><Space><Button type='primary'>添加管理员</Button>{ flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }</Space><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }rowSelection={rowSelection}/></Space>)
}export default Com

19.9.添加管理员

19.9.1 设置添加管理员的抽屉效果(无树形控件)

// src/views/account/Admin.tsximport { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);const onSelectChange = (newSelectedRowKeys: React.Key[]) => {console.log('selectedRowKeys changed: ', newSelectedRowKeys);setSelectedRowKeys(newSelectedRowKeys);};const rowSelection = {selectedRowKeys,onChange: onSelectChange,};const flag = useMemo(() => {return selectedRowKeys.length > 0}, [selectedRowKeys])const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const deleteMany = () => {// promise.allconst arr: any = []selectedRowKeys.forEach(item => {arr.push(deleteAdmin({ adminid: String(item) }))})Promise.all(arr).then(() => {getAdminListData()setSelectedRowKeys([])})}const [open, setOpen] = useState<boolean>(false)return (<Space direction='vertical' style = {{ width: '100%' }}><Space><Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>{ flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }</Space><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }rowSelection={rowSelection}/><Drawer title="添加管理员" placement="right" onClose={ () => { setOpen(false)} } open={open}><Space direction='vertical' style={{ width: '100%'}}><Input placeholder="管理员账户" /><Input placeholder="密码" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ () => {}}options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/></Space></Drawer></Space>)
}export default Com

19.9.2 修改菜单数据 添加了keyid字段

// 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'import Set from '@/views/set/Index'type MenuItem = Required<MenuProps>['items'][number];// 扩展固有的类型
export type IMyMenuItem = MenuItem & {path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性children?: IMyMenuItem[];redirect?: string; // 多级菜单的默认地址element?: ReactNode;hidden?: number;keyid: string 
}const menus: IMyMenuItem[] = [{path: '/',label: '系统首页',key: '/',icon: <HomeOutlined />,element: <Home />,keyid: '0-0'},{path: '/banner',label: '轮播图管理',key: '/banner',redirect: '/banner/list',icon: <HomeOutlined />,keyid: '0-1',children: [{path: '/banner/list',key: '/banner/list',label: '轮播图列表',icon: <HomeOutlined />,element: <BannerList />,keyid: '0-1-0'},{path: '/banner/add',key: '/banner/add',label: '添加轮播图',icon: <HomeOutlined />,element: <BannerAdd />,hidden: 1,keyid: '0-1-1'}]},{path: '/pro',label: '产品管理',key: '/pro',redirect: '/pro/list',icon: <HomeOutlined />,keyid: '0-2',children: [{path: '/pro/list',key: '/pro/list',label: '产品列表',icon: <HomeOutlined />,element: <ProList />,keyid: '0-2-0'},{path: '/pro/search',key: '/pro/search',label: '筛选列表',icon: <HomeOutlined />,element: <SearchList />,keyid: '0-2-1'}]},{path: '/account',label: '账户管理',key: '/account',redirect: '/account/user',icon: <HomeOutlined />,keyid: '0-3',children: [{path: '/account/user',key: '/account/user',label: '用户列表',icon: <HomeOutlined />,element: <UserList />,keyid: '0-3-0'},{path: '/account/admin',key: '/account/admin',label: '管理员列表',icon: <HomeOutlined />,element: <AdminList />,keyid: '0-3-1'}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />,hidden: 1,keyid: '0-4'},
]export default menus

19.9.3 添加管理员时选择该管理员权限

// src/views/account/Admin.tsximport { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus, { IMyMenuItem } from '@/router/menu'
import type { DataNode } from 'antd/es/tree';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}const getTreeData = (menus: any[] ) => {const arr: DataNode[] = []menus.forEach(item => {let obj: DataNode = {key: '',title: ''}if (item.children) {obj = {key: item.keyid,title: item.label,children: getTreeData(item.children)}} else {obj = {key: item.keyid,title: item.label}}arr.push(obj)})return arr
}
const Com: FC<IAppProps> = (props) => {const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);const onSelectChange = (newSelectedRowKeys: React.Key[]) => {console.log('selectedRowKeys changed: ', newSelectedRowKeys);setSelectedRowKeys(newSelectedRowKeys);};const rowSelection = {selectedRowKeys,onChange: onSelectChange,};const flag = useMemo(() => {return selectedRowKeys.length > 0}, [selectedRowKeys])const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const deleteMany = () => {// promise.allconst arr: any = []selectedRowKeys.forEach(item => {arr.push(deleteAdmin({ adminid: String(item) }))})Promise.all(arr).then(() => {getAdminListData()setSelectedRowKeys([])})}const [open, setOpen] = useState<boolean>(false)const [checkedKeys, setCheckedKeys] = useState(['0-0'])const [adminname, setAdminname] = useState('')const [password, setPassword] = useState('')const [role, setRole] = useState(1)return (<Space direction='vertical' style = {{ width: '100%' }}><Space><Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>{ flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }</Space><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }rowSelection={rowSelection}/><Drawer title="添加管理员" placement="right" onClose={ () => { setOpen(false)} } open={open}><Space direction='vertical' style={{ width: '100%'}}><Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" /><Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ (value) => {setRole(value)}}value = { role }options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/><TreecheckabletreeData={getTreeData(menus)}onCheck={(checkedKeysValue: any) => {console.log(checkedKeysValue)setCheckedKeys(checkedKeysValue)}}checkedKeys={checkedKeys}/><Button type='primary' onClick={() => {const data = { adminname, password, role, checkedKeys }console.log(data)}}>添加</Button></Space></Drawer></Space>)
}export default Com

19.9.4 添加管理员

添加完毕一定要记得重置(表单,权限)

// src/views/account/Admin.tsximport { addAdmin, deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}const getTreeData = (menus: any[] ) => {const arr: DataNode[] = []menus.forEach(item => {let obj: DataNode = {key: '',title: ''}if (item.children) {obj = {key: item.keyid,title: item.label,children: getTreeData(item.children)}} else {obj = {key: item.keyid,title: item.label}}arr.push(obj)})return arr
}
const Com: FC<IAppProps> = (props) => {const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);const onSelectChange = (newSelectedRowKeys: React.Key[]) => {console.log('selectedRowKeys changed: ', newSelectedRowKeys);setSelectedRowKeys(newSelectedRowKeys);};const rowSelection = {selectedRowKeys,onChange: onSelectChange,};const flag = useMemo(() => {return selectedRowKeys.length > 0}, [selectedRowKeys])const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const deleteMany = () => {// promise.allconst arr: any = []selectedRowKeys.forEach(item => {arr.push(deleteAdmin({ adminid: String(item) }))})Promise.all(arr).then(() => {getAdminListData()setSelectedRowKeys([])})}const [open, setOpen] = useState<boolean>(false)const [checkedKeys, setCheckedKeys] = useState(['0-0'])const [adminname, setAdminname] = useState('')const [password, setPassword] = useState('')const [role, setRole] = useState(1)return (<Space direction='vertical' style = {{ width: '100%' }}><Space><Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>{ flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }</Space><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }rowSelection={rowSelection}/><Drawer title="添加管理员" placement="right" onClose={ () => { setAdminname('')setPassword('')setRole(1)setCheckedKeys(['0-0'])setOpen(false)} } open={open}><Space direction='vertical' style={{ width: '100%'}}><Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" /><Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ (value) => {setRole(value)}}value = { role }options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/><TreecheckabletreeData={getTreeData(menus)}onCheck={(checkedKeysValue: any) => {console.log(checkedKeysValue)setCheckedKeys(checkedKeysValue)}}checkedKeys={checkedKeys}/><Button type='primary' onClick={() => {const data = { adminname, password, role, checkedKeys }console.log(data)addAdmin(data).then(() => {getAdminListData()setAdminname('')setPassword('')setRole(1)setCheckedKeys(['0-0'])setOpen(false)})}}>添加</Button></Space></Drawer></Space>)
}export default Com

19.10管理员修改

修改不重新生成新的页面,还在这个页面,使用模态框实现

// src/views/account/Admin.tsximport { addAdmin, deleteAdmin, getAdminList, updateAdmin } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Modal, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}const getTreeData = (menus: any[] ) => {const arr: DataNode[] = []menus.forEach(item => {let obj: DataNode = {key: '',title: ''}if (item.children) {obj = {key: item.keyid,title: item.label,children: getTreeData(item.children)}} else {obj = {key: item.keyid,title: item.label}}arr.push(obj)})return arr
}
const Com: FC<IAppProps> = (props) => {const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);const onSelectChange = (newSelectedRowKeys: React.Key[]) => {console.log('selectedRowKeys changed: ', newSelectedRowKeys);setSelectedRowKeys(newSelectedRowKeys);};const rowSelection = {selectedRowKeys,onChange: onSelectChange,};const flag = useMemo(() => {return selectedRowKeys.length > 0}, [selectedRowKeys])const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" onClick={ () => {setIsModalOpen(true)setAdminname(record.adminname)setRole(record.role)setCheckedKeys(record.checkedKeys)}} shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const deleteMany = () => {// promise.allconst arr: any = []selectedRowKeys.forEach(item => {arr.push(deleteAdmin({ adminid: String(item) }))})Promise.all(arr).then(() => {getAdminListData()setSelectedRowKeys([])})}const [open, setOpen] = useState<boolean>(false)const [checkedKeys, setCheckedKeys] = useState(['0-0'])const [adminname, setAdminname] = useState('')const [password, setPassword] = useState('')const [role, setRole] = useState(1)const [isModalOpen, setIsModalOpen] = useState(false)return (<Space direction='vertical' style = {{ width: '100%' }}><Space><Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>{ flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }</Space><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }rowSelection={rowSelection}/><Drawer title="添加管理员" placement="right" onClose={ () => { setAdminname('')setPassword('')setRole(1)setCheckedKeys(['0-0'])setOpen(false)} } open={open}><Space direction='vertical' style={{ width: '100%'}}><Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" /><Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ (value) => {setRole(value)}}value = { role }options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/><TreecheckabletreeData={getTreeData(menus)}onCheck={(checkedKeysValue: any) => {console.log(checkedKeysValue)setCheckedKeys(checkedKeysValue)}}checkedKeys={checkedKeys}/><Button type='primary' onClick={() => {const data = { adminname, password, role, checkedKeys }console.log(data)addAdmin(data).then(() => {getAdminListData()setAdminname('')setPassword('')setRole(1)setCheckedKeys(['0-0'])setOpen(false)})}}>添加</Button></Space></Drawer><Modal title="编辑管理员" open={isModalOpen} footer={ null }  onCancel={() => {setIsModalOpen(false)setAdminname('')setRole(1)setCheckedKeys(['0-0'])}}><Space direction='vertical' style={{ width: '100%'}}><Input readOnly value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ (value) => {setRole(value)}}value = { role }options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/><TreecheckabletreeData={getTreeData(menus)}onCheck={(checkedKeysValue: any) => {console.log(checkedKeysValue)setCheckedKeys(checkedKeysValue)}}checkedKeys={checkedKeys}/><Button type='primary' onClick={() => {const data = { adminname, role, checkedKeys }console.log(data)updateAdmin(data).then(() => {getAdminListData()setAdminname('')setRole(1)setCheckedKeys(['0-0'])setIsModalOpen(false)})}}>更新</Button></Space></Modal></Space>)
}export default Com

20 系统首页数据统计

// src/api/home.ts
import request from '@/utils/request'export function getUserTotalNum () {return request({url: '/statistic/user'})
}export function getShopTotalNum () {return request({url: '/statistic/product'})
}
// src/views/home/Index.tsximport { getShopTotalNum, getUserTotalNum } from '@/api/home';
import { Col, Row, Statistic } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import CountUp from 'react-countup';
interface IAppProps {
}
const formatter: any = (value: number) => <CountUp end={value} separator="," />;
const Com: FC<IAppProps> = (props) => {const [usersLen, setUsersLen] = useState(0)const [prosLen, setProsLen] = useState(0)useEffect(() => {getUserTotalNum().then(res => setUsersLen(res.data.data))getShopTotalNum().then(res => setProsLen(res.data.data))}, [])return (<div><Row gutter={16}><Col span={6}><Statistic style = {{ backgroundColor: '#efefef', padding: "10px 20px"}} title="用户总数量" valueStyle={{ color: '#3f8600' }} value={usersLen} formatter={formatter} /></Col><Col span={6}><Statistic style = {{ backgroundColor: '#efefef', padding: "10px 20px"}} title="产品总数量" valueStyle={{ color: '#cf1322' }} value={prosLen} formatter={formatter} /></Col></Row></div>)
}export default Com

21 左侧菜单栏的权限

21.1 思路

  • 当用户登录的时候,可以获取到该用户的 checkedKeys 数据
  • 使用这个数据从 router/menu.tsx中提取匹配的数据,
  • 生成左侧菜单栏组件(目前是直接渲染router/menu.tsx

21.2 算法过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agoi8npW-1678063667816)(assets/image-20221027151803006.png)]

从一个数组['0-0', '0-1-0', '0-2-0-0', '0-2-0-1', '0-3-2']触发,筛选 router/menu.tsx,获取到满足条件的数据

21.3 算法实现

算法1:

  • 从[‘0-0’, ‘0-1-0’, ‘0-2-0-0’, ‘0-2-0-1’, ‘0-3-2’] 到
  • [‘0-0’, ‘0-1’, ‘0-1-0’, ‘0-2’, ‘0-2-0’, ‘0-2-0-0’, ‘0-2-0-1’, ‘0-3’, ‘0-3-2’]

// ['0-0', '0-1-0', '0-2-0-0', '0-2-0-1', '0-3-2'] 
// ['0-0', '0-1', '0-1-0',  '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
let arr = ['0-0','0-1-0','0-2-0-0','0-2-0-1','0-3-2', '0-4-0-0-2', '0-5-1-2-0-1']
// 0-0
// 0-1 0-1-0
// 0-2 0-2-0 0-2-0-0
// 0-2 0-2-0 0-2-0-1
// 0-3 0-3-2
// 0-4 0-4-0 0-4-0-0  0-4-0-0-2
// 0-5 0-5-1 0-5-1-2  0-5-1-2-0  0-5-1-2-0-1// let brr = []
// for(let i = 0; i < arr.length; i++){
//   for(let j = 0; j < arr[i].length; j += 2){
//     brr.push(arr[i].substring(0, j + 3))
//   }
// }// console.log(new Set(brr)); 
let brr = new Set()
for(let i = 0; i < arr.length; i++){for(let j = 0; j < arr[i].length; j += 2){brr.add(arr[i].substring(0, j + 3))}
}console.log(brr); 

算法2:

[‘0-0’, ‘0-1’, ‘0-1-0’, ‘0-2’, ‘0-2-0’, ‘0-2-0-0’, ‘0-2-0-1’, ‘0-3’, ‘0-3-2’] 提取数据

import { IMyMenuItem } from "./menu"// src/router/utils.tsx
export function getCheckedKeysArr (arr: string[]) {const brr: Set<string> = new Set()for(let i = 0; i < arr.length; i++){for(let j = 0; j < arr[i].length; j += 2){brr.add(arr[i].substring(0, j + 3))}}return [...brr] // 修改tsconfig.json中 "target": "es6",
}
// menus 原始数据  
// checkedKeys转换后的数据 ['0-0', '0-1', '0-1-0',  '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
export function getPermissionMenu (menus: IMyMenuItem[], checkedKeys: string[]) {let arr: IMyMenuItem[] = []// 处理第一级数据checkedKeys.forEach(value => {menus.forEach(item => {if (item.keyid === value) { // 这项数据又arr.push({...item})}})})// 处理子数据arr.forEach(item => {if (item.children) {let newArr = getPermissionMenu(item.children, checkedKeys)item.children = newArr}})return arr
}

此时提示 ts配置中target 需要更改为 ‘es2015’

// tsconfig.json
{"compilerOptions": {"target": "es2015",...}
}

21.4 生成动态的左侧菜单项

状态管理器拿用户名,使用接口获取 权限 数据,整合权限数据,提取菜单数据

admin账户 渲染 原始的 menus 数据

// src/layout/components/SideBar.tsx
import React, { useEffect, 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';
import { getAdminDetail } from '@/api/admin';
import { getCheckedKeysArr, getPermissionMenu } from '@/router/utils';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]) // 点击时需要告诉程序哪一项被选中}useEffect(() => {setSelectedKeys([pathname])setOpenKeys(['/' + pathname.split('/')[1] ])}, [pathname])const adminname = useAppSelector(state => state.admin.adminname)const [showMenu, setShowMenu] = useState<any>([])useEffect(() => {getAdminDetail({ adminname }).then(res => {// console.log(res.data.data)const oldCheckedKeys = res.data.data[0].checkedKeysconst checkedKeysArr = getCheckedKeysArr(oldCheckedKeys)const newMenus = adminname === 'admin' ? menus : getPermissionMenu(menus, checkedKeysArr)setShowMenu(newMenus)})}, [adminname])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={ showMenu }openKeys={openKeys}onOpenChange={onOpenChange}onClick={changeUrl}/></Sider>);
};export default App;

有的公司在登录之后,会直接返回类似router/menus.tsx的数据

22、页面权限

也称之为路由权限

一个有权限访问页面A的人,把整个链接地址发给了没有权限访问的另外一个人

根据数据库中存储的字段,提取当前用户需要的 menus 的数据

如果用户访问的当前路由在 总路由中但是不在当前用户的路由中,显示无权限页面,否则显示404页面

核心思想:

  • 当前路由在不在权限路由 – 生成路由时使用 当前权限路由 (getPermissionMenu)

  • 当前的路由在不在所有的路由

import { IMyMenuItem } from "./menu"// src/router/utils.tsx
export function getCheckedKeysArr (arr: string[]) {// console.log('arr', arr)const brr: Set<string> = new Set()for(let i = 0; i < arr.length; i++){for(let j = 0; j < arr[i].length; j += 2){brr.add(arr[i].substring(0, j + 3))}}return [...brr] // 修改tsconfig.json中 "target": "es6",
}
// menus 原始数据  
// checkedKeys转换后的数据 ['0-0', '0-1', '0-1-0',  '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
export function getPermissionMenu (menus: IMyMenuItem[], checkedKeys: string[]) {let arr: IMyMenuItem[] = []// 处理第一级数据checkedKeys.forEach(value => {menus.forEach(item => {if (item.keyid === value) { // 这项数据又arr.push({...item})}})})// 处理子数据arr.forEach(item => {if (item.children) {let newArr = getPermissionMenu(item.children, checkedKeys)item.children = newArr}})return arr
}// 判断当前请求的地址是不是在路由系统中
export function isContainMenus (menus: IMyMenuItem[], pathname: string) { // ++++++++++let bool = menus.some(item => {if (item.children) {if (item.key === pathname) {return true} else {return item.children.some(it => it!.key === pathname)}} else {return item.key === pathname}})return bool
}
// src/layout/components/AppMain.tsx
import React, { useEffect, useState } from 'react';import { Layout, theme } from 'antd';
import { Routes, Route, Navigate, useLocation } 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'
import { useAppSelector } from '@/store/hooks';
import { getAdminDetail } from '@/api/admin';
import { getCheckedKeysArr, getPermissionMenu, isContainMenus } from '@/router/utils';const { Content } = Layout;const App: React.FC = () => {const {token: { colorBgContainer },} = theme.useToken();const adminname = useAppSelector(state => state.admin.adminname)const [showMenu, setShowMenu] = useState<any>([])useEffect(() => {getAdminDetail({ adminname }).then(res => {// console.log(res.data.data)const oldCheckedKeys = res.data.data[0].checkedKeysconst checkedKeysArr = getCheckedKeysArr(oldCheckedKeys)const newMenus = adminname === 'admin' ? menus : getPermissionMenu(menus, checkedKeysArr)setShowMenu(newMenus)})}, [adminname])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 } />}})}const { pathname } = useLocation()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) } */}{ renderRoute(showMenu) }<Route path="*" element = { isContainMenus(menus, pathname) ? <div>无权限</div> : <Page404 /> } /></Routes></Content>);
};export default App;

23、按钮权限

超级管理员才可以批量删除

// src/views/account/Admin.tsximport { addAdmin, deleteAdmin, getAdminDetail, getAdminList, updateAdmin } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';
import { useAppSelector } from '@/store/hooks';interface IAppProps {
}
interface IAdmin {adminid: stringadminname: stringpassword: stringrole: numbercheckedKeys: any[]
}const getTreeData = (menus: any[] ) => {const arr: DataNode[] = []menus.forEach(item => {let obj: DataNode = {key: '',title: ''}if (item.children) {obj = {key: item.keyid,title: item.label,children: getTreeData(item.children)}} else {obj = {key: item.keyid,title: item.label}}arr.push(obj)})return arr
}
const Com: FC<IAppProps> = (props) => {const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);const onSelectChange = (newSelectedRowKeys: React.Key[]) => {console.log('selectedRowKeys changed: ', newSelectedRowKeys);setSelectedRowKeys(newSelectedRowKeys);};const rowSelection = {selectedRowKeys,onChange: onSelectChange,};const flag = useMemo(() => {return selectedRowKeys.length > 0}, [selectedRowKeys])const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const columns = [{title: '序号',render (text: any, record: IAdmin, index: number) {return <> { (current - 1) * pageSize + index + 1 }</>}},{title: '管理员账户',dataIndex: 'adminname'},{title: '管理员角色',dataIndex: 'role',render (text: number) { // 自定义列信息return (<>{ text === 2 ? <Tag color="#f50">超级管理员</Tag> :<Tag color="#2db7f5">普通管理员</Tag>}</>)}},{title: '操作',render (text: any, record: IAdmin) { return (<Space><Button type="dashed" onClick={ () => {setIsModalOpen(true)setAdminname(record.adminname)setRole(record.role)setCheckedKeys(record.checkedKeys)}} shape="circle" icon={<EditOutlined />} /><Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteAdmin({ adminid: record.adminid }).then(() => {getAdminListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm></Space>)}}]const [adminList, setAdminList] = useState([])const getAdminListData = () => {getAdminList().then(res => {console.log(res.data)setAdminList(res.data.data)})}useEffect(() => {getAdminListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const deleteMany = () => {if (deleteRole < 2) {message.error('暂无权限');} else {// promise.allconst arr: any = []selectedRowKeys.forEach(item => {arr.push(deleteAdmin({ adminid: String(item) }))})Promise.all(arr).then(() => {getAdminListData()setSelectedRowKeys([])})}}const [open, setOpen] = useState<boolean>(false)const [checkedKeys, setCheckedKeys] = useState(['0-0'])const [adminname, setAdminname] = useState('')const [password, setPassword] = useState('')const [role, setRole] = useState(1)const [isModalOpen, setIsModalOpen] = useState(false)const name = useAppSelector(state => state.admin.adminname)const [deleteRole, setDeleteRole] = useState(1)useEffect(() => {getAdminDetail({ adminname: name }).then(res => {setDeleteRole(res.data.data[0].role)})})return (<Space direction='vertical' style = {{ width: '100%' }}><Space><Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>{ flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }</Space><Table dataSource={ adminList }columns = { columns }rowKey = "adminid"scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: adminList.length,showTotal: (total) => `共有 ${total} 条数据`} }rowSelection={rowSelection}/><Drawer title="添加管理员" placement="right" onClose={ () => { setAdminname('')setPassword('')setRole(1)setCheckedKeys(['0-0'])setOpen(false)} } open={open}><Space direction='vertical' style={{ width: '100%'}}><Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" /><Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ (value) => {setRole(value)}}value = { role }options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/><TreecheckabletreeData={getTreeData(menus)}onCheck={(checkedKeysValue: any) => {console.log(checkedKeysValue)setCheckedKeys(checkedKeysValue)}}checkedKeys={checkedKeys}/><Button type='primary' onClick={() => {const data = { adminname, password, role, checkedKeys }console.log(data)addAdmin(data).then(() => {getAdminListData()setAdminname('')setPassword('')setRole(1)setCheckedKeys(['0-0'])setOpen(false)})}}>添加</Button></Space></Drawer><Modal title="编辑管理员" open={isModalOpen} footer={ null }  onCancel={() => {setIsModalOpen(false)setAdminname('')setRole(1)setCheckedKeys(['0-0'])}}><Space direction='vertical' style={{ width: '100%'}}><Input readOnly value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" /><Selectstyle={{ width: '100%'}}defaultValue={ 1 }onChange={ (value) => {setRole(value)}}value = { role }options={[{ value: 1, label: '普通管理员' },{ value: 2, label: '超级管理员' }]}/><TreecheckabletreeData={getTreeData(menus)}onCheck={(checkedKeysValue: any) => {console.log(checkedKeysValue)setCheckedKeys(checkedKeysValue)}}checkedKeys={checkedKeys}/><Button type='primary' onClick={() => {const data = { adminname, role, checkedKeys }console.log(data)updateAdmin(data).then(() => {getAdminListData()setAdminname('')setRole(1)setCheckedKeys(['0-0'])setIsModalOpen(false)})}}>更新</Button></Space></Modal></Space>)
}export default Com

24、轮播图管理

24.1 封装接口

// src/api/banner.ts
import request from '@/utils/request'
export interface IAddBannerParams {img: stringalt: stringlink: string
}
export function addBanner (params: IAddBannerParams) {return request({url: '/banner/add',method: 'POST',data: params})
}export function getBannerList () {return request({url: '/banner/list'})
}export function deleteBanner (params: { bannerid: string }) {return request({url: '/banner/delete',data: params})
}

24.2 轮播图页面渲染

// src/views/banner/List.tsximport { deleteBanner, getBannerList } from '@/api/banner';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Image, Popconfirm, Table } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';interface IAppProps {
}
const { Column } = Table;
const Com: FC<IAppProps> = (props) => {const [bannerList, setBannerList] = useState([])const getBannerListData = () => {getBannerList().then(res => setBannerList(res.data.data))}useEffect(() => {getBannerListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const navigate = useNavigate()return (<div><Button type='primary' onClick={ () => navigate('/banner/add') }>添加轮播图</Button><Table dataSource={ bannerList } rowKey = "bannerid" scroll={ { y: height - 330 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: bannerList.length,showTotal: (total) => `共有 ${total} 条数据`} }><Column title="序号" render = {(text, record, index) => {return <span>{ (current - 1) * pageSize + index + 1 }</span>}} /><Column title="图片" dataIndex="img" render = {(text) => {return <Image src = { text } style={{ height: 60, width: 100 }}></Image>}} /><Column title="提示" dataIndex="alt" /><Column title="链接" dataIndex="link" /><Column title="操作" dataIndex="img" render = {(text, record: any, index) => {return <Popconfirmtitle="确定删除吗"onConfirm={ () => {deleteBanner({ bannerid: record.bannerid }).then(() => {getBannerListData()})}}onCancel={() => {}}okText="删除"cancelText="取消"><Button danger shape="circle" icon={<DeleteOutlined />} /></Popconfirm>}} /></Table></div>)
}export default Com

23.3 添加轮播图

// src/views/banner/Add.tsximport { addBanner } from '@/api/banner';
import { Input, Space, Image, Button } from 'antd';
import React, { FC, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';interface IAppProps {
}const Com: FC<IAppProps> = (props) => {const navigate = useNavigate()const [link, setLink] = useState('')const [alt, setAlt] = useState('')const [img, setImg] = useState<any>('')const file = useRef<any>()const onChange = () => {const myFile = file.current.input.files[0]console.log(myFile)const reader = new FileReader()reader.readAsDataURL(myFile) // base64地址reader.onload = function () {setImg(this.result)}}const flag = useMemo(() => {return alt === '' || link === '' || img === ''}, [alt, img, link])const onAdd = () => {addBanner({alt, link, img}).then(() => {navigate(-1)})}return (<Space direction='vertical' style={{ width: 300 }}><Input placeholder='link' value = {link} onChange = { e => setLink(e.target.value)}/><Input placeholder='alt' value = {alt} onChange = { e => setAlt(e.target.value)}/><Input type="file" ref = { file } onChange = { onChange }/><Input placeholder='图片地址' value = {img} onChange = { e => setImg(e.target.value)}/><Image src={img} /><Button type='primary' disabled = { flag } onClick={ onAdd }>添加</Button></Space>)
}export default Com

25.产品管理

25.1 封装接口

// src/api/pro.ts
import request from '@/utils/request'export function getProList () {return request({url: '/pro/list'})
}export function getSearchList (params?: { category?: string, search?: string}) {return request({url: '/pro/searchPro',method: 'POST',data: params})
}export function getCategoryList () {return request({url: '/pro/getCategory'})
}

25.2 产品列表

// src/views/pro/List.tsximport { getProList } from '@/api/pro';
import { DeleteOutlined } from '@ant-design/icons';
import { Table, Image, Popconfirm, Button } from 'antd';
import React, { FC, useEffect, useState } from 'react';interface IAppProps {
}
const { Column } = Table;
const Com: FC<IAppProps> = (props) => {const [proList, setProList] = useState([])const getProListData = () => {getProList().then(res => setProList(res.data.data))}useEffect(() => {getProListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}return (<Table dataSource={ proList } rowKey = "proid" scroll={ { y: height - 300 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: proList.length,showTotal: (total: number) => `共有 ${total} 条数据`} }><Column title="序号" render = {(text, record, index) => {return <span>{ (current - 1) * pageSize + index + 1 }</span>}} /><Column title="图片" dataIndex="img1" render = {(text) => {return <Image src = { text } style={{ height: 60, width: 100 }}></Image>}} /><Column title="产品名称" dataIndex="proname" /><Column title="价格" dataIndex="originprice" /><Column title="操作" dataIndex="img" render = {(text, record: any, index) => {return <Button danger shape="circle" icon={<DeleteOutlined />} />}} /></Table>)
}export default Com

25.3 筛选列表

// src/views/pro/Search.tsximport { getCategoryList, getProList, getSearchList } from '@/api/pro';
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { Table, Image, Button, Space, Select, Input } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';interface IAppProps {
}
const { Column } = Table;
const Com: FC<IAppProps> = (props) => {const [proList, setProList] = useState([])const getProListData = () => {getProList().then(res => setProList(res.data.data))}useEffect(() => {getProListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const [categoryList, setCategoryList] = useState([])const [category, setCategory] = useState('')const [search, setSearch] = useState('')// const arr = [{ value: '', label: '全部' }]useEffect(() => {getCategoryList().then(res => {console.log(res.data.data)setCategoryList(res.data.data)})}, [])const arr = useMemo(() => {const brr: any = [{ value: '', label: '全部' }]categoryList.forEach((item: any) => {brr.push({value: item,label: item})})return brr}, [categoryList])return (<Space direction='vertical' style={{ width: '100%'}}><Space><Selectstyle={{ width: 120 }}defaultValue=''onChange={ (value) => {setCategory(value)}}value = { category }options={ arr }/><Input  placeholder='输入需要搜索的关键词' value = { search } onChange = { event=> setSearch(event.target.value)} /><Button onClick={() => {getSearchList({ category, search }).then(res => {setProList(res.data.data)})}} type="primary" shape="circle" icon={<SearchOutlined />} /></Space><Table dataSource={ proList } rowKey = "proid" scroll={ { y: height - 300 } }pagination = { {// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: proList.length,showTotal: (total: number) => `共有 ${total} 条数据`} }><Column title="序号" render = {(text, record, index) => {return <span>{ (current - 1) * pageSize + index + 1 }</span>}} /><Column title="图片" dataIndex="img1" render = {(text) => {return <Image src = { text } style={{ height: 60, width: 100 }}></Image>}} /><Column title="产品名称" dataIndex="proname" /><Column title="价格" dataIndex="originprice" /><Column title="操作" dataIndex="img" render = {(text, record: any, index) => {return <Button danger shape="circle" icon={<DeleteOutlined />} />}} /></Table></Space>)
}export default Com

26.数据可视化

方案:

echarts: https://echarts.apache.org/zh/index.html

ts中使用 echarts : https://echarts.apache.org/handbook/zh/basics/import#%E5%9C%A8-typescript-%E4%B8%AD%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5

highCharts:https://www.hcharts.cn/

Antv: https://antv.gitee.io/zh/

g2:https://antv-g2.gitee.io/zh/g2plot:https://g2plot.antv.vision/zh/react中使用g2:https://charts.ant.design/zh-CN

D3:视频地址:链接: https://pan.baidu.com/s/1SVS36TjtcR27Rqj_HURDZA 密码: p9ur

1.echarts

添加页面以及配置路由

// src/views/data/Echarts.tsx
const Com = () => {return (<>echarts</>)}
export default Com
// src/views/data/HighCharts.tsx
const Com = () => {return (<>HighCharts</>)}
export default Com
// src/views/data/Antv.tsx
const Com = () => {return (<>Antv</>)}
export default Com
// 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'import Set from '@/views/set/Index'import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'type MenuItem = Required<MenuProps>['items'][number];// 扩展固有的类型
export type IMyMenuItem = MenuItem & {path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性children?: IMyMenuItem[];redirect?: string; // 多级菜单的默认地址element?: ReactNode;hidden?: number;keyid: string 
}const menus: IMyMenuItem[] = [{path: '/',label: '系统首页',key: '/',icon: <HomeOutlined />,element: <Home />,keyid: '0-0'},{path: '/banner',label: '轮播图管理',key: '/banner',redirect: '/banner/list',icon: <HomeOutlined />,keyid: '0-1',children: [{path: '/banner/list',key: '/banner/list',label: '轮播图列表',icon: <HomeOutlined />,element: <BannerList />,keyid: '0-1-0'},{path: '/banner/add',key: '/banner/add',label: '添加轮播图',icon: <HomeOutlined />,element: <BannerAdd />,hidden: 1,keyid: '0-1-1'}]},{path: '/pro',label: '产品管理',key: '/pro',redirect: '/pro/list',icon: <HomeOutlined />,keyid: '0-2',children: [{path: '/pro/list',key: '/pro/list',label: '产品列表',icon: <HomeOutlined />,element: <ProList />,keyid: '0-2-0'},{path: '/pro/search',key: '/pro/search',label: '筛选列表',icon: <HomeOutlined />,element: <SearchList />,keyid: '0-2-1'}]},{path: '/account',label: '账户管理',key: '/account',redirect: '/account/user',icon: <HomeOutlined />,keyid: '0-3',children: [{path: '/account/user',key: '/account/user',label: '用户列表',icon: <HomeOutlined />,element: <UserList />,keyid: '0-3-0'},{path: '/account/admin',key: '/account/admin',label: '管理员列表',icon: <HomeOutlined />,element: <AdminList />,keyid: '0-3-1'}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />,hidden: 1,keyid: '0-4'},{path: '/data',label: '数据可视化',key: '/data',redirect: '/data/echarts',icon: <HomeOutlined />,keyid: '0-5',children: [{path: '/data/echarts',key: '/data/echarts',label: 'echarts',icon: <HomeOutlined />,element: <Echarts />,keyid: '0-5-0'},{path: '/data/HighCharts',key: '/data/HighCharts',label: 'HighCharts',icon: <HomeOutlined />,element: <HighCharts />,keyid: '0-5-1'},{path: '/data/antv',key: '/data/antv',label: 'antv',icon: <HomeOutlined />,element: <Antv />,keyid: '0-5-2'}]},
]export default menus
cnpm install echarts --save
// src/api/data.ts
import request from './../utils/request'export function getData () {return request({url: '/data/simpleData'})
}

处理数据

自适应

// src/views/data/Echarts.tsx
import { getServerData } from "@/api/data";
import { Button, Col, Row } from "antd"
import * as echarts from 'echarts';
import { useEffect } from "react";
const Com = () => {useEffect(() => {var BarChart = echarts.init(document.getElementById('barCharts') as HTMLElement);BarChart.setOption({title: {text: 'ECharts 入门示例'},tooltip: {},xAxis: {data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']},yAxis: {},series: [{name: '销量',type: 'bar',data: [5, 20, 36, 10, 10, 20]}]})}, [])useEffect(() => {var BarChart = echarts.init(document.getElementById('lineCharts') as HTMLElement);BarChart.setOption({title: {text: 'ECharts 入门示例'},tooltip: {},xAxis: {data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']},yAxis: {},series: [{name: '销量',type: 'line',data: [5, 20, 36, 10, 10, 20]}]})}, [])useEffect(() => {var BarChart = echarts.init(document.getElementById('randomCharts') as HTMLElement);BarChart.setOption({title: {text: 'Stacked Line'},tooltip: {trigger: 'axis'},legend: {data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},toolbox: {feature: {saveAsImage: {}}},xAxis: {type: 'category',boundaryGap: false,data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']},yAxis: {type: 'value'},series: [{name: 'Email',type: 'line',stack: 'Total',data: [120, 132, 101, 134, 90, 230, 210]},{name: 'Union Ads',type: 'line',stack: 'Total',data: [220, 182, 191, 234, 290, 330, 310]},{name: 'Video Ads',type: 'line',stack: 'Total',data: [150, 232, 201, 154, 190, 330, 410]},{name: 'Direct',type: 'line',stack: 'Total',data: [320, 332, 301, 334, 390, 330, 320]},{name: 'Search Engine',type: 'line',stack: 'Total',data: [820, 932, 901, 934, 1290, 1330, 1320]}]})}, [])useEffect(() => {getServerData().then(res => {const arr: any =[]const brr: any = []res.data.data.forEach((item: { x: any; val: any; }) => {arr.push(item.x)brr.push(item.val)})var BarChart = echarts.init(document.getElementById('serverCharts') as HTMLElement);BarChart.setOption({title: {text: 'ECharts 入门示例'},tooltip: {},xAxis: {data: arr},yAxis: {},series: [{name: '销量',type: 'bar',data: brr}]})})}, [])return (<>echarts<Row gutter={15}><Col span={12}><p>柱状图</p><div id='barCharts' style={{ width: '100%', height: 300 }}></div></Col><Col span={12}><p>折线图</p><div id='lineCharts' style={{ width: '100%', height: 300 }}></div></Col></Row><Row gutter={15}><Col span={12}><p>随意图形</p><div id='randomCharts' style={{ width: '100%', height: 300 }}></div></Col><Col span={12}><p>服务器数据</p><div id='serverCharts' style={{ width: '100%', height: 300 }}></div></Col></Row></>
)}
export default Com

2.Highcharts

vue: https://www.highcharts.com.cn/docs/highcharts-vue

react: https://www.highcharts.com.cn/docs/highcharts-react

cnpm install highcharts highcharts-react-official -S
// src/views/data/HighCharts.tsx
import React, { FC, useEffect, useState } from 'react';
import * as Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';console.log(HighchartsReact)
interface IHighChartsProps {};const HighCharts:FC<IHighChartsProps> = (props: HighchartsReact.Props) => {const [option, setOption] = useState<any>({title: {text: '2010 ~ 2016 年太阳能行业就业人员发展情况'},subtitle: {text: '数据来源:thesolarfoundation.com'},yAxis: {title: {text: '就业人数'}},legend: {layout: 'vertical',align: 'right',verticalAlign: 'middle'},plotOptions: {series: {label: {connectorAllowed: false},pointStart: 2010}},series: [{name: '安装,实施人员',data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]}, {name: '工人',data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434]}, {name: '销售',data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387]}, {name: '项目开发',data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227]}, {name: '其他',data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111]}],responsive: {rules: [{condition: {maxWidth: 500},chartOptions: {legend: {layout: 'horizontal',align: 'center',verticalAlign: 'bottom'}}}]}
})useEffect(() => {window.addEventListener('resize',() => { setOption({title: {text: '2010 ~ 2016 年太阳能行业就业人员发展情况'},subtitle: {text: '数据来源:thesolarfoundation.com'},yAxis: {title: {text: '就业人数'}},legend: {layout: 'vertical',align: 'right',verticalAlign: 'middle'},plotOptions: {series: {label: {connectorAllowed: false},pointStart: 2010}},series: [{name: '安装,实施人员',data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]}, {name: '工人',data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434]}, {name: '销售',data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387]}, {name: '项目开发',data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227]}, {name: '其他',data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111]}],responsive: {rules: [{condition: {maxWidth: 500},chartOptions: {legend: {layout: 'horizontal',align: 'center',verticalAlign: 'bottom'}}}]}})})}, [])return (<><h1>HighCharts</h1><HighchartsReacthighcharts={Highcharts}options={option}{...props}/></>)
};export default HighCharts;

3.antv – g2

https://antv-g2.gitee.io/zh

cnpm i @antv/g2 @antv/data-set -S
// src/views/data/Antv.tsx
import React, { FC, useEffect } from 'react';
import { Chart } from '@antv/g2';
interface IAntvProps {};const Antv: FC<IAntvProps> = () => {useEffect(() => {const data = [{ year: '1951 年', sales: 38 },{ year: '1952 年', sales: 52 },{ year: '1956 年', sales: 61 },{ year: '1957 年', sales: 145 },{ year: '1958 年', sales: 48 },{ year: '1959 年', sales: 38 },{ year: '1960 年', sales: 38 },{ year: '1962 年', sales: 38 },];const chart = new Chart({container: 'antv',autoFit: true,height: 500,});chart.data(data);chart.scale('sales', {nice: true,});chart.tooltip({showMarkers: false});chart.interaction('active-region');chart.interval().position('year*sales');chart.render();}, [])return (<><h1>Antv</h1><div id="antv" style={{ width: 800, height: 600, backgroundColor: '#efefef' }}></div></>)
};export default Antv;

27.编辑器

// 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'import Set from '@/views/set/Index'import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'type MenuItem = Required<MenuProps>['items'][number];// 扩展固有的类型
export type IMyMenuItem = MenuItem & {path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性children?: IMyMenuItem[];redirect?: string; // 多级菜单的默认地址element?: ReactNode;hidden?: number;keyid: string 
}const menus: IMyMenuItem[] = [{path: '/',label: '系统首页',key: '/',icon: <HomeOutlined />,element: <Home />,keyid: '0-0'},{path: '/banner',label: '轮播图管理',key: '/banner',redirect: '/banner/list',icon: <HomeOutlined />,keyid: '0-1',children: [{path: '/banner/list',key: '/banner/list',label: '轮播图列表',icon: <HomeOutlined />,element: <BannerList />,keyid: '0-1-0'},{path: '/banner/add',key: '/banner/add',label: '添加轮播图',icon: <HomeOutlined />,element: <BannerAdd />,hidden: 1,keyid: '0-1-1'}]},{path: '/pro',label: '产品管理',key: '/pro',redirect: '/pro/list',icon: <HomeOutlined />,keyid: '0-2',children: [{path: '/pro/list',key: '/pro/list',label: '产品列表',icon: <HomeOutlined />,element: <ProList />,keyid: '0-2-0'},{path: '/pro/search',key: '/pro/search',label: '筛选列表',icon: <HomeOutlined />,element: <SearchList />,keyid: '0-2-1'}]},{path: '/account',label: '账户管理',key: '/account',redirect: '/account/user',icon: <HomeOutlined />,keyid: '0-3',children: [{path: '/account/user',key: '/account/user',label: '用户列表',icon: <HomeOutlined />,element: <UserList />,keyid: '0-3-0'},{path: '/account/admin',key: '/account/admin',label: '管理员列表',icon: <HomeOutlined />,element: <AdminList />,keyid: '0-3-1'}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />,hidden: 1,keyid: '0-4'},{path: '/data',label: '数据可视化',key: '/data',redirect: '/data/echarts',icon: <HomeOutlined />,keyid: '0-5',children: [{path: '/data/echarts',key: '/data/echarts',label: 'echarts',icon: <HomeOutlined />,element: <Echarts />,keyid: '0-5-0'},{path: '/data/HighCharts',key: '/data/HighCharts',label: 'HighCharts',icon: <HomeOutlined />,element: <HighCharts />,keyid: '0-5-1'},{path: '/data/antv',key: '/data/antv',label: 'antv',icon: <HomeOutlined />,element: <Antv />,keyid: '0-5-2'}]},{path: '/braft',label: '父文本编辑器',key: '/braft',icon: <HomeOutlined />,element: <Braft />,keyid: '0-6'},{path: '/md',label: 'markDown编辑器',key: '/md',icon: <HomeOutlined />,element: <Md />,keyid: '0-7'},
]export default menus

1.富文本编辑器

react版本: https://braft.margox.cn/demos/basic

cnpm i braft-editor -S
// src/views/edit/Braft.tsx
import React, { useState } from 'react';
import 'braft-editor/dist/index.css'
import BraftEditor from 'braft-editor'
type Props = {}const  Com = (props: Props) => {const [editorState, setEditorState] = useState('')const [html, setHtml] = useState('')const handleChange = (editorState: any) => {console.log(editorState.toHTML())setEditorState(editorState)setHtml(editorState.toHTML())}return (<><BraftEditorvalue={editorState}onChange={handleChange}/><div dangerouslySetInnerHTML={ { __html: html } }></div></>);
}export default Com

2.markDown编辑器

阅读器:https://www.npmjs.com/package/react-markdown

编辑器:https://www.npmjs.com/package/react-markdown-editor-lite

cnpm i react-markdown react-markdown-editor-lite -S
// src/views/edit/Md.tsx
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown' // 阅读器
import MdEditor from 'react-markdown-editor-lite'; // 编辑器
// import style manually
import 'react-markdown-editor-lite/lib/index.css'; // 样式
type Props = {}const  Com = (props: Props) => {const [content, setContent] = useState('')return (<><h1>Markdown展示</h1><MdEditor style={{ height: '500px' }} renderHTML={text => {return <ReactMarkdown>{ text }</ReactMarkdown>}} onChange={ ( { html, text }: { html: any; text: any}) => {setContent(html)}} /><div dangerouslySetInnerHTML={ { __html: content } }></div></>);
}export default Com

28.导入以及导出

// src/views/excel/Import.tsx
import React from 'react';type ComProps = {}const Com = (props: ComProps) => (<><h1>导入</h1></>
);export default Com
// src/views/excel/Export.tsx
import React from 'react';type ComProps = {}const Com = (props: ComProps) => (<><h1>导出</h1></>
);export default Co
// 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'import Set from '@/views/set/Index'import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'import Import from '@/views/excel/Import'
import Export from '@/views/excel/Export'type MenuItem = Required<MenuProps>['items'][number];// 扩展固有的类型
export type IMyMenuItem = MenuItem & {path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性children?: IMyMenuItem[];redirect?: string; // 多级菜单的默认地址element?: ReactNode;hidden?: number;keyid: string 
}const menus: IMyMenuItem[] = [{path: '/',label: '系统首页',key: '/',icon: <HomeOutlined />,element: <Home />,keyid: '0-0'},{path: '/banner',label: '轮播图管理',key: '/banner',redirect: '/banner/list',icon: <HomeOutlined />,keyid: '0-1',children: [{path: '/banner/list',key: '/banner/list',label: '轮播图列表',icon: <HomeOutlined />,element: <BannerList />,keyid: '0-1-0'},{path: '/banner/add',key: '/banner/add',label: '添加轮播图',icon: <HomeOutlined />,element: <BannerAdd />,hidden: 1,keyid: '0-1-1'}]},{path: '/pro',label: '产品管理',key: '/pro',redirect: '/pro/list',icon: <HomeOutlined />,keyid: '0-2',children: [{path: '/pro/list',key: '/pro/list',label: '产品列表',icon: <HomeOutlined />,element: <ProList />,keyid: '0-2-0'},{path: '/pro/search',key: '/pro/search',label: '筛选列表',icon: <HomeOutlined />,element: <SearchList />,keyid: '0-2-1'}]},{path: '/account',label: '账户管理',key: '/account',redirect: '/account/user',icon: <HomeOutlined />,keyid: '0-3',children: [{path: '/account/user',key: '/account/user',label: '用户列表',icon: <HomeOutlined />,element: <UserList />,keyid: '0-3-0'},{path: '/account/admin',key: '/account/admin',label: '管理员列表',icon: <HomeOutlined />,element: <AdminList />,keyid: '0-3-1'}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />,hidden: 1,keyid: '0-4'},{path: '/data',label: '数据可视化',key: '/data',redirect: '/data/echarts',icon: <HomeOutlined />,keyid: '0-5',children: [{path: '/data/echarts',key: '/data/echarts',label: 'echarts',icon: <HomeOutlined />,element: <Echarts />,keyid: '0-5-0'},{path: '/data/HighCharts',key: '/data/HighCharts',label: 'HighCharts',icon: <HomeOutlined />,element: <HighCharts />,keyid: '0-5-1'},{path: '/data/antv',key: '/data/antv',label: 'antv',icon: <HomeOutlined />,element: <Antv />,keyid: '0-5-2'}]},{path: '/braft',label: '父文本编辑器',key: '/braft',icon: <HomeOutlined />,element: <Braft />,keyid: '0-6'},{path: '/md',label: 'markDown编辑器',key: '/md',icon: <HomeOutlined />,element: <Md />,keyid: '0-7'},{path: '/excel',label: '导入以及导出',key: '/excel',redirect: '/excel/export',icon: <HomeOutlined />,keyid: '0-8',children: [{path: '/excel/import',key: '/excel/import',label: '导入',icon: <HomeOutlined />,element: <Import />,keyid: '0-8-0'},{path: '/excel/export',key: '/excel/export',label: '导出',icon: <HomeOutlined />,element: <Export />,keyid: '0-8-1'}]},
]export default menus

1.导出

cnpm i js-export-excel -S

src/views/excel/test.d.ts

declare module 'js-export-excel'

本案例导出产品筛选列表的数据

// src/views/excel/Export.tsximport { getCategoryList, getProList, getSearchList } from '@/api/pro';
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { Table, Image, Button, Space, Select, Input } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import ExportJsonExcel from 'js-export-excel'interface IAppProps {
}
interface DataType {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const { Column } = Table;
const Com: FC<IAppProps> = (props) => {const [proList, setProList] = useState([])const getProListData = () => {getProList().then(res => setProList(res.data.data))}useEffect(() => {getProListData()}, [])const [height] = useState(document.body.offsetHeight) // 计算body的高度const [current, setCurrent] = useState(1)const [pageSize, setPageSize] = useState(10)const onChange = (page: number, pageSize: number) => {setCurrent(page)setPageSize(pageSize)}const [categoryList, setCategoryList] = useState([])const [category, setCategory] = useState('')const [search, setSearch] = useState('')// const arr = [{ value: '', label: '全部' }]useEffect(() => {getCategoryList().then(res => {console.log(res.data.data)setCategoryList(res.data.data)})}, [])const arr = useMemo(() => {const brr: any = [{ value: '', label: '全部' }]categoryList.forEach((item: any) => {brr.push({value: item,label: item})})return brr}, [categoryList])return (<Space direction='vertical' style={{ width: '100%' }}><Space><Selectstyle={{ width: 120 }}defaultValue=''onChange={(value) => {setCategory(value)}}value={category}options={arr}/><Input placeholder='输入需要搜索的关键词' value={search} onChange={event => setSearch(event.target.value)} /><Button onClick={() => {getSearchList({ category, search }).then(res => {setProList(res.data.data)})}} type="primary" shape="circle" icon={<SearchOutlined />} /><Button onClick={() => {let option: {fileName: stringdatas: {sheetData: DataType[],sheetName: string,sheetFilter: string[],sheetHeader: string[],columnWidths: number[]}[]};option = {fileName: "产品列表", // 导出的文件的名称datas: [{sheetData: proList, // 表格数据sheetName: "产品列表1", // excel表格中表格的名字sheetFilter: ["proname", "img1", "category"], // 需要导出的数据的字段sheetHeader: ["产品名称", "图片", "分类"], // 表头的值columnWidths: [20, 20],},{sheetData: proList, // 表格数据sheetName: "产品列表2", // excel表格中表格的名字sheetFilter: ["proname", "img1", "category", 'originprice'], // 需要导出的数据的字段sheetHeader: ["产品名称", "图片", "分类", '价格'], // 表头的值columnWidths: [20, 20],},]}var toExcel = new ExportJsonExcel(option); //newtoExcel.saveExcel(); //保存}} type="primary">导出数据</Button></Space><Table dataSource={proList} rowKey="proid" scroll={{ y: height - 300 }}pagination={{// position: ['bottomLeft', 'topRight']showQuickJumper: true,showSizeChanger: true,current,pageSize,onChange,total: proList.length,showTotal: (total: number) => `共有 ${total} 条数据`}}><Column title="序号" render={(text, record, index) => {return <span>{(current - 1) * pageSize + index + 1}</span>}} /><Column title="图片" dataIndex="img1" render={(text) => {return <Image src={text} style={{ height: 60, width: 100 }}></Image>}} /><Column title="产品名称" dataIndex="proname" /><Column title="价格" dataIndex="originprice" /><Column title="操作" dataIndex="img" render={(text, record: any, index) => {return <Button danger shape="circle" icon={<DeleteOutlined />} />}} /></Table></Space>)
}export default Com

以上方案为纯前端的导出,实际上还有其余的导出方法,比如通过接口实现,前端可以通过a的href属性实现

2.导入

数据在 src/views/excel/pro.xlsx

cnpm install xlsx
// src/views/excel/Import.tsx
import { Button, Table, Image, Switch } from 'antd';
import React, { useState } from 'react';
import * as XLSX from 'xlsx';
type ComProps = {}
interface DataType {banners: string[]brand: stringcategory: stringdesc: stringdiscount: numberimg1: stringimg2: stringimg3: stringimg4: stringisrecommend: numberissale: numberisseckill: numberoriginprice: numberproid: stringproname: stringsales: numberstock: number
}
const Com = (props: ComProps) => {const [proList, setProList] = useState([])const importExcel = () => { // 导入数据const file = (document.getElementById('fileRef') as HTMLInputElement).files![0]const reader = new FileReader()reader.readAsBinaryString(file!) // 转成 二进制格式reader.onload = function () {const workbook = XLSX.read(this.result, { type: 'binary' });const t = workbook.Sheets['list'] // 拿到表格数据// console.log(t)const r: any = XLSX.utils.sheet_to_json(t) // 转换成json格式// console.log(r)setProList(r)// 将r的数据上传至服务器}}return (<><h1>导入</h1><Button onClick={() => { // 触发文件选择器(document.getElementById('fileRef') as HTMLInputElement).click()}}>导入数据</Button><input type="file" hidden id = 'fileRef' onChange = { importExcel }/><Table dataSource={ proList }  rowKey = "proid" scroll={{ y: 600}}><Table.Columntitle="序号"render={(text, record, index) => { return <span>{ index + 1 }</span>}}></Table.Column><Table.Columntitle="产品分类"dataIndex="category"></Table.Column><Table.Columntitle="产品品牌"dataIndex="brand"></Table.Column><Table.Columntitle="产品名称"dataIndex="proname"></Table.Column><Table.Columntitle="图片"dataIndex="img1"render={ (text) => {return <Image src={text} width={80} height={80} /> }}></Table.Column><Table.Columntitle="产品价格"dataIndex="originprice"sorter={(a: DataType, b: DataType) => a.originprice - b.originprice}></Table.Column><Table.Columntitle="产品折扣"dataIndex="discount"sorter={(a: DataType, b: DataType) => a.discount - b.discount}></Table.Column><Table.Columntitle="产品销量"dataIndex="sales"sorter={(a: DataType, b: DataType) => a.sales - b.sales}></Table.Column><Table.Columntitle="产品库存"dataIndex="stock"sorter={(a: DataType, b: DataType) => a.stock - b.stock}></Table.Column><Table.Columntitle="是否上架"dataIndex="issale"render={ (text) => {return <Switch checked = { text === 1 } /> }}></Table.Column><Table.Columntitle="是否推荐"dataIndex="isrecommend"render={ (text) => {return <Switch checked = { text === 1 } /> }}></Table.Column><Table.Columntitle="是否秒杀"dataIndex="isseckill"render={ (text) => {return <Switch checked = { text === 1 } /> }}></Table.Column></Table></>)
};export default Com

如果在nodejs环境中,通过接口实现

const xlsx = require('node-xlsx').default;
// 导入excel表格的数据
router.get('/uploadPro', (req, res, next) => {const originData = xlsx.parse(`${__dirname}/pro.xlsx`);const firstData = originData[0].dataconst arr = []for (var i = 0; i < firstData.length; i++) { if (i !== 0) {arr.push({proid: 'pro_'+ uuid.v4(),category: firstData[i][0],brand: firstData[i][1],proname: firstData[i][2],banners: firstData[i][3],originprice: firstData[i][4],sales: firstData[i][5],stock: firstData[i][6],desc: firstData[i][7],issale: firstData[i][8],isrecommend: firstData[i][9],discount: firstData[i][10],isseckill: firstData[i][11],img1: firstData[i][12],img2: firstData[i][13],img3: firstData[i][14],img4: firstData[i][15],})}}// 拿到 arr 的数据,先清空 产品的表格数据,然后再插入mysql.delete(Product, {}, 1).then(() => { // 不要忘记写1,因为1 代表的是删除多条数据// 所有的数据已删除完成// 插入数据mysql.insert(Product, arr).then(() => {// 重定向到 商品的管理的页面路由res.send('导入数据成功') // 相当于浏览器自动跳转到了 /pro 的路由})})
})

29.地图

https://huiyan.baidu.com/github/react-bmapgl/#/%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8

https://lbsyun.baidu.com/

// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';interface IBaiduProps {};const Baidu:FC<IBaiduProps> = () => {return (<><h1>百度地图</h1></>)
};export default Baidu;
// src/views/map/Gaode.tsx
import React, { FC, useEffect } from 'react';interface IBaiduProps {};const Baidu:FC<IBaiduProps> = () => {return (<><h1>高德地图</h1></>)
};export default Baidu;
// 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'import Set from '@/views/set/Index'import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'import Import from '@/views/excel/Import'
import Export from '@/views/excel/Export'import Baidu from '@/views/map/Baidu'
import Gaode from '@/views/map/Gaode'type MenuItem = Required<MenuProps>['items'][number];// 扩展固有的类型
export type IMyMenuItem = MenuItem & {path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性children?: IMyMenuItem[];redirect?: string; // 多级菜单的默认地址element?: ReactNode;hidden?: number;keyid: string 
}const menus: IMyMenuItem[] = [{path: '/',label: '系统首页',key: '/',icon: <HomeOutlined />,element: <Home />,keyid: '0-0'},{path: '/banner',label: '轮播图管理',key: '/banner',redirect: '/banner/list',icon: <HomeOutlined />,keyid: '0-1',children: [{path: '/banner/list',key: '/banner/list',label: '轮播图列表',icon: <HomeOutlined />,element: <BannerList />,keyid: '0-1-0'},{path: '/banner/add',key: '/banner/add',label: '添加轮播图',icon: <HomeOutlined />,element: <BannerAdd />,hidden: 1,keyid: '0-1-1'}]},{path: '/pro',label: '产品管理',key: '/pro',redirect: '/pro/list',icon: <HomeOutlined />,keyid: '0-2',children: [{path: '/pro/list',key: '/pro/list',label: '产品列表',icon: <HomeOutlined />,element: <ProList />,keyid: '0-2-0'},{path: '/pro/search',key: '/pro/search',label: '筛选列表',icon: <HomeOutlined />,element: <SearchList />,keyid: '0-2-1'}]},{path: '/account',label: '账户管理',key: '/account',redirect: '/account/user',icon: <HomeOutlined />,keyid: '0-3',children: [{path: '/account/user',key: '/account/user',label: '用户列表',icon: <HomeOutlined />,element: <UserList />,keyid: '0-3-0'},{path: '/account/admin',key: '/account/admin',label: '管理员列表',icon: <HomeOutlined />,element: <AdminList />,keyid: '0-3-1'}]},{path: '/set',label: '设置',key: '/set',icon: <HomeOutlined />,element: <Set />,hidden: 1,keyid: '0-4'},{path: '/data',label: '数据可视化',key: '/data',redirect: '/data/echarts',icon: <HomeOutlined />,keyid: '0-5',children: [{path: '/data/echarts',key: '/data/echarts',label: 'echarts',icon: <HomeOutlined />,element: <Echarts />,keyid: '0-5-0'},{path: '/data/HighCharts',key: '/data/HighCharts',label: 'HighCharts',icon: <HomeOutlined />,element: <HighCharts />,keyid: '0-5-1'},{path: '/data/antv',key: '/data/antv',label: 'antv',icon: <HomeOutlined />,element: <Antv />,keyid: '0-5-2'}]},{path: '/braft',label: '父文本编辑器',key: '/braft',icon: <HomeOutlined />,element: <Braft />,keyid: '0-6'},{path: '/md',label: 'markDown编辑器',key: '/md',icon: <HomeOutlined />,element: <Md />,keyid: '0-7'},{path: '/excel',label: '导入以及导出',key: '/excel',redirect: '/excel/export',icon: <HomeOutlined />,keyid: '0-8',children: [{path: '/excel/import',key: '/excel/import',label: '导入',icon: <HomeOutlined />,element: <Import />,keyid: '0-8-0'},{path: '/excel/export',key: '/excel/export',label: '导出',icon: <HomeOutlined />,element: <Export />,keyid: '0-8-1'}]},{path: '/map',label: '地图',key: '/map',redirect: '/map/baidu',icon: <HomeOutlined />,keyid: '0-8',children: [{path: '/map/baidu',key: '/map/baidu',label: '百度地图',icon: <HomeOutlined />,element: <Baidu />,keyid: '0-9-0'},{path: '/map/gaode',key: '/map/gaode',label: '高德地图',icon: <HomeOutlined />,element: <Gaode />,keyid: '0-9-1'}]},
]export default menus
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><link rel="icon" href="%PUBLIC_URL%/favicon.ico" /><meta name="viewport" content="width=device-width, initial-scale=1" /><meta name="theme-color" content="#000000" /><metaname="description"content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /><!--manifest.json provides metadata used when your web app is installed on auser's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/--><link rel="manifest" href="%PUBLIC_URL%/manifest.json" /><!--Notice the use of %PUBLIC_URL% in the tags above.It will be replaced with the URL of the `public` folder during the build.Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" willwork correctly both with client-side routing and a non-root public URL.Learn how to configure a non-root public URL by running `npm run build`.--><title>React App</title><script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=17qecKvCwmMWGwzVqPQvG9GQkRSPZHc8"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><!--This HTML file is a template.If you open it directly in the browser, you will see an empty page.You can add webfonts, meta tags, or analytics to this file.The build step will place the bundled scripts into the <body> tag.To begin the development, run `npm start` or `yarn start`.To create a production bundle, use `npm run build` or `yarn build`.--></body>
</html>
// src/views/map/map.d.ts.
interface Window {BMapGL: any
}
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';interface IBaiduProps {};const Baidu:FC<IBaiduProps> = () => {useEffect(() => {var map = new window.BMapGL.Map("allmap");map.centerAndZoom(new window.BMapGL.Point(116.280190, 40.049191), 19);map.enableScrollWheelZoom(true);map.setHeading(64.5);map.setTilt(73);}, [])return (<><h1>百度地图</h1><div id="allmap" style={{width:' 100%',height: '500px'}}></div></>)
};export default Baidu;

30.项目打包发布

https://blog.csdn.net/daxunshuo/article/details/102976306?spm=1001.2014.3001.5501

$ cnpm run build
# 估计需要一点时间请耐心等待

打包完毕项目 build 文件夹即为 项目打包出来的文件,只需要把build文件夹上传至服务器,一般情况下,都需要修改build文件夹的名字

如果打开 build/index.html 文件,发现 css js 文件的引入使用的都是绝对路径

如果服务器只部署这一个项目,那么可以把 build文件夹的内容替换了 服务器的静态资源文件 — 绝对路径

如果服务器部署多个项目,那么可以把修改过后的 build的文件夹上传到 服务器的静态资源文件 — 项目路径

如何打包项目时使用相对路径

package.json

{"homepage": '.'
}

http://121.89.205.189:2207/

port 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’

import Set from ‘@/views/set/Index’

import Echarts from ‘@/views/data/Echarts’
import HighCharts from ‘@/views/data/HighCharts’
import Antv from ‘@/views/data/Antv’

import Braft from ‘@/views/edit/Braft’
import Md from ‘@/views/edit/Md’

import Import from ‘@/views/excel/Import’
import Export from ‘@/views/excel/Export’

import Baidu from ‘@/views/map/Baidu’
import Gaode from ‘@/views/map/Gaode’

type MenuItem = Required[‘items’][number];

// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}

const menus: IMyMenuItem[] = [
{
path: ‘/’,
label: ‘系统首页’,
key: ‘/’,
icon: ,
element: ,
keyid: ‘0-0’
},
{
path: ‘/banner’,
label: ‘轮播图管理’,
key: ‘/banner’,
redirect: ‘/banner/list’,
icon: ,
keyid: ‘0-1’,
children: [
{
path: ‘/banner/list’,
key: ‘/banner/list’,
label: ‘轮播图列表’,
icon: ,
element: ,
keyid: ‘0-1-0’
},
{
path: ‘/banner/add’,
key: ‘/banner/add’,
label: ‘添加轮播图’,
icon: ,
element: ,
hidden: 1,
keyid: ‘0-1-1’
}
]
},
{
path: ‘/pro’,
label: ‘产品管理’,
key: ‘/pro’,
redirect: ‘/pro/list’,
icon: ,
keyid: ‘0-2’,
children: [
{
path: ‘/pro/list’,
key: ‘/pro/list’,
label: ‘产品列表’,
icon: ,
element: ,
keyid: ‘0-2-0’
},
{
path: ‘/pro/search’,
key: ‘/pro/search’,
label: ‘筛选列表’,
icon: ,
element: ,
keyid: ‘0-2-1’
}
]
},
{
path: ‘/account’,
label: ‘账户管理’,
key: ‘/account’,
redirect: ‘/account/user’,
icon: ,
keyid: ‘0-3’,
children: [
{
path: ‘/account/user’,
key: ‘/account/user’,
label: ‘用户列表’,
icon: ,
element: ,
keyid: ‘0-3-0’
},
{
path: ‘/account/admin’,
key: ‘/account/admin’,
label: ‘管理员列表’,
icon: ,
element: ,
keyid: ‘0-3-1’
}
]
},
{
path: ‘/set’,
label: ‘设置’,
key: ‘/set’,
icon: ,
element: ,
hidden: 1,
keyid: ‘0-4’
},
{
path: ‘/data’,
label: ‘数据可视化’,
key: ‘/data’,
redirect: ‘/data/echarts’,
icon: ,
keyid: ‘0-5’,
children: [
{
path: ‘/data/echarts’,
key: ‘/data/echarts’,
label: ‘echarts’,
icon: ,
element: ,
keyid: ‘0-5-0’
},
{
path: ‘/data/HighCharts’,
key: ‘/data/HighCharts’,
label: ‘HighCharts’,
icon: ,
element: ,
keyid: ‘0-5-1’
},
{
path: ‘/data/antv’,
key: ‘/data/antv’,
label: ‘antv’,
icon: ,
element: ,
keyid: ‘0-5-2’
}
]
},
{
path: ‘/braft’,
label: ‘父文本编辑器’,
key: ‘/braft’,
icon: ,
element: ,
keyid: ‘0-6’
},
{
path: ‘/md’,
label: ‘markDown编辑器’,
key: ‘/md’,
icon: ,
element: ,
keyid: ‘0-7’
},
{
path: ‘/excel’,
label: ‘导入以及导出’,
key: ‘/excel’,
redirect: ‘/excel/export’,
icon: ,
keyid: ‘0-8’,
children: [
{
path: ‘/excel/import’,
key: ‘/excel/import’,
label: ‘导入’,
icon: ,
element: ,
keyid: ‘0-8-0’
},
{
path: ‘/excel/export’,
key: ‘/excel/export’,
label: ‘导出’,
icon: ,
element: ,
keyid: ‘0-8-1’
}
]
},
{
path: ‘/map’,
label: ‘地图’,
key: ‘/map’,
redirect: ‘/map/baidu’,
icon: ,
keyid: ‘0-8’,
children: [
{
path: ‘/map/baidu’,
key: ‘/map/baidu’,
label: ‘百度地图’,
icon: ,
element: ,
keyid: ‘0-9-0’
},
{
path: ‘/map/gaode’,
key: ‘/map/gaode’,
label: ‘高德地图’,
icon: ,
element: ,
keyid: ‘0-9-1’
}
]
},
]

export default menus


```html
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8" /><link rel="icon" href="%PUBLIC_URL%/favicon.ico" /><meta name="viewport" content="width=device-width, initial-scale=1" /><meta name="theme-color" content="#000000" /><metaname="description"content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /><!--manifest.json provides metadata used when your web app is installed on auser's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/--><link rel="manifest" href="%PUBLIC_URL%/manifest.json" /><!--Notice the use of %PUBLIC_URL% in the tags above.It will be replaced with the URL of the `public` folder during the build.Only files inside the `public` folder can be referenced from the HTML.Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" willwork correctly both with client-side routing and a non-root public URL.Learn how to configure a non-root public URL by running `npm run build`.--><title>React App</title><script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=17qecKvCwmMWGwzVqPQvG9GQkRSPZHc8"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><!--This HTML file is a template.If you open it directly in the browser, you will see an empty page.You can add webfonts, meta tags, or analytics to this file.The build step will place the bundled scripts into the <body> tag.To begin the development, run `npm start` or `yarn start`.To create a production bundle, use `npm run build` or `yarn build`.--></body>
</html>
// src/views/map/map.d.ts.
interface Window {BMapGL: any
}
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';interface IBaiduProps {};const Baidu:FC<IBaiduProps> = () => {useEffect(() => {var map = new window.BMapGL.Map("allmap");map.centerAndZoom(new window.BMapGL.Point(116.280190, 40.049191), 19);map.enableScrollWheelZoom(true);map.setHeading(64.5);map.setTilt(73);}, [])return (<><h1>百度地图</h1><div id="allmap" style={{width:' 100%',height: '500px'}}></div></>)
};export default Baidu;

30.项目打包发布

https://blog.csdn.net/daxunshuo/article/details/102976306?spm=1001.2014.3001.5501

$ cnpm run build
# 估计需要一点时间请耐心等待

打包完毕项目 build 文件夹即为 项目打包出来的文件,只需要把build文件夹上传至服务器,一般情况下,都需要修改build文件夹的名字

如果打开 build/index.html 文件,发现 css js 文件的引入使用的都是绝对路径

如果服务器只部署这一个项目,那么可以把 build文件夹的内容替换了 服务器的静态资源文件 — 绝对路径

如果服务器部署多个项目,那么可以把修改过后的 build的文件夹上传到 服务器的静态资源文件 — 项目路径

如何打包项目时使用相对路径

package.json

{"homepage": '.'
}

http://121.89.205.189:2207/