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 类型 虽然可以将RootStateandAppDispatch类型导入到每个组件中,但最好创建useDispatchand useSelectorhooks 的类型化版本以在您的应用程序中使用 。
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/