ReactProject

147 阅读14分钟

react后台项目

项目技术选型

react + react-router + react-redux + react-tookit + ts + antd-pro + axios + sass
react:框架
react-router:路由
react-redux/react-tookit:仓库(真实项目中只会用一个)
ts:TypeScript
antd-pro:UI框架
axios:请求
sass:CSS预处理

创建项目

  • react + ts
npx create-react-app my-app --template typescript

项目安装sass

npm install sass sass-loader

处理全局样式

1.高度100%
2.字体大小(项目规定的)
3.主题色
4.公共的布局
实现:
src>style写全局样式,通过sass进行模块化划分

封装网络请求

  • 注意:
    • 做开发是有三个服务器(服务器要进行切换):
      • 开发的、测试的、线上的
  • 实现与页面请求一一对应
  • axios 可以防止攻击
实现:
	1.安装axios,指令:npm install axios
    2.src>https>index.tsx,创建axios实例,配置公共属性,添加拦截器

路由

  • 安装

    npm install react-router-dom
    
  • 配置路由

    1.根组件中选择路由模式
    2.创建路由表组件
    

Antd组件

  • 安装

    npm install antd
    
  • 使用

    入口文件引入css:import 'antd/dist/reset.css'; 
    

封装输入框、按钮组件

  • 思路

    1.完成基本样式
    2.确定动态属性
    3.添加时间、组件通信
    
// 封装input组件
function Zeroinput(props: any) {
    let [type, setType] = useState(props.type) // input类型
    let [isEmpty, setIsEmpty] = useState(false) // 校验是否为空
    let [value, setValue] = useState('') // input表单的值
    let [flag, setFlag] = useState(false) // 控制表单是否开启校验
    // 隐藏密码
    const changehide = () => {
        setType('password')
    }
    // 显示密码
    const changeshow = () => {
        setType('text')
    }
    // 明密文切换
    const eye = () => {
        // 由于ract的更新机制是重新加载组件,所以此处陆毅不使用监听器
        if (type == 'text') {
            return (<i onClick={() => changehide()} className={'iconfont ' + 'icon-xianshi'}></i>)
        } else {
            return (<i onClick={() => changeshow()} className={'iconfont ' + 'icon-yincang'}></i>)
        }
    }
    // 监听
    useEffect(() => {
        if (!value && flag) { // 表单value值为空且已输入过内容
            setIsEmpty(true)
        } else {
            setIsEmpty(false)
        }
    }, [value])
    // 受控组件(事件双向绑定)
    const getValue = (e: any) => {
        setValue(e.target.value)
        setFlag(true) // 此时开启表单非空校验
    }
    return (
        <div className='zeroinput'>
            {/* icon图标 */}
            {props.icon ? <i className={'iconfont ' + props.icon}></i> : ''}
            {/* input */}
            <input onInput={(e) => getValue(e)} type={type} placeholder={props.placeholder} value={value} />
            {/* 明密文 */}
            {props.eye ? eye() : ''}
            {/* 校验提示 */}
            {isEmpty ? <div className='isEmpty'>不能为空哦~</div> : ''}
        </div>
    )
}
export default Zeroinput
// 封装按钮组件
function Zerobutton(params: any) {
    return (
        <div>
            <button>{params.children}</button>
        </div>
    )
}
export default Zerobutton

权限设计

  • 页面级权限

基础版本

1.前端通过路由导航守卫处理内部页面和外部页面
2.后端通过内部接口和外部接口
	前端需要告诉服务器自己已经登录:在请求头添加标识(token)
  • 前端使用路由守卫

    注意:自己写的路由守卫hooks放的位置
    放在App组件中可能会导致App组件的多次重新创建,可以考虑放在Layout布局页中
    
    // 自定义hooks--导航守卫,判断是否登录
    import { useLocation, useNavigate } from "react-router-dom"; // 导入路由表盒路由信息
    import { useEffect } from "react"; // 导入react的hooks
    // 判断是否登录
    export function useRouterBeforeEach() {
        let location = useLocation()
        let navigate = useNavigate()
        // 监听路由的变化
        useEffect(() => {
            console.log('路由信息', location);
            // 判断内外部页面
            if (location.pathname != '/login') {
                // 内部页面
                if (!sessionStorage.getItem('token')) { // 未登录
                    navigate('/login', { replace: true }) // 无历史记录跳转到登录
                }
            }
        }, [location])
    }
    
  • 前端在请求头添加标识告诉后端自己已登录

    // 请求拦截器
    server.interceptors.request.use(
        (config: any) => {
            //添加token,用来判断内外部接口,告诉服务器自己已登录
            if (sessionStorage.getItem('token')) {
                config.headers['token'] = sessionStorage.getItem('token')
            }
            return config
        },
        (err) => {
            console.log('请求拦截:', err);
    
        }
    )
    

进阶版本

  • 动态生成侧边栏导航

    1.根据后端给的数据动态生成侧边栏导航
    2.需要会使用 'antd''Menu'菜单组件的各个属性
    3.获取到后端给的数据,使用递归生成侧边栏导航的数据
    	需要数据结构就可以了
    
    // 侧边导航组件
    import React, { useState } from 'react'; // 导入react
    import {
        AppstoreOutlined,
        ContainerOutlined,
        DesktopOutlined,
        MailOutlined,
        MenuFoldOutlined,
        MenuUnfoldOutlined,
        PieChartOutlined,
    } from '@ant-design/icons'; // icon
    import type { MenuProps } from 'antd';
    import { Button, Menu } from 'antd';
    
    type MenuItem = Required<MenuProps>['items'][number];
    
    function getItem( // 每一项
        label: React.ReactNode, // 名称
        key: React.Key, // key
        icon?: React.ReactNode, // icon
        children?: MenuItem[], // 子集
        type?: 'group',
    ): MenuItem {
        return {
            key,
            icon,
            children,
            label,
            type,
        } as MenuItem;
    }
    // 侧边栏数据 => [{}] 数组对象结构
    // const items: MenuItem[] = [
    //     // getItem('名称', 'key', <icon图标 />,[子集])
    //     getItem('Option 1', '1', <PieChartOutlined />),
    //     getItem('Option 2', '2', <DesktopOutlined />),
    //     getItem('Option 3', '3', <ContainerOutlined />),
    
    //     getItem('Navigation One', 'sub1', <MailOutlined />, [
    //         getItem('Option 5', '5'),
    //         getItem('Option 6', '6'),
    //         getItem('Option 7', '7'),
    //         getItem('Option 8', '8'),
    //     ]),
    // ];
    
    // 递归方法,生成侧边栏数据
    function getNavList(list: any, items: any) {
        // 遍历侧边栏数据list
        list.forEach((item: any, index: any) => {
            if (item.children) {
                // 如果有嵌套,使用递归再次调用此函数
                #//第二项(key)后面改成了item.name
                items.push(getItem(item.name, index, <PieChartOutlined />, getNavList(item.children, [])))
            } else {
                // 如果没嵌套
                #//第二项(key)后面改成了路由url
                items.push(getItem(item.name, index, <PieChartOutlined />))
            }
        })
        return items
    }
    // NavList组件
    const NavList: React.FC = () => {
        const [collapsed, setCollapsed] = useState(false);
        // 接收后端给的数据
        let list = [
            { name: "dashboard", children: [{ name: "分析页" }, { name: '监控页', children: [{ name: '张三' }] }] }
            ,
            { name: '首页' },
            { name: '权限设置' }
        ]
    
        // 调用处理侧边栏数据的方法
        let items: MenuItem[] = getNavList(list, [])
        
        const toggleCollapsed = () => {
            setCollapsed(!collapsed);
        };
    
        return (
            <div style={{ width: 256 }}>
                <Menu
                    defaultSelectedKeys={['1']} // 侧边栏选中效果
                    defaultOpenKeys={['sub1']} // 展开嵌套的子集
                    mode="inline" // 菜单类型
                    theme="dark" // 主题颜色
                    inlineCollapsed={collapsed} // 收缩与展开
                    items={items} // 侧边栏数据
                />
            </div>
        );
    };
    
    export default NavList;
    
    /*
    defaultOpenKeys:展开嵌套的子集
    defaultSelectedKeys:侧边栏选中效果
    inlineCollapsed:收缩与展开
    items:菜单内容、侧边栏数据
    mode:菜单类型:垂直、水平和内嵌
    theme:主题颜色
    onClick:点击 MenuItem 触发时间
    */
    

