【React】pro-pc

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

1.创建项目

# 现在
npx create-react-app react-admin-app --template typescript

熟悉目录结构

- react-admin-app
	-node_modules
	-public
	-src
		App.css
		App.test.tsx App.tsx的测试文件  npm run test 查看测试结果
		App.tsx
		index.css
		index.tsx react应用程序的入口文件
		logo.svg 
		react-app-env.d.ts // 声明文件 // 指令声明对包的依赖关系
		reportWebVitals.ts // 测试性能
		seupTests.ts // 使用jest做为测试工具
	.gitignore
	package-lock.json
	package.json
	README.md
	tsconfig.json

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

2.改造目录结构

src
	api
	components
	layout
	store
	router
	utils
	views
	App.tsx
	index.tsx
	logo.svg
	react-app-env.d.ts
	reportWebVitals.ts 
  seupTests.ts 
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLDivElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

// src/App.tsx
import React, { FC } from 'react';

interface IAppProps {
}

const App: FC<IAppProps> = (props) => {
  return (
    <>App</>
  )
}

export default App

3.安装一些必须的模块

3.1 配置预处理器

两种方式:

  • 抽离配置文件配置预处理器
  • 不抽离配置文件craco进行预处理器配置

本项目推荐使用第二种方式

$ cnpm i @craco/craco @types/node -D

https://www.npmjs.com/package/@craco/craco

3.1.1 配置别名@

项目根目录创建 craco.config.js,代码如下:

// craco.config.js
const path = require('path')
module.exports = {
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

为了使 TS 文件引入时的别名路径能够正常解析,需要配置 tsconifg.json,在 compilerOptions选项里添加 path 等属性。为了防止配置被覆盖,需要单独创建一个文件 tsconfig.path.json,添加以下代码

// tsconfig.path.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": [
      "node"
    ]
  }
}

tsconifg.json 引入配置文件:

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "extends": "./tsconfig.path.json",
  "include": [
    "src"
  ]
}

修改 package.json 如下:

"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test"
},
$ npm run start

3.2安装状态管理器

根据项目需求 任选其一即可

$ cnpm i redux -S
$ cnpm i redux react-redux -S
$ cnpm i redux react-redux redux-thunk -S
$ cnpm i redux react-redux redux-saga -S
$ cnpm i redux react-redux redux-thunk immutable redux-immutable -S
$ cnpm i redux react-redux redux-saga immutable redux-immutable -S
$ cnpm i mobx mobx-react -S

本项目不采用之前的状态管理模式,使用 rtk 技术

cnpm i @reduxjs/toolkit redux react-redux -S

3.3 路由

2021年11月4日 发布了 react-router-dom的v6.0.0版本:https://reactrouter.com/

如需使用v5版本:https://v5.reactrouter.com/web/guides/quick-start cnpm i react-router-dom@5 -S

本项目采用 V6版本

cnpm i react-router-dom -S

3.4 数据验证

思考,有没有必要安装 prop-types ?

cnpm i prop-types -S

本项目其实没有必要安装,因为所有的数据都是基于ts,而ts需要指定类型注解

3.5数据请求

cnpm i axios -S

以前版本中 cnpm i @types/axios -S

Ts 中 @types/* 为声明文件

3.6ui库

官网地址:https://ant.design/index-cn 5.2.0

国内官方镜像地址:https://ant-design.antgroup.com/index-cn

国内gitee镜像地址:https://ant-design.gitee.io/index-cn

cnpm i antd @ant-design/icons -S

src/index.tsx

// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import App from './App';
import reportWebVitals from './reportWebVitals';

import 'antd/dist/reset.css'; // antd重置样式表

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLDivElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

测试组件库

// src/App.tsx
import React, { FC } from 'react';
import { Button } from 'antd';

interface IAppProps {
}

const App: FC<IAppProps> = (props) => {
  return (
    <>
      App
      <Button type="primary">
        Primary
      </Button>
    </>
  )
}

export default App

浏览器查看发现测试通过

3.6.1 自定义主题

https://ant-design.antgroup.com/docs/react/use-in-typescript-cn

antd 内建了深色主题和紧凑主题,你可以参照 使用暗色主题和紧凑主题 进行接入。

可以定制的变量列表如下:

@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
@border-radius-base: 2px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
  0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import { ConfigProvider } from 'antd';

import App from './App';
import reportWebVitals from './reportWebVitals';

import 'antd/dist/reset.css'; // antd重置样式表

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLDivElement
);
root.render(
  <React.StrictMode>
    <ConfigProvider
      theme = { {
        token: {
          colorPrimary: '#1890ff'
        }
      } }
    >
      <App />
    </ConfigProvider>
  </React.StrictMode>
);

reportWebVitals();

3.7 其他第三方工具包

https://www.lodashjs.com/

Lodash 工具包,项目必装,它提供了很多使用的函数

$ cnpm i lodash -S
$ cnpm i @types/lodash -D
import _ from 'lodash'

var users = [
  { 'user': 'barney',  'active': false },
  { 'user': 'fred',    'active': false },
  { 'user': 'pebbles', 'active': true }
];

console.log(_.findIndex(users, (item) => item.user === 'pebbles'))
console.log(users.findIndex((item) => item.user === 'pebbles'))

4.创建主布局文件

预览模板:https://pro.ant.design/zh-CN/

src/layout/Index.tsx 作为后台管理系统的主页面布局(包含左侧的菜单栏,顶部,底部等)

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

不要照着代码敲,直接复制即可,给 Layout 组件添加 id为admin-app

// src/layout/Index.tsx
import React, { useState } from 'react';
import {
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  UploadOutlined,
  UserOutlined,
  VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu, theme } from 'antd';

const { Header, Sider, Content } = Layout;

const App: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  return (
    <Layout id="components-layout-demo-custom-trigger">
      <Sider trigger={null} collapsible collapsed={collapsed}>
        <div className="logo" />
        <Menu
          theme="dark"
          mode="inline"
          defaultSelectedKeys={['1']}
          items={[
            {
              key: '1',
              icon: <UserOutlined />,
              label: 'nav 1',
            },
            {
              key: '2',
              icon: <VideoCameraOutlined />,
              label: 'nav 2',
            },
            {
              key: '3',
              icon: <UploadOutlined />,
              label: 'nav 3',
            },
          ]}
        />
      </Sider>
      <Layout className="site-layout">
        <Header style={{ padding: 0, background: colorBgContainer }}>
          {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
            className: 'trigger',
            onClick: () => setCollapsed(!collapsed),
          })}
        </Header>
        <Content
          style={{
            margin: '24px 16px',
            padding: 24,
            minHeight: 280,
            background: colorBgContainer,
          }}
        >
          Content
        </Content>
      </Layout>
    </Layout>
  );
};

export default App;

主组件引入 主界面的布局文件

// src/App.tsx
import React, { FC } from 'react';

import Index from '@/layout/Index'

import './App.css'

interface IAppProps {
}

const App: FC<IAppProps> = (props) => {
  return (
    <>
      <Index />
    </>
  )
}

export default App

查看浏览器,预览运行结果

发现页面并不是全屏。审查元素设置 root以及 components-layout-demo-custom-trigger 高度为 100%

/* src/App.css */
#root, #components-layout-demo-custom-trigger { height: 100%;}

#components-layout-demo-custom-trigger .trigger {
  padding: 0 24px;
  font-size: 18px;
  line-height: 64px;
  cursor: pointer;
  transition: color 0.3s;
}

#components-layout-demo-custom-trigger .trigger:hover {
  color: #1890ff;
}

#components-layout-demo-custom-trigger .logo {
  height: 32px;
  margin: 16px;
  background: rgba(255, 255, 255, 0.3);
}

5.拆分主界面

先拆分左侧的菜单栏组件

// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import {
  UploadOutlined,
  UserOutlined,
  VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';

const { Sider } = Layout;

const App: React.FC = () => {
  const [collapsed] = useState(false);

  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" />
      <Menu
        theme="dark"
        mode="inline"
        defaultSelectedKeys={['1']}
        items={[
          {
            key: '1',
            icon: <UserOutlined />,
            label: 'nav 1',
          },
          {
            key: '2',
            icon: <VideoCameraOutlined />,
            label: 'nav 2',
          },
          {
            key: '3',
            icon: <UploadOutlined />,
            label: 'nav 3',
          },
        ]}
      />
    </Sider>
  );
};

export default App;
// src/layout/components/AppHeader.tsx
import React, { useState } from 'react';
import {
  MenuFoldOutlined,
  MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme } from 'antd';

const { Header } = Layout;

const App: React.FC = () => {
  const [collapsed, setCollapsed] = useState(false);
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  return (
    <Header style={{ padding: 0, background: colorBgContainer }}>
      {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
        className: 'trigger',
        onClick: () => setCollapsed(!collapsed),
      })}
    </Header>
  );
};

export default App;
// src/layout/components/AppMain.tsx
import React from 'react';

import { Layout, theme } from 'antd';

const { Content } = Layout;

const App: React.FC = () => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  return (
    <Content
      style={{
        margin: '24px 16px',
        padding: 24,
        minHeight: 280,
        background: colorBgContainer,
      }}
    >
      Content
    </Content>
  );
};

export default App;

整和组件资源

// src/layout/components/index.ts

export { default as SideBar } from './SideBar'
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
// src/layout/Index.tsx
import React from 'react';

import { Layout } from 'antd';

// import SideBar from './components/SideBar'
// import AppHeader from './components/AppHeader'
// import AppMain from './components/AppMain'
import { SideBar, AppHeader, AppMain } from './components'

const App: React.FC = () => {

  return (
    <Layout id="components-layout-demo-custom-trigger">
      <SideBar />
      <Layout className="site-layout">
        <AppHeader />
        <AppMain />
      </Layout>
    </Layout>
  );
};

export default App;

此时点击头部的控制器,发现只有头部组件的 图标在切换,但是并没有影响左侧菜单的收缩

建议使用状态管理器管理控制的这个状态

6.使用rtk来管理状态

http://cn.redux.js.org/

参考链接:http://cn.redux.js.org/tutorials/typescript-quick-start

6.1 定义State和Dispatch类型

// src/store/index.ts
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({
  reducer: {}
})

// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch

export default store

构建app的模块用于管理 头部和 左侧菜单的共同的状态

6.2 定义 Hooks 类型

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

// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './index'

// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

6.3 应用程序中使用

创建状态管理

// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'

interface IAppState {
  collapsed: boolean
}

const initialState: IAppState = {
  collapsed: false
}

export const appSlice = createSlice({
  name: 'app',
  initialState,
  reducers: {
    changeCollapsed (state) {
      state.collapsed = !state.collapsed
    }
  }
})

export const { changeCollapsed } = appSlice.actions

export default appSlice.reducer

6.4 整合reducer

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'

import app from './modules/app'

const store = configureStore({
  reducer: {
    app
  }
})

// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch

export default store

6.5 入口文件配置状态管理器

// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'

import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store'

import 'antd/dist/reset.css'; // antd重置样式表

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLDivElement
);
root.render(
  <React.StrictMode>
    <ConfigProvider
      theme = { {
        token: {
          colorPrimary: '#1890ff'
        }
      } }
    >
      <Provider store = { store }>
        <App />
      </Provider>
    </ConfigProvider>
  </React.StrictMode>
);

reportWebVitals();

6.6 左侧菜单栏使用状态管理器

// src/layout/components/SideBar.tsx
import React from 'react';
import {
  UploadOutlined,
  UserOutlined,
  VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import { useAppSelector } from '@/store/hooks'
// import { useSelector } from 'react-redux'
// import type { RootState } from '@/store'

const { Sider } = Layout;

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)
  // const collapsed = useSelector((state: RootState) => state.app.collapsed)
  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" />
      <Menu
        theme="dark"
        mode="inline"
        defaultSelectedKeys={['1']}
        items={[
          {
            key: '1',
            icon: <UserOutlined />,
            label: 'nav 1',
          },
          {
            key: '2',
            icon: <VideoCameraOutlined />,
            label: 'nav 2',
          },
          {
            key: '3',
            icon: <UploadOutlined />,
            label: 'nav 3',
          },
        ]}
      />
    </Sider>
  );
};

export default App;

6.7 头部组件使用状态管理器

// src/layout/components/AppHeader.tsx
import React from 'react';
import {
  MenuFoldOutlined,
  MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'

const { Header } = Layout;

const App: React.FC = () => {
  // const [collapsed, setCollapsed] = useState(false);
  const collapsed = useAppSelector(state => state.app.collapsed)
  const dispatch = useAppDispatch()
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  return (
    <Header style={{ padding: 0, background: colorBgContainer }}>
      {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
        className: 'trigger',
        // onClick: () => setCollapsed(!collapsed),
        onClick: () => dispatch(changeCollapsed())
      })}
    </Header>
  );
};

export default App;

6.8保留用户习惯-可选

永久存储 用户习惯

数据持久化: redux-persist

此时发现 头部的 按钮可以控制左侧菜单栏了,但是还没有满足需求

需求如下:保留用户的使用习惯

// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'

interface IAppState {
  collapsed: boolean
}

const initialState: IAppState = {
  // collapsed: false
  collapsed: localStorage.getItem('collapsed') === 'true'
}

export const appSlice = createSlice({
  name: 'app',
  initialState,
  reducers: {
    changeCollapsed (state) {
      state.collapsed = !state.collapsed
      localStorage.setItem('collapsed', String(state.collapsed))
    }
  }
})

export const { changeCollapsed } = appSlice.actions

export default appSlice.reducer

6.9 永久存储的 类 localStorage 的工具 store2

$ cnpm i store2 -S

https://www.npmjs.com/package/store2

推荐一个好用的永久存储的 类 localStorage 的工具 store2

// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
import store2 from 'store2'
interface IAppState {
  collapsed: boolean
}

const initialState: IAppState = {
  // collapsed: false
  // collapsed: localStorage.getItem('collapsed') === 'true'
  collapsed: store2.get('collapsed') === 'true'
}

