自学内容网 自学内容网

react后台管理系统(四)

​🌈个人主页:前端青山
🔥系列专栏:React篇
🔖人终将被年少不可得之物困其一生

依旧青山,本期给大家带来React篇专栏内容:react后台管理系统(四)

前言

本文将详细介绍如何在React项目中集成ECharts,实现柱状图和折线图的动态渲染,并通过路由配置实现页面导航。

目录

前言

26.实现登录

27.封装获取左侧菜单数据

28.动态渲染左侧的菜单栏

29退出

30.配置路由 的权限

31.未登录直接访问登录页面

32.使用token验证

33.使用redux管理状态

34.使用redux-thunk解决异步问题

35.数据可视化

echarts

antv

highCharts

d3

react + echarts

总结

26.实现登录

// layout/login/Index.jsx
import React, { Component } from 'react'
​
class App extends Component {
  render() {
    return (
      <div>
        登录
      </div>
    )
  }
}
export default App
// App.jsx
import React from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import Main from './layout/main/index.jsx'
import Login from './layout/login/Index.jsx'
function App () {
  return (
    <Router >
      <Switch>
        <Route path="/login" exact component = {Login}/>
        <Route path="/" component = {Main}/>
      </Switch>
    </Router>
  )
}
​
export default App
​

完善登录布局

