数据可视化大屏 前端屏幕多分辨率适配方案(vue,echarts)

数据可视化大屏 前端屏幕多分辨率适配方案(vue,echarts)


写在前面:
第一次写博客, csdn账号注册很久了, 应该是2010年注册的, 当时我还在上高中, 当时还在写易语言的, 有些问题搞不懂的会来csdn看大佬是怎么解决的, 也写了些没什么用的小工具上传到csdn, 当然现在都在用git了. 很多人好奇, " 你为什么要学易语言, 垃圾语言 " , 当时是实在看不懂英语, 但是对编程(当时的理解就是编程)很有兴趣, 非常想学.
现在回想从 c语言 易语言 vb javascript java python , 一路过来都在学习, 但是好像从来没有写过什么东西, 遇到过无数的bug, 掉了无数根头发, 现在大学毕业了, 赶上了2020年的疫情, 这次经历让我对生命有了新的看法, 也是时候静下心来写些东西, 沉淀一下自己的历程了! 第一次写博客不知道写什么, 胡乱写了这么一段, 留在这里, 反正也没有人看, 回头再看到自己第一篇博客的时候, 希望不忘初心!

项目代码链接: gitee 代码链接

一直在写 react 的, 新到一家公司, 同事都是写 vue , 改写 vue 不太习惯, 觉得还是 react 用起来更灵活更顺手一些. 好久没写前端了, 现在也回忆一下前端适配, 也回忆一下 vue 的写法

项目有一个数据可视化大屏的需求, 要展示一些数字资源的使用情况,里面有一些 echarts 的图表,使用量一般是用仪表盘的形式,使用情况是一段时间数据的柱状图.

数据大屏

在这里插入图片描述

这个项目是由我同事来写的, 他的习惯是直接按照设计稿的px像素直接写到页面上, 在电脑上预览的时候没有什么问题, 但是这个项目要部署在一块很大的大屏上的, 这个时候就发现了问题所在, 屏幕不适配!!!!


在这里插入图片描述

我也是刚来到这个公司和那个同事关系比较好, 我的工作也基本完成了, 所以我决定帮帮他. 选择一个合适的适配方案然后还需要快速的把代码完成改版, 最终我用java写了一个代码转换工具, 一键帮同事把代码转为适配

方案选择

  1. px 转 rem
  2. viewpoint
  3. 媒体查询 @media
  4. 计算屏幕缩放比 动态设置像素值

每个人有不同的适配经验, 我这个同事适配经验比较少, 之前写小程序适配是用的 rpx , 这个类似于 rem 了.


备选方案1: px转rem

px 转 rem 是基于页面的fontSize 设置一个 rempx 换算比例 比如 16px=1rem , 20px=1rem 根据不同的屏幕设计不同的, 页面fontSize, 在移动端开发中这种方案非常常见.

html { /* 普通状态 */
    font-size: 20px
}
html { /* 1.5倍分辨率 */
    font-size: 30px
}
html { /* 2倍分辨率 */
    font-size: 40px
}
.div-box {
    width: 5rem; /* 5倍的font-size 普通状态下 = 100px */
}

但是当前的项目可能并不适用:

  1. 页面中有其他已经开发有其他的元素有导航文字, 设置fontSize时会影响到页面中已经写好的内容
  2. 页面中使用了 echarts 图表, 里面的参数没办法应用 rem 的比例

备选方案2: 媒体查询

媒体查询是比较常见的屏幕适配方案了, 可以根据不同的屏幕大小提供不同的样式方案, 媒体查询可以很好的支持多数的pc端网页布局需要了.

@media only screen and (max-width: 1000px) {
    .div-class {
        width: 720px;
    }
}

但是问题也是比较明显:

  1. 大量书写媒体查询代码, 比较繁琐
  2. 针对多种屏幕进行适配, 也无法保证完全兼容所有的屏幕
  3. 也无法支持 echarts 图表中的参数进行适配

备选方案3: viewpoint 视口

viewpoint 基本是目前多数移动端开发都会使用的适配方式, 可以设置对移动端设备的的界面进行整体的缩放, 这种方式的适配是最佳的方案. 但是缺点是很明显的, 只能在移动端进行 viewpoint 适配, 我们目前的数据大屏项目就没办法用了.

<meta name="viewport" content="target-densitydpi=high-dpi" />


最终采用方案: 计算屏幕缩放比

我们的设计稿宽高比是 1920 * 960
由于这个数据可视化的项目是适配宽屏的, 我可以先铺满高然后屏幕左右可能会有空白, 空白的部分用背景图片填充就好了. 画面的布局像素依然使用设计标注的像素值然后再乘屏幕缩放比.

页面适配样例代码(vue) :