export const appSlice = createSlice({
  name: 'app',
  initialState,
  reducers: {
    changeCollapsed (state) {
      state.collapsed = !state.collapsed
      // localStorage.setItem('collapsed', String(state.collapsed))
      store2.set('collapsed', String(state.collapsed))
    }
  }
})

export const { changeCollapsed } = appSlice.actions

export default appSlice.reducer

7.左侧菜单栏

7.1.设计左侧菜单栏的数据

https://ant-design.gitee.io/components/menu-cn/#components-menu-demo-sider-current

Antd 4.20以上版本直接实现 递归

antd 4.20版本以下需要手动实现

// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'

type MenuItem = Required<MenuProps>['items'][number];

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

const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
      }
    ]
  }
]

export default menus

7.2.渲染左侧菜单栏

左侧菜单栏的头部设定logo以及后台管理系统名称

// src/layout/components/SideBar.tsx
import React from 'react';

import { Layout, Menu, Image } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'

const { Sider } = Layout;

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)

  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" style={ { 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center',
        color: '#fff'
      }}>
        <Image src = { logo } width="28px" height="28px" preview={ false }></Image>
        { !collapsed && <div style={{
          height: '32px', 
          overflow: 'hidden', 
          lineHeight: '32px'
        }}>嗨购后台管理系统</div> }
      </div>
      <Menu
        theme="dark"
        mode="inline"
        defaultSelectedKeys={['1']}
        items={ menus }
      />
    </Sider>
  );
};

export default App;

7.3 低版本处理

以上菜单项的设置在antd 4.20.0版本以上好使,如果在4.20.0版本以下,应该使用 递归组件实现

// src/layout/components/SideBar.tsx
import React from 'react';

import { Layout, Menu, Image } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'

const { Sider } = Layout;

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)

  // 自定义左侧菜单栏 - 递归
  const renderMenus = (menus: any[]) => {
    return menus.map(item => {
      if (item.children) {
        return (
          <Menu.SubMenu title = { item.label } key = { item.key }>
            { renderMenus(item.children) }
          </Menu.SubMenu>
        )
      } else {
        return <Menu.Item key = { item.key }>{ item.label }</Menu.Item>
      }
    })
  }


  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" style={ { 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center',
        color: '#fff'
      }}>
        <Image src = { logo } width="28px" height="28px" preview={ false }></Image>
        { !collapsed && <div style={{
          height: '32px', 
          overflow: 'hidden', 
          lineHeight: '32px'
        }}>嗨购后台管理系统</div> }
      </div>
      <Menu
        theme="dark"
        mode="inline"
        defaultSelectedKeys={['1']}
      >
        {
          renderMenus(menus)
        }
      </Menu>
      
    </Sider>
  );
};

export default App;

组件形式渲染左侧菜单目前并不推荐使用

7.4 菜单渲染优化

如果左侧菜单栏数据过于庞大,每个管理项里又有很多项,需要只展开一个菜单项

// src/layout/components/SideBar.tsx
import React, { useState } from 'react';

import { Layout, Menu, Image } from 'antd';

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'

const { Sider } = Layout;

// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
  if (item.children) {
    rootSubmenuKeys.push(item.key as string)
  }
})

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)


  const [openKeys, setOpenKeys] = useState(['sub1']);

  const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
    const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
    if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
      setOpenKeys(keys);
    } else {
      setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
    }
  };


  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" style={ { 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center',
        color: '#fff'
      }}>
        <Image src = { logo } width="28px" height="28px" preview={ false }></Image>
        { !collapsed && <div style={{
          height: '32px', 
          overflow: 'hidden', 
          lineHeight: '32px'
        }}>嗨购后台管理系统</div> }
      </div>
      <Menu
        theme="dark"
        mode="inline"
        defaultSelectedKeys={['1']}
        items={ menus }
        openKeys={openKeys}
        onOpenChange={onOpenChange}
      />
      
    </Sider>
  );
};

export default App;

8.定义路由

8.1 官方文档

https://reactrouter.com/

8.2 创建对应的页面

|-src
|  |- ...
|  |-views
|    |- banner
|    	|- List.tsx     #首页轮播图
|	 |  |- Add.tsx		#添加轮播图
|	 	 |- home
|    |  |- Index.tsx	#系统首页
|    |- pro
|    |  |- List.tsx 	#产品管理
|    |  |- Search.tsx 	#筛选列表
|    |- account
|    |  |- User.tsx #用户列表
|    |  |- Admin.tsx#管理员列表
// src/views/home/Index.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>系统首页</div>
  )
}

export default Com
// src/views/account/Admin.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>管理员列表</div>
  )
}

export default Com
// src/views/account/User.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>用户列表</div>
  )
}

export default Com
// src/views/banner/Add.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>添加轮播图</div>
  )
}

export default Com
// src/views/banner/List.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>轮播图列表</div>
  )
}

export default Com
// src/views/pro/List.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>产品列表</div>
  )
}

export default Com
// src/views/pro/Search.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>筛选列表</div>
  )
}

export defa

8.3 定义菜单路由信息

v6的路由通过 element 属性定义匹配的组件

因此menus中可以添加一个 element 属性,值就为组件的引用即可

// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />
      }
    ]
  }
]

export default menus

8.4.装载路由

在根组件添加 BrowserRouter 或者 HashRouter

// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'

import { BrowserRouter } from 'react-router-dom'

import App from './App';
import reportWebVitals from './reportWebVitals';

import 'antd/dist/reset.css'; // antd重置样式表

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLDivElement
);
root.render(
  <React.StrictMode>
    <ConfigProvider
      theme = { {
        token: {
          colorPrimary: '#1890ff'
        }
      } }
    >
      <Provider store = { store }>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </Provider>
    </ConfigProvider>
  </React.StrictMode>
);

reportWebVitals();

8.5 定义路由组件

menu.tsx里已经定义好了请求的路径(其实就是数据中key属性)和路径对应组件(其实就是数据中的element属性),剩下就是定义路由组件了

组件渲染的区域 AppMain 组件

// src/layout/components/AppMain.tsx
import React from 'react';

import { Layout, theme } from 'antd';
import { Routes, Route, Navigate } from 'react-router-dom';

// import BannerAdd from '@/views/banner/Add'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'

const { Content } = Layout;

const App: React.FC = () => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const renderRoute: any = (menus: IMyMenuItem[]) => {
    return menus.map(item => {
      if (item.children) {
        // React.Fragment 也为空标签,可以设置 key 属性
        // 实现 重定向 
        return (
          <React.Fragment key = { item.path }>
            <Route path = { item.path } element = { <Navigate to = { item.redirect! } />} />
            {
              renderRoute(item.children!)
            }
          </React.Fragment>
        )
      } else {
        return <Route key = { item.path } path = { item.path } element = { item.element } />
      }
    })
  }
  return (
    <Content
      style={{
        margin: '24px 16px',
        padding: 24,
        minHeight: 280,
        background: colorBgContainer,
      }}
    >
      <Routes>
        {/* <Route path="/banner" element = { <Navigate to="/banner/add" /> } /> */}
        {/* <Route path="/banner/add" element = { <BannerAdd /> } /> */}
        { renderRoute(menus) }
      </Routes>
    </Content>
  );
};

export default App;

8.6 手动测试路由

可以在地址栏输入路径,测试是否正常

http://localhost:3000/ 					#系统首页

http://localhost:3000/banner			#轮播图管理
http://localhost:3000/banner/list		#轮播图列表
http://localhost:3000/banner/add		#添加轮播图

http://localhost:3000/pro				#产品管理
http://localhost:3000/pro/search		#筛选列表
http://localhost:3000/pro/list			#产品列表

http://localhost:3000/account			#账户管理
http://localhost:3000/account/user	#用户列表
http://localhost:3000/account/admin	#管理员列表

8.7 设置404页面

// src/views/error/Page404.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>404</div>
  )
}

export default Com
// src/layout/components/AppMain.tsx
import React from 'react';

import { Layout, theme } from 'antd';
import { Routes, Route, Navigate } from 'react-router-dom';

// import BannerAdd from '@/views/banner/Add'

import Page404 from '@/views/error/Page404'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'

const { Content } = Layout;

const App: React.FC = () => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const renderRoute: any = (menus: IMyMenuItem[]) => {
    return menus.map(item => {
      if (item.children) {
        // React.Fragment 也为空标签,可以设置 key 属性
        // 实现 重定向 
        return (
          <React.Fragment key = { item.path }>
            <Route path = { item.path } element = { <Navigate to = { item.redirect! } />} />
            {
              renderRoute(item.children!)
            }
          </React.Fragment>
        )
      } else {
        return <Route key = { item.path } path = { item.path } element = { item.element } />
      }
    })
  }
  return (
    <Content
      style={{
        margin: '24px 16px',
        padding: 24,
        minHeight: 280,
        background: colorBgContainer,
      }}
    >
      <Routes>
        {/* <Route path="/banner" element = { <Navigate to="/banner/add" /> } /> */}
        {/* <Route path="/banner/add" element = { <BannerAdd /> } /> */}
        { renderRoute(menus) }
        <Route path="*" element = { <Page404 /> } />
      </Routes>
    </Content>
  );
};

export default App;

9 切换路由

上述项目中,切换路由都是手动输入的,实际上应该点击左侧菜单栏进行路由导航。

左侧菜单的逻辑交互,前面已经生成了(openKeys 以及 onOpenChanges 实现)

现在通过点击事件来切换导航

9.1 点击切换路由

// src/layout/components/SideBar.tsx
import React, { useState } from 'react';

import { Layout, Menu, Image } from 'antd';

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useNavigate } from 'react-router-dom';

const { Sider } = Layout;

// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
  if (item.children) {
    rootSubmenuKeys.push(item.key as string)
  }
})

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)


  const [openKeys, setOpenKeys] = useState(['']);

  const onOpenChange: MenuProps['onOpenChange'] = (keys) => { 
    // console.log('keys', keys)
    const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
    // console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
    if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
      setOpenKeys(keys);
    } else {
      setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
    }
  };

  const navigate = useNavigate()
  const changeUrl = ({ key }: { key: string }) => {
    console.log(key)
    navigate(key)
  }
  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" style={ { 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center',
        color: '#fff'
      }}>
        <Image src = { logo } width="28px" height="28px" preview={ false }></Image>
        { !collapsed && <div style={{
          height: '32px', 
          overflow: 'hidden', 
          lineHeight: '32px'
        }}>嗨购后台管理系统</div> }
      </div>
      <Menu
        theme="dark"
        mode="inline"
        defaultSelectedKeys={['1']}
        items={ menus }
        openKeys={openKeys}
        onOpenChange={onOpenChange}
        onClick={changeUrl}
      />
      
    </Sider>
  );
};

export default App;

9.2 刷新保持左侧菜单状态

当页面刷新时,需要保证当前二级路由是展开的,且当前路由是被选中的状态

// src/layout/components/SideBar.tsx
import React, { useState } from 'react';

import { Layout, Menu, Image } from 'antd';

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';

const { Sider } = Layout;

// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
  if (item.children) {
    rootSubmenuKeys.push(item.key as string)
  }
})

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)

  // /pro/search
  const { pathname } = useLocation() // /pro/search
  // console.log(location)
  const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
  const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
  const onOpenChange: MenuProps['onOpenChange'] = (keys) => { 
    // console.log('keys', keys)
    const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
    // console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
    if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
      setOpenKeys(keys);
    } else {
      setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
    }
  };

  const navigate = useNavigate()
  const changeUrl = ({ key }: { key: string }) => {
    // console.log(key)
    navigate(key)
    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"
        selectedKeys={ selectedKeys }
        items={ menus }
        openKeys={openKeys}
        onOpenChange={onOpenChange}
        onClick={changeUrl}
      />
      
    </Sider>
  );
};

export default App;

10 设置面包屑导航

10.1 参考文档

通过案例项目,得知 面包屑组件应该包含在 页面的头部 https://vvbin.cn/next/#/feat/breadcrumb/flat

参照组件库的面包屑 https://ant-design.gitee.io/components/breadcrumb-cn/#components-breadcrumb-demo-react-router

10.2 设置面包屑导航

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

// src/layout/components/AppHeader.tsx
import React from 'react';
import {
  MenuFoldOutlined,
  MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { useLocation, Link } from 'react-router-dom'
import menus, { IMyMenuItem } from '@/router/menu'
const { Header } = Layout;

// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}

function getBreadcrumbNameMap (menus: any[]) {
  menus.forEach(item => {
    if (item.children) {
      breadcrumbNameMap[item.path] = item.label
      getBreadcrumbNameMap(item.children)
    } else {
      breadcrumbNameMap[item.path] = item.label
    }
  })
}
console.log(breadcrumbNameMap)

getBreadcrumbNameMap(menus)
const App: React.FC = () => {
  // const [collapsed, setCollapsed] = useState(false);
  const collapsed = useAppSelector(state => state.app.collapsed)
  const dispatch = useAppDispatch()
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const location = useLocation(); // /pro/list
  const pathSnippets = location.pathname.split('/').filter((i) => i);
  console.log(pathSnippets) // ['pro', 'list']

  const extraBreadcrumbItems = pathSnippets.map((_, index) => {
    const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
    console.log(url) // /pro   /pro/list
    return (
      <Breadcrumb.Item key={url}>
        <Link to={url}>{breadcrumbNameMap[url]}</Link>
      </Breadcrumb.Item>
    );
  });

  const breadcrumbItems = [
    <Breadcrumb.Item key="home">
      <Link to="/">系统首页</Link>
    </Breadcrumb.Item>,
  ].concat(extraBreadcrumbItems);

  return (
    <Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>
      {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
        className: 'trigger',
        // onClick: () => setCollapsed(!collapsed),
        onClick: () => dispatch(changeCollapsed())
      })}
      <Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb>
    </Header>
  );
};

export default App;

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

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

// src/layout/components/SideBar.tsx
import React, { useEffect, useState } from 'react';

