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


写在前面:
第一次写博客, 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 }
  },
}}


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

Logo

前往低代码交流专区

更多推荐