一次搞懂資料大屏適配方案 (vw vh、rem、scale)

当下是吾發表於2024-03-12

前言

當接到視覺化大屏需求時,你是否會有以下疑問👇
如何做一款定製化的資料大屏?
開發視覺化資料大屏如何做自適應?
vw vh、rem、scale 到底哪種比較好?
時間不夠,有沒有偷懶的方法?

最近在公司開發了一個視覺化大屏,開發定製化大屏,大家可能都一個感受,開發大屏主要是兩方面的工作:

  • 大屏之關鍵-前期的自適應適配
  • 根據 ui 稿繪製圖表,調細節

而解決了適配問題後,後面就只是一個慢工出細活,耗時間的事情了。

適配方案分析

看了網上的各種方案,目前大家採用的大概有 3 種👇

以上 3 種方案在實際應用中該怎麼選擇視具體情況而定,也有看到大家說自適應在地圖的適配中會有一些相容問題,我這邊還沒有實踐過。

  • 如果想簡單,客戶能同意留白,選用 scale 即可
  • 如果需要相容不同比例的大屏,並且想在不同比例中都有比較好的效果,圖表佔滿螢幕,類似於移動端的響應式,可以採用 vw vh 的方案
  • 至於 rem,個人覺得就是 scale 和 vw vh 的綜合,最終的效果跟 scale 差不多

接下來介紹下三種方案的具體實現,方案中的程式碼都以 vue2.0 和 vue-cli3 搭建的 vue 專案為例,因為是 demo,圖表的一些細節就沒有過多細緻的調整了

方案一:vw vh

上效果

當螢幕的尺寸比例剛好是 16:9 時

當螢幕的尺寸比例大於 16:9 時

當螢幕的尺寸比例小於 16:9 時

實現思路

按照設計稿的尺寸,將px按比例計算轉為vwvh,轉換公式如下

假設設計稿尺寸為 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 作為寬高的,就會自動適應不同解析度的螢幕
css 方案 - sass

util.scss

// 使用 scss 的 math 函式,https://sass-lang.com/documentation/breaking-changes/slash-div
@use "sass:math";

// 預設設計稿的寬度
$designWidth: 1920;
// 預設設計稿的高度
$designHeight: 1080;

// px 轉為 vw 的函式
@function vw($px) {
  @return math.div($px, $designWidth) * 100vw;
}

// px 轉為 vh 的函式
@function vh($px) {
  @return math.div($px, $designHeight) * 100vh;
}

路徑配置
只需在vue.config.js裡配置一下utils.scss的路徑,就可以全域性使用了

vue.config.js

const path = require("path");

function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  publicPath: "",
  configureWebpack: {
    name: "app name",
    resolve: {
      alias: {
        "@": resolve("src"),
      },
    },
  },
  css: {
    // 全域性配置 utils.scs,詳細配置參考 vue-cli 官網
    loaderOptions: {
      sass: {
        prependData: `@import "@/styles/utils.scss";`,
      },
    },
  },
};

在 .vue 中使用

<template>
    <div class="box">            
    </div>
</template>

<script>
export default{
    name: "Box",
}
</script>

<style lang="scss" scoped="scoped">
/* 
 直接使用 vw 和 vh 函式,將畫素值傳進去,得到的就是具體的 vw vh 單位         
 */
.box{
    width: vw(300);
    height: vh(100);
    font-size: vh(16);
    background-color: black;
    margin-left: vw(10);
    margin-top: vh(10);
    border: vh(2) solid red;
}
</style>
css 方案 - less

utils.less

@charset "utf-8";

// 預設設計稿的寬度
@designWidth: 1920;

// 預設設計稿的高度
@designHeight: 1080;

.px2vw(@name, @px) {
  @{name}: (@px / @designWidth) * 100vw;
}

.px2vh(@name, @px) {
  @{name}: (@px / @designHeight) * 100vh;
}

.px2font(@px) {
  font-size: (@px / @designWidth) * 100vw;
}

路徑配置
vue.config.js裡配置一下utils.less

const path = require("path");

function resolve(dir) {
  return path.join(__dirname, dir);
}