<template>
    <div class="chart-container" :class="chartContainer">
        <div :style="[{width:202*rate+'px',height:184*rate+'px',marginLeft:134*rate+'px',marginTop:40*rate+'px',}]">
        </div>
    </div>
</template>

<script>
  // 宽高比
  const scale = 1920 / 960; 
  // 屏幕导航高度
  const headerHeight = 47; 
  // 标签栏高度
  const tabHeight = 27; 
  // 标签栏间隔
  const pageMargin = 5; 
  // 设计稿高度
  const designHeight = 960; 
  // 画面上方间隔高度
  const marginTop = headerHeight + tabHeight + pageMargin;
  // 画布下方间隔高度
  const marginBottom = pageMargin;
  // 页面宽度
  const clientWidth = document.body.clientWidth;
  // 页面高度
  const windowHeight = document.body.clientHeight;
  // 面试高度去年 上方间隔 下方间隔
  const clientHeight = windowHeight - marginTop - marginBottom;
  // 画面高度
  const innerHeight = clientHeight;
  // 缩放比率
  const rate = innerHeight / designHeight;
  // 画面宽度
  const centerWidth = clientHeight * scale;
  // 画面左右侧空白宽度
  const paddingWidth = (((clientWidth - pageMargin - pageMargin) - (clientHeight * scale)) / 2)
  export default{
    data:()=>({
        chartContainer: {height: 181 * rate + 'px',},
    }),
    methods:{
        dataOtherEChart (eleId, label, value, itemValue, color, color1, temp3) {
        const _self = this;
        let chartEle = _self.$echarts.init(document.getElementById(eleId));
        let option = {
          title: {
            textAlign: 'center',
            textStyle: {
              rich: { num: { fontSize: 25 * rate,}, key: { fontSize: 15 * rate,}}
            },
            subtextStyle: { lineHeight: 30 * rate, fontSize: 15 * rate
            }
          }
        };
        chartEle.setOption(option, true);
      },
    }
}
</script>


改造前代码

有些是写在行内的 style

<div id="top-item1" class="chart-container">
    <div id="main11" style="width: 80%;height: 184px;margin-left: 134px;margin-top: 40px"></div>
    <div id="other11" style="width: 80%;height: 165px;margin-left: 138px;margin-top: 40px"></div>
    <div id="other12" style="width: 80%;height: 165px;margin-left: 120px;margin-top: 40px"></div>
    <div id="other13" style="width: 80%;height: 165px;margin-left: 120px;margin-top: 40px"></div>
</div>

高度直接使用了设计稿中的像素值 165px


有些是使用了css 样式定义的:

  .box-to-box{
    height: 100px;
    width: 85%;
    margin-top: 49px;
    margin-left: 60px;
    display: flex;
  }

把高度 间距 都设计成了设计稿里面的像素值, 他好像还不太会用flex 弹性盒布局, 这里的 display:flex 也没有生效

echarts 参数:

let option = {
          title: {
            subtextStyle: { lineHeight: 30, fontSize: 15 }
          },
        }}

这里的参数是没有单位的也需要按缩放比缩放

vue 代码转换工具

用代码转换工具将写死的像素值乘以缩放比例
gitee 代码连接

1.读取vue文件, 定义文件行链表 class的映射

    fileReader = new FileReader(url);
    // 读取文件
    bufferedReader = new BufferedReader(fileReader);
    
    // 结果文本
    StringBuilder resultText = new StringBuilder();

    // 行链表 用于查找 class样式名称
    LinkedList<String> lineList = new LinkedList<>();
    // class样式映射
    Map<String, Map<String, String>> classMap = new HashMap<>();


2.遍历行, 定义样式识别的正则表达式

    // 每行插入链表头
    lineList.addFirst(line);
    // class样式 识别正则
    Matcher classMatcher = Pattern.compile(".*?-?.*?:.*?px.*?;").matcher(line);
    // id class 绑定样式 识别正则
    Matcher classUseMatcher = Pattern.compile("(class|id)=\"([0-9a-z-])*?\"").matcher(line);


3.处理style 有px的位置乘以 rate

    if (line.contains("style=\"")) { // 处理style
        // 行文本头部加入结果文本
        resultText.append(line, 0, line.indexOf("style=\""));
        // style 代码正则
        Pattern pattern = Pattern.compile("style=\".*?\"");
        Matcher matcher = pattern.matcher(line);
        // 将 style="name:value;"  转为 :style="[{name:value}]"
        resultText.append(":style=\"");
        while (matcher.find()) {
            String styleStr = matcher.group();
            styleStr = styleStr.replace("style=\"", "").replace("\"", "");
            resultText.append(parseStyleList(styleStr));
        }
        resultText.append("\"");
        String[] tailArr = pattern.split(line);
        // 行文本尾部 加入结果文本
        if (tailArr.length != 0 && tailArr.length > 1) {
            resultText.append(tailArr[1]);
        }
    }


