如何做一款自适应的数据大屏:`vw/vh`、`rem`、`scale` 应该怎么选?

如何做一款定制化的数据大屏?
开发可视化数据大屏如何做自适应?
vw vh、rem、scale 到底哪种比较好?

目前比较常见的方案主要有三种:

  • vw/vh
  • CSS scale
  • rem + vw/vh

这三种方案没有绝对的好坏,最终还是要根据项目需求来选择。

  • 如果希望实现简单,客户也能够接受屏幕比例不一致时出现留白,可以直接选择 scale
  • 如果需要兼容不同比例的大屏,并希望页面尽量铺满屏幕,可以选择 vw/vh
  • rem 可以理解为 scalevw/vh 之间的一种折中方案,但在数据大屏中并不一定比前两种更合适。

下面分别介绍三种方案的实现方式。

方案一:使用 vw/vh 实现大屏自适应(推荐)

1. 实现思路

按照设计稿的尺寸,将px按比例计算转为vwvh,转换公式如下

json 复制代码
假设设计稿尺寸为 1920*1080(做之前一定问清楚 ui 设计稿的尺寸)

即:
网页宽度=1920px
网页高度=1080px

我们都知道
网页宽度=100vw
网页宽度=100vh

所以,在 1920px*1080px 的屏幕分辨率下

1920px = 100vw

1080px = 100vh

这样一来,以一个宽 300px 和 200px 的 div 来说,其所占的宽高,以 vw 和 vh 为单位,计算方式如下:

vwDiv = (300px / 1920px ) * 100vw
vhDiv = (200px / 1080px ) * 100vh

所以,就在 1920*1080 的屏幕分辨率下,计算出了单个 div 的宽高

当屏幕放大或者缩小时,div 还是以 vw 和 vh 作为宽高的,就会自动适应不同分辨率的屏幕

2. Sass 方案

新建一个 utils.scss 文件:

scss 复制代码
// utils.scss
@use 'sass:math';

// UI 设计稿宽度
$design-width: 1920;

// UI 设计稿高度
$design-height: 1080;

// 将设计稿中的横向像素转换成 vw
@function vw($px) {
  @return math.div($px, $design-width) * 100vw;
}

// 将设计稿中的纵向像素转换成 vh
@function vh($px) {
  @return math.div($px, $design-height) * 100vh;
}

在 Vue 组件中使用:

vue 复制代码
<template>
  <div class="box">数据大屏</div>
</template>

<style lang="scss" scoped>
@use '@/styles/utils.scss' as *;

.box {
  width: vw(300);
  height: vh(100);
  margin-left: vw(10);
  margin-top: vh(10);
  font-size: vh(16);
  border: vh(2) solid red;
  background-color: #000;
}
</style>

这样开发时仍然可以直接使用设计稿标注的像素值,不需要关心每一个值最后应该等于多少 vwvh

3. Less 方案

如果项目使用 Less,可以新建一个 utils.less 文件:

less 复制代码
@charset "utf-8";

// UI 设计稿宽度
@designWidth: 1920;

// UI 设计稿高度
@designHeight: 1080;

// 横向尺寸转换
.px2vw(@name, @px) {
  @{name}: (@px / @designWidth) * 100vw;
}

// 纵向尺寸转换
.px2vh(@name, @px) {
  @{name}: (@px / @designHeight) * 100vh;
}

// 字体尺寸转换
.px2font(@px) {
  font-size: (@px / @designWidth) * 100vw;
}

在 Vue 组件中使用:

vue 复制代码
<template>
  <div class="box">数据大屏</div>
</template>

<style lang="less" scoped>
@import '@/styles/utils.less';

.box {
  .px2vw(width, 300);
  .px2vh(height, 100);
  .px2vw(margin-left, 10);
  .px2vh(margin-top, 10);
  .px2font(16);
  background-color: #000;
}
</style>

4. JavaScript 转换函数

有些样式需要通过 JavaScript 动态设置,这时也可以封装三个简单的工具函数:

js 复制代码
// screenUtils.js
const designWidth = 1920;
const designHeight = 1080;

// px 转 vw
export const px2vw = (px) => {
  return `${(px * 100) / designWidth}vw`;
};

// px 转 vh
export const px2vh = (px) => {
  return `${(px * 100) / designHeight}vh`;
};

// 字体按照设计稿宽度进行转换
export const px2font = (px) => {
  return `${(px * 100) / designWidth}vw`;
};

使用时直接传入设计稿中的像素值:

js 复制代码
const style = {
  width: px2vw(300),
  height: px2vh(100),
  fontSize: px2font(16),
};
屏幕变化后,图表自动调整