import { Layout, Menu, Image } from 'antd';

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';

const { Sider } = Layout;

// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
  if (item.children) {
    rootSubmenuKeys.push(item.key as string)
  }
})

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)

  // /pro/search
  const { pathname } = useLocation() // /pro/search
  // console.log(location)
  const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
  const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
  const onOpenChange: MenuProps['onOpenChange'] = (keys) => { 
    // console.log('keys', keys)
    const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
    // console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
    if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
      setOpenKeys(keys);
    } else {
      setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
    }
  };

  const navigate = useNavigate()
  const changeUrl = ({ key }: { key: string }) => {
    // console.log(key)
    navigate(key)
    setSelectedKeys([key]) // 点击时需要告诉程序哪一项被选中
  }

  useEffect(() => { // ++++++++++++
    setSelectedKeys([pathname])
    setOpenKeys(['/' + pathname.split('/')[1] ])
  }, [pathname])
  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" style={ { 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center',
        color: '#fff'
      }}>
        <Image src = { logo } width="28px" height="28px" preview={ false }></Image>
        { !collapsed && <div style={{
          height: '32px', 
          overflow: 'hidden', 
          lineHeight: '32px'
        }}>嗨购后台管理系统</div> }
      </div>
      <Menu
        theme="dark"
        mode="inline"
        selectedKeys={ selectedKeys }
        items={ menus }
        openKeys={openKeys}
        onOpenChange={onOpenChange}
        onClick={changeUrl}
      />
      
    </Sider>
  );
};

export default App;

11.快捷切换页

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

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

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

11.1 准备组件

// src/layout/components/AppTabs.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
 
  return (
    <div style={{ height: 40, background: '#fff', borderTop: '1px solid #ccc' }}>AppTabs</div>
  )
}

export default Com
// src/layout/components/index.ts

export { default as SideBar } from './SideBar'
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
export { default as AppTabs } from './AppTabs'
// src/layout/Index.tsx
import React from 'react';

import { Layout } from 'antd';

// import SideBar from './components/SideBar'
// import AppHeader from './components/AppHeader'
// import AppMain from './components/AppMain'
import { SideBar, AppHeader, AppMain, AppTabs } from './components'

const App: React.FC = () => {

  return (
    <Layout id="components-layout-demo-custom-trigger">
      <SideBar />
      <Layout className="site-layout">
        <AppHeader />
        <AppTabs />
        <AppMain />
      </Layout>
    </Layout>
  );
};

export default App;

11.2 处理数据

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

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

11.3 监听路由添加数据

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

// src/layout/components/AppTabs.tsx

import React, { FC, useEffect, useState } from 'react';
import menus from '@/router/menu'
import { useLocation, useNavigate } from 'react-router-dom';
import { Tag } from 'antd'
interface IAppProps {
}
// 需要的原始数据
const tabAttr: { label: any; key: any; }[] = []
function getTabAttrs (menus: any[]) {
  menus.forEach(item => {
    if (item.children) {
      getTabAttrs(item.children)
    } else {
      tabAttr.push({
        label: item.label,
        key: item.key
      })
    }
  })
}
getTabAttrs(menus)
// console.log('tabAttr', tabAttr)
const Com: FC<IAppProps> = (props) => {
  // 当前地址栏的地址
  const { pathname } = useLocation()
  // 快捷导航的数组
  const [arr, setArr] = useState([{ label: '系统首页', key: '/' }])
  // 选中的索引值 - 加样式
  const [current, setCurrent] = useState(0)

  const [num, setNum] = useState(0) // 为了获取最新的数据

  useEffect(() => {
    // 判断当前的路由在不在快捷导航的数组中
    const index = arr.findIndex(item => item.key === pathname)
    if (index !== -1) {
      // 如果在,拿到索引值,添加样式
      setCurrent(index)
    } else {
      // 如果不在快捷导航数组中
      // 从原始数据中获取值
      const item = tabAttr.find(item => item.key === pathname)

      const newArr = arr
      item && newArr.push(item)
      // 修改状态
      setArr(newArr)
      setCurrent(arr.length - 1)
    }
  }, [pathname, arr, num]) // 一旦num发生变化 一定会获取到最新的数据
  const navigate = useNavigate()
  return (
    <div style={{ height: 40, background: '#fff', borderTop: '1px solid #ccc', paddingLeft: 16, display: 'flex', flexWrap: 'nowrap', overflowX: 'auto' }}>
      {
        arr && arr.map((item, index) => {
          return (
            <Tag 
              style = {{ height: 26, lineHeight: '26px', marginTop: 7, borderRadius: 0, cursor: 'pointer'}}
              onClose={ () => {
                // 当前选中的这一项删除
                if (current === index) {
                  // 选中上一个数据,跳转页面
                  navigate(arr[index - 1].key)
                  setCurrent(current - 1) 
                  
                } else {
                  // 未选中删除
                  if (index < current) { // 删除选中左边
                    // 索引值减一
                    setCurrent(current - 1)
                  } else {
                    // 让组件的状态发生改变
                    // console.log('current', current)
                    // console.log('arr', arr)
                    // console.log('index', index)
                    setNum(Math.random()) // 初始获取最新数据
                  }
                }
                // 删除数据
                const deleteArr = arr
                deleteArr.splice(index, 1)
                setArr(deleteArr)
                
              }} 
              onClick = { () => {
                // console.log('test', index)
                
                navigate(arr[index].key)
              }}
              closable = { index !== 0 } key = { item.key } color = { current === index ? '#108ee9': '#ccc' }>
              { item.label }
            </Tag>
          )
        })
      }
    </div>
  )
}

export default Com

12.数据请求的封装

// src/utils/request.ts
import axios, { AxiosRequestConfig } from  'axios'
import store2 from 'store2'

const isDev = process.env.NODE_ENV === 'development'

const ins = axios.create({
  baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
})

ins.interceptors.request.use(config => {

  config.headers!.token = store2.get('token') || ''

  return config
}, error => Promise.reject(error))

ins.interceptors.response.use(response => {
  if (response.data.code === '10119') {
    store2.remove('token')
    store2.remove('adminname')
    window.location.href = "/login"
  }
  return response
}, error => Promise.reject(error))

// 自定义各种常用的restful api的请求
// axios.get('url', { params: { key: value } })
// axios.post('url', { key: value })
// axios({ url: '', method: 'GET', params: { key: value }})
// axios({ url: '', method: 'POST', data: { key: value }})
export default function request( config: AxiosRequestConfig ) {
  // 接口请求 必须参数  url method  data  headers
  const { url = '', method = 'GET', data = {}, headers = {} } = config

  // 区分不同的数据请求 为了执行时传入的数据请求方式统一性 GEt GeT get GET
  switch (method.toUpperCase()) {
    case 'GET':
      return ins.get(url, { params: data })

    case 'POST': 
      // 可能数据请求方式 表单提交  文件提交   默认json
      // 表单提交
      if (headers['content-type'] === 'application/x-www-form-url-encoded') {
        // 转换参数  URLSearchParams  / 第三方库 qs
        const p = new URLSearchParams()
        for (const key in data) {
          p.append(key, data[key])
        }
        return ins.post(url, p, { headers })
      }

      // 文件提交
      if (headers['content-type'] === 'multipart/form-data') {
        const p = new FormData()
        for (const key in data) {
          p.append(key, data[key])
        }
        return ins.post(url, p, { headers })
      }

      // 默认 application/json
      return ins.post(url, data)
    
    // 修改数据 - 所有的数据的更新
    case 'PUT':
      return ins.put(url, data)

    // 删除数据
    case 'DELETE': 
      return ins.delete(url, { data })  

    // 修改数据 - 部分的数据的更新
    case 'PATCH':
      return ins.patch(url, data)

    default:
      return ins(config)
  }
}

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

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

13 构建登录页面

13.1 参考组件库组件

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

13.2 构造登录接口API

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

13.3 创建登录的页面

// src/views/login/Index.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>登录</div>
  )
}

export default Com

13.4 创建登录路由

// src/App.tsx
import React, { FC } from 'react';

import { Routes, Route } from 'react-router-dom'

import Index from '@/layout/Index'
import Login from '@/views/login/Index'

import './App.css'

interface IAppProps {
}

const App: FC<IAppProps> = (props) => {
  return (
    <Routes>
      <Route path="/login" element = { <Login /> } />
      <Route path='/*' element = { <Index /> } />
      {/* <Index /> */}
    </Routes>
  )
}

export default App

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

13.4 完善登录界面

/* src/views/login/login.module.css */

.loginBox {
  width: 100%;
  height: 100%;
  background-color: #2d3a4b;
  display: flex;
  justify-content: center;
  align-items: center;
}

.loginForm {
  width: 460px;
  height: 350px;
  /* background-color: #fff; */
}
.loginTitle {
  text-align: center;
  color: #fff;
  font-size: 26px;
  margin-bottom: 30px;
}
.myInput {
  height: 47px;
  background-color: #2d3a4b;
}
.myInput input {
  background-color: #2d3a4b;
  color: #fff;
}
.myInput input::-webkit-input-placeholder{
  color:#fff;
}
.loginBtn {
  height: 36px;
}
.tip {
  display: flex;
  color: #fff;
  width: 50%;
}
.tip div {
  flex: 1;
}
// src/views/login/Index.tsx

import { IAdminLoginParams } from '@/api/admin';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import React, { FC } from 'react';

import style from './login.module.css'
interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  const onFinish = (values: IAdminLoginParams) => {
    console.log('Received values of form: ', values);
  };
  return (
    <div className={ style.loginBox }>
      <div className={ style.loginForm }>
        <h1 className={ style.loginTitle }>系统登录</h1>
        <Form
          name="normal_login"
          className="login-form"
          initialValues={ { adminname: 'admin', password: '123456' }}
          onFinish={onFinish}
        >
          <Form.Item
            name="adminname"
            rules={[{ required: true, message: '请输入管理员账户!' }]}
          >
            <Input 
              className={ style.myInput } 
              style = {{ color: '#fff' }}
              prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账户" />
          </Form.Item>
          <Form.Item
            name="password"
            rules={[{ required: true, message: '请输入密码!' }]}
          >
            <Input
              className={ style.myInput }
              style = {{ color: '#fff' }}
              prefix={<LockOutlined className="site-form-item-icon" />}
              type="password"
              placeholder="密码"
            />
          </Form.Item>

          <Form.Item>
            <Button className={ style.loginBtn } block type="primary" htmlType="submit" >
              登录
            </Button>
            
          </Form.Item>
          <div className={ style.tip }>
            <div>账户:admin</div>
            <div>密码:123456</div>
          </div>
        </Form>
      </div>
    </div>
  )
}

export default Com

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

14 执行登录

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

14.1 构建模块 admins

// src/store/modules/admin.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import store2 from 'store2'
interface IAppState {
  loginState: boolean
  adminname: string
  token: string
}

const initialState: IAppState = {
  loginState: Boolean(store2.get('loginState')) || false,
  adminname: store2.get('adminname') || '',
  token: store2.get('token') || ''
}

export const appSlice = createSlice({
  name: 'admin',
  initialState,
  reducers: {
    changeLoginState (state, action: PayloadAction<boolean>) {
      state.loginState = action.payload
      store2.set('loginState', action.payload)
    },
    changeAdminName (state, action: PayloadAction<string>) {
      state.adminname = action.payload
      store2.set('adminname', String(state.adminname))
    },
    changeToken (state, action: PayloadAction<string>) {
      state.token = action.payload
      store2.set('token', String(state.token))
    }
  }
})

export const { changeLoginState, changeAdminName, changeToken } = appSlice.actions

export default appSlice.reducer

14.2 装载模块

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'

import app from './modules/app'
import admin from './modules/admin'

const store = configureStore({
  reducer: {
    app,
    admin
  }
})

// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch

export default store

14.3 登录实现

// src/views/login/Index.tsx

import { IAdminLoginParams, loginFn } from '@/api/admin';
import { useAppDispatch } from '@/store/hooks';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input, message } from 'antd';
import React, { FC } from 'react';
import { changeAdminName, changeLoginState, changeToken } from '@/store/modules/admin'
import style from './login.module.css'
import { useNavigate } from 'react-router-dom';
interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {

  const dispatch = useAppDispatch()
  const navigate = useNavigate()

  const onFinish = (values: IAdminLoginParams) => {
    console.log('Received values of form: ', values);
    
    loginFn(values).then(res => {
      if (res.data.code === '10003') {
        message.warning('密码输入错误')
      } else if (res.data.code === '10005') {
        message.error('账户不存在')
      } else {
        message.success('登录成功')
        console.log(res.data.data)

        dispatch(changeLoginState(true))
        dispatch(changeAdminName(res.data.data.adminname))
        dispatch(changeToken(res.data.data.token))

        navigate('/')
      }
    })

  };
  return (
    <div className={ style.loginBox }>
      <div className={ style.loginForm }>
        <h1 className={ style.loginTitle }>系统登录</h1>
        <Form
          name="normal_login"
          className="login-form"
          initialValues={ { adminname: 'admin', password: '123456' }}
          onFinish={onFinish}
        >
          <Form.Item
            name="adminname"
            rules={[{ required: true, message: '请输入管理员账户!' }]}
          >
            <Input 
              className={ style.myInput } 
              style = {{ color: '#fff' }}
              prefix={<UserOutlined className="site-form-item-icon" />} placeholder="管理员账户" />
          </Form.Item>
          <Form.Item
            name="password"
            rules={[{ required: true, message: '请输入密码!' }]}
          >
            <Input
              className={ style.myInput }
              style = {{ color: '#fff' }}
              prefix={<LockOutlined className="site-form-item-icon" />}
              type="password"
              placeholder="密码"
            />
          </Form.Item>

          <Form.Item>
            <Button className={ style.loginBtn } block type="primary" htmlType="submit" >
              登录
            </Button>
            
          </Form.Item>
          <div className={ style.tip }>
            <div>账户:admin</div>
            <div>密码:123456</div>
          </div>
        </Form>
      </div>
    </div>
  )
}