import React, { Component } from 'react'
import { Divider, Space, Input, Button } from 'antd'
class App extends Component {
  render() {
    return (
      <div className="loginContainer">
        <div className="loginBox">
          <div className="loginLogo">
            嗨购管理系统
          </div>
          <Divider>使用用户名登录</Divider>
          <Space direction="vertical" style={{ width: '100%'}}>
            <div style={{ width: '100%', height: 40, borderBottom: '1px solid #999'}}>
              <Input size="large" bordered = { false } placeholder="请输入用户名"/>
            </div>
            <div style={{ width: '100%', height: 40, borderBottom: '1px solid #999'}}>
              <Input size="large" type="password" bordered = { false } placeholder="请输入密码"/>
            </div>
            <Button style={{ marginTop: 50}} type="primary" block size="large">登录</Button>
          </Space>
        </div>
      </div>
    )
  }
}
export default App
@import '~antd/dist/antd.css';
#root { height: 100%;}
.ant-layout { height: 100%;}
.mySider .trigger {
  padding: 0 24px;
  font-size: 18px;
  line-height: 64px;
  cursor: pointer;
  transition: color 0.3s;
}
​
.mySider .trigger:hover {
  color: #1890ff;
}
​
.mySider .logo {
  height: 32px;
  margin: 16px;
  /* background: rgba(255, 255, 255, 0.3); */
  color: #fff;
  font-size: 18px;
  line-height: 32px;
}
​
.site-layout .site-layout-background {
  background: #fff;
}
/* 登录 */
.loginContainer {
  width: 100%;
  height: 100%;
  background: url(https://cas.1000phone.net/cas/images/login/bg.png) center no-repeat;
  background-size: cover;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.loginBox {
  width: 420px;
  height: 460px;
  background-color: rgba(255, 255, 255, 1);
  border-radius: 32px;
  padding: 10px 30px;
}
.loginLogo {
  width: 100%;
  height: 80px;
  font-size: 32px;
  display: flex;
  justify-content: center;
  align-items: center;
}

实现登录功能

import React, { Component } from 'react'
import { Divider, Space, Input, Button, message } from 'antd'
import { login } from './../../api/users'
class App extends Component {
  state = {
    adminname: '',
    password: ''
  }
  changeAdminname = (e) => this.setState({ adminname: e.target.value })
  changePassword = (e) => this.setState({ password: e.target.value })
  loginFn = () => {
    const params = {
      adminname: this.state.adminname,
      password: this.state.password
    }
    login(params).then(res => {
      if (res.data.code === '10003') {
        message.warning('密码错误')
      } else if (res.data.code === '10005') {
        message.warning('账户不存在')
      } else {
        console.log(res.data.data)
        // 信息存入本地
        localStorage.setItem('adminToken', res.data.data.token)
        localStorage.setItem('adminname', res.data.data.adminname)
        localStorage.setItem('role', res.data.data.role)
        localStorage.setItem('loginState', true)
        // 跳转到首页
        this.props.history.push('/')
      }
    })
  }
  render() {
    return (
      <div className="loginContainer">
        <div className="loginBox">
          <div className="loginLogo">
            嗨购管理系统
          </div>
          <Divider>使用用户名登录</Divider>
          <Space direction="vertical" style={{ width: '100%'}}>
            <div style={{ width: '100%', height: 40, borderBottom: '1px solid #999'}}>
              <Input value = { this.state.adminname } onChange = { this.changeAdminname }
              size="large" bordered = { false } placeholder="请输入用户名"/>
            </div>
            <div style={{ width: '100%', height: 40, borderBottom: '1px solid #999'}}>
              <Input value = { this.state.password }  onChange = { this.changePassword }
              size="large" type="password" bordered = { false } placeholder="请输入密码"/>
            </div>
            <Button onClick={ this.loginFn } style={{ marginTop: 50}} type="primary" block size="large">登录</Button>
          </Space>
        </div>
      </div>
    )
  }
}
export default App

27.封装获取左侧菜单数据

// router/getMenu.js
// import menus from './menus'
​
// const checkedKeys = ['0-0', '0-1-0', '0-2-1', '0-3-1']
// ['0-0', '0-1', '0-1-0', '0-2', '0-2-1', '0-3']
export function getMenu (menus, checkedKeys) {
  console.log(checkedKeys)
  // 1.想办法获取到 ['0-0', '0-1', '0-1-0', '0-2', '0-2-1', '0-3']
  // 先获取到所有的一级菜单, 合并原来的数组,再去重
  const arr = []
  checkedKeys.forEach(item => {
    arr.push(item.slice(0, 3))
  }) // ['0-0', '0-1', '0-2', '0-3']
  // 合并数组,并且去重
  const lastArr = Array.from(new Set([ ...arr, ...checkedKeys]))
  // ['0-0', '0-1', '0-1-0', '0-2', '0-2-1', '0-3']
  console.log(lastArr)
  // 2.遍历lastArr,组合数据
  const result = []
  lastArr.forEach(item => {
    // 2.1获取真实对应的索引值
    const index1 = item.split('-')[1]
    const index2 = item.split('-')[2]
    let obj = {}
    // 2.2 如果index2存在,一定有二级菜单,如果不存在, 判断index1对应的数据存在不存在
    if (index2 >= 0) {
      // 2.4 先构建一级菜单的数据
      obj = {
        title: menus[index1].title,
        path: menus[index1].path,
        key: menus[index1].key,
        icon: menus[index1].icon,
        redirect: menus[index1].redirect,
        children: []
      }
      // 2.5
      const cindex = result.findIndex(item => item.key === obj.key)
      if (cindex === -1) { // 添加二级菜单
        obj.children.push({
          title: menus[index1].children[index2].title,
          path: menus[index1].children[index2].path,
          key: menus[index1].children[index2].key,
          icon: menus[index1].children[index2].icon,
          component: menus[index1].children[index2].component
        })
        result.push(obj)
      } else { // 给既有的一级菜单添加二级菜单
        result[cindex].children.push({
          title: menus[index1].children[index2].title,
          path: menus[index1].children[index2].path,
          key: menus[index1].children[index2].key,
          icon: menus[index1].children[index2].icon,
          component: menus[index1].children[index2].component
        })
      }
    } else {
      // 2.3 先构建一级菜单的数据
      obj = {
        title: menus[index1].title,
        path: menus[index1].path,
        key: menus[index1].key,
        icon: menus[index1].icon,
        component: menus[index1].component,
        children: []
      }
      // 判断数据中有没有obj对应的数据,如果没有,则插入,如果有,不做操作
      !result.some(item => item.key === obj.key) && result.push(obj)
    }
  })
  // console.log(result)
  return result
}

28.动态渲染左侧的菜单栏

// api/users.js
import request from './../utils/request'
​
export function getAdminList (params) {
  return request.get('/admin/list', { params })
}
​
export function addAdmin (params) {
  return request.post('/admin/add', params)
}
​
export function updateAdmin (params) {
  return request.post('/admin/update', params)
}
export function deleteAdmin (params) {
  return request.post('/admin/delete', params)
}
​
export function login (params) {
  return request.post('/admin/login', params)
}
​
​
export function getAdminDetail (params) {
  return request.get('/admin/detail', {params})
}
// layout/main/SideMenu.jsx
import React, { useState, useEffect } from 'react'
import { Menu } from 'antd';
import menus from '../../router/menus'
import { useHistory, useLocation } from 'react-router-dom'
import { getMenu } from './../../router/getMenu'
import { getAdminDetail } from './../../api/users'
const { SubMenu } = Menu;
​
// submenu keys of first level
const keyArr = []
menus.forEach(item => { // 只需要一级菜单项即可
  keyArr.push(item.path)
})
// const rootSubmenuKeys = ['sub1', 'sub2', 'sub4'];
const rootSubmenuKeys = keyArr
​
const SideMenu = () => {
  // 获取地址栏中的地址
  const location = useLocation()
  const { pathname } = location // /banner/list
  console.log(location)
  // 用来 编程式跳转
  const history = useHistory()
  // 设置默认打开哪一项  ---- 参数是 string[] ['/banner']
  const [openKeys, setOpenKeys] = React.useState(['/' + pathname.split('/')[1] ]);
​
  const onOpenChange = keys => {
    const latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1);
    if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
      setOpenKeys(keys);
    } else {
      setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
    }
  };
  const renderMenus = (menus) => {
    return menus.map(item => {
      // 判断当前的选项有没有子菜单
      // ——————————————————*********开启********——————————————
      if (item.children && item.children.length > 0) { // 有子级菜单
        return (
          <SubMenu key={ item.path } icon={ item.icon } title={item.title}>
            {/* 递归调用自身,渲染多级菜单 */}
            { renderMenus(item.children) }
          </SubMenu>
        )
      } else {
        // path具有唯一性,key也具有唯一性
        // 为了后续操作方便,此时建议将path作为遍历的key值
        return <Menu.Item key={ item.path } icon = { item.icon }>
          { item.title }
        </Menu.Item>
      }
    })
  }
  const changeUrl = ({ key }) => {
    // console.log(key) // 为什么在渲染菜单栏时 把 item.path 置为 key属性
    // console.log(item)
    // 编程式跳转 props.history.push
    history.push(key)
  }
​
  {/*——————————————————*********开启********——————————————*/}
  const [myMenu, setMyMenu] = useState([])
  useEffect(() => {
    const adminname = localStorage.getItem('adminname')
    getAdminDetail({
      adminname
    }).then(res => {
      console.log(res.data.data)
      if (adminname === 'admin') {
        setMyMenu(menus)
      } else {
        console.log(res.data.data[0].checkedKeys)
        const testMenus = getMenu(menus, res.data.data[0].checkedKeys)
        setMyMenu(testMenus)
      }
    })
  }, [])
  {/*——————————————————*********结束********——————————————*/}
  return (
    <Menu onClick = { changeUrl } theme="dark" 
    mode="inline" 
    openKeys={openKeys} 
    selectedKeys = { [pathname] }
    onOpenChange={onOpenChange} >
      {
        {/*——————————————————*******myMenu**********——————————————*/}
        renderMenus(myMenu)
      }
      {/* <Menu.Item key="1">Option 1</Menu.Item>
      <SubMenu key="sub1" icon={<MailOutlined />} title="Navigation One">
        <Menu.Item key="1">Option 1</Menu.Item>
        <Menu.Item key="2">Option 2</Menu.Item>
        <Menu.Item key="3">Option 3</Menu.Item>
        <Menu.Item key="4">Option 4</Menu.Item>
      </SubMenu> */}
    </Menu>
  );
};
​
export default SideMenu

