2
头图

原文地址:使用Vue+DataV+Echarts打造新冠肺炎疫情数据大屏(可动态刷新)

源码

查看:https://github.com/lanweihong/data-visualization-with-covid-19

效果图

演示

仅适配 1080P 屏幕,使用浏览器访问后按 F11 进入全屏可看最佳显示效果。

  1. 疫情真实数据演示地址:演示地址-真实数据
  2. 模拟数据演示地址:演示地址-模拟数据

效果图

前端框架和类库

代码实现

创建项目

使用 Vue Cli 创建 Vue 项目,没有 Vue Cli 的使用以下命令安装:

npm install -g @vue/cli

创建项目:

vue create datav-covid-19

安装依赖

# 安装 DataV
npm install @jiaminghi/data-view

# 安装 echarts
npm install echarts -S

# 安装 element-ui
npm i element-ui -S

# 安装 vue-router
npm install vue-router

# 安装 mockjs
npm install mockjs --save-dev

# 安装 axios
npm install axios

# 安装 echarts-liquidfill
npm i echarts-liquidfill

引入注册

在项目中引入,编辑 main.js

import Vue from 'vue'
import App from './App.vue'
import dataV from '@jiaminghi/data-view'
import * as echarts from 'echarts'
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios'
// 引入 echarts 水球图
import 'echarts-liquidfill'
import VueRouter from 'vue-router'

import { 
  Icon, Row, Col,  Table, TableColumn, Button, Dialog, Link
} from 'element-ui';

// 注册 echarts
Vue.prototype.$echarts = echarts
Vue.config.productionTip = false

// 注册 axios
Vue.prototype.axios = axios

// 注册 dataV
Vue.use(dataV)

// 注册路由
Vue.use(VueRouter)

// 按需注册其他 element-ui 组件
Vue.use(Icon)
Vue.use(Row)
Vue.use(Col)
Vue.use(Table)
Vue.use(TableColumn)
Vue.use(Button)
Vue.use(Dialog)
Vue.use(Link)

new Vue({
  render: h => h(App),
}).$mount('#app')

编写组件

因为篇幅有限,为了阅读体验,这里以 累计排名 组件为例,其他的组件请看 Github 上的代码。

累计排名组件

效果图

累计排名组件效果图

累计排名组件采用 ECharts 的柱状图来显示,实现代码如下:

<template>
  <div
    ref="provinceRankingBarChart"
    style="width: 100%; height: 100%"
    />
</template>
<script>
import * as echarts from 'echarts'
let chart = null
export default {
  props: {
    data: {
      type: Object,
      default () {
        return {
          provinceList: [],
          valueList: []
        }
      }
    }
  },
  methods: {
    initChart () {
      if (null != chart && undefined != chart) {
        chart.dispose()
      }
      chart = this.$echarts.init(this.$refs.provinceRankingBarChart)
      this.setOptions()
    },
    setOptions() {
      var salvProValue = this.data.valueList;
      var salvProMax = [];
      for (let i = 0; i < salvProValue.length; i++) {
        salvProMax.push(salvProValue[0])
      }
      let option = {
        grid: {
          left: '2%',
          right: '2%',
          bottom: '2%',
          top: '2%',
          containLabel: true
        },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'none'
          },
          formatter: function (params) {
            return params[0].name + ' : ' + params[0].value
          }
        },
        xAxis: {
          show: false,
          type: 'value'
        },
        yAxis: [{
          type: 'category',
          inverse: true,
          axisLabel: {
            show: true,
            textStyle: {
              color: '#fff'
            },
          },
          splitLine: {
            show: false
          },
          axisTick: {
            show: false
          },
          axisLine: {
            show: false
          },
          data: this.data.provinceList
        }, {
          type: 'category',
          inverse: true,
          axisTick: 'none',
          axisLine: 'none',
          show: true,
          axisLabel: {
            textStyle: {
              color: '#ffffff',
              fontSize: '12'
            },
          },
          data: salvProValue
        }],
        series: [{
          name: '值',
          type: 'bar',
          zlevel: 1,
          itemStyle: {
            normal: {
              barBorderRadius: 30,
              color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{
                offset: 0,
                color: 'rgb(2,163,254,1)'
              }, {
                offset: 1,
                color: 'rgb(125,64,255,1)'
              }]),
            },
          },
          barWidth: 20,
          data: salvProValue
        },
        {
          name: '背景',
          type: 'bar',
          barWidth: 20,
          barGap: '-100%',
          data: salvProMax,
          itemStyle: {
            normal: {
              color: 'rgba(24,31,68,1)',
              barBorderRadius: 30,
            }
          },
        },
        ]
      }
      chart.setOption(option)
    }
  },
  watch: {
    data: {
      handler(newList, oldList) {
        if (oldList != newList) {
          this.setOptions()
        }
      },
      deep: true
    }
  }
}
</script>