export default Com

15.前端登录验证

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

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

// src/App.tsx
import React, { FC } from 'react';

import { Routes, Route, Navigate } from 'react-router-dom'

import Index from '@/layout/Index'
import Login from '@/views/login/Index'

import './App.css'
import { useAppSelector } from './store/hooks';

interface IAppProps {
}

const App: FC<IAppProps> = (props) => {
  const loginState = useAppSelector(state => state.admin.loginState)
  return (
    <Routes>
      <Route path="/login" element = { loginState ? <Navigate to="/"/> :<Login /> } />
      <Route path='/*' element = { loginState ? <Index /> : <Navigate to="/login"/> } />
      {/* <Index /> */}
    </Routes>
  )
}

export default App

16 .后端token校验

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

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

// src/utils/request.ts
import axios, { AxiosRequestConfig } from  'axios'
import store2 from 'store2'

const isDev = process.env.NODE_ENV === 'development'

const ins = axios.create({
  baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
})

ins.interceptors.request.use(config => {

  config.headers!.token = store2.get('token') || ''

  return config
}, error => Promise.reject(error))

ins.interceptors.response.use(response => {
  if (response.data.code === '10119') {
    store2.remove('token')
    store2.remove('adminname')
    store2.remove('loginState')
    window.location.href = "/login"
  }
  return response
}, error => Promise.reject(error))

// 自定义各种常用的restful api的请求
// axios.get('url', { params: { key: value } })
// axios.post('url', { key: value })
// axios({ url: '', method: 'GET', params: { key: value }})
// axios({ url: '', method: 'POST', data: { key: value }})
export default function request(config: AxiosRequestConfig) {
  // 接口请求 必须参数  url method  data  headers
  const { url = '', method = 'GET', data = {}, headers = {} } = config

  // 区分不同的数据请求 为了执行时传入的数据请求方式统一性 GEt GeT get GET
  switch (method.toUpperCase()) {
    case 'GET':
      return ins.get(url, { params: data })

    case 'POST': 
      // 可能数据请求方式 表单提交  文件提交   默认json
      // 表单提交
      if (headers['content-type'] === 'application/x-www-form-url-encoded') {
        // 转换参数  URLSearchParams  / 第三方库 qs
        const p = new URLSearchParams()
        for (const key in data) {
          p.append(key, data[key])
        }
        return ins.post(url, p, { headers })
      }

      // 文件提交
      if (headers['content-type'] === 'multipart/form-data') {
        const p = new FormData()
        for (const key in data) {
          p.append(key, data[key])
        }
        return ins.post(url, p, { headers })
      }

      // 默认 application/json
      return ins.post(url, data)
    
    // 修改数据 - 所有的数据的更新
    case 'PUT':
      return ins.put(url, data)

    // 删除数据
    case 'DELETE': 
      return ins.delete(url, { data })  

    // 修改数据 - 部分的数据的更新
    case 'PATCH':
      return ins.patch(url, data)

    default:
      return ins(config)
  }
}

17.退出登录

17.1 实现退出登录

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

// src/layout/components/AppHeader.tsx
import React from 'react';
import {
  DownOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;

// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}

function getBreadcrumbNameMap (menus: any[]) {
  menus.forEach(item => {
    if (item.children) {
      breadcrumbNameMap[item.path] = item.label
      getBreadcrumbNameMap(item.children)
    } else {
      breadcrumbNameMap[item.path] = item.label
    }
  })
}
// console.log(breadcrumbNameMap)

getBreadcrumbNameMap(menus)
const App: React.FC = () => {
  // const [collapsed, setCollapsed] = useState(false);
  const collapsed = useAppSelector(state => state.app.collapsed)
  const dispatch = useAppDispatch()
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const location = useLocation(); // /pro/list
  const pathSnippets = location.pathname.split('/').filter((i) => i);
  // console.log(pathSnippets) // ['pro', 'list']

  const extraBreadcrumbItems = pathSnippets.map((_, index) => {
    const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
    // console.log(url) // /pro   /pro/list
    return (
      <Breadcrumb.Item key={url}>
        <Link to={url}>{breadcrumbNameMap[url]}</Link>
      </Breadcrumb.Item>
    );
  });

  const breadcrumbItems = [
    <Breadcrumb.Item key="home">
      <Link to="/">系统首页</Link>
    </Breadcrumb.Item>,
  ].concat(extraBreadcrumbItems);

  const items: MenuProps['items'] = [
    {
      label: '个人中心',
      key: '/center',
    },
    {
      type: 'divider',
    },
    {
      label: '退出',
      key: '/logout',
    },
  ];
  const navigate = useNavigate()

  const onClick: MenuProps['onClick'] = ({ key }) => {
    // console.log(key)
    // navigate(key)
    if (key === '/logout') {
      store2.remove('loginState')
      store2.remove('adminname')
      store2.remove('token')
      dispatch(changeLoginState(false)) // 只需要修改 loginState
      navigate('/login')
    }
  }
  return (
    <Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>
      {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
        className: 'trigger',
        // onClick: () => setCollapsed(!collapsed),
        onClick: () => dispatch(changeCollapsed())
      })}
      <Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb>
      <div style={{ position: 'absolute', right: 16 }}>
        <Dropdown menu={{ items, onClick }} trigger={['click']} >
          <span onClick={(e) => e.preventDefault()}>
            <Space>
              <Image preview = { false } style={{ width: 40, height: 40, borderRadius: '10px' }} src='https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80' />
              <DownOutlined />
            </Space>
          </span>
        </Dropdown>
      </div>
    </Header>
  );
};

export default App;

17.2 保留退出时的页面

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

// src/layout/components/AppHeader.tsx
import React from 'react';
import {
  DownOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;

// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}

function getBreadcrumbNameMap (menus: any[]) {
  menus.forEach(item => {
    if (item.children) {
      breadcrumbNameMap[item.path] = item.label
      getBreadcrumbNameMap(item.children)
    } else {
      breadcrumbNameMap[item.path] = item.label
    }
  })
}
// console.log(breadcrumbNameMap)

getBreadcrumbNameMap(menus)
const App: React.FC = () => {
  // const [collapsed, setCollapsed] = useState(false);
  const collapsed = useAppSelector(state => state.app.collapsed)
  const dispatch = useAppDispatch()
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const location = useLocation(); // /pro/list
  const pathSnippets = location.pathname.split('/').filter((i) => i);
  // console.log(pathSnippets) // ['pro', 'list']

  const extraBreadcrumbItems = pathSnippets.map((_, index) => {
    const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
    // console.log(url) // /pro   /pro/list
    return (
      <Breadcrumb.Item key={url}>
        <Link to={url}>{breadcrumbNameMap[url]}</Link>
      </Breadcrumb.Item>
    );
  });

  const breadcrumbItems = [
    <Breadcrumb.Item key="home">
      <Link to="/">系统首页</Link>
    </Breadcrumb.Item>,
  ].concat(extraBreadcrumbItems);

  const items: MenuProps['items'] = [
    {
      label: '个人中心',
      key: '/center',
    },
    {
      type: 'divider',
    },
    {
      label: '退出',
      key: '/logout',
    },
  ];
  const navigate = useNavigate()
  const { pathname } = useLocation()

  const onClick: MenuProps['onClick'] = ({ key }) => {
    // console.log(key)
    // navigate(key)
    if (key === '/logout') {
      store2.remove('loginState')
      store2.remove('adminname')
      store2.remove('token')
      dispatch(changeLoginState(false)) // 只需要修改 loginState
      // navigate('/login')
      navigate('/login?r=' + pathname)
    }
  }
  return (
    <Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>
      {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
        className: 'trigger',
        // onClick: () => setCollapsed(!collapsed),
        onClick: () => dispatch(changeCollapsed())
      })}
      <Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb>
      <div style={{ position: 'absolute', right: 16 }}>
        <Dropdown menu={{ items, onClick }} trigger={['click']} >
          <span onClick={(e) => e.preventDefault()}>
            <Space>
              <Image preview = { false } style={{ width: 40, height: 40, borderRadius: '10px' }} src='https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80' />
              <DownOutlined />
            </Space>
          </span>
        </Dropdown>
      </div>
    </Header>
  );
};

export default App;

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

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

// src/App.tsx
import React, { FC } from 'react';

import { Routes, Route, Navigate, useSearchParams, useLocation } from 'react-router-dom'

import Index from '@/layout/Index'
import Login from '@/views/login/Index'

import './App.css'
import { useAppSelector } from './store/hooks';

interface IAppProps {
}

const App: FC<IAppProps> = (props) => {
  const loginState = useAppSelector(state => state.admin.loginState)
  // 1
  // const [params] = useSearchParams()
  // console.log('params', params.get('r') as string)
  // const url = params.get('r') as string

  // 2
  const location = useLocation()
  console.log('location', location.search)
  const url = location.search.split('?r=')[1]
  return (
    <Routes>
      <Route path="/login" element = { loginState ? <Navigate to={ url ? url : "/" }/> :<Login /> } />
      <Route path='/*' element = { loginState ? <Index /> : <Navigate to="/login"/> } />
      {/* <Index /> */}
    </Routes>
  )
}

export default App

18.隐藏左侧菜单项

添加一个设置页面

// src/views/set/Index.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  return (
    <div>设置</div>
  )
}

export default Com
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />
      }
    ]
  },
  {
    path: '/set',
    label: '设置',
    key: '/set',
    icon: <HomeOutlined />,
    element: <Set />
  },
]

export default menus

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

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

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

// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />,
        hidden: 1
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />
      }
    ]
  },
  {
    path: '/set',
    label: '设置',
    key: '/set',
    icon: <HomeOutlined />,
    element: <Set />,
    hidden: 1
  },
]

export default menus

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

// src/layout/components/AppHeader.tsx
import React from 'react';
import {
  DownOutlined,
  MenuFoldOutlined,
  MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;

// const breadcrumbNameMap: any = {
//   '/': '系统首页',
//   '/banner': '轮播图管理',
//   '/banner/list': '轮播图列表',
//   '/banner/add': '添加轮播图',
//   '/pro': '产品管理',
//   '/pro/list': '产品列表',
//   '/pro/search': '筛选列表',
//   '/account': '账户管理',
//   '/account/user': '用户列表',
//   '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}

function getBreadcrumbNameMap (menus: any[]) {
  menus.forEach(item => {
    if (item.children) {
      breadcrumbNameMap[item.path] = item.label
      getBreadcrumbNameMap(item.children)
    } else {
      breadcrumbNameMap[item.path] = item.label
    }
  })
}
// console.log(breadcrumbNameMap)

getBreadcrumbNameMap(menus)
const App: React.FC = () => {
  // const [collapsed, setCollapsed] = useState(false);
  const collapsed = useAppSelector(state => state.app.collapsed)
  const dispatch = useAppDispatch()
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const location = useLocation(); // /pro/list
  const pathSnippets = location.pathname.split('/').filter((i) => i);
  // console.log(pathSnippets) // ['pro', 'list']

  const extraBreadcrumbItems = pathSnippets.map((_, index) => {
    const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
    // console.log(url) // /pro   /pro/list
    return (
      <Breadcrumb.Item key={url}>
        <Link to={url}>{breadcrumbNameMap[url]}</Link>
      </Breadcrumb.Item>
    );
  });

  const breadcrumbItems = [
    <Breadcrumb.Item key="home">
      <Link to="/">系统首页</Link>
    </Breadcrumb.Item>,
  ].concat(extraBreadcrumbItems);

  const items: MenuProps['items'] = [
    {
      label: '个人中心',
      key: '/center',
    },
    {
      label: '设置',
      key: '/set',
    },
    {
      type: 'divider',
    },
    {
      label: '退出',
      key: '/logout',
    },
  ];
  const navigate = useNavigate()
  const { pathname } = useLocation()

  const onClick: MenuProps['onClick'] = ({ key }) => {
    // console.log(key)
    // navigate(key)
    if (key === '/logout') {
      store2.remove('loginState')
      store2.remove('adminname')
      store2.remove('token')
      dispatch(changeLoginState(false)) // 只需要修改 loginState
      // navigate('/login')
      navigate('/login?r=' + pathname)
    } else if (key === '/set') {
      navigate(key)
    }
  }
  return (
    <Header style={{ padding: 0, background: colorBgContainer,display: 'flex' }}>
      {React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
        className: 'trigger',
        // onClick: () => setCollapsed(!collapsed),
        onClick: () => dispatch(changeCollapsed())
      })}
      <Breadcrumb style={{ marginTop: 20 }}>{breadcrumbItems}</Breadcrumb>
      <div style={{ position: 'absolute', right: 16 }}>
        <Dropdown menu={{ items, onClick }} trigger={['click']} >
          <span onClick={(e) => e.preventDefault()}>
            <Space>
              <Image preview = { false } style={{ width: 40, height: 40, borderRadius: '10px' }} src='https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80' />
              <DownOutlined />
            </Space>
          </span>
        </Dropdown>
      </div>
    </Header>
  );
};

export default App;

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

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

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

    items.forEach(item => { // 因为上面只提取了第一层级的数据
     if(item.children) {
       let a = getData(item.children)
       item.children = a
     }
    })
    return items
}

19. 管理员管理

19.1.设计接口

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

// 修改管理员信息
export function updateAdmin (params: IUpdateParams) {
  return request({
    url: '/admin/update',
    method: 'POST',
    data: params
  })
}

// 删除
export function deleteAdmin (params: { adminid: string }) {
  return request({
    url: '/admin/delete',
    method: 'POST',
    data: params
  })
}

19.2.展示管理员列表

// src/views/account/Admin.tsx

import { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render () { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Button danger shape="circle" icon={<DeleteOutlined />} />
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  useEffect(() => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }, [])

  return (
    <>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey="adminid"
      />
    </>
  )
}

export default Com

19.3 优化表格滚动

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

height scroll

