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(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

reportWebVitals();

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

interface IAppProps {
}

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

export default App

3.安装一些必须的模块

3.1 配置预处理器

两种方式:

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

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

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

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

3.1.1 配置别名@

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

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

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

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

tsconifg.json 引入配置文件:

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

修改 package.json 如下:

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

3.2安装状态管理器

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

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

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

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

3.3 路由

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

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

本项目采用 V6版本

cnpm i react-router-dom -S

3.4 数据验证

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

cnpm i prop-types -S

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

3.5数据请求

cnpm i axios -S

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

Ts 中 @types/* 为声明文件

3.6ui库

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

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

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

cnpm i antd @ant-design/icons -S

src/index.tsx

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

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

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

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

reportWebVitals();

测试组件库

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

interface IAppProps {
}

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

export default App

浏览器查看发现测试通过

3.6.1 自定义主题

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

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

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

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

import {
    ConfigProvider } from 'antd';

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

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

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

reportWebVitals();

3.7 其他第三方工具包

https://www.lodashjs.com/

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

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

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

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

4.创建主布局文件

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

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

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

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

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

const { Header, Sider, Content } = Layout;

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

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

export default App;

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

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

import Index from '@/layout/Index'

import './App.css'

interface IAppProps {
}

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

export default App

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

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

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

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

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

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

5.拆分主界面

先拆分左侧的菜单栏组件

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

const { Sider } = Layout;

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

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

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

const { Header } = Layout;

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

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

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

import { Layout, theme } from 'antd';

const { Content } = Layout;

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

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

export default App;

整和组件资源

// src/layout/components/index.ts

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

import { Layout } from 'antd';

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

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

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

export default App;

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

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

6.使用rtk来管理状态

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

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

6.1 定义State和Dispatch类型

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

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

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

export default store

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

6.2 定义 Hooks 类型

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

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

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

6.3 应用程序中使用

创建状态管理

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

interface IAppState {
   
  collapsed: boolean
}

const initialState: IAppState = {
   
  collapsed: false
}

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

export const {
    changeCollapsed } = appSlice.actions

export default appSlice.reducer

6.4 整合reducer

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

import app from './modules/app'

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

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

export default store

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

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

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

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

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

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

reportWebVitals();

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

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

const { Sider } = Layout;

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

export default App;

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

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

const { Header } = Layout;

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

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

export default App;

6.8保留用户习惯-可选

永久存储 用户习惯

数据持久化: redux-persist

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

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

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

interface IAppState {
  collapsed: boolean
}

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

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

export const { changeCollapsed } = appSlice.actions

export default appSlice.reducer

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

$ cnpm i store2 -S

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

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

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

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

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

export const { changeCollapsed } = appSlice.actions

export default appSlice.reducer

7.左侧菜单栏

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

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

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

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

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

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

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

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

export default menus

7.2.渲染左侧菜单栏

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

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

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

import menus from '@/router/menu'

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

const { Sider } = Layout;

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

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

export default App;

7.3 低版本处理

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

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

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

import menus from '@/router/menu'

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

const { Sider } = Layout;

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

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


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

export default App;

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

7.4 菜单渲染优化

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

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

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

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

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

const { Sider } = Layout;

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

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


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

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


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

export default App;

8.定义路由

8.1 官方文档

https://reactrouter.com/

8.2 创建对应的页面

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

import React, { FC } from 'react';

interface IAppProps {
}

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

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

import React, { FC } from 'react';

interface IAppProps {
}

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

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

import React, { FC } from 'react';

interface IAppProps {
}

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

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

import React, { FC } from 'react';

interface IAppProps {
}

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

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

import React, { FC } from 'react';

interface IAppProps {
}

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

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

import React, { FC } from 'react';

interface IAppProps {
}

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

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

import React, { FC } from 'react';

interface IAppProps {
}

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

export defa

8.3 定义菜单路由信息

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

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

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

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

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

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

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

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

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


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

export default menus

8.4.装载路由

在根组件添加 BrowserRouter 或者 HashRouter

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

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

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

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

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

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

reportWebVitals();

8.5 定义路由组件

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

组件渲染的区域 AppMain 组件

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

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

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

const { Content } = Layout;

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

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

export default App;

8.6 手动测试路由

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

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

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

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

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

8.7 设置404页面

// src/views/error/Page404.tsx

import React, { FC } from 'react';

interface IAppProps {
}

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

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

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

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

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

const { Content } = Layout;

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

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

export default App;

9 切换路由

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

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

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

9.1 点击切换路由

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

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

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

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

const { Sider } = Layout;

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

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


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

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

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

export default App;

9.2 刷新保持左侧菜单状态

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

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

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

import type { MenuProps } from 'antd';

import menus from '@/router/menu'

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

const { Sider } = Layout;

// 获取哪些项具有二级菜单
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值