集合版本

  • 动态生成路由

    路由:
    	1.写死的路由(公共页面)
        2.动态路由
    
    实现步骤:
    	1.获取到后端的数据
        2.将数据扁平化(拍平数组)
        3.对数据进行遍历
    
    • 扁平化

      // 路由表
      // 导入路由表、路由信息
      import { Routes, Route } from "react-router-dom";
      // 引入组件
      import Login from "../views/Login";
      import Layout from "../Layout";
      // 扁平化:多维数组=>一维数组
      function getRoutesList(list: any, arrs: any) {
          // 遍历后端给的数据
          list.forEach((item: any, index: number) => {
              if (item.children) {
                  // 如果有嵌套,则递归在次调用
                  getRoutesList(item.children, arrs)
              } else {
                  // 如果没嵌套,则直接添加
                  arrs.push(item)
              }
          });
          return arrs
      }
      // 创建路由表组件
      function RouterList() {
          /* 
              获取到用户登录时给的路由信息(动态路由信息)
              注意:后端给到结构是数型结构,我们的导航栏路由信息(扁平化)
              不管侧边栏嵌套多少成,只要点击导航右边就是对应的页面,
              路由信息多少第一层(这也就是扁平化的原因)
          */
          // 获取到用户登录时后端给的数据
          let list = [
              { name: "dashboard", children: [{ name: "分析页" }, { name: '监控页' }] },
              { name: '首页' },
              { name: '权限设置' }
          ]
          // 扁平化:将多维数组变成一维数组
          let arrs = getRoutesList(list, [])
          // console.log('路由数据扁平化', arrs);
          return (
              <Routes>
                  <Route path="/login" element={<Login />}></Route>
                  <Route path="/" element={<Layout />}></Route>
              </Routes>
          )
      }
      export default RouterList
      
    • 如果后端给的数据没有路径,则需要对路由进行处理。(查表思想

      • utitls 创建一个文件
      // 处理路由(查表思想),根据名称返回路径
      
      // 创建一个路由信息表
      let routerList = [
          { name: "分析页", path: 'fenxi', Com: 'Fenxi' },
          { name: "监控页", path: 'jiankong', Com: 'Jiankong' },
          { name: "首页", path: 'index', Com: 'Index' },
          { name: "权限设置", path: 'limit', Com: 'Limit' }
      ]
      // 查表方法
      export function findRouteUrl(obj: any) {
          let ob = routerList.find((item) => {
              // 查表
              return item.name == obj.name
          })
      
          // 如果查到了则返回路径
          if (ob) {
              return ob.path
          } else {
              return 'notfound'
          }
      }
      
    • 处理组件(路由懒加载组件)

      // 路由表
      // 导入react
      

    import React, { Suspense } from "react"; // 引入处理路由方法 import { findRouteUrl, findRouteCom } from "../utils/routerM"; // 导入路由表、路由信息 import { Routes, Route } from "react-router-dom"; // 引入组件 import Login from "../views/Login"; import Layout from "../Layout"; // 自动创建路由懒加载组件 function getLazyCom(obj: any) { let Com Com = React.lazy(() => import(../views/${findRouteCom(obj)}/index)) // 返回懒加载组件 return } // 扁平化:多维数组=>一维数组 function getRoutesList(list: any, arrs: any) { // 遍历后端给的数据 list.forEach((item: any, index: number) => { if (item.children) { // 如果有嵌套,则递归在次调用 getRoutesList(item.children, arrs) } else { // 如果没嵌套,则直接添加 arrs.push(item) } }); return arrs } // 创建路由表组件 function RouterList() { /* 获取到用户登录时给的路由信息(动态路由信息) 注意:后端给到结构是数型结构,我们的导航栏路由信息(扁平化) 不管侧边栏嵌套多少成,只要点击导航右边就是对应的页面, 路由信息多少第一层(这也就是扁平化的原因) */

      // 获取到用户登录时后端给的数据
      let list = [
          { name: "dashboard", children: [{ name: "分析页" }, { name: '监控页' }] },
          { name: '首页' },
          { name: '权限设置' }
      ]
      // 扁平化:将多维数组变成一维数组
      let arrs = getRoutesList(list, [])
      // console.log('路由数据扁平化', arrs);
    
      return (
          <Suspense fallback={<div>loading......</div>}>
              <Routes>
                  <Route path="/login" element={<Login />}></Route>
                  <Route path="/" element={<Layout />}>
                      {/* 动态处理路由 */}
                      {
                          arrs.map((item: any, index: number) => {
                              return (
                                  <Route path={findRouteUrl(item)} key={index} element={getLazyCom(item)}></Route>
                              )
                          })
                      }
                  </Route>
    
              </Routes>
          </Suspense>
      )
    

    } export default RouterList

    
    ```react
    // 处理路由组件(查表思想)
    export function findRouteCom(obj: any) {
        // 查表
        let ob = routerList.find((item: any) => {
            return item.name == obj.name
        })
    
        // 查到了则返回该数据对应的组件路径片段
        if (ob) {
            return ob.Com
        } else {
            return 'Notfound'
        }
    }
    
    • 动态处理icon,查表思想

      // 处理icon图标
      // 创建一张表
      import {
          AppstoreOutlined,
          ContainerOutlined,
          DesktopOutlined,
          MailOutlined,
          MenuFoldOutlined,
          MenuUnfoldOutlined,
          PieChartOutlined,
      } from '@ant-design/icons';  //icon
      
      let routesIcon: any = [
          { name: "dashboard", path: 'fenxi', ComI: <AppstoreOutlined></AppstoreOutlined> },
          { name: "分析页", path: 'fenxi', ComI: '' },
          { name: "监控页", path: 'jiankong', ComI: '' },
          { name: "首页", path: 'index', ComI: <MailOutlined></MailOutlined> },
          { name: "权限设置", path: 'limit', ComI: <PieChartOutlined></PieChartOutlined> }
      ]
      // 查表
      export function findRouteIcon(obj: any) {
          let ob = routesIcon.find((item: any) => {
              return item.name == obj.name
          })
          if (ob) {
              return ob.ComI
          } else {
              return ''
          }
      }
      
      // NavList组件
      ...
      function getNavList(list: any, items: any) {
          // 遍历侧边栏数据list
          list.forEach((item: any, index: any) => {
              if (item.children) {
                  // 如果有嵌套,使用递归再次调用此函数
                  items.push(getItem(item.name, item.name, findRouteIcon(item), getNavList(item.children, [])))
              } else {
                  // 如果没嵌套
                  items.push(getItem(item.name, findRouteUrl(item), findRouteIcon(item)))
              }
          })
          return items
      }
      ...
      
    • 展开与收缩

      <div className='foot-shousuo'>
          {collapsed ? <i onClick={() => toggleCollapsed()} className='iconfont icon-zhankai'></i> : <i onClick={() => toggleCollapsed()} className='iconfont icon-shousuo'></i>}
      </div>
      
    • 点击侧边栏,侧边栏内容重新渲染问题

      原因:加载效果给了全局 => 布局组件
      分析:用户点击侧边栏后跳转对应的组件,加载效果应该给右侧显示的组件
      
      import {Outlet} from 'react-router-dom'
      import React,{ Suspense} from 'react'
      function  Content(){
          return (
              <div>
                  <Suspense fallback={<div>...loading</div>}>
                      <Outlet></Outlet>
                  </Suspense>
              </div>
          )
      }
      export default  Content
      

头部组件

封装搜索组件

  • 功能

    1.给icon添加点击事件,显示input组件
    2.inpit组件,聚焦显示弹框组件
    2.选中弹框中的内容,关闭弹框组件,input中有对应内容
    
    // 搜索组件
    import "./Zerosearch.scss";
    import { useState } from "react";
    function Zerosearch() {
        let [show, setShow] = useState(false) // 控制input是否显示
        // 显示input
        const showIpt = () => {
            setShow(!show)
        }
        return (
            <div className="zerosearch">
                <div className="icon">
                    <i className="iconfont icon-sousuo" onClick={() => showIpt()}></i>
                </div>
                {show ? <InputSelect /> : ''}
            </div>
        )
    }
    // input
    function InputSelect() {
        let list = ['umi ui', 'Zero', 'zhang'] // 弹框内容
        let [showpop, setShowpop] = useState(false) // 控制弹框是否显示
        let [value, setValue] = useState(list[0])
        // 打开弹框
        const focusI = () => {
            setShowpop(!showpop)
        }
        // 表单赋值
        const changeItem = (item: any) => {
            setValue(item)
        }
        // 关闭弹框
        const BlurIn = () => {
            setTimeout(() => { setShowpop(!showpop) }, 100)
        }
        return (
            <div className="inputBox">
                <input type="text" onFocus={() => focusI()} value={value} onBlur={() => BlurIn()} />
    
                {/* 弹框 */}
                {showpop ? <div className="pop">
                    {
                        list.map((item, index) => {
                            return (
                                <div className="popItem" key={index} onClick={() => changeItem(item)}>{item}</div>
                            )
                        })
                    }
                </div> : ''}
    
            </div>
        )
    }
    export default Zerosearch
    

封装消息组件

  • 功能

    1.基本样式
    2.添加点击事件 => 显示组件
    3.列表渲染头部
    4.发送请求获取数据
    5.显示数据
    6.默认显示通知,点击每一项,添加高亮,通知栏同理
    7.底部处理
    8.头部数据优化
    9.用户处理消息,自己修改每一项的数据
    
    // 消息组件
    import "./zeromessage.scss";// 样式
    

import { messageTopData, ChangeMessage } from "../../https/api/layout"; // 请求 import { useEffect, useState } from "react";

function Zeromessage() { let [show, setShow] = useState(false) // 控制是否显示消息弹窗 // 打开消息弹框 const showMessageData = () => { setShow(!show) } return (

11
<i onClick={() => showMessageData()} className="iconfont icon-xiaoxi"> {/* 消息弹框 */} {show ? : ''}
) }

// 消息弹框组件 function MassageData() { let topList = ['通知', '消息', '代办'] // 头部内容 let [currentIndex, setCurrentIndex] = useState(0) // 默认选中头部第一项 let [messageObj, setMessageObj] = useState({ infrom: [], messageList: [], commissionList: [] }) // 弹窗数据 let [listContent, setListContent] = useState([{ zhutai: false }]) // 列表的数据 // 获取弹窗数据方法 const getmessAgeDataList = () => { let token = sessionStorage.getItem('token') // 发起请求 messageTopData({ token }).then((res: any) => { console.log('弹窗数据', res); setMessageObj(state => { return { ...res.messageObj } }) // 默认数据-通知列表 setListContent(res.messageObj.infrom) }) } // !!在页面加载完毕后,发送请求:防止页面重绘和回流 useEffect(() => { // 获取弹窗数据 getmessAgeDataList() }, []) // 点击头部切换,显示内容 const selectItem = (index: number) => { setCurrentIndex(index) if (index == 0) { setListContent(messageObj.infrom) } else if (index == 1) { setListContent(messageObj.messageList) } else { setListContent(messageObj.commissionList) } } // 前端手动修改已读 const findListData = (index: number) => { setListContent(state => { // 修改已读状态 state[index].zhutai = true return [...state] }) } // 点击每一项,处理已读 const selectCItem = (index: number) => { // 发送请求 ChangeMessage({ indexID: index }).then((res: any) => { if (res.code == 200) { // 修改已读样式 findListData(index) } }) }

  return (
      <div className="messageData">
          {/* 头部 */}
          <div className="messageTop">
              {
                  topList.map((item, index) => {
                      return (
                          <div className={currentIndex == index ? 'topItem' : ''} key={index} onClick={() => selectItem(index)}>{item}</div>
                      )
                  })
              }
          </div>
          {/* 弹窗内容列表 */}
          <div className="listContent">
              {
                  listContent.map((item: any, index: number) => {
                      return (
                          <div className={item.zhutai ? 'listItem' : ''} onClick={() => selectCItem(index)} key={index}>
                              <div>{item.content}</div>
                              <div>{item.time}</div>
                          </div>
                      )
                  })
              }
          </div>
          {/* 尾部 */}
          <div className="footMessage">
              <div>清空</div>
              <div>查看更多</div>
          </div>

      </div>
  )

} export default Zeromessage


### 退出登录组件

- 使用了 `antd` 的 `下拉菜单` 组件

```react
//  头部退出登录组件
import "./zeroOutLogin.scss"; // 样式
import React from 'react';
import { useNavigate } from "react-router-dom";
import type { MenuProps } from 'antd';
import { Button, Dropdown, Space } from 'antd';
// 下拉框中每一项的数据
const items: MenuProps['items'] = [
    { key: '1', label: (<div>个人中心</div>), },
    { key: '2', label: (<div>个人设置</div>), },
    { key: '3', label: (<div>退出登录</div>), },
];
// 组件
function ZeroOutLogin() {
    let navigate = useNavigate()
    // 获取用户信息
    let userObj = JSON.parse(sessionStorage.getItem('user') as any)
    // 点击事件
    const onClick: MenuProps['onClick'] = (obj: any) => {
        // console.log('退出登录组件', obj);
        if (obj.key == 3) {
            // 清楚本地数据
            sessionStorage.clear()
            // 跳转登录(无记录跳转)
            navigate('/login', { replace: true })
        }
    }
    return (
        <div className="zeroOutLogin">
            <Dropdown menu={{ items, onClick }} placement="bottomLeft">
                <div className="content">
                    <img src={userObj.image} />
                    <div>{userObj.name}</div>
                </div>
            </Dropdown>
        </div>
    )
};
export default ZeroOutLogin;

