vue3 + ts + svg + ECharts 实现双十一数据大屏

这不双十一刚过十几天,本篇文章就来介绍下如何使用 vue3 + ts + svg + ECharts 实现一个如下所示的双十一数据大屏页面:

创建项目

执行命令 npm create vue@latest 创建基于 Vite 构建的 vue3 项目,功能选择如下:

我选择使用 pnpm 安装项目依赖:pnpm i,各安装包的版本号可见于下图:

在 vite.config.ts 中添加配置,以便在项目启动时能自动打开浏览器:

typescript 复制代码
export default defineConfig({
  // ...
  server: {
    open: true
  }
})

现在,就可以通过 pnpm dev 启动新建的项目了。

大屏适配

大屏适配的方案有很多,比如 rem、vw 和 flex 布局等,我选择使用缩放(scale)的方式来适配大屏,因为该方案使用起来比较简单,也不用考虑第三方库的单位等问题。

假设设计稿的尺寸为 1920 * 1080px,为了保证效果,在大屏中放大时应该保持宽高比 designRatio 不变,designRatio1920 / 1080 ≈ 1.78。放大的倍数 scaleRatio,可以分为以下 2 种情况计算:

  • 当用于展示的设备屏幕的宽高比 deviceRatio 等于或小于设计稿的宽高比 designRatio 时,我们可以按照两者的宽度之比进行缩放,即让设计稿保持宽高比的情况下放大到与设备等宽,在高度上可能留有空白;
  • 当设备屏幕的宽高比 deviceRatio 大于设计稿宽高比 designRatio 时,也就是说设备为超宽屏,scaleRatio 应该按照两者的高度之比决定,即让设计稿保持宽高比的情况下放大到与设备等高,在宽度上可能留有空白,所以还要做个居中布局。

具体代码我封装成了一个 hook:

typescript 复制代码
// 屏幕适配,src\hooks\useScreenAdapt.ts
import _ from 'lodash'
import { onMounted, onUnmounted } from 'vue'

export default function useScreenAdapt(dWidth: number = 1920, dHeight: number = 1080) {
  // 节流
  const throttleAdjustZoom = _.throttle(() => {
    AdjustZoom()
  }, 1000)

  onMounted(() => {
    AdjustZoom()
    // 响应式
    window.addEventListener('resize', throttleAdjustZoom)
  })

  // 释放资源
  onUnmounted(() => {
    window.removeEventListener('resize', throttleAdjustZoom)
  })

  function AdjustZoom() {
    // 设计稿尺寸及宽高比
    const designWidth = dWidth
    const designHeight = dHeight
    const designRatio = designWidth / designHeight // 1.78

    // 当前屏幕的尺寸及宽高比
    const deviceWidth = document.documentElement.clientWidth
    const devicHeight = document.documentElement.clientHeight
    const deviceRatio = deviceWidth / devicHeight

    // 计算缩放比
    let scaleRatio = 1
    // 如果当前屏幕的宽高比大于设计稿的,则以高度比作为缩放比
    if (deviceRatio > designRatio) {
      scaleRatio = devicHeight / designHeight
    } else {
      // 否则以宽度比作为缩放比
      scaleRatio = deviceWidth / designWidth
    }

    document.body.style.transform = `scale(${scaleRatio}) translateX(-50%)`
  }
}

最后是给 body 添加了 transform 属性,为了实现居中效果,还需要给 body 添加上相应样式:

css 复制代码
/* \src\assets\base.css */
* {
  box-sizing: border-box;
}

body {
  position: relative;
  margin: 0;
  width: 1920px;
  height: 1080px;
  transform-origin: left top;
  left: 50%;
  background-color: black;
}

使用 lodash 实现节流

为避免改变屏幕尺寸时过于频繁触发 AdjustZoom,我借助 lodash 的 throttle 方法做了个节流,这就需要安装 lodash:pnpm add lodash。因为用到了 ts,如果直接引入使用 lodash 会遇到如下报错:

我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。提示里已经告诉了我们解决办法,就是去安装 @types/lodash:pnpm add -D @types/lodash,之后就能在 ts 文件中正常使用 lodash 了。

使用 svg

页面头部使用的就是一张 svg,样式中给 #top 添加绝对定位 position: absolute; ,目的在于开启一个单独的渲染层,以减少之后添加动画造成的回流损耗:

vue 复制代码
<template>
  <main class="main-bg">
    <div id="top"></div>
  </main>
</template>
<style scoped>
#top {
  position: absolute;
  width: 100%;
  height: 183px;
  background-size: cover;
  background-image: url(@/assets/imgs/top_bg.svg);
}
</style>

作为背景引入的 top_bg.svg 是我使用 Illustrator 绘制后导出的,绘制时注意做好图层的命名:

因为图层的名称会影响到导出的 svg 文件中元素的 id 名称。另外导出的 svg 文件中也可能存在一些中文命名或一些不必要的代码,我们可以自行修改:

添加动画及滤镜

使用 Illustrator 绘制的都是静态图形,现在我们以其中一个圆球为例,添加上平移的动画以及高斯模糊的滤镜:

xml 复制代码
<!-- top_bg.svg 部分代码 -->
<?xml version="1.0" encoding="UTF-8"?>
<svg id="top-bg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1920 183">
  <defs>
    <style>
      #circle-1 {
        opacity: 0;
        transform: translate(800px, -18px) scale(0.5);
        animation: circle-1-ani 1.8s ease-out forwards infinite;
      }
      @keyframes circle-1-ani {
        90%,
        100% {
          opacity: 0.95;
          transform: translate(600px, 80px) scale(1);
        }
      }
    </style>
    <filter id="blurMe">
        <feGaussianBlur stdDeviation="2" />
      </filter>
  </defs>
	<circle id="circle-1" class="cls-1" r="12.96" filter="url(#blurMe)" />
