在构建可视化大屏的过程中,首先要先布局组件位置和大小。上两篇文章《关于各种像素概念和前端长度单位的理解》 和《CSS数学表达式calc()的规范草案翻译》 学习了 CSS 相对单位 vw
、vh
、rem
和 clac()
,本文以上两篇内容为基础,加上 mentor 仕春哥的启发,分析前端构建可视化大屏的一种布局方式。
大屏系统的内在原理 硬件 大屏系统实际上是由任意 n × m
块窄边显示器组成,具体如下图示例所示:
该例子中左下信号源的两台电脑通过显卡计算显示被投屏的内容,然后输出到大屏接收器中的矩阵切换器。矩阵切换器将两台电脑中的多路信号转换成单路信号。这个过程中播放控制设备可以控制两个画面的拼接的位置和模式(比如组合显示、开窗、漫游、叠加)。最后传输给上半部分的 4 × 2
块显示器大屏(拼接墙),最后展示大屏画面。
最佳显示效果 如果想实现最佳显示效果,应该满足下面的条件:
大屏逻辑分辨率(设计稿尺寸)的长宽比 = 大屏实际物理分辨率的长宽比
大屏逻辑分辨率(设计稿尺寸)的长宽比 = 显卡输出分辨率的长宽比
显卡输出分辨率 = 视频矩阵切换器支持分辨率 = 大屏实际物理分辨率
在实际实现时,大屏一般由 n × n
块长宽比 16:9
,分辨率为 1920×1080
的液晶屏拼接成,如果是 5 × 5
块大屏,实际物理分辨率已经达到了 9600×5400
。但是,不管是显卡还是矩阵切换器,能支持 4k
显示(4096×2160
)就很优秀了。所以在设计和前端实现时,一般符合 16:9
的比例的 1920×1080
分辨率,就可以高质量的进行大屏展示。
设计稿分割 设计规则 大屏的排版布局一般遵循下面的规则,主中间,次两边,附加各种动效和下钻:
实例 在谷歌上搜索了一个大屏设计稿的例子——中国移动全球基站管理 ,示例图片如下:
这个例子很符合主中间,次两边的规则,而且很显然中间的 3D 地球应该是有动效,两侧的卡片是可以下钻的。将图片大致分割一些,应该主要由以下部分构成:
前端布局 由于大屏的每个组件卡片排布方式都不同,普通的定位很难适配所有情况,所以这里使用 position: absolute
绝对定位脱离文档流,用 top
、left
、width
、height
来定位组件卡片。
定义基本量和卡片名 首先把基本量的长度先定义下来,这里的基本量可以复用,用画图做一个巨丑的简单示意图如下:
标题的高度为 heightHeader
,正文主体部分的外边距
分别为 marginMainTop
、marginMainRight
、marginMainBotom
、marginMainLeft
,正文中每个小卡片的外边距
为 marginCardTopBottom
、marginCardLeftRight
(这里的边距并不是真正的 margin
,只是表达类似的概念)。与 x
轴相关的长度统一用相对单位 vw
,与 y
轴相关的长度统一用 vw
,基础变量设置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 const heightHeader = '8vh' ;const marginMainTop = '0.7vh' ;const marginMainBottom = '0.7vh' ;const marginMainLeft = '0.7vw' ;const marginMainRight = '0.7vw' ;const marginCardTopBottom = '0.5vh' ;const marginCardLeftRight = '0.5vw' ;
将每个卡片的中文标题翻译为英文简称
,命名如下:
名称
英文翻译
简称
标题
header
HD
信号处理情况
Signal processing situation
SPS
动态 3D 地球
Dynamic 3D earth
D3E
基站信息数据统计
Base station information data statistics
BSIDS
国家基站变化对比
Comparison of changes in national base stations
CCNBS
基站总体变化
Overall change of base station
OCBS
使用 calc()计算 接下来就是最核心的地方,新建一个 config.js
文件,存放 calc()
模板,给每个卡片都计算自己的 top
、left
、width
、height
值。
以 y
方向举例,“标题”只需要计算 height
,,但是计算“信号处理情况”的 top
就需要在 heightHD
的基础上加上正文上边距 marginMainTop
和卡片上边距 marginCardTopBottom
。
另外,最下面和最右面的组件卡片 width
和 height
需要利用 100%
来减,完成自适应效果。还是以 y
方向举例,“基站总体变化”卡片,height
计算时,需要用 100%减
去正文下边距 marginMainBottom
、卡片下边距 marginCardTopBottom
和卡片自身的 top
值 topOCBS
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 const topHD = `0vh` ;const heightHD = `calc(${heightHeader} )` ;const leftHD = `0vw` ;const widthHD = `100%` ;const topSPS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom} )` ;const heightSPS = `50vh` ;const leftSPS = `calc(${marginMainLeft} + ${marginCardLeftRight} )` ;const widthSPS = `15vw` ;const topD3E = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom} )` ;const heightD3E = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topD3E} )` ;const leftD3E = `calc(${marginMainLeft} + ${marginCardLeftRight} )` ;const widthD3E = `70vw` ;const topBSIDS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom} )` ;const heightBSIDS = `35vh` ;const leftBSIDS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight} )` ;const widthBSIDS = `calc(100% - ${leftBSIDS} - ${marginCardLeftRight} - ${marginMainRight} )` ;const topCCNBS = `calc(${topBSIDS} + ${heightBSIDS} + ${marginCardTopBottom} )` ;const heightCCNBS = `35vh` ;const leftCCNBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight} )` ;const widthCCNBS = `calc(100% - ${leftCCNBS} - ${marginCardLeftRight} - ${marginMainRight} )` ;const topOCBS = `calc(${topCCNBS} + ${heightCCNBS} + ${marginCardTopBottom} )` ;const heightOCBS = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topOCBS} )` ;const leftOCBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight} )` ;const widthOCBS = `calc(100% - ${leftOCBS} - ${marginCardLeftRight} - ${marginMainRight} )` ;
配置输出 把上述计算的配置项输出,使用简称
为键名
,需要配置的 CSS 属性
为键值
。
比较特殊的两项是“信号处理情况”和“动态 3D 地球”。这两个组件相互叠加,需要把上层组件和下层组件设置不同的 z-index
来区分高低。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 let generatorConfigLayout = () => { return { HD: { top: topHD, left: leftHD, height: heightHD, width: widthHD }, SPS: { top: topSPS, left: leftSPS, height: heightSPS, width: widthSPS, 'z-index' : 2 }, D3E: { top: topD3E, left: leftD3E, height: heightD3E, width: widthD3E, 'z-index' : 1 }, BSIDS: { top: topBSIDS, left: leftBSIDS, height: heightBSIDS, width: widthBSIDS }, CCNBS: { top: topCCNBS, left: leftCCNBS, height: heightCCNBS, width: widthCCNBS }, OCBS: { top: topOCBS, left: leftOCBS, height: heightOCBS, width: widthOCBS } }; }; export default generatorConfigLayout;
页面样式 html 页面的样式就比较简单,首先新建一个 index.html
,然后要给 html
和 body
设置一个基础样式,z-index
是为了把 body
放在最底层,overflow: hidden
防止出现滚动条。设置根元素字体大小时使用相对大小,让文字在不同分辨率中视觉效果尽量统一(注意:Chrome 最小字体大小为 12px
)。
每个组件的标签都有自己的 id
,id
为简称。在通用.layout
样式中,给每个组件设置绝对定位
。为了容易区分,给每个标签设置了不同的背景颜色
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > 可视化大屏布局</title > <style > html, body { margin: 0; padding: 0; width: 100%; height: 100%; z-index: -1; overflow: hidden; font-size: calc(100vw / 120); background-color : #fdffdf ; } p { margin: 0; } .layout { position: absolute; overflow: hidden; display: flex; flex-direction: column; justify-content: space-around; align-items: center; } </style > </head > <body > <div id ="HD" class ="layout" style ="background-color: #EFCEE8;" > 标题 </div > <div id ="SPS" class ="layout" style ="background-color:#F0F0F0;" > <p > 信号处理情况</p > </div > <div id ="D3E" class ="layout" style ="background-color:#F3D7B5;" > <p > 动态3D地球</p > </div > <div id ="BSIDS" class ="layout" style ="background-color:#DAF9CA;" > <p > 基站信息数据统计</p > </div > <div id ="CCNBS" class ="layout" style ="background-color:#C7B3E5;" > <p > 国家基站变化对比</p > </div > <div id ="OCBS" class ="layout" style ="background-color:#A79496;" > <p > 基站总体变化</p > </div > </body > </html >
配置导入 为了让浏览器能使用 ES6 的导入导出,需要在 script 标签
中添加 type="module"
。
从上节的配置文件
中导入 generatorConfigLayout
函数,执行函数生成布局变量 configLayout
。遍历 configLayout
,每次执行 setStyle()
函数,将 css 样式设置给相对应的组件标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <script type ="module" > import generatorConfigLayout from './config.js' ; /** * @description 根据id和对应配置项config给dom添加style * @param {String } id id * @param {Object } config 每个id对应配置项 */ const setStyle = (id, config ) => { let dom = document .querySelector(`#${id} ` ); Object .keys(config).forEach(item => { dom.style[item] = config[item]; dom.innerHTML += `<p>${item} : ${config[item]} </p>` ; }); }; let configLayout = generatorConfigLayout(); Object .keys(configLayout).forEach(key => { setStyle(key, configLayout[key]); }); </script >
最后生成的布局结果如下所示,完成!
全部代码 全部代码如下: config.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 const heightHeader = '8vh' ;const marginMainTop = '0.7vh' ;const marginMainBottom = '0.7vh' ;const marginMainLeft = '0.7vw' ;const marginMainRight = '0.7vw' ;const marginCardTopBottom = '0.5vh' ;const marginCardLeftRight = '0.5vw' ;const topHD = `0vh` ;const heightHD = `calc(${heightHeader} )` ;const leftHD = `0vw` ;const widthHD = `100%` ;const topSPS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom} )` ;const heightSPS = `50vh` ;const leftSPS = `calc(${marginMainLeft} + ${marginCardLeftRight} )` ;const widthSPS = `15vw` ;const topD3E = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom} )` ;const heightD3E = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topD3E} )` ;const leftD3E = `calc(${marginMainLeft} + ${marginCardLeftRight} )` ;const widthD3E = `70vw` ;const topBSIDS = `calc(${heightHeader} + ${marginMainTop} + ${marginCardTopBottom} )` ;const heightBSIDS = `35vh` ;const leftBSIDS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight} )` ;const widthBSIDS = `calc(100% - ${leftBSIDS} - ${marginCardLeftRight} - ${marginMainRight} )` ;const topCCNBS = `calc(${topBSIDS} + ${heightBSIDS} + ${marginCardTopBottom} )` ;const heightCCNBS = `35vh` ;const leftCCNBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight} )` ;const widthCCNBS = `calc(100% - ${leftCCNBS} - ${marginCardLeftRight} - ${marginMainRight} )` ;const topOCBS = `calc(${topCCNBS} + ${heightCCNBS} + ${marginCardTopBottom} )` ;const heightOCBS = `calc(100% - ${marginMainBottom} - ${marginCardTopBottom} - ${topOCBS} )` ;const leftOCBS = `calc(${leftD3E} + ${widthD3E} + ${marginCardLeftRight} )` ;const widthOCBS = `calc(100% - ${leftOCBS} - ${marginCardLeftRight} - ${marginMainRight} )` ;let generatorConfigLayout = () => { return { HD: { top: topHD, left: leftHD, height: heightHD, width: widthHD }, SPS: { top: topSPS, left: leftSPS, height: heightSPS, width: widthSPS, 'z-index' : 2 }, D3E: { top: topD3E, left: leftD3E, height: heightD3E, width: widthD3E, 'z-index' : 1 }, BSIDS: { top: topBSIDS, left: leftBSIDS, height: heightBSIDS, width: widthBSIDS }, CCNBS: { top: topCCNBS, left: leftCCNBS, height: heightCCNBS, width: widthCCNBS }, OCBS: { top: topOCBS, left: leftOCBS, height: heightOCBS, width: widthOCBS } }; }; export default generatorConfigLayout;
index.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > 可视化大屏布局</title > <style > html, body { margin: 0; padding: 0; width: 100%; height: 100%; z-index: -1; overflow: hidden; font-size: calc(100vw / 120); background-color : #fdffdf ; } p { margin: 0; } .layout { position: absolute; overflow: hidden; display: flex; flex-direction: column; justify-content: space-around; align-items: center; } </style > </head > <body > <div id ="HD" class ="layout" style ="background-color: #EFCEE8;" > 标题 </div > <div id ="SPS" class ="layout" style ="background-color:#F0F0F0;" > <p > 信号处理情况</p > </div > <div id ="D3E" class ="layout" style ="background-color:#F3D7B5;" > <p > 动态3D地球</p > </div > <div id ="BSIDS" class ="layout" style ="background-color:#DAF9CA;" > <p > 基站信息数据统计</p > </div > <div id ="CCNBS" class ="layout" style ="background-color:#C7B3E5;" > <p > 国家基站变化对比</p > </div > <div id ="OCBS" class ="layout" style ="background-color:#A79496;" > <p > 基站总体变化</p > </div > <script type ="module" > import generatorConfigLayout from './config.js' ; /** * @description 根据id和对应配置项config给dom添加style * @param {String } id id * @param {Object } config 每个id对应配置项 */ const setStyle = (id, config ) => { let dom = document .querySelector(`#${id} ` ); Object .keys(config).forEach(item => { dom.style[item] = config[item]; dom.innerHTML += `<p>${item} : ${config[item]} </p>` ; }); }; let configLayout = generatorConfigLayout(); Object .keys(configLayout).forEach(key => { setStyle(key, configLayout[key]); }); </script > </body > </html >