Flask+Echarts搭建全国疫情可视化大屏

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: Flask+Echarts搭建全国疫情可视化大屏

本项目是基于flask+echarts搭建的全国疫情实时追踪的可视化大屏,主要涉及到的技术有爬虫,mysql数据库,flask框架,echarts图表。关于flask知识点,可学习另一篇文章Flask全套知识点从入门到精通,学完可直接做项目


最终效果如下:




需求分析

从最终效果图可以看出,我们将屏幕分为4大板块(页面排布是左中右+上),第一板块是最上面的部分,包括大屏标题以及当前的实时时间;第二板块是最左边,上面的全国新增趋势折线图(新增确诊、治愈、死亡),下面是全国累计趋势折线图(累计确诊、治愈、死亡);第三板块是中间,上面是当天的一些数据,下面是全国累计确诊的疫情地图;第四板块是右边,上面是新增确诊人数Top前五的省份柱状图,下面是微博热搜话题的词云图。


开发工具:


vscode编辑器


python3.8


如果你在开发的过程中发现你编写的html或css文件在页面中没有更新,而是你上次编写的代码,也就是缓存的问题,这时候你需要在app.py中添加如下代码即可解决:

@app.after_request
def apply_caching(response):
    response.headers["Cache-Control"] = "no-cache"
    return response

项目实施

项目的最终文件目录结构如下:



1.数据采集

首先,我们得要有数据才能进行展示,这里我们选择用爬虫来进行数据采集并保存到mysql数据库中,考虑到平台限制,这里就不方便展示爬虫代码,需要的评论留言或私信。


2.搭建flask应用

这里我们先搭建一个基础的flask应用

from flask import Flask,render_template
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
@app.route('/')
def index():
    return render_template('main.html')
if __name__ == '__main__':
    app.run(debug=True)

接着,需要编写main.html页面(这里我就直接放最终的代码)


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>全国疫情实时追踪</title>
    <link rel="stylesheet" href="../static/css/main.css">
    <script src="../static/js/echarts.min.js"></script>
    <script src="../static/js/china.js"></script>
    <script src="../static/js/jquery-3.6.0.min.js"></script>
    <script src="../static/js/echarts-wordcloud.min.js"></script>
</head>
<body>
  <div class="title">全国疫情实时追踪</div>
  <div class="tim"></div>
  <div class="l1" id="l1"></div>
  <div class="l2" id="l2"></div>
  <div class="c1">
      <div class="num"><h1></h1></div>
      <div class="num"><h1></h1></div>
      <div class="num"><h1></h1></div>
      <div class="num"><h1></h1></div>
      <div class="txt"><h2>累计确诊</h2></div>
      <div class="txt"><h2>新增确诊</h2></div>
      <div class="txt"><h2>累计治愈</h2></div>
      <div class="txt"><h2>累计死亡</h2></div>
    </div>
    <div class="c2" id="main" ></div>
    <div class="r1" id="r1"></div>
    <div class="r2" id="r2"></div>
   <script src="../static/js/get_data.js"></script>
   <script src="../static/js/ec_center.js"></script>
   <script src="../static/js/ec_left1.js"></script>
   <script src="../static/js/ec_left2.js"></script>
   <script src="../static/js/ec_right1.js"></script>
   <script src="../static/js/ec_right2.js"></script>
</body>
</html>


其次,我们还需要编写css来进行板块划分