这种使用方式有个弊端,就是屏幕尺寸发生变化后,需要手动刷新一下才能完成自适应调整

为了解决这个问题,你需要在各个图表中监听页面尺寸变化,重新调整图表,在 vue 项目中,也可以借助element-resize-detector,最好封装个 resize 的指令,在各图表中就只要使用该指令就可以了,毕竟作为程序员,能偷懒就偷懒

  1. 安装 element-resize-detector
css 复制代码
pnpm install element-resize-detector --save
  1. 引入工具包在组件中使用或者在单独的 js 中使用
javascript 复制代码
import resizeDetector from 'element-resize-detector'
  1. 封装 directive
ini 复制代码
// directive.js
import * as ECharts from "echarts";
import elementResizeDetectorMaker from "element-resize-detector";
import Vue from "vue";
const HANDLER = "_vue_resize_handler";
function bind(el, binding) {
  el[HANDLER] = binding.value
    ? binding.value
    : () => {
      let chart = ECharts.getInstanceByDom(el);
      if (!chart) {
        return;
      }
      chart.resize();
    };
  // 监听绑定的div大小变化,更新 echarts 大小
  elementResizeDetectorMaker().listenTo(el, el[HANDLER]);
}
function unbind(el) {
  // window.removeEventListener("resize", el[HANDLER]);
  elementResizeDetectorMaker().removeListener(el, el[HANDLER]);
  delete el[HANDLER];
}
// 自定义指令:v-chart-resize 示例:v-chart-resize="fn"
Vue.directive("chart-resize", { bind, unbind });
  1. main.js 中引入
arduino 复制代码
import '@/directive/directive';
  1. html 代码
xml 复制代码
<template>
  <div class="linechart">
    <div ref="chart" v-chart-resize class="chart"></div>
  </div>
</template>

这里要注意的是,图表中如果需要 tab 切换动态更新图表数据,在更新数据时一定不要用 echarts 的 dispose 方法先将图表移除,再重新绘制,因为 resize 指令中挂载到的图表实例还是旧的,就监听不到新的 chart 元素的 resize 了,更新数据只需要用 chart 的 setOption 方法重新设置配置项即可。

图表字体、间距、位移等尺寸自适应

echarts 的字体大小只支持具体数值(像素),不能用百分比或者 vw 等尺寸,一般字体不会去做自适应,当宽高比跟 ui 稿比例出入太大时,会出现文字跟图表重叠的情况

这里我们就需要封装一个工具函数,来处理图表中文字自适应了

  • 默认情况下,这里以你的设计稿是 1920*1080 为例,即网页宽度是 1920px (做之前一定问清楚 ui 设计稿的尺寸)
  • 把这个函数写在一个单独的工具文件dataUtil.js里面,在需要的时候调用
  • 其原理是计算出当前屏幕宽度和默认设计宽度的比值,将原始的尺寸乘以该值
  • 另外,其它 echarts 的配置项,比如间距、定位、边距也可以用该函数
  1. 编写 dataUtil.js 工具函数
javascript 复制代码
// Echarts图表字体、间距自适应
export const fitChartSize = (size,defalteWidth = 1920) => {
  let clientWidth = window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth;
  if (!clientWidth) return size;
  let scale = (clientWidth / defalteWidth);
  return Number((size*scale).toFixed(3));
}
  1. 将函数挂载到原型上
javascript 复制代码
import {fitChartSize} from '@src/utils/dataUtil.js'
Vue.prototype.fitChartFont = fitChartSize;
  1. 这样你可以在.vue文件中直接使用this.fitChartSize()调用
