数字化大屏适配:从画布缩放到图表响应

本文是一篇实现教程,假设项目已经选择了固定画布固定舞台 + 内部弹性布局。如果还没有确定页面策略,先阅读选型篇:固定画布、响应式与混合布局

这里集中解决实现阶段的四个问题:Vue 如何测量真实容器并缩放 1920×1080 舞台,背景和业务画布如何分层,布局档位如何控制信息密度,以及 ECharts 如何响应自身容器。配套代码位于 example/digital-screen-adaptation

一、实现前先分清三个尺寸

  1. 视口:浏览器或 Electron 内容区真实可用的 CSS 像素宽高,记为 Vw × Vh
  2. 设计画布:设计稿坐标,本文使用 Dw × Dh = 1920 × 1080
  3. 图表容器:某个 ECharts 节点在舞台内部最终获得的布局宽高。

布局计算应读取容器的 clientWidth/clientHeightResizeObserverEntry.contentRect。4K 是物理分辨率;受系统缩放和浏览器缩放影响,网页得到的 CSS 像素不一定是 3840×2160。devicePixelRatio 影响渲染清晰度,不直接作为布局倍率。

这三个尺寸对应三层职责:视口决定舞台能放多大,设计画布提供稳定坐标,图表根据自己的容器调整绘图区和信息密度。

二、用 Vue 实现固定舞台缩放

舞台使用 scale = min(Vw / 1920, Vh / 1080),保证整张设计画布完整显示。下面是一套可以直接放进 Vue 3 组件的完整结构。

<template>
  <div ref="host" class="viewport">
    <main class="stage" :class="`stage--${mode}`" :style="stageStyle">
      <header>大屏标题</header>
      <section class="content">
        <aside>左侧指标</aside>
        <div>中央地图与图表</div>
        <aside>右侧指标</aside>
      </section>
    </main>
  </div>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'

const DESIGN_WIDTH = 1920
const DESIGN_HEIGHT = 1080

const host = ref<HTMLElement | null>(null)
const width = ref(DESIGN_WIDTH)
const height = ref(DESIGN_HEIGHT)
let observer: ResizeObserver | null = null

const scale = computed(() =>
  Math.min(width.value / DESIGN_WIDTH, height.value / DESIGN_HEIGHT),
)

const mode = computed(() => {
  const aspect = width.value / Math.max(height.value, 1)
  if (aspect < 1.45 || scale.value < 0.68) return 'compact'
  if (aspect > 2.15) return 'wide'
  return 'standard'
})

const stageStyle = computed(() => ({
  width: `${DESIGN_WIDTH}px`,
  height: `${DESIGN_HEIGHT}px`,
  transform: `scale(${scale.value})`,
  transformOrigin: 'center center',
}))

onMounted(() => {
  if (!host.value) return
  observer = new ResizeObserver(([entry]) => {
    if (!entry) return
    const { width: nextWidth, height: nextHeight } = entry.contentRect
    if (nextWidth !== width.value) width.value = nextWidth
    if (nextHeight !== height.value) height.value = nextHeight
  })
  observer.observe(host.value)
})

onBeforeUnmount(() => observer?.disconnect())
</script>

<style scoped>
.viewport {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100dvh;
  overflow: hidden;
}
.stage {
  flex: none;
  display: flex;
  flex-direction: column;
}
.content {
  display: grid;
  grid-template-columns: 420px minmax(0, 1fr) 420px;
  gap: 24px;
  flex: 1;
  min-height: 0;
}
.stage--compact .content {
  grid-template-columns: 340px minmax(0, 1fr) 340px;
}
</style>

transform 只改变视觉尺寸,.stage 的布局尺寸仍是 1920×1080。不要再让根字号随视口变化,也不要对子区域做第二次 scale();否则实际尺寸会由多套倍率共同决定。

这里使用内联 transform,因此 CSS 不再声明另一套 transform: scale(var(...))。如果选择 CSS 变量写法,也应只保留一个倍率来源。

compactwide 的阈值是示例值。实际项目要用真实标题、卡片和图例验收后确定;档位负责隐藏次要说明、改变列宽或减少信息量,不能只为了填满窗口而任意拉伸内容。

三、把背景层与业务画布分开

背景可以铺满真实视口,关键数据继续放在等比缩放的舞台中。只需在上一节的 .viewport 中增加一个与 .stage 同级的背景层:

<div class="viewport">
  <div class="background" aria-hidden="true"></div>
  <main class="stage">关键指标和图表</main>
</div>
.viewport {
  position: relative; /* 为绝对定位的背景提供参照。 */
}
.background {
  position: absolute;
  inset: 0;
  background: url('/screen-background.webp') center / cover no-repeat;
}
.stage {
  position: relative; /* 让业务画布位于背景层上方。 */
}

视频背景可以使用 object-fit: covercover 会保持比例并裁切边缘,所以视觉焦点要放在安全区域,关键数字不能烘焙进背景图片。

