一个基于 Vue、Datav、Echart 框架的 " 数据大屏项目 ",通过 Vue 组件实现数据动态刷新渲染,内部图表可实现自由替换。部分图表使用 DataV 自带组件, 组件库基于Vue (React版) ,主要用于构建大屏(全屏)数据展示页面即数据可视化,具有多种类型组件可供使用。
项目环境:Vue-cli、DataV、Echarts、node
友情链接:
项目展示
文件 | 作用/功能 |
---|---|
main.js | 主目录文件,引入 Echart/DataV 等文件 |
utils | 工具函数与 mixins 函数等 |
components/ HomeWord.vue | 项目主结构 |
assets | 静态资源目录,放置 logo 与背景图片 |
引用的模块(先下载 install)
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import * as echarts from 'echarts' //echarts 引用使用断言,否则可能报错
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import dataV from '@jiaminghi/data-view'
Vue.use(ElementUI); //部分图标使用 element-ui
Vue.use(dataV) //全局启用 dataV
Vue.prototype.$echarts=echarts //将echarts挂载到Vue原型上,全局可使用this.$echarts 调用
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
所有的 ECharts 图表已经对数据和屏幕改动进行了监听,能够动态渲染图表数据。在监听窗口小大的模块。
中间部分中国地图json数据来源 (地址)
我这边直接复制内容存到本地 (把复制的链接在网页打开Ctrl+A Ctrl+C 全选复制粘贴): assets > json > china.json
渲染地图的子组件
有几个注意点:
<template>
<div
id="map"
:style="{
width: '700px',
height: '730px',
marginTop: '10px',
}"
>div>
template>
<script>
import china from "../assets/json/china.json"; //引入地图json文件
export default {
mounted() {
this.initChart();
},
props: {
mapData: { //接收父组件传过来的值
type: Object,
default: () => ({}),
},
},
data() {
return {
options: {},
chart: null,
};
methods: {
// 地图
initChart() {
this.$echarts.registerMap("china", china); //echarts的map需要注册,根据引入的json文件名,并定义map的名称
this.chart = this.$echarts.init(document.getElementById("map"), null, {
renderer: "svg",
}); //init()挂载在某个元素,所以还需要在mounted调用, { renderer: "svg" } 将原来的canvas绘图改为svg 清晰度更高
this.drawMap();
},
drawMap() {
this.chart.setOption(
{
title: {
text: "平台运营实时数据",
textStyle: {
color: "#fff",
fontSize: 28,
},
},
tooltip: { //鼠标移入的提示信息框
show: true,
trigger: "item",
formatter: function (a, b) {
// 将人数改千分位
let a2 = "";
let olda = Number(a["data"].value);
if (olda >= 0) {
a2 = olda;
if (olda > 999) {
let parts = olda.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
a2 = parts.join(".");
}
}
// 日活兼容
let a3 = a["data"].ratio ? a["data"].ratio : 0;
let a1 = a["name"];
if (a1 == "台湾省" && !a2) { // 台湾省显示暂无数据
return `${a1}
暂无数据`;
}
return `${a1}
累计注册: ${a2}
日活: ${a3} %`;
},
},
series: [
{
type: "map",
map: "china", //对应registerMap() 对应的名称
top: "150", //调整地图在页面的位置
zoom: 1.3, //缩放比例
emphasis: {
label: { show: false },
itemStyle: {
areaColor: "rgba(136, 132, 216)",
},
},
itemStyle: {
borderColor: "#fff",
},
data: this.mapData.mapArr, //有数据才显示颜色
},
],
visualMap: {
show: true,
type: "continuous",
calculable: true,
orient: "horizontal",
textStyle: {
color: "#fff",
},
min: 0,
max: 10000000,
text: ["累计注册/人", ""],
color: ["#df3d20", "#fff"],
inRange: {
// color: [ "#fff","#44effb", "#3399ff","#2b8afe", "#006699"],
color: ["#44effb", "#3399ff", "#2b8afe", "#006699"], //地图颜色
},
},
},
true
);
},
},
watch: {
// handler 监听数据发生变化需要具体执行的方法
// deep 需要监听的数据的深度,一般用来监听对象中某个属性的变化
mapData: {
handler() {
this.drawMap();
},
deep: true,
},
},
};
script>
<style>style>
边框是使用了 DataV 自带的组件,只需要去 views 目录下去寻找对应的位置去查找并替换就可以,具体的种类请去 DavaV 官网查看
如:
<dv-border-box-1> 内容撑开 dv-border-box-1>
<dv-border-box-2> 内容撑开 dv-border-box-2>
<dv-border-box-3> 内容撑开 dv-border-box-3>
使用 mixins 注入解决了界面大小变动图表自适应适配的功能,函数在 utils/resizeMixins.js
中,应用在 common/echart/index.vue
的封装渲染组件,主要是对 this.chart
进行了功能注入。
使用更流程通用的 css3:scale
缩放方案,通过 ref
指向 主页面的元素,基准尺寸是 1980px*1080px
,所以支持同比例屏幕 100% 填充,如果非同比例则会自动计算比例居中填充,不足的部分则留白。实现代码在 `src/utils/drawMixin ,如果有其它的适配方案,欢迎交流。
src > utils > drawMixin.js
// 屏幕适配 mixin 函数
//需要先设置index.html meta 标签 user-scalable=no
//
//并且需要绑定ref
// * 默认缩放值
const scale = {
width: '1',
height: '1',
}
// * 设计稿尺寸(px)
// 1920×1080
const baseWidth =1920
const baseHeight = 1080
// * 需保持的比例(默认1.77778)
const baseProportion = parseFloat((baseWidth / baseHeight).toFixed(5))
export default {
data() {
return {
// * 定时函数
drawTiming: null
}
},
mounted () {
this.calcRate()
window.addEventListener('resize', this.resize)
},
beforeDestroy () {
window.removeEventListener('resize', this.resize)
},
methods: {
calcRate () {
const appRef = this.$refs["appRef"]
if (!appRef) return
// 当前宽高比
const currentRate = parseFloat((window.innerWidth / window.innerHeight).toFixed(5))
if (appRef) {
if (currentRate > baseProportion) {
// 表示更宽
scale.width = ((window.innerHeight * baseProportion) / baseWidth).toFixed(5)
scale.height = (window.innerHeight / baseHeight).toFixed(5)
appRef.style.transform = `scale(${scale.width}, ${scale.height}) translate(-50%, -50%)`
} else {
// 表示更高
scale.height = ((window.innerWidth / baseProportion) / baseHeight).toFixed(5)
scale.width = (window.innerWidth / baseWidth).toFixed(5)
appRef.style.transform = `scale(${scale.width}, ${scale.height}) translate(-50%, -50%)`
}
}
},
resize () {
clearTimeout(this.drawTiming)
this.drawTiming = setTimeout(() => {
this.calcRate()
}, 200)
}
},
}
homeword.vue
<template>
<div id="index" ref="appRef">
...页面内容...
div>
template>
<script>
import drawMixin from "../utils/drawMixin";
export default {
components: {
//.....
},
mixins: [drawMixin], //混入 (保持页面缩放比例)
}
script>
使用 axios
进行数据请求,在 main.js
位置进行全局配置。
src > request
文件统一处理所有的请求
src > request > request.js
请求和响应拦截
// 封装axios实例的拦截器(请求, 响应)
import axios from 'axios';
// 1. 创建axios实例
const instance = axios.create({
timeout: 15000, // 超时时间15s
baseURL: '这里是你请求是ip地址', // ip+端口, 公用的前缀路径
});
// 重写实例请求前拦截器
instance.interceptors.request.use((config) => {
return config;
}, (err) => {
return Promise.reject(err);
})
// 重写实例响应后拦截器
instance.interceptors.response.use((result) => {
return result.data;
}, (err) => {
return Promise.reject(err);
})
// 导出axios实例
export default instance;
src > request > request.js
请求拼接地址
import request from './request';
// 主要指标 /mcpbd-data/data/mainIndex
export const getMainIndex= () => request.get('/mainIndex') //这里是get请求,"/mainIndex" 是接口文档的请求拼接字段
//......
在homeword.vue
引入,并发送请求数据
由于一个页面要发送多个请求,并且成功获取数据再渲染,我使用了promise.all
并发请求,但是发现请求很慢(不知道有什么方法可以优化)
<template>
...
template>
<script>
import drawMixin from "../utils/drawMixin";
//接口
import {
getMainIndex,
} from "../request/httpApi";
export default {
components: {
//...
},
mixins: [drawMixin],
name: "HelloWorld",
props: {},
data() {
return {
//渲染子组件,还未请求到数据的时候,需要放数据的所有元素不显示
falg: false,
loading: true,
loadTimer: null,
resd: [],
realVal: 0,
MainIndicators: {
// 主要指标 默认数据
userAllCnt: "",
userNewCnt: "",
userDailyActvCnt: "",
userDailyActvRatio: "",
userMonthlyActvCnt: "",
userMonthlyActvRatio: "",
mctAllCnt: "",
},
//累计注册用户数折线
//....
mapData: {
//地图数据
mapArr: [],
},
};
},
mounted() {
this.cancelLoading();
},
filters: {
//过滤数据
numFilter(val) {
return (parseFloat(val) * 100).toFixed(1);
},
},
created() {
this.getJ();
this.getShishi();
// 实时更新数据(隔一个小时请求数据)
setInterval(() => {
this.getJ();
}, 3600000);
//左下角数据实时更新(1分钟)
setInterval(() => {
this.getShishi();
}, 60000);
},
methods: {
getShishi() {
//左下角数据 (因为这部分数据需要每分钟更新一次所以单独拎出来)
getRealTimeIndex() //发送请求
.then((res) => {
this.RealTimeIndex.newUserAllUserCnt = [];
this.RealTimeIndex.newUserAllUserDate = [];
this.resd = res;
this.resd.forEach((item) => {
this.RealTimeIndex.newUserAllUserCnt.push(
Number(item.userAllCnt).toFixed()
),
this.RealTimeIndex.newUserAllUserDate.push(
item.calTime.substring(11)
);
});
})
.catch((err) => {
return;
});
},
sortData(attr) {
return function (a, b) {
return b[attr] - a[attr];
};
},
getJ() {
// 累计注册用户
let p1 = new Promise((resolve, reject) => {
getUserAllCnt()
.then((res) => {
resolve(res);
})
.catch((err) => {
reject(err);
});
});
// 用户
let p2 = new Promise((resolve, reject) => {
//....
});
// 累计用户数
let p3 = new Promise((resolve, reject) => {
//....
});
// 日活用户数
let p4 = new Promise((resolve, reject) => {
//....
});
// 新增用户数
let p5 = new Promise((resolve, reject) => {
//....
});
// 指标排行表
let p6 = new Promise((resolve, reject) => {
//....
});
// 主要指标
let p7 = new Promise((resolve, reject) => {
getMainIndex()
.then((res) => {
setTimeout(() => {
resolve(res);
}, 700);
})
.catch((err) => {
reject(err);
});
});
// 地图数据省份
let p8 = new Promise((resolve, reject) => {
//....
});
Promise.all([p1, p2, p3, p4, p5, p6, p7, p8])
.then((res) => {
// 累计注册用户
//数据处理。。。
// 日活
//数据处理。。。
// 累计用户数 (只展示前5条数据)
//数据处理。。。
// 日活用户数
//数据处理。。。
// 新增用户数
//数据处理。。。
// 指标排行表
//数据处理。。。
// 主要指标
let mainIndex = res[6];
this.MainIndicators.userAllCnt = Number(
mainIndex.userAllCnt
).toLocaleString("en-US"); //使用千分符
this.MainIndicators.userNewCnt = Number(
mainIndex.userNewCnt
).toLocaleString("en-US");
this.MainIndicators.userDailyActvCnt = Number(
mainIndex.userDailyActvCnt
).toLocaleString("en-US");
this.MainIndicators.userDailyActvRatio = mainIndex.userDailyActvRatio;
this.MainIndicators.userMonthlyActvCnt = Number(
mainIndex.userMonthlyActvCnt
).toLocaleString("en-US");
this.MainIndicators.userMonthlyActvRatio =
mainIndex.userMonthlyActvRatio;
this.MainIndicators.mctAllCnt = Number(
mainIndex.mctAllCnt
).toLocaleString("en-US");
// 地图省份指标
let FenUserMap = res[7];
this.mapData.mapArr = FenUserMap.map((item) => ({
name: item.regionName,
value: Number(item.userAllCnt).toFixed(),
ratio: (Number(item.userActvDailyRatio) * 100).toFixed(1),
}));
//由于暂无数据隐藏南海诸岛,不太好,后面想到既然可以隐藏也可以将提示信息改成暂无数据
this.mapData.mapArr.push({
name: "南海诸岛",
value: 0,
itemStyle: { opacity: 0, label: { show: false } },
});
this.falg = true; //子组件渲染
})
.catch((err) => {});
},
cancelLoading() {
if (!this.loadTimer) {
this.loadTimer = setTimeout(() => {
this.loading = false;
}, 500);
} else {
clearTimeout(this.loadTimer);
}
},
},
destroyed() {},
};
script>