这不双十一刚过十几天,本篇文章就来介绍下如何使用 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
不变,designRatio
为 1920 / 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>
中的用于定义圆心坐标的 cx
和 cy
属性删除了,通过在 #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>