考虑到在添加管理员时,如果不勾选权限管理项,代码会出错误,就在添加管理员时做判断

// views/users/AdminList.jsx
const addAdminFn = () => {
 const obj = { adminname, password, role, checkedKeys }
 console.log(obj)
 if (checkedKeys.length === 0) { // ***************************
   message.error('请先选择权限管理项')
   return
 }
 addAdmin(obj).then((res) => {
   if (res.data.code === '10004') {
     notification.warning({
       duration: 2,
       message: '消息提醒',
       description:
         '该管理员已存在',
       onClick: () => {
       },
     });
   } else {
     // 抽屉消失,表单回归初始状态
     setVisible(false)
     setAdminname('')
     setPasssword('')
     setRole('1')
     setCheckedKeys([])
     // 重新渲染一次页面
     getAdminList().then(res => {
       setAdminList(res.data.data)
     })
   }
 })
}

发现左侧菜单渲染时顺序会有变化,给menus.js 每一个一级菜单添加 index 索引值

export function getMenu (menus, checkedKeys) {
​
// console.log(result)
result.sort((a, b) => a.index - b.index) // **************************
return result
}

29退出

import React from 'react'
import { Layout, Button } from 'antd';
import logo from '../../logo.svg'
import { withRouter } from 'react-router-dom'
import {
  MenuUnfoldOutlined,
  MenuFoldOutlined
} from '@ant-design/icons';
import SideMenu from './SideMenu';
import RouterView from './RouterView';
​
const { Header, Sider, Content } = Layout;
​
class App extends React.Component {
  
}
​
export default withRouter(App)
 // ************************

