一、效果展示
二、简介
该项目涉及到的图表有:
- 渐变堆叠面积图
- 中国地图
- 涟漪特效散点图
- 饼图
- 横向柱状图
- 竖向柱状图
- 圆环饼图
该项目主要展示的是使用Echarts制作的大屏可视化,所用到的技术有:
2.1 前端:
vue3、vite、echarts、pinia、sass、websocket、vue-router
2.2 后端:
nodejs、koa、mysql、websocket、cors
2.3 功能描述:
该项目是使用websocket实现数据的实时推送,每一个图表就是一个单独的组件,该组件可以进行全屏和取消全屏的操作,可以实现多个浏览器同时访问http://localhost:5173/screen的时候一个人的操作所有人都可以看见,可以实现全局图表主题的切换,主题的切换也是可以实现多个浏览器之间联动的。并且随着浏览器窗口的变化,所有的图表都可以自动适配屏幕的大小。
2.3.1 商品销量趋势图表
点击图标的标题可以进行 商品销量趋势、地区销量趋势、商家销量趋势 三个图表之间的来回切换。
2.3.2 热销商品销售金额占比统计图表
点击该组件的左右箭头可以实现热销商品中 手机数码、美妆护肤、女装 三个图表之间的来回切换。
2.3.3 商家销售统计图表
该组件显示的图表并不是一次性将所有的商家数据进行展示,而是按照销售数量从小到大进行排序,然后每次先显示5条数据,每隔3s切换一次数据。
2.3.4 地区销售排行图表
该组件显示的图表并不是一次性将所有的地区数据进行展示,而是按照销售数量从大到小进行排序,然后每次先显示10条数据,每隔2s将数据向左移动一个。
三、代码展示
3.1 热销商品销售金额占比统计表
<template>
<div class="com-container">
<div
class="com-chart"
ref="sellerRef"
></div>
<!-- 左右按钮 -->
<div class="icon-group">
<span
class="iconfont"
:style="{ fontSize: fontSize + 'px', color: theme.color }"
@click="handleLeft"
></span>
<span
class="iconfont"
:style="{ fontSize: fontSize + 'px', color: theme.color }"
@click="handleRight"
></span>
</div>
<div
class="title"
:style="{ fontSize: fontSize / 2 + 'px', color: theme.color }"
>{{ currentType.name }}</div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue'
import api from '@/utils/url'
import { get } from '@/utils/request'
import * as echarts from 'echarts'
// 引入仓库
import { useChart } from '@/store'
import { getThemeValue } from '@/utils/theme'
import Socket from '@/utils/socket'
const theme = computed(() => getThemeValue(store.theme))
const store = useChart()
const sellerRef = ref(null)//图表容器
const chartInstance = shallowRef(null)//图表实例
const currentType = ref({})
const fontSize = ref()
const handleLeft = () => {
const index = allData.value.findIndex(item => item.name === currentType.value.name)
if (index - 1 >= 0) {
currentType.value = allData.value[index - 1]
} else {
currentType.value = allData.value[allData.value.length - 1]
}
updateChart()
}
const handleRight = () => {
const index = allData.value.findIndex(item => item.name === currentType.value.name)
if (index + 1 <= allData.value.length - 1) {
currentType.value = allData.value[index + 1]
} else {
currentType.value = allData.value[0]
}
updateChart()
}
// 监听主题的切换
watch(() => store.theme, () => {
// 一旦主题切换了就将原来的图表销毁掉
chartInstance.value.dispose()
initChart()// 重新以最新的主题进行初始化图表
screenAdapter()//完成屏幕适配
updateChart()//更新图表
})
// 初始化柱状图实例化对象
const initChart = () => {
chartInstance.value = echarts.init(sellerRef.value, store.theme)
// 对图标初始化的配置
const initOption = {
// 标题配置
title: {
text: '▎热销商品销售金额占比统计',
left: 20,//设置标题距离左边的位置
top: 20//设置标题距离上边的位置
},
// 提示框配置
tooltip: {
trigger: 'item',// 鼠标移入到坐标轴的时候触发提示框
formatter: (val) => {
let total = 0
// 三级分类
const thirdCategory = val.data.children
// 计算出所有三级分类的数值总和
thirdCategory.map(item => {
total += item.value
})
let str = ''
thirdCategory.forEach(item => {
str += `${item.name}: ${(item.value / total * 100).toFixed()}%<br>`
})
return str
}
},
// 图例配置
legend: {
top: '15%',
icon: 'circle',//图标类型
},
// 图表类型
series: [
{
type: 'pie',
label: {
show: false,//是否显示标签
},
emphasis: {//鼠标移入到饼图上的时候
label: {
show: true
},
labelLine: {//标签线
show: false//折线
}
}
}
]
}
chartInstance.value.setOption(initOption)
}
const allData = ref([])
// 获取服务器数据
const getData = async (res) => {
// const res = await get(api.hot)
allData.value = res
currentType.value = res[0]
updateChart()
}
// 在组件创建完成之后,进行回调函数的注册
Socket.registerCallback('hotData', getData)
// 更新图表
const updateChart = () => {
const { children, name } = currentType.value
// 图表配置项
const option = {
// 图例配置
legend: {
data: children.map(item => item.name),
},
// 图表类型
series: [
{
data: children.map(item => (
{
name: item.name,
value: item.value,
children: item.children
}
)),
}
]
}
chartInstance.value.setOption(option)
}
// 屏幕适配【当浏览器的大小发生变化时,来完成屏幕的适配】
const screenAdapter = () => {
const titleFontSize = sellerRef.value.offsetWidth / 100 * 3.6
fontSize.value = titleFontSize * 2
// 和分辨率大小相关的配置项
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
},
},
legend: {
// 设置图标的大小和文字大小
itemWidth: titleFontSize,
itemHeight: titleFontSize,
textStyle: {
fontSize: titleFontSize
},
},
series: [
{
radius: titleFontSize * 4.5,// 设置饼图的半径大小
center: ['50%', '60%'],// 设置饼图的位置
}
]
}
chartInstance.value.setOption(adapterOption)
// 手动调用图标对象的resize方法,才能生效
chartInstance.value.resize()
}
// 将当前组件中的变量暴露出去,让父组件进行调用
defineExpose({ screenAdapter })
onMounted(() => {
initChart()
// getData()
// 发送数据给服务器,告诉服务器,我现在需要数据
Socket.send({
action: 'getData',
socketType: 'hotData',
chartName: 'hot',
value: ''
})
// 监听窗口大小变化事件
window.addEventListener('resize', screenAdapter)
// 在页面加载完成的时候,主动进行屏幕的适配
screenAdapter()
})
onBeforeUnmount(() => {
// 在页面加载完成的时候,主动进行屏幕的适配
window.removeEventListener('resize', screenAdapter)
// 在组件销毁的时候,进行回调函数的注销
Socket.unregisterCallback('hotData')
})
</script>
<style lang="scss" scoped>
.icon-group {
position: absolute;
left: 10px;
right: 10px;
top: 50%;
display: flex;
justify-content: space-between;
z-index: 1;
color: #fff;
.iconfont {
cursor: pointer;
}
}
.title {
position: absolute;
left: 80%;
bottom: 20px;
z-index: 1;
color: #fff;
}
</style>
3.2 商家分布表
<template>
<div
class="com-container"
@dblclick="revertChainMap"
>
<div
class="com-chart"
ref="sellerRef"
></div>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue'
import api from '@/utils/url'
import { get } from '@/utils/request'
import * as echarts from 'echarts'
import getMapData from '@/utils/getMapData'
// 引入仓库
import { useChart } from '@/store'
import Socket from '@/utils/socket'
const store = useChart()
const sellerRef = ref(null)//图表容器
const chartInstance = shallowRef(null)//图表实例
const mapData = ref({})//所获取的省份的矢量数据
// 监听主题的切换
watch(() => store.theme, () => {
// 一旦主题切换了就将原来的图表销毁掉
chartInstance.value.dispose()
initChart()// 重新以最新的主题进行初始化图表
screenAdapter()//完成屏幕适配
updateChart()//更新图表
})
// 初始化柱状图实例化对象
const initChart = async () => {
chartInstance.value = echarts.init(sellerRef.value, store.theme)
// 获取中国地图的矢量数据
const chinaMap = await getMapData('中国')
echarts.registerMap('china', chinaMap)
// 对图标初始化的配置
const initOption = {
// 标题配置
title: {
text: '▎商家分布',
left: 20,
top: 20
},
// 地图配置
geo: {
type: 'map',
map: 'china',
top: '5%',
bottom: '5%',
itemStyle: {
areaColor: '#2E72BF',//设置地图的背景颜色
borderColor: '#333',//设置地图的边框颜色【每个省份之间边界线的颜色】
}
},
legend: {
left: '5%',
bottom: '5%',
orient: 'vertical'//垂直显示
}
}
chartInstance.value.setOption(initOption)
// 对地图点击事件的监听
chartInstance.value.on('click', async params => {
if (!mapData.value[params.name]) {
// 获取对应省份的矢量数据
const county = await getMapData(params.name)
mapData.value[params.name] = county
// 注册地图的矢量数据
echarts.registerMap(params.name, county)
}
// 切换地图的显示
const changeOption = {
geo: {
map: params.name
}
}
chartInstance.value.setOption(changeOption)
})
}
const allData = ref([])
// 获取服务器数据
const getData = async (res) => {
// const res = await get(api.map)
allData.value = res
updateChart()
}// 在组件创建完成之后,进行回调函数的注册
Socket.registerCallback('mapData', getData)
// 更新图表
const updateChart = () => {
// 图表配置项
const option = {
// 图例
legend: {
data: allData.value.map(item => item.name)
},
// 图表类型
/*
返回的对象就代表的是一个类别下的所有散点数据
如果想在地图中显示散点的数据,所以需要给散点的图标增加一个配置,coordinateSystem: 'geo'
*/
series: allData.value.map(item => ({
type: 'effectScatter',//散点类型【涟漪】
rippleEffect: {//涟漪效果
scale: 5,//散点涟漪范围的大小
brushType: 'stroke',//涟漪效果的形状【stroke:空心,fill:实心】
},
name: item.name,
data: item.children,
coordinateSystem: 'geo',
})
),
}
chartInstance.value.setOption(option)
}
// 屏幕适配【当浏览器的大小发生变化时,来完成屏幕的适配】
const screenAdapter = () => {
const titleFontSize = sellerRef.value.offsetWidth / 100 * 3.6
// 和分辨率大小相关的配置项
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
}
},
legend: {
// 设置图标的大小和文字大小
itemWidth: titleFontSize / 2,
itemHeight: titleFontSize / 2,
itemGap: titleFontSize / 2,
textStyle: {
fontSize: titleFontSize / 2
},
}
}
chartInstance.value.setOption(adapterOption)
// 手动调用图标对象的resize方法,才能生效
chartInstance.value.resize()
}
// 将当前组件中的变量暴露出去,让父组件进行调用
defineExpose({ screenAdapter })
// 回到中国地图
const revertChainMap = () => {
chartInstance.value.setOption({
geo: {
map: 'china'
}
})
}
onMounted(() => {
initChart()
// getData()
// 发送数据给服务器,告诉服务器,我现在需要数据
Socket.send({
action: 'getData',
socketType: 'mapData',
chartName: 'map',
value: ''
})
// 监听窗口大小变化事件
window.addEventListener('resize', screenAdapter)
// 在页面加载完成的时候,主动进行屏幕的适配
screenAdapter()
})
onBeforeUnmount(() => {
// 在页面加载完成的时候,主动进行屏幕的适配
window.removeEventListener('resize', screenAdapter)
// 在组件销毁的时候,进行回调函数的注销
Socket.unregisterCallback('mapData')
})
</script>
3.3 地区销售排行表
<template>
<div class="com-container">
<div
class="com-chart"
ref="sellerRef"
></div>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue'
import api from '@/utils/url'
import { get } from '@/utils/request'
import * as echarts from 'echarts'
// 引入仓库
import { useChart } from '@/store'
import Socket from '@/utils/socket'
const store = useChart()
const sellerRef = ref(null)//图表容器
const chartInstance = shallowRef(null)//图表实例
// 监听主题的切换
watch(() => store.theme, () => {
// 一旦主题切换了就将原来的图表销毁掉
chartInstance.value.dispose()
initChart()// 重新以最新的主题进行初始化图表
screenAdapter()//完成屏幕适配
updateChart()//更新图表
})
// 初始化柱状图实例化对象
const initChart = () => {
chartInstance.value = echarts.init(sellerRef.value, store.theme)
// 对图标初始化的配置
const initOption = {
// 标题配置
title: {
text: '▎地区销售排行',
left: 20,//设置标题距离左边的位置
top: 20//设置标题距离上边的位置
},
// 坐标轴四周的边距
grid: {
top: "40%",
left: "5%",
right: "5%",
bottom: "5%",
containLabel: true//距离是否包含坐标轴刻度标签
},
// x轴配置
xAxis: { type: 'category', },
// y轴配置
yAxis: { type: 'value', },
// 图表类型
series: [
{
type: 'bar',
label: {
show: true,//是否显示数值
position: "top",//数值的显示位置
color: '#fff'//数值颜色
}
}
]
}
chartInstance.value.setOption(initOption)
// 鼠标移入图表,停止定时器
chartInstance.value.on('mouseover', () => {
clearInterval(timer.value)
})
// 鼠标移出图表,恢复定时器
chartInstance.value.on('mouseout', () => {
startIntervalue()
})
}
const allData = ref([])
// 获取服务器数据
const getData = async (res) => {
// const res = await get(api.rank)
// 对数据进行排序,从大到小
allData.value = res.sort((a, b) => b.value - a.value)
updateChart()
startIntervalue()
}
// 在组件创建完成之后,进行回调函数的注册
Socket.registerCallback('rankData', getData)
const startValue = ref(0)//缩略轴的起始值
const endValue = ref(9)//缩略轴的结束值
// 更新图表
const updateChart = () => {
const colorArr = [
["#0BA82C", "#4FF778"],
["#2E72BF", "#23E5E5"],
["#5052EE", "#AB6EE5"]
]
const data = allData.value
// 图表配置项
const option = {
// x轴配置
xAxis: { data: data.map(item => item.name) },
dataZoom: {
show: false,//是否显示缩略轴
startValue: startValue.value,//缩略轴的起始值
endValue: endValue.value//缩略轴的结束值
},
// 图表类型
series: [
{
data: data.map(item => item.value),
itemStyle: {
color: (params) => {
let color = ''
if (params.data > 300) {
color = colorArr[0]
} else if (params.data > 200) {
color = colorArr[1]
} else {
color = colorArr[2]
}
// 从上到下渐变
return new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: color[0] },//0%处的颜色
{ offset: 1, color: color[1] }//100%处的颜色
])
}
}
}
]
}
chartInstance.value.setOption(option)
}
// 屏幕适配【当浏览器的大小发生变化时,来完成屏幕的适配】
const screenAdapter = () => {
const titleFontSize = sellerRef.value.offsetWidth / 100 * 3.6
// 和分辨率大小相关的配置项
const adapterOption = {
title: {
textStyle: { fontSize: titleFontSize },
},
tooltip: {
axisPointer: {
lineStyle: { width: titleFontSize }
}
},
series: [
{
barWidth: titleFontSize,
itemStyle: {//柱状图样式
borderRadius: [titleFontSize / 2, titleFontSize / 2, 0, 0],//柱状图圆角
}
}
]
}
chartInstance.value.setOption(adapterOption)
// 手动调用图标对象的resize方法,才能生效
chartInstance.value.resize()
}
// 将当前组件中的变量暴露出去,让父组件进行调用
defineExpose({ screenAdapter })
const timer = ref(null)
const startIntervalue = () => {
timer.value && clearInterval(timer.value)
timer.value = setInterval(() => {
startValue.value++
endValue.value++
if (endValue.value > allData.value.length - 1) {
startValue.value = 0
endValue.value = 9
}
updateChart()
}, 2000)
}
onMounted(() => {
initChart()
// getData()
// 发送数据给服务器,告诉服务器,我现在需要数据
Socket.send({
action: 'getData',
socketType: 'rankData',
chartName: 'ranks',
value: ''
})
// 监听窗口大小变化事件
window.addEventListener('resize', screenAdapter)
// 在页面加载完成的时候,主动进行屏幕的适配
screenAdapter()
})
onBeforeUnmount(() => {
// 清除定时器
clearInterval(timer.value)
// 在页面加载完成的时候,主动进行屏幕的适配
window.removeEventListener('resize', screenAdapter)
Socket.unregisterCallback('rankData')
})
</script>
3.4 商家销量统计表
<template>
<div class="com-container">
<div
class="com-chart"
ref="sellerRef"
></div>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue'
import api from '@/utils/url'
import { get } from '@/utils/request'
import * as echarts from 'echarts'
// 引入仓库
import { useChart } from '@/store'
import Socket from '@/utils/socket'
const store = useChart()
const sellerRef = ref(null)//图表容器
const chartInstance = shallowRef(null)//图表实例
// 监听主题的切换
watch(() => store.theme, () => {
// 一旦主题切换了就将原来的图表销毁掉
chartInstance.value.dispose()
initChart()// 重新以最新的主题进行初始化图表
screenAdapter()//完成屏幕适配
updateChart()//更新图表
})
// 初始化柱状图实例化对象
const initChart = () => {
chartInstance.value = echarts.init(sellerRef.value, store.theme)
// 对图标初始化的配置
const initOption = {
// 标题配置
title: {
text: '▎商家销售统计',
left: 20,//设置标题距离左边的位置
top: 20//设置标题距离上边的位置
},
// 坐标轴四周的边距
grid: {
top: "20%",
left: "3%",
right: "6%",
bottom: "3%",
containLabel: true//距离是否包含坐标轴刻度标签
},
// x轴配置
xAxis: {
type: 'value',
},
// y轴配置
yAxis: {
type: 'category',
},
// 提示框配置
tooltip: {
trigger: 'axis',//鼠标移入到坐标轴的时候触发提示框
axisPointer: {//指示器样式
type: 'line',
z: 0,
lineStyle: {
type: 'solid',
color: "#2D3443"
}
}
},
// 图表类型
series: [
{
type: 'bar',
label: {
show: true,//是否显示数值
position: "right",//数值的显示位置
color: '#fff'//数值颜色
},
itemStyle: {//柱状图样式
/*
线性渐变:
指明颜色渐变的方向
指明不同百分比之下颜色的值
0,0,1,0:表示两个坐标(0,0)(1,0)【从左到右渐变】
*/
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#5052EE' },//0%处的颜色
{ offset: 1, color: '#AB6EE5' }//100%处的颜色
])
}
}
]
}
chartInstance.value.setOption(initOption)
// 对图表对象进行鼠标事件的监听,鼠标移入的时候停止更新,鼠标移除的时候恢复更新
chartInstance.value.on('mouseover', () => {
clearInterval(timer.value) // 将定时器进行取消
})
chartInstance.value.on('mouseout', () => {
startInterval()// 重新开启定时器
})
}
const allData = ref([])
const currentPage = ref(1)//当前页码
const totalPage = ref(0)//一共有多少页
// 获取服务器数据
const getData = async (res) => {
// const res = await get(api.seller)
// 对数据进行排序,从小到大
allData.value = res.sort((a, b) => a.value - b.value)
totalPage.value = Math.ceil(allData.value.length / 10)
updateChart()
// 启动定时器
startInterval()
}
// 在组件创建完成之后,进行回调函数的注册
Socket.registerCallback('sellerData', getData)
// 更新图表
const updateChart = () => {
const data = allData.value.slice((currentPage.value - 1) * 5, currentPage.value * 5)
// 图表配置项
const option = {
// y轴配置
yAxis: {
data: data.map(item => item.name)
},
// 图表类型
series: [
{
data: data.map(item => item.value),
}
]
}
chartInstance.value.setOption(option)
}
// 定时器id
const timer = ref(null)
const startInterval = () => {
// 先将之前的定时器清空之后再开启新的定时器
timer.value && clearInterval(timer.value)
timer.value = setInterval(() => {
currentPage.value++
if (currentPage.value > totalPage.value) {
currentPage.value = 1
}
updateChart()
}, 3000);
}
// 屏幕适配【当浏览器的大小发生变化时,来完成屏幕的适配】
const screenAdapter = () => {
const titleFontSize = sellerRef.value.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],//柱状图圆角
}
}
]
}
chartInstance.value.setOption(adapterOption)
// 手动调用图标对象的resize方法,才能生效
chartInstance.value.resize()
}
// 将当前组件中的变量暴露出去,让父组件进行调用
defineExpose({ screenAdapter })
onMounted(() => {
initChart()
// getData()
// 发送数据给服务器,告诉服务器,我现在需要数据
Socket.send({
action: 'getData',
socketType: 'sellerData',
chartName: 'seller',
value: ''
})
// 监听窗口大小变化事件
window.addEventListener('resize', screenAdapter)
// 在页面加载完成的时候,主动进行屏幕的适配
screenAdapter()
})
onBeforeUnmount(() => {
clearInterval(timer.value)
// 在页面加载完成的时候,主动进行屏幕的适配
window.removeEventListener('resize', screenAdapter)
Socket.unregisterCallback('sellerData')
})
</script>
3.5 库存和销量分析表
<template>
<div class="com-container">
<div
class="com-chart"
ref="sellerRef"
></div>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue'
import api from '@/utils/url'
import { get } from '@/utils/request'
import * as echarts from 'echarts'
// 引入仓库
import { useChart } from '@/store'
import Socket from '@/utils/socket'
const store = useChart()
const sellerRef = ref(null)//图表容器
const chartInstance = shallowRef(null)//图表实例
// 监听主题的切换
watch(() => store.theme, () => {
// 一旦主题切换了就将原来的图表销毁掉
chartInstance.value.dispose()
initChart()// 重新以最新的主题进行初始化图表
screenAdapter()//完成屏幕适配
updateChart()//更新图表
})
// 初始化柱状图实例化对象
const initChart = () => {
chartInstance.value = echarts.init(sellerRef.value, store.theme)
// 对图标初始化的配置
const initOption = {
// 标题配置
title: {
text: '▎库存和销量分析',
left: 20,//设置标题距离左边的位置
top: 20//设置标题距离上边的位置
},
}
chartInstance.value.setOption(initOption)
// 对图表对象进行鼠标事件的监听,鼠标移入的时候停止更新,鼠标移除的时候恢复更新
chartInstance.value.on('mouseover', () => {
clearInterval(timer.value) // 将定时器进行取消
})
chartInstance.value.on('mouseout', () => {
startInterval()// 重新开启定时器
})
}
const allData = ref([])
const currentPage = ref(1)//当前页码
const totalPage = ref(0)//一共有多少页
// 获取服务器数据
const getData = async (res) => {
// const res = await get(api.stock)
allData.value = res
totalPage.value = Math.ceil(allData.value.length / 5)
updateChart()
// 启动定时器
startInterval()
}
Socket.registerCallback('stockData', getData)
// 更新图表
const updateChart = () => {
const data = allData.value.slice((currentPage.value - 1) * 5, currentPage.value * 5)
const centerArr = [
['18%', '40%'],
['50%', '40%'],
['82%', '40%'],
['34%', '75%'],
['66%', '75%']
]
const colorArr = [
['#4FF778', '#0BA82C'],
['#E5DD45', '#E8B11C'],
['#E8821C', '#E55445'],
['#5052EE', '#AB6EE5'],
['#23E5E5', '#2E72BF']
]
// 图表配置项
const option = {
// 图表类型
series: data.map((item, index) => ({
type: 'pie',
center: centerArr[index],//饼图中心点的坐标
emphasis: { scale: false },//关闭鼠标移入时的动画效果
labelLine: {
show: false//隐藏指示线
},
label: {
position: 'center',
color: colorArr[index][0]
},
data: [
{
value: item.sales,
itemStyle: {
// 从下到上渐变
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: colorArr[index][0] },//0%处的颜色
{ offset: 1, color: colorArr[index][1] }//100%处的颜色
])
}
},
{
name: item.name + '\n\n' + item.sales,
value: item.stock,
itemStyle: {
color: '#333843'
}
}
]
}))
}
chartInstance.value.setOption(option)
}
// 定时器id
const timer = ref(null)
const startInterval = () => {
// 先将之前的定时器清空之后再开启新的定时器
timer.value && clearInterval(timer.value)
timer.value = setInterval(() => {
currentPage.value++
if (currentPage.value > totalPage.value) {
currentPage.value = 1
}
updateChart()
}, 5000);
}
// 屏幕适配【当浏览器的大小发生变化时,来完成屏幕的适配】
const screenAdapter = () => {
const data = allData.value.slice((currentPage.value - 1) * 5, currentPage.value * 5)
const titleFontSize = sellerRef.value.offsetWidth / 100 * 3.6
// 和分辨率大小相关的配置项
const adapterOption = {
title: {
textStyle: {
fontSize: titleFontSize
},
},
series: [
{
type: 'pie',//图表类型
radius: [titleFontSize * 2.8, titleFontSize * 3.15],//[内圆半径,外圆半径]
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',//图表类型
radius: [titleFontSize * 2.8, titleFontSize * 3.15],//[内圆半径,外圆半径]
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',//图表类型
radius: [titleFontSize * 2.8, titleFontSize * 3.15],//[内圆半径,外圆半径]
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',//图表类型
radius: [titleFontSize * 2.8, titleFontSize * 3.15],//[内圆半径,外圆半径]
label: {
fontSize: titleFontSize / 2
}
},
{
type: 'pie',//图表类型
radius: [titleFontSize * 2.8, titleFontSize * 3.15],//[内圆半径,外圆半径]
label: {
fontSize: titleFontSize / 2
}
}
]
}
chartInstance.value.setOption(adapterOption)
// 手动调用图标对象的resize方法,才能生效
chartInstance.value.resize()
}
// 将当前组件中的变量暴露出去,让父组件进行调用
defineExpose({ screenAdapter })
onMounted(() => {
initChart()
// getData()
// 发送数据给服务器,告诉服务器,我现在需要数据
Socket.send({
action: 'getData',
socketType: 'stockData',
chartName: 'stock',
value: ''
})
// 监听窗口大小变化事件
window.addEventListener('resize', screenAdapter)
// 在页面加载完成的时候,主动进行屏幕的适配
screenAdapter()
})
onBeforeUnmount(() => {
clearInterval(timer.value)
// 在页面加载完成的时候,主动进行屏幕的适配
window.removeEventListener('resize', screenAdapter)
Socket.unregisterCallback('stockData')
})
</script>
3.6 商品销量趋势表
<template>
<div class="com-container">
<div
class="title"
:style="{ fontSize: titleSize + 'px', color: theme.color, background: theme.color === '#000' ? '#fff' : '#222733' }"
>
<div
class="title-con"
@click="() => isShow = !isShow"
>
<span>▎{{ curTitle.text }}</span>
<span
class="iconfont"
:style="{ fontSize: titleSize + 'px' }"
></span>
</div>
<div
class="select-con"
v-show="isShow"
:style="{ marginLeft: ((titleSize / 3) * 2) + 'px' }"
>
<div
v-for="(item, index) in title"
:key=index
class="select-item"
@click="handleChangeTitle(item)"
>{{ item.text }}</div>
</div>
</div>
<div
class="com-chart"
ref="trendRef"
></div>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, watch, } from 'vue'
import api from '@/utils/url'
import { get } from '@/utils/request'
import * as echarts from 'echarts'
import Socket from '@/utils/socket'
// 引入仓库
import { useChart } from '@/store'
import { getThemeValue } from '@/utils/theme'
const store = useChart()
const isShow = ref(false)
const titleSize = ref()
const trendRef = ref(null)//图表容器
const chartInstance = shallowRef(null)//图表实例
const theme = computed(() => getThemeValue(store.theme))
// 监听主题的切换
watch(() => store.theme, () => {
// 一旦主题切换了就将原来的图表销毁掉
chartInstance.value.dispose()
initChart()// 重新以最新的主题进行初始化图表
screenAdapter()//完成屏幕适配
updateChart()//更新图表
})
// 初始化柱状图实例化对象
const initChart = () => {
chartInstance.value = echarts.init(trendRef.value, store.theme)
// 对图标初始化的配置
const initOption = {
// 坐标轴四周的边距
grid: {
top: "35%",
left: "3%",
right: "4%",
bottom: "1%",
containLabel: true//距离是否包含坐标轴刻度标签
},
// x轴配置
xAxis: {
type: 'category',
boundaryGap: false // 是否显示坐标轴两边的空白
},
// y轴配置
yAxis: {
type: 'value',
},
// 提示框配置
tooltip: {
trigger: 'axis',//鼠标移入到坐标轴的时候触发提示框
},
legend: {
left: 20,
top: '15%',
icon: 'circle'//图例形状
}
}
chartInstance.value.setOption(initOption)
}
const title = computed(() => {
if (!allData.value) {
return []
} else {
return allData.value.type.filter(item => item.key !== curTitle.value.key)
}
})
const curTitle = ref({})
// 切换标题
const handleChangeTitle = (item) => {
curTitle.value = item
isShow.value = false
updateChart()//切换标题之后更新数据
}
const allData = ref(null)
// 获取服务器数据
// res: 服务器返回的图表数据
const getData = async (res) => {
// const res = await get(api.trend)
// 对数据进行排序,从小到大
allData.value = res
curTitle.value = res.type[0]
updateChart()
}
// 在组件创建完成之后,进行回调函数的注册
Socket.registerCallback('trendData', getData)
// 更新图表
const updateChart = () => {
const { common, map, commodity, seller, type } = allData.value
const data = allData.value[curTitle.value.key].data
const colorArr1 = [
'rgba(11,168,44,0.5)',
'rgba(44,110,255,0.5)',
'rgba(22,242,217,0.5)',
'rgba(254,33,30,0.5)',
'rgba(250,105,0,0.5)',
]
const colorArr2 = [
'rgba(11,168,44,0)',
'rgba(44,110,255,0)',
'rgba(22,242,217,0)',
'rgba(254,33,30,0)',
'rgba(250,105,0,0)',
]
// 图表配置项
const option = {
xAxis: {
data: common.month//类目轴数据
},
yAxis: {
data: data,//y轴数据
},
// 图表类型
series: data.map((item, index) => {
// 将stack都设置为一样的值就可以变为折线堆叠图
return {
name: item.name,
type: 'line',
data: item.data,
stack: 'map',
areaStyle: {
/*
线性渐变:
指明颜色渐变的方向
指明不同百分比之下颜色的值
0,0,1,0:表示两个坐标(0,0)(0,1)【从上到下渐变】
*/
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: colorArr1[index] },//0%处的颜色
{ offset: 1, color: colorArr2[index] }//100%处的颜色
])
}
}
}),
legend: {
data: data.map(item => item.name),
}
}
chartInstance.value.setOption(option)
}
// 屏幕适配【当浏览器的大小发生变化时,来完成屏幕的适配】
const screenAdapter = () => {
const titleFontSize = trendRef.value.offsetWidth / 100 * 3.6
titleSize.value = titleFontSize
// 和分辨率大小相关的配置项
const adapterOption = {
legend: {
// 设置图标的大小和文字大小
itemWidth: titleFontSize,
itemHeight: titleFontSize,
textStyle: {
fontSize: titleFontSize
},
}
}
chartInstance.value.setOption(adapterOption)
// 手动调用图标对象的resize方法,才能生效
chartInstance.value.resize()
}
// 将当前组件中的变量暴露出去,让父组件进行调用
defineExpose({ screenAdapter })
onMounted(() => {
initChart()
// getData()
// 发送数据给服务器,告诉服务器,我现在需要数据
Socket.send({
action: 'getData',
socketType: 'trendData',
chartName: 'trend',
value: ''
})
// 监听窗口大小变化事件
window.addEventListener('resize', screenAdapter)
// 在页面加载完成的时候,主动进行屏幕的适配
screenAdapter()
})
onBeforeUnmount(() => {
// 在页面加载完成的时候,主动进行屏幕的适配
window.removeEventListener('resize', screenAdapter)
// 在组件销毁的时候,进行回调函数的注销
Socket.unregisterCallback('trendData')
})
</script>
<style lang="scss" scoped>
.title {
position: absolute;
left: 20px;
top: 20px;
z-index: 1;
color: #fff;
// background: #222733;
&-con {
cursor: pointer; //将鼠标移入时的指针改为小手
.iconfont {
margin-left: 10px;
}
}
}
</style>
3.7 展示页面
<template>
<div
class="screen-container"
:style="{ backgroundColor: theme.backgroundColor, color: theme.color }"
>
<header class="header">
<div>
<img :src="headerSrc">
</div>
<span class="title">电商平台实时监控系统</span>
<div class="title-right">
<img
:src="themeSrc"
class="qiehuan"
@click="handleTheme"
>
<span class="datetime">2424-12-21 15:32:00</span>
</div>
</header>
<div class="body">
<section class="left">
<div
class="top"
:class="{ fullscreen: fullScreenStatus.trend }"
>
<!-- 销量趋势图表 -->
<Trend ref="trendRef" />
<div
class="resize"
@click="handleFullScreen('trend')"
>
<span
class="iconfont"
:class="fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt'"
></span>
</div>
</div>
<div
class="bottom"
:class="{ fullscreen: fullScreenStatus.seller }"
>
<!-- 商家销售金额图表 -->
<Seller ref="sellerRef" />
<div
class="resize"
@click="handleFullScreen('seller')"
>
<span
class="iconfont "
:class="fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt'"
></span>
</div>
</div>
</section>
<section class="middle">
<div
class="top"
:class="{ fullscreen: fullScreenStatus.map }"
>
<!-- 商家分布图表 -->
<Map ref="mapRef" />
<div
class="resize"
@click="handleFullScreen('map')"
>
<span
class="iconfont"
:class="fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt'"
></span>
</div>
</div>
<div
class="bottom"
:class="{ fullscreen: fullScreenStatus.rank }"
>
<!-- 地区销量排行图表 -->
<Rank ref="rankRef" />
<div
class="resize"
@click="handleFullScreen('rank')"
>
<span
class="iconfont"
:class="fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt'"
></span>
</div>
</div>
</section>
<section class="right">
<div
class="top"
:class="{ fullscreen: fullScreenStatus.hot }"
>
<!-- 热销商品占比图表-->
<Hot ref="hotRef" />
<div
class="resize"
@click="handleFullScreen('hot')"
>
<span
class="iconfont"
:class="fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt'"
></span>
</div>
</div>
<div
class="bottom"
:class="{ fullscreen: fullScreenStatus.stock }"
>
<!-- 库存销量分析图表 -->
<Stock ref="stockRef" />
<div
class="resize"
@click="handleFullScreen('stock')"
>
<span
class="iconfont"
:class="fullScreenStatus.trend ? 'icon-compress-alt' : 'icon-expand-alt'"
></span>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
// 引入所需要的图表组价
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 { reactive, ref } from '@vue/reactivity'
import { computed, nextTick, onMounted, onUnmounted } from '@vue/runtime-core'
import Socket from '@/utils/socket'
// 引入仓库
import { useChart } from '@/store'
import { getThemeValue } from '@/utils/theme'
const theme = computed(() => getThemeValue(store.theme))
const headerSrc = computed(() => '/images/' + theme.value.headerBorderSrc)
const themeSrc = computed(() => '/images/' + theme.value.themeSrc)
const store = useChart()
// 点击切换主题
const handleTheme = () => {
Socket.send({
action: 'themeChange',
socketType: 'themeChange',
chartName: '',
value: ''
})
}
// 接收到服务器返回的切换主题之后的数据
const recvThemeChange = () => {
store.changeTheme()
}
// 注册切换主题的回调函数
Socket.registerCallback('themeChange', recvThemeChange)
// 接收到服务器返回的全屏数据之后的处理
const revData = (res) => {
// 取出是哪一个图表需要进行切换
// 取出需要切换成什么状态
const { chartName, value } = res
fullScreenStatus[chartName] = value
nextTick(() => {
switch (chartName) {
case 'trend':
trendRef.value.screenAdapter()
break;
case 'seller':
sellerRef.value.screenAdapter()
break;
case 'map':
mapRef.value.screenAdapter()
break;
case 'rank':
rankRef.value.screenAdapter()
break;
case 'hot':
hotRef.value.screenAdapter()
break;
case 'stock':
stockRef.value.screenAdapter()
break;
}
})
}
// 注册接收到数据的回调函数
Socket.registerCallback('fullScreen', revData)
//定义每一个图表的全屏状态数据,同一时刻只能有一个处于全屏状态
const fullScreenStatus = reactive({
trend: false,
seller: false,
map: false,
rank: false,
hot: false,
stock: false
})
const trendRef = ref(null)
const sellerRef = ref(null)
const mapRef = ref(null)
const rankRef = ref(null)
const hotRef = ref(null)
const stockRef = ref(null)
// 点击切换全屏状态
const handleFullScreen = (type) => {
const isfull = fullScreenStatus[type]
// 将数据发送给服务器
Socket.send({
action: 'fullScreen',
socketType: 'fullScreen',
chartName: type,
value: !isfull
})
}
onUnmounted(() => {
// 在组件销毁的时候注销注册的回调函数
Socket.unregisterCallback('fullScreen')
Socket.unregisterCallback('themeChange')
})
</script>
<style lang="scss" scoped>
.screen-container {
padding: 0 20px;
width: 100%;
height: 100%;
// background: #161522;
color: #fff;
box-sizing: border-box;
.header {
position: relative;
width: 100%;
height: 64px;
font-size: 20px;
&>div {
img {
width: 100%;
}
}
.title {
position: absolute;
left: 50%;
top: 50%;
font-size: 20px;
transform: translate(-50%, -50%);
&-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;
}
}
}
}
.body {
width: 100%;
height: 100%;
display: flex;
margin-top: 10px;
.top,
.bottom {
position: relative;
}
.left {
height: 100%;
width: 27.6%;
.top {
height: 53%;
}
.bottom {
height: 31%;
margin-top: 25px;
}
}
.middle {
height: 100%;
width: 41.5%;
margin-left: 1.6%;
margin-right: 1.6%;
.top {
width: 100%;
height: 56%;
}
.bottom {
margin-top: 25px;
width: 100%;
height: 28%;
}
}
.right {
height: 100%;
width: 27.6%;
.top {
height: 46%;
}
.bottom {
height: 38%;
margin-top: 25px;
}
}
.resize {
position: absolute;
right: 20px;
top: 20px;
cursor: pointer;
}
}
// 全屏样式
.fullscreen {
margin-top: 0 !important;
position: fixed !important;
top: 0;
left: 0;
right: 0;
height: 100% !important;
z-index: 100;
}
}
</style>
说明:
以上代码只展示了前端6个图表组件以及展示页面的所有代码,axios部分在使用了websocket之后就可以不用了。
如果想要完整的钱后端代码可以从仓库进行下载
zss5527/大屏可视化https://gitee.com/zss5527/large-screen-visualization.git