// src/views/account/Admin.tsx

import { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render () { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Button danger shape="circle" icon={<DeleteOutlined />} />
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  useEffect(() => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Button type='primary'>添加管理员</Button>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
      />
    </Space>
  )
}

export default Com

19.4 优化表格的分页器

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

// src/views/account/Admin.tsx

import { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render () { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Button danger shape="circle" icon={<DeleteOutlined />} />
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  useEffect(() => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  

  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Button type='primary'>添加管理员</Button>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
      />
    </Space>
  )
}

export default Com

19.5 添加中文包

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

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

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

// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';

import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'

import zhCN from 'antd/locale/zh_CN';

import { BrowserRouter } from 'react-router-dom'

import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store'

import 'antd/dist/reset.css'; // antd重置样式表

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLDivElement
);
root.render(
  <React.StrictMode>
    <ConfigProvider
      locale={ zhCN }
      theme = { {
        token: {
          colorPrimary: '#1890ff'
        }
      } }
    >
      <Provider store = { store }>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </Provider>
    </ConfigProvider>
  </React.StrictMode>
);

reportWebVitals()

19.6删除管理员

// src/views/account/Admin.tsx

import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Popconfirm, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  

  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Button type='primary'>添加管理员</Button>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
      />
    </Space>
  )
}

export default Com

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

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

// src/views/account/Admin.tsx

import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Popconfirm, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange,
  };

  const flag = useMemo(() => {
    return selectedRowKeys.length > 0
  }, [selectedRowKeys])

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  
  const deleteMany = () => {
    // promise.all
    const arr: any = []
    selectedRowKeys.forEach(item => {
      arr.push(deleteAdmin({ adminid: String(item) }))
    })
    Promise.all(arr).then(() => {
      getAdminListData()
      setSelectedRowKeys([])
    })
  }
  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Space>
        <Button type='primary'>添加管理员</Button>
        { flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }
      </Space>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
        rowSelection={rowSelection}
      />
    </Space>
  )
}

export default Com

19.9.添加管理员

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

// src/views/account/Admin.tsx

import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}
const Com: FC<IAppProps> = (props) => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange,
  };

  const flag = useMemo(() => {
    return selectedRowKeys.length > 0
  }, [selectedRowKeys])

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  
  const deleteMany = () => {
    // promise.all
    const arr: any = []
    selectedRowKeys.forEach(item => {
      arr.push(deleteAdmin({ adminid: String(item) }))
    })
    Promise.all(arr).then(() => {
      getAdminListData()
      setSelectedRowKeys([])
    })
  }

  const [open, setOpen] = useState<boolean>(false)
  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Space>
        <Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>
        { flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }
      </Space>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
        rowSelection={rowSelection}
      />
      <Drawer title="添加管理员" placement="right" onClose={ () => { setOpen(false)} } open={open}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input placeholder="管理员账户" />
          <Input placeholder="密码" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ () => {}}
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
        </Space>
        
      </Drawer>
    </Space>
  )
}

export default Com

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

// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />,
    keyid: '0-0'
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    keyid: '0-1',
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />,
        keyid: '0-1-0'
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />,
        hidden: 1,
        keyid: '0-1-1'
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    keyid: '0-2',
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />,
        keyid: '0-2-0'
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />,
        keyid: '0-2-1'
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    keyid: '0-3',
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />,
        keyid: '0-3-0'
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />,
        keyid: '0-3-1'
      }
    ]
  },
  {
    path: '/set',
    label: '设置',
    key: '/set',
    icon: <HomeOutlined />,
    element: <Set />,
    hidden: 1,
    keyid: '0-4'
  },
]

export default menus

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

// src/views/account/Admin.tsx

import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus, { IMyMenuItem } from '@/router/menu'
import type { DataNode } from 'antd/es/tree';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}

const getTreeData = (menus: any[] ) => {
  const arr: DataNode[] = []
  menus.forEach(item => {
    let obj: DataNode = {
      key: '',
      title: ''
    }
    if (item.children) {
      obj = {
        key: item.keyid,
        title: item.label,
        children: getTreeData(item.children)
      }
    } else {
      obj = {
        key: item.keyid,
        title: item.label
      }
    }
    arr.push(obj)
  })
  return arr
}
const Com: FC<IAppProps> = (props) => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange,
  };

  const flag = useMemo(() => {
    return selectedRowKeys.length > 0
  }, [selectedRowKeys])

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  
  const deleteMany = () => {
    // promise.all
    const arr: any = []
    selectedRowKeys.forEach(item => {
      arr.push(deleteAdmin({ adminid: String(item) }))
    })
    Promise.all(arr).then(() => {
      getAdminListData()
      setSelectedRowKeys([])
    })
  }

  const [open, setOpen] = useState<boolean>(false)
  const [checkedKeys, setCheckedKeys] = useState(['0-0'])
  const [adminname, setAdminname] = useState('')
  const [password, setPassword] = useState('')
  const [role, setRole] = useState(1)
  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Space>
        <Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>
        { flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }
      </Space>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
        rowSelection={rowSelection}
      />
      <Drawer title="添加管理员" placement="right" onClose={ () => { setOpen(false)} } open={open}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" />
          <Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ (value) => {
              setRole(value)
            }}
            value = { role }
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
          <Tree
            checkable
            treeData={getTreeData(menus)}
            onCheck={(checkedKeysValue: any) => {
              console.log(checkedKeysValue)
              setCheckedKeys(checkedKeysValue)
            }}
            checkedKeys={checkedKeys}
          />
          <Button type='primary' onClick={
            () => {
              const data = { adminname, password, role, checkedKeys }
              console.log(data)
            }
          }>添加</Button>
        </Space>
        
      </Drawer>
    </Space>
  )
}

export default Com

19.9.4 添加管理员

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

// src/views/account/Admin.tsx

import { addAdmin, deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}

const getTreeData = (menus: any[] ) => {
  const arr: DataNode[] = []
  menus.forEach(item => {
    let obj: DataNode = {
      key: '',
      title: ''
    }
    if (item.children) {
      obj = {
        key: item.keyid,
        title: item.label,
        children: getTreeData(item.children)
      }
    } else {
      obj = {
        key: item.keyid,
        title: item.label
      }
    }
    arr.push(obj)
  })
  return arr
}
const Com: FC<IAppProps> = (props) => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange,
  };

  const flag = useMemo(() => {
    return selectedRowKeys.length > 0
  }, [selectedRowKeys])

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  
  const deleteMany = () => {
    // promise.all
    const arr: any = []
    selectedRowKeys.forEach(item => {
      arr.push(deleteAdmin({ adminid: String(item) }))
    })
    Promise.all(arr).then(() => {
      getAdminListData()
      setSelectedRowKeys([])
    })
  }

  const [open, setOpen] = useState<boolean>(false)
  const [checkedKeys, setCheckedKeys] = useState(['0-0'])
  const [adminname, setAdminname] = useState('')
  const [password, setPassword] = useState('')
  const [role, setRole] = useState(1)
  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Space>
        <Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>
        { flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }
      </Space>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
        rowSelection={rowSelection}
      />
      <Drawer title="添加管理员" placement="right" onClose={ () => { 
         setAdminname('')
         setPassword('')
         setRole(1)
         setCheckedKeys(['0-0'])
         setOpen(false)
      } } open={open}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" />
          <Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ (value) => {
              setRole(value)
            }}
            value = { role }
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
          <Tree
            checkable
            treeData={getTreeData(menus)}
            onCheck={(checkedKeysValue: any) => {
              console.log(checkedKeysValue)
              setCheckedKeys(checkedKeysValue)
            }}
            checkedKeys={checkedKeys}
          />
          <Button type='primary' onClick={
            () => {
              const data = { adminname, password, role, checkedKeys }
              console.log(data)
              addAdmin(data).then(() => {
                getAdminListData()
                setAdminname('')
                setPassword('')
                setRole(1)
                setCheckedKeys(['0-0'])
                setOpen(false)
              })

            }
          }>添加</Button>
        </Space>
        
      </Drawer>
    </Space>
  )
}

export default Com

19.10管理员修改

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

// src/views/account/Admin.tsx

import { addAdmin, deleteAdmin, getAdminList, updateAdmin } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Modal, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}

const getTreeData = (menus: any[] ) => {
  const arr: DataNode[] = []
  menus.forEach(item => {
    let obj: DataNode = {
      key: '',
      title: ''
    }
    if (item.children) {
      obj = {
        key: item.keyid,
        title: item.label,
        children: getTreeData(item.children)
      }
    } else {
      obj = {
        key: item.keyid,
        title: item.label
      }
    }
    arr.push(obj)
  })
  return arr
}
const Com: FC<IAppProps> = (props) => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange,
  };

  const flag = useMemo(() => {
    return selectedRowKeys.length > 0
  }, [selectedRowKeys])

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" onClick={ () => {
              setIsModalOpen(true)
              setAdminname(record.adminname)
              setRole(record.role)
              setCheckedKeys(record.checkedKeys)
            }} shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  
  const deleteMany = () => {
    // promise.all
    const arr: any = []
    selectedRowKeys.forEach(item => {
      arr.push(deleteAdmin({ adminid: String(item) }))
    })
    Promise.all(arr).then(() => {
      getAdminListData()
      setSelectedRowKeys([])
    })
  }

  const [open, setOpen] = useState<boolean>(false)
  const [checkedKeys, setCheckedKeys] = useState(['0-0'])
  const [adminname, setAdminname] = useState('')
  const [password, setPassword] = useState('')
  const [role, setRole] = useState(1)

  const [isModalOpen, setIsModalOpen] = useState(false)
  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Space>
        <Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>
        { flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }
      </Space>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
        rowSelection={rowSelection}
      />
      <Drawer title="添加管理员" placement="right" onClose={ () => { 
         setAdminname('')
         setPassword('')
         setRole(1)
         setCheckedKeys(['0-0'])
         setOpen(false)
      } } open={open}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" />
          <Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ (value) => {
              setRole(value)
            }}
            value = { role }
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
          <Tree
            checkable
            treeData={getTreeData(menus)}
            onCheck={(checkedKeysValue: any) => {
              console.log(checkedKeysValue)
              setCheckedKeys(checkedKeysValue)
            }}
            checkedKeys={checkedKeys}
          />
          <Button type='primary' onClick={
            () => {
              const data = { adminname, password, role, checkedKeys }
              console.log(data)
              addAdmin(data).then(() => {
                getAdminListData()
                setAdminname('')
                setPassword('')
                setRole(1)
                setCheckedKeys(['0-0'])
                setOpen(false)
              })

            }
          }>添加</Button>
        </Space>
        
      </Drawer>
      <Modal title="编辑管理员" open={isModalOpen} footer={ null }  onCancel={() => {
        setIsModalOpen(false)
        setAdminname('')
        setRole(1)
        setCheckedKeys(['0-0'])
      }}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input readOnly value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ (value) => {
              setRole(value)
            }}
            value = { role }
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
          <Tree
            checkable
            treeData={getTreeData(menus)}
            onCheck={(checkedKeysValue: any) => {
              console.log(checkedKeysValue)
              setCheckedKeys(checkedKeysValue)
            }}
            checkedKeys={checkedKeys}
          />
          <Button type='primary' onClick={
            () => {
              const data = { adminname, role, checkedKeys }
              console.log(data)
              updateAdmin(data).then(() => {
                getAdminListData()
                setAdminname('')
                setRole(1)
                setCheckedKeys(['0-0'])
                setIsModalOpen(false)
              })

            }
          }>更新</Button>
        </Space>
      </Modal>
    </Space>
  )
}

export default Com

20 系统首页数据统计

// src/api/home.ts
import request from '@/utils/request'

export function getUserTotalNum () {
  return request({
    url: '/statistic/user'
  })
}

export function getShopTotalNum () {
  return request({
    url: '/statistic/product'
  })
}
// src/views/home/Index.tsx

import { getShopTotalNum, getUserTotalNum } from '@/api/home';
import { Col, Row, Statistic } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import CountUp from 'react-countup';
interface IAppProps {
}
const formatter: any = (value: number) => <CountUp end={value} separator="," />;
const Com: FC<IAppProps> = (props) => {
  const [usersLen, setUsersLen] = useState(0)
  const [prosLen, setProsLen] = useState(0)

  useEffect(() => {
    getUserTotalNum().then(res => setUsersLen(res.data.data))
    getShopTotalNum().then(res => setProsLen(res.data.data))
  }, [])
  return (
    <div>
      <Row gutter={16}>
        <Col span={6}>
          <Statistic style = {{ backgroundColor: '#efefef', padding: "10px 20px"}} title="用户总数量" valueStyle={{ color: '#3f8600' }} value={usersLen} formatter={formatter} />
        </Col>
        <Col span={6}>
          <Statistic style = {{ backgroundColor: '#efefef', padding: "10px 20px"}} title="产品总数量" valueStyle={{ color: '#cf1322' }} value={prosLen} formatter={formatter} />
        </Col>
      </Row>
    </div>
  )
}

export default Com

21 左侧菜单栏的权限

21.1 思路

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

21.2 算法过程

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

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

21.3 算法实现

算法1:

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

// ['0-0', '0-1-0', '0-2-0-0', '0-2-0-1', '0-3-2'] 
// ['0-0', '0-1', '0-1-0',  '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
let arr = ['0-0','0-1-0','0-2-0-0','0-2-0-1','0-3-2', '0-4-0-0-2', '0-5-1-2-0-1']
// 0-0
// 0-1 0-1-0
// 0-2 0-2-0 0-2-0-0
// 0-2 0-2-0 0-2-0-1
// 0-3 0-3-2
// 0-4 0-4-0 0-4-0-0  0-4-0-0-2
// 0-5 0-5-1 0-5-1-2  0-5-1-2-0  0-5-1-2-0-1

// let brr = []
// for(let i = 0; i < arr.length; i++){
//   for(let j = 0; j < arr[i].length; j += 2){
//     brr.push(arr[i].substring(0, j + 3))
//   }
// }

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

