React实现后台管理系统(一)

一、脚手架create-react-app创建项目

create-react-app官方文档:https://create-react-app.bootcss.com/

1、使用命令行创建项目:

npx create-react-app my-app

在这里插入图片描述
出现以下内容说明创建成功:
在这里插入图片描述

2、使用npm start启动项目

在这里插入图片描述
在这里插入图片描述

二、react-router路由配置

react-router官方文档:https://reactrouter.com/en/main

1、下载react-router

下载指定版本的react-router

npm install react-router-dom@6.22.2

在这里插入图片描述

2、代码中路由配置

在src目录下新建router文件夹,在router文件夹下新建index.js文件:
在这里插入图片描述
在src目录下新建layout.js、home/index.js文件:
在这里插入图片描述

/* src/home/index.js */
import React from 'react'

const Home = () => {
  return <div>Home</div>
}
export default Home
/* src/layout.js */
import React from 'react'
import { Outlet } from 'react-router-dom'

const Layout = () => {
  return (
    <div>
      main
      <Outlet /> //子路由出口
    </div>
  )
}

export default Layout
/* src/router/index.js */
import { createBrowserRouter } from 'react-router-dom'
import Layout from '../pages/layout'
import Home from '../pages/home'

const routes = [
  {
    path: '/',
    Component: Layout,
    children: [
      {
        path: 'home',
        Component: Home
      }
    ]
  }
]

export default createBrowserRouter(routes)

修改App.js文件中的内容:

import './App.css'
import { RouterProvider } from 'react-router-dom'
import router from './router'

function App() {
  return (
    <div className='app'>
      <RouterProvider router={router} />
    </div>
  )
}

export default App

按照上述方法完成剩余页面路由配置:Mail页面,User页面,pageOne,pageTwo页面等

重定向配置:访问/时需要跳转到/home页面
修改后的router配置如下:

import { createBrowserRouter, Navigate } from 'react-router-dom'
import Layout from '../pages/layout'
import Home from '../pages/home'
import Mail from '../pages/mail'
import User from '../pages/user'
import pageOne from '../pages/other/pageOne'
import pageTwo from '../pages/other/pageTwo'

const routes = [
  {
    path: '/',
    Component: Layout,
    children: [
      //重定向
      {
        path: '/',
        element: <Navigate to='home' replace />
      },
      {
        path: 'home',
        Component: Home
      },
      {
        path: 'mail',
        Component: Mail
      },
      {
        path: 'user',
        Component: User
      },
      {
        path: 'other',
        children: [
          {
            path: 'pageOne',
            Component: pageOne
          },
          {
            path: 'pageTwo',
            Component: pageTwo
          }
        ]
      }
    ]
  }
]

export default createBrowserRouter(routes)

三、layout布局配置

1、layout布局组件引入

安装antd:

npm install antd

使用antd中的Layout组件:

/*  src/layout.js  */
import React, { useState } from 'react'
import {
  MenuFoldOutlined,
  MenuUnfoldOutlined,
  UploadOutlined,
  UserOutlined,
  VideoCameraOutlined
} from '@ant-design/icons'
import { Button, Layout, Menu, theme } from 'antd'
import { Outlet } from 'react-router-dom'

const { Header, Sider, Content } = Layout

const MyLayout = () => {
  const [collapsed, setCollapsed] = useState(false)
  const {
    token: { colorBgContainer, borderRadiusLG }
  } = theme.useToken()
  return (
    <Layout className='main-container'>
      <Sider trigger={null} collapsible collapsed={collapsed}>
        <h3 className='app-name'>通用后台管理系统</h3>
        <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'
            }
          ]}
          style={{
            height:'100%'
          }}
        />
      </Sider>
      <Layout>
        <Header
          style={{
            padding: 0,
            background: colorBgContainer
          }}
        >
          <Button
            type='text'
            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
            onClick={() => setCollapsed(!collapsed)}
            style={{
              fontSize: '16px',
              width: 64,
              height: 64
            }}
          />
        </Header>
        <Content
          style={{
            margin: '24px 16px',
            padding: 24,
            minHeight: 280,
            background: colorBgContainer,
            borderRadius: borderRadiusLG
          }}
        >
          Content
        </Content>
      </Layout>
    </Layout>
  )
}

