阅读 2419

全新Echarts电商平台数据可视化大屏全栈实战项目分享(附源码)

1. 前言

五一假期重学了新版Echarts,一个基于JavaScript的开源可视化图表库,收集参考了很多网上资料,最终选择电商平台作为练手项目。此篇涉及技术知识点有:Vue全家桶、WebSocket前后端数据推送、后端框架Koa2、Echarts新版图表组件(折线图、柱状图、饼图、地图、散点图),还支持主题切换, 展示酷炫的图表效果,同时也能够支持大屏和小屏的切换,保证了图表在不同屏幕上呈现的效果。

2. 入门篇(新手指南)

ECharts数据可视化快速入门

3. 实战篇(上下两部)

Vue+Echarts构建可视化大数据平台实战项目分享

4. 进阶篇

4.1 前后端分离

前端项目采用的技术栈是基于Vue + Echarts,用vue-cli构建前端界面,后端项目采用的技术栈是基于Node.js + Koa2 + WebSocket,用Koa2搭建的后端服务器。

在线演示DEMO地址👉:http://106.55.168.13:8999/

附上详细的思维导图如下:

1.png

分享之前,我们先来了解一下新版 Echarts 5.x,都有哪些变化,如下图:

echarts-5.png

4.2 后端部分

4.2.1 Koa2的介绍

  • 基于 Node.js 平台的Web服务器框架
  • 由 Express 原班人马打造,Express、Koa、Koa2 都是 Web 服务器的框架,他们之间的区别如下图:

2.png

  • 环境依赖 Node v7.6.0 及以上

由于 Koa2 它是支持 async 和 await ,所以它对 Node 的版本是有要求的,它要求 Node 的版本至少是在7.6级以上,因为语法糖 async和await 是在 Node7.6 版本之后出现才支持

  • 洋葱模型的中间件

如下图所示, 对于服务器而言,它其实就是来处理一个又一个的请求, Web 服务器接收由浏览器发 过来的一个又一个请求之后,它形成一个又一个的响应返回给浏览器. 而请求到达我们的服务器是 需要经过程序处理的,程序处理完之后才会形成响应,返回给浏览器,我们服务器处理请求的这一 块程序,在 Koa2 的世界当中就把它称之为中间件

3.png

这种中间件可能还不仅仅只有一个,可能会存在多个,比如上图所示, 它就存在三层中间件,这三 层中间件在处理请求的过程以及它调用的顺序为:

  • 当一个请求到达咱们的服务器,最先最先处理这个请求的是第一层中间件
  • 第一层的中间件在处理这个请求之后,它会把这个请求给第二层的中间件
  • 第二层的中间件在处理这个请求之后,它会把这个请求给第三层的中间件
  • 第三层中间件内部并没有中间件了, 所以第三层中间件在处理完所有的代码之后,这个请求又会到了第二层的中间件,所以第二层中间件对这个请求经过了两次的处处理
  • 第二层的中间件在处理完这个请求之后,又到了第一层的中间件, 所以第一层的中间件也对这个请求经过了两次的处理

这个调用顺序就是洋葱模型, 中间件对请求的处理有一种先进后出的感觉,请求最先到达第一层中 间件,而最后也是第一层中间件对请求再次处理了一下

4.2.2 Koa2的快速上手

4.2.2.1 检查node版本,Koa2的使用要求node版本在7.6以上

node -v
复制代码

4.2.2.2 安装 Koa2

npm init -y
npm install koa
复制代码

如果下载特别慢,可以将npm的下载源换成国内的下载源,命令如下:

npm set registry https://registry.npm.taobao.org/
复制代码

4.2.2.3 编写入口文件app.js

  • 创建Koa的实例对象
const Koa = require('koa') // 导入构造方法
const app = new Koa() // 通过构造方法,创建实例对象
复制代码
  • 编写响应函数(中间件)

    响应函数是通过use的方式才能产生效果, 这个函数有两个参数, 一个是 ctx,一个是 next

    ctx:上下文, 指的是请求所处于的Web容器,我们可以通过 ctx.request 拿到请求对象, 也可以通过 ctx.response 拿到响应对象

    next:内层中间件执行的入口

app.use((ctx, next) => {
    ctx.response.body = 'Hello Echarts'
})
复制代码
  • 绑定端口号
app.listen(9898)
复制代码
  • 启动服务器
node app.js
复制代码

然后在浏览器中输入 http://localhost:9898/ 你将会看到浏览器中出现 Hello Echarts 的字符串, 并且在服务器的终端中, 也能看到请求的 url

4.2.3 Koa2中间件的特点

  • Koa2 的实例对象通过 use 方法加入一个中间件
  • 一个中间件就是一个函数,这个函数具备两个参数,分别是 ctx 和 next
  • 中间件的执行符合洋葱模型
  • 内层中间件能否执行取决于外层中间件的 next 函数是否调用
  • 调用 next 函数得到的是 Promise 对象, 如果想得到 Promise 所包装的数据, 可以结合 await 和 async
app.use(async (ctx, next) => { 
    // 刚进入中间件想做的事情 
    await next() 
    // 内层所有中间件结束之后想做的事情 
})
复制代码

4.2.4 后端项目

4.2.4.1 目标

我们已学完 Koa2 的快速上手, 并且对 Koa2 当中的中间件的特点进行了了解. 接下来就是利用Koa2 的知识来进行后台项目的开发,后台项目需要达到以下几个目标:

  • 计算服务器处理请求的总耗时

    计算出服务器对于这个请求它的所有中间件总耗时时长究竟是,我们需要计算一下

  • 在响应头上加上响应内容的 mime 类型

    加入mime类型, 可以让浏览器更好的来处理由服务器返回的数据

    如果响应给前端浏览器是 json 格式的数据,这时候就需要在咱们的响应头当中增加 Content- Type 它的值就是 application/json , application/json 就是 json 数据类型的 mime 类型

  • 根据URL读取指定目录下的文件内容

    为了简化后台服务器的代码,前端图表所要的数据, 并没有存在数据库当中,而是将存在文件当中

的,这种操作只是为了简化咱们后台的代码. 所以咱们是需要去读取某一个目录下面的文件内容 的。

每一个目标就是一个中间件需要实现的功能, 所以后台项目中需要有三个中间件

4.2.4.2 步骤

创建一个新的文件夹 koa-server , 这个文件夹就是后台项目的文件夹

4.2.4.2.1 项目准备
  • 安装包
npm init -y 
npm install koa
复制代码
  • 创建文件和目录结构

    app.js 是后台服务器的入口文件

    data 目录是用来存放所有模块的 json 文件数据

    middleware 是用来存放所有的中间件代码

    koa_response_data.js 是业务逻辑中间件

    koa_response_duration.js 是计算服务器处理时长的中间件

    koa_response_header.js 是用来专门设置响应头的中间件

接着将各个模块的 json 数据文件复制到 data 的目录之下, 接着在 app.js 文件中写上代码如下:

// 服务器的入口文件 
// 1.创建KOA的实例对象 
const Koa = require('koa') 
const app = new Koa() 
// 2.绑定中间件 
// 绑定第一层中间件 
// 绑定第二层中间件 
// 绑定第三层中间件 
// 3.绑定端口号 9898 
app.listen(9898)
复制代码
4.2.4.2.2 总耗时中间件
  • 第1层中间件

    总耗时中间件的功能就是计算出服务器所有中间件的总耗时,应该位于第一层,因为第一层的中间件是最先处理请求的中间件,同时也是最后处理请求的中间件

  • 计算执行时间

    第一次进入咱们中间件的时候,就记录一个开始的时间,当其他所有中间件都执行完之后,再记录下结束时间以后,将两者相减就得出总耗时

  • 设置响应头

    将计算出来的结果,设置到响应头的 X-Response-Time 中, 单位是毫秒 ms

具体代码如下:

// app.js 文件
// 绑定第一层中间件 
const respDurationMiddleware = require('./middleware/koa_response_duration') app.use(respDurationMiddleware)
复制代码
// koa_response_duration.js 文件
// 计算服务器消耗时长的中间件 
module.exports = async (ctx, next) => { 
// 记录开始时间
const start = Date.now() 
// 让内层中间件得到执行 
await next() 
// 记录结束的时间 
const end = Date.now() 
// 设置响应头 X-Response-Time 
const duration = end - start 
// ctx.set 设置响应头 
ctx.set('X-Response-Time', duration + 'ms') }
复制代码
4.2.4.2.3 响应头中间件
  • 第2层中间件

    这个第2层中间件没有特定的要求

  • 获取mime类型

    由于咱们所响应给前端浏览器当中的数据都是 json 格式的字符串,所以 mime 类型可以统一的给它写成 application/json , 当然这一块也是简化的处理,因为 mime 类型有几十几百种,我们没有必要在项目当中考虑那么多,所以这里简化处理一下

  • 设置响应头

    响应头的key是 Content-Type ,它的值是 application/json , 顺便加上 charset=utf-8 告诉浏览器,我这部分响应的数据,它的类型是 application/json ,同时它的编码是 utf- 8

具体代码如下:

// app.js 文件
// 绑定第二层中间件 const respHeaderMiddleware = require('./middleware/koa_response_header') 
app.use(respHeaderMiddleware)
复制代码
// koa_response_header.js 文件
// 设置响应头的中间件 
module.exports = async (ctx, next) => { 
const contentType = 'application/json; charset=utf-8' 
ctx.set('Content-Type', contentType) 
await next() }
复制代码
4.2.4.2.4 业务逻辑中间件
  • 第3层中间件

    这个第3层中间件没有特定的要求

  • 读取文件内容

// 获取 URL 请求路径
const url = ctx.request.url
复制代码
// 根据URL请求路径,拼接出文件的绝对路径
let filePath = url.replace('/api', '') 
filePath = '../data' + filePath + '.json' 
filePath = path.join(__dirname, filePath)
复制代码

这个 filePath 就是需要读取文件的绝对路径

读取这个文件的内容,使用 fs 模块中的 readFile 方法进行实现

  • 设置响应体
ctx.response.body
复制代码

具体代码如下:

// app.js 文件
// 绑定第三层中间件 const respDataMiddleware = require('./middleware/koa_response_data') 
app.use(respDataMiddleware)
复制代码
// koa_response_data.js 文件
// 处理业务逻辑的中间件,读取某个json文件的数据 
const path = require('path') 
const fileUtils = require('../utils/file_utils') 

module.exports = async (ctx, next) => { 
// 根据url 
const url = ctx.request.url // /api/seller ../data/seller.json 
let filePath = url.replace('/api', '') // /seller 
filePath = '../data' + filePath + '.json' // ../data/seller.json 
filePath = path.join(__dirname, filePath) 
try { 
    const ret = await fileUtils.getFileJsonData(filePath) 
    ctx.response.body = ret
} catch (error) { 
    const errorMsg = { 
            message: '读取文件内容失败, 文件资源不存在', 
            status: 404 
        }
        ctx.response.body = JSON.stringify(errorMsg) 
    }
    console.log(filePath) 
    await next() 
}
复制代码
// file_utils.js 文件
// 读取文件的工具方法 
const fs = require('fs') 

module.exports.getFileJsonData = (filePath) => { 
    // 根据文件的路径, 读取文件的内容 
    return new Promise((resolve, reject) => { 
        fs.readFile(filePath, 'utf-8', (error, data) => { 
            if(error) { 
                // 读取文件失败 
                reject(error) 
            } else { 
                // 读取文件成功 
                resolve(data) 
            } 
        }) 
    }) 
}
复制代码
4.2.4.2.5 允许跨域
  • 设置响应头
app.use(async (ctx, next) => { 
    ctx.set("Access-Control-Allow-Origin", "*") 
    ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE") 
    await next(); 
})
复制代码

4.3 前端部分

4.3.1 前端项目的准备

4.3.1.1 vue-cli 脚手架创建项目

vue-cli 脚手架安装

npm install -g @vue/cli
复制代码

创建工程项目

vue create screen
复制代码

手动选择配置项如下图所示:

4.png

5.png

6.png

7.png

8.png

9.png

10.png

11.png

12.png

13.png

安装成功执行以下命令:

cd screen
npm run serve
复制代码

删除无关代码

  • 修改 App.vue 中的代码,将布局和样式删除, 变成如下代码:
<template>
  <div id="app">
    <!-- 路由占位符 -->
    <router-view />
  </div>
</template>

<style lang="less">