</svg>

动画使用 css 定义,可以直接写在 <defs> 里的 <style> 中。一旦用到 transform,那么圆的坐标系就会移动到圆的中心点,所以我将原本 <circle> 中的用于定义圆心坐标的 cxcy 属性删除了,通过在 #circle-1 中直接使用 transform: translate(800px, -18px); 来定位圆的初始位置:

滤镜定义在 <defs> 里的 <filter> 中,使用的是高斯模糊 <feGaussianBlur>stdDeviation 用于指定钟形(bell-curve),可以理解为模糊程度。在圆形 <circle> 上通过 filter 属性,传入滤镜的 id 应用滤镜。

使用 ECharts

安装

首先是安装 ECharts:pnpm add echarts。在 npm 的仓库搜索 echarts 可以看到其带有如下所示的 ts 标志:

说明它的库文件中已经包含了 .d.ts 文件:

所以不需要像上面使用 lodash 那样再去额外安装声明文件了。

封装组件

接着就可以封装 echarts 组件了。组件中只需要提供一个展示图表的 dom 容器 <div>,然后在 onMounted(确保可以获取到 dom 容器) 中创建一个 ECharts 实例 myChart,最后通过 myChart.setOption(option) 传入从父组件获取的图表实例的配置项以及数据 option

vue 复制代码
<!-- src\components\BaseEChart.vue -->
<template>
  <div ref="mainRef" :style="{ width: width, height: height }"></div>
</template>

<script lang="ts" setup>
  import * as echarts from 'echarts'
  import { onMounted, onUnmounted, ref } from 'vue'

  interface IProps {
    width?: string
    height?: string
    chartOption: echarts.EChartsOption
  }
  const props = withDefaults(defineProps<IProps>(), {
    width: '100%',
    height: '100%'
  })

  const mainRef = ref(null)

  let myChart: echarts.ECharts | null = null
  onMounted(() => {
    myChart = echarts.init(mainRef.value, 'dark', { renderer: 'svg' })
    const option = props.chartOption
    myChart.setOption(option)
  })

  onUnmounted(() => {
    // 销毁 echart 实例,释放资源
    myChart?.dispose()
  })
</script>

更多关于 echarts 的使用细节可以阅览《ECharts 实现掘金数据中心折线图》

使用示例

以左上角的"人均消费金额排名"柱状图为例,代码如下:

vue 复制代码
<!-- src\views\HomeView.vue -->
<template>
  <main class="main-bg">
    <div id="left-top">
      <div class="title">人均消费金额排名</div>
      <div class="sub-title">Ranking of per capita consumption amount</div>
      <BaseEChart :chartOption="amountRankOption" />
    </div>
  </main>
</template>

<script setup lang="ts">
import BaseEChart from '@/components/BaseEChart.vue'
import { amountRankOption } from './config/amount-rank-option'
</script>

<style scoped>
#left-top {
  position: absolute;
  top: 130px;
  left: 20px;
  width: 500px;
  height: 320px;
}
</style>

在页面引入 BaseEChart 后,传入定义好的 amountRankOption 即可:

typescript 复制代码
// 人均消费金额排名柱状图配置
import * as echarts from 'echarts'
type EChartsOption = echarts.EChartsOption

export const amountRankOption: EChartsOption = {
  grid: {
    top: 20,
    bottom: 50,
    left: 40,
    right: 40
  },
  xAxis: {
    axisTick: {
      show: false // 隐藏 x 坐标轴刻度
    },
    data: ['思明', '湖里', '集美', '同安', '海沧', '翔安']
  },
  yAxis: {
    axisLabel: {
      show: false // 隐藏 y 坐标轴刻度标签
    },
    splitLine: {
      show: false // 隐藏平行于 x 轴的分隔线
    }
  },
  series: [
    {
      type: 'bar',
      data: [5, 20, 36, 10, 10, 20],
      barWidth: 20 // 设置柱形的宽度
    }
  ]
}

至于剩下的图表的实现,只是配置不同而已,如有兴趣可以去该项目的 git 仓库查看。

数字滚动动画

最后添加成交额的数字滚动动画,用到了 countup.js,需要先安装: pnpm add countup.js

使用时,直接 new CountUp() 生成 countUp,第 1 个参数为要添加动画的 dom 的 id,第 2 个参数为动画结束时显示的数字,还可以传入第 3 个参数 options 实现一些配置,比如设置前缀,小数点等。然后通过 countUp.start() 即可实现动画效果:

vue 复制代码
<!-- src\components\Digital.vue -->
<template>
  <div>
    <span class="t1">成交额</span>
    <span id="amount" class="t2">150</span>
    <span class="t1">亿</span>
  </div>
</template>

<script lang="ts" setup>
  import { CountUp } from 'countup.js'
  import { onMounted } from 'vue'

  onMounted(() => {
    const countUp = new CountUp('amount', 150)

    if (!countUp.error) {
      countUp.start()
    } else {
      console.error(countUp.error)
    }
  })
</script>

项目 git 仓库:githubgitee

相关推荐
百万蹄蹄向前冲4 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳58142 分钟前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter1 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry2 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog3 小时前
低端设备加载webp ANR
前端·算法
LKAI.3 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy4 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js