module.exports = {
  publicPath: "",
  configureWebpack: {
    name: "app name",
    resolve: {
      alias: {
        "@": resolve("src"),
      },
    },
  },
  css: {
    // 全域性配置utils.less
    loaderOptions: {
      less: {
        additionalData: `@import "@/styles/utils.less";`,
      },
    },
  },
};

在 .vue 檔案中使用

<template>
    <div class="box">            
    </div>
</template>

<script>
export default{
    name: "Box",
}
</script>

<style lang="less" scoped="scoped">
/* 
 直接使用 vw 和 vh 函式,將畫素值傳進去,得到的就是具體的 vw vh單位         
 */
.box{
    .px2vw(width, 300);
    .px2vh(height, 100);
    .px2font(16);
    .px2vw(margin-left, 300);
    .px2vh(margin-top, 100);
    background-color: black;
}
</style>
定義 js 樣式處理函式
// 定義設計稿的寬高
const designWidth = 1920;
const designHeight = 1080;

// px轉vw
export const px2vw = (_px) => {
  return (_px * 100.0) / designWidth + 'vw';
};

export const px2vh = (_px) => {
  return (_px * 100.0) / designHeight + 'vh';
};

export const px2font = (_px) => {
  return (_px * 100.0) / designWidth + 'vw';
};
螢幕變化後,圖表自動調整

這種使用方式有個弊端,就是螢幕尺寸發生變化後,需要手動重新整理一下才能完成自適應調整

為了解決這個問題,你需要在各個圖表中監聽頁面尺寸變化,重新調整圖表,在 vue 專案中,也可以藉助element-resize-detector,最好封裝個 resize 的指令,在各圖表中就只要使用該指令就可以了,畢竟作為程式設計師,能偷懶就偷懶

  1. 安裝 element-resize-detector
npm install element-resize-detector --save
  1. 引入工具包在元件中使用或者在單獨的 js 中使用
import resizeDetector from 'element-resize-detector'
  1. 封裝 directive
// 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 中引入
import '@/directive/directive';
  1. html 程式碼
<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 工具函式
// 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. 將函式掛載到原型上
import {fitChartSize} from '@src/utils/dataUtil.js'
Vue.prototype.fitChartFont = fitChartSize;
  1. 這樣你可以在.vue檔案中直接使用this.fitChartSize()呼叫
<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>

方案二:scale

透過 css 的 scale 屬性,根據螢幕大小,對圖表進行整體的等比縮放,從而達到自適應效果

上效果

當螢幕的尺寸比例剛好是 16:9 時,頁面能剛好全屏展示,內容佔滿顯示器

當螢幕的尺寸比例小於 16:9 時,頁面上下留白,左右佔滿並上下居中,顯示比例保持 16:9

當螢幕尺寸比例大於 16:9 時,頁面左右留白,上下佔滿並居中,顯示比例保持 16:9

html 部分

<div className="screen-wrapper">
    <div className="screen" id="screen">

    </div>
 </div>

js 部分

<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部分

/*
  除了設計稿的寬高是根據您自己的設計稿決定以外,其他複製貼上就完事
*/  
.screen-root {
    height: 100%;
    width: 100%;
    .screen {
        display: inline-block;
        width: 1920px;  //設計稿的寬度
        height: 960px;  //設計稿的高度
        transform-origin: 0 0;
        position: absolute;
        left: 50%;
        top: 50%;
    }
}

實現思路

如何縮放

螢幕寬高比 < 設計稿寬高比,我們需要縮放的比例是螢幕寬度 / 設計稿寬度
螢幕寬高比 > 設計稿寬高比,我們需要縮放的比例是螢幕高度 / 設計稿高度

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%

接下來利用絕對定位將圖表定位到中間位置

position: absolute;
left: 50%;
top: 50%;

偷懶方法-外掛

v-scale-screen是使用 css 屬性 transform 實現縮放效果的一個大屏自適應元件,透過 scale 進行等比例計算,達到等比例縮放的效果,同時也支援鋪滿全屏,寬度等比,高度等比,等自適應方案,具體可查大屏自適應終極解決方案