console.log(brr); 

算法2:

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

import { IMyMenuItem } from "./menu"

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

  return arr
}

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

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

21.4 生成动态的左侧菜单项

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

admin账户 渲染 原始的 menus 数据

// src/layout/components/SideBar.tsx
import React, { useEffect, useState } from 'react';

import { Layout, Menu, Image } from 'antd';

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';
import { getAdminDetail } from '@/api/admin';
import { getCheckedKeysArr, getPermissionMenu } from '@/router/utils';

const { Sider } = Layout;

// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
  if (item.children) {
    rootSubmenuKeys.push(item.key as string)
  }
})

const App: React.FC = () => {
  
  const collapsed = useAppSelector(state => state.app.collapsed)

  // /pro/search
  const { pathname } = useLocation() // /pro/search
  // console.log(location)
  const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
  const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
  const onOpenChange: MenuProps['onOpenChange'] = (keys) => { 
    // console.log('keys', keys)
    const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
    // console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
    if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
      setOpenKeys(keys);
    } else {
      setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
    }
  };

  const navigate = useNavigate()
  const changeUrl = ({ key }: { key: string }) => {
    // console.log(key)
    navigate(key)
    setSelectedKeys([key]) // 点击时需要告诉程序哪一项被选中
  }

  useEffect(() => {
    setSelectedKeys([pathname])
    setOpenKeys(['/' + pathname.split('/')[1] ])
  }, [pathname])

  const adminname = useAppSelector(state => state.admin.adminname)
  const [showMenu, setShowMenu] = useState<any>([])
  useEffect(() => {
    getAdminDetail({ adminname }).then(res => {
      // console.log(res.data.data)
      const oldCheckedKeys = res.data.data[0].checkedKeys
      const checkedKeysArr = getCheckedKeysArr(oldCheckedKeys)
      const newMenus = adminname === 'admin' ? menus : getPermissionMenu(menus, checkedKeysArr)
      setShowMenu(newMenus)
    })
  }, [adminname])

  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <div className="logo" style={ { 
        display: 'flex', 
        justifyContent: 'center', 
        alignItems: 'center',
        color: '#fff'
      }}>
        <Image src = { logo } width="28px" height="28px" preview={ false }></Image>
        { !collapsed && <div style={{
          height: '32px', 
          overflow: 'hidden', 
          lineHeight: '32px'
        }}>嗨购后台管理系统</div> }
      </div>
      <Menu
        theme="dark"
        mode="inline"
        selectedKeys={ selectedKeys }
        items={ showMenu }
        openKeys={openKeys}
        onOpenChange={onOpenChange}
        onClick={changeUrl}
      />
      
    </Sider>
  );
};

export default App;

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

22、页面权限

也称之为路由权限

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

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

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

核心思想:

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

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

import { IMyMenuItem } from "./menu"

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

  return arr
}

// 判断当前请求的地址是不是在路由系统中
export function isContainMenus (menus: IMyMenuItem[], pathname: string) { // ++++++++++
  let bool = menus.some(item => {
    if (item.children) {
      if (item.key === pathname) {
        return true
      } else {
        return item.children.some(it => it!.key === pathname)
      }
    } else {
      return item.key === pathname
    }
  })
  
  return bool
}
// src/layout/components/AppMain.tsx
import React, { useEffect, useState } from 'react';

import { Layout, theme } from 'antd';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';

// import BannerAdd from '@/views/banner/Add'

import Page404 from '@/views/error/Page404'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks';
import { getAdminDetail } from '@/api/admin';
import { getCheckedKeysArr, getPermissionMenu, isContainMenus } from '@/router/utils';

const { Content } = Layout;

const App: React.FC = () => {
  const {
    token: { colorBgContainer },
  } = theme.useToken();

  const adminname = useAppSelector(state => state.admin.adminname)
  const [showMenu, setShowMenu] = useState<any>([])
  useEffect(() => {
    getAdminDetail({ adminname }).then(res => {
      // console.log(res.data.data)
      const oldCheckedKeys = res.data.data[0].checkedKeys
      const checkedKeysArr = getCheckedKeysArr(oldCheckedKeys)
      const newMenus = adminname === 'admin' ? menus : getPermissionMenu(menus, checkedKeysArr)
      setShowMenu(newMenus)
    })
  }, [adminname])

  const renderRoute: any = (menus: IMyMenuItem[]) => {
    return menus.map(item => {
      if (item.children) {
        // React.Fragment 也为空标签,可以设置 key 属性
        // 实现 重定向 
        return (
          <React.Fragment key = { item.path }>
            <Route path = { item.path } element = { <Navigate to = { item.redirect! } />} />
            {
              renderRoute(item.children!)
            }
          </React.Fragment>
        )
      } else {
        return <Route key = { item.path } path = { item.path } element = { item.element } />
      }
    })
  }

  const { pathname } = useLocation()
  return (
    <Content
      style={{
        margin: '24px 16px',
        padding: 24,
        minHeight: 280,
        background: colorBgContainer,
      }}
    >
      <Routes>
        {/* <Route path="/banner" element = { <Navigate to="/banner/add" /> } /> */}
        {/* <Route path="/banner/add" element = { <BannerAdd /> } /> */}
        {/* { renderRoute(menus) } */}
        { renderRoute(showMenu) }
        <Route path="*" element = { isContainMenus(menus, pathname) ? <div>无权限</div> : <Page404 /> } />
      </Routes>
    </Content>
  );
};

export default App;

23、按钮权限

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

// src/views/account/Admin.tsx

import { addAdmin, deleteAdmin, getAdminDetail, getAdminList, updateAdmin } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';
import { useAppSelector } from '@/store/hooks';

interface IAppProps {
}
interface IAdmin {
  adminid: string
  adminname: string
  password: string
  role: number
  checkedKeys: any[]
}

const getTreeData = (menus: any[] ) => {
  const arr: DataNode[] = []
  menus.forEach(item => {
    let obj: DataNode = {
      key: '',
      title: ''
    }
    if (item.children) {
      obj = {
        key: item.keyid,
        title: item.label,
        children: getTreeData(item.children)
      }
    } else {
      obj = {
        key: item.keyid,
        title: item.label
      }
    }
    arr.push(obj)
  })
  return arr
}
const Com: FC<IAppProps> = (props) => {
  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
    console.log('selectedRowKeys changed: ', newSelectedRowKeys);
    setSelectedRowKeys(newSelectedRowKeys);
  };

  const rowSelection = {
    selectedRowKeys,
    onChange: onSelectChange,
  };

  const flag = useMemo(() => {
    return selectedRowKeys.length > 0
  }, [selectedRowKeys])

  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const columns = [
    {
      title: '序号',
      render (text: any, record: IAdmin, index: number) {
        return <> { (current - 1) * pageSize + index + 1 }</>
      }
    },
    {
      title: '管理员账户',
      dataIndex: 'adminname'
    },
    {
      title: '管理员角色',
      dataIndex: 'role',
      render (text: number) { // 自定义列信息
        return (
          <>
            { 
              text === 2 ? <Tag color="#f50">超级管理员</Tag> :
              <Tag color="#2db7f5">普通管理员</Tag>
            }
          </>
        )
      }
    },
    {
      title: '操作',
      render (text: any, record: IAdmin) { 
        return (
          <Space>
            <Button type="dashed" onClick={ () => {
              setIsModalOpen(true)
              setAdminname(record.adminname)
              setRole(record.role)
              setCheckedKeys(record.checkedKeys)
            }} shape="circle" icon={<EditOutlined />} />
            <Popconfirm
              title="确定删除吗"
              onConfirm={ () => {
                deleteAdmin({ adminid: record.adminid }).then(() => {
                  getAdminListData()
                })
              }}
              onCancel={() => {}}
              okText="删除"
              cancelText="取消"
            >
              <Button danger shape="circle" icon={<DeleteOutlined />} />
            </Popconfirm>
          </Space>
        )
      }
    }
  ]

  const [adminList, setAdminList] = useState([])
  const getAdminListData = () => {
    getAdminList().then(res => {
      console.log(res.data)
      setAdminList(res.data.data)
    })
  }
  useEffect(() => {
    getAdminListData()
  }, [])

  const [height] = useState(document.body.offsetHeight) // 计算body的高度

  
  const deleteMany = () => {
    if (deleteRole < 2) {
      message.error('暂无权限');
      
    } else {
      // promise.all
      const arr: any = []
      selectedRowKeys.forEach(item => {
        arr.push(deleteAdmin({ adminid: String(item) }))
      })
      Promise.all(arr).then(() => {
        getAdminListData()
        setSelectedRowKeys([])
      })
    }
    
  }

  const [open, setOpen] = useState<boolean>(false)
  const [checkedKeys, setCheckedKeys] = useState(['0-0'])
  const [adminname, setAdminname] = useState('')
  const [password, setPassword] = useState('')
  const [role, setRole] = useState(1)

  const [isModalOpen, setIsModalOpen] = useState(false)

  const name = useAppSelector(state => state.admin.adminname)
  const [deleteRole, setDeleteRole] = useState(1)
  useEffect(() => {
    getAdminDetail({ adminname: name }).then(res => {
      setDeleteRole(res.data.data[0].role)
    })
  })
  return (
    <Space direction='vertical' style = {{ width: '100%' }}>
      <Space>
        <Button type='primary' onClick={ () => setOpen(true) }>添加管理员</Button>
        { flag ? <Button type='primary' onClick={ deleteMany }>批量删除</Button> : null }
      </Space>
      <Table 
        dataSource={ adminList }
        columns = { columns }
        rowKey = "adminid"
        scroll={ { y: height - 330 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: adminList.length,
          showTotal: (total) => `共有 ${total} 条数据`
        } }
        rowSelection={rowSelection}
      />
      <Drawer title="添加管理员" placement="right" onClose={ () => { 
         setAdminname('')
         setPassword('')
         setRole(1)
         setCheckedKeys(['0-0'])
         setOpen(false)
      } } open={open}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" />
          <Input value = { password  } onChange = { event=> setPassword(event.target.value)} placeholder="密码" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ (value) => {
              setRole(value)
            }}
            value = { role }
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
          <Tree
            checkable
            treeData={getTreeData(menus)}
            onCheck={(checkedKeysValue: any) => {
              console.log(checkedKeysValue)
              setCheckedKeys(checkedKeysValue)
            }}
            checkedKeys={checkedKeys}
          />
          <Button type='primary' onClick={
            () => {
              const data = { adminname, password, role, checkedKeys }
              console.log(data)
              addAdmin(data).then(() => {
                getAdminListData()
                setAdminname('')
                setPassword('')
                setRole(1)
                setCheckedKeys(['0-0'])
                setOpen(false)
              })

            }
          }>添加</Button>
        </Space>
        
      </Drawer>
      <Modal title="编辑管理员" open={isModalOpen} footer={ null }  onCancel={() => {
        setIsModalOpen(false)
        setAdminname('')
        setRole(1)
        setCheckedKeys(['0-0'])
      }}>
        <Space direction='vertical' style={{ width: '100%'}}>
          <Input readOnly value = { adminname } onChange = { event=> setAdminname(event.target.value)} placeholder="管理员账户" />
          <Select
            style={{ width: '100%'}}
            defaultValue={ 1 }
            onChange={ (value) => {
              setRole(value)
            }}
            value = { role }
            options={[
              { value: 1, label: '普通管理员' },
              { value: 2, label: '超级管理员' }
            ]}
          />
          <Tree
            checkable
            treeData={getTreeData(menus)}
            onCheck={(checkedKeysValue: any) => {
              console.log(checkedKeysValue)
              setCheckedKeys(checkedKeysValue)
            }}
            checkedKeys={checkedKeys}
          />
          <Button type='primary' onClick={
            () => {
              const data = { adminname, role, checkedKeys }
              console.log(data)
              updateAdmin(data).then(() => {
                getAdminListData()
                setAdminname('')
                setRole(1)
                setCheckedKeys(['0-0'])
                setIsModalOpen(false)
              })

            }
          }>更新</Button>
        </Space>
      </Modal>
    </Space>
  )
}

export default Com

24、轮播图管理

24.1 封装接口

// src/api/banner.ts
import request from '@/utils/request'
export interface IAddBannerParams {
  img: string
  alt: string
  link: string
}
export function addBanner (params: IAddBannerParams) {
  return request({
    url: '/banner/add',
    method: 'POST',
    data: params
  })
}

export function getBannerList () {
  return request({
    url: '/banner/list'
  })
}

export function deleteBanner (params: { bannerid: string }) {
  return request({
    url: '/banner/delete',
    data: params
  })
}

24.2 轮播图页面渲染

// src/views/banner/List.tsx

import { deleteBanner, getBannerList } from '@/api/banner';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Image, Popconfirm, Table } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

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

export default Com

23.3 添加轮播图

// src/views/banner/Add.tsx