30.配置路由 的权限

// layout/main/RouterView.jsx
import React, { Suspense, useState, useEffect } from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'
import { Spin } from 'antd';
import menus from './../../router/menus'
import NotFound from './../../views/error/NotFound/Index'
import { getMenu } from './../../router/getMenu'
import { getAdminDetail } from './../../api/users'
function RouterView() {
  const renderRoute = (menus) => {
    return menus.map(item => {
      // ********************************************
      if (item.children && item.children.length > 0) {
        // 递归调用
        return renderRoute(item.children)
      } else {
        return <Route exact path = { item.path } key = { item.path } component = { item.component } />
      }
    })
  }
  const renderRedirect = (menus) => {
    return menus.map(item => {
      if (item.redirect) {
        return <Redirect exact key={ item.key } from = { item.path } to = { item.redirect } />
      } else {
        return null
      }
    })
  }
  // ************************************
  const [myMenu, setMyMenu] = useState([])
  useEffect(() => {
    const adminname = localStorage.getItem('adminname')
    getAdminDetail({
      adminname
    }).then(res => {
      console.log(res.data.data)
      if (adminname === 'admin') {
        setMyMenu(menus)
      } else {
        // console.log(res.data.data[0].checkedKeys)
        const testMenus = getMenu(menus, res.data.data[0].checkedKeys)
        console.log(testMenus)
        setMyMenu(testMenus)
      }
    })
  }, [])
  return (
    <Suspense fallback = { <Spin /> }>
      <Switch>
        {/* <Route path="" component={} /> */}
        {
          // ***************
          renderRoute(myMenu)
        }
        {
          // ***************
          renderRedirect(myMenu)
        }
        <Redirect exact from = "/" to="/home" />
        <Route component = { NotFound } />
      </Switch>
    </Suspense>
    
  )
}
​
export default RouterView

31.未登录直接访问登录页面

import React from 'react'
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'
import Main from './layout/main/index.jsx'
import Login from './layout/login/Index.jsx'
function App () {
  return (
    <Router >
      <Switch>
        <Route path="/login" exact component = {Login}/>
        {/* <Route path="/" component = {Main}/> */}
        <Route path = "/" render = { () => {
          return localStorage.getItem('loginState') === 'true' ? <Main /> :
            <Redirect to="/login" /> 
        }} />
      </Switch>
    </Router>
  )
}
​
export default App

32.使用token验证

可以通过axios的拦截器统一处理 token信息

import axios from 'axios'
​
// 判断用户当前运行的环境 开发环境 生产环境
// 访问不同的环境 ,请求不同的地址
// 开发环境  npm run start  ---- development
// 生产环境  npm run build  ---- production
// process.env.NODE_ENV 
const isDev = process.env.NODE_ENV === 'development'
​
// http://121.89.205.189/admin/banner/add
// 请求是请求 /banner/add
const request = axios.create({ // 自定义axios
  // baseURL: 'http://121.89.205.189/admin'
  // baseURL: isDev ? 'http://localhost:3001/admin' : 'http://121.89.205.189/admin'
  baseURL: isDev ? 'http://121.89.205.189/admin' : 'http://121.89.205.189/admin'
})
​
// axios 拦截器
// 添加请求拦截器
request.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  config.headers.common.token = localStorage.getItem('adminToken')
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});
​
// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  if (response.data.code === '10119') {
    window.location.href = "/login"
  }
  return response;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});