export default MyLayout

修改App.js同级下的index.css文件内容:

html,body,div,span,applet,object,iframe,
h1,h2,h3,h4,h5,h6,p,blockquote,pre,
a,abbr,acronym,address,big,cite,
code,del,dfn,em,img,ins,kbd,
q,s,samp,small,strike,strong,
sub,sup,tt,var,b,u,i,center,
dl,dt,dd,ol,ul,li,
fieldset,form,label,legend,
table,caption,tbody,tfoot,thead,tr,th,td,
article,aside,canvas,details,embed,
figure,figcaption,footer,header,hgroup,
menu,nav,output,ruby,section,summary,
time,mark,audio,video {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
  box-sizing: border-box;
}

article,aside,details,figcaption,figure,
footer,header,hgroup,menu,nav,section {
  display: block;
}
body {
  line-height: 1;
}
ol,ul {
  list-style: none;
}
blockquote,q{
  quotes: none;
}

a,a:hover{
  color: inherit;
  text-decoration: none;
}
table{
  border-collapse: collapse;
  border-spacing: 0;
}
html,body{
  width: 100%;
  background-color: #f5f5f5;
}
.fl{
  float: left;
}
.fr{
  float:right;
}

.main-container{
  min-height: calc(100vh - 64px);
}
.main-container .ant-layout-content {
  margin: 0px !important;
}
.main-container .app-name{
  background-color: #001520;
  text-align: center;
  color: #fff;
  line-height: 64px;
  font-weight: bold;
  font-size: 16px;
}

.main-container .ant-layout-content {
  background-color: #f5f5f5 !important;
}

.main-container .ant-menu {
  height: calc(100vh - 64px) !important;
}

2、aside组件拆分及功能实现

① 拆分

在src目录下新建components文件夹用于存放公用组件,在components文件夹下新建aside.js文件:

/* src/components/aside.js */
import React, { useState } from 'react'
import { UploadOutlined, UserOutlined, VideoCameraOutlined } from '@ant-design/icons'
import { Layout, Menu } from 'antd'

const { Sider } = Layout

const Aside = props => {
  const { collapsed } = props
  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <h3 className='app-name'>通用后台管理系统</h3>
      <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'
          }
        ]}
        style={{
          height: '100%'
        }}
      />
    </Sider>
  )
}
export default Aside
/* src/layout.js */
import React, { useState } from 'react'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { Button, Layout, Menu, theme } from 'antd'
import Aside from '../components/aside'
import { Outlet } from 'react-router-dom'

const { Header, Sider, Content } = Layout

const MyLayout = () => {
  const [collapsed, setCollapsed] = useState(false)
  const {
    token: { colorBgContainer, borderRadiusLG }
  } = theme.useToken()
  return (
    <Layout className='main-container'>
      <Aside collapsed={collapsed} />
      <Layout>
        <Header
          style={{
            padding: 0,
            background: colorBgContainer
          }}
        >
          <Button
            type='text'
            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
            onClick={() => setCollapsed(!collapsed)}
            style={{
              fontSize: '16px',
              width: 64,
              height: 64
            }}
          />
        </Header>
        <Content
          style={{
            margin: '24px 16px',
            padding: 24,
            minHeight: 280,
            background: colorBgContainer,
            borderRadius: borderRadiusLG
          }}
        >
          Content
        </Content>
      </Layout>
    </Layout>
  )
}

export default MyLayout

② 动态菜单功能实现

在src文件下新建config/index.js文件用于存放菜单数据:

const menuList = [
  {
    path: '/home',
    name: 'home',
    label: '首页',
    icon: 'HomeOutlined',
    url: '/home/index'
  },
  {
    path: '/mail',
    name: 'mail',
    label: '商品管理',
    icon: 'ShopOutlined',
    url: '/mail/index'
  },
  {
    path: '/user',
    name: 'user',
    label: '用户管理',
    icon: 'UserOutlined',
    url: '/user/index'
  },
  {
    path: '/other',
    label: '其他',
    icon: 'SettingOutlined',
    children: [
      {
        path: '/other/pageOne',
        name: 'page1',
        label: '页面1'
      },
      {
        path: '/other/pageTwo',
        name: 'page2',
        label: '页面2'
      }
    ]
  }
]
export default menuList

修改aside.js文件中的内容,处理menuList中的数据为antd菜单组件Menu所需的数据格式:

import React, { useState } from 'react'
import * as Icon from '@ant-design/icons'
import { Layout, Menu } from 'antd'
import menuList from '../../config'

const { Sider } = Layout

const Aside = props => {
  const { collapsed } = props

  //动态获取icon
  const iconToElement = name => React.createElement(Icon[name])

  //菜单数据处理
  const getItems = (items, list = []) => {
    items.map(item => {
      list.push({
        key: item.path,
        ...(item.icon ? { icon: iconToElement(item?.icon) } : []),
        label: item.label,
        ...(item.children ? { children: getItems(item.children) } : [])
      })
    })
    return list
  }

  return (
    <Sider trigger={null} collapsible collapsed={collapsed}>
      <h3 className='app-name'>通用后台管理系统</h3>
      <Menu
        theme='dark'
        mode='inline'
        defaultSelectedKeys={['1']}
        items={getItems(menuList)}
        style={{
          height: '100%'
        }}
      />
    </Sider>
  )
}
export default Aside

3、header组件拆分及功能实现

① 拆分

在components文件夹下新建header.js文件:

/* src/components/header.js */
import React, { useState } from 'react'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { Button, Layout, Avatar } from 'antd'

const { Header, Sider, Content } = Layout

const MyHeader = props => {
  const { collapsed, setCollapsed } = props
  return (
    <Header className='header-container'>
      <Button
        type='text'
        icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
        onClick={() => setCollapsed(!collapsed)}
        style={{
          fontSize: '16px',
          width: 64,
          height: 32,
        }}
      />
    </Header>
  )
}
export default MyHeader
/* src/layout.js */
import React, { useState } from 'react'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { Button, Layout, Menu, theme } from 'antd'
import Aside from '../components/aside'
import MyHeader from '../components/header'
import { Outlet } from 'react-router-dom'

const { Header, Sider, Content } = Layout

const MyLayout = () => {
  const [collapsed, setCollapsed] = useState(false)
  const {
    token: { colorBgContainer, borderRadiusLG }
  } = theme.useToken()
  return (
    <Layout className='main-container'>
      <Aside collapsed={collapsed} />
      <Layout>
        <MyHeader collapsed={collapsed} setCollapsed={setCollapsed} />
        <Content
          style={{
            margin: '24px 16px',
            padding: 24,
            minHeight: 280,
            background: colorBgContainer,
            borderRadius: borderRadiusLG
          }}
        >
          Content
        </Content>
      </Layout>
    </Layout>
  )
}
export default MyLayout

② 导航样式及功能实现

在src文件下新建assets/images文件用于存放静态资源:
在这里插入图片描述
修改header/index.js文件中内容,加入头像相关内容:

/*  src/components/header.js  */
import React, { useState } from 'react'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons'
import { Button, Layout, Avatar, Dropdown } from 'antd'
import './index.css'

const { Header } = Layout

