全新 Echarts 电商平台数据可视化大屏全栈

1. 前言

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

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

4.说明

4.1 前后端分离

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

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

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

4.2 后端部分

4.2.1 Koa2 的介绍

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

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

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

  • 洋葱模型的中间件

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

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

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

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

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

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

安装成功执行以下命令:

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,如下图:

  • 全局 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 商家销量统计

最终效果如下图所示:

组件结构设计

在 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

初始化配置项

拥有数据之后的配置项

分页动画实现

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

数据的处理

动画的启动和停止

鼠标事件的处理

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 销量趋势分析

最终效果如下图所示:

  • 代码环境准备
// 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><spanclass="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 商家地图分布

最终效果如下图所示:

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

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

4.3.2.4 地区销量排行

最终效果如下图所示:

4.3.2.5 热销商品占比

最终效果如下图所示:

4.3.2.6 库存销量分析

最终效果如下图所示:

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.chartNamefilePath = 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,stockdestroyed () { 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.actionif (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"><divid="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><divid="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"><divid="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><divid="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><divid="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 两种通信方式讲解
  • 适合进阶数据可视化的练手项目

♻️ 资源

大小: 1.94MB
➡️ 资源下载:https://download.csdn.net/download/s1t16/87379043

基于JavaScript+Koa2实现 Echarts 电商平台数据可视化大屏全栈【100010415】相关推荐

  1. 电商平台数据可视化Echarts-Vue项目综合练习(黑马pink老师)学习记录

    放假在家没事,跟着b站黑马前端课程手把手做了个电商平台数据可视化实时监控系统.老师课讲得非常好,几乎是保姆级别,对我这种小白非常友好.在这里记录一些自己遇到的问题,欢迎批评指正. 问题记录 1.ECh ...

  2. 电商平台数据可视化如何实现

    一.什么是API? API,即Application Programming Interface,翻译过来就是"应用程序编程接口".它是用于不同软件和应用之间进行交互的一种技术规范 ...

  3. 「ECharts」电商平台数据可视化实时监控系统之后台开发

    此项目后台采用 Koa2 进行开发配置,相关配置整理如下. 1. Koa2 概述 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领 ...

  4. 电商平台数据可视化实时监控系统(开发目录)

    目录 项目地址github 1.koa2快速上手 2.后台项目初步 3.前端项目的创建和准备 4.单独图表组件的开发---商家销售统计(横向柱状图) 5.单独图表组件的开发---销量趋势图表(折线图) ...

  5. 基于阿米巴经营模式的电商平台研发

    7月份是一个崭新开始,着手基于阿米巴经营模式的电商平台研发项目,运行多年的混沌状态接口不清晰/能力不分明的牵一发而动全身的旧业务系统,终于迎来了可负载均衡的三层设计的平台重构,前端有To C的运营中心 ...

  6. 电商平台数据解锁网红零食销量密码

    " 你知道"巨型猪饲料""单身狗粮"是什么吗?这不是给动物吃的,也许你或多或少听说过,这些在网上引起巨大反响的零食,完全激起了大家的购买欲望. &qu ...

  7. 电商平台数据仓库搭建01-项目介绍

    1,项目说明 本项目来源于github 电商平台数据仓库搭建 .该项目仅供个学习使用 项目为个人学习记录,项目代码及文件可访问 电商平台数据仓库搭建 获得.访问不了的同学也可以私信我. 2,项目流程设 ...

  8. 电商平台数据仓库搭建02-Hadoop集群搭建

    1,项目说明 本项目来源于github 电商平台数据仓库搭建 . 项目为个人学习记录,项目代码及文件可访问 电商平台数据仓库搭建 获得. 2,项目准备 虚拟机准备 虚拟机开发工具为 VMware15. ...

  9. 电商数据监测:如何获取想要的电商平台数据?

    随着电商行业的发展,越来越多的企业开始通过电商平台销售商品.为了更好地掌握市场信息和消费者需求,企业需要获取电商平台上的数据.这些数据可以帮助企业制定营销策略.优化产品设计和提高竞争力.本文将介绍如何 ...

最新文章

  1. div 自动换行_js自动打字--autotypejs
  2. Rust 语言风靡学术界
  3. __getitem__的作用
  4. SAP 创业计划 ---之三
  5. 使用头文件的原因和规范
  6. 计算机信息的编码教案,信息的编码教案信息的编码教案.doc
  7. hihoCoder挑战赛14 -1223
  8. Flex 3快速入门: 构建高级用户界面 添加拖放支持
  9. C和指针 第十六章 标准函数库 本地跳转setjmp.h
  10. 学习笔记-Rabin-Karp哈希
  11. Centos 6 编译安装 Apache 2.4
  12. GBK编码转换及Md5算法工具
  13. 做自媒体1年投资4百W亏损370W,自媒体的水太深
  14. 2022年美赛e题资料(森林固碳)
  15. 解决英伟达CUDA和cuDNN下载过慢的问题
  16. 7 Hive数据仓库
  17. 给排水计算机应用参考文献,给排水专业专著参考文献 给排水专业外文文献怎么找...
  18. 【云原生 · Kubernetes】Kubernetes基础环境搭建
  19. 雅虎通——从怀念我的雅虎说起
  20. windows mobile ?

热门文章

  1. 【网管必备技巧:如何跟踪IP地址】
  2. 训练误差和泛化误差分别是什么,如何区分?
  3. html延时属性css,CSS属性参考 | transition-delay
  4. 什么NAS能实现多平台同步?AirDisk-Q3X/Q2/Q3S微力同步告诉你怎么实现
  5. CSS属性书写顺序规范
  6. ICLR 2021 | GSL:通过可控的解耦表征学习模拟人脑想象力
  7. 2017中国(海南)智慧城市创新大会举行
  8. TurboVNC启用一次性密码TOTP
  9. Chrome/Edge/Firefox浏览器离线安装包下载地址总汇
  10. Java:Lambda简化匿名内部类