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


写在前面:
第一次写博客, 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` ```html ``` 高度直接使用了设计稿中的像素值 `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 }
  },
}}


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

更多推荐

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