前阵子接了一个挺典型的校园需求:教学楼大厅要放一块大屏,把全校学生每天早晚打卡的数据实时滚动出来。领导的要求很直接——“要好看、要直观、不能像Excel表格”。这句话基本就给这个项目定了调子:前端数据可视化大屏,技术栈顺着 Vue 生态走。所以最后落地就是一套 Vue + ECharts 的高校学生打卡数据可视化大屏。这里先解释一下标题里那个“G”,我当时理解成图表库,实战里选的是 Apache ECharts,因为它在可视化大屏这个场景下群众基础最好,坑最少,文档也最全。
这篇文章不讲虚的,就把我从需求梳理、技术选型、环境配置、图表开发、大屏适配到最终上线的完整过程拆开说一遍。适合正在做类似“XX数据可视化大屏”的 Vue 开发者参考,也适合准备把 ECharts 用进实际项目的同学拿来当手册。
1. 这个打卡大屏到底要解决什么问题:从需求整理到技术选型
1.1 一块大屏背后站着谁:领导、辅导员、还是学生自己?
很多人接到大屏需求的第一反应是“赶紧找个模板改改”,但这样做出来的东西往往只是好看,用不起来。我拿到需求后先问了三件事:谁在看?看什么?看了之后做什么决定?
这个打卡大屏的受众其实有三类。第一类是校领导,他们只需要一眼看到整体出勤情况,比如今天打卡总人次、出勤率、异常人数,扫描周期按秒算;第二类是辅导员,他们关注的是自己学院出勤率的变化趋势,以及哪些班级连续三天异常;第三类是大厅里路过的学生,他们更关心排行榜和活动通知,这个屏幕对学生来说更像一个“场景入口”。
想清楚受众之后,页面的信息层级就出来了:最上面一排核心指标,中间是趋势图和排行榜,底层或者角落放地图/楼栋分布。这个结构不是我拍脑袋定的,而是根据“扫一眼就能拿到结论”的大屏设计原则反推出来的。如果一开始没做这一步,后面写代码大概率会陷入“图表多就是好”的误区,最后整块屏幕花花绿绿,谁也抓不住重点。
1.2 为什么选 Vue 而不是 jQuery、React 或者纯静态页面?
关于技术栈,说实话这个项目用纯 HTML + ECharts 也能跑,但后续维护会非常痛苦。打卡数据可视化大屏不是一个一次性页面,它需要接接口、轮播切换、动态更新、状态管理,甚至还要根据角色显示不同模块。这种情况下 Vue 的响应式体系和组件化拆分优势非常明显。
那为什么不用 React?不是说 React 不行,而是团队现有的技术积累在 Vue 这边,招人、交接、后续迭代都更顺畅。项目里还有个 Vue 3 的坑要注意:如果用<script setup>写组件,ECharts 实例的创建和销毁要在onMounted和onBeforeUnmount生命周期里手动管理,不能依赖自动垃圾回收,否则页面切换或数据刷新时会出现内存泄漏。
1.3 图表库选型:核心是 ECharts,补刀是自研小组件
可视化大屏的图表库,市面上叫得上号的有 ECharts、G2Plot、AntV、Chart.js、Highcharts。我在这个项目里选了 ECharts,理由很简单:大屏最常用的图表类型——折线图、柱状图、饼图、地图、热力图、雷达图——ECharts 全部覆盖,而且内置的动画效果和数据更新 API 在大屏场景下表现最稳。
实际用下来,ECharts 有两点特别适合大屏:第一是setOption的合并机制,更新数据时不需要整个图表重绘,性能损耗小;第二是内置的dispatchAction可以模拟 hover、点击等交互,配合轮播做高亮联动非常方便。这些能力在打卡大屏里基本都用上了。
不过也要提醒一句,ECharts 也不是万能的,比如超炫酷的3D效果、飞线动画这类需求,ECharts 做起来吃力,通常得搭配 Three.js。但如果你的核心需求是“把数据讲清楚”,ECharts 是投入产出比最高的选择。
2. 项目初始化:Vue 环境、依赖安装和图表库按需引入
2.1 Vite 快速创建 Vue3 项目,Node 版本先对齐
这个项目的脚手架我用的是 Vite 而不是 Vue CLI。不是 Vue CLI 不行,而是 Vite 在开发体验和构建速度上确实有代差,尤其是大屏项目后期要引很多图表库,Vite 的按需编译能让启动时间稳定在秒级。
初始化命令很简单:
npm create vite@latest checkin-screen -- --template vue cd checkin-screen npm install这里有一个特别容易踩的坑:Node 版本。Vite 4 以上要求 Node 14.18+,Vite 5 要求 16+。如果你的开发机还停在 Node 12,那个npm run dev会报各种莫名其妙的错。所以先跑node -v确认版本,最好直接用 nvm 切到 LTS 版本,比如 Node 18 或 20,省得后面为环境问题调半天。
2.2 按需引入 ECharts,而不是一把梭 import
首次做 ECharts 大屏的人最容易犯的错误,就是在入口文件直接import * as echarts from 'echarts'。这么做功能上没问题,但打包体积会直接飙到 1MB 以上,大屏项目的首屏加载会明显变慢。正确做法是按需引入。
我的做法是在src/utils/echarts.js里统一封装:
// src/utils/echarts.js import * as echarts from 'echarts/core' import { BarChart, LineChart, PieChart, MapChart } from 'echarts/charts' import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' echarts.use([ BarChart, LineChart, PieChart, MapChart, TitleComponent, TooltipComponent, GridComponent, LegendComponent, DataZoomComponent, CanvasRenderer ]) export default echarts这样打包后 ECharts 相关代码只有 400KB 左右,配合 gzip 后实际加载不到 150KB,对一个大屏来说完全可接受。需要注意,每次用到新的图表类型或组件时,都要回到这个文件里补充 import 和use,比如后面我加了地图,就需要把MapChart和GeoComponent加进去,否则图表会白屏且控制台提示“Component series.map not exists”。
2.3 目录结构:把大屏当成一个独立工程来做
大屏页面虽然看起来只是一个全屏页面,但内部模块非常多。我习惯把目录按功能和业务拆开,而不是把所有组件都堆在views下:
src/ ├── api/ # 接口请求 │ └── checkin.js ├── components/ │ ├── dashboard/ # 大屏业务组件 │ │ ├── StatCard.vue │ │ ├── TrendChart.vue │ │ ├── RankChart.vue │ │ ├── MapPanel.vue │ │ └── CarouselPanel.vue │ ├── common/ # 通用组件 │ │ └── ScreenAdapter.vue ├── utils/ │ ├── echarts.js # ECharts 按需引入封装 │ ├── format.js # 数字格式化工具 │ └── adapter.js # 大屏适配核心逻辑 ├── router/ │ └── index.js ├── views/ │ └── Dashboard.vue # 大屏主页面 └── App.vue这样一个结构的好处是,后续如果要把大屏从主系统里独立部署,只需要把components/dashboard和api目录带走就行。而且每个图表组件都是独立的.vue文件,职责单一,调试的时候只开一个组件,不会牵一发动全身。
3. 核心图表组件实战:打卡指标、时段趋势、排行榜与地图模块的落地实现
3.1 顶部指标卡片:最简单的模块也有设计讲究
顶部四个指标卡是大屏最显眼的区域,我放了:今日打卡总人次、总体出勤率、今日异常次数、连续未打卡人数。这四个数字基本覆盖了领导最关心的维度。
指标卡本身不复杂,就是一个数字加一个环比趋势:
<template> <div class="stat-card"> <div class="stat-title">{{ title }}</div> <div class="stat-value">{{ value }}</div> <div class="stat-trend" :class="trend > 0 ? 'up' : 'down'"> {{ trendText }} </div> </div> </template>这里要特别提醒一个设计细节:大屏的观众通常离屏幕三到五米,数字的字号不能小于 40px,而且最好用font-family: 'DIN Alternate', 'Bebas Neue', sans-serif这类偏瘦的数字字体,视觉上更醒目。别用默认字体,那会显得页面很“素”。
数字跳动的动画我用了一个很轻量的方案,没有引第三方库:
// 数字滚动动画 function animateValue(el, start, end, duration = 1000) { const range = end - start const startTime = performance.now() const step = (currentTime) => { const progress = Math.min((currentTime - startTime) / duration, 1) el.textContent = Math.floor(start + range * progress).toLocaleString() if (progress < 1) requestAnimationFrame(step) } requestAnimationFrame(step) }3.2 打卡时段分布折线图:这张图最能发现管理问题
打卡趋势图我选的是折线图,横轴是时间(6点到23点),纵轴是打卡次数。这张图在管理上的价值很大——如果晚上8点还有一个明显的小高峰,说明有人在这个时间补卡或者夜跑打卡;如果早上8点前后曲线陡峭上升,说明大家集中在早课前进校。
折线图的配置里有一个值得说的点:areaStyle渐变。纯折线在大屏上容易显得单薄,加一层从半透明到透明的渐变后,视觉重量感立刻不一样:
const option = { tooltip: { trigger: 'axis' }, grid: { left: 60, right: 30, top: 40, bottom: 50 }, xAxis: { type: 'category', data: timeLabels, axisLine: { lineStyle: { color: 'rgba(255,255,255,0.2)' } }, axisLabel: { color: '#a0c4ff' } }, yAxis: { type: 'value', splitLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } } }, series: [{ name: '打卡次数', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: checkinCounts, lineStyle: { width: 3, color: '#36cfc9' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(54, 207, 201, 0.35)' }, { offset: 1, color: 'rgba(54, 207, 201, 0)' } ]) } }] }3.3 学院排行榜横向柱状图:排序逻辑和动画节奏
排行榜我用了横向柱状图,直观的排序展示每个学院的出勤率。横向柱状图在大屏里的优势是学院名称可以完整展示,不会像纵向柱状图那样旋转字体,阅读体验更好。
这个组件有一个细节:数据更新时,柱状图应该平滑地过渡,而不是立刻跳变。setOption天然支持动画过渡,但前提是series的name保持一致,否则 ECharts 会认为你换了数据系列,动画就变成了重新渲染。这里我还会配合dataZoom组件把前10名的学院展示得清楚点,或者限定只展示排名前10,不然数据多的时候柱状图会被压缩得看不清。
// 排行榜数据格式 const rankData = [ { name: '计算机学院', value: 98.6 }, { name: '外国语学院', value: 97.2 }, // ... ]3.4 地图和楼栋分布模块:让数据回到真实场景
这个打卡大屏还有一个模块是校园地图或各楼栋的热力分布,用来展示不同教学楼、宿舍楼当前的打卡集中度。这里要注意,如果你只是展示省市级地图,ECharts 的MapChart配合 GeoJSON 就够了;但如果是校园内部场景,通常没有现成的 GeoJSON,就需要自己把楼栋坐标整理成散点图或者热力图。
我采用的是“校园平面底图 + 散点层”的方案,底图直接用一张渲染好的平面图,散点坐标通过图片上的比例换算得到。这么做比折腾真实地图轻量得多,效果也足够。
地图模块的实际代码相对复杂,核心配置里需要注册一个registerMap,但因为是校园平面图,我用的是scatter系列配合平面图作为背景。具体坐标换算可以写一个工具函数:
// 将设计稿上的像素坐标换算为图表坐标 function pxToCoord(pxX, pxY, scale) { return { x: (pxX / scale - 100) / 10, y: (700 - pxY / scale) / 10 } }4. 大屏常见的适配与布局问题:不同分辨率下怎么保证显示效果
4.1 三种主流适配方案对比:vw/vh、rem、scale
大屏适配是每个做可视化大屏的人绕不开的坎。设计稿通常是 1920×1080,但实际投放的屏幕可能是 1080p、2K、4K,甚至是一块竖屏拼接屏。适配方案主流有三种:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| vw/vh 单位 | 所有尺寸以视口宽度/高度百分比书写 | 简单,随屏幕等比缩放 | 字体和图表无法精准控制,某些场景会被拉伸 |
| rem + 动态 fontSize | 根元素 font-size 随视口宽度变化,布局用 rem | 布局灵活,常用在移动端 | 需要自己封装图表尺寸计算,比较繁琐 |
| scale 整体缩放 | 内容按固定尺寸绘制,外层容器 scale 缩放 | 效果最接近设计稿,不用改图表逻辑 | 屏幕比例不一致时会出现黑边或裁切 |
4.2 我最终采用的适配方案:基于 scale 的整体缩放
这个项目我最后用的是第三套方案:scale 整体缩放。核心思路是:整个大屏内容始终按 1920×1080 设计,运行的时候根据实际屏幕尺寸计算缩放比,再用 CSS transform 进行缩放。
工具函数代码大致如下:
// src/utils/adapter.js export function calcScale(designWidth = 1920, designHeight = 1080) { const clientWidth = document.documentElement.clientWidth const clientHeight = document.documentElement.clientHeight const scaleX = clientWidth / designWidth const scaleY = clientHeight / designHeight return Math.min(scaleX, scaleY) }然后在ScreenAdapter.vue组件里,监听窗口变化并触发重绘。这样可以保证图表内部所有尺寸都用设计稿的像素值,不需要像 rem 方案那样去换算,页面效果最接近设计稿。
用 scale 方案也有一个坑,就是图表中的文字、图例大小会跟着整体一起缩放,当播放屏很小或很大时,字号可能显得过大或过小。解决办法是:在数据刷新或窗口变化时,动态计算ECharts 实例的 resize 并进行微调,比如根据缩放比重新setOption调整字体大小。
4.3 打包后布局异常的排查过程
这个项目上线前遇到一个非常典型的“打包后布局异常”问题:开发环境跑得好好的,npm run build之后部署到服务器,大屏背景图和部分图标显示不出来,有的图表位置也发生了偏移。
排查链路是这样的:先用 F12 看 Network,发现静态资源的请求路径是/assets/xxx.png,但项目部署在 nginx 的子路径下,根路径跳转不到。解决方案是在vite.config.js里配置base: './'(相对路径),这样打包后的资源路径就是相对当前页面,可以在任意目录下部署。
布局偏移的问题更隐蔽,排查发现是因为部署环境浏览器窗口缩放比例不是 100%,系统显示缩放是 125% 或 150%,导致document.documentElement.clientWidth拿到的是缩放后的值。但这其实不是 bug,因为大屏本身就是全屏展示,用户应该按 100% 比例打开。不过为了避免用户在非整屏浏览器里预览时出现滚动条和错位,我在适配组件里加了强制overflow: hidden和全屏遮盖处理,保证无论如何都不会出现滚动条。
5. 让大屏真正能“看”:轮播、自动刷新、实时数据接入与上线注意事项
5.1 大屏轮播:单屏放不下的时候怎么自动切换
大屏的信息容量是有限的,如果页面超过一屏,就需要考虑轮播。这里的轮播不是页面整屏翻页,而是两个面板块(比如“实时动态”和“打卡趋势”)按时间段交替显示。
我采用的方案是 CSS 动画控制transfrom: translateY来实现纵向轮播,配合一个Vue定时器来控制切换周期,这样不依赖额外的轮播插件的体积和配置。如果要做横向轮播或者带指示点的复杂效果,也可以考虑第三方库,比如swiper,但大屏场景下我还是推荐自己写简单的轮播,因为可控性强,而且不会和 ECharts 的 resize 逻辑冲突。
下面是一个比较朴素的轮播核心逻辑:
// composables/use-carousel.js import { ref, onUnmounted } from 'vue' export function useCarousel(interval = 5000) { const activeIndex = ref(0) let timer = null const start = () => { timer = setInterval(() => { activeIndex.value = (activeIndex.value + 1) % maxCount }, interval) } const stop = () => { clearInterval(timer) timer = null } onUnmounted(stop) return { activeIndex, start, stop } }5.2 数据自动刷新:轮询、定时器还是 WebSocket
打卡数据是分钟级更新的,不需要像股票那样毫秒级推送,所以我优先用了轮询方案。定时器每 60 秒请求一次接口,拿到新数据后调用各个图表的setOption更新。
这个环节最大的坑是:定时器回调里直接操作 ECharts 实例可能会报“Get initialized failed”,因为组件已经销毁了。解决方案是在组件卸载前清理定时器,同时用echartInstance.isDisposed()做一次安全判断。
如果后续要求做到秒级更新,比如大屏右下角显示“当前在线人数”,那就需要换 WebSocket。Vue 里使用 WebSocket 要注意在onUnmounted中关闭连接,不然页面切换后连接仍然存在,会不断报错。这个项目暂时没有用到 WebSocket,但我在代码里预留了消息入口,后面如果需要,可以在 API 模块统一替换。
5.3 上线前的检查清单和性能优化
最后整理一下上线前需要检查的项目,这些都是实际踩过的坑:
- 确认
vite.config.js的base: './',避免静态资源 404; - 打包后用
nginx或http-server本地预览一遍,不要只在 dev 模式下看效果; - 检查所有图表实例的
dispose逻辑,避免内存泄漏; - 大屏长时间运行,建议定时器统一管理,避免多个组件各自设置定时器导致资源浪费;
- 对于部署在子路径的情况,接口地址要使用相对路径或环境变量控制;
- 大屏项目如果自带背景音乐或音效,记得加一个开关按钮,因为大厅环境并不一定适合外放。
性能方面,ECharts 的setOption是大屏刷新最耗时的操作。如果你有多个图表同时更新,尽量合并请求、批量更新,或者在数据不变的情况下跳过setOption调用。实测下来,一次全量刷新 6 个图表,在 2K 屏上帧率仍然流畅,但如果用老旧电视或一体机,就建议把动画关掉或降低刷新频率,优先保证实时性。
这次打卡大屏做完之后,我最大的感受是:可视化大屏项目真正难的从来不是写一个 ECharts 图表,而是把数据、布局、适配、刷新节奏、运营场景全部揉在一起,还能让一个外行走过来一眼看懂。如果你也正在做类似的 Vue 可视化大屏项目,建议从业务需求出发,先把页面分区想明白,再动手写代码,后面会顺很多。