import { addBanner } from '@/api/banner';
import { Input, Space, Image, Button } from 'antd';
import React, { FC, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';

interface IAppProps {
}

const Com: FC<IAppProps> = (props) => {
  const navigate = useNavigate()

  const [link, setLink] = useState('')
  const [alt, setAlt] = useState('')
  const [img, setImg] = useState<any>('')

  const file = useRef<any>()

  const onChange = () => {
    const myFile = file.current.input.files[0]
    console.log(myFile)
    const reader = new FileReader()
    reader.readAsDataURL(myFile) // base64地址
    reader.onload = function () {
      setImg(this.result)
    }
  }
  const flag = useMemo(() => {
    return alt === '' || link === '' || img === ''
  }, [alt, img, link])

  const onAdd = () => {
    addBanner({
      alt, link, img
    }).then(() => {
      navigate(-1)
    })
  }
  return (
    <Space direction='vertical' style={{ width: 300 }}>
      <Input placeholder='link' value = {link} onChange = { e => setLink(e.target.value)}/>
      <Input placeholder='alt' value = {alt} onChange = { e => setAlt(e.target.value)}/>
      <Input type="file" ref = { file } onChange = { onChange }/>
      <Input placeholder='图片地址' value = {img} onChange = { e => setImg(e.target.value)}/>
      <Image src={img} />
      <Button type='primary' disabled = { flag } onClick={ onAdd }>添加</Button>
    </Space>
  )
}

export default Com

25.产品管理

25.1 封装接口

// src/api/pro.ts
import request from '@/utils/request'

export function getProList () {
  return request({
    url: '/pro/list'
  })
}

export function getSearchList (params?: { category?: string, search?: string}) {
  return request({
    url: '/pro/searchPro',
    method: 'POST',
    data: params
  })
}

export function getCategoryList () {
  return request({
    url: '/pro/getCategory'
  })
}


25.2 产品列表

// src/views/pro/List.tsx

import { getProList } from '@/api/pro';
import { DeleteOutlined } from '@ant-design/icons';
import { Table, Image, Popconfirm, Button } from 'antd';
import React, { FC, useEffect, useState } from 'react';

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

export default Com

25.3 筛选列表

// src/views/pro/Search.tsx

import { getCategoryList, getProList, getSearchList } from '@/api/pro';
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { Table, Image, Button, Space, Select, Input } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';

interface IAppProps {
}
const { Column } = Table;
const Com: FC<IAppProps> = (props) => {
  const [proList, setProList] = useState([])
  const getProListData = () => {
    getProList().then(res => setProList(res.data.data))
  }
  useEffect(() => {
    getProListData()
  }, [])
  const [height] = useState(document.body.offsetHeight) // 计算body的高度
  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const [categoryList, setCategoryList] = useState([])
  const [category, setCategory] = useState('')
  const [search, setSearch] = useState('')

  // const arr = [{ value: '', label: '全部' }]
  useEffect(() => {
    getCategoryList().then(res => {
      console.log(res.data.data)
      setCategoryList(res.data.data)
    
    })
  }, [])
  const arr = useMemo(() => {
    const brr: any = [{ value: '', label: '全部' }]
    categoryList.forEach((item: any) => {
      brr.push({
        value: item,
        label: item
      })
    })
    return brr
  }, [categoryList])
  return (
    <Space direction='vertical' style={{ width: '100%'}}>
      <Space>
        <Select
          style={{ width: 120 }}
          defaultValue=''
          onChange={ (value) => {
            setCategory(value)
          }}
          value = { category }
          options={ arr }
        />
        <Input  placeholder='输入需要搜索的关键词' value = { search } onChange = { event=> setSearch(event.target.value)} />
        <Button onClick={() => {
          getSearchList({ category, search }).then(res => {
            setProList(res.data.data)
          })
        }} type="primary" shape="circle" icon={<SearchOutlined />} />
      </Space>
      <Table dataSource={ proList } rowKey = "proid" scroll={ { y: height - 300 } }
        pagination = { {
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: proList.length,
          showTotal: (total: number) => `共有 ${total} 条数据`
        } }
      >
        <Column title="序号" render = {(text, record, index) => {
          return <span>{ (current - 1) * pageSize + index + 1 }</span>
        }} />
        <Column title="图片" dataIndex="img1" render = {(text) => {
          return <Image src = { text } style={{ height: 60, width: 100 }}></Image>
        }} />
        <Column title="产品名称" dataIndex="proname" />
        <Column title="价格" dataIndex="originprice" />
        <Column title="操作" dataIndex="img" render = {(text, record: any, index) => {
          return <Button danger shape="circle" icon={<DeleteOutlined />} />
        
        }} />
      </Table>
    </Space>
  )
}

export default Com

26.数据可视化

方案:

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

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

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

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

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

g2plot:https://g2plot.antv.vision/zh/

react中使用g2:https://charts.ant.design/zh-CN

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

1.echarts

添加页面以及配置路由

// src/views/data/Echarts.tsx
const Com = () => {
  return (
    <>
      echarts
    </>
  )

}
export default Com
// src/views/data/HighCharts.tsx
const Com = () => {
  return (
    <>
      HighCharts
    </>
  )

}
export default Com
// src/views/data/Antv.tsx
const Com = () => {
  return (
    <>
      Antv
    </>
  )

}
export default Com
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />,
    keyid: '0-0'
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    keyid: '0-1',
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />,
        keyid: '0-1-0'
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />,
        hidden: 1,
        keyid: '0-1-1'
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    keyid: '0-2',
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />,
        keyid: '0-2-0'
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />,
        keyid: '0-2-1'
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    keyid: '0-3',
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />,
        keyid: '0-3-0'
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />,
        keyid: '0-3-1'
      }
    ]
  },
  {
    path: '/set',
    label: '设置',
    key: '/set',
    icon: <HomeOutlined />,
    element: <Set />,
    hidden: 1,
    keyid: '0-4'
  },
  {
    path: '/data',
    label: '数据可视化',
    key: '/data',
    redirect: '/data/echarts',
    icon: <HomeOutlined />,
    keyid: '0-5',
    children: [
      {
        path: '/data/echarts',
        key: '/data/echarts',
        label: 'echarts',
        icon: <HomeOutlined />,
        element: <Echarts />,
        keyid: '0-5-0'
      },
      {
        path: '/data/HighCharts',
        key: '/data/HighCharts',
        label: 'HighCharts',
        icon: <HomeOutlined />,
        element: <HighCharts />,
        keyid: '0-5-1'
      },
      {
        path: '/data/antv',
        key: '/data/antv',
        label: 'antv',
        icon: <HomeOutlined />,
        element: <Antv />,
        keyid: '0-5-2'
      }
    ]
  },
]

export default menus
cnpm install echarts --save
// src/api/data.ts
import request from './../utils/request'

export function getData () {
  return request({
    url: '/data/simpleData'
  })
}

处理数据

自适应

// src/views/data/Echarts.tsx
import { getServerData } from "@/api/data";
import { Button, Col, Row } from "antd"
import * as echarts from 'echarts';
import { useEffect } from "react";
const Com = () => {
  useEffect(() => {
    var BarChart = echarts.init(document.getElementById('barCharts') as HTMLElement);
    BarChart.setOption({
      title: {
        text: 'ECharts 入门示例'
      },
      tooltip: {},
      xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
      },
      yAxis: {},
      series: [
        {
          name: '销量',
          type: 'bar',
          data: [5, 20, 36, 10, 10, 20]
        }
      ]
    })
  }, [])
  useEffect(() => {
    var BarChart = echarts.init(document.getElementById('lineCharts') as HTMLElement);
    BarChart.setOption({
      title: {
        text: 'ECharts 入门示例'
      },
      tooltip: {},
      xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
      },
      yAxis: {},
      series: [
        {
          name: '销量',
          type: 'line',
          data: [5, 20, 36, 10, 10, 20]
        }
      ]
    })
  }, [])

  useEffect(() => {
    var BarChart = echarts.init(document.getElementById('randomCharts') as HTMLElement);
    BarChart.setOption({
      title: {
        text: 'Stacked Line'
      },
      tooltip: {
        trigger: 'axis'
      },
      legend: {
        data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
      },
      grid: {
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
      },
      toolbox: {
        feature: {
          saveAsImage: {}
        }
      },
      xAxis: {
        type: 'category',
        boundaryGap: false,
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
        type: 'value'
      },
      series: [
        {
          name: 'Email',
          type: 'line',
          stack: 'Total',
          data: [120, 132, 101, 134, 90, 230, 210]
        },
        {
          name: 'Union Ads',
          type: 'line',
          stack: 'Total',
          data: [220, 182, 191, 234, 290, 330, 310]
        },
        {
          name: 'Video Ads',
          type: 'line',
          stack: 'Total',
          data: [150, 232, 201, 154, 190, 330, 410]
        },
        {
          name: 'Direct',
          type: 'line',
          stack: 'Total',
          data: [320, 332, 301, 334, 390, 330, 320]
        },
        {
          name: 'Search Engine',
          type: 'line',
          stack: 'Total',
          data: [820, 932, 901, 934, 1290, 1330, 1320]
        }
      ]
    })
  }, [])

  useEffect(() => {
    getServerData().then(res => {
      const arr: any =[]
      const brr: any = []
      res.data.data.forEach((item: { x: any; val: any; }) => {
        arr.push(item.x)
        brr.push(item.val)
      })
      var BarChart = echarts.init(document.getElementById('serverCharts') as HTMLElement);
      BarChart.setOption({
        title: {
          text: 'ECharts 入门示例'
        },
        tooltip: {},
        xAxis: {
          data: arr
        },
        yAxis: {},
        series: [
          {
            name: '销量',
            type: 'bar',
            data: brr
          }
        ]
      })
    })
  }, [])

return (
  <>
    echarts
    <Row gutter={15}>
      <Col span={12}>
        <p>柱状图</p>
        <div id='barCharts' style={{ width: '100%', height: 300 }}></div>
      </Col>
      <Col span={12}>
        <p>折线图</p>
        <div id='lineCharts' style={{ width: '100%', height: 300 }}></div>
      </Col>
    </Row>
    <Row gutter={15}>
      <Col span={12}>
        <p>随意图形</p>
        <div id='randomCharts' style={{ width: '100%', height: 300 }}></div>
      </Col>
      <Col span={12}>
        <p>服务器数据</p>
        <div id='serverCharts' style={{ width: '100%', height: 300 }}></div>
      </Col>
    </Row>
  </>
)

}
export default Com

2.Highcharts

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

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

cnpm install highcharts highcharts-react-official -S
// src/views/data/HighCharts.tsx
import React, { FC, useEffect, useState } from 'react';
import * as Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';

console.log(HighchartsReact)
interface IHighChartsProps {
  
};

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

export default HighCharts;

3.antv - g2

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

cnpm i @antv/g2 @antv/data-set -S
// src/views/data/Antv.tsx
import React, { FC, useEffect } from 'react';
import { Chart } from '@antv/g2';
interface IAntvProps {

};

const Antv: FC<IAntvProps> = () => {
  useEffect(() => {

    const data = [
      { year: '1951 年', sales: 38 },
      { year: '1952 年', sales: 52 },
      { year: '1956 年', sales: 61 },
      { year: '1957 年', sales: 145 },
      { year: '1958 年', sales: 48 },
      { year: '1959 年', sales: 38 },
      { year: '1960 年', sales: 38 },
      { year: '1962 年', sales: 38 },
    ];
    const chart = new Chart({
      container: 'antv',
      autoFit: true,
      height: 500,
    });

    chart.data(data);
    chart.scale('sales', {
      nice: true,
    });

    chart.tooltip({
      showMarkers: false
    });
    chart.interaction('active-region');

    chart.interval().position('year*sales');

    chart.render();

  }, [])

  return (
    <>
      <h1>Antv</h1>
      <div id="antv" style={{ width: 800, height: 600, backgroundColor: '#efefef' }}></div>
    </>
  )
};

export default Antv;

27.编辑器

// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'

import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />,
    keyid: '0-0'
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    keyid: '0-1',
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />,
        keyid: '0-1-0'
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />,
        hidden: 1,
        keyid: '0-1-1'
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    keyid: '0-2',
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />,
        keyid: '0-2-0'
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />,
        keyid: '0-2-1'
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    keyid: '0-3',
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />,
        keyid: '0-3-0'
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />,
        keyid: '0-3-1'
      }
    ]
  },
  {
    path: '/set',
    label: '设置',
    key: '/set',
    icon: <HomeOutlined />,
    element: <Set />,
    hidden: 1,
    keyid: '0-4'
  },
  {
    path: '/data',
    label: '数据可视化',
    key: '/data',
    redirect: '/data/echarts',
    icon: <HomeOutlined />,
    keyid: '0-5',
    children: [
      {
        path: '/data/echarts',
        key: '/data/echarts',
        label: 'echarts',
        icon: <HomeOutlined />,
        element: <Echarts />,
        keyid: '0-5-0'
      },
      {
        path: '/data/HighCharts',
        key: '/data/HighCharts',
        label: 'HighCharts',
        icon: <HomeOutlined />,
        element: <HighCharts />,
        keyid: '0-5-1'
      },
      {
        path: '/data/antv',
        key: '/data/antv',
        label: 'antv',
        icon: <HomeOutlined />,
        element: <Antv />,
        keyid: '0-5-2'
      }
    ]
  },
  {
    path: '/braft',
    label: '父文本编辑器',
    key: '/braft',
    icon: <HomeOutlined />,
    element: <Braft />,
    keyid: '0-6'
  },
  {
    path: '/md',
    label: 'markDown编辑器',
    key: '/md',
    icon: <HomeOutlined />,
    element: <Md />,
    keyid: '0-7'
  },
]

export default menus

1.富文本编辑器

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

cnpm i braft-editor -S
// src/views/edit/Braft.tsx
import React, { useState } from 'react';
import 'braft-editor/dist/index.css'
import BraftEditor from 'braft-editor'
type Props = {}

const  Com = (props: Props) => {
  const [editorState, setEditorState] = useState('')
  const [html, setHtml] = useState('')
  const handleChange = (editorState: any) => {
    console.log(editorState.toHTML())
    setEditorState(editorState)
    setHtml(editorState.toHTML())
  }
  return (
    <>
      <BraftEditor
        value={editorState}
        onChange={handleChange}
      />
      <div dangerouslySetInnerHTML={ { __html: html } }></div>
    </>
  );
}

export default Com

2.markDown编辑器

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

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

cnpm i react-markdown react-markdown-editor-lite -S
// src/views/edit/Md.tsx
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown' // 阅读器
import MdEditor from 'react-markdown-editor-lite'; // 编辑器
// import style manually
import 'react-markdown-editor-lite/lib/index.css'; // 样式
type Props = {}