const MyHeader = props => {
  const { collapsed, setCollapsed } = props

  //登出
  const logout = () => {}

  const items = [
    {
      key: '1',
      label: (
        <a target='_blank' rel='noopener noreferrer'>
          个人中心
        </a>
      )
    },
    {
      key: '2',
      label: (
        <a target='_blank' onClick={() => logout} rel='noopener noreferrer'>
          退出
        </a>
      )
    }
  ]

  return (
    <Header className='header-container'>
      <Button
        type='text'
        icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
        onClick={() => setCollapsed(!collapsed)}
        style={{
          fontSize: '16px',
          width: 64,
          height: 32,
          backgroundColor: '#fff'
        }}
      />
      <Dropdown
        menu={{
          items
        }}
      >
        <Avatar src={<img src={require('../../assets/images/avatar.jpg')} />} />
      </Dropdown>
    </Header>
  )
}
export default MyHeader

在header文件下新建index.css文件用于导航样式修改:

.header-container {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

四、redux实现全局状态管理(以菜单栏展开收起状态为例)

redux中文官网:https://cn.redux.js.org/

1、安装React Redux、Redux Toolkit

npm install @reduxjs/toolkit react-redux

2、redux相关文件配置

在src文件夹下新建store用于存放redux相关内容:
在这里插入图片描述

src/store/reducers/tab.js文件内容:

import { createSlice } from '@reduxjs/toolkit'

const tabSlice = createSlice({
  name: 'tab',
  initialState: {
    isCollapse: false
  },
  reducers: {
    changeCollapse: state => {
      state.isCollapse = !state.isCollapse
    }
  }
})

export const { changeCollapse } = tabSlice.actions
export default tabSlice.reducer

src/store/index.js文件内容:

import { configureStore } from '@reduxjs/toolkit'
import TabReducer from './reducers/tab'

export default configureStore({
  reducer: {
    tab: TabReducer
  }
})

挂载Redux:
在src文件下的index.js文件中新增redux挂载内容:

import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'

import { Provider } from 'react-redux'
import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)
reportWebVitals()

页面使用:
菜单展开收起状态其实只需要组件间传值就能简单实现,只是此处用此作为例子实现redux,原始使用传值实现的方式此处我也做了保留。

  • src/pages/layout.js文件中加入redux中定义的展开收起状态,并将该状态传入aside、header子组件中:
... ...
import { useSelector } from 'react-redux'

... ...

const MyLayout = () => {
  ... ....
  //通过redux获取展开收起的状态
  const Collapsed = useSelector(state => state.tab.isCollapse)
  return (
    ... ...
      <Aside collapsed={collapsed} Collapsed={Collapsed} />
      <Layout>
        <MyHeader collapsed={collapsed} Collapsed={Collapsed} setCollapsed={setCollapsed} />
        ... ...
      </Layout>
    </Layout>
  )
}

export default MyLayout
  • src/components/header/index.js文件中添加如下代码:
... ...
import { useDispatch } from 'react-redux'
import { changeCollapse } from '../../store/reducers/tab'

... ...
const MyHeader = props => {
  const { collapsed, setCollapsed, Collapsed } = props
  const dispatch = useDispatch()

  ... ... 
  const changeCollapsed = () => {
    dispatch(changeCollapse())
  }

 ... ... 
      <Button
        type='text'
        icon={Collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
        // onClick={() => setCollapsed(!collapsed)}
        onClick={changeCollapsed}
        ... ...
      />
     ... ...
    </Header>
  )
}
export default MyHeader
  • src/components/aside/index.js文件中添加如下代码:
... ...
const Aside = props => {
  const { collapsed, Collapsed } = props
  
  ... ...
  
    <Sider trigger={null} collapsible collapsed={Collapsed}>
      <h3 className='app-name'>{Collapsed ? '后台' : '通用后台管理系统'}</h3>
      ... ...
      

五、菜单路由联动

在aside.js文件中增加菜单的点击事件,使用useNavigate钩子实现路由跳转功能(如果你使用的是react-router-dom v6以下的版本,那么你应该使用useHistory钩子来替代useNavigate):

... ...
import { useNavigate } from 'react-router-dom'

... ...

  const clickMenu = e => {
    navigate(e.key)
  }

  ... ...
      <Menu
        theme='dark'
        mode='inline'
        defaultSelectedKeys={['1']}
        items={getItems(menuList)}
        style={{
          height: '100%'
        }}
        onClick={clickMenu}
      />
   ... ...

在layout.js中使用Outlet设置子页面出口:

... ...

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

... ...
        <Content
          ... ...
        >
          <Outlet />
        </Content>
... ...

六、Axios二次封装

axios官网:https://www.axios-http.cn/

1、安装axios

npm install axios

在这里插入图片描述

2、二次封装

① 新建文件

在src文件下新建utils/request.js文件用于封装axios
在这里插入图片描述

② 创建axios实例

const axiosInstance = axios.create({
  baseURL: '/api',
  timeout: 1000 //设置请求超时时间
})

③ 拦截器

  • 请求拦截器
//请求拦截器
axiosInstance.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token') //假设token存储在localStorage中
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    } else {
      throw new Error('Token not found')
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
  • 响应拦截器
//响应拦截器
axiosInstance.interceptors.response.use(
  response => {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response
  },
  error => {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error)
  }
)

④ 封装GET请求

const get = (url, params = {}, options = {}) => {
  return axiosInstance.get(url, { params, ...options });
}

⑤ 封装POST请求

const post = (url, data = {}, options = {}) => {
  return axiosInstance.post(url, data, options);
}

⑥ 导出函数

export { get, post, axiosInstance }

七、mock模拟接口数据

mockjs官网:http://mockjs.com/

1、安装mockjs

npm install mockjs

在这里插入图片描述

2、mock配置文件

在src/utils文件夹下新建mock.js文件用于编写mock请求拦截及接口数据模拟,并在根目录下的index.js文件(入口文件)中引入mock.js文件(import './utils/mock'

① mock请求拦截

/* src/utils/mockjs */
import Mock from 'mockjs'

//拦截接口
Mock.mock(/home\/getData/, function () {
  console.log('被拦截的getData接口')
})

② mock数据引入

在src文件下新建mock/home.js存放首页相关数据:

import Mock from 'mockjs'

//图表数据
let List = []
for (let i = 0; i < 7; i++) {
  List.push(
    Mock.mock({
      苹果: Mock.Random.float(100, 8000, 0, 0),
      vivo: Mock.Random.float(100, 8000, 0, 0),
      oppo: Mock.Random.float(100, 8000, 0, 0),
      魅族: Mock.Random.float(100, 8000, 0, 0),
      三星: Mock.Random.float(100, 8000, 0, 0),
      小米: Mock.Random.float(100, 8000, 0, 0)
    })
  )
}

const homeApi = {
  code: 200,
  data: 
    videoData: [
      {
        name: '小米',
        value: 2999
      },
      {
        name: '苹果',
        value: 5999
      },
      {
        name: 'vivo',
        value: 1500
      },
      {
        name: 'oppo',
        value: 1999
      },
      {
        name: '魅族',
        value: 2200
      },
      {
        name: '三星',
        value: 4500
      }
    ],
    userData: [
      {
        date: '周一',
        new: 5,
        active: 200
      },
      {
        date: '周二',
        new: 10,
        active: 500
      },
      {
        date: '周三',
        new: 12,
        active: 550
      },
      {
        date: '周四',
        new: 60,
        active: 800
      },
      {
        date: '周五',
        new: 65,
        active: 550
      },
      {
        date: '周六',
        new: 53,
        active: 770
      },
      {
        date: '周日',
        new: 33,
        active: 170
      }
    ],
    orderData: {
      date: ['20241001', '20241002', '20241003', '20241004', '20241005', '20241006', '20241007'],
      data: List
    },
    tableData: [
      {
        name: 'oppo',
        todayBuy: 500,
        monthBuy: 3500,
        totalBuy: 22000
      },
      {
        name: 'vivo',
        todayBuy: 300,
        monthBuy: 2200,
        totalBuy: 24000
      },
      {
        name: '苹果',
        todayBuy: 800,
        monthBuy: 4500,
        totalBuy: 65000
      },
      {
        name: '小米',
        todayBuy: 1200,
        monthBuy: 6500,
        totalBuy: 45000
      },
      {
        name: '三星',
        todayBuy: 300,
        monthBuy: 2000,
        totalBuy: 34000
      },
      {
        name: '魅族',
        todayBuy: 350,
        monthBuy: 3000,
        totalBuy: 22000
      }
    ],
    //统计数据
    countData: [
      {
        name: '今日支付订单',
        value: 1234,
        icon: 'PayCircleOutlined',
        color: '#33d294'
      },
      {
        name: '今日收藏订单',
        value: 4545,
        icon: 'HeartOutlined',
        color: '#ff6a4d'
      },
      {
        name: '今日未支付订单',
        value: 1234,
        icon: 'CloseCircleOutlined',
        color: '#5ab1ef'
      },
      {
        name: '本月支付订单',
        value: 1234,
        icon: 'PayCircleOutlined',
        color: '#33d294'
      },
      {
        name: '本月收藏订单',
        value: 666,
        icon: 'HeartOutlined',
        color: '#ff6a4d'
      },
      {
        name: '本月未支付订单',
        value: 3423,
        icon: 'CloseCircleOutlined',
        color: '#5ab1ef'
      }
    ]
  }
}