封装面包屑组件

  • 引用 antd 中的 面包屑 组件,二次处理
  • 面包屑数据为数组数据结构
  • 首次进入后台系统,展示 首页
// 面包屑组件
import "./zeroBreadcrumb.scss"; // 样式
import { useState, useEffect } from 'react';
import { useNavigate, useLocation } from "react-router-dom";
import { Breadcrumb } from 'antd'; // 面包屑组件
// 创建一张面包屑对应的表
let pathNameCode = [
    { path: '/index', name: ['首页'], },
    { path: '/limit', name: ["权限"] },
    // 侧边导航,有嵌套
    { path: '/fenxi', name: ['dashboard', '分析'] },
    // 侧边导航,有嵌套
    { path: '/jiankong', name: ['dashboard', '监控'] }
]
// 查表方法
function getPathName(path: string) {
    // 查表
    let item = pathNameCode.find((item) => {
        return item.path == path
    })
    // 如果查到了,则返回面包屑数据
    if (item) {
        return item.name
    }
}
function ZeroBreadcrumb() {
    let navigate = useNavigate() // 路由跳转
    let location = useLocation() // 路由信息
    let [breadList, setBreadList] = useState(['首页']) // 面包屑数据,默认显示首页
    // 监听路由信息
    useEffect(() => {
        console.log('面包屑监听路由信息');
        if (location.pathname != '/index') {
            // !!查表思想
            let arr: any = getPathName(location.pathname)
            setBreadList(start => {
                return ['首页', ...arr]
            })
        } else {
            // 点击首页,面包屑只展示首页
            setBreadList(['首页'])
        }
    }, [location])
    // 点击首页,跳转首页
    const goPathIndex = (index: number) => {
        if (index == 0) {
            navigate('/index')
            sessionStorage.setItem('currentPath', 'index')
        }
    }
    return (
        // separator => 分隔符
        <div className="zeroBreadcrumb">
            <Breadcrumb separator='/'>
                {
                    breadList.map((item, index) => {
                        return (
                            <Breadcrumb.Item onClick={() => goPathIndex(index)} key={index}>{item}</Breadcrumb.Item>
                        )
                    })
                }
            </Breadcrumb>
        </div >
    )
}
export default ZeroBreadcrumb