4.处理class样式 class 样式表转为 hashMap 有px乘以 rate

if (classMatcher.find()) { // 处理class样式
    // 遍历查找 class 名称
    for (String classNameLine : lineList) {
        // 查询  .class-name #id-name 样式定义 不支持 tag-name
        if (classNameLine.contains("{") && (classNameLine.contains(".") || classNameLine.contains("#"))) {
            String className = classNameLine.trim().replace(".", "").replace("#", "").replace("{", "");
            // 横线转驼峰
            className = lineToHump(className);
            // 如果是多重定义的class 只保留一个
            if (className.contains(" ")) {
                className = className.split(" ")[0];
            }
            // 处理样式键值对
            String styleStr = classMatcher.group().trim().replace(";", "");
            String[] styleArr = parseStyle(styleStr).replace(",", "").split(":");
            // class 键值对映射
            Map<String, String> innerClassMap = classMap.get(className);
            if (innerClassMap == null) {
                innerClassMap = new HashMap<>();
            }
            // class 键值对映射加入 class样式映射
            innerClassMap.put(styleArr[0], styleArr[1]);
            classMap.put(className, innerClassMap);
            break;
        }
    }
}


5.使用 class="class-name" 的地方 加入 :class="className"

if (classUseMatcher.find()) {
    String classUseStr = classUseMatcher.group();
    String classUseHumpStr = lineToHump(classUseStr.replace("class=", "").replace("id=", "").replaceAll("\"", ""));
    // 行文本头部加入结果文本
    resultText.append(line, 0, line.indexOf(classUseStr));
    resultText.append(classUseStr);
    resultText.append(" :class=\"");
    // class 转 v-bind:class 横线命名转驼峰
    resultText.append(classUseHumpStr);
    resultText.append("\"");
    // 行文本尾部加入结果文本
    resultText.append(line, line.indexOf(classUseStr) + classUseStr.length(), line.length());
}


6.vue data中加入 缩放比率 rate 组件中 有 rate 会自动缩放

    StringBuffer dataBuffer = new StringBuffer();
    Matcher dataMatcher = Pattern.compile("data.*?\n.*?return.*?\\{", Pattern.MULTILINE).matcher(resultText);
    if (dataMatcher.find()) {
        dataMatcher.appendReplacement(dataBuffer, "data: function () {\n" +
                "      return {\n" +
                "        rate,\n");
        for (String key : classMap.keySet()) {
            Map<String, String> innerClassMap = classMap.get(key);
            dataBuffer.append("        ");
            dataBuffer.append(key);
            dataBuffer.append(": {");
            for (String innerKey : innerClassMap.keySet()) {
                dataBuffer.append(innerKey);
                dataBuffer.append(": ");
                dataBuffer.append(innerClassMap.get(innerKey));
                dataBuffer.append(",");
            }
    //                    stringBuffer.append("        ");
            dataBuffer.append("},\n");
        }
    }
    dataMatcher.appendTail(dataBuffer);
    resultText = new StringBuilder(dataBuffer);


7.常量加入script中

String rateDefineStr = "\n" +
           "  const scale = 16 / 9\n" +
           "  const headerHeight = 47;\n" +
           "  const tabHeight = 27;\n" +
           "  const tabPadding = 5;\n" +
           "  const designHeight=1080;\n" +
           "  const marginTop = headerHeight + tabHeight + tabPadding;\n" +
           "  const marginBottom = tabPadding;\n" +
           "  const clientWidth = document.body.clientWidth\n" +
           "  const windowHeight = document.body.clientHeight;\n" +
           "  const clientHeight = windowHeight - marginTop - marginBottom;\n" +
           "  const innerHeight = clientHeight;\n" +
           "  const rate = innerHeight / designHeight\n" +
           "  const centerWidth = clientHeight * scale;\n" +
           "  const paddingWidth = (((clientWidth - 5 - 5) - (clientHeight * scale)) / 2);" +
           "\n  ;\n";
    StringBuffer constBuffer = new StringBuffer();
    Matcher constMatcher = Pattern.compile("export default \\{", Pattern.MULTILINE).matcher(resultText);
    
    if (constMatcher.find()) {
       constMatcher.appendReplacement(constBuffer, rateDefineStr);
       constBuffer.append("  export default {");
       constMatcher.appendTail(constBuffer);
       System.out.println(constBuffer);
    }


8.ecahrts 中的参数可以乘以 rate 常量

let option = {
  title: {
    subtextStyle: { lineHeight: 30 * rate , fontSize: 15 * rate }
  },
}}




代码中有些设计没有解释, 太晚了准备睡觉, 后续有空会再更新博客, 做一些思路上面的分享, 如果有遇到同样问题或者有疑问的朋友可以联系我, 这是我的第一篇博客中存在的问题也感谢大家能够指正.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容