const  Com = (props: Props) => {
  const [content, setContent] = useState('')
  return (
    <>
      <h1>Markdown展示</h1>
      <MdEditor style={{ height: '500px' }} renderHTML={text => {
        return <ReactMarkdown>{ text }</ReactMarkdown>
      }} onChange={ ( { html, text }: { html: any; text: any}) => {
        setContent(html)
      }} />

      <div dangerouslySetInnerHTML={ { __html: content } }></div>
    </>
  );
}

export default Com

28.导入以及导出

// src/views/excel/Import.tsx
import React from 'react';

type ComProps = {}

const Com = (props: ComProps) => (
  <>
    <h1>导入</h1>
  </>
);

export default Com
// src/views/excel/Export.tsx
import React from 'react';

type ComProps = {}

const Com = (props: ComProps) => (
  <>
    <h1>导出</h1>
  </>
);

export default Co
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'

import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'

import Import from '@/views/excel/Import'
import Export from '@/views/excel/Export'

type MenuItem = Required<MenuProps>['items'][number];

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


const menus: IMyMenuItem[] = [
  {
    path: '/',
    label: '系统首页',
    key: '/',
    icon: <HomeOutlined />,
    element: <Home />,
    keyid: '0-0'
  },
  {
    path: '/banner',
    label: '轮播图管理',
    key: '/banner',
    redirect: '/banner/list',
    icon: <HomeOutlined />,
    keyid: '0-1',
    children: [
      {
        path: '/banner/list',
        key: '/banner/list',
        label: '轮播图列表',
        icon: <HomeOutlined />,
        element: <BannerList />,
        keyid: '0-1-0'
      },
      {
        path: '/banner/add',
        key: '/banner/add',
        label: '添加轮播图',
        icon: <HomeOutlined />,
        element: <BannerAdd />,
        hidden: 1,
        keyid: '0-1-1'
      }
    ]
  },
  {
    path: '/pro',
    label: '产品管理',
    key: '/pro',
    redirect: '/pro/list',
    icon: <HomeOutlined />,
    keyid: '0-2',
    children: [
      {
        path: '/pro/list',
        key: '/pro/list',
        label: '产品列表',
        icon: <HomeOutlined />,
        element: <ProList />,
        keyid: '0-2-0'
      },
      {
        path: '/pro/search',
        key: '/pro/search',
        label: '筛选列表',
        icon: <HomeOutlined />,
        element: <SearchList />,
        keyid: '0-2-1'
      }
    ]
  },
  {
    path: '/account',
    label: '账户管理',
    key: '/account',
    redirect: '/account/user',
    icon: <HomeOutlined />,
    keyid: '0-3',
    children: [
      {
        path: '/account/user',
        key: '/account/user',
        label: '用户列表',
        icon: <HomeOutlined />,
        element: <UserList />,
        keyid: '0-3-0'
      },
      {
        path: '/account/admin',
        key: '/account/admin',
        label: '管理员列表',
        icon: <HomeOutlined />,
        element: <AdminList />,
        keyid: '0-3-1'
      }
    ]
  },
  {
    path: '/set',
    label: '设置',
    key: '/set',
    icon: <HomeOutlined />,
    element: <Set />,
    hidden: 1,
    keyid: '0-4'
  },
  {
    path: '/data',
    label: '数据可视化',
    key: '/data',
    redirect: '/data/echarts',
    icon: <HomeOutlined />,
    keyid: '0-5',
    children: [
      {
        path: '/data/echarts',
        key: '/data/echarts',
        label: 'echarts',
        icon: <HomeOutlined />,
        element: <Echarts />,
        keyid: '0-5-0'
      },
      {
        path: '/data/HighCharts',
        key: '/data/HighCharts',
        label: 'HighCharts',
        icon: <HomeOutlined />,
        element: <HighCharts />,
        keyid: '0-5-1'
      },
      {
        path: '/data/antv',
        key: '/data/antv',
        label: 'antv',
        icon: <HomeOutlined />,
        element: <Antv />,
        keyid: '0-5-2'
      }
    ]
  },
  {
    path: '/braft',
    label: '父文本编辑器',
    key: '/braft',
    icon: <HomeOutlined />,
    element: <Braft />,
    keyid: '0-6'
  },
  {
    path: '/md',
    label: 'markDown编辑器',
    key: '/md',
    icon: <HomeOutlined />,
    element: <Md />,
    keyid: '0-7'
  },
  {
    path: '/excel',
    label: '导入以及导出',
    key: '/excel',
    redirect: '/excel/export',
    icon: <HomeOutlined />,
    keyid: '0-8',
    children: [
      {
        path: '/excel/import',
        key: '/excel/import',
        label: '导入',
        icon: <HomeOutlined />,
        element: <Import />,
        keyid: '0-8-0'
      },
      {
        path: '/excel/export',
        key: '/excel/export',
        label: '导出',
        icon: <HomeOutlined />,
        element: <Export />,
        keyid: '0-8-1'
      }
    ]
  },
]

export default menus

1.导出

cnpm i js-export-excel -S

src/views/excel/test.d.ts

declare module 'js-export-excel'

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

// src/views/excel/Export.tsx

import { getCategoryList, getProList, getSearchList } from '@/api/pro';
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { Table, Image, Button, Space, Select, Input } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import ExportJsonExcel from 'js-export-excel'

interface IAppProps {
}
interface DataType {
  banners: string[]
  brand: 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 { Column } = Table;
const Com: FC<IAppProps> = (props) => {
  const [proList, setProList] = useState([])
  const getProListData = () => {
    getProList().then(res => setProList(res.data.data))
  }
  useEffect(() => {
    getProListData()
  }, [])
  const [height] = useState(document.body.offsetHeight) // 计算body的高度
  const [current, setCurrent] = useState(1)
  const [pageSize, setPageSize] = useState(10)
  const onChange = (page: number, pageSize: number) => {
    setCurrent(page)
    setPageSize(pageSize)
  }

  const [categoryList, setCategoryList] = useState([])
  const [category, setCategory] = useState('')
  const [search, setSearch] = useState('')

  // const arr = [{ value: '', label: '全部' }]
  useEffect(() => {
    getCategoryList().then(res => {
      console.log(res.data.data)
      setCategoryList(res.data.data)

    })
  }, [])
  const arr = useMemo(() => {
    const brr: any = [{ value: '', label: '全部' }]
    categoryList.forEach((item: any) => {
      brr.push({
        value: item,
        label: item
      })
    })
    return brr
  }, [categoryList])
  return (
    <Space direction='vertical' style={{ width: '100%' }}>
      <Space>
        <Select
          style={{ width: 120 }}
          defaultValue=''
          onChange={(value) => {
            setCategory(value)
          }}
          value={category}
          options={arr}
        />
        <Input placeholder='输入需要搜索的关键词' value={search} onChange={event => setSearch(event.target.value)} />
        <Button onClick={() => {
          getSearchList({ category, search }).then(res => {
            setProList(res.data.data)
          })
        }} type="primary" shape="circle" icon={<SearchOutlined />} />
        <Button onClick={() => {
          let option: {
            fileName: string
            datas: {
              sheetData: DataType[],
              sheetName: string,
              sheetFilter: string[],
              sheetHeader: string[],
              columnWidths: number[]
            }[]
          };
          option = {
            fileName: "产品列表", // 导出的文件的名称
            datas: [
              {
                sheetData: proList, // 表格数据
                sheetName: "产品列表1", // excel表格中表格的名字
                sheetFilter: ["proname", "img1", "category"], // 需要导出的数据的字段
                sheetHeader: ["产品名称", "图片", "分类"], // 表头的值
                columnWidths: [20, 20],
              },
              {
                sheetData: proList, // 表格数据
                sheetName: "产品列表2", // excel表格中表格的名字
                sheetFilter: ["proname", "img1", "category", 'originprice'], // 需要导出的数据的字段
                sheetHeader: ["产品名称", "图片", "分类", '价格'], // 表头的值
                columnWidths: [20, 20],
              },
            ]
          }

          var toExcel = new ExportJsonExcel(option); //new
          toExcel.saveExcel(); //保存
        }} type="primary">导出数据</Button>
      </Space>
      <Table dataSource={proList} rowKey="proid" scroll={{ y: height - 300 }}
        pagination={{
          // position: ['bottomLeft', 'topRight']
          showQuickJumper: true,
          showSizeChanger: true,
          current,
          pageSize,
          onChange,
          total: proList.length,
          showTotal: (total: number) => `共有 ${total} 条数据`
        }}
      >
        <Column title="序号" render={(text, record, index) => {
          return <span>{(current - 1) * pageSize + index + 1}</span>
        }} />
        <Column title="图片" dataIndex="img1" render={(text) => {
          return <Image src={text} style={{ height: 60, width: 100 }}></Image>
        }} />
        <Column title="产品名称" dataIndex="proname" />
        <Column title="价格" dataIndex="originprice" />
        <Column title="操作" dataIndex="img" render={(text, record: any, index) => {
          return <Button danger shape="circle" icon={<DeleteOutlined />} />

        }} />
      </Table>
    </Space>
  )
}

export default Com

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

2.导入

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

cnpm install xlsx
// src/views/excel/Import.tsx
import { Button, Table, Image, Switch } from 'antd';
import React, { useState } from 'react';
import * as XLSX from 'xlsx';
type ComProps = {}
interface DataType {
  banners: string[]
  brand: 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 Com = (props: ComProps) => {
  const [proList, setProList] = useState([])

  const importExcel = () => { // 导入数据
    const file = (document.getElementById('fileRef') as HTMLInputElement).files![0]
    const reader = new FileReader()
    reader.readAsBinaryString(file!) // 转成 二进制格式
    reader.onload = function () {
      const workbook = XLSX.read(this.result, { type: 'binary' });
      const t = workbook.Sheets['list'] // 拿到表格数据
      // console.log(t)
      const r: any = XLSX.utils.sheet_to_json(t) // 转换成json格式
      // console.log(r)
      setProList(r)
      // 将r的数据上传至服务器
    }
  }
  return (
    <>
      <h1>导入</h1>
      <Button onClick={() => { // 触发文件选择器
          (document.getElementById('fileRef') as HTMLInputElement).click()
        }}>导入数据</Button>
        <input type="file" hidden id = 'fileRef' onChange = { importExcel }/>
        <Table dataSource={ proList }  rowKey = "proid" scroll={{ y: 600}}>
        <Table.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>
    </>
  )
};

export default Com

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

const xlsx = require('node-xlsx').default;
// 导入excel表格的数据
router.get('/uploadPro', (req, res, next) => {
  const originData = xlsx.parse(`${__dirname}/pro.xlsx`);
  const firstData = originData[0].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],
      })
    }
  }
  // 拿到 arr 的数据,先清空 产品的表格数据,然后再插入
  mysql.delete(Product, {}, 1).then(() => { // 不要忘记写1,因为1 代表的是删除多条数据
    // 所有的数据已删除完成
    // 插入数据
    mysql.insert(Product, arr).then(() => {
      // 重定向到 商品的管理的页面路由
      res.send('导入数据成功') // 相当于浏览器自动跳转到了 /pro 的路由
    })
  })
})

29.地图

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

https://lbsyun.baidu.com/

// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';

interface IBaiduProps {
  
};

const Baidu:FC<IBaiduProps> = () => {
  
  return (
    <>
      <h1>百度地图</h1>
    </>
  )
};

export default Baidu;
// src/views/map/Gaode.tsx
import React, { FC, useEffect } from 'react';

interface IBaiduProps {
  
};

const Baidu:FC<IBaiduProps> = () => {
  
  return (
    <>
      <h1>高德地图</h1>
    </>
  )
};

export default Baidu;
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';

import Home from '@/views/home/Index'

import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'

import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'

import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'

import Set from '@/views/set/Index'

import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'

import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'

import Import from '@/views/excel/Import'
import Export from '@/views/excel/Export'

import Baidu from '@/views/map/Baidu'
import Gaode from '@/views/map/Gaode'


type MenuItem = Required<MenuProps>['items'][number];

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


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

export default menus
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
    <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=17qecKvCwmMWGwzVqPQvG9GQkRSPZHc8">
    </script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

// src/views/map/map.d.ts.
interface Window {
  BMapGL: any
}
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';

interface IBaiduProps {
  
};

const Baidu:FC<IBaiduProps> = () => {
  useEffect(() => {
    var map = new window.BMapGL.Map("allmap");
    map.centerAndZoom(new window.BMapGL.Point(116.280190, 40.049191), 19);
    map.enableScrollWheelZoom(true);
    map.setHeading(64.5);
    map.setTilt(73);
  }, [])
  return (
    <>
      <h1>百度地图</h1>
      <div id="allmap" style={{width:' 100%',height: '500px'}}></div>
    </>
  )
};

export default Baidu;

30.项目打包发布

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

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

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

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

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

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

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

package.json

{
	"homepage": '.'
}

http://121.89.205.189:2207/

port Home from ‘@/views/home/Index’

import BannerList from ‘@/views/banner/List’
import BannerAdd from ‘@/views/banner/Add’

import ProList from ‘@/views/pro/List’
import SearchList from ‘@/views/pro/Search’

import UserList from ‘@/views/account/User’
import AdminList from ‘@/views/account/Admin’

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

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

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

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

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

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

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

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

export default menus


```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
    <script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=17qecKvCwmMWGwzVqPQvG9GQkRSPZHc8">
    </script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

// src/views/map/map.d.ts.
interface Window {
  BMapGL: any
}
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';

interface IBaiduProps {
  
};

const Baidu:FC<IBaiduProps> = () => {
  useEffect(() => {
    var map = new window.BMapGL.Map("allmap");
    map.centerAndZoom(new window.BMapGL.Point(116.280190, 40.049191), 19);
    map.enableScrollWheelZoom(true);
    map.setHeading(64.5);
    map.setTilt(73);
  }, [])
  return (
    <>
      <h1>百度地图</h1>
      <div id="allmap" style={{width:' 100%',height: '500px'}}></div>
    </>
  )
};

export default Baidu;

30.项目打包发布

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

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

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

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

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

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

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

package.json

{
	"homepage": '.'
}

http://121.89.205.189:2207/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值