export default homeApi

修改src/utils/mockjs文件内容:

import Mock from 'mockjs'
import homeApi from '../mock/home'

//拦截接口
Mock.mock(/home\/getData/, homeApi)

3、页面调用接口获取mock数据

在src/page/home文件夹下新建services.js文件:
在这里插入图片描述

/* src/page/home/services.js */
import { get, post } from '../../utils/request'

export const getData = async () => {
  return get('/home/getData')
}

修改src/page/home/index.js文件:

import React, { useEffect, useState } from 'react'
import { getData } from './services'

const Home = () => {
  const [homeData, setHomeData] = useState({})
  useEffect(() => {
    getData().then(res => {
      console.log(res, 'res')
      setHomeData(res.data.data)
    })
  }, [])
  return <div>Home页面</div>
}
export default Home

八、首页内容实现

1、首页用户card内容展示

使用antd中的Grid栅格组件进行布局,然后使用Card组件实现首页用户Card的内容展示。

src/page/home/index.js文件:

... ...
import { Col, Row, Card } from 'antd'
import './index.css'

const Home = () => {
  ... ...
  return (
    <Row className='home' gutter={20}>
      <Col span={8}>
        <Card hoverable>
          <div className='user'>
            <img src={require('../../assets/images/avatar.jpg')}></img>
            <div className='userInfo'>
              <p className='name'>Admin</p>
              <p className='role'>超级管理员</p>
            </div>
          </div>
          <div className='login-info'>
            <p>
              上次登录时间:
              <span>2024-07-17</span>
            </p>
            <p>
              上次登录地点:
              <span>成都</span>
            </p>
          </div>
        </Card>
      </Col>
      <Col span={16}></Col>
    </Row>
  )
}
export default Home

src/page/home/index.css文件:

.user {
  border-bottom: 1px solid #ccc;
}

.user {
  display: flex;
  align-items: center;
  padding-bottom: 20px;
  margin-bottom: 20px;
}

.user img {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background-size: contain;
  margin-right: 20px;
}

.user .name {
  font-size: 20px;
  font-weight: 600;
  margin-bottom: 10px;
}

.user .role {
  font-size: 10px;
}

.login-info p {
  line-height: 28px;
  font-size: 12px;
  color: #999;
}