存在问题

  • 问题

    1.重启项目 => 访问'/'路由去布局组件 => 加载头部的'退出登录组件'时,由于头像使用了用户信息 => 用户信息为空则报错
    2.面包屑组件 => 检测到用户进入'/'页面 => 添加面包屑数据
    3.用户进入后台 => 默认展示'首页' => 加载侧边栏组件
    
  • 解决

    // 退出登录组件
    ...
    // 获取用户信息
    // !!解决重启项目用户信息为空的问题
    let datas = sessionStorage.getItem('user') || false
    let userObj = datas ? JSON.parse(datas) : {}
    ...
    {/* // !!解决重启项目用户信息为空的问题 */}
    {datas ? <img src={userObj.image} /> : ''}
    ...
    
    // 面包屑组件
    ...
    // 监听路由信息
    useEffect(() => {
        console.log('面包屑监听路由信息');
        // !!解决重启项目是面包屑问题
        if (location.pathname != '/index' && location.pathname != '/') {
            // !!查表思想
            let arr: any = getPathName(location.pathname)
            setBreadList(start => {
                return ['首页', ...arr]
            })
        } else {
            // 点击首页,面包屑只展示首页
            setBreadList(['首页'])
        }
    }, [location])
    ...
    

路由标签组件

  • 思路

    1.完成基本样式
    2.用户进入后台显示'首页'
    3.用户'切换'侧边栏导航时,添加一个路由标签(查表思想:根据路径返回名称)
    4.从第二个路由标签开始,显示可删除的icon图标
    5.当前路由标签具有高亮效果
    6.点击删除路由标签
    7.点击路由标签跳转
    
  • 实现

    1.切换侧边栏导航添加路由标签,不重复(查表思想:根据路径返回名称)
    2.处理高亮效果,当前路径与标签路径一致时高亮显示
    3.添加删除的icon图标
    4.点击删除
    	4-1.没有高亮效果的,根据index删除
    	4-2.有高亮效果的,根据index删除并跳转至路由标签最后一项
    5.点击路由标签进行跳转
    
    // 路由标签
    import "./routerTags.scss";
    import { useState, useEffect } from "react";
    import { tagName } from "../../utils/tagName"; //查表思想
    import { useLocation, useNavigate } from "react-router-dom"; // 路由信息,路由跳转
    function RouterTags() {
        let location = useLocation() // 路由信息
        let navigate = useNavigate() // 路由跳转
        let [routerTagsList, setRouterTagsList] = useState([{ name: '首页', path: '/index', active: true, del: false }]) // 路由标签数据,默认展示首页
        // 添加路由标签方法
        const addRouteTags = (pathname: string) => {
            // 路由标签中查询一遍,判断该路由标签是否存在
            let index = routerTagsList.findIndex((item) => {
                return item.path == pathname
            })
            // 判断该路由标签是否存在
            if (index != -1) {
                // 该路由标签存在,则高亮显示
                changStyle(pathname)
            } else {
                // 该路由标签存在,则添加
                setRouterTagsList((state: any) => {
                    // concat => 合并数组
                    let arrs = state.concat({ path: pathname, show: true, del: false, name: tagName(pathname) })
                    return [...arrs]
                })
                // 处理高亮
                changStyle(pathname)
            }
        }
        // 处理高亮显示方法
        const changStyle = (pathname: string) => {
            setRouterTagsList(state => {
                state.forEach((item: any) => {
                    if (item.path == pathname) {
                        // 当前项高亮
                        item.active = true
                    } else {
                        // 其余项不高亮
                        item.active = false
                    }
                })
                return [...state]
            })
        }
        // 监听路由
        useEffect(() => {
            // 添加路由标签,去重
            addRouteTags(location.pathname)
        }, [location])
        // 点击删除
        const deleteIndex = (item: any, index: number) => {
            // 判断该项是否高亮(活动)项
            if (item.active) {
                // 高亮 => 根据index伤处并跳转最后一个路由标签
                setRouterTagsList(state => {
                    state.splice(index, 1) // 根据index删除
                    let url = state[state.length - 1].path // 获取最后一个路由标签的路由
                    navigate(url) // 跳转
                    return [...state]
                })
            } else {
                // 非高亮 => 根据index删除
                setRouterTagsList(state => {
                    state.splice(index, 1) // 根据index删除
                    return [...state]
                })
            }
        }
        // 点击路由标签,路由跳转
        const goPath = (path: string) => {
            navigate(path)
        }
        // 鼠标移入
        const mouseEnter = (name: string) => {
            setRouterTagsList(state => {
                state.forEach((item: any, index: number) => {
                    // 该项显示删除的icon图标
                    if (item.name == name) {
                        item.del = true
                    } else {
                        // 其他项不显示删除的icon图标
                        item.del = false
                    }
                })
                return [...state]
            })
        }
        // 鼠标移出
        const mouseOut = (name: string) => {
            // 不显示删除的icon图标
            setRouterTagsList(state => {
                state.forEach((item) => {
                    item.del = false
                })
                return [...state]
            })
        }
        return (
            <div className="routerTags">
                {
                    routerTagsList.map((item, index) => {
                        return (
                            <div key={index} className={item.active ? 'tagItemA' : 'tagItem'} >
                                <span onClick={() => goPath(item.path)} onMouseEnter={() => mouseEnter(item.name)} onMouseOut={() => mouseOut(item.name)}>{item.name}</span>
                                {/* 删除的icon图标 */}
                                {/* 为了解决鼠标在padding空隙中的闪烁问题,在<i/>标签中也添加了移入移出事件 */}
                                {
                                    item.name != '首页' && item.del ? <i className="iconfont icon-shanchu" onClick={() => deleteIndex(item, index)} onMouseEnter={() => mouseEnter(item.name)} onMouseOut={() => mouseOut(item.name)}></i> : ''
                                }
                            </div>
                        )
                    })
                }
            </div>
        )
    }
    export default RouterTags
    
    // 路由标签组件 查表思想 utils>tagName.tsx
    // 根据路径返回名称
    // 创建一张表
    let routerList: any = [
        { name: "分析页", path: '/fenxi' },
        { name: "监控页", path: '/jiankong' },
        { name: "首页", path: '/index' },
        { name: "权限设置", path: '/limit' }
    ]
    // 查表
    export function tagName(path: any) {
        let ob = routerList.find((item: any) => {
            return item.path == path
        })
        if (ob) {
            return ob.name
        } else {
            return 'Notfound'
        }
    }
    

