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;
}
效果: