ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ECharts 数据可视化大屏开发避坑与工程化实践

ECharts 数据可视化大屏开发避坑与工程化实践 大屏这活儿做过一版和做过十版的人写出来的代码完全不是一个样子。同样一个 ECharts 可视化大屏新手会把 option 一股脑塞在组件里改一个字号要翻三百行做过几轮的人会把配置抽成工厂函数容器、适配、刷新各自成体系现场改需求十分钟搞定。这篇就把我这些年做 ECharts 数据可视化大屏时反复遇到的经典场景做一次集中梳理——适配方案怎么选、pxtorem 为什么对 ECharts 没用、折线图 x 轴刻度怎么算、tooltip 怎么自动换行、饼图 labelLine 末尾的小圆点为什么总是歪、中国地图怎么注册、柱子能不能用图片画。适合刚接手第一个大屏项目的前端也适合手里已经有半成品、但总觉得哪里不对的同学对照着查一遍。1. 先定适配方案再写第一行代码大屏项目最容易出现的返工不是图表画得不好看而是适配方案中途换。等你把二十个图表组件写完才发现现场那块屏是 3840×1080 的横向拼接屏之前按 rem 铺的布局全乱那一刻的绝望感我经历过一次就再也不想经历第二次。1.1 设计稿尺寸和现场屏宽永远不一致业内默认的设计稿尺寸大多是 1920×1080也有用 2560×1440 的但客户现场的屏幕可能是 4K 显示器、超宽屏、拼接屏甚至是竖屏的信息发布屏。两者的宽高比往往对不上1920×1080 是 16:93840×1080 是 32:9硬拉伸的结果就是圆变椭圆、字变扁字看着非常廉价。所以第一步要跟对方确认清楚三件事物理分辨率是多少、是不是多屏拼接拼接屏之间有几毫米的物理缝靠边的内容要预留安全区、浏览器是否固定用某个内核版本的终端机。这三件事问清楚方案基本就定了。1.2 scale 缩放、rem、vw/vh 三种路线的取舍我把常见的三种方案放在一起对比直接看结论比看原理快方案实现方式优点缺点适用场景transform scale容器固定 1920×1080JS 算 scale 后整体缩放代码零改动ECharts 内部字号跟着缩放效果所见即所得缩放后非整数像素细边框可能发虚单屏、大屏首选remhtml 的 font-size 跟随屏宽CSS 尺寸写 rem布局弹性字体比例自然只影响 CSS管不到 canvas 内部响应式官网、管理后台vw/vhCSS 单位直接按视口计算不依赖 JS纯 CSS 就能跑同样管不到 canvas 内部简单页面、纯 DOM 大屏我的选择很明确固定画布 transform scale。原因很实在ECharts 是在 canvas 上画图的option 里的fontSize: 14是实打实的像素数值不是 CSS 单位。只要整体缩放画布里所有文字、线宽、图标会一起等比变化视觉比例天然正确不需要我去考虑每个图表该配多大字号。scale 的计算逻辑就一行核心代码// 等比缩放Math.min 保证不裁切容器居中 function fitScreen(el, designWidth 1920, designHeight 1080) { const scale Math.min( window.innerWidth / designWidth, window.innerHeight / designHeight ); el.style.transform translate(-50%, -50%) scale(${scale}); }配套的 CSS 是固定宽高 绝对居中.screen { position: fixed; left: 50%; top: 50%; width: 1920px; height: 1080px; transform-origin: center center; transform: translate(-50%, -50%) scale(1); background: #0a1024; overflow: hidden; }这里的顺序不能反。translate(-50%, -50%)是先按元素自身尺寸把中心点推到视口中心再执行scale配合默认的transform-origin: center center元素中心始终锁在屏幕中心。如果写成scale在前translate在后缩放原点变了画面会往左上角飘。这个坑我调了半个小时才找到原因。注意16:9 设计稿遇到 32:9 超宽屏时等比缩放左右会留大黑边。此时可以在最外层再加一层背景图或渐变底覆盖整个视口内容层保持等比居中这样黑边区域也是有视觉内容的观感上不会显得空。1.3 pxtorem 对 ECharts 失效的根本原因搜索词里pxtorem 对 echarts 没起到效果 vue3这个问题出现频率极高我见过太多人在 Vue 项目里装了 postcss-pxtorem 插件CSS 里的字号确实变了结果图表里的文字纹丝不动。原因不复杂postcss-pxtorem 处理的是 CSS 文件里的 px 值它压根不碰 JavaScript 对象。你的 option 长这样const option { xAxis: { axisLabel: { fontSize: 14 } // 这是个 JS 数字postcss 看不见 } };postcss 插件解析的是.css、.vue里的style块JS 里的字面量它扫描不到也不该扫描。所以插件工作正常只是它管不了 ECharts。想在大屏里做响应式字号正确的做法是在 JS 侧自己维护一个基准比例// 以 1920 设计稿为基准 const BASE_WIDTH 1920; const ratio document.documentElement.clientWidth / BASE_WIDTH; const px (n) n * ratio; // 设计稿上的 18px实际写 px(18) const option { xAxis: { axisLabel: { fontSize: px(14) } }, series: [{ name: 产量, type: bar }] };不过说实话如果你用的是 transform scale 方案这个px()函数根本不需要——整体缩放已经把所有尺寸处理好了。这就是我推荐 scale 方案最实际的理由少一层换算少一类 bug。2. 图表初始化的那几个白屏瞬间大屏上线前最后一次联调最容易翻车的地方不是样式是某个图表没出来。页面不报错控制台干干净净就那块区域是空白特别难查。2.1 容器没有确定宽高ECharts 就什么都画不出来ECharts 初始化时必须能读到容器的clientWidth和clientHeight值为 0 就直接白屏而且不抛异常只会在控制台留一句Cant get DOM width or height。最常见的原因是父级用了 flex 布局子元素写了height: 100%但父级本身的高度是auto。此时高度链路断了子元素算出来的高度就是 0。解决办法有三种最顶层容器给定死高度比如.screen { height: 1080px; }链路一路height: 100%传下去图表容器用绝对定位父级position: relative子级inset: 0用 flex 时给中间层加min-height: 0避免 flex 子项被内容撑开后无法收缩。我个人的习惯是在样式里给所有图表容器强制加一层边框做调试.chart-box { border: 1px dashed rgba(0, 229, 255, 0.3); }调完再删掉。只要看得见框就知道是容器没撑开还是图表没画出来排查效率翻倍。2.2 Vue3 里 DOM 拿不到的两个时机问题在 Vue3 中用ref拿 DOM 再echarts.init有两个经典时机陷阱。第一个是v-if。如果图表容器外面套了v-if在onMounted阶段条件还是 falseref.value就是 undefined初始化直接报Initialize failed: invalid dom。这种情况要么用v-show要么把初始化放进watch等条件为真再执行。第二个是弹窗、抽屉这类渲染后才有尺寸的组件。即使 DOM 存在如果它此时是display: none宽高同样是 0。正确姿势是等动画结束或nextTick之后再 initimport { ref, onMounted, nextTick } from vue; import * as echarts from echarts; const boxRef ref(null); let chart null; onMounted(async () { await nextTick(); if (!boxRef.value) return; chart echarts.init(boxRef.value, null, { renderer: canvas }); chart.setOption(getOption()); });顺带说一句renderer的选择。大屏基本都选canvas因为svg渲染器在图表元素多的时候 DOM 节点会爆炸滚动和缩放的性能明显更差。只有当你要在图表上做节点级的事件委托、或者需要导出矢量图时才考虑svg。2.3 多个实例的销毁与 ResizeObserver一个中大型大屏通常有十几个图表实例。Vue 组件卸载时如果只清了定时器没dispose实例会挂在内存里切几次路由后内存曲线就开始往上爬。import { onBeforeUnmount } from vue; onBeforeUnmount(() { clearInterval(timer); chart chart.dispose(); chart null; });另外窗口尺寸变化的监听我早些年用window.addEventListener(resize, ...)后来全换成了ResizeObserver。理由是大屏里经常有折叠面板、侧边栏收起这类局部布局变化窗口尺寸没变但容器变了只监听 window 是感知不到的。const ro new ResizeObserver(() { chart chart.resize(); }); ro.observe(boxRef.value);同时记得给resize加节流。不加节流的话拖动窗口时每帧都触发重绘十几个图表同时 resize浏览器会卡到拖不动。我一般用 100ms 的节流体感上完全够用let ti null; const onResize () { clearTimeout(ti); ti setTimeout(() chart.resize(), 100); };3. 折线图和柱状图把默认配置改成大屏能用的样子ECharts 的默认配置是给 400px 宽的博客插图用的直接放大到 500×300 的大屏卡片里问题全出来了x 轴标签挤成一坨、柱子贴着边框、网格线糊成一片。3.1 x 轴刻度密度怎么算才不重叠ECharts 的axisLabel.interval默认是auto它会自己判断是否隐藏部分标签。但在大屏场景下auto经常判断失误尤其是标签文字长短不一时——短的能显示、长的被藏看起来像随机丢数据。我的做法是禁掉自动自己算const labelCount Math.floor(chartWidth / 70); // 每 70px 放一个标签 const interval Math.max(0, Math.ceil(data.length / labelCount) - 1); xAxis: { axisLabel: { interval, // 0 表示全显示N 表示每隔 N 个显示一个 rotate: data.length 12 ? 30 : 0, formatter: (v) (v.length 6 ? v.slice(0, 6) … : v), color: rgba(200, 220, 255, 0.7), fontSize: 13 } }interval的语义要记牢0是全部显示1是隔一个显示一个auto是交给 ECharts 判断function(index, value)则可以精确控制某几个刻度。如果你要做只显示首尾和中间点这种效果就得用函数形式。还有一个细节当数据量很大比如 24 小时每 5 分钟一个点共 288 个除了控制显示数量还要关掉标签的动画和阴影否则渲染会明显变慢。这种情况下我会把axisLabel.hideOverlap打开它是官方提供的防重叠开关配合自己算的 interval 双保险。3.2 grid 边距和坐标轴留白默认的 grid 是left: 10%, right: 10%, top: 60, bottom: 60听起来挺合理实际上大屏卡片窄的时候10% 换算出来只有三四十像素y 轴标签直接被截断。我固定用像素值 containLabel: truegrid: { left: 12, right: 20, top: 40, bottom: 8, containLabel: true // 让 grid 区域把坐标轴标签也算进去 }containLabel: true这个属性很多人不写结果就是明明留了left: 12y 轴文字还是被切掉半边。它的作用是告诉 ECharts计算绘图区时把轴标签占用的空间也算进来标签不会溢出到 grid 之外。坐标轴本身还有几个视觉细节值得调axisLine.show设成false更干净大屏上那根轴线通常是多余的splitLine.lineStyle.type用dashed再配opacity: 0.15的浅色网格不会抢数据的视觉柱状图的boundaryGap保持默认true柱子两边留白折线图必须设成false否则折线起点会离 y 轴有一段空隙看起来像数据缺失。3.3 柱子用自定义图片显示到底行不行echarts 柱状图柱子可以用自定义图片显示不这个问题答案是能但分两种情况。第一种情况是用叠加的方式正常画柱状图然后叠一个pictorialBar系列在同样的数据上把symbol设成图片地址。series: [ { name: 销量, type: bar, barWidth: 18, itemStyle: { color: rgba(0, 200, 255, 0.25) }, data: [120, 200, 150, 80, 70] }, { type: pictorialBar, symbol: image:///assets/bar-cap.png, // 自定义图片 symbolSize: [18, 12], symbolPosition: end, // 只在柱子顶部显示 symbolOffset: [0, -6], z: 3, data: [120, 200, 150, 80, 70] } ]第二种是把整根柱子做成图片用pictorialBar的symbolRepeat: true让图片在柱体上重复平铺{ type: pictorialBar, symbol: image:///assets/stripe.png, symbolRepeat: true, // 沿柱子方向重复 symbolSize: [18, 6], // 单个图案的尺寸 symbolMargin: 2, // 图案之间的间隔 symbolClip: true, // 超出数据范围的部分裁掉保证高度准确 data: [120, 200, 150] }symbolClip: true是关键不写的话图案会完整渲染柱子高度看起来比数据值高视觉上会骗人。我吃过这个亏客户拿尺子量屏幕对比数据差点以为系统算错了。注意symbol: image://后面必须是可访问的完整路径本地开发时用/assets/xx.png没问题但打包后路径变了就加载不出来。稳妥的做法是import icon from /assets/bar.png然后用模板字符串拼交给构建工具处理路径。3.4 3D 柱状图值不值得上bar3D需要额外引入echarts-gl并且要配grid3D、xAxis3D、yAxis3D、zAxis3D一整套写法和平面的柱状图完全是两套体系。对比项平面柱状图3D 柱状图bar3D额外依赖无echarts-gl数据可读性高能精确比对高度低透视会带来误导性能好一般大量柱子时明显掉帧交互点击、hover 都很顺旋转视角易遮挡触摸屏体验差我的态度是除非客户明确要求要有立体感否则一律不做 3D。3D 图最致命的问题是透视失真同样高度的柱子靠后的一根看起来更矮这在讲数据准确性的场景里是硬伤。真要做视觉冲击用渐变 圆角 高光的小技巧平面柱状图一样好看itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: #4facfe }, { offset: 1, color: rgba(79, 172, 254, 0.1) } ]), shadowBlur: 12, shadowColor: rgba(79, 172, 254, 0.5) }4. 饼图和地图视觉重心与数据表达的博弈大屏的视觉中心位通常留给地图或环形图。这两个图表的坑有个共同特点默认配置在细节上总是差那么一点点不改就一直觉得别扭但又说不上来哪里不对。4.1 labelLine 末尾小圆点偏移的真实成因echarts 饼图 labelLine 末尾小圆点偏移这个问题本质上是引导线和标签文字的定位逻辑没对齐。饼图的引导线是两段式的length是从扇区边缘往外延伸的第一段length2是拐弯之后的第二段拐弯后的末端才是标签文字的锚点。当标签文字宽度不一致时alignTo的默认行为会让文字以锚点为中心左右展开长文字往左伸、短文字往右伸看起来就像圆点歪了。正确的解法是把标签统一对齐到画布边缘series: [{ type: pie, radius: [45%, 68%], center: [50%, 52%], avoidLabelOverlap: true, label: { alignTo: edge, // 关键标签对齐到容器边缘 edgeDistance: 10, // 距边缘的留白 minMargin: 8, // 相邻标签的最小间距 formatter: {b}\n{d}%, color: rgba(220, 235, 255, 0.85), lineHeight: 18 }, labelLine: { length: 12, // 第一段 length2: 16, // 第二段 smooth: true, // 曲线更柔和 lineStyle: { color: rgba(140, 170, 210, 0.5) } } }]alignTo: edge让所有标签贴在左右两侧配合minMargin自动保持间距这才是大屏饼图该有的样子。另外avoidLabelOverlap一定要开它在标签太密时会自动做二次排布比手动算位置靠谱得多。4.2 环形图中心文案与多环叠加环形图中间那块空白浪费了太可惜通常要放一个总数或者核心指标。写法不复杂但位置需要手动调因为 ECharts 不会自动居中多行文本graphic: [ { type: text, left: center, top: 46%, style: { text: 总排放量, fill: rgba(180, 200, 230, 0.7), fontSize: 14 } }, { type: text, left: center, top: 52%, style: { text: 2,847, fill: #00e5ff, fontSize: 30, fontWeight: 600 } } ]用graphic而不是title的原因是graphic支持任意位置和多个文本块而title的副标题定位参数很有限。注意graphic不会随数据更新自动变数据刷新时要调用chart.setOption({ graphic: [...] })重新写一遍或者用chart.setOption的replaceMerge策略。如果要做内环占比 外环明细的双环图两个pie系列的radius要错开内环用[0%, 40%]外环用[50%, 68%]中间留 10% 的间隙视觉上层次清楚。4.3 中国地图的注册与数据合规ECharts 5 之后不再内置地图数据需要自己引入 GeoJSON 并注册import * as echarts from echarts; import chinaGeoJson from /assets/map/china.json; echarts.registerMap(china, chinaGeoJson); const option { geo: { map: china, roam: false, // 大屏上通常禁用缩放拖拽防止用户误操作 zoom: 1.15, itemStyle: { areaColor: rgba(20, 60, 110, 0.6), borderColor: rgba(0, 229, 255, 0.6), borderWidth: 1, shadowColor: rgba(0, 150, 255, 0.5), shadowBlur: 20 }, emphasis: { itemStyle: { areaColor: rgba(0, 200, 255, 0.35) }, label: { show: true, color: #fff } } }, series: [ { type: effectScatter, coordinateSystem: geo, rippleEffect: { scale: 3, brushType: stroke }, symbolSize: (val) Math.max(6, val[2] / 12), data: [ { name: 示例点, value: [116.4, 39.9, 320] } ] } ] };几个实操要点值得强调。第一roam在大屏上一般设false因为大屏多是触摸操作用户手指一划地图就飘走了找不回来。第二散点尺寸建议用symbolSize函数按数值映射比固定值更能表达数据差异。第三飞线用type: lines加effect: { show: true, trailLength: 0.3, symbol: arrow }配合curveness做弧度视觉效果最好。关于地图数据来源务必使用公开、标准、可商用的行政区划地理数据避免使用来源不明、边界表达不规范的版本这在正式项目里是必须守住的一条线。另外大屏上如果地图只做展示用途建议关闭所有交互并加一层静态遮罩防止误触。4.4 管线类图形的绘制思路工业、能源类大屏经常要画管道、线路的走向图。ECharts 没有现成的管道图形通常用custom系列自己画核心是坐标转换series: [{ type: custom, coordinateSystem: geo, renderItem: (params, api) { const p1 api.coord([116.4, 39.9]); const p2 api.coord([121.5, 31.2]); const width api.size([1, 1])[0] * 0.08; // 线宽随缩放变化 return { type: line, shape: { x1: p1[0], y1: p1[1], x2: p2[0], y2: p2[1] }, style: { stroke: rgba(0, 229, 255, 0.8), lineWidth: width, lineCap: round, shadowBlur: 10, shadowColor: rgba(0, 229, 255, 0.6) } }; }, data: [{}] }]api.coord把经纬度转成画布坐标api.size拿到一个数据单位对应的像素尺寸用它算线宽可以让管道随地图缩放同步变粗变细。如果要做流动效果就在管道上方叠一层lines系列带effect或者用 CSS 动画的前景层盖上去比在 canvas 里逐帧重绘省事。5. tooltip 和交互细节那些看着别扭的地方tooltip 是大屏上被点开次数最多的交互元素也是调整频率最高的。默认样式在深色背景上几乎没法看白底黑字、直角边框、文字一长就横向撑开。5.1 tooltip 自动换行的正确配置要让 tooltip 自动换行需要理解它的 DOM 结构外层是容器内层有一个系列名的 span 和内容的 div内层默认带white-space: nowrap所以文字会一直往右撑。第一种解法是直接覆盖 CSS 属性tooltip: { trigger: axis, confine: true, // 限制在图表容器内不会被裁掉 extraCssText: max-width: 320px; white-space: normal; word-break: break-all; box-shadow: 0 4px 16px rgba(0,0,0,0.6); border-radius: 6px;, backgroundColor: rgba(10, 30, 60, 0.92), borderColor: rgba(0, 229, 255, 0.4), textStyle: { color: #dceaff, fontSize: 13 } }white-space: normal覆盖掉内层的nowrapword-break: break-all保证长英文或数字串也能断行max-width约束宽度。这三个一起写才有效少一个都会出现设了宽度但文字还是跑出去的情况。第二种解法是自定义 formatter自己拼 HTML 结构控制力最强formatter: (params) { const list Array.isArray(params) ? params : [params]; const rows list.map((p) div styledisplay:flex;justify-content:space-between;gap:16px;min-width:180px; span stylecolor:${p.color};${p.seriesName}/span strong stylecolor:#fff;${p.value}/strong /div ).join(); return div stylefont-size:13px;line-height:20px; div stylecolor:#8fb4dd;margin-bottom:6px;${list[0].axisValue}/div ${rows} /div; }自定义 formatter 有个性能讲究它会在鼠标移动时被高频调用。如果里面做了复杂计算或者字符串模板嵌套很深滑动时会明显卡顿。工具函数能提前算好的就提前算好不要在 formatter 里现算。5.2 axisPointer 与高亮联动axisPointer是折线图上跟着鼠标走的那条竖线和标签默认样式是大块灰色跟深色大屏完全不搭tooltip: { axisPointer: { type: line, lineStyle: { color: rgba(0, 229, 255, 0.5), width: 1, type: dashed }, label: { backgroundColor: rgba(0, 120, 200, 0.9), borderRadius: 4, padding: [4, 8] } } }多个图表联动的场景比如鼠标移到折线图上地图对应区域高亮用echarts.connect把实例连起来最省事echarts.connect([chartA, chartB]);要注意connect是按group走的同一个 group 内所有实例的 tooltip 会同步。如果大屏上有十几个图表全连在一起反而乱我一般只连总览 明细这两三个真正需要联动的。6. 数据刷新与性能大屏要能连续跑一整周大屏和普通页面最大的区别在于运行时长。普通的后台页面用户点完就走大屏是挂在墙上 7×24 小时开着的任何一点内存泄漏或未清理的定时器几天后都会变成白屏或者卡死。6.1 定时器、页面隐藏与内存曲线定时刷新数据的定时器必须严格管理。除了组件卸载时清理还要考虑标签页隐藏时暂停let timer null; function startPolling() { stopPolling(); timer setInterval(fetchAndUpdate, 30000); } function stopPolling() { if (timer) { clearInterval(timer); timer null; } } document.addEventListener(visibilitychange, () { if (document.hidden) stopPolling(); else { fetchAndUpdate(); startPolling(); } });为什么要处理visibilitychange因为浏览器对隐藏标签页的定时器有节流策略恢复时可能一次性堆积多次回调导致瞬间发起一堆请求。而且大屏终端如果因为系统休眠断了网络恢复后一次性打十几个请求也很危险最好在恢复时先做一次单次刷新再重启轮询。6.2 setOption 的合并在什么时候会坑你setOption默认是合并模式新的 option 会跟旧的做深度合并。这在改数据时很方便但在两种场景下会出问题。第一种是系列数量变化。原来是 3 条折线新数据只有 2 条合并之后第 3 条还在显示的是上一次的陈旧数据。这时需要replaceMergechart.setOption(option, { replaceMerge: [series] });第二种是 dataZoom 和 legend 的选中状态。整体 setOption 会让滚动条位置被重置回初始值用户正在看第 70% 到 80% 的区间数据一刷新就跳回开头体验很差。处理方式是只更新数据不动配置// 推荐只改数据配置不动 chart.setOption({ series: [ { data: newLineData }, { data: newBarData } ] });这也是我坚持把图表配置和数据填充分开写的原因——配置一次性初始化好后续刷新只传数据既不重置交互状态也少一次深度合并的开销。6.3 大数据量下的降采样与动画取舍数据点超过一两千之后折线的渲染压力会明显上升。ECharts 提供了几个开关series: [{ type: line, large: true, // 开启大数据量优化 largeThreshold: 2000, // 超过这个数量走优化路径 sampling: lttb, // 降采样算法保留趋势特征 smooth: false, // 大数据时关掉平滑平滑计算很贵 animation: false, // 关掉入场动画 showSymbol: false, // 不画每个数据点的圆点 lineStyle: { width: 2 } }]sampling: lttb是最推荐的降采样方式它会在保留趋势的前提下把点数量砍到目标值视觉上几乎看不出差别。相比之下average会把峰谷抹平如果你要展示的是波峰波谷比如温度曲线用它会丢关键信息。动画方面我的经验是首屏加载时保留入场动画数据刷新时全部关闭。首屏动画是给客户看的高级感刷新时的动画纯粹是性能浪费还会让数字和图形在短暂时间内对不上。7. 把重复劳动压下去封装与主题做完两三个大屏项目之后你会发现真正重复的不是业务逻辑而是那一堆 option 配置。这时候就该做抽象了。7.1 一个能复用的图表组合式函数我把初始化、resize、销毁三件事打包成一个 composable// composables/useChart.js import * as echarts from echarts; import { onMounted, onBeforeUnmount, shallowRef, nextTick } from vue; import { registerTheme } from /charts/theme; registerTheme(); // 注册自定义主题 export function useChart(elRef, optionFactory, { theme screen-dark } {}) { const chart shallowRef(null); let ro null; const setData (payload) { if (!chart.value) return; chart.value.setOption(optionFactory(payload), { lazyUpdate: true }); }; onMounted(async () { await nextTick(); if (!elRef.value) return; chart.value echarts.init(elRef.value, theme, { renderer: canvas }); setData(null); ro new ResizeObserver(() chart.value chart.value.resize()); ro.observe(elRef.value); }); onBeforeUnmount(() { ro ro.disconnect(); chart.value chart.value.dispose(); chart.value null; }); return { chart, setData }; }这里用了shallowRef而不是ref。原因是 ECharts 实例是个巨大的对象用ref会做深度响应式代理不仅浪费性能某些情况下还会破坏实例内部的方法绑定。这类不需要响应式的重型对象一律用shallowRef或markRaw。lazyUpdate: true的含义是把更新推迟到下一帧执行。当你在同一个 tick 内连续调用多次 setOption它只会真正渲染最后一次这对批量更新的场景很有用。7.2 主题与配色的统一管理大屏配色最忌讳每个图表自己定颜色。我把主题抽成单独文件// charts/theme.js import * as echarts from echarts; export function registerTheme() { echarts.registerTheme(screen-dark, { color: [#00e5ff, #4facfe, #7c5cff, #ffb74d, #00e676, #ff5252], backgroundColor: transparent, textStyle: { color: rgba(210, 230, 255, 0.85) }, title: { textStyle: { color: #e6f2ff, fontSize: 16, fontWeight: 600 } }, categoryAxis: { axisLine: { show: false }, axisTick: { show: false }, splitLine: { show: false }, axisLabel: { color: rgba(170, 195, 225, 0.75) } }, valueAxis: { axisLine: { show: false }, axisTick: { show: false }, splitLine: { lineStyle: { color: rgba(120, 160, 210, 0.12), type: dashed } }, axisLabel: { color: rgba(170, 195, 225, 0.75) } }, tooltip: { backgroundColor: rgba(10, 30, 60, 0.92), borderColor: rgba(0, 229, 255, 0.4), textStyle: { color: #dceaff } } }); }主题能解决的问题比你想的多一是配色统一客户看着舒服二是减少重复配置每个图表的 option 体积能砍掉三分之一三是改版效率客户说整体换成蓝绿色系改主题文件一个地方就够了。配色还有个数不对不行的原则同色系相邻色不要超过 6 个否则人眼分不清。超过 6 个分类时用得最多的两类用最亮的颜色其余用相近的沉色靠明度而不是靠色相区分。7.3 目录结构建议一个能撑住后续迭代的大屏项目目录大致长这样src/ ├─ charts/ │ ├─ theme.js 主题注册 │ ├─ options/ │ │ ├─ lineFactory.js 折线图配置工厂 │ │ ├─ barFactory.js 柱状图配置工厂 │ │ ├─ pieFactory.js 饼图配置工厂 │ │ └─ mapFactory.js 地图配置工厂 ├─ composables/ │ ├─ useChart.js 图表生命周期 │ ├─ useScreenScale.js 大屏适配 │ └─ usePolling.js 轮询数据 ├─ components/ │ ├─ ChartBox.vue 图表卡片外壳标题边框图表 │ └─ panels/ 各业务面板 └─ assets/ └─ map/ 地理数据关键是把 option 写成工厂函数而不是常量对象export const lineFactory (source) ({ tooltip: { trigger: axis, confine: true }, grid: { left: 12, right: 20, top: 40, bottom: 8, containLabel: true }, xAxis: { type: category, boundaryGap: false, data: source.map(i i.time) }, yAxis: { type: value }, series: [{ type: line, smooth: true, showSymbol: false, areaStyle: { opacity: 0.15 }, data: source.map(i i.value) }] });工厂函数的好处是同一个配置可以接不同的数据源一套配置复用五六个图表改一处全局生效。8. 上线前我会走一遍的自检清单大屏项目的验收往往在客户现场改一行代码要跑一趟所以上线前我会按这个清单从头到尾过一遍能省掉很多往返。检查项具体动作常见后果分辨率适配用 100%、125%、150% 三种系统缩放各看一遍缩放后模糊、被裁切字体最小字号检查所有标签最小不小于 12px现场远距离看不清图表容器高度窗口拉窄到 1280 宽看是否塌陷图表白屏定时器清理切换路由 20 次看内存是否回落内存持续上涨断网表现拔网线后等待 5 分钟页面卡死无提示长文本把最长的一条数据名塞进去标签溢出、遮挡数据为空接口返回空数组图表报错或显示异常颜色对比关灯看屏幕看深色背景上的浅色文字灰度不足看不清数字格式检查千分位和单位后缀数字挤成一排其中我最想强调的是数据为空和长文本这两项。接口第一次返回空数组的场景开发阶段几乎遇不到但上线当天很可能因为数据还没接入而出现一旦图表抛异常导致整页白屏客户第一印象就崩了。空数据时给一个兜底if (!list.length) { chart.setOption({ title: { text: 暂无数据, left: center, top: middle, textStyle: { color: rgba(150, 175, 205, 0.5), fontSize: 14 } }, series: [] }); return; }长文本则建议在入口处统一做截断不要指望图表自己处理。写一个ellipsis(str, 8)工具函数所有标签和 tooltip 都过一遍比在每个 formatter 里各写一套可靠。还有一条经验是给大屏加一个隐形的开发者入口在页面角落放一个 8×8 像素的透明热区点开显示当前分辨率、缩放比例、各图表实例数量、最近一次数据更新时间。现场排查问题时这个小面板能帮你省掉一半的沟通成本也不需要开控制台。最后分享一个我在同事之间口口相传的小技巧大屏做完之后用浏览器截一张 1920×1080 的整页图跟设计稿做 50% 透明叠加对比。哪块间距不对、哪个图表的位置偏了几个像素一眼就能看出来比肉眼盯着屏幕找快太多了。这个方法我每次验收都用屡试不爽。
返回列表