export default request

33.使用redux管理状态

cnpm i redux react-redux redux-thunk -S
// store/modules/user.js
const reducer = (state = {
  adminname: localStorage.getItem('adminname') || '',
  adminToken: localStorage.getItem('adminToken') || '',
  role: localStorage.getItem('role') || 1,
  loginState: localStorage.getItem('loginState') === 'true' || false
}, { type, payload }) => {
  switch (type) {
    case 'CHANGE_ADMIN_NAME':
      return { ...state, adminname: payload }
    case 'CHANGE_ADMIN_TOKEN':
      return { ...state, adminToken: payload }
    case 'CHANGE_ROLE':
      return { ...state, role: payload  }
    case 'CHANGE_LOGIN_STATE':
      return { ...state, loginState: payload }
    default:
      return state
  }
}
export default reducer


// store/modules/pro.js
const reducer = (state = {
  prolist: [],
  bannerlist: []
}, { type, payload }) => {
  return state
}
export default reducer
// store/index.js
import { createStore, combineReducers } from 'redux'
import user from './modules/user'
import pro from './modules/pro'
const reducer = combineReducers({
  user,
  pro
})
const store = createStore(reducer)
export default store
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App.jsx';
import { ConfigProvider } from 'antd';
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from 'antd/lib/locale/zh_CN';
import { Provider } from 'react-redux'
import store from './store'
// import { getMenu } from './router/getMenu'
ReactDOM.render(
  <ConfigProvider locale = { zhCN }>
    <Provider store = { store }>
      <App />
    </Provider>
  </ConfigProvider>,
  document.getElementById('root')
);
// getMenu()
​

多个组件需要使用同一个状态

// App.jsx
import React from 'react'
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'
import Main from './layout/main/index.jsx'
import Login from './layout/login/Index.jsx'
import { connect } from 'react-redux' // _______******______
function App (props) {
  return (
    <Router >
      <Switch>
        <Route path="/login" exact component = {Login}/>
        {/* <Route path="/" component = {Main}/> */}
        <Route path = "/" render = { () => {
          // 导航守卫 - 路由守卫 // _______******______
          return props.loginState ? <Main /> :
            <Redirect to="/login" /> 
        }} />
      </Switch>
    </Router>
  )
}
// _______******______
const mapStateToProps = (state) => {
  return {
    loginState: state.user.loginState
  }
}
// _______******______
export default connect(mapStateToProps)(App)

来自不同组件的行为需要变更同一个状态

import React, { Component } from 'react'
import { Divider, Space, Input, Button, message } from 'antd'
import { login } from './../../api/users'
import { connect } from 'react-redux'
class App extends Component {
  state = {
    adminname: '',
    password: ''
  }
  changeAdminname = (e) => this.setState({ adminname: e.target.value })
  changePassword = (e) => this.setState({ password: e.target.value })
  loginFn = () => {
    const params = {
      adminname: this.state.adminname,
      password: this.state.password
    }
    this.props.loginFn(params).then(() => {
      this.props.history.push('/')
    })
    /*login(params).then(res => {
      if (res.data.code === '10003') {
        message.warning('密码错误')
      } else if (res.data.code === '10005') {
        message.warning('账户不存在')
      } else {
        console.log(res.data.data)
        // 信息存入本地
        localStorage.setItem('adminToken', res.data.data.token)
        localStorage.setItem('adminname', res.data.data.adminname)
        localStorage.setItem('role', res.data.data.role)
        localStorage.setItem('loginState', true)
        // 跳转到首页
        this.props.history.push('/')
      }
    })*/
  }
  render() {
    return (
      <div className="loginContainer">
        <div className="loginBox">
          <div className="loginLogo">
            嗨购管理系统
          </div>
          <Divider>使用用户名登录</Divider>
          <Space direction="vertical" style={{ width: '100%'}}>
            <div style={{ width: '100%', height: 40, borderBottom: '1px solid #999'}}>
              <Input value = { this.state.adminname } onChange = { this.changeAdminname }
              size="large" bordered = { false } placeholder="请输入用户名"/>
            </div>
            <div style={{ width: '100%', height: 40, borderBottom: '1px solid #999'}}>
              <Input value = { this.state.password }  onChange = { this.changePassword }
              size="large" type="password" bordered = { false } placeholder="请输入密码"/>
            </div>
            <Button onClick={ this.loginFn } style={{ marginTop: 50}} type="primary" block size="large">登录</Button>
          </Space>
        </div>
      </div>
    )
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    loginFn (params) {
      // 解决完成组件需要后续的业务逻辑
      return new Promise((resolve, reject) => { 
        login(params).then(res => {
          if (res.data.code === '10003') {
            message.warning('密码错误')
          } else if (res.data.code === '10005') {
            message.warning('账户不存在')
          } else {
            console.log(res.data.data)
            // 信息存入本地
            localStorage.setItem('adminToken', res.data.data.token)
            localStorage.setItem('adminname', res.data.data.adminname)
            localStorage.setItem('role', res.data.data.role)
            localStorage.setItem('loginState', true)
            // 给状态管理器存储数据
            dispatch({type: 'CHANGE_ADMIN_TOKEN', payload: res.data.data.token})
            dispatch({type: 'CHANGE_ADMIN_NAME', payload: res.data.data.adminname})
            dispatch({type: 'CHANGE_ROLE', payload: res.data.data.role})
            dispatch({type: 'CHANGE_LOGIN_STATE', payload: true})
            // 跳转到首页
            resolve()
          }
        })
      })
    }
  }
}
export default  connect(null, mapDispatchToProps)(App)