全局设置组件

  • 思路

    1.完成基本样式
    	1-1.设置按钮放在头部中,点击设置打开弹框
        1-2.设置弹框凡在布局组件中,与头部组件同级
    2.功能
    	2-1.组件间数据传递(多个组件需要使用这个数据),使用redux全局状态管理
    
  • 安装 redux

     npm install redux
     npm install react-redux
    
  • 实现

    • src > stort > index 创建仓库

      // 创建仓库
      // redux => store actions reducer
      import { createStore } from "redux"; // 引入redux中的创建仓库方法
      import reducers from "./reducers"; // 引入reducer行为
      let store = createStore(reducers) // 创建仓库
      // 仓库创建完毕,将仓库和项目关联 => react-redux
      export default store
      
    • 创建各自模块的 reducer 行为,合并 reducer 行为 scr>store>redcues>模块文件

      // 创建reducer行为
      // 默认数据
      let data = {
          showSet: false, // 控制设置弹框的显隐
          selectLayout: 3 // 布局:1:左右、2:上下、3:上左右  ["左右","上下","上左右"]
      }
      // 行为
      function glbSetState(state: any = data, actions: any) {
          switch (actions.type) {
              // 打开弹窗
              case 'showT':
                  return { ...state, showSet: true }
              // 关闭弹窗
              case 'showF':
                  return { ...state, showSet: false }
              // 设置布局
              case 'setLoyout':
                  return { ...state, selectLayout: actions.index }
              default:
                  return state;
          }
      }
      export default glbSetState
      
      // 合并reducer行为
      import { combineReducers } from "redux"; // 引入combineReducers方法
      import glbSetState from './glbSetState'; // 引入各个reducer模块
      // 合并reducer
      let reducers = combineReducers({
          glbSetState
      })
      export default reducers // 导出合并好的模块
      
    • 使用 react-redux 让仓库和项目关联

      // 入口文件 index.tsx
      ...
      import store from "./store/index"; // 仓库
      import { Provider } from "react-redux"; // 仓库和项目关联
      ...
      root.render(
          <Provider store={store}>
              <BrowserRouter>
                  <App />
              </BrowserRouter>
          </Provider>
      );
      
    • 功能1:点击 设置,打开弹框

      // 头部组件中
      ...
      // 引入react-redux提供的useDispatch
      import { useDispatch } from "react-redux";
      function LayoutTop() {
          let dispatch = useDispatch() // dispath
          // 点击设置,打开弹窗
          const showSetPop = () => {
              // 触发行为
              dispatch({ type: 'showT' })
          }
          ...
      }
      
    • 功能2:关闭弹框

      dispath({ type: 'showF' })
      
    • 功能3:切换布局

      // 布局切换子组件
      function SelectNav() {
          let dispach = useDispatch() // dispach
          let list = ['左右布局', '上下布局', '左中右布局']
          let setState = useSelector((state: any) => state.glbSetState.selectLayout) // 获取仓库里的布局数据
          // 点击切换布局
          const selectType = (index: number) => {
              dispach({ type: 'setLoyout', index: index + 1 })
          }
          return (
              <div className="selectNav">
                  <h2>布局切换</h2>
                  <div className="selectNavList">
                      {
                          list.map((item: any, index: number) => {
                              return (
                                  <div className={setState - 1 == index ? 'listItemA' : 'listItem'} key={index} onClick={() => selectType(index)}>
                                      {item}
                                  </div>
                              )
                          })
                      }
                  </div>
              </div>
          )
      }
      
    • 功能2、功能3代码整合

      // 弹框组件
      import "./setPop.scss"; // 样式
      import { useState, useEffect } from 'react';
      import { Drawer, Switch } from 'antd';
      import { useDispatch, useSelector } from "react-redux";
      
      function SetPop() {
          let dispach = useDispatch() // dispach
          // 获取仓库中的数据
          let showSet = useSelector((state: any) => state.glbSetState.showSet)
          // 弹框的显隐
          const [open, setOpen] = useState(false);
          // 监听仓库中showSet的变化
          useEffect(() => {
              setOpen(showSet)
          }, [showSet])
          // 关闭弹窗
          const onClose = () => {
              dispach({ type: 'showF' })
          };
          return (
              <div>
                  <Drawer title="页面设置" placement="right" onClose={onClose} open={open}>
                      {/* 主题切换 */}
                      <SelectTheme />
                      {/* 布局切换 */}
                      <SelectNav />
                  </Drawer>
              </div>
          );
      };
      
      // 主题切换组件
      function SelectTheme() {
          let defaultChecked = false
          return (
              <div className='changeTheme'>
                  <h2>主题</h2>
                  <Switch checkedChildren="☀" unCheckedChildren="🌙" defaultChecked={defaultChecked} />
              </div>
          )
      }
      
      // 布局切换组件
      function SelectNav() {
          let dispach = useDispatch() // dispach
          let list = ['左右布局', '上下布局', '左中右布局']
          let setState = useSelector((state: any) => state.glbSetState.selectLayout) // 获取仓库里的布局数据
          // 点击切换布局
          const selectType = (index: number) => {
              dispach({ type: 'setLoyout', index: index + 1 })
          }
          return (
              <div className="selectNav">
                  <h2>布局切换</h2>
                  <div className="selectNavList">
                      {
                          list.map((item: any, index: number) => {
                              return (
                                  <div className={setState - 1 == index ? 'listItemA' : 'listItem'} key={index} onClick={() => selectType(index)}>
                                      {item}
                                  </div>
                              )
                          })
                      }
                  </div>
              </div>
          )
      }
      
      export default SetPop;
      
    • 功能4:对应布局展示

      // layout布局组件
      function Layout() {
          console.log('layout');
          // 获取仓库数据
          let showSet = useSelector((state: any) => state.glbSetState.showSet) // 弹窗显隐
          let selectLayout = useSelector((state: any) => state.glbSetState.selectLayout) // 布局
      
          useRouterBeforeEach() //路由导航守卫,判断是否登录
      
          // 判断当前布局模式
          const getLayout = (num: number) => {
              if (num == 3) { // 左中右布局
                  return (
                      <div className="layout">
                          {/* 头部 */}
                          <LayoutTop />
                          <div className="main">
                              {/* 侧边栏 */}
                              <NavList />
                              <div className='right'>
                                  {/* 路由标签 */}
                                  <RouterTags />
                                  {/* 内容显示区域 */}
                                  <Content />
                              </div>
                          </div>
                      </div >
                  )
              } else if (num == 2) { // 上下布局
                  return (
                      <div className="layout">
                          {/* 头部 */}
                          <LayoutTop />
                          <div className="main">
                              <div className='right'>
                                  {/* 路由标签 */}
                                  <RouterTags />
                                  {/* 内容显示区域 */}
                                  <Content />
                              </div>
                          </div>
                      </div >
                  )
      
              } else { // 左右布局
                  return (
                      <div className="layout">
                          <div className="main">
                              {/* 侧边栏 */}
                              <NavList />
                              <div className='right'>
                                  {/* 头部 */}
                                  <LayoutTop />
                                  {/* 路由标签 */}
                                  <RouterTags />
                                  {/* 内容显示区域 */}
                                  <Content />
                              </div>
                          </div>
                      </div >
                  )
              }
          }
      
          return (
              <div className='layout'>
                  {getLayout(selectLayout)}
                  {/* 弹框组件 */}
                  {showSet ? <SetPop /> : ''}
              </div>
          )
      
      }
      export default Layout
      