css 复制代码
<template>
  <div class="chartsdom" ref="chart" v-chart-resize></div>
  </template>

  <script>
  export default {
    name: "dashboardChart",
    data() {
      return {
        option: null,
      };
    },
    mounted() {
      this.getEchart();
    },
    methods: {
      getEchart() {
        let myChart = this.$echarts.init(this.$refs.chart);
        const option = {
          backgroundColor: "transparent",
          tooltip: {
            trigger: "item",
            formatter: "{a} <br/>{b} : {c}%",
          },
          grid: {
            left: this.fitChartSize(10),
            right: this.fitChartSize(20),
            top: this.fitChartSize(20),
            bottom: this.fitChartSize(10),
            containLabel: true,
          },
          calculable: true,
          series: [
            {
              color: ["#0db1cdcc"],
              name: "计划投入",
              type: "funnel",
              width: "45%",
              height: "70%",
              x: "5%",

              minSize: "10%",
              funnelAlign: "right",

              center: ["50%", "50%"], // for pie

              data: [
                {
                  value: 30,
                  name: "下单30%",
                },
                {
                  value: 55,
                  name: "咨询55%",
                },
                {
                  value: 65,
                  name: "点击65%",
                },
                {
                  value: 60,
                  name: "访问62%",
                },
                {
                  value: 80,
                  name: "展现80%",
                },
              ].sort(function (a, b) {
                return a.value - b.value;
              }),
              roseType: true,
              label: {
                normal: {
                  formatter: function () {},
                  position: "inside",
                },
              },
              itemStyle: {
                normal: {
                  borderWidth: 0,
                  shadowBlur: this.fitChartSize(20),
                  shadowOffsetX: 0,
                  shadowOffsetY: this.fitChartSize(5),
                  shadowColor: "rgba(0, 0, 0, 0.3)",
                },
              },
            },

            {
              color: ["#0C66FF"],
              name: "实际投入",
              type: "funnel",
              width: "45%",
              height: "70%",
              x: "50%",

              minSize: "10%",
              funnelAlign: "left",

              center: ["50%", "50%"], // for pie

              data: [
                {
                  value: 35,
                  name: "下单35%",
                },
                {
                  value: 40,
                  name: "咨询40%",
                },
                {
                  value: 70,
                  name: "访问70%",
                },
                {
                  value: 90,
                  name: "点击90%",
                },
                {
                  value: 95,
                  name: "展现95%",
                },
              ].sort(function (a, b) {
                return a.value - b.value;
              }),
              roseType: true,
              label: {
                normal: {
                position: "inside",
              },
            },
            itemStyle: {
              normal: {
                borderWidth: 0,
                shadowBlur: this.fitChartSize(20),
                shadowOffsetX: 0,
                shadowOffsetY: this.fitChartSize(5),
                shadowColor: "rgba(0, 0, 0, 0.3)",
              },
            },
          },
        ],
      };
      myChart.setOption(option, true);
    },
  },
  beforeDestroy() {},
};
</script>

<style lang="scss" scoped>
.chartsdom {
  width: 100%;
  height: 100%;
}
</style>

可以参考一下这个 代码 big-screen-adapter.git

5. vw/vh 方案的优缺点

优点:

  • 可以根据视口宽高动态调整元素尺寸;
  • 屏幕比例变化后,页面仍然可以尽量铺满整个屏幕;
  • 适合需要兼容多种分辨率和宽高比的数据大屏。

缺点:

  • 字体、间距、定位和边距等尺寸都需要进行换算;
  • 当实际屏幕比例与设计稿差异较大时,横向和纵向的缩放比例并不一致;
  • 项目中的特殊组件和动态样式可能需要单独处理。

整体来说,如果项目需要兼容不同比例的大屏,又希望内容尽量填满整个视口,vw/vh 是比较合适的方案。

方案二:scale

通过 css 的 scale 属性,根据屏幕大小,对图表进行整体的等比缩放,从而达到自适应效果

html 部分

ini 复制代码
<div className="screen-wrapper">
  <div className="screen" id="screen">

  </div>
</div>

js 部分

xml 复制代码
<script>
  export default {
    mounted() {
      // 初始化自适应  ----在刚显示的时候就开始适配一次
      handleScreenAuto();
      // 绑定自适应函数   ---防止浏览器栏变化后不再适配
      window.onresize = () => handleScreenAuto();
    },
    deleted() {
      window.onresize = null;
    },
    methods: {
      // 数据大屏自适应函数
      handleScreenAuto() {
        const designDraftWidth = 1920; //设计稿的宽度
        const designDraftHeight = 960; //设计稿的高度
        // 根据屏幕的变化适配的比例
        const scale =
          document.documentElement.clientWidth /
          document.documentElement.clientHeight <
          designDraftWidth / designDraftHeight
          ? document.documentElement.clientWidth / designDraftWidth
          : document.documentElement.clientHeight / designDraftHeight;
        // 缩放比例
        document.querySelector(
          '#screen',
        ).style.transform = `scale(${scale}) translate(-50%, -50%)`;
      },
    },
  };
</script>

css部分

css 复制代码
/*
除了设计稿的宽高是根据您自己的设计稿决定以外,其他复制粘贴就完事
*/  
.screen-root {
  height: 100%;
  width: 100%;
  .screen {
    display: inline-block;
    width: 1920px;  //设计稿的宽度
    height: 960px;  //设计稿的高度
    transform-origin: 0 0;
    position: absolute;
    left: 50%;
    top: 50%;
  }
}

实现思路

如何缩放

屏幕宽高比 < 设计稿宽高比,我们需要缩放的比例是屏幕宽度 / 设计稿宽度

屏幕宽高比 > 设计稿宽高比,我们需要缩放的比例是屏幕高度 / 设计稿高度