34.使用redux-thunk解决异步问题

分reducer操作不变----- 创建分reducer

创建store时使用redux-thunk中间件

import { createStore, combineReducers, applyMiddleware } from 'redux'
import user from './modules/user'
import pro from './modules/pro'
import thunk from 'redux-thunk'  // ******
const reducer = combineReducers({
  user,
  pro
})
// ******
const store = createStore(reducer, applyMiddleware(thunk))
export default store

创建异步处理操作 store/actionCreator/user.js

import { message } from 'antd'
import { login } from './../../api/users'
const action = {
  // fn (params) { // 可以传递参数
  //   return (dispatch) => { // dispatch 是默认参数,可以触发改变reducer
  //  }
  // }
  // fn2(dispatch) { // 不可以传递参数
  // }
  loginActionFn (params) {
    return (dispatch) => {
      // 如果含有后续的操作,需要使用promise
      return new Promise(resolve => {
        login(params).then(res => {
          if (res.data.code === '10003') {
            message.warning('密码错误')
          } else if (res.data.code === '10005') {
            message.warning('账户不存在')
          } else {
            console.log(res.data.data)
            // 信息存入本地
            localStorage.setItem('adminToken', res.data.data.token)
            localStorage.setItem('adminname', res.data.data.adminname)
            localStorage.setItem('role', res.data.data.role)
            localStorage.setItem('loginState', true)
            // 给状态管理器存储数据
            dispatch({type: 'CHANGE_ADMIN_TOKEN', payload: res.data.data.token})
            dispatch({type: 'CHANGE_ADMIN_NAME', payload: res.data.data.adminname})
            dispatch({type: 'CHANGE_ROLE', payload: res.data.data.role})
            dispatch({type: 'CHANGE_LOGIN_STATE', payload: true})
            // 跳转到首页
            resolve()
          }
        })
      })
    }
  }
}
export default action

组件内触发aciton, layout/login/Index.jsx

​
import action from './../../store/acitonCreator/user'
class App extends Component {
  
}
const mapDispatchToProps = (dispatch) => {
  return {
    loginFn (params) {
      // dispatch(action.fn2)
      return dispatch(action.loginActionFn(params))
    }
  }
}
export default  connect(null, mapDispatchToProps)(App)

35.数据可视化

echarts

Apache ECharts

antv

https://antv.vision/zh

highCharts

d3

react + echarts

echarts-for-react - npm

cnpm i echarts echarts-for-react -S

创建 views/data/Bar.jsx + Line.jsx

配置路由

