cra-ts-pro-reactRouter6-rtk

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(
  
    
  
);

reportWebVitals();

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

interface IAppProps {
}

const App: FC = (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(
  
    
  
);

reportWebVitals();

测试组件库

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

interface IAppProps {
}

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

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 (
    
      
        
, label: 'nav 1', }, { key: '2', icon: , label: 'nav 2', }, { key: '3', icon: , label: 'nav 3', }, ]} />
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => setCollapsed(!collapsed), })}
Content
); }; export default App;

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

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

import Index from '@/layout/Index'

import './App.css'

interface IAppProps {
}

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

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 (
    
      
, label: 'nav 1', }, { key: '2', icon: , label: 'nav 2', }, { key: '3', icon: , label: 'nav 3', }, ]} /> ); }; 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 (
    
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', onClick: () => setCollapsed(!collapsed), })}
); }; 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
    
  );
};

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 (
    
      
      
        
        
      
    
  );
};

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(
  
    
      
        
      
    
  
);

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 (
    
      
, label: 'nav 1', }, { key: '2', icon: , label: 'nav 2', }, { key: '3', icon: , label: 'nav 3', }, ]} /> ); }; 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 (
    
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', // onClick: () => setCollapsed(!collapsed), onClick: () => dispatch(changeCollapsed()) })}
); }; 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 (
    
      
{ !collapsed &&
嗨购后台管理系统
}
); }; 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 (
          
            { renderMenus(item.children) }
          
        )
      } else {
        return { item.label }
      }
    })
  }


  return (
    
      
{ !collapsed &&
嗨购后台管理系统
}
{ renderMenus(menus) }
); }; 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 (
    
      
{ !collapsed &&
嗨购后台管理系统
}
); }; 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 = (props) => {
  return (
    
系统首页
) } export default Com
// src/views/account/Admin.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
管理员列表
) } export default Com
// src/views/account/User.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
用户列表
) } export default Com
// src/views/banner/Add.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
添加轮播图
) } export default Com
// src/views/banner/List.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
轮播图列表
) } export default Com
// src/views/pro/List.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
产品列表
) } export default Com
// src/views/pro/Search.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
筛选列表
) } 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(
  
    
      
        
          
        
      
    
  
);

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 (
          
            } />
            {
              renderRoute(item.children!)
            }
          
        )
      } else {
        return 
      }
    })
  }
  return (
    
      
        {/*  } /> */}
        {/*  } /> */}
        { renderRoute(menus) }
      
    
  );
};

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 = (props) => {
  return (
    
404
) } 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 (
          
            } />
            {
              renderRoute(item.children!)
            }
          
        )
      } else {
        return 
      }
    })
  }
  return (
    
      
        {/*  } /> */}
        {/*  } /> */}
        { renderRoute(menus) }
         } />
      
    
  );
};

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 (
    
      
{ !collapsed &&
嗨购后台管理系统
}
); }; 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 (
    
      
{ !collapsed &&
嗨购后台管理系统
}
); }; 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 (
      
        {breadcrumbNameMap[url]}
      
    );
  });

  const breadcrumbItems = [
    
      系统首页
    ,
  ].concat(extraBreadcrumbItems);

  return (
    
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', // onClick: () => setCollapsed(!collapsed), onClick: () => dispatch(changeCollapsed()) })} {breadcrumbItems}
); }; 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 (
    
      
{ !collapsed &&
嗨购后台管理系统
}
); }; export default App;

11.快捷切换页

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

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

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

11.1 准备组件

