☰
基于Vue3+G2的高校打卡数据可视化大屏实战解析
2026/10/29 18:35:02 网站建设 项目流程

今晚十点刚过,辅导员的群里照例弹出一句:“今天还有几个班没完成打卡?数据给我一下。”如果你做过高校信息化相关的东西,应该能感同身受——打卡数据不是没有,而是散落在后台表格里,光看数字根本看不出哪个学院在恶化、哪个时段最容易漏打、哪些宿舍楼老是拖后腿。我这次用 Vue3 + G2 做了一块高校学生打卡数据可视化大屏,把这些问题全部压到一屏里,顺便把整套源码结构整理成了可以直接照着搭的实战项目。这篇文章就把整个实现过程、选型理由、关键代码和踩过的坑一次说清楚,适合正在做数据可视化大屏、准备入门 Vue3 + G2,或者想给自己学校/公司快速搭一个监控面板的同学参考。

1. 一块打卡大屏背后的真实需求拆解

很多前端同学拿到“大屏”需求,第一反应就是找模板、堆图表,结果做出来很炫但用户根本不用。我这次先没碰代码,而是把使用场景和角色理了一遍,因为高校打卡大屏和普通企业驾驶舱有本质区别:它的数据规律非常强,而且使用者基本是两类人——领导只看总览,辅导员要看异常。

1.1 辅导员想看什么,教务领导又想看什么

辅导员的核心痛点是“今天谁没打卡、哪个班掉链子”,他们需要的是实时异常提醒和按学院下钻的能力;校级领导则更关心整体趋势——这个月的打卡率是升是降、高峰期集中在哪几个时段、重点学院是不是在改善。所以大屏不能只做一堆花哨图表,第一屏必须在 5 秒内回答三个问题:今天整体情况怎么样?哪个学院最差?这个月趋势是变好还是变坏?

我把需求整理成了四个维度:

  • 总览指标:今日应打卡人数、已打卡人数、打卡率、连续打卡天数最高值。
  • 学院粒度:各学院打卡率排行,支持点击下钻到年级和班级。
  • 时段粒度:全天 24 小时打卡人数分布,定位打卡高峰和漏打卡高发时段。
  • 空间粒度:宿舍楼栋的热力分布,方便辅导员针对性提醒。

1.2 打卡数据的结构和口径设计

打卡数据本身不复杂,但口径不定义清楚,后续所有图表都会打架。我设计的核心数据模型如下:

// 打卡记录(原始数据) { studentId: '2024031201', name: '张同学', college: '计算机学院', grade: '2024级', classId: '计科2402', dorm: '梅苑3栋', floor: 6, checkInTime: '2025-04-08 21:47:33', device: 'Android', status: 'on-time' // on-time 正常 / late 补卡 / missed 未打卡 }

口径上做了几条硬性约定:一天只计算一次有效打卡;晚上 22:30 之后打卡计入“补卡”;未打卡学生的打卡时间为空但记录保留。这些规则在模拟数据层里直接生成,方便后续切换真实后端时只改数据源、不动图表层。

2. 为什么表格不用 ECharts,而选了 Vue3 + G2

很多人大屏首选是 ECharts,我这次选 G2 并不是说 ECharts 不好,而是这个项目的数据结构天然适合 G2 的“图形语法”表达方式。G2 的核心理念是把数据到图形映射的每一步都显式声明出来,写图表像是在描述数据和图形的关系,而不是堆配置项,这在做自定义组合图表时优势特别明显。

2.1 G2 和 ECharts 的思维差异

ECharts 是“开箱即用”的图表库,饼图就是饼图,柱状图就是柱状图,配置项非常完善,适合快速出图。G2 则更像一套可视化语法,它把柱状图拆成interval + coordinate('theta'),把雷达图拆成line + coordinate('polar'),你可以用基础的 mark 组合出业务需要的特殊图形,而不是在 ECharts 里翻有没有这个 type。

举一个实际例子:打卡率趋势图需要在折线下方加渐变面积、在最高点自动标注班级事件。ECharts 需要同时配 line、area 和 markPoint 三套 series,G2 里就是一条线加一个面积标记,数据映射逻辑完全一致,代码维护起来心理负担小得多。

2.2 Vue3 组合式 API 更适合封装 G2 生命周期

G2 有一个绕不开的问题:图表实例的创建、更新、销毁必须和组件生命周期严格对应,写不好就会内存泄漏。Vue3 的组合式 API 正好可以把new Chart、chart.render()、chart.destroy()封装在 onMounted / onBeforeUnmount 钩子里,再配合ref统一管理容器,比 Vue2 的 Options API 舒服很多。