javascript 复制代码
const scale = document.documentElement.clientWidth / document.documentElement.clientHeight < designDraftWidth / designDraftHeight ?
  (document.documentElement.clientWidth / designDraftWidth) :
  (document.documentElement.clientHeight / designDraftHeight);

如果我们拿到的设计稿宽高为: 1920 * 960 px ,而我们的屏幕大小是 1440 * 900 px,那么 1440/900 = 1.6,920/960 = 2

因为 1.6 < 2 (当前屏幕宽高比小于设计稿宽高比)

所以我们需要缩放的比例是:屏幕宽度除以设计稿宽度 = 1440/1920 = 0.75

如何居中

首先我们利用 transform:translate(-50%,-50%) ,将动画的基点设为左上角

transform-origin:设置动画的基点(中心点),默认点是元素的中心点

语法

transform-origin: x-axis y-axis z-axis;

然后利用transform:translate(-50%,-50%),将图表沿 x,y 轴移动 50%

接下来利用绝对定位将图表定位到中间位置

css 复制代码
position: absolute;
left: 50%;
top: 50%;

简单的方法-使用插件

v-scale-screen 是使用 css 属性 transform 实现缩放效果的一个大屏自适应组件,通过 scale 进行等比例计算,达到等比例缩放的效果,同时也支持铺满全屏,宽度等比,高度等比,等自适应方案

方案三:rem + vw/vh

rem 是相对于根元素 htmlfont-size 进行计算的。

使用这种方案时,通常会先根据屏幕宽度动态计算根元素的字号,再把设计稿中的 px 转换为 rem

js 复制代码
const designWidth = 1920;

function setRootFontSize() {
  const clientWidth = document.documentElement.clientWidth;
  document.documentElement.style.fontSize =
    `${(clientWidth / designWidth) * 100}px`;
}

setRootFontSize();
window.addEventListener('resize', setRootFontSize);

假设约定设计稿中的 100px 对应 1rem,那么:

css 复制代码
.box {
  width: 3rem;  /* 设计稿中的 300px */
  height: 1rem; /* 设计稿中的 100px */
}

在一些项目中,也会将 remvw/vh 混合使用:

  • 页面主体尺寸使用 rem
  • 横向、纵向定位使用 vw/vh
  • 特殊尺寸再通过 JavaScript 动态计算。

这种方式能够统一控制一部分尺寸,但整体实现和维护成本也会更高。从最终效果来看,它仍然需要面对屏幕宽高比不同的问题,因此并不一定比直接使用 vw/vh 更简单。

如果项目本身已经有成熟的 rem 适配体系,可以继续沿用;如果是新开发的数据大屏,则没有必要为了适配而专门引入一套 rem 方案。

三种方案应该怎么选择

最后再总结一下:

方案 优点 缺点 适用场景
vw/vh 能跟随屏幕宽高变化,兼容不同比例 每个尺寸都要转换,处理相对细致 需要铺满屏幕、兼容多种分辨率
scale 实现简单,代码量少,还原度高 屏幕比例不一致时会留白 固定环境、演示大屏、允许留白
rem + vw/vh 可以统一控制部分尺寸 方案更复杂,效果与前两种有重叠 已有成熟 rem 体系的项目

如果只给一个建议:

  • 项目要求不高,想快速完成适配,优先使用 scale
  • 项目需要适配不同尺寸和不同比例的大屏,优先使用 vw/vh
  • rem 可以作为已有项目的补充,但不建议把它作为新大屏项目的第一选择。

大屏自适应没有一种方案能够覆盖所有情况。开发之前应先确认设计稿尺寸、目标设备分辨率、实际屏幕宽高比,以及客户是否可以接受留白。只有先把这些问题确认清楚,后续的技术选型才不会反复返工。

相关推荐
雪隐43 分钟前
WPF + MVVM 实战系列02-告别 INPC 手写时代,做个体面的现代 WPF 人
前端·后端·c#
肉肉不吃 肉1 小时前
无渲染组件和组合式函数
前端·javascript·vue.js
未来智慧谷1 小时前
从 Atlas 关停复盘:AI 应用的三种载体形态怎么选(独立应用 / 浏览器扩展 / 桌面宿主)
前端·人工智能·ai·架构·浏览器
sugar__salt1 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
Canace1 小时前
Harness Engineering 到底在做什么
前端·人工智能·ai编程
用户84298142418101 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端
snow@li1 小时前
Vue Axios封装与SpringBoot Payload封装全景关联分析(前后端数据交互底层闭环)
前端·vue.js·spring boot
Csvn1 小时前
🐛 React StrictMode 下 useEffect 执行两次:不是 Bug,是特性
前端
做好一个小前端2 小时前
ECharts 折线图大数据量性能优化参考
前端·性能优化·echarts