.login-info p span {
  /* line-height: 28px;
  font-size: 12px; */
  color: #666;
  margin-left: 60px;
}

效果:
在这里插入图片描述

2、首页table数据显示

使用antd中的table组件实现。

src/page/home/index.js文件:

... ...
import { Col, Row, Card, Table } from 'antd'
... ...

const Home = () => {
  ... ...
  const [tableData, setTableData] = useState([])
  useEffect(() => {
    getData().then(res => {
      setTableData(res.data.data.tableData)
    })
  }, [])

  const columns = [
    {
      title: '课程',
      dataIndex: 'name',
      key: 'name'
    },
    {
      title: '今日购买',
      dataIndex: 'todayBuy',
      key: 'todayBuy'
    },
    {
      title: '本月购买',
      dataIndex: 'monthBuy',
      key: 'monthBuy'
    },
    {
      title: '总购买',
      key: 'totalBuy',
      dataIndex: 'totalBuy'
    }
  ]
  return (
    <Row className='home'>
      <Col span={8}>
        ... ...
        <Card hoverable>
          <Table columns={columns} dataSource={tableData} pagination={false} rowKey={'name'} />
        </Card>
      </Col>
      <Col span={16}></Col>
    </Row>
  )
}
export default Home

src/page/home/index.css文件:

... ...
.login-info p span {
  /* line-height: 28px;
  font-size: 12px; */
  color: #666;
  margin-left: 60px;
}

.home .table {
  margin-top: 20px;
}

效果:
在这里插入图片描述

3、首页订单统计实现

src/page/home/index.js文件:

... ...
import * as Icon from '@ant-design/icons'

const Home = () => {
  ... ...
  const [countData, setCountData] = useState([])
  useEffect(() => {
    getData().then(res => {
      ... ...
      setCountData(res.data.data.countData)
    })
  }, [])

  //动态获取icon
  const iconToElement = name => React.createElement(Icon[name])
  
 ... ...
 
  return (
    <Row className='home' gutter={20}>
      <Col span={8}>
        ... ...
      </Col>
      <Col span={16}>
        <div className='num'>
          {countData.map((item, index) => {
            return (
              <Card key={index}>
                <div className='icon' style={{ backgroundColor: item.color }}>
                  {iconToElement(item.icon)}
                </div>
                <div className='desc'>
                  <p className='value'>{item.value}</p>
                  <p className='name'>{item.name}</p>
                </div>
              </Card>
            )
          })}
        </div>
        <div></div>
        <div></div>
      </Col>
    </Row>
  )
}
export default Home

src/page/home/index.css文件:

... ...

.home .table {
  margin-top: 20px;
}

.home .num {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
}

.home .num .ant-card {
  width: 33%;
}

.home .num .ant-card-body {
  display: flex;
  padding: 10px 25px;
}

.home .num .icon {
  width: 80px;
  height: 80px;
  text-align: center;
  line-height: 85px;
  color: #fff;
  border-radius: 5px 0 0 5px;
}

.home .anticon {
  font-size: 30px;
}

.home .desc {
  margin-left: 15px;
}

.home .desc .value {
  font-size: 30px;
  margin-bottom: 10px;
}

.home .desc .name {
  font-size: 14px;
  text-align: center;
  color: #999;
}

效果:
在这里插入图片描述

4、首页图表统计实现

使用echart实现图表的展示效果。

  • 安装echarts
npm install echarts --save

在这里插入图片描述

echarts组件封装

在components文件夹下新建echarts/index.js文件以及index.css文件:
在这里插入图片描述

echarts/index.js文件:

import React, { useState, useEffect, useRef } from 'react'
import * as echarts from 'echarts'