在页面中引入使用:

<template>
  <div class="demo">
    <province-ranking-bar-chart
      ref="rankChart"
      :data="dataList"
      style="width: 100%; height: 380px"
    />
  </div>
</template>
<script>
// 引入组件
import ProvinceRankingBarChart from '../components/ProvinceRankingBarChart'
export default {
  components: {
    ProvinceRankingBarChart
  },
  data () {
    return {
      // 定义数据
      dataList: {
        provinceList: ['湖北', '台湾'],
        valueList: [68188, 15379]
      }
    }
  },
  mounted() {
    // 创建图表并初始化
    this.$refs.rankChart.initChart()
  }
}
</script>
<style>
.demo {
  width: 500px;
  height: 600px;
}
</style>

其他组件的代码就不在这里写了,完整代码已上传 Github ,需要的可以去查看。

完整的组件结构如下:

完整的组件结

准备模拟数据

项目中提供了两种数据提供方式,一是请求真实后台地址,返回的数据格式参考 data 目录下的 json 文件;二是在本地使用 Mock 生成模拟数据。这里仅介绍使用 Mock 生成模拟数据方式。

使用 Mock 生成模拟数据

在项目根目录下创建文件夹 mock,分别创建 covid19.jsindex.js

编写 mock 服务

covid19.js 代码如下,代码中使用到一些 Mock 的语法,具体使用方法请查看 Mock 的文档。

// 从本地读取 json 数据
const provinceData = require('../data/covid19-province.json')
const dailyData = require('../data/covid19-daily-list.json')
// 引入 mockjs
const Mock = require('mockjs')
// 使用 mockjs 的 Random 生成随机数据
const Random = Mock.Random

module.exports = [
  {
    url: '/api/covid-19/overall',
    type: 'get',
    response: config => {
      return {
        success: true,
        code: 200,
        message: "操作成功",
        data: {
          confirmedCount: Random.integer(110000, 120000),
          confirmedIncr: 72,
          curedCount: Random.integer(100000, 110000),
          curedIncr: 173,
          currentConfirmedCount: Random.integer(3000, 4000),
          currentConfirmedIncr: -110,
          deadCount: Random.integer(4000, 6000),
          deadIncr: 12,
          importedCount: Random.integer(6000, 8000),
          importedIncr: 23,
          noInFectCount: Random.integer(400, 600),
          noInFectIncr: 8,
          suspectIncr: 0,
          suspectCount: 2,
          updateTime: "2021-07-15 20:39:11",
          curedRate: Random.float(90, 95, 0, 9),
          deadRate: Random.float(1, 5, 0, 9)
        }
      }
    }
  },
  {
    url: '/api/covid-19/area/latest/list',
    type: 'get',
    response: config => {
      return provinceData
    }
  },
  {
    url: '/api/covid-19/list',
    type: 'get',
    response: config => {
      return dailyData
    }
  }
]
注册 mock 服务

编辑 index.js,这里主要是注册 mock 服务,调用方法 initMockData() 完成注册;

const Mock = require('mockjs')
// 引入写好的 mock 服务
const covid19 = require('./covid19')

const mocks = [
  ...covid19
]

function param2Obj(url) {
  const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
  if (!search) {
    return {}
  }
  const obj = {}
  const searchArr = search.split('&')
  searchArr.forEach(v => {
    const index = v.indexOf('=')
    if (index !== -1) {
      const name = v.substring(0, index)
      const val = v.substring(index + 1, v.length)
      obj[name] = val
    }
  })
  return obj
}

const initMockData = () => {

  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
  Mock.XHR.prototype.send = function() {
    if (this.custom.xhr) {
      this.custom.xhr.withCredentials = this.withCredentials || false

      if (this.responseType) {
        this.custom.xhr.responseType = this.responseType
      }
    }
    this.proxy_send(...arguments)
  }

  function XHR2ExpressReqWrap(respond) {
    return function(options) {
      let result = null
      if (respond instanceof Function) {
        const { body, type, url } = options
        result = respond({
          method: type,
          body: JSON.parse(body),
          query: param2Obj(url)
        })
      } else {
        result = respond
      }
      return Mock.mock(result)
    }
  }

  for (const i of mocks) {
    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
  }
}