我封装的图表组件结构大致是这样的:

<template> <div ref="chartRef" class="chart-container"></div> </template> <script setup> import { ref, onMounted, onBeforeUnmount, watch } from 'vue'; import { Chart } from '@antv/g2'; const props = defineProps({ data: { type: Array, required: true }, options: { type: Object, default: () => ({}) }, }); const chartRef = ref(null); let chart = null; onMounted(() => { chart = new Chart({ container: chartRef.value, autoFit: true, padding: 24, }); renderChart(); }); watch(() => props.data, () => { renderChart(); }, { deep: true }); function renderChart() { if (!chart) return; chart.clear(); // 保留实例,只清空画布 // ...根据 props.options.type 绘制不同 mark chart.render(); } onBeforeUnmount(() => { if (chart) { chart.destroy(); chart = null; } }); </script>

这种封装方式好处是:一个组件可以统治全站所有图表,换图只需改 options,新增图表不用新开组件。后面接真实接口时,也只需要在父组件里把接口返回的数据传给data属性即可。

3. 从零搭建 Vite + Vue3 工程

大屏项目我用 Vite 初始化,不推荐 Vue CLI,Vite 冷启动快太多,而且对 G2 这种用 ESM 模块的库支持非常自然。初始化命令如下:

npm create vite@latest checkin-dashboard -- --template vue cd checkin-dashboard npm install npm install @antv/g2@^5 dayjs npm run dev

3.1 目录结构与数据模拟层

目录结构直接影响项目能不能长期维护。我按“数据层 / 业务组件 / 图表组件”三层隔离,这样以后就算数据源从 mock 换成后端接口,页面不用动:

src/ ├── api/ │ └── dashboard.js # 接口请求模块(目前返回 mock 数据) ├── assets/ │ └── styles/ │ ├── variables.css # 大屏颜色变量 │ └── dashboard.css # 布局样式 ├── components/ │ ├── charts/ │ │ ├── BaseChart.vue # 通用图表容器 │ │ ├── TrendLine.vue # 打卡趋势 │ │ ├── CollegeRank.vue # 学院排行 │ │ └── DormHeatmap.vue # 宿舍热力 │ ├── panel/ │ │ ├── StatCard.vue # 顶部指标卡 │ │ └── RealTimeList.vue # 实时打卡滚动列表 │ └── layout/ │ └── ScreenFooter.vue ├── mock/ │ └── checkin.js # 模拟打卡数据生成器 └── views/ └── DashboardView.vue # 大屏主页面

3.2 模拟数据生成器的设计

模拟数据要尽量贴近真实高校场景,不能全都随机。我给每个学院设置了不同的基础打卡率:计算机学院高一些,艺术学院偶尔断层;时段分布上,早上 7:30 到 8:30 是早打卡高峰,晚上 19:00 到 22:00 是晚自习打卡高峰,深夜还会有少量“补卡选手”。生成逻辑用加权随机控制,确保图表看起来“真实得像真的数据”。

// mock/checkin.js const colleges = [ { name: '计算机学院', baseRate: 0.96 }, { name: '外国语学院', baseRate: 0.92 }, { name: '艺术设计学院', baseRate: 0.86 }, // ... ]; export function generateRecords(date = '2025-04-08') { const records = []; colleges.forEach((college) => { const total = Math.floor(Math.random() * 400) + 600; for (let i = 0; i < total; i++) { const checked = Math.random() < college.baseRate; const hour = weightedHour(); records.push({ studentId: String(100000 + Math.floor(Math.random() * 900000)), college: college.name, checkInTime: checked ? `${date} ${hour}:${rand(60)}:${rand(60)}` : null, // ... }); } }); return records; }

4. 核心图表逐个击破:大屏上的六块拼图

大屏总共六个模块,每个模块对应一种 G2 mark,我按实现顺序逐个说,并附上关键代码。

4.1 今日打卡总览:用 theta 坐标做双色进度环

顶部中间放一个大进度环,一眼看出今天整体打卡率。G2 里没有专门的“仪表盘”叫法,但区间图的 theta 坐标可以轻松实现:

chart .interval() .data([ { type: '已完成', value: rate }, { type: '剩余', value: 100 - rate }, ]) .encode('x', 1) .encode('y', 'value') .encode('color', 'type') .coordinate({ type: 'theta', startAngle: -Math.PI / 2, endAngle: (Math.PI * 3) / 2 }) .scale('color', { range: ['#00E0FF', '#1A2A4A'] }) .style('radius', 10) .legend(false);

startAngle和endAngle把圆环的起始点挪到正上方,视觉上就是最常见的“进度环”。中间百分比数字不靠 G2 画,直接用 HTML 绝对定位覆盖在容器中央,这样更可控,还方便添加“较昨日 +3.2%”的动画数字。

4.2 各学院打卡率排行:横向柱状图比纵向更适合中文标签

学院名称都是四到六个汉字,纵向柱状图在窄屏上文字会重叠。我直接把 x、y 轴交换,做成了横向条形图:

chart .interval() .data(collegeData) .encode('x', 'rate') // 数值放 x .encode('y', 'college') // 学院名称放 y .encode('color', 'rate') .scale('x', { domain: [0, 100] }) .scale('color', { range: ['#3AA0FF', '#FFC53D'] }) .axis('y', { labelAutoHide: false }) .label({ text: 'rate', formatter: (v) => `${v}%`, position: 'right', }) .style('radius', 4);

这里有一个细节:color直接映射到rate,G2 会自动把数值映射成渐变色,值越高颜色越偏暖色,异常学院一眼就能扫出来。不用额外排序,因为我要让柱状图按打卡率从高到低显示,需要在拿到数据后先sort((a, b) => b.rate - a.rate)再传给图表。

4.3 分时段打卡趋势:折线加面积渐变

全天打卡时段分布是判断“学生习惯”的关键图表。我在折线图基础上叠加面积渐变,既能看到峰值也能感知走势:

chart .line() .data(hourlyData) .encode('x', 'hour') .encode('y', 'count') .encode('color', '#00E0FF') .style('lineWidth', 2) .style('gradient', 'y'); // 折线渐变 chart .area() .data(hourlyData) .encode('x', 'hour') .encode('y', 'count') .scale('y', { zero: true }) .style('fill', 'linear-gradient(-90deg, rgba(0,224,255,0.3), rgba(0,224,255,0.02))') .style('fillOpacity', 1);

hourlyData的生成逻辑是把每条打卡记录的checkInTime按小时聚合。G2 本身不提供聚合函数,所以聚合我放在数据层做:

function groupByHour(records) { const map = {}; for (let i = 0; i < 24; i++) map[i] = 0; records.forEach((r) => { if (r.checkInTime) { const hour = new Date(r.checkInTime).getHours(); map[hour] += 1; } }); return Object.entries(map).map(([hour, count]) => ({ hour: `${hour}:00`, count })); }

4.4 学院打卡率雷达图:把“均衡性”变成可见形状

雷达图用于对比各学院在“准时率 / 补卡率 / 连续打卡天数 / 晚间打卡占比”四个指标上的表现。数据可以形成不同形状的“多边形”,哪个学院形状越饱满说明各方面越平衡:

chart .line() .data(radarData) .encode('x', 'indicator') .encode('y', 'value') .encode('color', 'college') .coordinate({ type: 'polar' }) .scale('x', { padding: 0.5 }) .style('lineWidth', 2); chart .area() .data(radarData) .encode('x', 'indicator') .encode('y', 'value') .encode('color', 'college') .coordinate({ type: 'polar' }) .style('fillOpacity', 0.15);

雷达图同时展示三到四个学院就够了,全部学院放上去会变成“蜘蛛网”,什么都看不清。我默认展示的是今天排名前四的学院。

4.5 宿舍楼栋热力:用 cell 标记铺出网格

宿舍打卡热力图是最直观的空间维度图表。我把 x 轴设为楼栋名,y 轴设为楼层,每个格子颜色深浅代表该楼层打卡人数:

chart .cell() .data(dormData) .encode('x', 'building') .encode('y', 'floor') .encode('color', 'count') .scale('color', { range: ['#0B1A30', '#00E0FF', '#FFC53D'] }) .style('inset', 0.5) .axis('x', { title: false }) .axis('y', { title: false });

inset是格子间距,数值小一点能让网格看起来整洁又不糊成一片。这里要注意:楼层是离散值,如果直接传数字,G2 会把它当成连续轴,页面拉开后中间会出现不存在的楼层,所以要先转成字符串。

4.6 实时滚动打卡列表:G2 管不到的展示交回 Vue

底部放一条最近打卡记录滚动列表,这块不需要 G2,直接用 Vue3 的 TransitionGroup 控制滚入滚出。我用一个定时器每 3 秒从模拟数据队列头部取一条新记录插入数组,同时裁剪数组长度保证最多显示 8 条:

const latestRecords = ref([]); let timer = null; function fetchLatest() { timer = setInterval(() => { const record = mockStream.shift(); if (record) { latestRecords.value.unshift(record); latestRecords.value = latestRecords.value.slice(0, 8); } }, 3000); } onMounted(fetchLatest); onBeforeUnmount(() => clearInterval(timer));

这里的动态效果完全走 Vue 响应式,不涉及 canvas 重绘,CPU 占用很低,大屏挂着过夜也不怕。

5. 大屏布局和视觉细节

可视化大屏最容易翻车的不是图表,而是布局和配色。灯光昏暗的大屏环境里,背景用纯白就是灾难;信息密度拉满却没有层级,用户根本找不到重点。我参考了很多指挥中心大屏的设计原则,定下“深色底 + 高亮主色 + 网格分区”的基调。

5.1 16:9 栅格布局:不写死像素

大屏最常见的屏幕是 16:9,我用 CSS Grid 把一个屏分成 12 列,顶部指标卡横跨整行,中间图表区按比例分配:

.dashboard { display: grid; grid-template-columns: repeat(12, 1fr); grid-template-rows: auto 1fr 1fr auto; gap: 16px; padding: 20px; width: 100vw; height: 100vh; background: linear-gradient(135deg, #050A1C 0%, #0A1B33 50%, #0B2545 100%); color: #D6E4FF; box-sizing: border-box; } .stat-card { grid-column: span 3; } .trend-panel { grid-column: span 8; } .radar-panel { grid-column: span 4; } .college-panel { grid-column: span 5; } .dorm-panel { grid-column: span 7; }

每个面板再套一层半透明渐变背景和 1px 发光边框,视觉上像“悬浮模块”。发光效果用 box-shadow 做,注意不要全屏都发光,否则会刺眼,我只在面板顶部加一条 2px 的青色描边,下面渐隐到透明。

5.2 过渡动画:数据更新时不至于闪瞎眼

数据刷新时如果直接重绘,整个图表会跳一下,观感很差。G2 5 默认有补间动画,但如果你在renderChart里先chart.clear()再重新声明 mark,动画会重置。更好的方式是chart.changeData(nextData),G2 会自动以旧数据为起点平滑过渡到新数据:

function updateChart(nextData) { chart.changeData(nextData); chart.render(); }

不过changeData只更新数据不更新配置,如果你的数据结构发生大变化(比如从柱状图切成折线图),还是得销毁重建。我在定时刷新方案里严格约束后端接口返回结构不变,这样就能用最平滑的更新方式。

5.3 大屏自适应:transform 缩放方案的一次教训

大屏常见的适配方案有 rem 和 transform 缩放。我最初用 CSS 媒体查询,结果显示器一换就错位。后来改成“以 1920×1080 为设计稿,用 transform scale 等比缩放”:先按 1920 宽度写死内部尺寸,再在根容器上根据实际宽高计算缩放倍率:

function resize() { const scaleX = window.innerWidth / 1920; const scaleY = window.innerHeight / 1080; const scale = Math.min(scaleX, scaleY); dashboardRef.value.style.transform = `scale(${scale})`; dashboardRef.value.style.transformOrigin = 'top left'; } window.addEventListener('resize', resize);

这样做唯一的问题是 canvas 的尺寸和 CSS 显示尺寸可能不一致,tooltip 偶尔出现偏移。后面我会在踩坑章节详细说这个问题的根治办法。

6. 性能调优:上万条打卡记录下的大屏不卡

大屏项目经常在一个浏览器标签页里挂几天,性能调优不能只看首屏加载,还得看长时间运行的稳定性。我从数据量、渲染频率和对象生命周期三个层面做了优化。

6.1 数据瘦身:别把所有原始记录全塞给图表

第一版我直接把 1 万条打卡记录同时传给折线图和热力图,结果是浏览器卡到无法呼吸。后来我梳理清楚一个原则:G2 图表只接收“聚合后的数据”,不做聚合计算;聚合在数据层完成后传给图表的是几十个点的小数组,而不是上万条原始数据。折线图只需要 24 个点,热力图最多几百个格子。

这一条对大屏项目来说是最有用的优化,没有之一。聚合逻辑比如把 1 万条原始记录变成学院 8 项指标、24 小时趋势,全部在 mock 层完成,图表层永远只拿轻量数组。

6.2 定时刷新与实例生命周期管理

大屏的实时性通常靠轮询模拟,每 30 秒拉一次数据然后changeData。但如果组件被切走或者页面隐藏了,定时器还在跑,就会白白浪费性能。我统一在onActivated/onDeactivated钩子里暂停和恢复轮询:

import { onActivated, onDeactivated } from 'vue'; onActivated(() => startPolling()); onDeactivated(() => stopPolling());

另外,每个图表组件卸载时必须chart.destroy(),否则 canvas 实例和事件监听器会堆积在内存里。我基本不在手写组件里放过多的setInterval,统一交给父组件调度,子组件只通过 watch props 响应数据更新。

7. 踩坑记录:这个项目里最花时间的四个问题

做这个项目的过程并不顺畅,有几个问题卡了我很久,写出来帮大家避开。

7.1 图表在 display:none 容器里渲染后宽度为 0

大屏刚加载时,某些面板可能处于折叠状态,或者我用v-if控制图表显隐。这时候如果 G2 初始化时容器还没有宽度,图表渲染出来就是个 0 宽度的空画布,等容器再显示时也不会自动恢复。我排查了很久,最终确定两个方案:

  • 尽量用v-show而不是v-if,保证容器始终在布局中;
  • 如果必须用v-if,在显示后调用一次chart.changeSize(container.clientWidth, container.clientHeight),或者直接调用nextTick后重新chart.render()。

7.2 transform 缩放后 tooltip 错位

用transform: scale()缩放整个大屏后,鼠标在 canvas 上的坐标和实际元素位置之间会有偏差,G2 的 tooltip 定位就会跟着偏移。网上很多方案是调整 transform-origin,但我试了只能解决部分场景,并不能完全根治。最后的建议是:大屏项目如果非常依赖 tooltip,优先使用 rem + 按视口宽度动态设置根字号方案,而不是整体 transform 缩放。如果舍不得 transform 的等比效果,可以在resize时把缩放倍率保存下来,在 tooltip 回调里手动补偿坐标。

7.3 定时刷新时旧图例和旧事件监听堆积

我第一次做轮询刷新时,直接在定时器里反复执行new Chart() + chart.render(),忘了销毁旧实例,结果页面跑了半小时后切图卡顿,内存一路飙升。G2 5 的clear()方法只清除画布内容,不销毁实例,正确做法是每次新数据到达后用changeData,真正离开页面时用destroy()。如果需要彻底重建图表的配置,也要先destroy()再new Chart,不要叠加实例。

7.4 G2 的 scale 类型推断问题

热力图和雷达图这类离散字段,如果传入数字格式的数据,G2 会自动推断成线性轴,导致图表比例完全不对。比如宿舍楼层 1、2、3 中间可能会多出 1.5 层。解决方案很简单:把离散字段全部转成字符串,或者在 scale 里显式指定type: 'band'。这个知识点不复杂,但第一次遇到时很容易怀疑是数据问题,建议写在代码注释里提醒后来维护的人。

8. 源码结构、二次开发与后续扩展方向

这个项目的完整源码按前文目录组织,核心文件就是 mock 数据生成器、BaseChart 封装和大屏布局页面。拿到源码之后,你不需要理解每一行 G2 代码,只要记住一条开发链路:原始数据 → 聚合处理 → 传给图表组件 → 更新视图。接入真实后端时,只需要把api/dashboard.js里的 mock 返回替换成fetch('/api/dashboard/summary'),并在vite.config.js里配置代理即可:

export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, }, }, });