body{
    margin: 0;
    background-color: #333;
}
.title{
    position: absolute;
    width: 40%;
    height: 10%;
    top: 0;
    left: 30%;
    color: white;
    font-size: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.l1{
    position: absolute;
    width: 30%;
    height: 45%;
    top: 10%;
    left: 0;
    background-color: aquamarine;
}
.l2{
    position: absolute;
    width: 30%;
    height: 45%;
    top: 55%;
    left: 0;
    background-color: blue;
}
.c1{
    position: absolute;
    width: 40%;
    height: 25%;
    top: 10%;
    left: 30%;
    /* background-color: blue; */
}
.num{
    width: 25%;
    float: left;
    display: flex;
    align-items: center;
    justify-content: center;
    color: gold;
    font-size: 16px;
}
.txt{
    width: 25%;
    float: left;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: "幼圆";
    color: whitesmoke;
    font-size: 14px;
}
.c2{
    position: absolute;
    width: 40%;
    height: 65%;
    top: 35%;
    left: 30%;
    /* background-color: whitesmoke; */
}
.r1{
    position: absolute;
    width: 30%;
    height: 45%;
    top: 10%;
    right: 0;
    background-color: burlywood;
}
.r2{
    position: absolute;
    width: 30%;
    height: 45%;
    top: 55%;
    right: 0;
    background-color: brown;
}
.tim{
    position: absolute;
    /* width: 30%; */
    height: 10%;
    top: 5%;
    right: 2%;
    /* background-color: blueviolet; */
    font-size: 20px;
    color: whitesmoke;
}


3.可视化展示

接下来我将按照4大板块进行介绍


第一板块

那个大屏标题文字在上面的html页面中有,这里就不说了。还有一个就是右上角的时间显示,这里我们需要编写一个获取时间的接口,然后通过ajax来发送请求进行调用.


utils.py


import time
import pymysql
import collections
import jieba
import re
def get_time():
    time_str = time.strftime('%Y{}%m{}%d{} %X ')
    return time_str.format('年','月','日')

app.py

import utils
@app.route('/time')
def time():
    return utils.get_time()

get_data.js


function gettime(){
    $.ajax({
        url:'/time',
        timeout:10000,//超时时间
        success:function(data){
            $('.tim').html(data)
        },
        error:function(data){
        }
    });
};

第二板块

第二板块是左边的两个图


流程步骤就是先从数据库中获取数据,在flask应用中编写接口,最后在页面中通过ajax来发送进行调用,这是图表展示的通用步骤,下面我就不再叙述了。


这里因为我们要多次从数据库获取数据,所以 我们先封装一下方法,便于后面获取数据


utils.py


def get_conn():
    """
    :return: 连接,游标
    """
    # 创建连接
    conn = pymysql.connect(host="127.0.0.1",
                           user="xxx", # 这里写你的mysql用户名
                           password="xxx", # 这里写你的mysql密码
                           db="yiqing",  # 这里写你的mysql中创建的数据库
                           charset="utf8")
    # 创建游标
    cursor = conn.cursor()# 执行完毕返回的结果集默认以元组显示
    return conn, cursor
def close_conn(conn, cursor):
    cursor.close()
    conn.close()
def query(sql,*args):
    """
    封装通用查询
    :param sql:
    :param args:
    :return: 返回查询到的结果,((),(),)的形式
    """
    conn, cursor = get_conn()
    cursor.execute(sql,args)
    res = cursor.fetchall()
    close_conn(conn, cursor)
    return res


左上的图:


utils.py

def get_l1_data():
    # 因为会更新多次数据,取时间戳最新的那组数据
  sql = '''
    SELECT ds,confirm_add,heal_add,dead_add
    FROM history
    '''
  res = query(sql)
  return res

app.py


from flask import jsonify
@app.route('/l1')
def get_l1_data():
    data = utils.get_l1_data()
    day,confirm_add,heal_add,dead_add = [],[],[],[]
    for item in data:
        day.append(item[0].strftime('%m-%d'))
        confirm_add.append(item[1])
        heal_add.append(item[2])
        dead_add.append(item[3])
    return jsonify({'day':day,'confirm_add':confirm_add,'heal_add':heal_add,'dead_add':dead_add})

get_data.js

function get_l1_data(){
    $.ajax({
        url:'/l1',
        success:function(data){
            ec_left1_Option.xAxis[0].data=data.day
            ec_left1_Option.series[0].data=data.confirm_add
            ec_left1_Option.series[1].data=data.heal_add
            ec_left1_Option.series[2].data=data.dead_add
            ec_left1.setOption(ec_left1_Option)
        },
        error:function(data){
        }
    });
}

ec_left1.js


var ec_left1 = echarts.init(document.getElementById('l1'), "dark");
var ec_left1_Option = {
  tooltip: {
    trigger: 'axis',
    //指示器
    axisPointer: {
      type: 'line',
      lineStyle: {
        color: '#7171C6'
      }
    },
  },
  legend: {
    data: ['新增确诊', '新增治愈','新增死亡'],
    left: "right"
  },
  //标题样式
  title: {
    text: "全国新增趋势",
    textStyle: {
      color: 'white',
    },
    left: 'left'
  },
  //图形位置
  grid: {
    left: '4%',
    right: '6%',
    bottom: '4%',
    top: 50,
    containLabel: true
  },
  xAxis: [{
    type: 'category',
    data: []
  }],
  yAxis: [{
    type: 'value',
    //y轴线设置显示
    axisLine: {
      show: true
    },
    position:'left',
    axisLabel: {
      show: true,
      color: 'white',
      fontSize: 12,
      formatter: function(value) {
        if (value >= 1000) {
          value = value / 1000 + 'k';
        }
        return value;
      }
    },
    //与x轴平行的线样式
    splitLine: {
      show: true,
      lineStyle: {
        width: 1,
      }
    }
  },
  {
    type: 'value',
    //y轴线设置显示
    axisLine: {
      show: true
    },
    position:'right',
    axisLabel: {
      show: true,
      color: 'white',
      fontSize: 12,
      formatter: function(value) {
        return value;
      }
    },
    //与x轴平行的线样式
    splitLine: {
      show: true,
      lineStyle: {
        width: 1,
      }
    }
  } 
],
  series: [{
    name: "新增确诊",
    type: 'line',
    smooth: true,
    yAxisIndex:0,
    data: []
  }, {
    name: "新增治愈",
    type: 'line',
    smooth: true,
    yAxisIndex:1,
    data: []
  },{
    name: "新增死亡",
    type: 'line',
    smooth: true,
    yAxisIndex:1,
    data: []
  }
]
};
ec_left1.setOption(ec_left1_Option)


左下:


utils.py

def get_l2_data():
  sql = '''
    SELECT ds,confirm,heal,dead
    FROM history;
    '''
  res = query(sql)
  return res

app.py


@app.route('/l2')
def get_l2_data():
    data = utils.get_l2_data()
    day,confirm,heal,dead = [],[],[],[]
    for item in data:
        day.append(item[0].strftime('%m-%d'))
        confirm.append(item[1])
        heal.append(item[2])
        dead.append(item[3])
    return jsonify({'day':day,'confirm':confirm,'heal':heal,'dead':dead})

get_data.js

function get_l2_data(){
    $.ajax({
        url:'/l2',
        success:function(data){
            ec_left2_Option.xAxis[0].data=data.day
            ec_left2_Option.series[0].data=data.confirm
            ec_left2_Option.series[1].data=data.heal
            ec_left2_Option.series[2].data=data.dead
            ec_left2.setOption(ec_left2_Option)
        },
        error:function(data){
        }
    });
}

ec_left2.js

var ec_left2 = echarts.init(document.getElementById('l2'), "dark");
var ec_left2_Option = {
  tooltip: {
    trigger: 'axis',
    //指示器
    axisPointer: {
      type: 'line',
      lineStyle: {
        color: '#7171C6'
      }
    },
  },
  legend: {
    data: ['累计确诊', '累计治愈','累计死亡'],
    left: "right"
  },
  //标题样式
  title: {
    text: "全国累计趋势",
    textStyle: {
      color: 'white',
    },
    left: 'left'
  },
  //图形位置
  grid: {
    left: '4%',
    right: '6%',
    bottom: '4%',
    top: 50,
    containLabel: true
  },
  xAxis: [{
    type: 'category',
    data: []
  }],
  yAxis: [{
    type: 'value',
    //y轴字体设置
    //y轴线设置显示
    axisLine: {
      show: true
    },
    axisLabel: {
      show: true,
      color: 'white',
      fontSize: 12,
      formatter: function(value) {
        if (value >= 1000) {
          value = value / 1000 + 'k';
        }
        return value;
      }
    },
    //与x轴平行的线样式
    splitLine: {
      show: true,
      lineStyle: {
        // color: '#FFF',
        width: 1,
        // type: 'solid',
      }
    }
  },
  {
    type: 'value',
    //y轴线设置显示
    axisLine: {
      show: true
    },
    position:'right',
    axisLabel: {
      show: true,
      color: 'white',
      fontSize: 12,
      formatter: function(value) {
        if (value >= 1000) {
          value = value / 1000 + 'k';
        }
        return value;
      }
    },
    //与x轴平行的线样式
    splitLine: {
      show: true,
      lineStyle: {
        width: 1,
      }
    }
  } ],
  series: [{
    name: "累计确诊",
    type: 'line',
    smooth: true,
    yAxisIndex:0,
    data: []
  }, {
    name: "累计治愈",
    type: 'line',
    smooth: true,
    yAxisIndex:1,
    data: []
  },{
    name: "累计死亡",
    type: 'line',
    smooth: true,
    yAxisIndex:1,
    data: []
  }
]
};
ec_left2.setOption(ec_left2_Option)


第三板块

第三板块是中间部分


先完成上面的数值数据填充


utils.py

def get_c1_data():
    """
    :return: 返回大屏div id=c1 的数据
    """
    # 因为会更新多次数据,取时间戳最新的那组数据
    sql = """
    SELECT confirm,confirm_add,heal,dead
    FROM history
    ORDER BY ds DESC LIMIT 1;
    """
    res = query(sql)
    return res[0]

app.py

@app.route('/c1')
def get_c1_data():
    data = utils.get_c1_data()
    return jsonify({'confirm':int(data[0]),'confirm_add':int(data[1]),'heal':int(data[2]),'dead':int(data[3])})

get_data.js

function get_c1_data(){
    $.ajax({
        url:'/c1',
        success:function(data){
            $(".num h1").eq(0).text(data.confirm)
            $(".num h1").eq(1).text(data.confirm_add)
            $(".num h1").eq(2).text(data.heal)
            $(".num h1").eq(3).text(data.dead)
        },
        error:function(data){
        }
    });
}

接着完成下面的地图


utils.py

def get_c2_data():
    """
    :return:  返回各省数据
    """
    # 因为会更新多次数据,取时间戳最新的那组数据
    sql = '''
    SELECT province,sum(confirm_now)
    FROM details
    GROUP BY province;
    '''
    res = query(sql)
    return res

app.py


@app.route('/c2')
def get_c2_data():
    res = []
    for item in utils.get_c2_data():
        res.append({'name':item[0],'value':int(item[1])})
    return jsonify({'data':res})

get_data.py


function get_c2_data(){
    $.ajax({
        url:'/c2',
        success:function(data){
            ec_center_option.series[0].data=data.data
            ec_center_option.series[0].data.push({
                name:"南海诸岛",value:0
            })
            ec_center.setOption(ec_center_option)
        },
        error:function(data){
        }
    });
}

ec_center.js


const ec_center_option = {
    tooltip: {
        trigger: 'item',
        formatter: '名称:{a}<br/>省份:{b}<br/>确诊人数:{c}'
    },
    //左侧小导航图标
    visualMap: {
        show: true,
        x: 'left',
        y: 'bottom',
        textStyle: {
            fontSize: 8,
            color:['#FFFFFF']
        },
        splitList: [{ start: 0,end: 9 },
            {start: 10, end: 99 }, 
      { start: 100, end: 999 },
            {  start: 1000, end: 9999 },
            { start: 10000 }],
        color: ['#8A3310', '#C64918', '#E55B25', '#F2AD92', '#F9DCD1']
    },
    series: [
        {
            name: '数据',
            type: 'map',
            mapType: 'china',
            roam: false,
            itemStyle: {
                normal: {
                    borderWidth: .5, //区域边框宽度
                    borderColor: '#62d3ff', //区域边框颜色
                    areaColor: "#b7ffe6", //区域颜色
                    label: { show: true }
                },
                emphasis: { //鼠标滑过地图高亮的相关设置
                    borderWidth: .5,
                    borderColor: '#fff',
                    areaColor: "#fff",
                    label: { show: true }
                }},
            data: [] // data_list
        }
    ]
};
ec_center = echarts.init(document.getElementById('main'));
ec_center.setOption(ec_center_option)


到这里第三板块就完成了!


第四板块

首先完成上面的柱状图


utils.py


def get_r1_data():
    """
    :return:  返回新增确诊人数前5名的省份
    """
    sql = '''
            SELECT province,confirm FROM 
            (select province ,sum(confirm_add) as confirm from details
            where update_time=(select update_time from details
            order by update_time desc limit 1)
            group by province) as a 
            ORDER BY confirm DESC LIMIT 7;
            '''
    res = query(sql)
    return res

app.py


@app.route('/r1')
def get_r1_data():
    name,value = [],[]
    for n,v in utils.get_r1_data()[2:]:
        name.append(n)
        value.append(int(v))
    return jsonify({'name':name,'value':value})

get_data.js

function get_r1_data(){
    $.ajax({
        url:'/r1',
        success:function(data){
            ec_right1_option.xAxis.data=data.name
            ec_right1_option.series[0].data=data.value
            ec_right1.setOption(ec_right1_option)
        }
    });
}

ec_right1.js


var ec_right1 = echarts.init(document.getElementById('r1'),"dark");
var ec_right1_option = {
  //标题样式
  title : {
      text : "新增确诊人数TOP5",
      textStyle : {
          color : 'white',
      },
      left : 'left'
  },
    color: ['#3398DB'],
      tooltip: {
          trigger: 'axis',
          axisPointer: {            // 坐标轴指示器,坐标轴触发有效
              type: 'shadow'        // 默认为直线,可选为:'line' | 'shadow'
          }
      },
    xAxis: {
        type: 'category',
    color : 'white',
        data: []
    },
    yAxis: {
        type: 'value',
     color : 'white',
    },
    series: [{
        data: [],
        type: 'bar',
    barMaxWidth:"50%"
    }]
};
ec_right1.setOption(ec_right1_option)


接着完成下面的词云图


utils.py


def get_r2_data():
    """
    :return:  返回最近的20条热搜
    """
    sql = 'select title from focu_news order by news_time desc limit 20'
    res = query(sql)
    all_word = ''
    for item in res:
        all_word += item[0]
    new_data = re.findall('[\u4e00-\u9fa5]+', all_word, re.S)
    new_data = "/".join(new_data)
    seg_list_exact = jieba.cut(new_data, cut_all=True)
    result_list = []
    with open('停用词库.txt', encoding='utf-8') as f: 
        con = f.readlines()
        stop_words = set()
        for i in con:
            i = i.replace("\n", "")   # 去掉读取每一行数据的\n
            stop_words.add(i)
    for word in seg_list_exact:
        if word not in stop_words and len(word) > 1:
            result_list.append(word)
    word_counts = collections.Counter(result_list)
    # 词频统计:获取前80最高频的词
    word_counts_top = word_counts.most_common(80)
    return word_counts_top


app.py


@app.route('/r2')
def get_r2_data():
    data = utils.get_r2_data()
    d = []
    for i in data:
        k = i[0]
        v = int(i[1])
        d.append({"name": k, "value": v})
    return jsonify({"kws": d}) 

get_data.py


function get_r2_data() {
    $.ajax({
        url: "/r2",
        success: function (data) {
            ec_right2_option.series[0].data=data.kws;
            ec_right2.setOption(ec_right2_option);
        }
    })
}

ec_right2.js

var ec_right2 = echarts.init(document.getElementById('r2'), "dark");
var ec_right2_option = {
    // backgroundColor: '#515151',
    title: {
        text: "微博热搜话题",
        textStyle: {
            color: 'white',
        },
        left: 'left'
    },
    tooltip: {
        show: false
    },
    series: [{
        type: 'wordCloud',
        // drawOutOfBound:true,
        gridSize: 1,
        sizeRange: [12, 55],
        rotationRange: [-45, 0, 45, 90],
        // maskImage: maskImage,
        textStyle: {
            normal: {
                color: function () {
                    return 'rgb(' +
                        Math.round(Math.random() * 255) +
                        ', ' + Math.round(Math.random() * 255) +
                        ', ' + Math.round(Math.random() * 255) + ')'
                }
            }
        },
        // left: 'center',
        // top: 'center',
        // // width: '96%',
        // // height: '100%',
        right: null,
        bottom: null,
        // width: 300,
        // height: 200,
        // top: 20,
        data: []
    }]
}
ec_right2.setOption(ec_right2_option);


到这里,全部的页面及渲染就编写完成!


4.添加定时任务

这里忘记说了,前面每次在get_data.js中编写的函数,最后要调用才能使用

get_c1_data()
get_c2_data()
get_l1_data()
get_l2_data()
get_r1_data()
get_r2_data()

这里我们还需要给定义发送请求的ajax函数设置定时,也就是在get_data.js里面添加

setInterval(gettime,1000) # 时间是1s获取一次
setInterval(get_c1_data,1000*60*60) # 1小时发送一次请求
setInterval(get_c2_data,1000*60*60*6)
setInterval(get_l1_data,1000*60*60*12)
setInterval(get_l2_data,1000*60*60*12)
setInterval(get_r1_data,1000*60*60*6)
setInterval(get_r2_data,1000*10)

项目总结

本项目适合flask初学者来进行练手,当然前提也要会一些前端的知识,关于Echarts的使用可以去官网进行学习。关于页面的布局,可自由发挥来进行设计,或者在此基础上来进行创新,开发新的功能。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3月前
|
JavaScript
echarts根据上级元素的大小自动更新echarts(element-resize-detector)配合防抖解决大屏页面卡顿的问题
如何在Vue中使用`element-resize-detector`库来自动更新ECharts图表的大小,以适应其上级元素大小的变化,并结合使用防抖技术来解决大屏页面上的卡顿问题。
153 5
|
4月前
|
数据可视化 前端开发 JavaScript
Echarts英雄联盟可视化大屏(记得收藏)
Echarts英雄联盟可视化大屏(记得收藏)
|
4月前
|
数据可视化 前端开发 JavaScript
Echarts+JS实现农业指挥舱可视化大屏!!附源码!!
Echarts+JS实现农业指挥舱可视化大屏!!附源码!!
|
4月前
|
数据可视化 前端开发 JavaScript
【Echarts大屏】智慧医疗可视化大屏(新手必用)
【Echarts大屏】智慧医疗可视化大屏(新手必用)
|
4月前
|
数据可视化 前端开发 JavaScript
Echarts+JS实现数据分析可视化大屏!!附源码!!
Echarts+JS实现数据分析可视化大屏!!附源码!!
|
4月前
|
数据可视化
【Echarts大屏】智慧门店可视化大屏
【Echarts大屏】智慧门店可视化大屏
|
4月前
|
数据可视化 前端开发 JavaScript
【Echarts大屏】茶山指挥舱可视化大屏(记得收藏)
【Echarts大屏】茶山指挥舱可视化大屏(记得收藏)
|
4月前
|
数据可视化 前端开发 JavaScript
【Echarts大屏】大客户银行可视化大屏(附源码一键复制)
【Echarts大屏】大客户银行可视化大屏(附源码一键复制)
|
4月前
|
数据可视化 前端开发 JavaScript
【Echarts大屏】智慧图书馆可视化大屏(附源码一键复制)
【Echarts大屏】智慧图书馆可视化大屏(附源码一键复制)
|
4月前
|
数据可视化 前端开发 JavaScript
【Echarts大屏】共享单车运营平台|可视化大屏
【Echarts大屏】共享单车运营平台|可视化大屏