</style>
复制代码
  • 删除 components/HelloWorld.vue 这个文件
  • 删除 views/About.vue 和 views/Home.vue 这两个文件
  • 修改 router/index.js 中的代码,去除路由配置和 Home 组件导入的代码
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    redirect: '/screen'
  },
  {
    path: '/screen',
    component: () => import('@/views/screenPage')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router
复制代码

4.3.1.2 项目基本配置

在项目根目录下创建 vue.config.js 文件,新增以下代码:

// 使用vue-cli创建出来的vue工程, Webpack的配置是被隐藏起来了的 
// 如果想覆盖Webpack中的默认配置,需要在项目的根路径下增加vue.config.js文件
module.exports = {
    devServer: {
        port: 8999, // 端口号配置
        // open: true // 自动打开浏览器
    },
    productionSourceMap: false, // 生产环境是否生成 sourceMap 文件
    configureWebpack: (config) => {
        if (process.env.NODE_ENV === 'production') { // 为生产环境修改配置...
            config.mode = 'production';
            config["performance"] = { //打包文件大小配置
                "maxEntrypointSize": 10000000,
                "maxAssetSize": 30000000
            }
        }
    }
}
复制代码

4.3.1.3 全局echarts对象

  • 引入echarts文件

在public/index.html文件中引入外部CDN文件echarts.min.js,如下图:

14.png

  • 全局echarts挂载到Vue原型对象上并使用

在src/main.js文件中挂载,代码如下:

// 将全局的echarts对象挂载到Vue的原型对象上
// 在别的组件中使用 this.$echarts
Vue.prototype.$echarts = window.echarts
复制代码

4.3.1.4 axios的处理

安装axios包

npm install axios
复制代码

封装与使用axios

在 src/main.js 文件中配置 axios 并且挂载到Vue的原型对象上,代码如下:

// 将axios挂载到Vue的原型对象上
// 在别的组件中使用 this.$http
Vue.prototype.$http = axios
复制代码

4.3.2 单独图表组件开发

每个图表会单独进行开发,最后再将所有的图表合并到一个页面中,在单独开发每个图表的时候,一个图表会用一个单独的路径进行全屏展示,他们分别是:

  • 商家销售统计

http://localhost:8999/sellerPage

  • 销量趋势分析

http://localhost:8999/trendPage

  • 商家地图分布

http://localhost:8999/mapPage

  • 地区销量排行

http://localhost:8999/rankPage

  • 热销商品占比

http://localhost:8999/hotPage

  • 库存销量分析

http://localhost:8999/stockPage

4.3.2.1 商家销量统计

最终效果如下图所示:

15.png

组件结构设计

在 src/components/ 目录下建立 Seller.vue , 这个组件是真实展示图表的组件

  • 给外层div增加类样式 com-container
  • 建立一个显示图表的div元素
  • 给新增的这个div增加类样式 com-chart
<template>
  <div class="com-container">
    <div class="com-chart" ref="seller_ref"></div>
  </div>
</template>

<script> 
export default { 
    data () { 
        return {}
    },
    methods: {} 
}
</script> 

<style lang="less" scoped> 
</style>
复制代码

在 src/views/ 目录下建立 sellerPage.vue,这个组件是对应于路由 /seller 而展示的

  • 给外层div元素增加样式 com-page
  • 在 sellerPage 中引入 Seller 组件,并且注册和使用
<template>
  <div class="com-page">
    <Seller />
  </div>
</template>

<script>
import Seller from "@/components/Seller";

export default {
  components: {
    Seller,
  },
  data() {
    return {};
  },
  methods: {},
};
</script>

<style lang="less" scoped>
</style>
复制代码

增加路由规则, 在 src/router/index.js 文件新增如下代码:

const routes = [ 
    { 
        path: '/sellerPage', 
        component: () => import('@/views/sellerPage') 
    } 
]
复制代码

新建 src/assets/css/global.less 增加宽高样式

原则就是将所有的容器的宽度和高度设置为占满父容器

html, 
body, 
#app {
    width: 100%;
    height: 100%;
    padding: 0;
    margin: 0;
    overflow: hidden;
}

.com-page, 
.com-container,
.com-chart {
    width: 100%;
    height: 100%;
    overflow: hidden;
}

canvas {
    border-radius: 20px;
}

.com-container {
    position: relative;
}
复制代码

在 main.js 中引入样式

import './assets/css/global.less'
复制代码

打开浏览器, 输入 http://localhost:8999/sellerPage 看Seller组件是否能够显示

图表Seller.vue基本功能实现

  • 在mounted生命周期中初始化 echartsInstance 对象
  • 在mounted中获取服务器的数据
  • 将获取到的数据设置到图表上
<script>
import { mapState } from "vuex";
import { getThemeValue } from "@/utils/theme_utils";
export default {
  data() {
    return {
      myChart: null, // echarts实例对象
      allData: null, // 服务器获取的所有数据
    };
  },
  mounted() {
    this.initChart();
    this.getData();
  },
  methods: {
    // 初始化echartsInstance对象
    initChart() {
      this.myChart = this.$echarts.init(this.$refs.seller_ref, this.theme);
    },
    // 获取服务端的数据
   async getData() {
      const { data: ret } = await this.$http.get("seller");
      // console.log("获取后端数据===", ret);
      this.allData = ret;
      // 对数据排序
      this.allData.sort((a, b) => {
        return a.value - b.value;
      });
      this.updateChart();
    },
    // 更新图表
    updateChart() {
      const sellerName = showData.map((item) => {
        return item.name;
      });

      const sellerValue = showData.map((item) => {
        return item.value;
      });

      const dataOption = {
        xAxis: {
          type: "value"
        },
        yAxis: {
          type: "category",
          data: sellerName,
        },
        series: [
          {
            type: "bar",
            data: sellerValue,
          },
        ],
      };

      this.myChart.setOption(dataOption);
    },
  },
};
</script>
复制代码

拆分配置项option

初始化配置项

21.png

拥有数据之后的配置项

22.png

分页动画实现

  • 数据的处理, 每5个元素显示一页

数据的处理

22.png

23.png

24.png

动画的启动和停止

25.png

26.png

27.png

鼠标事件的处理

28.png

UI效果调整

主题的指定,在初始化echarts实例对象的时候指定

// src/components/Seller.vue
methods: {
    initChart() {
      this.myChart = this.$echarts.init(this.$refs.seller_ref, 'dark');
      // 对图表对象进行鼠标事件的监听
      this.myChart.on("mouseover", () => {
        clearInterval(this.timer);
      });
      this.myChart.on("mouseout", () => {
        this.startInterval();
      });
    }
}
复制代码

边框圆角设置

//  src/assets/css/global.less
canvas {
    border-radius: 20px;
}
复制代码

其他图标样式配置

// 标题的位置和颜色
const initOption = {
    title: {
      text: "▎ 商家销售统计",
      textStyle: {
        fontSize: 66,
      },
      left: 20,
      top: 20,
    }
}
复制代码
// 坐标轴的大小
const initOption = {
    grid: {
      top: "20%",
      left: "3%",
      right: "6%",
      bottom: "3%",
      containLabel: true, // 距离包含坐标轴上的文字
    }
}
复制代码
// 工具提示和背景
const initOption = {
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: 'shadow'
      },
    }
}
复制代码
// 文字显示和位置
const initOption = {
    series: [
      {
        label: {
          show: true,
          position: "right",
          textStyle: {
            color: '#fff',
          },
        }
     ]
}
复制代码
// 柱宽度和柱圆角的实现
const initOption = {
    series: [
      {
        barWidth: 66,
            itemStyle: {
              barBorderRadius: [0, 33, 33, 0],
            },
        }
     ]
}
复制代码
// 柱颜色渐变的实现,线性渐变可以通过 LinearGradient 进行实现
// LinearGradient 需要传递5个参数, 前四个代表两个点的相对位置,第五个参数代表颜色变化的范围 
// 0, 0, 1, 0 代表的是从左往右的方向
const initOption = {
    series: [
      {
            itemStyle: {
              barBorderRadius: [0, 33, 33, 0],
              // 指明颜色渐变的方向
              // 指明不同百分比之下颜色的值
              color: {
                type: "linear",
                x: 0,
                y: 0,
                x2: 1,
                y2: 0,
                colorStops: [
                  {
                    offset: 0,
                    color: "#5052EE", // 0% 处的颜色
                  },
                  {
                    offset: 1,
                    color: "#AB6EE5", // 100% 处的颜色
                  },
                ],
                global: false, // 缺省为 false
              },
            },
        }
     ]
}
复制代码

分辨率适配

  • 对窗口大小变化的事件进行监听
mounted() {
    window.addEventListener("resize", this.screenAdapter);
}

destroyed() {
    // 在组件销毁时,需将监听器注销
    window.removeEventListener("resize", this.screenAdapter);
},
复制代码
  • 获取图表容器的宽度计算字体大小
// 当浏览器的大小发生变化时,会调用的方法,来完成屏幕的适配
methods: {
    screenAdapter() {
      const titleFontSize = (this.$refs.seller_ref.offsetWidth / 100) * 3.6;
    }
}
复制代码
  • 将字体大小的值设置给图表的某些区域
// 标题大小、背景大小、柱宽度、圆角大小
methods: {
    screenAdapter() {
      const titleFontSize = (this.$refs.seller_ref.offsetWidth / 100) * 3.6;
      const adapterOption = {
        title: {
          textStyle: {
            fontSize: titleFontSize,
          },
        },
        tooltip: {
          axisPointer: {
            lineStyle: {
              width: titleFontSize,
            },
          },
        },
        series: [
          {
            barWidth: titleFontSize,
            itemStyle: {
              borderRadius: [0, titleFontSize / 2, titleFontSize / 2, 0],
            },
          },
        ],
      };
      this.myChart.setOption(adapterOption);
      // 手动调用图表对象的resize才能生效
      this.myChart.resize();
    }
}
复制代码

4.3.2.2 销量趋势分析

最终效果如下图所示:

17.png

  • 代码环境准备
// trendPage.vue
// 针对于 /trendPage 这条路径而显示出来的 在这个组件中, 通过子组件注册的方式, 要显示出Trend.vue这个组件
<template>
  <div class="com-page">
    <Trend />
  </div>
</template>

<script>
import Trend from "@/components/Trend";

export default {
  components: {
    Trend,
  },
  data() {
    return {};
  },
  methods: {},
};
</script>

<style lang="less" scoped>
</style>
复制代码
// Trend.vue
<template>
  <div class="com-container">
    <div class="com-chart" ref="trend_ref"></div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      myChart: null,
      allData: null
    };
  },
  created() {
  },
  mounted() {
    this.initChart();
    this.getData();
    window.addEventListener("resize", this.screenAdapter);
    this.screenAdapter();
  },
  destroyed() {
    window.removeEventListener("resize", this.screenAdapter);
  },
  methods: {
    initChart() {
      this.myChart = this.$echarts.init(this.$refs.trend_ref, 'dark');
      const initOption = {};
      this.myChart.setOption(initOption);
    },
    async getData() {
      const { data: ret } = await this.$http.get("trend");
      this.allData = ret;
      this.updateChart();
    },
    updateChart() {
      const dataOption = {};
      this.myChart.setOption(dataOption);
    },
    screenAdapter() {
      const adapterOption = {};
      this.myChart.setOption(adapterOption);
      this.myChart.resize();
    }
  },
};
</script>