module.exports = {
  mocks,
  initMockData
}
使用 mock 服务

main.js 中引入:

const { initMockData } = require('../mock')
// 完成注册
initMockData()

然后在页面中使用 request.get('/api/covid-19/list') 就能请求获取到数据,这里的 request.get() 是我用 axios 封装写的方法。

封装数据接口

封装 axios

项目中的数据请求都是使用 axios 为方便使用,我简单封装了一个工具类 request.js

import axios from "axios"
import sysConst from '../libs/const'
const fetch = (method = 'GET', url, param = '') => {
  // 处理 url
  url = `${sysConst.baseUrl}${url}`
  return new Promise((resolve, reject) => {
    axios({
      method: method,
      url: url,
      changeOrigin: true,
      data: JSON.stringify(param)
    }).then((res) => {
      resolve(res.data)
    }, error => {
      reject(error)
    }).catch((error) => {
      reject(error)
    })
  })
}

const get = (url) => {
  return fetch('GET', url)
}

const post = (url, data) => {
  return fetch('POST', url, data)
}

const put = (url, data) => {
  return fetch('PUT', url, data)
}

const remove = (url, data) => {
  return fetch('DELETE', url, data)
}

export {
  get,
  post,
  put,
  remove
}

这里引入的 const.js 代码如下:

let baseUrl = ''
if (process.env.NODE_ENV === 'development') {
  // 修改你的 API 地址
  baseUrl = ''
} else {
  // 你的 API 地址
  baseUrl = ''
}

export default {
  baseUrl
}

封装数据接口

在项目根目录下新建文件夹 api ,用于保存编写数据接口,在该目录下新增文件 covid19.js,用于封装请求获取数据:

import * as request from '@/utils/request'

/**
 * 接口封装
 */
export default {
  getOverall() {
    let url = `/api/covid-19/overall?_=${Math.random()}`
    return request.get(url)
  },
  getProvinceDataList() {
    let url = `/api/covid-19/area/latest/list?_=${Math.random()}`
    return request.get(url)
  },
  getDailyList() {
    let url = `/api/covid-19/list?t=${Math.random()}`
    return request.get(url)
  }
}

调用数据接口获取数据并更新图表展示

// 引入
import covid19Service from '../api/covid19'

// 使用
let self = this
covid19Service.getOverall().then((res) => {
    if (!res.success) {
        console.log('错误:' + res.info)
        return
    }
    // 修改数据,图表组件检测到数据变化会触发 setOptions() 方法更新显示( setOptions() 在图表组件中已定义好)
    self.basicData = res.data
})

项目结构

完整的项目结构如下:

├─build
├─data                                   # 本地模拟数据目录
├─mock                                   # mock 配置
├─public
└─src
    ├─api                                # 接口封装目录
    ├─assets
    ├─components                         # 组件目录
    │  ├─About                           # 关于
    │  ├─BasicDataItemLabel              # 基本数据显示标签
    │  ├─BasicProportionChart            # 占比图表
    │  ├─BasicTrendChart                 # 趋势图表
    │  ├─ChartCard                       # 图表面板
    │  ├─CuredAndDeadRateChart           # 治愈率和死亡率图表
    │  ├─CurrentConfirmedCompareBarChart # 最近一周累计治愈图表
    │  ├─DataMap                         # 数据地图
    │  └─ProvinceRankingBarChart         # 累计排名图表
    ├─libs                               # 一些常用的配置
    ├─router                             # 路由配置
    ├─utils                              # 工具类
    └─views                              # 视图

详细结构:

完整的项目结构

总结

  1. 采用组件化封装各个展示图表,能更好的图表展示及复用;
  2. 使用 axios 请求后台服务或本地 mock 服务获取数据,然后重新赋值图表中指定的数据;

项目源码:本项目源码已上传至 Github,在我的博客中可查看到地址:使用Vue+DataV+Echarts打造新冠肺炎疫情数据大屏(可动态刷新)

这个项目是个人学习作品,能力有限,难免会有 BUG 和错误,敬请谅解。如有更好的建议或想法,请指出,谢谢


lanweihong
255 声望5 粉丝