首页(ECharts)

  • 安装 ECharts

    npm install echarts --save
    
  • 对常有用的可视化图表进行封装的优点

    1.统一标准
    2.提高开发效率
    3.方便二次维护
    
  • 实现步骤

    根据ui图 >> 找类似的示例 >> 找对应的属性 >> 死数据 >> 和ui图样式一致
    
  • ECharts 的使用

    1.导入
    2.生成ECharts图表
    	2-1.获取元素,用来放置图表
        	let dom: any = document.getElementById('main')
    	2-2.创建echarts实例
        	let myChart = echarts.init(dom)
        2-3.配置项
        	myChart.setOption({
                xAxis:{},
                yAxis:{},
                series:{}
            })
    
  • 实现(以下代码处于一个文件)

    // 首页-引入
    import "./index.scss";
    import * as echarts from 'echarts'; // 引入ECharts
    import { DatePicker } from 'antd'; // 日期选择器(方法)
    import { useState, useEffect } from "react";
    import { getIndexFenxi, getIndexEcharts,getIndexEchartsTwo } from "../../https/api/index"; // 引入请求
    
    // 首页组件(父组件)
    function Index() {
        let [topshow, settopshow] = useState([]) // 获取会员分析数据
        let [echartsD, setEchartsD] = useState({}) // 获取会员分析ECharts数据
        let [echartsTwo, setEchartsTwo] = useState({}) // 会员等级(多个系列)图表
        // 挂载后生命周期
        useEffect(() => {
            // 发起请求,获取数据
            getshowData()
            getIndexEchartsd()
            getTwoEcharts()
        }, [])
        // 获取会员分析数据(父组件发起请求)
        const getshowData = (time = '最近7天', endTime = '') => {
            // 发起请求
            getIndexFenxi({ time, endTime }).then((res: any) => {
                // 获取到数据
                settopshow(res.rember)
            })
        }
        // 获取会员分析ECharts数据(父组件发起请求)
        const getIndexEchartsd = (time = '最近7天', endTime = '') => {
            // 发起请求
            getIndexEcharts({ time, endTime }).then((res: any) => {
                // 获取到会员分析ECharts数据
                setEchartsD(res.echartsObj)
            })
        }
        // 获取会员等级(多个系列)的ECharts数据
        const getTwoEcharts = (time = '最近7天', endTime = "") => {
            // 发起请求
            getIndexEchartsTwo({ time, endTime }).then((res: any) => {
                // 获取到会员等级(多个系列)的ECharts数据
                setEchartsTwo(res.echartsObj)
            })
        }
        // 通过判断,获取会员分析数据与ECharts数据(父组件发起请求)
        const getTopSt = (val: any, enTtime: any) => {
            // console.log('获取会员数据和ECharts数据:', val, enTtime);
            if (enTtime) { // 表示为日期选择器选择了日期
                // 发起请求,获取会员分析数据
                getshowData(val, enTtime)
            } else { // 表示时间选项选择了数据
                getshowData(val) // 发起请求,获取会员分析数据
                getIndexEchartsd(val) // 发起请求,获取会员分析ECharts数据
                getTwoEcharts(val) // 发起请求,获取到会员等级(多个系列)的ECharts数据
            }
        }
        return (
            <div className="indexEcharts">
                <div className="index-top">
                    <div><b>交易分析</b></div>
                    {/* 日期选择 */}
                    <SelectTime getTopSt={getTopSt} />
                </div>
                {/* 会员分析模块 */}
                <Meber topshow={topshow} />
                {/* 会员数据图表展示 */}
                <Secharts echartsD={echartsD} />
                {/* 会员等级占比优势图标展示2 */}
                <SechartsTwo echartsTwo={echartsTwo} />
            </div>
        )
    }
    
    // 日期选择组件(子组件)
    function SelectTime(props: any) {
        let { RangePicker } = DatePicker // 日期选择器(组件)
        let timeList = ['今天', '昨天', '最近7天', '最近30天'] // 选项数据
        let [topSelect, setTopSelect] = useState('最近7天') // 控制选项高亮
        let [selectLeft, setSelectLeft] = useState(false) // 日期选择器是否选择了时间的状态
        // 点击切换选项
        const selectTop = (item: string) => {
            props.getTopSt(item) // 子传父,触发父组件的判断方法,发起请求
            setTopSelect(item) // 设置高亮
            setSelectLeft(false) // 更改日期选择器是否选择了时间的状态
        }
        // 日期选择器发生改变
        const SelectTimeS = (e: any) => {
            console.log('日期选择器', e);
            if (e) {
                // e[0].$d:起始日期  e[1].$d:结束日期
                props.getTopSt(e[0].$d, e[1].$d) // 子传父,触发父组件的判断方法,发起请求
            }
            setSelectLeft(true) // 更改日期选择器是否选择了时间的状态
        }
        return (
            <div className='top-right'>
                <div className="right-time">
                    统计日期:
                    <RangePicker onChange={(e) => SelectTimeS(e)} />
                </div>
                <div className="right-select">
                    {
                        timeList.map((item, index) => {
                            return (
                                <div key={index} className={topSelect == item && !selectLeft ? 'selectItemA' : ''} onClick={() => selectTop(item)}>
                                    {item}
                                </div>
                            )
                        })
                    }
                </div>
            </div>
        )
    }
    
    // 会员分析模块组件(子组件)
    function Meber(props: any) {
        // console.log('会员分析', props); // 打印2次 => 原理:组件创建 比 异步获取数据 快
        // 显示数据的标题
        const showRemberTitle = (index: number) => {
            if (index == 0) {
                return '累计会员数'
            } else if (index == 1) {
                return '新增会员数'
            } else if (index == 2) {
                return '支付会员数'
            } else {
                return '储值会员数'
            }
        }
        return (
            <div className="meberShow">
                {
                    props.topshow.map((item: any, index: number) => {
                        return (
                            <div key={index} className='meberItem'>
                                {/* 标题 */}
                                <div>
                                    {showRemberTitle(index)}
                                </div>
                                {/* 数据 */}
                                <div className="meberdata">
                                    {item.all || item.newR || item.paym || item.sm}
                                </div>
                                {/* 比较 */}
                                <div>
                                    较上期
                                    <i className="iconfont icon-xiangshang">{item.allb || item.newRb || item.paymb || item.smb}</i>
    
                                </div>
                            </div>
                        )
                    })
                }
            </div>
        )
    }
    
    // 会员等级占比优势模块(多个系列的图表)(子组件)
    function SechartsTwo(props: any) {
        console.log('会员等级图标展示', props); // 打印2次 => 原理:组件创建 比 异步获取数据 快
        // 监听数据变化
        useEffect(() => {
            if (props.echartsTwo.ydata) {
                getECharts()
            }
        }, [props])
        // 生成ECharts图表
        const getECharts = () => {
            let dom: any = document.getElementById('mains') // 获取元素,用来放置图表
            let myChart = echarts.init(dom) // 创建echarts实例
            // 配置项 => 绘制图表
            myChart.setOption({
                // 提示框组件
                tooltip: {
                    trigger: 'axis' // 触发类型
                },
                // 配置X轴
                xAxis: {
                    type: 'category', // 坐标轴类型:如:类目轴
                    boundaryGap: false, // 坐标轴两边的留白策略
                    data: props.echartsTwo.xdata // 类目数据
                },
                // 配置Y轴
                yAxis: {
                    type: 'value' // 坐标轴类型:如:数值轴
                },
                // 图表
                series: [
                    {
                        data: props.echartsTwo.ydata.oneRe, // 图表数据
                        type: 'line', // 图标类型:如:折线/面积图
                        name: '白金会员', // 系列名称
                        symbol: 'none', // 标记的图形
                        stack: 'Total', // 数据的堆叠
                    },
                    {
                        data: props.echartsTwo.ydata.twoRe, // 图表数据
                        type: 'line', // 图标类型:如:折线/面积图
                        name: '黄金会员', // 系列名称
                        symbol: 'none', // 标记的图形
                        stack: 'Total', // 数据的堆叠
                    },
                    {
                        data: props.echartsTwo.ydata.threeRe, // 图表数据
                        type: 'line', // 图标类型:如:折线/面积图
                        name: '钛金会员', // 系列名称
                        symbol: 'none', // 标记的图形
                        stack: 'Total', // 数据的堆叠
                    },
                    {
                        data: props.echartsTwo.ydata.foreRe, // 图表数据
                        type: 'line', // 图标类型:如:折线/面积图
                        name: '超级会员', // 系列名称
                        symbol: 'none', // 标记的图形
                        stack: 'Total', // 数据的堆叠
                    },
                ]
            })
        }
        return (
            <div className="echartsBoxTwo">
                <div>
                    <div>会员等级占比趋势</div>
                </div>
                <div id="mains" style={{ width: 100 + '%', height: 350 + 'px' }}></div>
            </div>
        )
    }
    
    export default Index // 导出首页组件(父组件)
    
  • **注意:**如果在同一个页面级组件中展示多个 EChartsdom 元素的 id 不能相同