<style lang="less" scoped>
</style>
复制代码
// router/index.js
const routes = [
  {
     path: '/trendPage',
     component: () => import('@/views/trendPage')
  }
]
复制代码
  • 图表基本功能的实现

数据的获取

// 获取服务器的数据, 对this.allData进行赋值之后, 调用updateChart方法更新图表
async getData() {
  const { data: ret } = await this.$http.get("trend");
  this.allData = ret;
  this.updateChart();
}
复制代码

数据的处理

updateChart() {
  // 类目轴数据
  const timeArr = this.allData.common.month;
  // y轴数据 series下的数据
  // map代表地区销量趋势 
  // seller代表商家销量趋势 
  // commodity代表商品销量趋势
  const valueArr = this.allData.map.data;
  // 图表数据, 一个图表中显示5条折线图
  const seriesArr = valueArr.map((item, index) => {
    return {
      name: item.name,
      type: "line",
      data: item.data,
      smooth: true,
      stack: 'map' // stack值相同, 可以形成堆叠图效果
    };
  });
  // 图例数据
  const legendArr = valueArr.map((item) => {
    return item.name;
  });
  const dataOption = {
    xAxis: {
      data: timeArr,
    },
    legend: {
      data: legendArr,
    },
    series: seriesArr,
  };
  this.myChart.setOption(dataOption);
}
复制代码

初始化配置

const initOption = {
    xAxis: {
        type: "category",
        boundaryGap: false
    },
    yAxis: {
        type: "value"
    }
}
复制代码
  • UI效果调整

主题的使用

initChart() {
  this.myChart = this.$echarts.init(this.$refs.trend_ref, 'dark');
}
复制代码

坐标轴大小和位置,工具提示,图例位置和形状

const initOption = {
    // 坐标轴大小和位置
    grid: {
      left: "3%",
      top: "30%",
      right: "4%",
      bottom: "1%",
      containLabel: true,
    },
    // 工具提示
    tooltip: {
      trigger: "axis",
    },
    // 图例位置和形状
    legend: {
      left: 20,
      top: "15%",
      icon: "circle",
    }
}
复制代码

区域面积和颜色渐变的设置

updateChart() {
      // 半透明颜色值
      const colorArr1 = [
        "rgba(73, 146, 255, .5)",
        "rgba(124, 255, 178, .5)",
        "rgba(253, 221, 96, .5)",
        "rgba(255, 110, 118, .5)",
        "rgba(88, 217, 249, .5)",
      ];
      // 全透明颜色值
      const colorArr2 = [
        "rgba(73, 146, 255, 0)",
        "rgba(124, 255, 178, 0)",
        "rgba(253, 221, 96, 0)",
        "rgba(255, 110, 118, 0)",
        "rgba(88, 217, 249, 0)",
      ];
      const seriesArr = valueArr.map((item, index) => {
        return {
          // 区域面积只需要给series的每一个对象增加一个 areaStyle 即可
          areaStyle: {
            // 颜色渐变可以通过 LinearGradient 进行设置, 颜色渐变的方向从上往下
            color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
              {
                offset: 0,
                color: colorArr1[index],
              },
              {
                offset: 1,
                color: colorArr2[index],
              },
            ]),
          },
        };
      });
    }
复制代码
  • 切换图表

  • 分辨率适配

分辨率适配主要就是在 screenAdapter 方法中进行, 需要获取图表容器的宽度,计算出标题字体大小,将字体的大小赋值给 titleFontSize

<script>
export default {
    data() {
        return {
            titleFontSize: 0
        }
    },
    methods: {
        screenAdapter() {
          this.titleFontSize = (this.$refs.trend_ref.offsetWidth / 100) * 3.6;
        }
    }
}
</script>
复制代码

通过 titleFontSize 去设置给标题文字的大小和图例的大小

标题文字的大小,增加计算属性comStyle并设置给对应的div,代码如下:

<template>
  <div class="com-container">
    <div class="title" :style="comStyle">
      <span>{{ "▎ " + showTitle }}</span>
      <span
        class="iconfont icon-arrow-down title-icon"
        :style="comStyle"
        @click="showChoice = !showChoice"
      ></span>
    </div>
  </div>
</template>

<script>
export default {
    data() {
        return {
            titleFontSize: 0
        }
    },
    computed: {
        // 设置给标题的样式
        comStyle() {
          return {
            fontSize: this.titleFontSize + "px"
          };
        }
    }
}
</script>
复制代码

图例的大小

methods: {
    screenAdapter() {
      this.titleFontSize = (this.$refs.trend_ref.offsetWidth / 100) * 3.6;
      const adapterOption = {
        legend: {
          itemWidth: this.titleFontSize,
          itemHeight: this.titleFontSize,
          itemGap: this.titleFontSize,
          textStyle: {
            fontSize: this.titleFontSize / 2,
          },
        },
      };
      this.myChart.setOption(adapterOption);
      this.myChart.resize();
    }
}
复制代码

4.3.2.3 商家地图分布

最终效果如下图所示:

18.png

如需获取更多资料及思维导图,可以关注作者公众号《懒人码农》,后台回复关键词“大屏”即可获取

查看完整源代码,请移步到github访问👉:github.com/jackchen012…

4.3.2.4 地区销量排行

最终效果如下图所示:

16.png

4.3.2.5 热销商品占比

最终效果如下图所示:

19.png

4.3.2.6 库存销量分析

最终效果如下图所示:

20.png

4.3.3 WebScoket的使用

4.3.3.1 后端代码

安装WebSocket包

npm install ws -S
复制代码

创建 service\web_socket_service.js 文件

  • 创建WebSocket实例对象
const WebSocket = require('ws');
// 创建websocket服务端的对象,绑定端口号为9998
const wss = new WebSocket.Server({
	port: 9998
})
复制代码
  • 监听事件
wss.on("connection", client => { 
    console.log("有客户端连接...") 
    client.on("message", msg => { 
        console.log("客户端发送数据过来了") 
        // 发送数据给客户端 
        client.send('hello socket') 
    }) 
})
复制代码
  • 在app.js中引入web_scoket_service.js这个文件,并调用listen方法
const webSocketService = require('./service/web_socket_service')
// 开启服务端的监听,监听客户端的连接
// 当某一个客户端连接成功之后,就会对这个客户端进行message事件的监听
webSocketService.listen()
复制代码
  • 约定好喝客户端之前数据交互的格式和含义

客户端和服务端之间的数据交互采用 JSON 格式

客户端发送数据给服务端的字段如下:

{ 
  "action": "getData", 
  "socketType": "trendData", 
  "chartName": "trend", 
  "value": "" 
}
或者
{ 
  "action": "fullScreen", 
  "socketType": "fullScreen", 
  "chartName": "trend", 
  "value": true 
}
或者
{ 
  "action": "themeChange", 
  "socketType": "themeChange", 
  "chartName": "", 
  "value": "dark" 
}
复制代码

action : 代表某项行为,可选值有

  • getData 代表获取图表数据
  • fullScreen 代表产生了全屏事件
  • themeChange 代表产生了主题切换的事件

socketType : 代表业务模块类型, 这个值代表前端注册数据回调函数的标识, 可选值有:

  • trendData
  • sellerData
  • mapData
  • rankData
  • hotData
  • stockData
  • fullScreen
  • themeChange

chartName : 代表图表名称, 如果是主题切换事件, 可不传此值, 可选值有:

  • trend
  • seller
  • map
  • rank
  • hot
  • stock

value : 代表 具体的数据值, 在获取图表数据时, 可不传此值, 可选值有

  • 如果是全屏事件, true 代表全屏, false 代表非全屏
  • 如果是主题切换事件, 可选值有 chalk 或者 vintage

服务端发送给客户端的数据如下:

{
    "action": "getData",
    "socketType": "trendData",
    "chartName": "trend",
    "value": "",
    "data": "从文件读取出来的json文件的内容"
}
或者
{
    "action": "fullScreen",
    "socketType": "fullScreen",
    "chartName": "trend",
    "value": true
}
或者
 {
    "action": "themeChange",
    "socketType": "themeChange",
    "chartName": "",
    "value": "dark"
}
复制代码

注意, 除了 action 为 getData 时, 服务器会在客户端发过来数据的基础之上, 增加 data 字段,其他的情况, 服务器会原封不动的将从某一个客户端发过来的数据转发给每一个处于连接状态 的客户端

  • 代码实现
const path = require('path');
const fileUtils = require('../utils/file_utils');
const WebSocket = require('ws');
// 创建websocket服务端的对象,绑定端口号为9998
const wss = new WebSocket.Server({
	port: 9998
})

module.exports.listen = () => {
	// 对客户端的连接事件进行监听
	// client代表是客户端的连接socket对象
	wss.on('connection', client => {
		console.log('有客户端连接成功...');
		// 对客户端的连接对象进行message事件的监听
		// msg由客户端发送给服务端的数据
		client.on('message', async msg => {
			console.log('客户端发送数据给服务端===', msg);
			let payload = JSON.parse(msg);
			const action = payload.action;
			if (action === 'getData') {
				let filePath = '../data/' + payload.chartName + '.json';
				// trend seller map rank hot stock
				// payload.chartName
				filePath = path.join(__dirname, filePath);
				const ret = await fileUtils.getFileJsonData(filePath);
				// 需要在服务端获取到数据的基础之上,增加一个data的字段
				// data所对应的值,就是某个json文件的内容
				payload.data = ret;
				client.send(JSON.stringify(payload));
			} else {
				// 原封不动的将所接收到的数据转发给每一个处于连接状态的客户端
				// wss.clients 所有客户端的连接
				wss.clients.forEach(client => {
					client.send(msg);
				})
			}
			// 服务端向客户端发送数据
			// client.send('hello socket form backend');
		})
	})
}
复制代码

4.3.3.2 前端代码

  • 定义单例,创建WebSocket实例对象

创建scr/utils/socket_service.js文件,定义单例代码如下:

export default class SocketService {
  // 单例模式

  static instance = null;
  static get Instance () {
    if (!this.instance) {
      this.instance = new SocketService();
    }
    return this.instance;
  }
}
复制代码
  • 监听WebSocket事件

定义connect函数,将创建的WebSocket赋值给实例属性,代码如下:

// 实例属性ws和服务端连接的socket对象
ws = null;

// 定义连接服务器的方法
connect () {
    // 连接服务器
    if (!window.WebSocket) {
      return console.log('您的浏览器不支持websocket');
    }
    this.ws = new WebSocket(`ws://106.55.168.13:9998/ws/webSocket`);
}
复制代码

监听事件

connect() {
  if (!window.WebSocket) {
    return console.log('您的浏览器不支持 WebSocket!')
  }
  this.ws = new WebSocket('ws://localhost:9998')
  // 监听连接成功 
  this.ws.onopen = () => {
    console.log('WebSocket 连接成功')
  }
  // 服务器连接不成功,服务器关闭了连接 
  this.ws.onclose = e => {
    console.log('服务器关闭了连接')
  }
  // 监听接收消息 
  this.ws.onmessage = msg => {
    console.log('WebSocket 接收到数据')
  }
}
复制代码

定义注册函数

export default class SocketService { 
  // 业务类型和回调函数的对于关系 
  callBackMapping = {} 
  /*** socketType 
   * trendData sellerData mapData rankData hotData stockData 
   * fullScreen 
   * themeChange 
   * callBack 
   * 回调函数 
  */ 
 registerCallBack (socketType, callBack) { 
   // 往 callBackMap中存放回调函数 
   this.callBackMapping[socketType] = callBack 
  }
  unRegisterCallBack (socketType) { 
    this.callBackMapping[socketType] = null 
  }
}
复制代码

连接服务端

// 在 main.js 中连接服务器端
import SocketService from '@/utils/socket_service' SocketService.Instance.connect()

// 将 SocketService 实例对象挂载到 Vue 的原型对象上
Vue.prototype.$socket = SocketService.Instance
复制代码

发送数据给服务端

在 socket_service.js 中定义发送数据的方法

export default class SocketService {
    send (data) { 
        console.log('发送数据给服务器:') 
        this.ws.send(JSON.stringify(data)) 
    }
}
复制代码

先修改 Trend.vue 文件,代码如下:

mounted() {
    // 当socket来数据的时候, 会调用getData这个函数 
    this.$socket.registerCallBack('trendData', this.getData)
    // 往 socket 发送数据, 目的是想让服务端传输销量趋势这个模块的数据
    this.initChart();
    // this.getData();
    // 发送数据给服务端,告诉服务端,前端现在需要数据
    this.$socket.send({
      action: "getData",
      socketType: "trendData",
      chartName: "trend",
      value: ""
    })
}
// action的值不变,都是getData 
// socketType的可选值有:trendData,sellerData,mapData,rankData,hotData,stockData
// chartName的可选值有: trend,seller,map,rank,hot,stock

destroyed () { 
    this.$socket.unRegisterCallBack('trendData')
}
复制代码

运行代码, 发现数据发不出去

因为在刷新界面之后, 客户端和服务端的连接并不会立马连接成功, 在处于连接状态下就调用 send 是发送不成功的, 因此需要修改 service_socket.js 中的 send 方法进行容错处理

// 标识是否连接成功
connected = false;

// 记录重试的次数
sendRetryCount = 0;

// 发送数据的方法
send (data) {
    // 判断现在是否有连接成功
    if (this.connected) {
      this.sendRetryCount = 0;
      this.ws.send(JSON.stringify(data));
    } else {
      this.sendRetryCount++;
      setTimeout(() => {
        this.send(data);
      }, this.sendRetryCount * 500)
    }
}
复制代码

在 onopen 时设置 connected 的值

// 定义连接服务器的方法
connect () {
    // 连接成功的事件
    this.ws.onopen = () => {
      console.log('连接服务端成功');
      this.connected = true;
      this.connectRetryCount = 0;
    }
}
复制代码

在 socket_service.js 中修改接收到消息的代码处理

// 定义连接服务器的方法
connect () {
    // 得到服务端发送过来的数据
    this.ws.onmessage = msg => {
      // console.log('从服务端获取到的数据===', msg);
      // 真正服务端发送过来的原始数据时在msg中的data字段
      const recvData = JSON.parse(msg.data);
      const socketType = recvData.socketType;
      // 判断回调函数是否存在
      if (this.callBackMapping[socketType]) {
        const action = recvData.action
        if (action === 'getData') {
          const realData = JSON.parse(recvData.data);
          this.callBackMapping[socketType].call(this, realData);
        } else if (action === 'fullScreen') {
          this.callBackMapping[socketType].call(this, recvData);
        } else if (action === 'themeChange') {
          this.callBackMapping[socketType].call(this, recvData);
        }
      }
    }
}
复制代码

断开重连机制

如果初始化连接服务端不成功, 或者连接成功了, 后来服务器关闭了, 这两种情况都会触发 onclose 事件,我们需要在这个事件中,进行重连

connect() {
    // 监听连接成功 
    this.ws.onopen = () => {
      // 连接成功之后, 重置重连次数
      this.connectRetryCount = 0; 
    }
    // 连接服务端失败
    // 当连接成功之后,服务端关闭的情况
    this.ws.onclose = () => {
      console.log('连接服务端失败');
      this.connected = false;
      this.connectRetryCount++;
      setTimeout(() => {
        this.connect();
      }, this.connectRetryCount * 500)
    }
}
复制代码

4.3.4 组件合并

  • 创建screenPage.vue文件,并配置路由规则,代码如下:
// router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    redirect: '/screen'
  },
  {
    path: '/screen',
    component: () => import('@/views/screenPage')
  }
]
复制代码
  • 代码实现

静态图片资源放在public/static/img目录之下,完整代码如下:

// screenPage.vue
<template>
  <div class="screen-container" :style="containerStyle">
    <header class="screen-header">
      <div>
        <img :src="headerSrc" alt="" />
      </div>
      <span class="logo">
        <img :src="logoSrc" alt="" />
      </span>
      <span class="title">电商平台数据大屏实时监控系统</span>
      <div class="title-right">
        <img :src="themeSrc" class="qiehuan" @click="handleChangeTheme" />
        <span class="datetime">{{ timeValue }}</span>
      </div>
    </header>
    <div class="screen-body">
      <section class="screen-left">
        <div
          id="left-top"
          :class="[fullScreenStatus.trend ? 'fullscreen' : '']"
        >
          <!-- 销量趋势图表 -->
          <Trend ref="trend" />
          <div class="resize">
            <!-- icon-compress-alt -->
            <span
              @click="changeSize('trend')"
              :class="[
                'iconfont',
                fullScreenStatus.trend
                  ? 'icon-compress-alt'
                  : 'icon-expand-alt',
              ]"
            ></span>
          </div>
        </div>
        <div
          id="left-bottom"
          :class="[fullScreenStatus.seller ? 'fullscreen' : '']"
        >
          <!-- 商家销售金额图表 -->
          <Seller ref="seller" />
          <div class="resize">
            <!-- icon-compress-alt -->
            <span
              @click="changeSize('seller')"
              :class="[
                'iconfont',
                fullScreenStatus.seller
                  ? 'icon-compress-alt'
                  : 'icon-expand-alt',
              ]"
            ></span>
          </div>
        </div>
      </section>
      <section class="screen-middle">
        <div
          id="middle-top"
          :class="[fullScreenStatus.map ? 'fullscreen' : '']"
        >
          <!-- 商家分布图表 -->
          <Map ref="map" />
          <div class="resize">
            <!-- icon-compress-alt -->
            <span
              @click="changeSize('map')"
              :class="[
                'iconfont',
                fullScreenStatus.map ? 'icon-compress-alt' : 'icon-expand-alt',
              ]"
            ></span>
          </div>
        </div>
        <div
          id="middle-bottom"
          :class="[fullScreenStatus.rank ? 'fullscreen' : '']"
        >
          <!-- 地区销量排行图表 -->
          <Rank ref="rank" />
          <div class="resize">
            <!-- icon-compress-alt -->
            <span
              @click="changeSize('rank')"
              :class="[
                'iconfont',
                fullScreenStatus.rank ? 'icon-compress-alt' : 'icon-expand-alt',
              ]"
            ></span>
          </div>
        </div>
      </section>
      <section class="screen-right">
        <div id="right-top" :class="[fullScreenStatus.hot ? 'fullscreen' : '']">
          <!-- 热销商品占比图表 -->
          <hot ref="hot" />
          <div class="resize">
            <!-- icon-compress-alt -->
            <span
              @click="changeSize('hot')"
              :class="[
                'iconfont',
                fullScreenStatus.hot ? 'icon-compress-alt' : 'icon-expand-alt',
              ]"
            ></span>
          </div>
        </div>
        <div
          id="right-bottom"
          :class="[fullScreenStatus.stock ? 'fullscreen' : '']"
        >
          <!-- 库存销量分析图表 -->
          <Stock ref="stock" />
          <div class="resize">
            <!-- icon-compress-alt -->
            <span
              @click="changeSize('stock')"
              :class="[
                'iconfont',
                fullScreenStatus.stock
                  ? 'icon-compress-alt'
                  : 'icon-expand-alt',
              ]"
            ></span>
          </div>
        </div>
      </section>
    </div>
  </div>
</template>

<script>
import Hot from "@/components/Hot.vue";
import Map from "@/components/Map.vue";
import Rank from "@/components/Rank.vue";
import Seller from "@/components/Seller.vue";
import Stock from "@/components/Stock.vue";
import Trend from "@/components/Trend.vue";
import { mapState } from "vuex";
import { getThemeValue } from "@/utils/theme_utils";
export default {
  components: {
    Hot,
    Map,
    Rank,
    Seller,
    Stock,
    Trend,
  },
  data() {
    return {
      // 定义每一个图表的全屏状态
      fullScreenStatus: {
        trend: false,
        seller: false,
        map: false,
        rank: false,
        hot: false,
        stock: false,
      },
      timer: null,
      timeValue: "",
    };
  },
  created() {
    // 注册接收到数据的回调函数
    this.$socket.registerCallBack("fullScreen", this.recvData);
    this.$socket.registerCallBack("themeChange", this.recvThemeChange);
  },
  destroyed() {
    this.$socket.unRegisterCallBack("fullScreen");
    this.$socket.unRegisterCallBack("themeChange");
    clearInterval(this.timer);
  },
  mounted() {
    this.displayTime();
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.timer = setInterval(() => {
      this.displayTime();
    }, 1000)
  },
  methods: {
    displayTime() {
      //获取系统当前的年、月、日、小时、分钟、毫秒
      let date, year, month, day, h, m, s;
      date = new Date();
      year = date.getFullYear();
      month = date.getMonth() + 1;
      day = date.getDate();
      h = date.getHours();
      m = date.getMinutes();
      s = date.getSeconds();
      month = month < 10 ? "0" + month : month;
      day = day < 10 ? "0" + day : day;
      h = h < 10 ? "0" + h : h;
      m = m < 10 ? "0" + m : m;
      s = s < 10 ? "0" + s : s;
      return this.timeValue = year + "-" + month + "-" + day + "  " + h + ":" + m + ":" + s;
    },
    changeSize(chartName) {
      console.log(chartName);
      // 将数据发送给服务端
      const targetValue = !this.fullScreenStatus[chartName];
      this.$socket.send({
        action: "fullScreen",
        socketType: "fullScreen",
        chartName: chartName,
        value: targetValue,
      });
    },
    // 接收到全屏数据之后的处理
    recvData(data) {
      // 取出是哪一个图表需要进行切换
      const chartName = data.chartName;
      // 取出, 切换成什么状态
      const targetValue = data.value;
      this.fullScreenStatus[chartName] = targetValue;
      this.$nextTick(() => {
        this.$refs[chartName].screenAdapter();
      });
    },
    handleChangeTheme() {
      // 修改VueX中数据
      this.$socket.send({
        action: "themeChange",
        socketType: "themeChange",
        chartName: "",
        value: "",
      });
    },
    recvThemeChange() {
      this.$store.commit("changeTheme");
    },
  },
  computed: {
    logoSrc() {
      return "/static/img/" + getThemeValue(this.theme).logoSrc;
    },
    headerSrc() {
      return "/static/img/" + getThemeValue(this.theme).headerBorderSrc;
    },
    themeSrc() {
      return "/static/img/" + getThemeValue(this.theme).themeSrc;
    },
    containerStyle() {
      return {
        backgroundColor: getThemeValue(this.theme).backgroundColor,
        color: getThemeValue(this.theme).titleColor,
      };
    },
    ...mapState(["theme"]),
  },
};
</script>

<style lang="less" scoped>
// 全屏样式的定义
.fullscreen {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  width: 100% !important;
  height: 100% !important;
  margin: 0 !important;
  z-index: 9999;
}

.screen-container {
  width: 100%;
  height: 100%;
  padding: 0 20px;
  background-color: #2e2e2f;
  color: #fff;
  box-sizing: border-box;
}
.screen-header {
  width: 100%;
  font-size: 20px;
  position: relative;
  > div {
    img {
      width: 100%;
    }
  }
  .title {
    position: absolute;
    left: 50%;
    top: 50%;
    font-size: 20px;
    transform: translate(-50%, -50%);
  }
  .title-right {
    display: flex;
    align-items: center;
    position: absolute;
    right: 0px;
    top: 50%;
    transform: translateY(-80%);
  }
  .qiehuan {
    width: 28px;
    height: 21px;
    cursor: pointer;
  }
  .datetime {
    font-size: 15px;
    margin-left: 10px;
  }
  .logo {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-80%);
    img {
      height: 35px;
      width: 154px;
    }
  }
}
.screen-body {
  width: 100%;
  height: 100%;
  display: flex;
  margin-top: 10px;
  .screen-left {
    height: 100%;
    width: 27.6%;
    #left-top {
      height: 53%;
      position: relative;
    }
    #left-bottom {
      height: 31%;
      margin-top: 25px;
      position: relative;
    }
  }
  .screen-middle {
    height: 100%;
    width: 41.5%;
    margin-left: 1.6%;
    margin-right: 1.6%;
    #middle-top {
      width: 100%;
      height: 56%;
      position: relative;
    }
    #middle-bottom {
      margin-top: 25px;
      width: 100%;
      height: 28%;
      position: relative;
    }
  }
  .screen-right {
    height: 100%;
    width: 27.6%;
    #right-top {
      height: 46%;
      position: relative;
    }
    #right-bottom {
      height: 38%;
      margin-top: 25px;
      position: relative;
    }
  }
}
.resize {
  position: absolute;
  right: 20px;
  top: 20px;
  cursor: pointer;
}
</style>
复制代码

4.3.5 全屏切换

  • 全屏状态数据定义
export default {
  data() {
    return {
      // 定义每一个图表的全屏状态
      fullScreenStatus: {
        trend: false,
        seller: false,
        map: false,
        rank: false,
        hot: false,
        stock: false,
      },
      timer: null,
      timeValue: "",
    };
  },
}
复制代码
  • 全屏状态样式定义
<style lang="less" scoped>
// 全屏样式的定义
.fullscreen {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  width: 100% !important;
  height: 100% !important;
  margin: 0 !important;
  z-index: 9999;
}
</style>
复制代码
  • class值得处理
<div id="left-top" :class="[fullScreenStatus.trend ? 'fullscreen' : '']">
    <!-- 销量趋势图表 -->
    <Trend ref="trend" />
    <div class="resize">
    <!-- icon-compress-alt -->
    <span
      @click="changeSize('trend')"
      :class="[
        'iconfont',
        fullScreenStatus.trend
          ? 'icon-compress-alt'
          : 'icon-expand-alt',
      ]"
    ></span>
    </div>
</div>
复制代码
  • 全屏点击事件的处理
export default {
    methods: {
        changeSize(chartName) {
          console.log(chartName);
          // 将数据发送给服务端
          const targetValue = !this.fullScreenStatus[chartName];
          this.$socket.send({
            action: "fullScreen",
            socketType: "fullScreen",
            chartName: chartName,
            value: targetValue,
          });
        },
    }
}
复制代码
  • created时注册回调函数
export default {
    created() {
        // 注册接收到数据的回调函数
        this.$socket.registerCallBack("fullScreen", this.recvData);
        this.$socket.registerCallBack("themeChange", this.recvThemeChange);
    }
}
复制代码
  • destoryed时取消回调函数
