react-pc https://www.html.cn/create-react-app/
1.创建项目 1 2 npx create-react-app admin-react-app --template typescript
熟悉目录结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - admin-app -node_modules -public -src App.css App.test.tsx App.tsx的测试文件 npm run test 查看测试结果 App.tsx index.css index.tsx react应用程序的入口文件 logo.svg react-app-env.d.ts // 声明文件 // 指令声明对包的依赖关系 reportWebVitals.ts // 测试性能 seupTests.ts // 使用jest做为测试工具 .gitignore package-lock.json package.json README.md tsconfig.json
*.d.ts 代表ts的声明文件
2.改造目录结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 src api components layout store router utils views App.tsx index.tsx logo.svg react-app-env.d.ts reportWebVitals.ts seupTests.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import './index.css' ;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 ();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IAppProps { }; const App :FC <IAppProps > = () => { return ( <div > App </div > ) }; export default App ;
3.安装一些必须的模块 3.1 配置预处理器 两种方式:
抽离配置文件配置预处理器 不抽离配置文件craco进行预处理器配置 本项目推荐使用第二种方式
1 $ cnpm i @craco/craco @types/node -D
https://www.npmjs.com/package/@craco/craco
3.1.1 配置别名@ 项目根目录创建 craco.config.js
,代码如下:
1 2 3 4 5 6 7 8 9 const path = require ('path' )module .exports = { webpack : { alias : { '@' : path.resolve (__dirname, 'src' ) } } }
为了使 TS 文件引入时的别名路径能够正常解析,需要配置 tsconifg.json
,在 compilerOptions
选项里添加 path 等属性。为了防止配置被覆盖,需要单独创建一个文件 tsconfig.path.json
,添加以下代码
1 2 3 4 5 6 7 8 9 10 11 12 { "compilerOptions" : { "baseUrl" : "." , "paths" : { "@/*" : [ "./src/*" ] } , "types" : [ "node" ] } }
在 tsconifg.json
引入配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { "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
如下:
1 2 3 4 5 "scripts" : { "start" : "craco start" , "build" : "craco build" , "test" : "craco test" } ,
3.2安装状态管理器 根据项目需求 任选其一即可
1 2 3 4 5 6 7 $ 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 技术
1 cnpm i @reduxjs/toolkit redux react-redux redux-devtools -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版本
1 cnpm i react-router-dom -S
3.4 数据验证 思考,有没有必要安装 prop-types ?
本项目其实没有必要安装,因为所有的数据都是基于ts,而ts需要指定类型注解
3.5数据请求 以前版本中 cnpm i @types/axios -S
Ts 中 @types/* 为声明文件
3.6ui库 官网地址:https://ant.design/index-cn 4.22.2
国内官方镜像地址:https://ant-design.antgroup.com/index-cn
国内gitee镜像地址:https://ant-design.gitee.io/index-cn
src/index.css
1 2 @import '~antd/dist/antd.css' ;
src/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import './index.css' ;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 ();
测试组件库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import React , { FC } from 'react' ;import { Button } from 'antd' interface IAppProps { }; const App :FC <IAppProps > = () => { return ( <div > App <Button type ="primary" danger > Primary </Button > </div > ) }; export default App ;
浏览器查看发现测试通过
3.6.1 自定义主题 https://ant-design.antgroup.com/docs/react/use-in-typescript-cn
按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能。我们可以引入 craco-antd 或者是 craco-less
来帮助加载 less 样式和修改变量。
1 2 $ cnpm i craco-antd -D $ cnpm i craco-less -D
首先把 src/index.css
文件修改为 src/index.less
,然后修改样式引用为 less 文件。
修改 craco.config.js
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const CracoAntDesignPlugin = require ('craco-antd' ); const path = require ('path' )module .exports = { webpack : { alias : { '@' : path.resolve (__dirname, 'src' ) } }, plugins : [ { plugin : CracoAntDesignPlugin , options : { customizeTheme : { '@primary-color' : '#1DA57A' , }, }, }, ] }
这里利用了 less-loader 的 modifyVars
来进行主题配置,变量和其他配置方式可以参考 配置主题 文档。修改后重启 yarn start
,如果看到一个绿色的按钮就说明配置成功了。
antd 内建了深色主题和紧凑主题,你可以参照 使用暗色主题和紧凑主题 进行接入。
可以定制的变量列表如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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 );
完整的配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 const CracoAntDesignPlugin = require ('craco-antd' ); const path = require ('path' )module .exports = { webpack : { alias : { '@' : path.resolve (__dirname, 'src' ) } }, plugins : [ { plugin : CracoAntDesignPlugin , options : { customizeTheme : { '@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)' }, }, }, ] }
同样,你可以使用 react-app-rewired 和 customize-cra 来自定义 create-react-app 的 webpack 配置。
3.7其他第三方工具包 https://www.lodashjs.com/
Lodash 工具包,项目必装,它提供了很多使用的函数
1 2 $ cnpm i lodash -S $ cnpm i @types/lodash -D
1 2 3 4 5 6 7 8 9 10 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import { MenuFoldOutlined , MenuUnfoldOutlined , UploadOutlined , UserOutlined , VideoCameraOutlined , } from '@ant-design/icons' ; import { Layout , Menu } from 'antd' ;import React , { useState } from 'react' ;const { Header , Sider , Content } = Layout ;const App : React .FC = () => { const [collapsed, setCollapsed] = useState (false ); return ( <Layout id ='admin-app' > <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" /> <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} items ={[ { key: '1 ', icon: <UserOutlined /> , label: 'nav 1', }, { key: '2', icon: <VideoCameraOutlined /> , label: 'nav 2', }, { key: '3', icon: <UploadOutlined /> , label: 'nav 3', }, ]} /> </Sider > <Layout className ="site-layout" > <Header className ="site-layout-background" style ={{ padding: 0 }}> {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => setCollapsed(!collapsed), })} </Header > <Content className ="site-layout-background" style ={{ margin: '24px 16px ', padding: 24 , minHeight: 280 , }} > Content </Content > </Layout > </Layout > ); }; export default App ;
主组件引入 主界面的布局文件
1 2 3 4 5 6 7 8 9 10 11 12 13 import { FC } from "react" ;import Index from './layout/Index' type AppProps = {}const App : FC = (props: AppProps ) => ( <> <Index /> </> ); export default App
查看浏览器,预览运行结果
发现页面并不是全屏。审查元素设置 root以及 components-layout-demo-custom-trigger 高度为 100%
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @import '~antd/dist/antd.less' ;// 设置全屏 --- 审查元素 一层一层找 坚决不放过任何一个遗漏的 html , body , #root { height : 100% ; } #admin-app { height : 100% ; } // 布局文件 // #components-layout-demo-custom-trigger .trigger { // 组件库提供案例id,改成自己的 #admin-app .trigger { padding : 0 24px ; font-size : 18px ; line-height : 64px ; cursor : pointer; transition : color 0.3s ; } // #components-layout-demo-custom-trigger .trigger :hover #admin-app .trigger :hover { color : #1890ff ; } // #components-layout-demo-custom-trigger .logo { #admin-app .logo { height : 32px ; margin : 16px ; background : rgba (255 , 255 , 255 , 0.3 ); } .site-layout .site-layout-background { background : #fff ; }
5.拆分主界面 先拆分左侧的菜单栏组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import React , { FC , useState } from 'react' ;import { UploadOutlined , UserOutlined , VideoCameraOutlined , } from '@ant-design/icons' ; import { Layout , Menu } from 'antd' ;interface ISideBarProps { }; const { Sider } = Layout ;const SideBar :FC <ISideBarProps > = () => { const [collapsed] = useState (false ); return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" /> <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} items ={[ { key: '1 ', icon: <UserOutlined /> , label: 'nav 1', }, { key: '2', icon: <VideoCameraOutlined /> , label: 'nav 2', }, { key: '3', icon: <UploadOutlined /> , label: 'nav 3', }, ]} /> </Sider > ) }; export default SideBar ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import React , { FC , useState } from 'react' ;import { MenuFoldOutlined , MenuUnfoldOutlined } from '@ant-design/icons' ; import { Layout } from 'antd' ;const { Header } = Layout ;interface IAppHeaderProps { }; const AppHeader :FC <IAppHeaderProps > = () => { const [collapsed, setCollapsed] = useState (false ); return ( <Header className ="site-layout-background" style ={{ padding: 0 }}> {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => setCollapsed(!collapsed), })} </Header > ) }; export default AppHeader ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import React , { FC } from 'react' ;import { Layout } from 'antd' ;interface IAppMainProps { }; const { Content } = Layout ;const AppMain :FC <IAppMainProps > = () => { return ( <Content className ="site-layout-background" style ={{ margin: '24px 16px ', padding: 24 , minHeight: 280 , }} > Content </Content > ) }; export default AppMain ;
整和组件资源
1 2 3 4 export { default as SideBar } from './SideBar' export { default as AppHeader } from './AppHeader' export { default as AppMain } from './AppMain'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Layout } from 'antd' ;import React from 'react' ;import { AppHeader , AppMain , SideBar } from './components' const App : React .FC = () => { return ( <Layout id ='admin-app' > {/* 左侧菜单栏 */} <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类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { configureStore } from '@reduxjs/toolkit' ;const store = configureStore ({ reducer : { } }); export type RootState = ReturnType <typeof store.getState >export type AppDispatch = typeof store.dispatch export default store;
构建app的模块用于管理 头部和 左侧菜单的共同的状态
6.2 定义 Hooks 类型 虽然可以将RootState
andAppDispatch
类型导入到每个组件中,但最好创建useDispatch
and useSelector
hooks 的类型化版本以在您的应用程序中使用 。
1 2 3 4 5 6 7 8 import { useSelector, useDispatch, TypedUseSelectorHook } from 'react-redux' import { AppDispatch , RootState } from './index' export const useAppDispatch : () => AppDispatch = useDispatch export const useAppSelector : TypedUseSelectorHook <RootState > = useSelector
6.3 应用程序中使用 创建状态管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { createSlice, PayloadAction } from '@reduxjs/toolkit' interface IState { count : number collapsed : boolean } const initialState : IState = { count : 100 , collapsed : false } const appSlice = createSlice ({ name : 'app' , initialState, reducers : { addCount (state, action : PayloadAction <number >) { state.count += action.payload }, setCollapsed (state, action : PayloadAction <boolean >) { state.collapsed = action.payload } } }) export const { addCount, setCollapsed } = appSlice.actions export default appSlice.reducer
6.4 整合reducer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { configureStore } from '@reduxjs/toolkit' ;import appReducer from './modules/app' const store = configureStore ({ reducer : { app : appReducer } }); export type RootState = ReturnType <typeof store.getState > export type AppDispatch = typeof store.dispatch export default store;
6.5 入口文件配置状态管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import { Provider } from 'react-redux' ;import store from '@/store' import '@/index.less' ;import App from '@/App' ;import reportWebVitals from '@/reportWebVitals' ;const root = ReactDOM .createRoot ( document .getElementById ('root' ) as HTMLDivElement ); root.render ( <React.StrictMode > <Provider store = { store }> <App /> </Provider > </React.StrictMode > ); reportWebVitals ();
6.6 左侧菜单栏使用状态管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import React , { FC } from 'react' ;import { UploadOutlined , UserOutlined , VideoCameraOutlined , } from '@ant-design/icons' ; import { Layout , Menu } from 'antd' ;import { useAppSelector } from '@/store/hook' ;interface ISideBarProps { }; const { Sider } = Layout ;const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" /> <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} items ={[ { key: '1 ', icon: <UserOutlined /> , label: 'nav 1', }, { key: '2', icon: <VideoCameraOutlined /> , label: 'nav 2', }, { key: '3', icon: <UploadOutlined /> , label: 'nav 3', }, ]} /> </Sider > ) }; export default SideBar ;
6.7 头部组件使用状态管理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import React , { FC } from 'react' ;import { MenuFoldOutlined , MenuUnfoldOutlined } from '@ant-design/icons' ; import { Layout } from 'antd' ;import { useAppDispatch, useAppSelector } from '@/store/hook' ;import { setCollapsed } from '@/store/modules/app' const { Header } = Layout ;interface IAppHeaderProps {}; const AppHeader :FC <IAppHeaderProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ) const dispatch = useAppDispatch () return ( <Header className ="site-layout-background" style ={{ padding: 0 }}> {/* {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => { dispatch(setCollapsed(!collapsed)) } })} */} { collapsed ? <MenuUnfoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> : <MenuFoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> } </Header > ) }; export default AppHeader ;
6.8保留用户习惯-可选 永久存储 用户习惯
此时发现 头部的 按钮可以控制左侧菜单栏了,但是还没有满足需求
需求如下:保留用户的使用习惯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { createSlice, PayloadAction } from '@reduxjs/toolkit' interface IState { count : number collapsed : boolean } const initialState : IState = { count : 100 , collapsed : localStorage .getItem ('collapsed' ) === 'true' } const appSlice = createSlice ({ name : 'app' , initialState, reducers : { addCount (state, action : PayloadAction <number >) { state.count += action.payload }, setCollapsed (state, action : PayloadAction <boolean >) { localStorage .setItem ('collapsed' , String (action.payload )) state.collapsed = action.payload } } }) export const { addCount, setCollapsed } = appSlice.actions export default appSlice.reducer
6.9 永久存储的 类 localStorage 的工具 store2 https://www.npmjs.com/package/store2
推荐一个好用的永久存储的 类 localStorage 的工具 store2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { createSlice, PayloadAction } from '@reduxjs/toolkit' import store2 from 'store2' interface IState { count : number collapsed : boolean } const initialState : IState = { count : 100 , collapsed : store2.get ('collapsed' ) === 'true' } const appSlice = createSlice ({ name : 'app' , initialState, reducers : { addCount (state, action : PayloadAction <number >) { state.count += action.payload }, setCollapsed (state, action : PayloadAction <boolean >) { store2.set ('collapsed' , String (action.payload )) state.collapsed = action.payload } } }) export const { addCount, setCollapsed } = appSlice.actions export default appSlice.reducer
7.左侧菜单栏 7.1.设计左侧菜单栏的数据 https://ant-design.gitee.io/components/menu-cn/#components-menu-demo-sider-current
Antd 4.20以上版本直接实现 递归
antd 4.20版本以下需要手动实现
1 2 3 4 5 6 7 8 9 10 import { ReactNode } from 'react' export interface IMenuProps { label : string key : string icon?: ReactNode children?: IMenuProps [] }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , icon : <HomeOutlined /> }, { label : '轮播图管理' , key : '/banner' , icon : <UploadOutlined /> , children : [ { label : '首页轮播图' , key : '/banner/home' , icon : <PictureOutlined /> }, { label : '活动页轮播图' , key : '/banner/active' , icon : <PictureOutlined /> }, { label : '添加轮播图' , key : '/banner/add' , icon : <UploadOutlined /> }, ] }, { label : '产品管理' , key : '/pro' , icon : <ProfileOutlined /> , children : [ { label : '产品列表' , key : '/pro/list' , icon : <OrderedListOutlined /> , children : [ { label : '首页产品列表' , key : '/pro/list/home' }, { label : '详情推荐列表' , key : '/pro/list/detail' }, { label : '购物车推荐列表' , key : '/pro/list/cart' }, ] }, { label : '筛选列表' , key : '/pro/search' , icon : <FilterOutlined /> }, ] }, { label : '账户管理' , key : '/account' , icon : <TeamOutlined /> , children : [ { label : '用户列表' , key : '/account/userlist' , icon : <UserOutlined /> }, { label : '管理员列表' , key : '/account/adminlist' , icon : <UserOutlined /> } ] }, { label : '设置' , key : '/setting' , icon : <SettingOutlined /> } ] export default menus
7.2.渲染左侧菜单栏 左侧菜单栏的头部设定logo以及后台管理系统名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import React , { FC } from 'react' ;import { Layout , Menu , Image } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' interface ISideBarProps { }; const { Sider } = Layout ;const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} items ={ menus } /> </Sider > ) }; export default SideBar ;
7.3 低版本处理 以上菜单项的设置在antd 4.20.0
版本以上好使,如果在4.20.0
版本以下,应该使用 递归组件实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import React , { FC } from 'react' ;import { Layout , Menu , Image } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { IMenuProps } from '@/router/inter' ;interface ISideBarProps { }; const { Sider } = Layout ;const renderMenu = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return ( <Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu > ) } else { return <Menu.Item key = { item.key } icon = { item.icon }> { item.label }</Menu.Item > } }) } const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} > { renderMenu(menus) } </Menu > </Sider > ) }; export default SideBar ;
优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // src/router/utils.tsx import { Menu } from 'antd'; import { IMenuProps } from './inter'; export const renderMenu = (menus: IMenuProps[]) => { return menus.map(item => { if (item.children) { return ( <Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu> ) } else { return <Menu.Item key = { item.key } icon = { item.icon }>{ item.label }</Menu.Item> } }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import React , { FC } from 'react' ;import { Layout , Menu , Image } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { renderMenu } from '@/router/utils' interface ISideBarProps { }; const { Sider } = Layout ;const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} > { renderMenu(menus) } </Menu > </Sider > ) }; export default SideBar ;
7.4 菜单渲染优化 如果左侧菜单栏数据过于庞大,每个管理项里又有很多项,需要只展开一个菜单项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import React , { FC , useState } from 'react' ;import { Layout , Menu , Image , MenuProps } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { renderMenu } from '@/router/utils' interface ISideBarProps { }; const { Sider } = Layout ;const rootSubmenuKeys : string [] = [];menus.forEach (item => { item.children && rootSubmenuKeys.push (item.key ) }) console .log (rootSubmenuKeys)const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); const [openKeys, setOpenKeys] = useState<string []>([]) const onOpenChange : MenuProps ['onOpenChange' ] = keys => { console .log ('keys' , keys) console .log ('openKeys' , openKeys) const latestOpenKey = keys.find (key => openKeys.indexOf (key) === -1 ); console .log ('latestOpenKey' , latestOpenKey) if (rootSubmenuKeys.indexOf (latestOpenKey!) === -1 ) { setOpenKeys (keys); } else { setOpenKeys (latestOpenKey ? [latestOpenKey] : []); } } return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} openKeys = { openKeys } // 打开哪一个菜单项 onOpenChange = { onOpenChange } // 打开某一个菜单项时需要关闭其他菜单项 > { renderMenu(menus) } </Menu > </Sider > ) }; export default SideBar ;
8.定义路由 8.1 官方文档 https://reactrouter.com/
8.2 创建对应的页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |-src | |- ... | |-views | |- banner | | |- Index.tsx #轮播图管理页面 | | |- List.tsx #首页轮播图 | | |- Active.tsx #活动页轮播图 | | |- Add.tsx #添加轮播图 | |- home | | |- Index.tsx #系统首页 | |- product | | |- Index.tsx #产品管理 | | |- Search.tsx #筛选列表 | | |- list | | | |- Index.tsx #产品列表 | | | |- Home.tsx #首页产品列表 | | | |- Detail.tsx#详情推荐列表 | | | |- Cart.tsx #购物车推荐列表 | |- Account | | |- Index.tsx #账户管理 | | |- UserList.tsx #用户列表 | | |- AdminList.tsx#管理员列表 | |- setting | | |- Index.tsx #设置页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 账户管理</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 用户列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 管理员列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 轮播图管理</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 首页轮播图列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 活动页轮播图</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 添加轮播图</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 系统首页</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 产品管理</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 筛选列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 产品列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 首页产品列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 详情推荐列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 购物车推荐列表</h1 > </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 设置页面</h1 > </> ) }; export default Index ;
有二级菜单,能想到的 就是使用 嵌套路由
https://reactrouter.com/docs/en/v6/getting-started/overview#index-routes
8.3 定义菜单路由信息 v6的路由通过 element 属性定义匹配的组件
因此menus中可以添加一个 element 属性,值就为组件的引用即可
1 2 3 4 5 6 7 8 9 10 11 import { ReactNode } from 'react' export interface IMenuProps { label : string key : string icon?: ReactNode children?: IMenuProps [] element : ReactNode }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , icon : <PictureOutlined /> , element : <BannerList /> }, { label : '活动页轮播图' , key : '/banner/active' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , icon : <UploadOutlined /> , element : <BannerAdd /> }, ] }, { label : '产品管理' , key : '/pro' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , icon : <OrderedListOutlined /> , element : <ProductList /> , children : [ { label : '首页产品列表' , key : '/pro/list/home' , element : <ProductHomeList /> }, { label : '详情推荐列表' , key : '/pro/list/detail' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , icon : <UserOutlined /> , element : <UserList /> }, { label : '管理员列表' , key : '/account/adminlist' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , icon : <SettingOutlined /> , element : <Setting /> } ] export default menus
8.4.装载路由 在根组件添加 BrowserRouter
或者 HashRouter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import { Provider } from 'react-redux' ;import store from '@/store' import '@/index.less' ;import App from '@/App' ;import reportWebVitals from '@/reportWebVitals' ;import { BrowserRouter } from 'react-router-dom' const root = ReactDOM .createRoot ( document .getElementById ('root' ) as HTMLDivElement ); root.render ( <React.StrictMode > <Provider store = { store }> <BrowserRouter > <App /> </BrowserRouter > </Provider > </React.StrictMode > ); reportWebVitals ();
8.5 定义路由组件 在menu.tsx
里已经定义好了请求的路径(其实就是数据中key属性)和路径对应组件(其实就是数据中的element属性),剩下就是定义路由组件了
组件渲染的区域 AppMain
组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import { Menu } from 'antd' ;import { IMenuProps } from './inter' ;import { Route } from 'react-router-dom' export const renderMenu = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return ( <Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu > ) } else { return <Menu.Item key = { item.key } icon = { item.icon }> { item.label }</Menu.Item > } }) } export const renderRoute = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return ( <Route key = { item.key } path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> </Route > ) } }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import React , { FC } from 'react' ;import { Layout } from 'antd' ;import menus from '@/router/menu' ;import { renderRoute } from '@/router/utils' ;import { Routes } from 'react-router-dom' interface IAppMainProps { }; const { Content } = Layout ;const arr = renderRoute (menus)console .log ('arr' , arr[1 ])const AppMain :FC <IAppMainProps > = () => { return ( <Content className ="site-layout-background" style ={{ margin: '24px 16px ', padding: 24 , minHeight: 280 , }} > {/* 多个路由需要定义时,包裹在Routes选项下 */} <Routes > { renderRoute(menus) } </Routes > </Content > ) }; export default AppMain ;
8.6 二级乃至三级菜单生效 二级菜单对应的二级路由要生效,需要给以及页面添加 <Outlet />
标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 账户管理</h1 > <Outlet /> </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 轮播图管理</h1 > <Outlet /> </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 产品管理</h1 > <Outlet /> </> ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <> <h1 > 产品列表</h1 > <Outlet /> </> ) }; export default Index ;
8.7 手动测试路由 可以在地址栏输入路径,测试是否正常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 http://localhost:3000/ http://localhost:3000/banner http://localhost:3000/banner/home http://localhost:3000/banner/active http://localhost:3000/banner/add http://localhost:3000/pro http://localhost:3000/pro/search http://localhost:3000/pro/list http://localhost:3000/pro/list/home http://localhost:3000/pro/list/detail http://localhost:3000/pro/list/cart http://localhost:3000/account http://localhost:3000/account/userlist http://localhost:3000/account/adminlist http://localhost:3000/settinig
8.8 设置默认路由 参照 https://reactrouter.com/docs/en/v6/getting-started/overview#index-routes 学习默认路由
根据渲染需求,当路由包含一层或者多层子路由时,当选择父级路由时,希望能够自动跳转到某个子路由。也就是路由的重定向。
在路由表添加标记默认打开子路由
1 2 3 4 5 6 7 8 9 10 11 12 import { ReactNode } from 'react' export interface IMenuProps { label : string key : string icon?: ReactNode children?: IMenuProps [] element : ReactNode index?: 1 | 0 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , icon : <UploadOutlined /> , element : <BannerAdd /> }, ] }, { label : '产品管理' , key : '/pro' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , icon : <SettingOutlined /> , element : <Setting /> } ] export default menus
为了配合路由跳转组件,修改代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 import { Menu } from 'antd' ;import { IMenuProps } from './inter' ;import { Navigate , Route } from 'react-router-dom' import React from 'react' ;export const renderMenu = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return ( <Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu > ) } else { return <Menu.Item key = { item.key } icon = { item.icon }> { item.label }</Menu.Item > } }) } export const renderRoute = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { if (!!item.index ) { return ( <React.Fragment key ={item.key} > <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > ) } } else { if (!!item.index ) { return ( <React.Fragment key = { item.key }> <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> </Route > ) } } }) }
1 2 3 http://localhost:3000/banner ===> http://localhost:3000/banner/home http://localhost:3000/pro ===> http://localhost:3000/pro/list/home http://localhost:3000/account ===> http://localhost:3000/account/userlist
8.9 设置404页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React , { FC } from 'react' ;interface IPage404Props { }; const Page404 :FC <IPage404Props > = ({} ) => { return ( <h1 > 当前页面未找到</h1 > ) }; export default Page404 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import React , { FC } from 'react' ;import { Layout } from 'antd' ;import menus from '@/router/menu' ;import { renderRoute } from '@/router/utils' ;import { Routes , Route } from 'react-router-dom' import Page404 from '@/views/error/Page404' ;interface IAppMainProps { }; const { Content } = Layout ;const AppMain :FC <IAppMainProps > = () => { return ( <Content className ="site-layout-background" style ={{ margin: '24px 16px ', padding: 24 , minHeight: 280 , }} > {/* 多个路由需要定义时,包裹在Routes选项下 */} <Routes > { renderRoute(menus) } {/* ++++++ 404+++++ */} <Route path ="*" element = { <Page404 /> } /> </Routes > </Content > ) }; export default AppMain ;
9 切换路由 上诉项目中,切换路由都是手动输入的,实际上应该点击左侧菜单栏进行路由导航。
左侧菜单的逻辑交互,前面已经生成了(openKeys 以及 onOpenChanges 实现)
现在通过点击事件来切换导航
9.1 点击切换路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import React , { FC , useState } from 'react' ;import { Layout , Menu , Image , MenuProps } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { renderMenu } from '@/router/utils' import { useNavigate } from 'react-router-dom' ;interface ISideBarProps { }; const { Sider } = Layout ;const rootSubmenuKeys : string [] = [];menus.forEach (item => { item.children && rootSubmenuKeys.push (item.key ) }) console .log (rootSubmenuKeys)const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); const [openKeys, setOpenKeys] = useState<string []>([]) const onOpenChange : MenuProps ['onOpenChange' ] = keys => { console .log ('keys' , keys) console .log ('openKeys' , openKeys) const latestOpenKey = keys.find (key => openKeys.indexOf (key) === -1 ); console .log ('latestOpenKey' , latestOpenKey) if (rootSubmenuKeys.indexOf (latestOpenKey!) === -1 ) { setOpenKeys (keys); } else { setOpenKeys (latestOpenKey ? [latestOpenKey] : []); } } const navigate = useNavigate () const changeUrl = ({ key }: { key: string } ) => { console .log (key) navigate (key, { replace : false }) } return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} openKeys = { openKeys } // 打开哪一个菜单项 onOpenChange = { onOpenChange } // 打开某一个菜单项时需要关闭其他菜单项 onClick = { changeUrl } // +++++++++ > { renderMenu(menus) } </Menu > </Sider > ) }; export default SideBar ;
9.2 刷新保持左侧菜单状态 当页面刷新时,需要保证当前二级路由是展开的,且当前路由是被选中的状态
编写一个 getSubMenu
函数, 用来生成 ['pro', 'pro/list', 'pro/list/home']
的数组,用于展开当前的 SubMenu
菜单项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 import { Menu } from 'antd' ;import { IMenuProps } from './inter' ;import { Navigate , Route } from 'react-router-dom' import React from 'react' ;export const renderMenu = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return ( <Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu > ) } else { return <Menu.Item key = { item.key } icon = { item.icon }> { item.label }</Menu.Item > } }) } export const renderRoute = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { if (!!item.index ) { return ( <React.Fragment key ={item.key} > <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > ) } } else { if (!!item.index ) { return ( <React.Fragment key = { item.key }> <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> </Route > ) } } }) } export const getSubMenu = (pathname: string ) => { const pathArray = pathname.split ('/' ).slice (1 ) const result = pathArray.reduce ((newArr, item, index ) => { const str = newArr[index] + '/' + item newArr.push (str) return newArr }, ['' ]) console .log ('result' , result) return result }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import React , { FC , useState } from 'react' ;import { Layout , Menu , Image , MenuProps } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { renderMenu, getSubMenu } from '@/router/utils' import { useLocation, useNavigate } from 'react-router-dom' ;interface ISideBarProps { }; const { Sider } = Layout ;const rootSubmenuKeys : string [] = [];menus.forEach (item => { item.children && rootSubmenuKeys.push (item.key ) }) const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); const { pathname } = useLocation () const [selectedKeys, setSelectedKeys] = useState ([pathname]) const [openKeys, setOpenKeys] = useState<string []>(getSubMenu (pathname)) const onOpenChange : MenuProps ['onOpenChange' ] = keys => { console .log ('keys' , keys) console .log ('openKeys' , openKeys) const latestOpenKey = keys.find (key => openKeys.indexOf (key) === -1 ); console .log ('latestOpenKey' , latestOpenKey) if (rootSubmenuKeys.indexOf (latestOpenKey!) === -1 ) { setOpenKeys (keys); } else { setOpenKeys (latestOpenKey ? [latestOpenKey] : []); } } const navigate = useNavigate () const changeUrl = ({ key }: { key: string } ) => { console .log (key) navigate (key, { replace : false }) setSelectedKeys ([key]) } return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} openKeys = { openKeys } // 打开哪一个菜单项 onOpenChange = { onOpenChange } // 打开某一个菜单项时需要关闭其他菜单项 onClick = { changeUrl } selectedKeys = { selectedKeys } // ++++++++++ > { renderMenu(menus) } </Menu > </Sider > ) }; export default SideBar ;
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 设置面包屑导航 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import { IMenuProps } from '@/router/inter' ;import menus from '@/router/menu' ;import React , { FC } from 'react' ;import { Breadcrumb } from 'antd' import { useLocation, Link } from 'react-router-dom' ;interface IAppBreadcrumbProps { }; const breadcrumbNameMap : Record <string , string > = {}const getBreadcrumbData = (menus: IMenuProps[] ) => { menus.forEach (item => { if (item.children ) { breadcrumbNameMap[item.key ] = item.label getBreadcrumbData (item.children ) } else { breadcrumbNameMap[item.key ] = item.label } }) } getBreadcrumbData (menus)const AppBreadcrumb :FC <IAppBreadcrumbProps > = () => { const location = useLocation () const pathSnippets = location.pathname .split ('/' ).filter (i => i); console .log (pathSnippets) const extraBreadcrumbItems = pathSnippets.map ((_, index ) => { const url = `/${pathSnippets.slice(0 , index + 1 ).join('/' )} ` ; console .log (url) return ( <Breadcrumb.Item key ={url} > <Link to ={url} > {breadcrumbNameMap[url]}</Link > </Breadcrumb.Item > ); }); const breadcrumbItems = [ location.pathname === '/' ? null : <Breadcrumb.Item key ="home" > <Link to ="/" > 系统首页</Link > </Breadcrumb.Item > , ].concat (extraBreadcrumbItems); return ( <div style ={{ height: '100 %', display: 'flex ', alignItems: 'center ' }}> <Breadcrumb > {breadcrumbItems}</Breadcrumb > </div > ) }; export default AppBreadcrumb ;
1 2 3 4 5 export { default as SideBar } from './SideBar' export { default as AppHeader } from './AppHeader' export { default as AppMain } from './AppMain' export { default as AppBreadcrumb } from './AppBreadcrumb'
头部组件加入了面包屑导航组件,尽可能不动原来的布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import React , { FC } from 'react' ;import { MenuFoldOutlined , MenuUnfoldOutlined } from '@ant-design/icons' ; import { Layout } from 'antd' ;import { useAppDispatch, useAppSelector } from '@/store/hook' ;import { setCollapsed } from '@/store/modules/app' import AppBreadcrumb from './AppBreadcrumb' ;const { Header } = Layout ;interface IAppHeaderProps {}; const AppHeader :FC <IAppHeaderProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ) const dispatch = useAppDispatch () return ( <Header className ="site-layout-background" style ={{ padding: 0 , display: 'flex ' }}> {/* {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => { dispatch(setCollapsed(!collapsed)) } })} */} <div > { collapsed ? <MenuUnfoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> : <MenuFoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> } </div > <AppBreadcrumb /> </Header > ) }; export default AppHeader ;
随之而来的问题就是,当点击面包屑导航时,地址栏的路由已经发生了跳转,但是左侧菜单栏数据效果没有实时更新(左侧菜单栏组件早就创建完毕,选中和打开的选项已经做了固定, 点击面包屑没有引起左侧菜单栏组件的状态以及属性的更新,左侧菜单栏不会重新渲染)
此时可以在左侧菜单栏组件监听 路由的变化 – -useEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 import React , { FC , useState, useEffect } from 'react' ;import { Layout , Menu , Image , MenuProps } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { renderMenu, getSubMenu } from '@/router/utils' import { useLocation, useNavigate } from 'react-router-dom' ;interface ISideBarProps { }; const { Sider } = Layout ;const rootSubmenuKeys : string [] = [];menus.forEach (item => { item.children && rootSubmenuKeys.push (item.key ) }) const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); const { pathname } = useLocation () const [selectedKeys, setSelectedKeys] = useState ([pathname]) const [openKeys, setOpenKeys] = useState<string []>(getSubMenu (pathname)) 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] : []); } } const navigate = useNavigate () const changeUrl = ({ key }: { key: string } ) => { navigate (key, { replace : false }) setSelectedKeys ([key]) } useEffect (() => { setSelectedKeys ([pathname]) setOpenKeys (getSubMenu (pathname)) }, [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 > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} openKeys = { openKeys } // 打开哪一个菜单项 onOpenChange = { onOpenChange } // 打开某一个菜单项时需要关闭其他菜单项 onClick = { changeUrl } selectedKeys = { selectedKeys } > { renderMenu(menus) } </Menu > </Sider > ) }; export default SideBar ;
11.快捷切换页
系统默认路由为系统首页,所以第一个就为系统首页,且系统首页不可关闭 切换路由,判断当前页面是否已存在,如果存在,找到列表项的索引值,设置该索引值选中效果,并且页面切换至该索引值 如果当前路由对应的页面不存在,则在最后添加一项新的数据,并且设置最后一项为选中项 11.1 准备组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React , { FC } from 'react' ;interface IAppTabsProps { }; const AppTabs :FC <IAppTabsProps > = () => { return ( <div style ={ { 'backgroundColor ': '#f66 ', 'height ': 40 } }> </div > ) }; export default AppTabs ;
1 2 3 4 5 6 export { default as SideBar } from './SideBar' export { default as AppHeader } from './AppHeader' export { default as AppMain } from './AppMain' export { default as AppBreadcrumb } from './AppBreadcrumb' export { default as AppTabs } from './AppTabs'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { Layout } from 'antd' ;import React from 'react' ;import { AppHeader , AppMain , AppTabs , SideBar } from './components' const App : React .FC = () => { return ( <Layout id ='admin-app' > {/* 左侧菜单栏 */} <SideBar /> <Layout className ="site-layout" > {/* 头部 */} <AppHeader /> {/* 快捷切换组件 */} <AppTabs /> {/* 内容 */} <AppMain /> </Layout > </Layout > ); }; export default App ;
11.2 处理数据 后期 监听地址栏 从tabsArr 中提取数据
1 2 3 4 5 6 7 8 9 10 const tabsArr = [{"label" :"系统首页" ,"key" :"/" },{"label" :"首页轮播图" ,"key" :"/banner/home" }, {"label" :"活动页轮播图" ,"key" :"/banner/active" }, {"label" :"首页产品列表" ,"key" :"/pro/list/home" }, {"label" :"详情推荐列表" ,"key" :"/pro/list/detail" }, {"label" :"购物车推荐列表" ,"key" :"/pro/list/cart" }, {"label" :"筛选列表" ,"key" :"/pro/search" }, {"label" :"用户列表" ,"key" :"/account/userlist" }, {"label" :"管理员列表" ,"key" :"/account/adminlist" }, {"label" :"设置" ,"key" :"/setting" }]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 import { Menu } from 'antd' ;import { IMenuProps } from './inter' ;import { Navigate , Route } from 'react-router-dom' import React from 'react' ;export const renderMenu = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return ( <Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu > ) } else { return <Menu.Item key = { item.key } icon = { item.icon }> { item.label }</Menu.Item > } }) } export const renderRoute = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { if (!!item.index ) { return ( <React.Fragment key ={item.key} > <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > ) } } else { if (!!item.index ) { return ( <React.Fragment key = { item.key }> <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> </Route > ) } } }) } export const getSubMenu = (pathname: string ) => { const pathArray = pathname.split ('/' ).slice (1 ) const result = pathArray.reduce ((newArr, item, index ) => { const str = newArr[index] + '/' + item newArr.push (str) return newArr }, ['' ]) return result } export interface ITabProps { label : string key : string } const tabsArr : ITabProps [] = []export const getTabsData = (menus: IMenuProps[] ) => { menus.forEach (item => { if (item.children ) { getTabsData (item.children ) } else { tabsArr.push ({ label : item.label , key : item.key }) } }) return tabsArr }
11.3 监听路由添加数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import menus from '@/router/menu' ;import { getTabsData } from '@/router/utils' ;import { Tag } from 'antd' import React , { FC , useState, useEffect } from 'react' ;import { useLocation } from 'react-router-dom' ;interface IAppTabsProps { }; const tabArrs = getTabsData (menus)console .log (tabArrs)const AppTabs :FC <IAppTabsProps > = () => { const { pathname } = useLocation () const [arr, setArr] = useState ([{ label : '系统首页' , key : '/' }]) const [current, setCurrent] = useState (0 ) useEffect (() => { const index = arr.findIndex (item => item.key === pathname) if (index === -1 ) { const item = tabArrs.find (item => item.key === pathname) const newArr = arr item && newArr.push (item) setArr (newArr) setCurrent (arr.length - 1 ) } else { setCurrent (index) } }, [pathname, arr]) return ( <div style ={ { 'backgroundColor ': '#fff ', 'height ': 40 , 'borderTop ': '1px solid #ccc ', 'borderBottom ': '1px solid #ccc ', 'overflow ': 'auto ', 'whiteSpace ': 'nowrap ' // 不换行 滚动条 } }> { arr && arr.map((item, index) => { return ( <Tag key = { item.key } style ={ { height: 36 , lineHeight: '36px ', marginTop: 2 , marginLeft: 5 , color: current === index ? '#fff ' : '#333 ' } } color = { current === index ? '#108ee9 ' : '#efefef ' } > { item.label } </Tag > ) }) } </div > ) }; export default AppTabs ;
11.4 点击tab页切换路由,关闭效果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import menus from '@/router/menu' ;import { getTabsData } from '@/router/utils' ;import { Tag } from 'antd' import React , { FC , useState, useEffect } from 'react' ;import { useLocation, useNavigate } from 'react-router-dom' ;interface IAppTabsProps { }; const tabArrs = getTabsData (menus)console .log (tabArrs)const AppTabs :FC <IAppTabsProps > = () => { const { pathname } = useLocation () const [arr, setArr] = useState ([{ label : '系统首页' , key : '/' }]) const [current, setCurrent] = useState (0 ) useEffect (() => { const index = arr.findIndex (item => item.key === pathname) if (index === -1 ) { const item = tabArrs.find (item => item.key === pathname) const newArr = arr item && newArr.push (item) setArr (newArr) setCurrent (arr.length - 1 ) } else { setCurrent (index) } }, [pathname, arr]) const navigate = useNavigate () return ( <div style ={ { 'backgroundColor ': '#fff ', 'height ': 36 , 'borderTop ': '1px solid #ccc ', 'borderBottom ': '1px solid #ccc ', 'overflow ': 'auto ', 'whiteSpace ': 'nowrap ' // 不换行 滚动条 } }> { arr && arr.map((item, index) => { return ( <Tag key = { item.key } style ={ { height: 32 , lineHeight: '32px ', marginTop: 2 , marginLeft: 5 , color: current === index ? '#fff ' : '#333 ', cursor: 'pointer ' } } color = { current === index ? '#108ee9 ' : '#efefef ' } onClick = { () => {// ++++++ navigate(item.key, { replace: false }) setCurrent(index) }} closable = { current === index && index !== 0 } // ++++++ onClose = { (e) => { // ++++++ e.preventDefault() arr.splice(index, 1) setArr(arr) setCurrent(index - 1) navigate(arr[current - 1].key, { replace: false }) }} > { item.label } </Tag > ) }) } </div > ) }; export default AppTabs ;
12.数据请求的封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import { message } from 'antd' import axios, { AxiosRequestConfig } from 'axios' import store from 'store2' const isDev = process.env .NODE_ENV === 'development' const instance = axios.create ({ baseURL : isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin' , timeout : 6000 }) instance.interceptors .request .use ((config ) => { const storeUsers = store.get ('haigou-users' ) config.headers !.token = storeUsers && (storeUsers['X-Token' ] || '' ) return config }, (error ) => Promise .reject (error)) instance.interceptors .response .use ((response: any ) => { if (response.data .code === '10119' ) { message.warning ('登录失效,请重新登录' ); store.remove ('haigou-users' ) window .location .href = '/login' return } return response }, (error ) => Promise .reject (error)) export default function request ( config: AxiosRequestConfig ) { const { url = '' , method = 'GET' , data = {}, headers = {} } = config switch (method.toUpperCase ()) { case 'GET' : return instance.get (url, { params : data }) case 'POST' : if (headers['content-type' ] === 'application/x-www-form-url-encoded' ) { const p = new URLSearchParams () for (const key in data) { p.append (key, data[key]) } return instance.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 instance.post (url, p, { headers }) } return instance.post (url, data) case 'PUT' : return instance.put (url, data) case 'DELETE' : return instance.delete (url, { data }) case 'PATCH' : return instance.patch (url, data) default : return instance (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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import request from '@/utils/request' export interface IAdminParams { adminname : string password : string } export function adminLoginFn (params : IAdminParams ) { return request ({ url : '/admin/login' , method : 'POST' , data : params }) }
13.3 创建登录的页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface ILoginProps { }; const Login :FC <ILoginProps > = () => { return ( <> <h1 > 登录页面</h1 > </> ) }; export default Login ;
13.4 创建登录路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React , { FC } from 'react' ;import Index from '@/layout/Index' import Login from '@/views/login/Index' import { Routes , Route } from 'react-router-dom' interface IAppProps {}; const App :FC <IAppProps > = () => { return ( <Routes > <Route path ="/login" element = { <Login /> } /> <Route path ="/*" element = { <Index /> } /> </Routes > ) }; export default App ;
地址栏访问 http://localhost:3000/login
即可看到登录页面出现,其余路由还保持和之前一致
13.4 完善登录界面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import { LockOutlined , UserOutlined } from '@ant-design/icons' ;import { Button , Form , Input } from 'antd' ;import React from 'react' ;import './style.less' ;const App : React .FC = () => { const onFinish = (values: any ) => { console .log ('Received values of form: ' , values); }; return ( <div id ='loginForm' > <div className ='loginBox' > <h1 > 嗨购管理系统</h1 > <Form name ="normal_login" className ="login-form" initialValues ={{ // 表单自动填充的字段 adminname: 'admin ', password: '123456 ' }} onFinish ={onFinish} > <Form.Item name ="adminname" rules ={[{ required: true , message: '请输入管理员账户 !' }]} > <Input prefix ={ <UserOutlined className ="site-form-item-icon" /> } placeholder="账户" /> </Form.Item > <Form.Item name ="password" rules ={[{ required: true , message: '请输入密码 !' }]} > <Input prefix ={ <LockOutlined className ="site-form-item-icon" /> } type="password" placeholder="密码" /> </Form.Item > <Form.Item > <Button type ="primary" htmlType ="submit" className ="login-form-button" > 登 录 </Button > </Form.Item > </Form > </div > </div > ); }; export default App ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #loginForm { height : 100% ; display : flex; flex-direction : column; justify-content : center; align-items : center; background : url(https://cas.1000phone.net/cas/images/login/bg.png ) no-repeat center center; background-size : cover; .loginBox { width : 400px ; min-height : 300px ; background-color : #fff ; border-radius : 30px ; display : flex; flex-direction : column; justify-content : center; align-items : center; padding : 50px 0 ; .login-form { width : 300px ; } } } #loginForm .login-form-forgot { float : right; } #loginForm .ant-col-rtl .login-form-forgot { float : left; } #loginForm .login-form-button { width : 100% ; }
使用状态管理器,异步操作可以在组件,也可以在状态管理器
14 执行登录 使用状态管理器(RTK)管理登录信息。
14.1 构建模块 admins 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import { createSlice, PayloadAction } from '@reduxjs/toolkit' import store from 'store2' export interface IAdminState { loginState : boolean adminname : string token : string role : number checkedKeys : any [] } const initialState : IAdminState = { loginState : store.get ('haigou-users' ) ? store.get ('haigou-users' )['loginState' ] : false , adminname : store.get ('haigou-users' ) ? store.get ('haigou-users' )['adminname' ] : '' , token : store.get ('haigou-users' ) ? store.get ('haigou-users' )['X-Token' ] : '' , role : store.get ('haigou-users' ) ? store.get ('haigou-users' )['role' ] : 0 , checkedKeys : store.get ('haigou-users' ) ? store.get ('haigou-users' )['checkedKeys' ] : [] } const adminSlice = createSlice ({ name : 'admins' , initialState, reducers : { changeLoginState (state, action : PayloadAction <boolean >) { state.loginState = action.payload }, changeAdminName (state, action : PayloadAction <string >) { state.adminname = action.payload }, changeToken (state, action : PayloadAction <string >) { state.token = action.payload }, changeRole (state, action : PayloadAction <number >) { state.role = action.payload }, changecheckedKeys (state, action : PayloadAction <any []>) { state.checkedKeys = action.payload } } }) export const { changeLoginState, changeAdminName, changeToken, changeRole, changecheckedKeys } = adminSlice.actions export default adminSlice.reducer
14.2 装载模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { configureStore } from '@reduxjs/toolkit' ;import appReducer from './modules/app' import adminReducer from './modules/admins' const store = configureStore ({ reducer : { app : appReducer, admins : adminReducer } }); export type RootState = ReturnType <typeof store.getState > export type AppDispatch = typeof store.dispatch export default store;
14.3 登录实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import { IAdminParams , adminLoginFn } from '@/api/admin' ;import { LockOutlined , UserOutlined } from '@ant-design/icons' ;import { Button , Form , Input , message } from 'antd' ;import store from 'store2' import React from 'react' ;import './style.less' ;import { useAppDispatch } from '@/store/hook' ;import { changeAdminName, changecheckedKeys, changeLoginState, changeRole, changeToken } from '@/store/modules/admins' ;import { useNavigate } from 'react-router-dom' ;const App : React .FC = () => { const dispatch = useAppDispatch () const navigate = useNavigate () const onFinish = (values: IAdminParams ) => { adminLoginFn (values).then (res => { console .log (res.data ) switch (res.data .code ) { case '10003' : message.error ('密码错误' ) break ; case '10005' : message.error ('没有该账户,请联系超管' ) break ; default : message.success ('登录成功' ) const result = res.data .data const haigouUsers = { loginState : true , 'X-Token' : result.token , adminname : result.adminname , role : result.role , checkedKeys : result.checkedkeys } store.set ('haigou-users' , haigouUsers) dispatch (changeLoginState (true )) dispatch (changeAdminName (result.adminname )) dispatch (changeToken (result.token )) dispatch (changeRole (result.role )) dispatch (changecheckedKeys (result.checkedkeys )) navigate ('/' , { replace : true }) break ; } }) }; return ( <div id ='loginForm' > <div className ='loginBox' > <h1 > 嗨购管理系统</h1 > <Form name ="normal_login" className ="login-form" initialValues ={{ // 表单自动填充的字段 adminname: 'admin ', password: '123456 ' }} onFinish ={onFinish} > <Form.Item name ="adminname" rules ={[{ required: true , message: '请输入管理员账户 !' }]} > <Input prefix ={ <UserOutlined className ="site-form-item-icon" /> } placeholder="账户" /> </Form.Item > <Form.Item name ="password" rules ={[{ required: true , message: '请输入密码 !' }]} > <Input prefix ={ <LockOutlined className ="site-form-item-icon" /> } type="password" placeholder="密码" /> </Form.Item > <Form.Item > <Button type ="primary" htmlType ="submit" className ="login-form-button" > 登 录 </Button > </Form.Item > </Form > </div > </div > ); }; export default App ;
15.前端登录验证 当前路由在登录页面,判断用户的登录状态,如果登录,则跳转到系统的首页,如果未登录,显示登录页面
当前路由在非登录页面,判断用户的登录状态,如果登录,则显示非登录页面,如果未登录,跳转到登录页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import React , { FC } from 'react' ;import Index from '@/layout/Index' import Login from '@/views/login/Index' import { Routes , Route , Navigate } from 'react-router-dom' import { useAppSelector } from './store/hook' ;interface IAppProps {}; const App :FC <IAppProps > = () => { const loginState = useAppSelector (state => state.admins .loginState ) return ( <Routes > <Route path ="/login" element = { loginState ? <Navigate to ="/" /> : <Login /> } /> <Route path ="/*" element = { loginState ? <Index /> : <Navigate to ="/login" /> } /> </Routes > ) }; export default App ;
16 .后端token校验 封装axios时已经实现 — 响应拦截器
后台管理系统都需要请求数据,而请求数据 都需要添加token字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 .... instance.interceptors .request .use ((config ) => { const storeUsers = store.get ('haigou-users' ) config.headers !.token = storeUsers && (storeUsers['X-Token' ] || '' ) return config }, (error ) => Promise .reject (error)) instance.interceptors .response .use ((response: any ) => { if (response.data .code === '10119' ) { message.warning ('登录失效,请重新登录' ); store.remove ('haigou-users' ) window .location .href = '/login' return } return response }, (error ) => Promise .reject (error)) ....
17.退出登录 17.1 实现退出登录 https://ant-design.gitee.io/components/dropdown-cn/#components-dropdown-demo-trigger
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import React , { FC } from 'react' ;import { MenuFoldOutlined , MenuUnfoldOutlined , DownOutlined } from '@ant-design/icons' ; import { Layout , Dropdown , Space , Menu } from 'antd' ;import { useAppDispatch, useAppSelector } from '@/store/hook' ;import { setCollapsed } from '@/store/modules/app' import AppBreadcrumb from './AppBreadcrumb' ;import { useNavigate } from 'react-router-dom' ;import store from 'store2' ;import { changeLoginState } from '@/store/modules/admins' ;const { Header } = Layout ;interface IAppHeaderProps {}; const AppHeader :FC <IAppHeaderProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ) const dispatch = useAppDispatch () const navigate = useNavigate () const menu = ( <Menu onClick ={ ({key }) => { console.log(key) if (key === '/setting') { navigate(key) } else if (key === 'logout'){ store.remove('haigou-users') dispatch(changeLoginState(false)) navigate('/login') } else { // 其余选项 } }} items={[ { label: '设置', key: '/setting', }, { type: 'divider', }, { label: '退出', key: 'logout', }, ]} /> ); const adminname = useAppSelector (state => state.admins .adminname ) return ( <Header className ="site-layout-background" style ={{ padding: 0 , display: 'flex ' }}> {/* {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => { dispatch(setCollapsed(!collapsed)) } })} */} <div > { collapsed ? <MenuUnfoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> : <MenuFoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> } </div > <AppBreadcrumb /> <div style ={{ position: 'absolute ', right: '16px ' }}> <Dropdown overlay ={menu} trigger ={[ 'click ']}> <span > <Space > { adminname } <DownOutlined /> </Space > </span > </Dropdown > </div > </Header > ) }; export default AppHeader ;
17.2 保留退出时的页面 先获取退出登陆时 路由的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import React , { FC } from 'react' ;import { MenuFoldOutlined , MenuUnfoldOutlined , DownOutlined } from '@ant-design/icons' ; import { Layout , Dropdown , Space , Menu } from 'antd' ;import { useAppDispatch, useAppSelector } from '@/store/hook' ;import { setCollapsed } from '@/store/modules/app' import AppBreadcrumb from './AppBreadcrumb' ;import { useLocation, useNavigate } from 'react-router-dom' ;import store from 'store2' ;import { changeLoginState } from '@/store/modules/admins' ;const { Header } = Layout ;interface IAppHeaderProps {}; const AppHeader :FC <IAppHeaderProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ) const dispatch = useAppDispatch () const navigate = useNavigate () const { pathname } = useLocation () const menu = ( <Menu onClick ={ ({key }) => { console.log(key) if (key === '/setting') { navigate(key) } else if (key === 'logout'){ store.remove('haigou-users') dispatch(changeLoginState(false)) navigate('/login?r=' + pathname) // +++++++++ } else { // 其余选项 } }} items={[ { label: '设置', key: '/setting', }, { type: 'divider', }, { label: '退出', key: 'logout', }, ]} /> ); const adminname = useAppSelector (state => state.admins .adminname ) return ( <Header className ="site-layout-background" style ={{ padding: 0 , display: 'flex ' }}> {/* {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => { dispatch(setCollapsed(!collapsed)) } })} */} <div > { collapsed ? <MenuUnfoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> : <MenuFoldOutlined className ="trigger" onClick ={ () => { dispatch(setCollapsed(!collapsed)) }}/> } </div > <AppBreadcrumb /> <div style ={{ position: 'absolute ', right: '16px ' }}> <Dropdown overlay ={menu} trigger ={[ 'click ']}> <span > <Space > { adminname } <DownOutlined /> </Space > </span > </Dropdown > </div > </Header > ) }; export default AppHeader ;
正常考虑问题思路是,在登陆时,登录成功之后 判断有没有退出时的记录地址,然后跳转
但实际上程序运行的思路是,放你登录成功之后,已经修改了登录状态,状态的改变引起视图的二次渲染,所以真正决定跳转地址的是App.tsx组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import React , { FC } from 'react' ;import Index from '@/layout/Index' import Login from '@/views/login/Index' import { Routes , Route , Navigate , useSearchParams } from 'react-router-dom' import { useAppSelector } from './store/hook' ;interface IAppProps {}; const App :FC <IAppProps > = () => { const loginState = useAppSelector (state => state.admins .loginState ) const [params] = useSearchParams () const url = params.get ('r' ) as string console .log (url) return ( <Routes > <Route path ="/login" element = { loginState ? <Navigate to ={ url ? url : '/' }/> : <Login /> } /> <Route path ="/*" element = { loginState ? <Index /> : <Navigate to ="/login" /> } /> </Routes > ) }; export default App ;
18.隐藏子菜单 设置路由已经放到头部,左侧菜单栏不需要显示设置选项了
给router/menu.tsx中不需要出现的 添加 hidden
给添加轮播图以及设置选项添加 hidden 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 import { ReactNode } from 'react' export interface IMenuProps { label : string key : string icon?: ReactNode children?: IMenuProps [] element : ReactNode index?: 1 | 0 hidden?: 1 | 0 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , icon : <UploadOutlined /> , element : <BannerAdd /> , hidden : 1 }, ] }, { label : '产品管理' , key : '/pro' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , icon : <SettingOutlined /> , element : <Setting /> , hidden : 1 ++++++ } ] export default menus
渲染左侧菜单栏数据时,可以过滤数据,将有hidden: 1
子菜单删除掉
4.20.0以前写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 import { Menu } from 'antd' ;import { IMenuProps } from './inter' ;import { Navigate , Route } from 'react-router-dom' import React from 'react' ;export const renderMenu = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { return item.hidden ? null : (<Menu.SubMenu title = { item.label } icon = { item.icon } key = { item.key }> { renderMenu(item.children) } </Menu.SubMenu > ) } else { return item.hidden ? null : <Menu.Item key = { item.key } icon = { item.icon }> { item.label }</Menu.Item > } }) } export const renderRoute = (menus: IMenuProps[] ) => { return menus.map (item => { if (item.children ) { if (!!item.index ) { return ( <React.Fragment key ={item.key} > <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> { renderRoute(item.children) } </Route > ) } } else { if (!!item.index ) { return ( <React.Fragment key = { item.key }> <Route index element = { <Navigate to ={item.key} replace = { true } /> } /> <Route path = { item.key } element = { item.element }> </Route > </React.Fragment > ) } else { return ( <Route key = { item.key } path = { item.key } element = { item.element }> </Route > ) } } }) } export const getSubMenu = (pathname: string ) => { const pathArray = pathname.split ('/' ).slice (1 ) const result = pathArray.reduce ((newArr, item, index ) => { const str = newArr[index] + '/' + item newArr.push (str) return newArr }, ['' ]) return result } export interface ITabProps { label : string key : string } const tabsArr : ITabProps [] = []export const getTabsData = (menus: IMenuProps[] ) => { menus.forEach (item => { if (item.children ) { getTabsData (item.children ) } else { tabsArr.push ({ label : item.label , key : item.key }) } }) return tabsArr }
4.20.0以上写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import React , { FC , useState, useEffect } from 'react' ;import { Layout , Menu , Image , MenuProps } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { getSubMenu } from '@/router/utils' import { useLocation, useNavigate } from 'react-router-dom' ;interface ISideBarProps { }; const { Sider } = Layout ;const rootSubmenuKeys : string [] = [];menus.forEach (item => { item.children && rootSubmenuKeys.push (item.key ) }) const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); const { pathname } = useLocation () const [selectedKeys, setSelectedKeys] = useState ([pathname]) const [openKeys, setOpenKeys] = useState<string []>(getSubMenu (pathname)) 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] : []); } } const navigate = useNavigate () const changeUrl = ({ key }: { key: string } ) => { navigate (key, { replace : false }) setSelectedKeys ([key]) } useEffect (() => { setSelectedKeys ([pathname]) setOpenKeys (getSubMenu (pathname)) }, [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 > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} openKeys = { openKeys } // 打开哪一个菜单项 onOpenChange = { onOpenChange } // 打开某一个菜单项时需要关闭其他菜单项 onClick = { changeUrl } selectedKeys = { selectedKeys } items = { menus } > </Menu > </Sider > ) }; export default SideBar ;
隐藏子菜单使用 hidden 属性,如果使用的不是hidden属性,那么需要自行过滤数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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.设计接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import request from '@/utils/request' export interface IAdminParams { adminname : string password : string } export interface IAdminAddParams { adminname : string token : string role : number checkedKeys : any [] } export function adminLoginFn (params : IAdminParams ) { return request ({ url : '/admin/login' , method : 'POST' , data : params }) } export function getAdminList () { return request ({ url : '/admin/list' }) } export function getAdminDetail (params : { adminname : string }) { return request ({ url : '/admin/detail' , data : params }) } export function addAdmin (params : IAdminAddParams ) { return request ({ url : '/admin/add' , data : params, method : 'POST' }) } export function deleteAdmin (params : { adminid : string }) { return request ({ url : '/admin/delete' , method : 'POST' , data : params }) } export function updateAdmin (params : IAdminAddParams ) { return request ({ url : '/admin/update' , method : 'POST' , data : params }) }
19.2.展示管理员列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import React , { FC , useEffect, useState } from 'react' ;import { Table , Space , Button , Tag } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import { getAdminList } from '@/api/admin' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Space > ) } } ] return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Button type ='primary' > 添加管理员</Button > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } ></Table > </Space > ) }; export default Index ;
19.3 优化表格滚动 如果屏幕比较小,默认展示的都是10条数据,就容易超出固定容器大小,此时可以通过 限制表格的滚动属性解决问题
第三方hooks库 https://ahooks.js.org/zh-CN/
???? ahooks的useSize获取到的是 undefined
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 import React , { FC , useEffect, useState } from 'react' ;import { Table , Space , Button , Tag } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import { getAdminList } from '@/api/admin' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const [height] = useState (document .body .offsetHeight ) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Space > ) } } ] return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Button type ='primary' > 添加管理员</Button > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} ></Table > </Space > ) }; export default Index ;
19.4 优化表格的分页器 优化数据表格 (分页器优化 - 序号-分页之后需要要连贯)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import React , { FC , useEffect, useState } from 'react' ;import { Table , Space , Button , Tag } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import { getAdminList } from '@/api/admin' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const [current, setCurrent] = useState (1 ) const [pageSize, setPageSize] = useState (10 ) const [height] = useState (document .body .offsetHeight ) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (current - 1) * pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Space > ) } } ] return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Button type ='primary' > 添加管理员</Button > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = {{ // +++++++ position: ['bottomRight'], current: current, pageSize: pageSize, showSizeChanger: true, pageSizeOptions: [5, 10, 50, 200], showQuickJumper: true, showTotal: (total) => { return (<span > 共有{total}条数据</span > ) }, onChange: (page, pageSize) => { setCurrent(page) setPageSize(pageSize) } }} ></Table > </Space > ) }; export default Index ;
19.5 添加中文包 由于 antd 组件的默认文案是英文,所以需要修改为中文
https://ant-design.gitee.io/docs/react/getting-started-cn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from 'react' ;import ReactDOM from 'react-dom/client' ;import { Provider } from 'react-redux' ;import store from '@/store' import '@/index.less' ;import App from '@/App' ;import reportWebVitals from '@/reportWebVitals' ;import { BrowserRouter } from 'react-router-dom' import { ConfigProvider } from 'antd' ; import zhCN from 'antd/es/locale/zh_CN' ;const root = ReactDOM .createRoot ( document .getElementById ('root' ) as HTMLDivElement ); root.render ( <React.StrictMode > <Provider store = { store }> <BrowserRouter > { /*++++++++++*/} <ConfigProvider locale ={zhCN} > <App /> </ConfigProvider > </BrowserRouter > </Provider > </React.StrictMode > ); reportWebVitals ();
19.6 抽离分页器 可以抽离,但不是必须
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { JSXElementConstructor , ReactElement , ReactFragment , ReactPortal , SetStateAction , useState } from 'react' export default function usePagination (config: any ) { let { position, showSizeChanger, pageSizeOptions, showQuickJumper } = config const [current, setCurrent] = useState (1 ) const [pageSize, setPageSize] = useState (10 ) const onChange = (page: SetStateAction<number >, pageSize: SetStateAction<number > ) => { setCurrent (page) setPageSize (pageSize) } const showTotal = (total: string | number | boolean | ReactElement<any , string | JSXElementConstructor<any >> | ReactFragment | ReactPortal | null | undefined ) => { return (<span > 共有{total}条数据</span > ) } return { position, current, pageSize, showSizeChanger, pageSizeOptions, showQuickJumper, showTotal, onChange } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 import React , { FC , useEffect, useState } from 'react' ;import { Table , Space , Button , Tag } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import { getAdminList } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height] = useState (document .body .offsetHeight ) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Space > ) } } ] return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Button type ='primary' > 添加管理员</Button > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } ></Table > </Space > ) }; export default Index ;
19.7删除管理员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import React , { FC , useEffect, useState } from 'react' ;import { Table , Space , Button , Tag , Popconfirm } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import { deleteAdmin, getAdminList } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height] = useState (document .body .offsetHeight ) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > {/* +++++++++++++ */} <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Button type ='primary' > 添加管理员</Button > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } ></Table > </Space > ) }; export default Index ;
19.8 如何批量删除管理员数据 https://ant-design.gitee.io/components/table-cn/#components-table-demo-row-selection-custom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 import React , { FC , useEffect, useState, useMemo } from 'react' ;import { Table , Space , Button , Tag , Popconfirm } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import type { TableRowSelection } from 'antd/es/table/interface' ;import { deleteAdmin, getAdminList } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height] = useState (document .body .offsetHeight ) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] const [selectedRowKeys, setSelectedRowKeys] = useState<React .Key []>([]); const onSelectChange = (newSelectedRowKeys: React.Key[] ) => { console .log ('selectedRowKeys changed: ' , newSelectedRowKeys); setSelectedRowKeys (newSelectedRowKeys); }; const rowSelection : TableRowSelection <DataType > = { selectedRowKeys, onChange : onSelectChange, } const flag = useMemo (() => { return selectedRowKeys.length > 0 }, [selectedRowKeys]) return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Space > <Button type ='primary' > 添加管理员</Button > { flag && <Button type ='primary' onClick ={ () => { // ++++++++++ // 思考: 还用气泡弹出框? 自行选择删除的方案 // 接口没有提供批量删除,只能使用单个删除解决 // 每个删除都是基于 Promise, 综合考虑 Promise.all const arr: any = [] selectedRowKeys.forEach((item: any) => { arr.push(deleteAdmin({ adminid: item})) }) Promise.all(arr).then(() => { getAdminListData() }) }}>批量删除</Button > } </Space > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } rowSelection={rowSelection} ></Table > </Space > ) }; export default Index ;
19.9.添加管理员 19.9.1 设置添加管理员的抽屉效果(无树形控件) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 import React , { FC , useEffect, useState, useMemo } from 'react' ;import { Table , Space , Button , Tag , Popconfirm , Drawer , Input , Select } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import type { TableRowSelection } from 'antd/es/table/interface' ;import { deleteAdmin, getAdminList } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { console .log (66 ) getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] const [selectedRowKeys, setSelectedRowKeys] = useState<React .Key []>([]); const onSelectChange = (newSelectedRowKeys: React.Key[] ) => { console .log ('selectedRowKeys changed: ' , newSelectedRowKeys); setSelectedRowKeys (newSelectedRowKeys); }; const rowSelection : TableRowSelection <DataType > = { selectedRowKeys, onChange : onSelectChange, } const flag = useMemo (() => { return selectedRowKeys.length > 0 }, [selectedRowKeys]) const [open, setOpen] = useState (false ); return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Space > <Button type ='primary' onClick ={ () => setOpen(true) }>添加管理员</Button > { flag && <Button type ='primary' onClick ={ () => { // 思考: 还用气泡弹出框? 自行选择删除的方案 // 接口没有提供批量删除,只能使用单个删除解决 // 每个删除都是基于 Promise, 综合考虑 Promise.all const arr: any = [] selectedRowKeys.forEach((item: any) => { arr.push(deleteAdmin({ adminid: item})) }) Promise.all(arr).then(() => { getAdminListData() }) }}>批量删除</Button > } </Space > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } rowSelection={rowSelection} ></Table > <Drawer title ="添加管理员" placement ="right" onClose ={() => { setOpen(false) }} open={open}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input placeholder ='账户名' /> <Input placeholder ='密码' /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: </Space > </Drawer > </Space > ) }; export default Index ;
19.9.2 修改菜单数据 添加了keyid字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { ReactNode } from 'react' export interface IMenuProps { label : string key : string keyid : string icon?: ReactNode children?: IMenuProps [] element : ReactNode index?: 1 | 0 hidden?: 1 | 0 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , keyid : '0-0' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , keyid : '0-1' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , keyid : '0-1-0' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , keyid : '0-1-1' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , keyid : '0-1-2' , icon : <UploadOutlined /> , element : <BannerAdd /> , hidden : 1 }, ] }, { label : '产品管理' , key : '/pro' , keyid : '0-2' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , keyid : '0-2-0' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , keyid : '0-2-0-0' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , keyid : '0-2-0-1' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , keyid : '0-2-0-2' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , keyid : '0-2-1' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , keyid : '0-3' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , keyid : '0-3-1' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , keyid : '0-3-2' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , keyid : '0-4' , icon : <SettingOutlined /> , element : <Setting /> , hidden : 1 } ] export default menus
19.9.3 添加管理员时选择该管理员权限 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import type { DataNode } from 'antd/es/tree' ;export function getTreeData (menus : IMenuProps []) { 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 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 import React , { FC , useEffect, useState, useMemo } from 'react' ;import { Table , Space , Button , Tag , Popconfirm , Drawer , Input , Select , Tree } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import type { TableRowSelection } from 'antd/es/table/interface' ;import type { DataNode } from 'antd/es/tree' ;import { addAdmin, deleteAdmin, getAdminList } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;import menus from '@/router/menu' ;import { getTreeData } from '@/router/utils' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const treeData : DataNode [] = getTreeData (menus) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { console .log (66 ) getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] const [selectedRowKeys, setSelectedRowKeys] = useState<React .Key []>([]); const onSelectChange = (newSelectedRowKeys: React.Key[] ) => { console .log ('selectedRowKeys changed: ' , newSelectedRowKeys); setSelectedRowKeys (newSelectedRowKeys); }; const rowSelection : TableRowSelection <DataType > = { selectedRowKeys, onChange : onSelectChange, } const flag = useMemo (() => { return selectedRowKeys.length > 0 }, [selectedRowKeys]) const [adminname, setAdminname] = useState ('' ) const [password, setPassword] = useState ('' ) const [role, setRole] = useState (1 ) const [open, setOpen] = useState (false ); const [checkedKeys, setcheckedKeys] = useState (['0-0' ]) const onAdd = ( ) => { const result : any = { adminname, password, role, checkedKeys } console .log (result) } return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Space > <Button type ='primary' onClick ={ () => setOpen(true) }>添加管理员</Button > { flag && <Button type ='primary' onClick ={ () => { // 思考: 还用气泡弹出框? 自行选择删除的方案 // 接口没有提供批量删除,只能使用单个删除解决 // 每个删除都是基于 Promise, 综合考虑 Promise.all const arr: any = [] selectedRowKeys.forEach((item: any) => { arr.push(deleteAdmin({ adminid: item})) }) Promise.all(arr).then(() => { getAdminListData() }) }}>批量删除</Button > } </Space > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } rowSelection={rowSelection} ></Table > <Drawer title ="添加管理员" placement ="right" onClose ={() => { setOpen(false) }} open={open}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input placeholder ='账户名' value = { adminname } onChange = { e => setAdminname(e.target.value) } /> <Input placeholder ='密码' value = { password } onChange = { e => setPassword(e.target.value) } /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}} value = { role } onChange = { (value ) => setRole(value)}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: <Tree checkable checkedKeys = { checkedKeys } onCheck = { (checkedKeys: any ) => { setcheckedKeys(checkedKeys) }} treeData={ treeData } /> <Button type ="primary" onClick ={ onAdd }> 添加管理员</Button > </Space > </Drawer > </Space > ) }; export default Index ;
19.9.4 添加管理员 添加完毕一定要记得重置(表单,权限)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 import React , { FC , useEffect, useState, useMemo } from 'react' ;import { Table , Space , Button , Tag , Popconfirm , Drawer , Input , Select , Tree , message } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import type { TableRowSelection } from 'antd/es/table/interface' ;import type { DataNode } from 'antd/es/tree' ;import { addAdmin, deleteAdmin, getAdminList } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;import menus from '@/router/menu' ;import { getTreeData } from '@/router/utils' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const treeData : DataNode [] = getTreeData (menus) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> }></Button > <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] const [selectedRowKeys, setSelectedRowKeys] = useState<React .Key []>([]); const onSelectChange = (newSelectedRowKeys: React.Key[] ) => { console .log ('selectedRowKeys changed: ' , newSelectedRowKeys); setSelectedRowKeys (newSelectedRowKeys); }; const rowSelection : TableRowSelection <DataType > = { selectedRowKeys, onChange : onSelectChange, } const flag = useMemo (() => { return selectedRowKeys.length > 0 }, [selectedRowKeys]) const [open, setOpen] = useState (false ); const [adminname, setAdminname] = useState ('' ) const [password, setPassword] = useState ('' ) const [role, setRole] = useState (1 ) const [checkedKeys, setcheckedKeys] = useState (['0-0' ]) const onAdd = ( ) => { const result : any = { adminname, password, role, checkedKeys } addAdmin (result).then ((res ) => { console .log (res.data ) if (res.data .code === '10004' ) { message.warning ('该管理员账户已存在' ) } else if (res.data .code === '10008' ) { message.warning ('账户名不符合规则' ) } else { message.success ('添加成功' ) getAdminListData () setAdminname ('' ) setPassword ('' ) setRole (1 ) setcheckedKeys (['0-0' ]) setOpen (false ) } }) } const dis = useMemo (() => { return adminname === '' || password === '' }, [adminname, password]) return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Space > <Button type ='primary' onClick ={ () => setOpen(true) }>添加管理员</Button > { flag && <Button type ='primary' onClick ={ () => { // 思考: 还用气泡弹出框? 自行选择删除的方案 // 接口没有提供批量删除,只能使用单个删除解决 // 每个删除都是基于 Promise, 综合考虑 Promise.all const arr: any = [] selectedRowKeys.forEach((item: any) => { arr.push(deleteAdmin({ adminid: item})) }) Promise.all(arr).then(() => { getAdminListData() }) }}>批量删除</Button > } </Space > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } rowSelection={rowSelection} ></Table > <Drawer title ="添加管理员" placement ="right" onClose ={() => { setOpen(false) }} open={open}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input placeholder ='账户名' value = { adminname } onChange = { e => setAdminname(e.target.value) } /> <Input placeholder ='密码' value = { password } onChange = { e => setPassword(e.target.value) } /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}} value = { role } onChange = { (value ) => setRole(value)}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: <Tree checkable checkedKeys = { checkedKeys } onCheck = { (checkedKeys: any ) => { setcheckedKeys(checkedKeys) }} treeData={ treeData } /> <Button type ="primary" onClick ={ onAdd } disabled = { dis }> 添加管理员</Button > </Space > </Drawer > </Space > ) }; export default Index ;
19.10管理员修改 修改不重新生成新的页面,还在这个页面,使用模态框实现
可以使用之前的数据字段 —- 需要注意 树形控件的字段 为 checkedKeys 而不是 checkedkeys
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 import React , { FC , useEffect, useState, useMemo } from 'react' ;import { Table , Space , Button , Tag , Popconfirm , Drawer , Input , Select , Tree , message, Modal } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import type { TableRowSelection } from 'antd/es/table/interface' ;import type { DataNode } from 'antd/es/tree' ;import { addAdmin, deleteAdmin, getAdminList, updateAdmin } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;import menus from '@/router/menu' ;import { getTreeData } from '@/router/utils' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const treeData : DataNode [] = getTreeData (menus) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> } onClick = {() => { // 给表单填充数据 setAdminname(record.adminname) setRole(record.role) setCheckedkeys(record.checkedKeys) // 弹出框显示 setIsModalOpen(true) }}></Button > <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] const [selectedRowKeys, setSelectedRowKeys] = useState<React .Key []>([]); const onSelectChange = (newSelectedRowKeys: React.Key[] ) => { console .log ('selectedRowKeys changed: ' , newSelectedRowKeys); setSelectedRowKeys (newSelectedRowKeys); }; const rowSelection : TableRowSelection <DataType > = { selectedRowKeys, onChange : onSelectChange, } const flag = useMemo (() => { return selectedRowKeys.length > 0 }, [selectedRowKeys]) const [open, setOpen] = useState (false ); const [adminname, setAdminname] = useState ('' ) const [password, setPassword] = useState ('' ) const [role, setRole] = useState (1 ) const [checkedKeys, setCheckedkeys] = useState (['0-0' ]) const onAdd = ( ) => { const result : any = { adminname, password, role, checkedKeys : checkedKeys } addAdmin (result).then ((res ) => { console .log (res.data ) if (res.data .code === '10004' ) { message.warning ('该管理员账户已存在' ) } else if (res.data .code === '10008' ) { message.warning ('账户名不符合规则' ) } else { message.success ('添加成功' ) getAdminListData () setAdminname ('' ) setPassword ('' ) setRole (1 ) setCheckedkeys (['0-0' ]) setOpen (false ) } }) console .log (result) } const dis = useMemo (() => { return adminname === '' || password === '' }, [adminname, password]) const [isModalOpen, setIsModalOpen] = useState (false ); const onUpdate = ( ) => { const result : any = { adminname, role, checkedKeys } updateAdmin (result).then (() => { setIsModalOpen (false ) setAdminname ('' ) setRole (1 ) setCheckedkeys (['0-0' ]) getAdminListData () }) } return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Space > <Button type ='primary' onClick ={ () => setOpen(true) }>添加管理员</Button > { flag && <Button type ='primary' onClick ={ () => { // 思考: 还用气泡弹出框? 自行选择删除的方案 // 接口没有提供批量删除,只能使用单个删除解决 // 每个删除都是基于 Promise, 综合考虑 Promise.all const arr: any = [] selectedRowKeys.forEach((item: any) => { arr.push(deleteAdmin({ adminid: item})) }) Promise.all(arr).then(() => { getAdminListData() }) }}>批量删除</Button > } </Space > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } rowSelection={rowSelection} ></Table > <Drawer title ="添加管理员" placement ="right" onClose ={() => { getAdminListData() setAdminname('') setPassword('') setRole(1) setCheckedkeys(['0-0']) setOpen(false) }} open={open}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input placeholder ='账户名' value = { adminname } onChange = { e => setAdminname(e.target.value) } /> <Input placeholder ='密码' value = { password } onChange = { e => setPassword(e.target.value) } /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}} value = { role } onChange = { (value ) => setRole(value)}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: <Tree checkable checkedKeys = { checkedKeys } onCheck = { (checkedKeys: any ) => { setCheckedkeys(checkedKeys) }} treeData={ treeData } /> <Button type ="primary" onClick ={ onAdd } disabled = { dis }> 添加管理员</Button > </Space > </Drawer > <Modal title ="修改管理员" footer ={null} open ={isModalOpen} onOk ={() => {}} onCancel={ () => { setAdminname('') setRole(1) setCheckedkeys(['0-0']) setIsModalOpen(false) }}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input disabled placeholder ='账户名' value = { adminname } onChange = { e => setAdminname(e.target.value) } /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}} value = { role } onChange = { (value ) => setRole(value)}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: <Tree checkable checkedKeys = { checkedKeys } onCheck = { (checkedKeys: any ) => { setCheckedkeys(checkedKeys) }} treeData={ treeData } /> <Button type ="primary" onClick ={ onUpdate } > 修改管理员权限</Button > </Space > </Modal > </Space > ) }; export default Index ;
20 系统首页数据统计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import request from '@/utils/request' export function getUserTotalNum () { return request ({ url : '/statistic/user' }) } export function getShopTotalNum () { return request ({ url : '/statistic/product' }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React , { FC , useState, useEffect } from 'react' ;import { CalculatorOutlined , UsergroupDeleteOutlined } from '@ant-design/icons' ;import { Statistic , Col , Row , } from 'antd' ;import { getShopTotalNum, getUserTotalNum } from '@/api/home' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { const [ userNum, setUserNum ] = useState (0 ) const [ proNum, setProNum ] = useState (0 ) useEffect (() => { getUserTotalNum ().then (res => setUserNum (res.data .data )) getShopTotalNum ().then (res => setProNum (res.data .data )) }, []) return ( <> <Row gutter ={16} > <Col span ={3} > <Statistic title ="商品总数量" value ={proNum} prefix ={ <CalculatorOutlined /> } /> </Col > <Col span ={3} > <Statistic title ="用户总数量" value ={userNum} prefix ={ <UsergroupDeleteOutlined /> } /> </Col > </Row > </> ) }; export default Index ;
21 左侧菜单栏的权限 21.1 思路 当用户登录的时候,可以获取到该用户的 checkedKeys
数据 使用这个数据从 router/menu.tsx
中提取匹配的数据, 生成左侧菜单栏组件(目前是直接渲染router/menu.tsx
) 21.2 算法过程
从一个数组['0-0', '0-1-0', '0-2-0-0', '0-2-0-1', '0-3-2']
触发,筛选 router/menu.tsx
,获取到满足条件的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , keyid : '0-0' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , keyid : '0-1' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , keyid : '0-1-0' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 } ] }, { label : '产品管理' , key : '/pro' , keyid : '0-2' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , keyid : '0-2-0' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , keyid : '0-2-0-0' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , keyid : '0-2-0-1' , element : <ProductDetailList /> }, ] }, ] }, { label : '账户管理' , key : '/account' , keyid : '0-3' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '管理员列表' , key : '/account/adminlist' , keyid : '0-3-2' , icon : <UserOutlined /> , element : <AdminList /> } ] } ] export default menus
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’] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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' ]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’] 提取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { Menu } from 'antd' ;import { IMenuProps } from './inter' ;import { Navigate , Route } from 'react-router-dom' import React from 'react' ;import type { DataNode } from 'antd/es/tree' ;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] } export function getPermissionMenu (menus : IMenuProps [], checkedKeys : string []) { let arr : IMenuProps [] = [] 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’
1 2 3 4 5 6 7 { "compilerOptions" : { "target" : "es2015" , ... } }
21.4 生成动态的左侧菜单项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 import React , { FC , useState, useEffect } from 'react' ;import { Layout , Menu , Image , MenuProps } from 'antd' ;import { useAppSelector } from '@/store/hook' ;import menus from '@/router/menu' import logo from '@/logo.svg' import { getCheckedKeysArr, getPermissionMenu, getSubMenu } from '@/router/utils' import { useLocation, useNavigate } from 'react-router-dom' ;interface ISideBarProps { }; const { Sider } = Layout ;const rootSubmenuKeys : string [] = [];menus.forEach (item => { item.children && rootSubmenuKeys.push (item.key ) }) const SideBar :FC <ISideBarProps > = () => { const collapsed = useAppSelector (state => state.app .collapsed ); const { pathname } = useLocation () const [selectedKeys, setSelectedKeys] = useState ([pathname]) const [openKeys, setOpenKeys] = useState<string []>(getSubMenu (pathname)) 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] : []); } } const navigate = useNavigate () const changeUrl = ({ key }: { key: string } ) => { navigate (key, { replace : false }) setSelectedKeys ([key]) } useEffect (() => { setSelectedKeys ([pathname]) setOpenKeys (getSubMenu (pathname)) }, [pathname]) const adminname = useAppSelector (state => state.admins .adminname ); const checkedKeys = useAppSelector (state => state.admins .checkedKeys ); let newMenus = menus if (checkedKeys.length === 0 && adminname === 'admin' ) { newMenus = menus } else { const newCheckedKeys = getCheckedKeysArr (checkedKeys) newMenus = getPermissionMenu (menus, newCheckedKeys) } return ( <Sider trigger ={null} collapsible collapsed ={collapsed} > <div className ="logo" style ={ { display: 'flex ', justifyContent: 'center ', alignItems: 'center ', color: '#fff ' }}> <Image src = { logo } width ="28px" height ="28px" preview ={ false }> </Image > { !collapsed && <div style ={{ height: '32px ', overflow: 'hidden ', lineHeight: '32px ' }}> 嗨购后台管理系统</div > } </div > <Menu theme ="dark" mode ="inline" defaultSelectedKeys ={[ '1 ']} openKeys = { openKeys } // 打开哪一个菜单项 onOpenChange = { onOpenChange } // 打开某一个菜单项时需要关闭其他菜单项 onClick = { changeUrl } selectedKeys = { selectedKeys } items = { newMenus } > </Menu > </Sider > ) }; export default SideBar ;
有的公司在登录之后,会直接返回类似router/menus.tsx
的数据
22、页面权限 也称之为路由权限
一个有权限访问页面A的人,把整个链接地址发给了没有权限访问的另外一个人
根据数据库中存储的字段,提取当前用户需要的 menus 的数据
如果用户访问的当前路由在 总路由中但是不在当前用户的路由中,显示无权限页面,否则显示404页面
核心思想:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 export function isContainMenus (menus : IMenuProps [], 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 }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import React , { FC } from 'react' ;import { Layout } from 'antd' ;import menus from '@/router/menu' ;import { getCheckedKeysArr, getPermissionMenu, isContainMenus, renderRoute } from '@/router/utils' ;import { Routes , Route , useLocation } from 'react-router-dom' import Page404 from '@/views/error/Page404' ;import { useAppSelector } from '@/store/hook' ;interface IAppMainProps { }; const { Content } = Layout ;const AppMain :FC <IAppMainProps > = () => { const adminname = useAppSelector (state => state.admins .adminname ); const checkedKeys = useAppSelector (state => state.admins .checkedKeys ); let newMenus = menus if (checkedKeys.length === 0 && adminname === 'admin' ) { newMenus = menus } else { const newCheckedKeys = getCheckedKeysArr (checkedKeys) newMenus = getPermissionMenu (menus, newCheckedKeys) } const { pathname } = useLocation () const flag = isContainMenus (menus, pathname) return ( <Content className ="site-layout-background" id = 'test' style ={{ margin: '24px 16px ', padding: 24 , minHeight: 280 , }} > {/* 多个路由需要定义时,包裹在Routes选项下 */} <Routes > { renderRoute(newMenus) } {/* ++++++ 404+++++ */} <Route path ="*" element = { flag ? <div > 无权限</div > : <Page404 /> } /> </Routes > </Content > ) }; export default AppMain ;
23、按钮权限 超级管理员才可以批量删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 import React , { FC , useEffect, useState, useMemo } from 'react' ;import { Table , Space , Button , Tag , Popconfirm , Drawer , Input , Select , Tree , message, Modal } from 'antd' import { EditOutlined , DeleteOutlined } from '@ant-design/icons' import type { ColumnsType } from 'antd/es/table' ;import type { TableRowSelection } from 'antd/es/table/interface' ;import type { DataNode } from 'antd/es/tree' ;import { addAdmin, deleteAdmin, getAdminList, updateAdmin } from '@/api/admin' ;import usePagination from '@/hooks/usePagination' ;import menus from '@/router/menu' ;import { getTreeData } from '@/router/utils' ;import { useAppSelector } from '@/store/hook' ;interface IIndexProps {}; interface DataType { adminid : string adminname : string role : number checkedKeys : any [] } const Index :FC <IIndexProps > = () => { const treeData : DataNode [] = getTreeData (menus) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const [adminList, setAdminList] = useState<DataType []>([]) const getAdminListData = ( ) => { getAdminList ().then (res => { console .log (res.data ) setAdminList (res.data .data ) }) } useEffect (() => { getAdminListData () }, []) const columns : ColumnsType <DataType > = [ { title : '序号' , render (_, record, index) { return (<span > { (config.current - 1) * config.pageSize + index + 1 }</span > ) } }, { title : '账户' , dataIndex : 'adminname' }, { title : '角色' , dataIndex : 'role' , render : (text ) => { return text === 2 ? <Tag color ="cyan" > 超级管理员</Tag > : <Tag > 管理员</Tag > } }, { title : '操作' , render (_, record, index) { return ( <Space > <Button type ='ghost' shape ="circle" icon ={ <EditOutlined /> } onClick = {() => { // 给表单填充数据 setAdminname(record.adminname) setRole(record.role) setCheckedkeys(record.checkedKeys) // 弹出框显示 setIsModalOpen(true) }}></Button > <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteAdmin({ adminid: record.adminid}).then(() => { // 删除完毕重新获取数据 getAdminListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > </Space > ) } } ] const [selectedRowKeys, setSelectedRowKeys] = useState<React .Key []>([]); const onSelectChange = (newSelectedRowKeys: React.Key[] ) => { console .log ('selectedRowKeys changed: ' , newSelectedRowKeys); setSelectedRowKeys (newSelectedRowKeys); }; const rowSelection : TableRowSelection <DataType > = { selectedRowKeys, onChange : onSelectChange, } const flag = useMemo (() => { return selectedRowKeys.length > 0 }, [selectedRowKeys]) const [open, setOpen] = useState (false ); const [adminname, setAdminname] = useState ('' ) const [password, setPassword] = useState ('' ) const [role, setRole] = useState (1 ) const [checkedKeys, setCheckedkeys] = useState (['0-0' ]) const onAdd = ( ) => { const result : any = { adminname, password, role, checkedKeys : checkedKeys } addAdmin (result).then ((res ) => { console .log (res.data ) if (res.data .code === '10004' ) { message.warning ('该管理员账户已存在' ) } else if (res.data .code === '10008' ) { message.warning ('账户名不符合规则' ) } else { message.success ('添加成功' ) getAdminListData () setAdminname ('' ) setPassword ('' ) setRole (1 ) setCheckedkeys (['0-0' ]) setOpen (false ) } }) console .log (result) } const dis = useMemo (() => { return adminname === '' || password === '' }, [adminname, password]) const [isModalOpen, setIsModalOpen] = useState (false ); const onUpdate = ( ) => { const result : any = { adminname, role, checkedKeys } updateAdmin (result).then (() => { setIsModalOpen (false ) setAdminname ('' ) setRole (1 ) setCheckedkeys (['0-0' ]) getAdminListData () }) } const deleteRole = useAppSelector (state => state.admins .role ) return ( <Space style ={{ width: '100 %' }} direction ="vertical" > <Space > <Button type ='primary' onClick ={ () => setOpen(true) }>添加管理员</Button > { flag && <Button type ='primary' onClick ={ () => { if (deleteRole < 2) { message.warn('暂无权限') return } // 思考: 还用气泡弹出框? 自行选择删除的方案 // 接口没有提供批量删除,只能使用单个删除解决 // 每个删除都是基于 Promise, 综合考虑 Promise.all const arr: any = [] selectedRowKeys.forEach((item: any) => { arr.push(deleteAdmin({ adminid: item})) }) Promise.all(arr).then(() => { getAdminListData() }) }}>批量删除</Button > } </Space > <Table columns ={ columns } dataSource ={ adminList } // rowKey = "adminid" rowKey = { (record ) => record.adminid } scroll = {{ y: height - 330 }} pagination = { config } rowSelection={rowSelection} ></Table > <Drawer title ="添加管理员" placement ="right" onClose ={() => { getAdminListData() setAdminname('') setPassword('') setRole(1) setCheckedkeys(['0-0']) setOpen(false) }} open={open}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input placeholder ='账户名' value = { adminname } onChange = { e => setAdminname(e.target.value) } /> <Input placeholder ='密码' value = { password } onChange = { e => setPassword(e.target.value) } /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}} value = { role } onChange = { (value ) => setRole(value)}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: <Tree checkable checkedKeys = { checkedKeys } onCheck = { (checkedKeys: any ) => { setCheckedkeys(checkedKeys) console.log('666', checkedKeys) }} treeData={ treeData } /> <Button type ="primary" onClick ={ onAdd } disabled = { dis }> 添加管理员</Button > </Space > </Drawer > <Modal title ="修改管理员" footer ={null} open ={isModalOpen} onOk ={() => {}} onCancel={ () => { setAdminname('') setRole(1) setCheckedkeys(['0-0']) setIsModalOpen(false) }}> <Space direction ='vertical' style ={{ display: 'flex ' }}> <Input disabled placeholder ='账户名' value = { adminname } onChange = { e => setAdminname(e.target.value) } /> <Select defaultValue ={ 1 } style ={{ width: '100 %'}} value = { role } onChange = { (value ) => setRole(value)}> <Select.Option value ={1} > 管理员</Select.Option > <Select.Option value ={2} > 超级管理员</Select.Option > </Select > 选择该管理员的权限: <Tree checkable checkedKeys = { checkedKeys } onCheck = { (checkedKeys: any ) => { setCheckedkeys(checkedKeys) }} treeData={ treeData } /> <Button type ="primary" onClick ={ onUpdate } > 修改管理员权限</Button > </Space > </Modal > </Space > ) }; export default Index ;
单点登录
24、轮播图管理 24.1 封装接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import request from "../utils/request" ;export interface IBannerParams { img : string alt : string link : string } export function getBannerList () { return request ({ url : '/banner/list' }) } export function addBanner (params : IBannerParams ) { return request ({ url : '/banner/add' , method : 'POST' , data : params }) } export function deleteBanner (params : { bannerid : string }) { return request ({ url : '/banner/delete' , data : params }) }
24.2 轮播图页面渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import { getBannerList } from '@/api/banner' ;import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image } from 'antd' import { useNavigate } from 'react-router-dom' ;import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const navigate = useNavigate () const [list, setList] = useState ([]) const getBannerListData = ( ) => { getBannerList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getBannerListData () }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Button type ="primary" onClick ={ () => { navigate('/banner/add') }}>添加轮播图</Button > <Table rowKey ="bannerid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="alt" dataIndex ="alt" > </Table.Column > <Table.Column title ="link" dataIndex ="link" > </Table.Column > <Table.Column title ="操作" render ={ () => { return ( <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > ) }}></Table.Column > </Table > </Space > ) }; export default Index ;
24.3 删除轮播图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 import { deleteBanner, getBannerList } from '@/api/banner' ;import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image , Popconfirm } from 'antd' import { useNavigate } from 'react-router-dom' ;import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;interface IIndexProps { }; interface DataType { bannerid : string img : string alt : string link : string flag : boolean } const Index :FC <IIndexProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [1 , 2 , 3 , 4 ], showQuickJumper : true }) const navigate = useNavigate () const [list, setList] = useState ([]) const getBannerListData = ( ) => { getBannerList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getBannerListData () }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Button type ="primary" onClick ={ () => { navigate('/banner/add') }}>添加轮播图</Button > <Table rowKey ="bannerid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="alt" dataIndex ="alt" > </Table.Column > <Table.Column title ="link" dataIndex ="link" > </Table.Column > <Table.Column title ="操作" render ={ (_ , record: DataType ) => { return ( <Popconfirm title ="确定删除吗?" onConfirm ={ () => { deleteBanner({ bannerid: record.bannerid}).then(() => { // 删除完毕重新获取数据 getBannerListData() }) }} onOpenChange={() => console.log('open change')}> <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > </Popconfirm > ) }}></Table.Column > </Table > </Space > ) }; export default Index ;
23.4 添加轮播图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import { addBanner } from '@/api/banner' ;import { Image , Input , Space , Button } from 'antd' ;import React , { FC , useState, useRef, useMemo } from 'react' ;import { useNavigate } from 'react-router-dom' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { const navigate = useNavigate () const [link, setLink] = useState ('' ) const [alt, setAlt] = useState ('' ) const [img, setImg] = useState ('' ) const file = useRef<any >() const onChange = ( ) => { const fileInfo = file.current .input .files [0 ] const reader = new FileReader () reader.readAsDataURL (fileInfo) reader.onload = function ( ) { setImg (this .result as string ) } } const flag = useMemo (() => { return link === '' || alt === '' || img === '' }, [link, alt, img]) const onAdd = ( ) => { addBanner ({ link, alt, 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 Index ;
25.产品管理 25.1 封装接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import request from "../utils/request" ;interface ISearchParams { category : string search : string } export function getProList () { return request ({ url : '/pro/list' }) } export function getSearchProList (params : ISearchParams ) { return request ({ url : '/pro/searchPro' , method : 'POST' , data : params }) } export function getCategoryList () { return request ({ url : '/pro/getCategory' }) } export function getDetailRecommendList () { return request ({ url : '/pro/showdata' , method : 'POST' , data : { type : 'isrecommend' , flag : 1 } }) } export function getCartRecommendList () { return request ({ url : '/pro/showdata' , method : 'POST' , data : { type : 'isrecommend' , flag : 1 } }) } export function updateRecommend (params : { proid : string , flag : boolean }) { return request ({ url : '/pro/updateFlag' , method : 'POST' , data : { ...params, type : 'isrecommend' } }) } export function updateSeckill (params : { proid : string , flag : boolean }) { return request ({ url : '/pro/updateFlag' , method : 'POST' , data : { ...params, type : 'isseckill' } }) }
25.2 首页产品列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image , Switch } from 'antd' import { useNavigate } from 'react-router-dom' ;import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;import { getProList, updateRecommend, updateSeckill } from '@/api/pro' ;interface IIndexProps { }; interface DataType { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } const Index :FC <IIndexProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [10 , 20 , 30 , 40 ], showQuickJumper : true }) const navigate = useNavigate () const [list, setList] = useState ([]) const getProListData = ( ) => { getProList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getProListData () }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Button type ="primary" > 添加商品</Button > <Table rowKey ="proid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { (config.current - 1) * config.pageSize +index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img1" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="商品名称" dataIndex ="proname" > </Table.Column > <Table.Column title ="商品价格" dataIndex ="originprice" > </Table.Column > <Table.Column title ="是否秒杀" dataIndex ="isseckill" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { console.log(text) updateSeckill({ proid: record.proid, flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="是否推荐" dataIndex ="isrecommend" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { updateRecommend({ proid: record.proid , flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="操作" render ={ (_ , record: DataType ) => { return ( <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > ) }}></Table.Column > </Table > </Space > ) }; export default Index ;
25.3 详情推荐数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image , Switch } from 'antd' import { useNavigate } from 'react-router-dom' ;import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;import { getDetailRecommendList, updateRecommend, updateSeckill } from '@/api/pro' ;interface IIndexProps { }; interface DataType { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } const Index :FC <IIndexProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [10 , 20 , 30 , 40 ], showQuickJumper : true }) const navigate = useNavigate () const [list, setList] = useState ([]) const getProListData = ( ) => { getDetailRecommendList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getProListData () }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Button type ="primary" > 添加商品</Button > <Table rowKey ="proid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { (config.current - 1) * config.pageSize +index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img1" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="商品名称" dataIndex ="proname" > </Table.Column > <Table.Column title ="商品价格" dataIndex ="originprice" > </Table.Column > <Table.Column title ="是否推荐" dataIndex ="isrecommend" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { updateRecommend({ proid: record.proid , flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="操作" render ={ (_ , record: DataType ) => { return ( <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > ) }}></Table.Column > </Table > </Space > ) }; export default Index ;
25.4 购物车推荐数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image , Switch } from 'antd' import { useNavigate } from 'react-router-dom' ;import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;import { getDetailRecommendList, updateRecommend, updateSeckill } from '@/api/pro' ;interface IIndexProps { }; interface DataType { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } const Index :FC <IIndexProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [10 , 20 , 30 , 40 ], showQuickJumper : true }) const navigate = useNavigate () const [list, setList] = useState ([]) const getProListData = ( ) => { getDetailRecommendList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getProListData () }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Button type ="primary" > 添加商品</Button > <Table rowKey ="proid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { (config.current - 1) * config.pageSize +index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img1" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="商品名称" dataIndex ="proname" > </Table.Column > <Table.Column title ="商品价格" dataIndex ="originprice" > </Table.Column > <Table.Column title ="是否推荐" dataIndex ="isrecommend" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { updateRecommend({ proid: record.proid , flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="操作" render ={ (_ , record: DataType ) => { return ( <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > ) }}></Table.Column > </Table > </Space > ) }; export default Index ;
25.5 筛选列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image , Switch , Select , Input } from 'antd' import { useNavigate } from 'react-router-dom' ;import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;import { getCategoryList, getProList, updateRecommend, updateSeckill, getSearchProList } from '@/api/pro' ;interface IIndexProps { }; interface DataType { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } const Index :FC <IIndexProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [10 , 20 , 30 , 40 ], showQuickJumper : true }) const navigate = useNavigate () const [list, setList] = useState ([]) const getProListData = ( ) => { getProList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getProListData () }, []) const [categoryList, setCategoryList] = useState ([]) const [category, setCategory] = useState ('' ) const [search, setSearch] = useState ('' ) useEffect (() => { getCategoryList ().then (res => setCategoryList (res.data .data )) }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Space > <Select value ={ category } style ={{ width: 100 }} onChange = { value => setCategory(value)}> <Select.Option value ="" > 全部</Select.Option > { categoryList && categoryList.map((item, index) => { return ( <Select.Option key = {index} value ={ item }> { item }</Select.Option > ) }) } </Select > <Input placeholder ='请输入搜索关键词' value ={ search } onChange = { (e ) => setSearch(e.target.value)}></Input > <Button type ='primary' onClick ={ () => { getSearchProList({ category, search}).then(res => setList(res.data.data)) }}>搜索</Button > </Space > <Table rowKey ="proid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { (config.current - 1) * config.pageSize +index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img1" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="商品名称" dataIndex ="proname" > </Table.Column > <Table.Column title ="商品价格" dataIndex ="originprice" sorter ={(a: DataType ,b: DataType ) => a.originprice - b.originprice}></Table.Column > <Table.Column title ="是否秒杀" dataIndex ="isseckill" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { console.log(text) updateSeckill({ proid: record.proid, flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="是否推荐" dataIndex ="isrecommend" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { updateRecommend({ proid: record.proid , flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="操作" render ={ (_ , record: DataType ) => { return ( <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > ) }}></Table.Column > </Table > </Space > ) }; export default Index ;
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.highcharts.com.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 添加页面以及配置路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IDataProps { }; const Data :FC <IDataProps > = () => { return ( <> <h1 > 数据可视化</h1 > <Outlet /> </> ) }; export default Data ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IEChartsProps { }; const ECharts :FC <IEChartsProps > = () => { return ( <> <h3 > ECharts</h3 > </> ) }; export default ECharts ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IHighChartsProps { }; const HighCharts :FC <IHighChartsProps > = () => { return ( <> <h3 > HighCharts</h3 > </> ) }; export default HighCharts ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IAntvProps { }; const Antv :FC <IAntvProps > = () => { return ( <> <h3 > Antv</h3 > </> ) }; export default Antv ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' import Data from '@/views/data/Index' import ECharts from '@/views/data/ECharts' import HighCharts from '@/views/data/HighCharts' import Antv from '@/views/data/Antv' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , keyid : '0-0' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , keyid : '0-1' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , keyid : '0-1-0' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , keyid : '0-1-1' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , keyid : '0-1-2' , icon : <UploadOutlined /> , element : <BannerAdd /> , hidden : 1 }, ] }, { label : '产品管理' , key : '/pro' , keyid : '0-2' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , keyid : '0-2-0' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , keyid : '0-2-0-0' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , keyid : '0-2-0-1' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , keyid : '0-2-0-2' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , keyid : '0-2-1' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , keyid : '0-3' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , keyid : '0-3-1' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , keyid : '0-3-2' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , keyid : '0-4' , icon : <SettingOutlined /> , element : <Setting /> , hidden : 1 }, { label : '数据可视化' , key : '/data' , keyid : '0-5' , icon : <SettingOutlined /> , element : <Data /> , children : [ { label : 'ECharts' , key : '/data/echarts' , keyid : '0-5-0' , icon : <SettingOutlined /> , element : <ECharts /> , index : 1 }, { label : 'HighCharts' , key : '/data/highcharts' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <HighCharts /> }, { label : 'antv' , key : '/data/antv' , keyid : '0-5-2' , icon : <SettingOutlined /> , element : <Antv /> } ] } ] export default menus
1 cnpm install echarts --save
1 2 3 4 5 6 7 8 import request from './../utils/request' export function getData () { return request ({ url : '/data/simpleData' }) }
处理数据
自适应
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 import React , { FC , useEffect, useState } from 'react' ;import { Col , Row , Button } from 'antd' ;import * as echarts from 'echarts' ;import { getData } from '@/api/data' ;interface IEChartsProps { }; const ECharts :FC <IEChartsProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const [data, setData] = useState<any []>([]) useEffect (() => { getData ().then (res => { console .log (res.data ) setData (res.data .data ) }) }, []) useEffect (() => { var serverCharts = echarts.init (document .getElementById ('serverCharts' ) as HTMLDivElement ); serverCharts.setOption ({ title : { text : '折线图示例' , left : 'center' , }, legend : { data : ['销量' , '销量1' ], bottom : 10 , left : 'center' }, toolbox : { feature : { dataZoom : { yAxisIndex : 'none' }, restore : {}, saveAsImage : {} }, right : 200 }, xAxis : { data : data.map ((item:any ) => item.x ) }, yAxis : {}, series : [ { name : '销量' , type : 'line' , data : data.map ((item:any ) => item.val ) } ] }); window .addEventListener ('resize' ,() => { serverCharts && serverCharts.resize (); }) }, [data]) useEffect (() => { var barCharts = echarts.init (document .getElementById ('barCharts' ) as HTMLDivElement ); barCharts.setOption ({ title : { text : '柱状图示例' }, tooltip : {}, xAxis : { data : ['衬衫' , '羊毛衫' , '雪纺衫' , '裤子' , '高跟鞋' , '袜子' ] }, yAxis : {}, series : [ { name : '销量' , type : 'bar' , data : [5 , 20 , 36 , 10 , 10 , 20 ] } ] }); window .addEventListener ('resize' ,() => { barCharts && barCharts.resize (); }) }, []) useEffect (() => { var lineCharts = echarts.init (document .getElementById ('lineCharts' ) as HTMLDivElement ); lineCharts.setOption ({ title : { text : '折线图示例' , left : 'center' , }, legend : { data : ['销量' , '销量1' ], bottom : 10 , left : 'center' }, toolbox : { feature : { dataZoom : { yAxisIndex : 'none' }, restore : {}, saveAsImage : {} }, right : 200 }, xAxis : { data : ['衬衫' , '羊毛衫' , '雪纺衫' , '裤子' , '高跟鞋' , '袜子' ] }, yAxis : {}, series : [ { name : '销量' , type : 'line' , data : [5 , 20 , 36 , 10 , 10 , 20 ] }, { name : '销量1' , type : 'line' , data : [15 , 200 , 316 , 210 , 410 , 300 ] } ] }); window .addEventListener ('resize' ,() => { lineCharts && lineCharts.resize (); }) }, []) useEffect (() => { var randomCharts = echarts.init (document .getElementById ('randomCharts' ) as HTMLDivElement ); let base = +new Date (1988 , 9 , 3 ); let oneDay = 24 * 3600 * 1000 ; let data = [[base, Math .random () * 300 ]]; for (let i = 1 ; i < 20000 ; i++) { let now = new Date ((base += oneDay)); data.push ([+now, Math .round ((Math .random () - 0.5 ) * 20 + data[i - 1 ][1 ])]); } randomCharts.setOption ({ tooltip : { trigger : 'axis' , position : function (pt: any [] ) { return [pt[0 ], '10%' ]; } }, title : { left : 'center' , text : 'Large Ara Chart' }, toolbox : { feature : { dataZoom : { yAxisIndex : 'none' }, restore : {}, saveAsImage : {} } }, xAxis : { type : 'time' , boundaryGap : false }, yAxis : { type : 'value' , boundaryGap : [0 , '100%' ] }, dataZoom : [ { type : 'inside' , start : 0 , end : 20 }, { start : 0 , end : 20 } ], series : [ { name : 'Fake Data' , type : 'line' , smooth : true , symbol : 'none' , areaStyle : {}, data : data } ] }) window .addEventListener ('resize' ,() => { randomCharts && randomCharts.resize (); }) }, []) const changeData = ( ) => { setData ([ {x : '星期一' , val : Math .random ()*500 }, {x : '星期二' , val : Math .random ()*350 }, {x : '星期三' , val : Math .random ()*150 }, {x : '星期四' , val : Math .random ()*650 }, {x : '星期五' , val : Math .random ()*50 }, {x : '星期六' , val : Math .random ()*850 }, {x : '星期日' , val : Math .random ()*150 } ]) } return ( <> <h3 > ECharts</h3 > <Button onClick ={ changeData }> 改变数据data</Button > <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 ECharts ;
2.Highcharts vue: https://www.highcharts.com.cn/docs/highcharts-vue
react: https://www.highcharts.com.cn/docs/highcharts-react
1 cnpm install highcharts highcharts-react-official -S
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 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 > <HighchartsReact highcharts ={Highcharts} options ={option} {...props } /> </> ) }; export default HighCharts ;
3.antv - g2 https://antv-g2.gitee.io/zh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import React , { FC , useEffect } from 'react' ;import { Chart } from '@antv/g2' ;interface IAntvProps { }; const Antv :FC <IAntvProps > = () => { useEffect (() => { const data = [ { type : '未知' , value : 654 , percent : 0.02 }, { type : '17 岁以下' , value : 654 , percent : 0.02 }, { type : '18-24 岁' , value : 4400 , percent : 0.2 }, { type : '25-29 岁' , value : 5300 , percent : 0.24 }, { type : '30-39 岁' , value : 6200 , percent : 0.28 }, { type : '40-49 岁' , value : 3300 , percent : 0.14 }, { type : '50 岁以上' , value : 1500 , percent : 0.06 }, ]; const chart = new Chart ({ container : 'antv' , autoFit : true , height : 500 , padding : [50 , 20 , 50 , 20 ], }); chart.data (data); chart.scale ('value' , { alias : '销售额(万)' , }); chart.axis ('type' , { tickLine : { alignTick : false , }, }); chart.axis ('value' , false ); chart.tooltip ({ showMarkers : false , }); chart.interval ().position ('type*value' ); chart.interaction ('element-active' ); data.forEach ((item ) => { chart .annotation () .text ({ position : [item.type , item.value ], content : item.value , style : { textAlign : 'center' , }, offsetY : -30 , }) .text ({ position : [item.type , item.value ], content : (item.percent * 100 ).toFixed (0 ) + '%' , style : { textAlign : 'center' , }, offsetY : -12 , }); }); chart.render (); }, []) return ( <> <h1 > Antv</h1 > <div id ="antv" style ={{ width: 800 , height: 600 , backgroundColor: '#efefef ' }}> </div > </> ) }; export default Antv ;
27.编辑器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' import Data from '@/views/data/Index' import ECharts from '@/views/data/ECharts' import HighCharts from '@/views/data/HighCharts' import Antv from '@/views/data/Antv' import Editor from '@/views/editor/Index' import {Braft } from '@/views/editor/Braft' import {MarkDown } from '@/views/editor/MarkDown' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , keyid : '0-0' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , keyid : '0-1' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , keyid : '0-1-0' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , keyid : '0-1-1' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , keyid : '0-1-2' , icon : <UploadOutlined /> , element : <BannerAdd /> , hidden : 1 }, ] }, { label : '产品管理' , key : '/pro' , keyid : '0-2' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , keyid : '0-2-0' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , keyid : '0-2-0-0' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , keyid : '0-2-0-1' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , keyid : '0-2-0-2' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , keyid : '0-2-1' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , keyid : '0-3' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , keyid : '0-3-1' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , keyid : '0-3-2' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , keyid : '0-4' , icon : <SettingOutlined /> , element : <Setting /> , hidden : 1 }, { label : '数据可视化' , key : '/data' , keyid : '0-5' , icon : <SettingOutlined /> , element : <Data /> , children : [ { label : 'ECharts' , key : '/data/echarts' , keyid : '0-5-0' , icon : <SettingOutlined /> , element : <ECharts /> , index : 1 }, { label : 'HighCharts' , key : '/data/highcharts' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <HighCharts /> }, { label : 'antv' , key : '/data/antv' , keyid : '0-5-2' , icon : <SettingOutlined /> , element : <Antv /> } ] }, { label : '编辑器' , key : '/editor' , keyid : '0-6' , icon : <SettingOutlined /> , element : <Editor /> , children : [ { label : '富文本编辑器' , key : '/editor/braft' , keyid : '0-6-0' , icon : <SettingOutlined /> , element : <Braft /> , index : 1 }, { label : 'MarkDown' , key : '/editor/md' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <MarkDown /> } ] } ] export default menus
1.富文本编辑器 react版本: https://braft.margox.cn/demos/basic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IEditorProps { }; const Editor :FC <IEditorProps > = () => { return ( <> <h1 > 编辑器</h1 > <Outlet /> </> ) }; export default Editor ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import React , { useState } from 'react' ;import 'braft-editor/dist/index.css' import BraftEditor from 'braft-editor' type BraftProps = {}export const Braft = ({}: BraftProps ) => { const [editorState, setEditorState] = useState<any >('' ) const [html, setHTML] = useState<any >('' ) const handleChange = (editorState: any ) => { setEditorState (editorState) setHTML (editorState.toHTML ()) } return ( <> <h3 > 富文本编辑器</h3 > <BraftEditor value ={editorState} onChange ={handleChange} /> <div > 预览 <div dangerouslySetInnerHTML ={{ __html: html }}> </div > </div > </> ) };
2.markDown编辑器 阅读器:https://www.npmjs.com/package/react-markdown
编辑器:https://www.npmjs.com/package/react-markdown-editor-lite
1 cnpm i react-markdown react-markdown-editor-lite -S
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import React , { useState } from 'react' ;import ReactMarkdown from 'react-markdown' import MdEditor from 'react-markdown-editor-lite' ; import 'react-markdown-editor-lite/lib/index.css' ; type MarkDownProps = {}export const MarkDown = ({}:MarkDownProps ) => { const [content, setContent] = useState ('' ) return ( <> <h3 > MarkDown编辑器</h3 > <MdEditor style ={{ height: '500px ' }} renderHTML ={text => { return <ReactMarkdown > { text }</ReactMarkdown > }} onChange={ ( { html, text }: { html: any; text: any}) => { setContent(html) }} /> <hr /> <div dangerouslySetInnerHTML ={{ __html: content }}> </div > </> ); }
28.导入以及导出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import React , { FC } from 'react' ;import { Outlet } from 'react-router-dom' ;interface IIndexProps { }; const Index :FC <IIndexProps > = () => { return ( <Outlet > </Outlet > ) }; export default Index ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IImportProps { }; const Import :FC <IImportProps > = () => { return ( <> <h3 > 导入数据</h3 > </> ) }; export default Import ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { FC } from 'react' ;interface IExportProps { }; const Export :FC <IExportProps > = () => { return ( <> <h3 > 导出数据</h3 > </> ) }; export default Export ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' import Data from '@/views/data/Index' import ECharts from '@/views/data/ECharts' import HighCharts from '@/views/data/HighCharts' import Antv from '@/views/data/Antv' import Editor from '@/views/editor/Index' import {Braft } from '@/views/editor/Braft' import {MarkDown } from '@/views/editor/MarkDown' import Excel from '@/views/excel/Index' import Import from '@/views/excel/Import' import Export from '@/views/excel/Export' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , keyid : '0-0' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , keyid : '0-1' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , keyid : '0-1-0' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , keyid : '0-1-1' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , keyid : '0-1-2' , icon : <UploadOutlined /> , element : <BannerAdd /> , hidden : 1 }, ] }, { label : '产品管理' , key : '/pro' , keyid : '0-2' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , keyid : '0-2-0' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , keyid : '0-2-0-0' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , keyid : '0-2-0-1' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , keyid : '0-2-0-2' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , keyid : '0-2-1' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , keyid : '0-3' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , keyid : '0-3-1' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , keyid : '0-3-2' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , keyid : '0-4' , icon : <SettingOutlined /> , element : <Setting /> , hidden : 1 }, { label : '数据可视化' , key : '/data' , keyid : '0-5' , icon : <SettingOutlined /> , element : <Data /> , children : [ { label : 'ECharts' , key : '/data/echarts' , keyid : '0-5-0' , icon : <SettingOutlined /> , element : <ECharts /> , index : 1 }, { label : 'HighCharts' , key : '/data/highcharts' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <HighCharts /> }, { label : 'antv' , key : '/data/antv' , keyid : '0-5-2' , icon : <SettingOutlined /> , element : <Antv /> } ] }, { label : '编辑器' , key : '/editor' , keyid : '0-6' , icon : <SettingOutlined /> , element : <Editor /> , children : [ { label : '富文本编辑器' , key : '/editor/braft' , keyid : '0-6-0' , icon : <SettingOutlined /> , element : <Braft /> , index : 1 }, { label : 'MarkDown' , key : '/editor/md' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <MarkDown /> } ] }, { label : '导入以及导出' , key : '/excel' , keyid : '0-7' , icon : <SettingOutlined /> , element : <Excel /> , children : [ { label : '导入' , key : '/excel/import' , keyid : '0-7-0' , icon : <SettingOutlined /> , element : <Import /> , index : 1 }, { label : '导出' , key : '/excel/export' , keyid : '0-7-1' , icon : <SettingOutlined /> , element : <Export /> } ] } ] export default menus
1.导出 cnpm i js-export-excel -S
src/views/excel/test.d.ts
1 declare module 'js-export-excel'
本案例导出产品筛选列表的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 import React , { FC ,useState, useEffect } from 'react' ;import { Table , Space , Button , Image , Switch } from 'antd' import { DeleteOutlined } from '@ant-design/icons' import usePagination from '@/hooks/usePagination' ;import { getProList, updateRecommend, updateSeckill } from '@/api/pro' ;import ExportJsonExcel from 'js-export-excel' interface IExportProps { }; interface DataType { banners : string [] brand : string category : string desc : string discount : number img1 : string img2 : string img3 : string img4 : string isrecommend : number issale : number isseckill : number originprice : number proid : string proname : string sales : number stock : number } const Export :FC <IExportProps > = () => { const [height, setHeight] = useState (document .body .offsetHeight ) useEffect (() => { window .addEventListener ('resize' , () => { setHeight (document .body .offsetHeight ) }) }, []) const config = usePagination ({ position : ['bottomLeft' ], showSizeChanger : true , pageSizeOptions : [10 , 20 , 30 , 40 ], showQuickJumper : true }) const [list, setList] = useState ([]) const getProListData = ( ) => { getProList ().then (res => { console .log (res.data ) setList (res.data .data ) }) } useEffect (() => { getProListData () }, []) return ( <Space direction ='vertical' style ={ { display: 'flex ' }} > <Button type ='primary' onClick ={ () => {// ++++++++ let option: { fileName: string datas: { sheetData: DataType[], sheetName: string, sheetFilter: string[], sheetHeader: string[], columnWidths: number[] }[] }; option = { fileName: "产品列表", // 导出的文件的名称 datas: [ { sheetData: list, // 表格数据 sheetName: "产品列表1", // excel表格中表格的名字 sheetFilter: ["proname", "img1", "category"], // 需要导出的数据的字段 sheetHeader: ["产品名称", "图片", "分类"], // 表头的值 columnWidths: [20, 20], }, { sheetData: list, // 表格数据 sheetName: "产品列表2", // excel表格中表格的名字 sheetFilter: ["proname", "img1", "category", 'originprice'], // 需要导出的数据的字段 sheetHeader: ["产品名称", "图片", "分类", '价格'], // 表头的值 columnWidths: [20, 20], }, ] } var toExcel = new ExportJsonExcel(option); //new toExcel.saveExcel(); //保存 }}>导出数据</Button > <Table rowKey ="proid" dataSource = { list } scroll = {{ y: height - 330 }} pagination = { config } > <Table.Column title ="序号" render = { (_ ,record , index ) => { return ( <span > { (config.current - 1) * config.pageSize +index + 1 }</span > ) }}></Table.Column > <Table.Column title ="图片" dataIndex ="img1" render = { text => { return <Image src = { text } style ={{ height: 80 }} /> }}></Table.Column > <Table.Column title ="商品名称" dataIndex ="proname" > </Table.Column > <Table.Column title ="商品价格" dataIndex ="originprice" > </Table.Column > <Table.Column title ="是否秒杀" dataIndex ="isseckill" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { console.log(text) updateSeckill({ proid: record.proid, flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="是否推荐" dataIndex ="isrecommend" render ={ (text , record: DataType ) => { return ( <Switch defaultChecked = { text === 1} onChange ={ () => { updateRecommend({ proid: record.proid , flag: text === 0}).then(() => getProListData()) } } /> ) }}></Table.Column > <Table.Column title ="操作" render ={ (_ , record: DataType ) => { return ( <Button danger shape ="circle" icon ={ <DeleteOutlined /> }></Button > ) }}></Table.Column > </Table > </Space > ) }; export default Export ;
以上方案为纯前端的导出,实际上还有其余的导出方法,比如通过接口实现,前端可以通过a的href属性实现
2.导入 数据在 src/views/excel/pro.xlsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 import { Button , Table , Switch , Image } from 'antd' ;import { useState } from 'react' import * as XLSX from 'xlsx' ;type ImportProps = {}interface DataType { proid : string proname : string img1 : string originprice : number discount : number sales : number stock : number category : string brand : string issale : number isrecommend : number isseckill : number } export default function Import (props : ImportProps ) { 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' ] const r : any = XLSX .utils .sheet_to_json (t) setProList (r) } } return ( <> <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.Column title ="序号" render ={(text, record , index ) => { return <span > { index + 1 }</span > }} ></Table.Column > <Table.Column title ="产品分类" dataIndex ="category" > </Table.Column > <Table.Column title ="产品品牌" dataIndex ="brand" > </Table.Column > <Table.Column title ="产品名称" dataIndex ="proname" > </Table.Column > <Table.Column title ="图片" dataIndex ="img1" render ={ (text ) => {return <Image src ={text} width ={80} height ={80} /> }} ></Table.Column > <Table.Column title ="产品价格" dataIndex ="originprice" sorter ={(a: DataType , b: DataType ) => a.originprice - b.originprice} ></Table.Column > <Table.Column title ="产品折扣" dataIndex ="discount" sorter ={(a: DataType , b: DataType ) => a.discount - b.discount} ></Table.Column > <Table.Column title ="产品销量" dataIndex ="sales" sorter ={(a: DataType , b: DataType ) => a.sales - b.sales} ></Table.Column > <Table.Column title ="产品库存" dataIndex ="stock" sorter ={(a: DataType , b: DataType ) => a.stock - b.stock} ></Table.Column > <Table.Column title ="是否上架" dataIndex ="issale" render ={ (text ) => {return <Switch checked = { text === 1 } /> }} ></Table.Column > <Table.Column title ="是否推荐" dataIndex ="isrecommend" render ={ (text ) => {return <Switch checked = { text === 1 } /> }} ></Table.Column > <Table.Column title ="是否秒杀" dataIndex ="isseckill" render ={ (text ) => {return <Switch checked = { text === 1 } /> }} ></Table.Column > </Table > </> ) }
如果在nodejs环境中,通过接口实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const xlsx = require ('node-xlsx' ).default ;router.get ('/uploadPro' , (req, res, next ) => { const originData = xlsx.parse (`${__dirname} /pro.xlsx` ); const firstData = originData[0 ].data const 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 ], }) } } mysql.delete (Product , {}, 1 ).then (() => { mysql.insert (Product , arr).then (() => { res.send ('导入数据成功' ) }) }) })
29.地图 https://huiyan.baidu.com/github/react-bmapgl/#/%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8
https://lbsyun.baidu.com/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // src/views/map/Baidu.tsx import React, { FC, useEffect } from 'react'; interface IBaiduProps { }; const Baidu:FC<IBaiduProps> = () => { useEffect(() => { var map = new window.BMapGL.Map("container"); map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11); var p1 = new window.BMapGL.Point(116.301934,39.977552); var p2 = new window.BMapGL.Point(116.508328,39.919141); var driving = new window.BMapGL.DrivingRoute(map, {renderOptions:{map: map, autoViewport: true}}); driving.search(p1, p2); }, []) return ( <> <div id="container" style={ { width: '100%', height: 500 }}></div> <div id='result'>根据起点和终点经纬度驾车导航路线</div> </> ) }; export default Baidu;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 import { IMenuProps } from './inter' import { HomeOutlined , UploadOutlined , PictureOutlined , ProfileOutlined , OrderedListOutlined , FilterOutlined , TeamOutlined , UserOutlined , SettingOutlined } from '@ant-design/icons' import Home from '@/views/home/Index' import Banner from '@/views/banner/Index' import BannerList from '@/views/banner/List' import BannerActive from '@/views/banner/Active' import BannerAdd from '@/views/banner/Add' import Product from '@/views/product/Index' import ProductSearch from '@/views/product/Search' import ProductList from '@/views/product/list/Index' import ProductHomeList from '@/views/product/list/Home' import ProductDetailList from '@/views/product/list/Detail' import ProductCartList from '@/views/product/list/Cart' import Account from '@/views/account/Index' import UserList from '@/views/account/UserList' import AdminList from '@/views/account/AdminList' import Setting from '@/views/setting/Index' import Data from '@/views/data/Index' import ECharts from '@/views/data/ECharts' import HighCharts from '@/views/data/HighCharts' import Antv from '@/views/data/Antv' import Editor from '@/views/editor/Index' import {Braft } from '@/views/editor/Braft' import {MarkDown } from '@/views/editor/MarkDown' import Excel from '@/views/excel/Index' import Import from '@/views/excel/Import' import Export from '@/views/excel/Export' import Baidu from '@/views/Map/Baidu' const menus : IMenuProps [] = [ { label : '系统首页' , key : '/' , keyid : '0-0' , icon : <HomeOutlined /> , element : <Home /> }, { label : '轮播图管理' , key : '/banner' , keyid : '0-1' , icon : <UploadOutlined /> , element : <Banner /> , children : [ { label : '首页轮播图' , key : '/banner/home' , keyid : '0-1-0' , icon : <PictureOutlined /> , element : <BannerList /> , index : 1 }, { label : '活动页轮播图' , key : '/banner/active' , keyid : '0-1-1' , icon : <PictureOutlined /> , element : <BannerActive /> }, { label : '添加轮播图' , key : '/banner/add' , keyid : '0-1-2' , icon : <UploadOutlined /> , element : <BannerAdd /> , hidden : 1 }, ] }, { label : '产品管理' , key : '/pro' , keyid : '0-2' , icon : <ProfileOutlined /> , element : <Product /> , children : [ { label : '产品列表' , key : '/pro/list' , keyid : '0-2-0' , icon : <OrderedListOutlined /> , element : <ProductList /> , index : 1 , children : [ { label : '首页产品列表' , key : '/pro/list/home' , keyid : '0-2-0-0' , element : <ProductHomeList /> , index : 1 }, { label : '详情推荐列表' , key : '/pro/list/detail' , keyid : '0-2-0-1' , element : <ProductDetailList /> }, { label : '购物车推荐列表' , key : '/pro/list/cart' , keyid : '0-2-0-2' , element : <ProductCartList /> }, ] }, { label : '筛选列表' , key : '/pro/search' , keyid : '0-2-1' , icon : <FilterOutlined /> , element : <ProductSearch /> }, ] }, { label : '账户管理' , key : '/account' , keyid : '0-3' , icon : <TeamOutlined /> , element : <Account /> , children : [ { label : '用户列表' , key : '/account/userlist' , keyid : '0-3-1' , icon : <UserOutlined /> , element : <UserList /> , index : 1 }, { label : '管理员列表' , key : '/account/adminlist' , keyid : '0-3-2' , icon : <UserOutlined /> , element : <AdminList /> } ] }, { label : '设置' , key : '/setting' , keyid : '0-4' , icon : <SettingOutlined /> , element : <Setting /> , hidden : 1 }, { label : '数据可视化' , key : '/data' , keyid : '0-5' , icon : <SettingOutlined /> , element : <Data /> , children : [ { label : 'ECharts' , key : '/data/echarts' , keyid : '0-5-0' , icon : <SettingOutlined /> , element : <ECharts /> , index : 1 }, { label : 'HighCharts' , key : '/data/highcharts' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <HighCharts /> }, { label : 'antv' , key : '/data/antv' , keyid : '0-5-2' , icon : <SettingOutlined /> , element : <Antv /> } ] }, { label : '编辑器' , key : '/editor' , keyid : '0-6' , icon : <SettingOutlined /> , element : <Editor /> , children : [ { label : '富文本编辑器' , key : '/editor/braft' , keyid : '0-6-0' , icon : <SettingOutlined /> , element : <Braft /> , index : 1 }, { label : 'MarkDown' , key : '/editor/md' , keyid : '0-5-1' , icon : <SettingOutlined /> , element : <MarkDown /> } ] }, { label : '导入以及导出' , key : '/excel' , keyid : '0-7' , icon : <SettingOutlined /> , element : <Excel /> , children : [ { label : '导入' , key : '/excel/import' , keyid : '0-7-0' , icon : <SettingOutlined /> , element : <Import /> , index : 1 }, { label : '导出' , key : '/excel/export' , keyid : '0-7-1' , icon : <SettingOutlined /> , element : <Export /> } ] }, { label : '百度地图' , key : '/map' , keyid : '0-8' , icon : <SettingOutlined /> , element : <Baidu /> } ] export default menus
1 2 3 4 5 interface Window { BMapGL : any BMAP_STATUS_SUCCESS : any }
1 2 3 // public/index.html <title > React App</title > <script type ="text/javascript" src ="//api.map.baidu.com/api?type=webgl&v=1.0&ak=tvN3aLzFzvthpM9OwvsxmrSjD9dsdlbd" > </script >
30.项目打包发布 https://blog.csdn.net/daxunshuo/article/details/102976306?spm=1001.2014.3001.5501
打包完毕项目 build 文件夹即为 项目打包出来的文件,只需要把build文件夹上传至服务器,一般情况下,都需要修改build文件夹的名字
如果打开 build/index.html 文件,发现 css js 文件的引入使用的都是绝对路径
如果服务器只部署这一个项目,那么可以把 build文件夹的内容替换了 服务器的静态资源文件 — 绝对路径
如果服务器部署多个项目,那么可以把修改过后的 build的文件夹上传到 服务器的静态资源文件 — 项目路径
如何打包项目时使用相对路径
package.json
http://121.89.205.189:2207/