分析页(二次封装ECharts)

  • 本质

    1.组件的数据传递
    2.确定动态属性(函数劫持),动态属性越多,复用性越强
    
  • 封装优点:

    1.提高代码的复用性
    2.方便二次维护和二次开发
    
  • 实现

    // 二次封装ECharts
    
    import * as echarts from 'echarts'; // ECharts图表
    import { useEffect } from 'react'
    // 处理用户传递的数据
    const getEcharM = (prop: any) => {
        if (prop.datas) {
            if (prop.datas) { //用户给这个 echarts 传递 数据
                return {
                    xdata: [],
                    ydata: [],
                    dom: 'mains',
                    symbol: "none",   //标记的图形
                    lineStyleColor: '',
                    areaStyleColor: '#007acc',
                    opacity: 0.5,
                    ...prop.datas,
                    ...prop
                }
            }
        }
    }
    
    // 处理数据结构
    const getSlist = (list: any, props: any) => {
      	// 判断是否为多个系列  
        if (list) { // 表示多个系列
            let arr: any = []
            props.ydata.forEach((item: any, index: number) => {
                arr.push({
                    data: item.y,
                    type: 'line',
                    symbol: props.symbol,
                    lineStyle: {
                        color: props.lineStyleColor,
                        width: 2
                    },
                    areaStyle: {
                        color: props.areaStyleColor,
                        opacity: props.opacity
                    }
                })
            })
            return arr
        } else {// 表示单个系列
            return {
                data: props.ydata,
                type: 'line',
                symbol: props.symbol,
                lineStyle: {
                    color: props.lineStyleColor,
                    width: 2,
                },
                areaStyle: {
                    color: props.areaStyleColor,
                    opacity: props.opacity,
                }
            }
        }
    }
    
    // ECharts组件
    function LineCom(prop: any) {
        let props: any = getEcharM(prop) // 用户传递的参数:返回一个对象
        console.log('用户传递', prop);
        console.log('用户传递处理', props);
        // 监听
        useEffect(() => {
            getEcharts()
        }, [prop])
        // 生成可视化图表
        const getEcharts = () => {
            // 获取元素,用来放置图表
            let dom: any = document.getElementById(props.dom)
            // 创建echarts实例
            var myChart = echarts.init(dom);
            // 配置项 => 绘制图表
            myChart.setOption({
                // 配置X轴
                xAxis: {
                    type: 'category',
                    boundaryGap: false,
                    data: props.xdata
                },
                // 配置Y轴
                yAxis: {
                    type: 'value'
                },
                series: getSlist(prop.seriesList, props) // 传入(用户给的用来判断是否为多个系列,处理过的用户传递的数据)
            })
        }
        return (
            <div className='lineCom'>
                <div id={props.dom} style={{ height: 350 + 'px', width: 100 + "%" }}></div>
            </div>
        )
    }
    export default LineCom
    
    // 使用二次封装的ECharts
    function Fenxi() {
        let list = ['黄金会员', '白金会员'] // 表示多个系列
        let datasOne = {
            xdata: [1, 2, 3, 4, 5, 6, 7],
            ydata: [
                { name: '黄金会员', y: [111, 222, 333, 444, 555, 666, 777] },
                { name: '白金会员', y: [33, 22, 333, 444, 55, 66, 777] }
            ]
        }
        let datasTwo = {
            xdata: [1, 2, 3, 4, 5, 6, 7],
            ydata: [111, 432, 333, 234, 123, 452, 777]
        }
    
        return (
            <div className='fenxiBox'>
                <div className='fenxiBoxItem'>
                    <div>ECharts二次封装</div>
                    <LineCom datas={datasOne} lineStyleColor="#dd4c35" seriesList={list} />
                </div>
                <div className='fenxiBoxItem'>
                    <div>ECharts二次封装</div>
                    <LineCom datas={datasTwo} lineStyleColor="#1572b6" dom='ids' />
                </div>
            </div>
        )
    }
    export default Fenxi
    