// src/layout/components/AppTabs.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
 
  return (
    
AppTabs
) } 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 (
    
      
      
        
        
        
      
    
  );
};

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 = (props) => {
  return (
    
登录
) } 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 = (props) => {
  return (
    
       } />
       } />
      {/*  */}
    
  )
}

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 = (props) => {
  const onFinish = (values: IAdminLoginParams) => {
    console.log('Received values of form: ', values);
  };
  return (
    

系统登录

} placeholder="管理员账户" /> } type="password" placeholder="密码" />
账户:admin
密码:123456
) } 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 = (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 (
    

系统登录

} placeholder="管理员账户" /> } type="password" placeholder="密码" />
账户:admin
密码:123456
) } 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 = (props) => {
  const loginState = useAppSelector(state => state.admin.loginState)
  return (
    
       : } />
       :  } />
      {/*  */}
    
  )
}

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 (
      
        {breadcrumbNameMap[url]}
      
    );
  });

  const breadcrumbItems = [
    
      系统首页
    ,
  ].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 (
    
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', // onClick: () => setCollapsed(!collapsed), onClick: () => dispatch(changeCollapsed()) })} {breadcrumbItems}
e.preventDefault()}>
); }; 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 (
      
        {breadcrumbNameMap[url]}
      
    );
  });

  const breadcrumbItems = [
    
      系统首页
    ,
  ].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 (
    
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', // onClick: () => setCollapsed(!collapsed), onClick: () => dispatch(changeCollapsed()) })} {breadcrumbItems}
e.preventDefault()}>
); }; 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 = (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 (
    
       : } />
       :  } />
      {/*  */}
    
  )
}

export default App

18.隐藏左侧菜单项

添加一个设置页面

// src/views/set/Index.tsx

import React, { FC } from 'react';

interface IAppProps {
}

const Com: FC = (props) => {
  return (
    
设置
) } 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['items'][number];

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


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

export default menus

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

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

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

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

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

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

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

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

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

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

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


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

export default menus

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

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

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

function getBreadcrumbNameMap (menus: any[]) {
  menus.forEach(item => {
    if (item.children) {
      breadcrumbNameMap[item.path] = item.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 (
      
        {breadcrumbNameMap[url]}
      
    );
  });

  const breadcrumbItems = [
    
      系统首页
    ,
  ].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 (
    
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { className: 'trigger', // onClick: () => setCollapsed(!collapsed), onClick: () => dispatch(changeCollapsed()) })} {breadcrumbItems}
e.preventDefault()}>
); }; 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 = (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 ? 超级管理员 :
              普通管理员
            }
          
        )
      }
    },
    {
      title: '操作',
      render () { 
        return (
          
            
) } 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 = (props) => {
  const navigate = useNavigate()

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

  const file = useRef()

  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 (
    
       setLink(e.target.value)}/>
       setAlt(e.target.value)}/>
      
       setImg(e.target.value)}/>
      
      
    
  )
}

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 = (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 (
     `共有 ${total} 条数据`
        } }
      >
         {
          return { (current - 1) * pageSize + index + 1 }
        }} />
         {
          return 
        }} />
        
        
         {
          return 
) } 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 = (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 (
    
      
         setSearch(event.target.value)} />
        

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
    
      
        

柱状图

折线图

随意图形

服务器数据

) } 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 = (props: HighchartsReact.Props) => {
  const [option, setOption] = useState({
		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 (
    <>
      

HighCharts

) }; 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 = () => {
  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 (
    <>
      

Antv

) }; 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 (
    <>
      
      
); } 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 (
    <>
      

Markdown展示

{ return { text } }} onChange={ ( { html, text }: { html: any; text: any}) => { setContent(html) }} />
); } export default Com

28.导入以及导出

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

type ComProps = {}

const Com = (props: ComProps) => (
  <>
    

导入

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

type ComProps = {}

const Com = (props: ComProps) => (
  <>
    

导出

); 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['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'
      }
    ]
  },
]

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 = (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 (
    
      
         setSearch(event.target.value)} />
        
      
       `共有 ${total} 条数据`
        }}
      >
         {
          return {(current - 1) * pageSize + index + 1}
        }} />
         {
          return 
        }} />
        
        
         {
          return 
) } 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 (
    <>
      

导入

{ return { index + 1 }}} > {return }} > a.originprice - b.originprice} > a.discount - b.discount} > a.sales - b.sales} > a.stock - b.stock} > {return }} > {return }} > {return }} >
) }; 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 = () => {
  
  return (
    <>
      

百度地图

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

interface IBaiduProps {
  
};

const Baidu:FC = () => {
  
  return (
    <>
      

高德地图

) }; 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['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
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" />
    
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    
    <title>React Apptitle>
    <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>
    
  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 = () => {
  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 (
    <>
      

百度地图

) }; 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


  
    
    
    
    
    
    
    
    
    
    React App
    
  
  
    
    
// 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 = () => {
  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 (
    <>
      

百度地图

) }; 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/

你可能感兴趣的:(react,javascript,前端,react.js)