8.1 如果我想换成企业考勤大屏

这套代码不仅适用于高校打卡,稍微改一下数据字段就能变成企业考勤、门店巡检甚至设备打卡监控。需要替换的无非是三处:mock/checkin.js 里的数据生成器、顶部 StatCard 的指标名称、以及雷达图的四个维度指标。图表层几乎不需要改动,因为所有图表都只认{ name, value, category }这类通用结构。

8.2 下一步扩展:接入 WebSocket 实时推送

轮询本质上还是被动拉取,如果预算和时间充裕,建议把数据链路升级到 WebSocket。后端在打卡事件发生时主动推送一条新记录,前端拿到后做三件事:更新今日总览计数、更新最近打卡列表、在趋势图上追加一个点。G2 对增量更新的支持不如全量数据替换方便,我目前的经验是全量changeData哪怕数据量到几千点也足够流畅,不需要刻意做增量渲染。

我在实际项目里做这种大屏的体会是:技术本身不复杂,真正花时间的是数据口径的定义、布局细节的打磨和性能细节的调优。Vue3 + G2 的组合既能保持工程化清晰度,又能用图形语法灵活地表达任何自定义可视化场景,是很适合做数据大屏的一套方案。你拿到源码后先从 mock 数据切到自己的真实接口,多跑几天看看交互和性能,再根据领导的口味调整配色和指标,这块屏就能真正在值班室里发光了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询