const Echarts = props => {
  const { style = {}, chartData, isAxisChart = true } = props
  const chartRef = useRef()
  const echartObj = useRef(null)

  //echarts配置数据
  //有坐标系
  const axisOption = {
    //图例文字颜色
    textStyle: {
      color: '#333'
    },
    //提示框
    tooltip: {
      trigger: 'axis'
    },
    xAxis: {
      type: 'category', //类目轴
      data: [],
      axisLine: {
        lineStyle: {
          color: '#17b3a3'
        }
      },
      axisLabel: {
        interval: 0,
        color: '#333'
      }
    },
    yAxis: [
      {
        type: 'value',
        axisLine: {
          lineStyle: {
            color: '#17b3a3'
          }
        }
      }
    ],
    color: ['#2ec7c9', '#b6a2de', '#ffb980', '#5ab1ef', '#8d98b3', '#d87a80'],
    series: []
  }
  //没有坐标系
  const normalOption = {
    tooltip: {
      trigger: 'item'
    },
    color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452'],
    series: []
  }

  useEffect(() => {
    let options
    //echarts初始化
    echartObj.current = echarts.init(chartRef.current)
    //设置option
    if (isAxisChart) {
      //有坐标系
      axisOption.xAxis.data = chartData?.xData
      axisOption.series = chartData?.series
      options = axisOption
    } else {
      //没有坐标系
      normalOption.series = chartData?.series
      options = normalOption
    }
    echartObj.current.setOption(options)
  }, [chartData])
  return <div style={style} ref={chartRef}></div>
}
export default Echarts

②折线图的显示

page/home/index.js文件:

... ...
import MyEcharts from '../../components/echarts'

const Home = () => {
  ... ...
  const [echartData, setEchartData] = useState({})
  useEffect(() => {
    getData().then(res => {
      const { tableData, countData, orderData, userData, videoData } = res.data.data
      ... ...
      
      //折线图series数据组装
      const arr = Object.keys(orderData.data[0])
      const series = []
      arr.forEach(key => {
        series.push({
          name: key,
          type: 'line',
          stack: 'Total',
          data: orderData.data.map(item => item[key])
        })
      })
      setEchartData({
        order: {
          xData: orderData.date,
          series
        }
      })
    })
  }, [])
	... ...
  return (
    <Row className='home' gutter={20}>
      ... ...
        {echartData?.order && (
          <MyEcharts chartData={echartData?.order} style={{ height: '270px' }} />
        )}
        <div></div>
      </Col>
    </Row>
  )
}
export default Home

效果:
在这里插入图片描述

②柱状图和饼状图的显示

page/home/index.js文件:

... ...
import MyEcharts from '../../components/echarts'

const Home = () => {
  ... ...
  useEffect(() => {
    getData().then(res => {
      console.log(111, res.data.data)
      const { tableData, countData, orderData, userData, videoData } = res.data.data
      ... ...
      setEchartData({
        ... ...
        user: {
          xData: userData.map(item => item.date),
          series: [
            {
              name: '新增用户',
              type: 'bar',
              data: userData.map(item => item.new)
            },
            {
              name: '活跃用户',
              type: 'bar',
              data: userData.map(item => item.active)
            }
          ]
        },
        video: {
          series: [
            {
              data: videoData,
              type: 'pie',
              radius: '50%'
            }
          ]
        }
      })
    })
  }, [])
	... ...
	
  return (
    <Row className='home' gutter={20}>
      ... ...
        {echartData?.order && (
          <MyEcharts chartData={echartData?.order} style={{ height: '270px' }} />
        )}
        <div className='chart'>
          {echartData?.user && (
            <MyEcharts chartData={echartData?.user} style={{ height: '220px', width: '50%' }} />
          )}
          {echartData?.video && (
            <MyEcharts
              chartData={echartData?.video}
              isAxisChart={false}
              style={{ height: '220px', width: '50%' }}
            />
          )}
        </div>
      ... ...
    </Row>
  )
}
export default Home

page/home/index.css文件:

... ...
.home .desc .name {
  font-size: 14px;
  text-align: center;
  color: #999;
}

.home .chart {
  display: flex;
}

效果:
在这里插入图片描述

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值