监控页(二次封装表格组件)

  • 思路

    1.完成基本样式
    2.确定动态属性
    
    表格一行的数据:根据表头有多少个属性决定
    表格有多少行数据:根据数据的长度决定
    
    后端数据:
    	1、无需处理直接渲染;
    	2、处理后端数据:
    		在对应的表头项中添加render属性,属性值为一个方法,本质就是一个函数组件
    
  • 实现

    // 二次封装表格组件
    import { Table } from 'antd';
    import type { ColumnsType } from 'antd/es/table';
    import ZeroPagination from "../ZeroPagination"; // 分页器
    // 名称类型
    interface DataType {
        key: string; // 唯一标识
        MemberOrder: string; // 会员等级
        MemberNumber: number; // 成交会员数
        ratio: any; // 成交会员占比
        payNum: any; // 支付订单数
        tags: string[]; // 操作
    }
    // 列 => 表头
    const columns: ColumnsType<DataType> = [
        {
            title: '会员等级', // 表头名称
            dataIndex: 'MemberOrder', // 用来显示每一行的内容
            key: 'MemberOrder', // 唯一标识
        },
        {
            title: '成交会员数',
            dataIndex: 'MemberNumber',
            key: 'MemberNumber',
        },
        {
            title: '成交会员占比',
            dataIndex: 'ratio',
            key: 'ratio',
        },
        {
            title: '支付订单数',
            key: 'payNum',
            dataIndex: 'payNum',
        },
        {
            title: 'Action',
            key: 'action',
            // 该表头下的每一项可以自定义内容
            render: () => (
                <a>Delete</a>
            ),
        },
    ];
    // 表格组件
    function ZeroTable(props: any) {
        // 触发父组件方法
        const ongetData = (val: any) => {
            // console.log('子组件(表格)', val);
            props?.ongetData(val)
        }
        return (
            <div className='table'>
                <h2>二次封装表格组件</h2>
                {/* 表格 */}
                <Table columns={columns} pagination={{ position: [] }} dataSource={props.list}></Table>
                {/* 分页器 */}
                <ZeroPagination total={props.list.length} ongetData={ongetData} />
            </div>
        )
    }
    export default ZeroTable;
    
    // 分页器组件
    import { Pagination } from 'antd'; // 引入分页器
    function ZeroPagination(props: any) {
        // 页码发生改变,通过父组件让爷组件获取数据
        const onChange = (e: any) => {
            // console.log('改变页码(孙组件分页器)', e);
            props?.ongetData(e)
        }
        return (
            <Pagination defaultCurrent={1} total={props.total} defaultPageSize={3} onChange={(e) => onChange(e)} showSizeChanger />
        )
    }
    export default ZeroPagination
    
    // 使用二次封装的表格组件
    import './jiankong.scss' // 样式
    import ZeroTable from "../../components/ZeroTable";// 引入二次封装的Table
    import { getTableData } from "../../https/api/jiangkong";
    import { useState, useEffect } from "react";
    // 监控页组件
    function Jiankong() {
        let [tableList, setTableList] = useState([]) // 表格数据
        // 发起请求获取数据
        const getTable = (page = 1) => {
            getTableData({ page }).then((res: any) => {
                // console.log('表格数据', res.data);
                setTableList(res.data)
            })
        }
        // 加载后生命周期
        useEffect(() => {
            // 发起请求,获取表格数据
            getTable()
        }, [])
        // 通过子组件,将方法传给孙组件(分页器)
        const ongetData = (val: any) => {
            // console.log('爷组件', val);
            getTable(val) // 获取数据
        }
        return (
            <div className="jiankong">
                <ZeroTable list={tableList} ongetData={ongetData} />
            </div>
        )
    }
    export default Jiankong
    

权限设计页(按钮级权限)

  • 思路

    1.在项目中进行权限切换
    	本质:在登录页输入不同的用户得到不同的权限,将这些数据存放到本地或全局
    2.上面的`权限设计`都是页面级权限
    3.按钮级权限:不同用户可以看到同一个页面,但是页面里的内容不同
    	本质:条件判断
        在src下新建一个文件,在文件中配置好当前项目中的所有按钮级的权限
    
  • 扩展

    1.项目中有多少权限:
    	后台管理系统:主管,人事,员工,行政,财务,经理...
    2.项目的怎么实现权限:
    	2-1.页面级权限:根据不同的用户信息后去到不同的侧边栏导航
        2-2.按钮级权限:不同用户可以看到同一个页面,但是页面里的内容不同
    3.权限的特点:
    	3-1.动态生成路由 => 根据不同的用户信息动态生成路由信息
    	3-2.动态生成侧边栏导航
        3-3.按钮级权限,实现一一对应
    
  • 仓库

    // 创建权限设计的reducer行为
    let datas: any = {
        user: {}, // 用户信息
        navList: [] // 侧边栏信息
    }
    function limitData(state: any = datas, actions: any) {
        switch (actions.type) {
            case 'limitchange':
                let user = actions.user
                let navList = actions.navlist
                return { ...state, user, navList }
            default:
                return state
        }
    }
    export default limitData
    
  • utils

    export function limitFind(val: any) {
        //  权限表
        let promise = ['哆啦A梦', 'admin']
        let find = promise.findIndex((item) => {
            return item == val
        })
        if (find > -1) {
            return true
        } else {
            return false
        }
    }
    
  • 权限页面

    // 权限设置页
    import "./limit.scss"; // 样式
    import { Select } from 'antd'; // 选择器
    import { useState } from "react";
    import { changeLimit } from "../../https/api/limit"; // 引入请求
    import { useDispatch, useSelector } from "react-redux";
    import { limitFind } from "../../utils/antLimit";
    function Limit() {
        let dispatch = useDispatch() // dispatch
        // 按钮级权限
        let [show, setShow] = useState(false)
        // 获取本地存储的当前用户
        let obj: any = sessionStorage.getItem('user')
        let user = JSON.parse(obj)
        // 获取store中的用户信息
        let storeDataUser = useSelector((state: any) => state.LimitData.user)
        // value值发送变化时调用
        const handleChange = (value: any) => {
            console.log('切换选项', value);
            // 发起请求获取数据
            changeLimit({ name: value }).then((res: any) => {
                // console.log('权限接口数据', res.data);
                // 更该本地存储数据
                sessionStorage.setItem('user', JSON.stringify(res.data.user))
                sessionStorage.setItem('navList', JSON.stringify(res.data.navlist))
                // 触发reducer行为
                dispatch({ type: 'limitchange', user: res.data.user, navList: res.data.navlist })
                // 按钮级权限
                let anniu = limitFind(value)
                // 设置按钮级权限的显隐
                setShow(anniu)
            })
        }
        return (
            <div className="limit">
                <div className="nametext">当前用户: {storeDataUser.name || user.name}</div>
                <Select
                    style={{ width: 120 }}
                    defaultValue={storeDataUser.name || user.name} //默认选中的条目
                    onChange={handleChange} // value值发送变化时调用
                    options={[ // 选项数据
                        {
                            value: '小张',
                            label: '小张',
                        },
                        {
                            value: '哆啦A梦',
                            label: '哆啦A梦',
                        }
                    ]}
                />
                {/* 按钮级权限 */}
                {show ? <div>{storeDataUser.name || user.name}</div> : ''}
            </div>
        )
    }
    export default Limit
    
    • 处理头部的用户名称为响应式(store)

性能优化

  • react 模块处理

    将react内容进行模块化拆分,一个模块一个组件
    因为react更新机制是:组件重新创建
    
  • 避免使用内联函数

    在编译模块的时候,react会将模块装成vnode(虚拟dom),里面所有的处理方法会合成集中处理
    如果第一层触发,返回的是`useState`修改数据方法,这个方法是一个极其复杂的方法
    我们应该使用嵌套函数返回一个自己写的方法,这样可以降低项目的性能损耗
    
  • Fragments 空的占位符

    <></>
    这是一个空的双标签,是一个占位符
    在项目中需要父子结构但是不确定使用什么类型的标签时使用
    
  • 路由懒加载

    作用:在使用了路由懒加载后,项目首次加载时,用户没有点过的懒加载页面,这个页面就不会加载,提高项目的加载速度
    路由懒加载中包含了异步组件
    语法:React.lazy() 配合 Suspense(首页白屏) 一起使用
    
  • 组件的缓存

    React.memo()
    缓存组件,只有当这个缓存组件中的数据发生改变了才会重新创建
    工作中一般不用useMemo,useCallback来做react的性能优化
    何时用:
    	当组件存在子传父的时候就需要使用
    
  • 动态生成路由、动态生成侧边栏导航

    比如:项目有10个权限,对应的侧边栏导航是不同的,需要通过动态生成来减少项目的代码
    
  • 将项目中的一些大的本地图片,上传到到云端(阿里云、七牛云),从而减少项目的体积

  • 减少项目中的错误代码

  • 列表渲染需要添加 key,因为 diff 算法