import React from 'react'
import { 
  HomeOutlined,
  PictureOutlined,
  NotificationOutlined,
  UsergroupDeleteOutlined,
  SwitcherOutlined
} from '@ant-design/icons'
import Home from './../views/home/Index'
const menus = [
  ...,
  {
    index: 5,
    title: '数据可视化',
    path: '/data',
    key: '0-5',
    icon: <SwitcherOutlined />,
    redirect:"/data/bar",
    children: [
      {
        title: '柱状图',
        path: '/data/bar',
        key: '0-5-0',
        component: React.lazy(() => import('./../views/data/Bar'))
      },
      {
        title: '折线图',
        path: '/data/line',
        key: '0-5-1',
        component: React.lazy(() => import('./../views/data/Line'))
      }
    ]
  }
]
export default menus
import React from 'react'
import ReactECharts from 'echarts-for-react'
function Bar() {
  const getOption = () => {
    return {
      title: {
        text: '测试',
        left: 'center'
      },
      legend: {
        data: ['1销量', '2销量'],
        bottom: 'bottom'
      },
      xAxis: {
          type: 'category',
          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
          type: 'value'
      },
      tooltip: {
        show: true
      },
      series: [
      {
          name: '2销量',
          data: [350, 230, 284, 218, 35, 147, 260],
          type: 'bar'
      }]
  }
  }
  return (
    <div>
      柱状图
      <ReactECharts  option={getOption()} />
    </div>
  )
}
​
export default Bar
import React, { Component } from 'react'
import ReactECharts from 'echarts-for-react'
export default class Line extends Component {
  getOption = () => {
    return {
      title: {
        text: '测试',
        left: 'center'
      },
      legend: {
        data: ['1销量', '2销量'],
        bottom: 'bottom'
      },
      xAxis: {
          type: 'category',
          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
          type: 'value'
      },
      tooltip: {
        show: true
      },
      series: [
      {
          name: '2销量',
          data: [350, 230, 284, 218, 35, 147, 260],
          type: 'line'
      }]
  };
  }
  render() {
    return (
      <div>
        折线图
        <ReactECharts  option={this.getOption()} />
      </div>
    )
  }
}

动态渲染数据

import React, { useState } from 'react'
import ReactECharts from 'echarts-for-react'
function Bar() {
  const [series, setSeries] = useState([
    {
        name: '2销量',
        data: [350, 230, 284, 218, 35, 147, 260],
        type: 'bar'
    }])
  const getOption = () => {
    return {
      title: {
        text: '测试',
        left: 'center'
      },
      legend: {
        data: ['1销量', '2销量'],
        bottom: 'bottom'
      },
      xAxis: {
          type: 'category',
          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
          type: 'value'
      },
      tooltip: {
        show: true
      },
      series: series
  }
  }
  return (
    <div>
      柱状图
      <button onClick={ () => {
        setSeries([{
          name: '2销量',
          data: [550, 430, 184, 118, 135, 547, 160],
          type: 'bar'
        }])
      }}>改变数据</button>
      <ReactECharts  option={getOption()} />
    </div>
  )
}
​
export default Bar
import React, { Component } from 'react'
import ReactECharts from 'echarts-for-react'
export default class Line extends Component {
  state = {
    series: [
      {
          name: '2销量',
          data: [350, 230, 284, 218, 35, 147, 260],
          type: 'line'
      }]
  }
  getOption = () => {
    return {
      title: {
        text: '测试',
        left: 'center'
      },
      legend: {
        data: ['1销量', '2销量'],
        bottom: 'bottom'
      },
      xAxis: {
          type: 'category',
          data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
      },
      yAxis: {
          type: 'value'
      },
      tooltip: {
        show: true
      },
      series: this.state.series
  };
  }
  render() {
    return (
      <div>
        折线图
        <button onClick = { () => {
          this.setState({
            series: [{
              name: '2销量',
              data: [550, 430, 184, 118, 135, 547, 160],
              type: 'line'
            }]
          })
        }}>改变数据</button>
        <ReactECharts  option={this.getOption()} />
      </div>
    )
  }
}
​

总结

本文详细介绍了在React项目中使用ECharts进行数据可视化的步骤。通过创建Bar.jsxLine.jsx两个组件,我们实现了柱状图和折线图的动态数据更新。同时,通过配置路由,我们实现了数据可视化页面的导航功能。这些步骤不仅帮助开发者快速上手ECharts,还为构建复杂的图表应用提供了参考。希望本文的内容对大家在实际开发中有所帮助。


原文地址:https://blog.csdn.net/2302_76329106/article/details/144018947

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!