Echarts实现大屏可视化

一、效果展示

二、简介

该项目涉及到的图表有:

  • 渐变堆叠面积图
  • 中国地图
  • 涟漪特效散点图
  • 饼图
  • 横向柱状图
  • 竖向柱状图
  • 圆环饼图

该项目主要展示的是使用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 热销商品销售金额占比统计表

html 复制代码
<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"
      >&#xe6eb;</span>
      <span
        class="iconfont"
        :style="{ fontSize: fontSize + 'px', color: theme.color }"
        @click="handleRight"
      >&#xe6ee;</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 商家分布表

html 复制代码
<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 地区销售排行表

html 复制代码
<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 商家销量统计表

html 复制代码
<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 库存和销量分析表

html 复制代码
<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 商品销量趋势表

html 复制代码
<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' }"
        >&#xe6eb;</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 展示页面

html 复制代码
<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

相关推荐
zquwei6 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
qq_589568107 小时前
数据可视化echarts学习笔记
学习·信息可视化·echarts
carterslam11 小时前
解决:websocket 1002 connection rejected 426upgrade required
网络·websocket·网络协议
抓住鼹鼠不撒手11 小时前
xterm.js结合websocket实现web ssh
前端·javascript·websocket
m0_7482449613 小时前
echarts画风向杆
前端·数据库·echarts
图表制作解说(目标1000个图表)2 天前
ECharts柱状图-柱图42,附视频讲解与代码下载
echarts·统计分析·数据可视化·柱状图·大屏可视化
图表制作解说(目标1000个图表)2 天前
ECharts散点图-SymbolShapeMorph,附视频讲解与代码下载
echarts·统计分析·数据可视化·散点图·大屏可视化
图表制作解说(目标1000个图表)3 天前
ECharts关系图-关系图11,附视频讲解与代码下载
echarts·统计分析·数据可视化·关系图·大屏可视化
龙少95433 天前
【Http,Netty,Socket,WebSocket的应用场景和区别】
java·后端·websocket·网络协议·http