export default {
    destroyed() {
        this.$socket.unRegisterCallBack("fullScreen");
        this.$socket.unRegisterCallBack("themeChange");
        clearInterval(this.timer);
    }
}
复制代码
  • 得到数据的处理
export default {
    methods: {
        // 接收到全屏数据之后的处理
        recvData(data) {
          // 取出是哪一个图表需要进行切换
          const chartName = data.chartName;
          // 取出, 切换成什么状态
          const targetValue = data.value;
          this.fullScreenStatus[chartName] = targetValue;
          this.$nextTick(() => {
            this.$refs[chartName].screenAdapter();
          });
        }
    }
}
复制代码
  • socket_service.js 代码如下:
const action = recvData.action
if (action === 'getData') {
  const realData = JSON.parse(recvData.data);
  this.callBackMapping[socketType].call(this, realData);
} else if (action === 'fullScreen') {
  this.callBackMapping[socketType].call(this, recvData);
} else if (action === 'themeChange') {
  this.callBackMapping[socketType].call(this, recvData);
}
复制代码

4.3.6 主题切换

  • 当前主题数据的存储

当前主题的数据, 会在多个组件中使用, 因此设置在 VueX 中是最合适的, 增加仓库数据 theme , 并增加一个 mutation 用来修改 theme

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    theme: 'dark'
  },
  mutations: {
    changeTheme (state) {
      if (state.theme === 'dark') {
        state.theme = 'default';
      } else {
        state.theme = 'dark';
      }
    }
  },
  actions: {
  },
  modules: {
  }
})
复制代码
  • 点击切换主题按钮

点击事件的响应

<template>
    <div class="title-right">
        <img :src="themeSrc" class="qiehuan" @click="handleChangeTheme" />
        <span class="datetime">{{ timeValue }}</span>
    </div>
</template>
复制代码

点击事件的处理

export default {
    methods: {
       handleChangeTheme() {
          // 修改VueX中数据
          this.$socket.send({
            action: "themeChange",
            socketType: "themeChange",
            chartName: "",
            value: "",
          });
        }  
    }
}
复制代码
  • 监听主题的变化

以 Seller.vue 为例, 进行主题数据变化的监听

映射 store 中的 theme 作为当前组件的计算属性

import { mapState } from 'vuex' 
export default { 
    computed: { 
        ...mapState(['theme']);
    } 
}
复制代码

监听theme的变化

export default { 
    watch: { 
        theme () { 
            this.myChart.dispose(); // 销毁当前的图表
            this.initChart(); // 重新以最新的主题名称初始化图表对象
            this.screenAdapter(); // 完成屏幕适配
            this.updateChart(); // 更新图表展示
        } 
    } 
}
复制代码

主题的切换

export default { 
    methods: {
        // 初始化echartsInstance对象
        initChart() {
          this.myChart = this.$echarts.init(this.$refs.seller_ref, this.theme);
        }
    }   
}
复制代码

通过这个步骤就可以实现每一个图表组件切换主题了,不过有部分样式需要另外调整

  • 主题样式适配

创建utils/theme_utils.js文件

定义两个主题下, 需要进行样式切换的样式数据, 并对外导出一个函数, 用于方便的通过主题名称得到对应主题的某些配置项

const theme = {
  dark: {
    // 背景颜色
    backgroundColor: '#3f3f46',
    // 图表背景色
    bgColor: '#100c2a',
    // label文字颜色
    labelColor: '#fff',
    // 标题的文字颜色
    titleColor: '#fff',
    // 左上角logo的图标路径
    logoSrc: 'logo_dark.png',
    // 切换主题按钮的图片路径
    themeSrc: 'qiehuan_dark.png',
    // 页面顶部的边框图片
    headerBorderSrc: 'header_border_dark.png'

  },
  default: {
    // 背景颜色
    backgroundColor: '#eee',
    // 图表背景色
    bgColor: '#fff',
    // label文字颜色
    labelColor: '#100c2a',
    // 标题的文字颜色
    titleColor: '#000',
    // 左上角logo的图标路径
    logoSrc: 'logo_light.png',
    // 切换主题按钮的图片路径
    themeSrc: 'qiehuan_light.png',
    // 页面顶部的边框图片
    headerBorderSrc: 'header_border_light.png'
  }
}

export function getThemeValue (themeName) {
  return theme[themeName]
}
复制代码

映射 VueX 中的 theme 数据作为该组件的计算属性

// screenPage.vue
import { mapState } from 'vuex' 
export default { 
computed: { 
    ...mapState(['theme']) 
}
复制代码

定义一些控制样式的计算属性

// screenPage.vue
import { mapState } from "vuex";
import { getThemeValue } from "@/utils/theme_utils";
export default {
    computed: {
        logoSrc() {
          return "/static/img/" + getThemeValue(this.theme).logoSrc;
        },
        headerSrc() {
          return "/static/img/" + getThemeValue(this.theme).headerBorderSrc;
        },
        themeSrc() {
          return "/static/img/" + getThemeValue(this.theme).themeSrc;
        },
        containerStyle() {
          return {
            backgroundColor: getThemeValue(this.theme).backgroundColor,
            color: getThemeValue(this.theme).titleColor,
          };
        }
     }
  },
}
复制代码

将计算属性应用到布局中

<template>
    <div class="screen-container" :style="containerStyle">
        <header class="screen-header">
          <div>
            <img :src="headerSrc" alt="" />
          </div>
          <span class="logo">
            <img :src="logoSrc" alt="" />
          </span>
          <span class="title">电商平台数据大屏实时监控系统</span>
          <div class="title-right">
            <img :src="themeSrc" class="qiehuan" @click="handleChangeTheme" />
            <span class="datetime">{{ timeValue }}</span>
          </div>
        </header>
    </div>
</template>
复制代码

通过计算属性动态控制标题样式及下拉框选项

// trend.vue
import { mapState } from "vuex";
import { getThemeValue } from "@/utils/theme_utils";
export default {
    ...mapState(["theme"]),
    selectTypes() {
      if (!this.allData) {
        return [];
      } else {
        return this.allData.type.filter((item) => {
          return item.key !== this.choiceType;
        });
      }
    },
    showTitle() {
      if (!this.allData) {
        return "";
      } else {
        return this.allData[this.choiceType].title;
      }
    },
    // 设置给标题的样式
    comStyle() {
      return {
        fontSize: this.titleFontSize + "px",
        color: getThemeValue(this.theme).labelColor
      };
    },
    marginStyle() {
      return {
        marginLeft: this.titleFontSize + "px",
        backgroundColor: getThemeValue(this.theme).bgColor
      };
    },
}
复制代码

5. 写在最后

  • 升级Echarts新版本
  • 快速掌握KOA2后端框架开发API
  • 代码简洁优化及功能完善
  • Axios和WebSocket两种通信方式讲解
  • 适合进阶数据可视化的练手项目

如果对你有些许帮助,可以点赞、评论、转发分享,也是对我的一种支持,万分感谢。如需获取更多实战项目经验或源码资源,请关注我的公众号:「懒人码农」,也可以加我微信【lazycode520】,一起学习一起进步。

查看完整源代码,请移步到github访问👉:github.com/jackchen012…

参考资料:www.bilibili.com/video/BV1Uz…

文章分类
前端
文章标签