方案三:rem + vw wh

上效果

當螢幕的尺寸比例剛好是 16:9 時,頁面能剛好全屏展示,內容佔滿顯示器

當螢幕的尺寸比例小於 16:9 時,頁面上下留白,左右佔滿並上下居中,顯示比例保持 16:9

當螢幕尺寸比例大於 16:9 時,頁面左右留白,上下佔滿並居中,顯示比例保持 16:9

實現思路

關於 rem
rem(font size of the root element),是 css3 中新增的一個大小單位,即相對於根元素 font-size 值的大小。
自適應思路
動態的計算出頁面的 fontsize 從而改變 rem 的大小。

    1. 拿 1920 * 1080 的標準螢幕大小為例,將螢幕分為10份,先計算rem 的基準值: 1920 / 10 = 192;
    2. 把所有元素的長、寬、位置、字型大小等原來的 px 單位全部轉換成 rem;
    3. 網頁載入後,用 js 去計算當前瀏覽器的寬度,並設定 html 的 font-size 為 (當前瀏覽器視窗寬度 / 10) 。
      這樣的話 10rem 就剛好等於瀏覽器視窗的寬度,也就可以保證 100% 寬度,等比例縮放設計稿的頁面了。

因此 rem + vw vh 方案要解決三件事

  1. 獲得 rem 的基準值;
  2. 頁面內寫一段 js 程式碼,動態的計算html根元素的font-size
  3. 螢幕變化後,圖表自動調整和圖表字型、間距、位移等的自適應。

實現方案

第一點:獲得 rem 的基準值

  1. 首先安裝 @njleonzhang/postcss-px-to-rem 這個包
npm i @njleonzhang/postcss-px-to-rem -D
  1. 在專案根目錄新建.postcssrc.js配置檔案
module.exports = {
  plugins: {
    autoprefixer: {},
    "@njleonzhang/postcss-px-to-rem": {
      unitToConvert: 'px', // (String) 要轉換的單位,預設是 px。
      widthOfDesignLayout: 1920, // (Number) 設計佈局的寬度。對於pc儀表盤,一般是 1920.
      unitPrecision: 3, // (Number) 允許 rem 單位增長到的十進位制數字.
      selectorBlackList: ['.ignore', '.hairlines'], // (Array) 要忽略並保留為 px 的選擇器.
      minPixelValue: 1, // (Number) 設定要替換的最小畫素值.
      mediaQuery: false // (Boolean) 允許在媒體查詢中轉換 px.
    }
  }
}
  1. 配置完成後,頁面內的 px 就會被轉換成 rem 了

第二點:動態的計算html根元素的font-size

  1. 在工具函式檔案中新建一個 rem.js 檔案,用於動態計算 font-size
(function init(screenRatioByDesign = 16 / 9) {
  let docEle = document.documentElement
  function setHtmlFontSize() {
    var screenRatio = docEle.clientWidth / docEle.clientHeight;
    var fontSize = (
      screenRatio > screenRatioByDesign
        ? (screenRatioByDesign / screenRatio)
        : 1
    ) * docEle.clientWidth / 10;
    docEle.style.fontSize = fontSize.toFixed(3) + "px";
    console.log(docEle.style.fontSize);
  }
  setHtmlFontSize()
  window.addEventListener('resize', setHtmlFontSize)
})()
  1. 在入口檔案 main.js 中引入 rem.js 檔案
import './utils/rem.js';

至此,頁面就已經可以實現 16:9 自適應了。

第三點:螢幕變化,圖表自適應
螢幕變化後,圖表自動調整字型、間距、位移等,此處參考上面 vw vh 的實現方式即可,在此就不重複贅述了

參考資料

    • 推薦一個echarts 的案列網站,需要什麼直接圖表直接在上面去找,可以省去很多查 echarts 配置的時間
      全網echarts案例資源大總結和echarts的高效使用技巧(細節版)

    • scale 方案參考: 資料大屏最簡單自適應方案,無需適配rem單位

    • vw vh 方案參考: Vue+Echarts企業級大屏專案適配方案

    • rem 方案參考:資料大屏rem適配方案

原文

相關文章