固定画布内部尽量使用画布坐标。例如 grid-template-columns: 420px minmax(0, 1fr) 420px 在 1920px 舞台中含义稳定。若内部使用 23vw,它会先随真实视口变化,然后又随舞台整体缩放,形成两套尺寸机制。视口单位更适合背景层和舞台外浮层。

四、ECharts:监听容器,不只监听窗口

ECharts 在 init() 时读取容器大小;初始化前容器必须已有非零宽高。图表父级通常要有确定的高度,Flex 子项要设置 min-height: 0,图表节点再使用 flex: 1; min-height: 0。否则图表可能获得 0 高度,或把整个面板撑开。ECharts 官方尺寸指南特别提醒了初始化尺寸,并指出 CSS/JS 改变容器大小时,窗口 resize 不一定触发。

最直接的图表生命周期如下。完整版本在示例的 TrendChart.vue

let chart: echarts.ECharts | null = null
let observer: ResizeObserver | null = null
let frame = 0

function schedule(width: number, height: number) {
  cancelAnimationFrame(frame)
  frame = requestAnimationFrame(() => {
    if (!el.value || width < 1 || height < 1) return
    if (!chart) chart = echarts.init(el.value, undefined, { renderer: 'canvas' })
    chart.resize()
    chart.setOption(buildOption(width), true)
  })
}

onMounted(() => {
  observer = new ResizeObserver(([entry]) => {
    if (entry) schedule(entry.contentRect.width, entry.contentRect.height)
  })
  if (el.value) observer.observe(el.value)
})

onBeforeUnmount(() => {
  observer?.disconnect()
  cancelAnimationFrame(frame)
  chart?.dispose()
})

上面为突出流程省略了避免重复 resize/setOption 的判断;示例源码包含判断。requestAnimationFrame 使同一帧多次尺寸通知合并为一次。不要在 ResizeObserver 回调里无条件修改被观察节点的 CSS 宽高;如果写回造成尺寸再次变化,可能形成观察循环。MDN 的 ResizeObserver 文档说明了容器观察的用途。

路由切换、标签页、弹窗或 KeepAlive 还需注意生命周期:图表隐藏时容器可能是 0×0,等显示后再 resize();DOM 被销毁时要 dispose();缓存页面重新激活时可再同步一次尺寸。异步数据只更新 setOption(),不要为每次数据变化重新 init()

图表内部也要适配

chart.resize() 只调整绘图区域,并不会替你决定如何显示坐标轴文字。下面让窄容器减少刻度、缩小留白,并用 containLabel 给轴标签预留空间:

function buildOption(width: number): echarts.EChartsOption {
  const narrow = width < 420
  return {
    grid: { left: narrow ? 12 : 20, right: 20, top: 20, bottom: 14, containLabel: true },
    xAxis: {
      type: 'category',
      data: months,
      axisLabel: { interval: narrow ? 1 : 0, fontSize: narrow ? 14 : 16 },
    },
    yAxis: { type: 'value' },
    series: [{ type: 'line', data: values }],
  }
}

实际项目还要逐类检查:

  • 折线、柱状图:标签间隔、日期格式、轴名称、图例位置、grid 留白及 tooltip 是否越界;柱过密时考虑聚合或横向滚动,而不是把文字压到无法阅读。
  • 饼图、环图:半径应受容器短边约束;优先检查外部标签与引导线碰撞。小尺寸时改为内部百分比或旁边的 DOM 图例。
  • 地图和散点图:检查 geo 区域、点大小和标签层级;需要缩放时区分地图交互缩放和整屏视觉缩放。
  • Canvas 清晰度:整体放大到 4K 时检查文字与线条;Canvas/SVG 的选择取决于数据量和设备性能,不应靠反复重建图表解决模糊。ECharts 官方渲染器比较给出了选择依据。

五、验收清单与常见故障

场景应检查的结果
1920×1080对照设计稿检查位置、文字层级、图表留白
1280×720全屏缩小后重要数字仍能读;tooltip 可操作
2560×1440、3840×2160放大后图表、边框、背景清晰;动画不过载
2560×1080 或 3440×1440主体完整、留白或辅助区域有设计意图
拖动窗口、浏览器缩放、Electron 全屏切换不出现裁切、0 尺寸图表或持续重绘
面板自身变化、切换信息密度、路由缓存无窗口变化时,图表也能跟随容器更新

排查时按层次看问题:背景裁切cover 和焦点;整体错位查舞台尺寸、缩放中心和外层居中;面板溢出查 Grid/Flex 的 min-width: 0min-height: 0图表留白或截断查初始化时机、容器尺寸和内部 grid/axisLabelCPU 持续占用resize 事件互相派发、观察回调写回尺寸及重复初始化。

本文的方案适合以横向展示为主的数字化大屏。核心不是某一个缩放公式,而是让每层只承担自己的责任:视口决定可用空间,画布保持主视觉比例,布局档位控制信息密度,图表响应实际容器。

参考资料

数字化大屏适配:从画布缩放到图表响应

作者

panxiao

发布日期

2026 - 03 - 11

许可证

Unlicensed

评论