EMI云数据平台(数据大屏)— 面试问答
项目时间:2022年11月 - 2023年01月 | 工厂设备运行状态与化工用料实时数据展示 技术栈:Nuxt2 + Vue2 + DataV + v-scale-screen + ECharts + echarts-gl + Element UI + Less 线上地址:http://data-center.goomaker.com
Q1: v-zoom-up 自定义指令设计
面试官可能问:"v-zoom-up 自定义指令是怎么实现的?放大和引导线是怎么画的?"
回答:
v-zoom-up 是这个项目中最核心的自定义指令。它的作用是:鼠标悬浮在 DataV 边框盒子(dv-border-box-7 等)上时,出现一个放大按钮;点击后,该模块从原始位置居中放大到屏幕中央,原始位置出现一个 Canvas 绘制的引导线框(虚线框 + 方块阵列背景),点击遮罩层或引导线框可以关闭恢复原位。
完整流程如下:
1. 按钮生成:generateClickToCenterBtn(el, binding) 在每个绑定元素内部动态创建一个绝对定位的放大图标按钮(el-icon-zoom-in),默认隐藏(display: none)。悬浮时显示按钮,悬浮出去隐藏。按钮每次鼠标浮入时会重新通过 getBoundingClientRect() 获取元素最新位置存入 originRect,防止布局变化后坐标失效。
2. 居中放大:点击按钮后:
创建全屏半透明遮罩层(
position: fixed,z-index: 9999,background: rgba(0, 0, 0, 0.5)),带 300ms 淡入过渡通过 CSS
transition: all 0.3s ease-in-out让元素从原始位置平滑过渡到屏幕中心计算偏移量的核心公式:
javascriptconst scaleWidth = window.innerWidth / 2560; const scaleHeight = window.innerHeight / 1440; const diffX = (window.innerWidth - rect.width) / 2 / scaleWidth - elOffsetLeft; const diffY = (window.innerHeight - rect.height) / 2 / scaleHeight - elOffsetTop;因为外层 v-scale-screen 做了
transform: scale()(2560×1440 设计稿等比缩放),el.offsetLeft/offsetTop是基于缩放后布局的,所以需要除以 scale 系数反算真实偏移。el.style.left = diffX + 'px',el.style.top = diffY + 'px'让元素移到屏幕正中。缩放:
el.style.transform = scale(1.35),叠加发光阴影(box-shadow)
3. Canvas 引导线框(generateCanvas):放大动画结束后(transitionend 事件),调用 generateCanvas 在 body 上覆盖一个 Canvas:
- 绘制原始位置矩形框(虚线
setLineDash,带shadowBlur发光效果) - 在框内部绘制 10×10 像素的方块阵列(
drawRectInLine),两种深蓝色随机交替(#1d4aab/#132f6e),模拟科技感网格背景 - 绘制原始位置四角到目标位置四角的四条引导线
- 绘制目标位置矩形框(实线 + 发光效果)
4. 关闭:点击遮罩层或 Canvas 引导线框 → 元素 left/top 归零,transform: scale(1),遮罩层淡出(300ms 后移除 DOM)。
追问:"babel-visible 这个 arg 参数是干什么的?"
当 v-zoom-up:table-visible(即 binding.arg === 'table-visible')时,放大操作会让底部表格的表头变为半透明(classList.add('is-header-transparent')),关闭时恢复。因为表格被放大到屏幕中央后,DataV 边框盒子的发光边缘会和表格 head 层的深蓝色背景叠加,视觉上很突兀。表头半透明化让边框发光效果更突出。
追问:"为什么在 mouseenter 时用 setTimeout 300ms 重新获取 originRect?"
因为大屏展示时数据会定时刷新(每 5 秒拉一次后端数据),表格内容变化可能导致元素尺寸改变。如果缓存旧的 originRect,Canvas 引导线框的原始矩形框位置就不准了。每次悬浮时重新获取,确保坐标始终是最新的。同时加了 pointerEvents: 'none' 禁用按钮防止用户在坐标还没刷新完就点击。
追问:"normalShadow modifier 是做什么的?"
v-zoom-up.normalShadow="false" 用于中间大块的 dv-border-box-9——它原本有自己的发光样式,不需要指令再叠加 box-shadow。关闭时恢复 box-shadow: initial 而非默认的蓝色发光,保持其原本视觉效果。
Q2: echarts-gl 动态地球飞线图
面试官可能问:"echarts-gl 的 3D 地球飞线图是怎么做的?"
回答:
在首页(index.vue)中央区域,用 echarts-gl 实现了一个 3D 地球,展示全球业务分布——韶关→全球各地的飞线(出口业务线)和越南→全球各地的飞线(海外工厂线)。
核心配置:
Globe 地球:用 baseTexture(卫星图)+ heightTexture(地形高度图)两张纹理贴图构建 3D 地球。displacementScale: 0.04 产生轻微地形起伏,shading: 'color' 保留纹理色彩的同时有立体感。viewControl.autoRotate: true 让地球自动缓慢旋转(视角 28° 仰角)。
Lines3D 飞线:两个 lines3D 系列,分别用韶关和越南作为起点。每条飞线的 data 是 { coords: [[起点经度, 起点纬度], [终点经度, 终点纬度]] }。effect.trailLength: 0.5 控制流光拖尾长度,trailColor 区分颜色(韶关=青色 #18ffff,越南=蓝色 #448aff)。
随机飞线数据(randomData):韶关生成 33 条随机飞线,越南生成 32 条——起点固定,终点是 Math.random() * 360 - 180 的全球随机经纬度。这样地球上的飞线看起来随机向全球扩散,模拟全球业务流向。Math.random() 也让每次刷新页面飞线方向有变化,保持动态感。
Scatter3D 散点:三个固定标记点——北京(红色)、越南(蓝色)、韶关(青色),用 pin 图钉图标 + 白色文字标签标注在地球上。
追问:"为什么选择 Lines3D 而不是 LinesGL?"
echarts-gl 的 lines3D 是专门给 3D 地球(globe 坐标系)用的飞线图类型,自带流光特效(effect.show: true),不需要手动做动画。effect.period 控制流光速度(2 秒走完),trailLength 控制拖尾长度,blendMode: 'lighter' 让飞线叠加时产生叠加变亮效果——多条线交叉处会特别亮,视觉上更炫。
追问:"为什么不直接用 geo + lines 做 2D 飞线?"
2D 飞线图需要有世界地图的 geo 组件,而 echarts-gl 的 globe 本身就是 3D 的。项目追求的视觉效果就是"3D 地球旋转 + 流光飞线"的大屏震撼感。2D 飞线放在大屏上没有 3D 地球有冲击力。
Q3: v-scale-screen 屏幕自适应方案
面试官可能问:"大屏怎么适配不同分辨率的屏幕?"
回答:
用 v-scale-screen 实现等比缩放适配。设计稿固定尺寸 2560×1440。
核心原理:v-scale-screen 监听容器的 resize 事件,根据实际屏幕分辨率与设计稿尺寸算出缩放比例,取宽高比例中较小的值,用 CSS transform: scale() 等比缩放整个应用。
比如实际屏幕是 1920×1080:
- scaleX = 1920 / 2560 = 0.75
- scaleY = 1080 / 1440 = 0.75
- 两者相等,直接 scale(0.75)
如果实际屏幕是 3840×1080(超宽屏):
- scaleX = 3840 / 2560 = 1.5
- scaleY = 1080 / 1440 = 0.75
- 取较小值 0.75,两边留黑边
这样无论屏幕是 2K、4K 还是 1080p,界面都等比例展示,不会出现布局错乱。
Nuxt2 SSR 兼容处理:v-scale-screen 依赖浏览器 DOM,服务端渲染时会报错。页面用 <client-only> 包裹 <v-scale-screen>,只在客户端渲染。同时显示一个 dv-loading 加载动画(1234ms),等待组件初始化完成。
追问:"v-scale-screen 和 v-zoom-up 指令的坐标计算有什么关联?"
这是 v-zoom-up 指令设计中最关键的坑。因为 v-scale-screen 用 transform: scale() 缩放整个应用,元素的实际 offsetLeft/offsetTop 和屏幕像素坐标不一致。v-zoom-up 在居中计算时,需要通过 window.innerWidth / 2560 算出 scale 系数,然后 diffX = (window.innerWidth - rect.width) / 2 / scaleWidth - elOffsetLeft。除以 scale 系数就是反算真实布局偏移。这个公式调试了很久才得到正确结果。
Q4: 黑客帝国字幕雨效果
面试官可能问:"染色智能工厂页面的字幕雨是怎么实现的?"
回答:
factory.vue 和 theory.vue(智能工厂原理页)页面底层用了 Canvas 实现的"黑客帝国"风格代码雨(Matrix Rain)效果。
实现原理(components/matrix.vue):
- 初始化:Canvas 全屏(2560×1440),算出每列宽度 = 字体大小(18px),列数
columns = canvasWidth / fontSize - 雨滴位置数组:
rainDropPositionArray = new Array(columns).fill(0),每个元素代表该列的"雨滴"当前位置 - 动画循环(
requestAnimationFrame):- 每帧先画一层半透明背景(
fillStyle = rgba(0,0,0,0.12)),旧的字符会逐渐被覆盖→消失,形成坠落拖尾效果 - 遍历每列,
rainDropPositionArray[i]++下落一个字符高度,在该列绘制一个随机字符(从超长字符串里随机取一个:包含字母、数字、符号、日文假名) - 字符 50% 概率大写 / 50% 概率小写,增加随机感
- 当字符触底(
textYPostion > canvasHeight),90% 概率让该列从头重新下落
- 每帧先画一层半透明背景(
速度控制:speedCnt++ 计数器,每 speed 帧才绘制一次(默认 speed=2),即 30fps(60fps ÷ 2)。不降帧会导致字符下落太快看不清,降帧后效果更舒缓。
CSS 定位:Canvas 用 position: absolute + z-index: -1 放在页面最底层,不影响上层 DataV 边框盒子的交互。视频(theory 页)覆盖在 Canvas 之上,两人效果叠加。
追问:"为什么用 Canvas 而不用 CSS 动画?"
CSS 动画做字符下落需要为每一列创建 DOM 元素,2560 宽 ÷ 18px 字体 ≈ 142 列,每列多个字符同时在下落,DOM 节点数量会非常大。Canvas 只需一个元素,所有字符都是画上去的,性能远超 DOM 方案。而且 Canvas 可以直接控制字符内容(随机取)、颜色、透明度,比 CSS 灵活得多。
追问:"requestAnimationFrame 的兼容性怎么处理的?"
组件里手动做了 requestAnimationFrame 的兼容性封装——webkitRequestAnimationFrame / mozRequestAnimationFrame / oRequestAnimationFrame / msRequestAnimationFrame → 兜底 setTimeout(callback, 1000/60)。虽然现代浏览器基本都支持,但工业大屏可能跑在旧版 Chrome 上,多一层兼容保底。
Q5: 六边形背景动态闪烁
面试官可能问:"顶部的六边形背景是怎么做的?"
回答:
components/common/hexagon.vue 用纯 CSS 实现了蜂巢(六边形)网格背景,配合 JavaScript 定时器产生动态闪烁效果。
CSS 六边形:每个六边形格子宽 20px、高 22px,用 clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) 切成六边形。background-color: #0a284a(默认深蓝色),bright 类 = #0b3f7e(亮蓝色)。
蜂巢排列:5 行,每行 120 个六边形。偶数行 left: -30px,奇数行 left: -20px,形成交错排列的蜂巢效果。用 flex 布局 + flex-wrap: nowrap 保持每行不换行。
动态闪烁(generateGlitch):每 2 秒执行一次——遍历所有行和每个六边形,Math.random() > 0.5 随机切换 bright 类。每个格子独立随机亮/暗,产生"电路板闪烁"的科技感效果。transition: all 0.5s 让亮度切换有平滑过渡。
性能优化:六边形总数为 5 行 × 120 个 = 600 个。content-visibility: auto 让浏览器跳过不可见区域的渲染。mask 渐变让底部六边形自然淡出消失,配合顶部 header 的 DataV 装饰线。
追问:"为什么不用 Canvas 画六边形?"
Canvas 画六边形需要自己写绘制逻辑,且布局响应式适配麻烦。CSS clip-path 是浏览器原生支持的,配合 flex 布局自然适配不同宽度。600 个 DOM 元素虽然有开销,但 content-visibility: auto 可以有效降低渲染成本。而且 transition 动画比用 requestAnimationFrame 逐帧重绘更简洁。
Q6: Nuxt2 静态生成与预压缩优化
面试官可能问:"Nuxt2 项目的部署和性能优化怎么做的?"
回答:
静态生成:nuxt.config.js 里配了 target: 'static',npm run generate 后输出纯静态文件(HTML/CSS/JS),部署到 Nginx。Nuxt2 的 static target 会预渲染所有路由页面,不需要 Node 服务器。
Gzip + Brotli 双格式预压缩:nuxt-precompress 模块在构建时生成 .gz 和 .br 双格式压缩文件:
- Gzip:
compressionOptions: { level: 9 }(最高压缩率) - Brotli:
compressionOptions: { level: 11 }(最大压缩) - 过滤器:只压缩
.js/.css/.html/.txt/.xml/.svg - 阈值:文件 > 10KB 才压缩(
threshold: 10240),小文件压缩收益不大还可能变大 - 优先级:Nginx 优先返回 Brotli(
encodingsPriority: ['br', 'gzip']),不支持 Brotli 的浏览器降级到 Gzip
服务端:端口配了 7777(server: { port: 7777, host: '0.0.0.0' })。
追问:"为什么用静态生成而不是 SSR?"
数据大屏的数据是通过 Axios 从后端 API 实时拉取的(每 5 秒轮询),不是页面渲染时注入的。所以不需要 SSR,静态文件 + Nginx 部署即可。SSR 还需要维护 Node 进程,静态文件直接扔 CDN 更稳定。
追问:"Brotli 和 Gzip 的压缩率对比?"
Brotli 通常比 Gzip 高 15%-25%。对于大屏项目,ECharts + echarts-gl 的 JS 体积较大,Brotli 能显著减小传输体积。而且 Brotli 的 level: 11 是最高质量,构建时多花一些时间,部署后所有用户都受益。
Q7: 表格自动滚动与 sessionStorage 缓存
面试官可能问:"底部表格的自动滚动和数据缓存怎么做的?"
回答:
三个底部表格组件(染色化料、染色用料、浆染过程耗能)都实现了 requestAnimationFrame 驱动的自动滚动 + sessionStorage 数据缓存。
自动滚动(startScroll):
startScroll() {
const wrapper = table.bodyWrapper;
wrapper.scrollTop += 1; // 每帧向下滚 1px
if (wrapper.clientHeight + wrapper.scrollTop == wrapper.scrollHeight) {
wrapper.scrollTop = 0; // 滚到底自动回到顶部
}
timer = requestAnimationFrame(this.startScroll);
}每一帧向下滚动 1px,滚动速度约 60px/s(60fps × 1px)。触底后自动回到顶部,实现无限循环滚动。鼠标悬浮进入表格时 cancelAnimationFrame 暂停滚动(方便操作者查看某一行),鼠标离开时恢复。
数据缓存策略:
- 首次加载数据后,
sessionStorage.setItem('dyeChemicalTableData', JSON.stringify(data)) - 切换标签页回来时,如果 sessionStorage 有缓存就直接用缓存,不用重新请求
- sessionStorage 只在当前标签页生命周期有效,关闭浏览器标签页自动清除
追问:"为什么用 requestAnimationFrame 而不是 setInterval?"
requestAnimationFrame 是浏览器原生的动画帧 API,帧率与显示器刷新率同步(通常 60fps),滚动更平滑。setInterval 的定时不精确(可能因主线程忙碌而延迟或积压),且浏览器切到后台标签页时 setInterval 可能被降频(最低 1s),但 requestAnimationFrame 在后台标签页会自动暂停,回到前台恢复,更省资源。
追问:"为什么要做 sessionStorage 缓存?"
三个表格在底部 tabs 切换(染色化料 / 染色用料 / 浆染过程耗能),切回来如果用 v-if 会销毁组件数据丢失,需要用 v-if 避免三个表格同时渲染占用太多 DOM。所以每次切换都要重新挂载,用 sessionStorage 缓存可以避免重复请求后端。
Q8: 全屏 API 跨浏览器兼容
面试官可能问:"大屏的全屏功能怎么做的?不同浏览器怎么兼容?"
回答:
topHeader.vue 里封装了跨浏览器的 Fullscreen API 兼容层。
进入全屏(requestFullScreen):
const rfs =
el.requestFullScreen ||
el.webkitRequestFullScreen || // Chrome/Safari
el.mozRequestFullScreen || // Firefox
el.msRequestFullscreen; // IE/Edge
rfs.call(el);退出全屏(exitFullScreen):
const cfs =
document.cancelFullScreen ||
document.mozCancelFullScreen ||
document.msExitFullscreen ||
document.webkitExitFullscreen ||
document.exitFullscreen;
cfs.call(document);IE 兜底:如果以上 API 都不存在(如老版 IE),用 ActiveX 的 WScript.Shell.SendKeys("{F11}") 模拟 F11 按键。
全屏状态检测(isFullScreenOrNot):通过判断 screen.height - clientHeight <= 17(全屏时差值很小)+ document.fullscreenElement 等属性综合判断是否在全屏状态。
键盘快捷键:监听 keydown 事件,F12(keyCode === 122)未全屏时也能触发全屏,已全屏时不拦截(保持 F11 浏览器原生行为)。
追问:"为什么要自己封装而不是用 screenfull.js?"
screenfull.js 是社区库,但项目对体积敏感——Nuxt 构建的静态文件希望尽量小。全屏 API 兼容逻辑不到 30 行代码,完全没必要引入额外依赖。而且 IE 的 ActiveX 兜底是 screenfull.js 没有的。
Q9: 多路由页面架构与 DataV 装饰组件
面试官可能问:"这个项目有几个页面?是怎么组织的?"
回答:
Nuxt2 基于文件路由,三个页面:
首页(
/):EMI 云数据平台——核心大屏,展示机台运行效率、环保数据、染色智能工厂(3D 地球飞线)、水/汽/母液用量、母液雷达图、底部表格(染色化料/用料/耗能)染色智能工厂(
/factory):全屏黑客帝国代码雨 + DataV 边框盒子智能工厂原理(
/theory):代码雨背景 + MP4 视频展示(工厂工作原理)
DataV 装饰组件:大量使用 @jiaminghi/data-view 的装饰组件:
dv-border-box-7:发光边框盒子——用于左侧/右侧/底部的内容容器dv-border-box-9:中间大块的边框盒子(地球容器)dv-border-box-3/dv-border-box-8:工厂/原理页的边框盒子dv-decoration-1/2/3/5/8/10/12:各种装饰线、装饰条、装饰图形——用于标题下划线、tab 分隔线、顶部 header 装饰dv-digital-flop:数字翻牌器——展示机台效率百分比、投入长度/出轴总长等数值dv-loading:加载动画——页面初始化时的 Loading 效果
Header 导航:顶部 header 带三个导航按钮(返回首页、染色智能工厂、智能工厂原理),非首页时显示"返回首页"按钮。三个按钮用 skewX(±30deg) 做平行四边形倾斜设计,内部文字反向倾斜保持可读性。带 box-shadow 霓虹发光效果。
追问:"为什么用 Nuxt2 而不是 Vue CLI?"
当时(2022 年底)公司的官网(中恒智造官网)也是用 Nuxt2 做的,技术栈统一。Nuxt2 的 target: 'static' 可以输出纯静态文件部署到 Nginx,不需要额外维护 Node 服务器。而且 Nuxt 的 pages 目录自动生成路由,不需要手动配置 Vue Router。
行为面试常见题快速答
"为什么做这个项目?"
公司(中恒智造)做纺织行业的智能制造。客户工厂需要一个大屏展示系统——挂在工厂大厅或会议室,实时展示设备运行状态(机台效率)、原材料消耗(水/汽/母液用量)、环保数据(原水量/外排水量)、生产数据(染色化料/用料表格)。之前没有数据大屏,老板来视察只能看 Excel 表格。我从零搭建了这套 Nuxt2 + DataV + ECharts 的大屏方案。
"遇到的最大难点?"
v-zoom-up 指令的坐标计算:v-scale-screen 缩放后,
getBoundingClientRect拿到的是屏幕像素坐标,offsetLeft/offsetTop是缩放后的布局坐标,Canvas 引导线框又需要屏幕坐标。三个坐标系之间的转换调试了很久。发现需要用window.innerWidth / 2560算出 scale 系数来反算真实偏移。echarts-gl 地球的性能:3D 地球 + 65 条飞线(33 + 32)+ 3 个散点,在低配电脑上帧率很低。优化手段包括——降低 Globe 的
displacementQuality: 'medium'(中等精度地形)、关闭postEffect(后处理特效)、environment: 'auto'自动降低环境光计算量。Nuxt2 SSR 和纯客户端库的冲突:DataV、v-scale-screen、echarts-gl 都是纯客户端库,Nuxt SSR 时
require('echarts-gl')会报错(window is not defined)。解决方案是用if (process.client) { require('echarts-gl') }判断只在客户端加载,页面用<client-only>包裹整个大屏内容。
"有什么遗憾?"
- 没有用 WebSocket 做实时推送——数据刷新用的是
setInterval5 秒轮询,延迟较大且浪费带宽。如果用 WebSocket,数据更新能更及时。 - 页面间的代码重复——
factoryContent.vue和theoryContent.vue都是dv-border-box-3包一层内容,几乎一样,没有抽公共组件。 - 数据都是 mock 或模拟的(随机数
Math.random()),没有真正接入工厂的 IoT 传感器数据。如果能接入 MQTT 实时数据流,大屏的实用价值会更高。
"跟其他项目有什么关联?"
EMI 云数据平台是公司智能制造产品线的一部分。和 EMI 协同管控平台是同期的项目——管控平台是中台(微前端架构,整合 6+ 子系统),云数据平台是独立的数据大屏展示端。技术栈不同——管控平台用 Vue3 + Vite + TypeScript,云数据平台用 Nuxt2 + Vue2,但都是围绕纺织行业的数字化转型。v-scale-screen 屏幕适配方案在 SOP 看板、数字孪生、云数据平台三个项目中都复用了。
"DataV 和 ECharts 有什么区别?"
DataV(@jiaminghi/data-view)主要做"装饰性可视化"——边框盒子、装饰线、数字翻牌器、Loading 动画这些大屏常用的 UI 装饰组件。ECharts 做"数据可视化"——折线图、柱状图、雷达图、3D 地球这些真正的数据图表。两者互补——DataV 负责"好看",ECharts 负责"有数据"。
上一篇:07-EMI协同管控平台 下一篇:09-中恒智造官网