前端大屏开发详解 - 布局篇

2023-10-19 09:10 月霖 1036

对于前端来说,要想把大屏做的好看,屏幕适配、css的灵活使用、图表的效果都是必不可少的,这里我打算通过一个实例来详细介绍一下相关的一些技术,因为内容较多,所以分为两篇:布局篇和图表篇。

布局篇,也就是本篇,主要介绍适配、布局和其他涉及到的一些css相关的内容。

图表篇则着重讨论一下使用ECharts绘制各类图表。

本案例使用VUE + Element-UI构建。

源码:https://gitee.com/moonlight_lin/display-demo

本期实现的效果:

一、适配方案

1.简述

大屏开发中的适配问题:

  • 设计稿按照1920*1080的分辨率,16:9的比例设计,而在实际开发中,document窗口不足16:9(高度减掉顶部tab及导航栏,地址栏等)。
  • 不同分辨率:实际应用场景中,显示场景不同,不能固定写死px单位。
  • 不同比例:不同的显示器宽高比与设计稿不一致。

常用的方案是使用rem适配。rem是CSS3中的一种相对长度单位,它的单位长度取决于根标签html的字体大小。比如:html设置font-size: 20px;,那么1rem = 20px;如果设置font-size: 30px;,那么1rem = 30px。因此,在不同的屏幕尺寸下,设置不同的根字体大小,并且在需要适配的地方使用rem单位,就可以达到适配的目的。

我们可以自己编写代码实现这样的功能,但难免有些繁琐,而且已经有两个库为我们做了这些事:amfe-flexible、postcss-pxtorem,我们在项目里使用它们来进行适配。

2.安装依赖

npm install amfe-flexible
npm install postcss-pxtorem@5.1.1

amfe-flexible

amfe-flexible是lib-flexible的升级版,它会根据设备宽度,修改根字体大小。

amfe-flexible实际上做了这几件事情:

  • 动态改写 <meta> 标签
  • <html> 元素添加 data-dpr 属性,并且动态改写 data-dpr 的值
  • < html> 元素添加 font-size 属性,并且动态改写 font-size 的值

直接在main.js中引入amfe-flexible即可:

import "amfe-flexible"

postcss-pxtorem

postcss-pxtorem是PostCSS的插件,可以自动将px转换成rem,并且可以对一些特殊情况进行处理。

postcss-pxtorem的配置可以在vue.config.js、postcsssrc.js、postcss.config.js其中任意一个文件中进行,权重从左到右降低,项目中没有对应文件的话直接新建就可以了。

具体配置项参见:https://www.npmjs.com/package/postcss-pxtorem

另外需要注意:行内样式中的px不会被转换为rem,所以对于需要自适应的样式不要写在行内。对于不需要转化的样式就可以写在行内,或者使用PX(大写)作为单位。

// vue.congif.js

/***
注意点:
(1)rootValue根据设计稿宽度除以10进行设置,假设设计稿为1920,rootValue设为192;
(2)propList是设置需要转换的属性,*为所有都进行转换。
***/

module.exports = function(){
    devServer:{
        // ...
    },
    css: {
       loaderOptions: {
            postcss: {
              plugins: [
                require('postcss-pxtorem')({
                  rootValue: 192, // 根元素字体大小
                  propList: ['*'], // 可以从px转换为rem的属性,匹配正则
                  // unitPrecision: 5, // 允许rem单位增长的十进制数字
                  // replace: true, // 替换包含rems的规则,而不添加后备
                  // mediaQuery: false, // 允许在媒体查询中转换px
                  // minPixelValue: 0, // 设置要替换的最小像素值
                  // selectorBlackList: [], // 忽略转换正则匹配项
                  // exclude: /node_modules/i  // 要忽略并保留为px的文件路径
                })
              ]
            }
        }
    },
}

二、整体布局

接着我们看一下整体的布局及其相关的css。页面划分如下图所示,顶部区域为系统名称、日期等信息,下方左右两侧区域用来放置各类图表,中间区域用来放置中国地图。这里我们先用背景色填充示意。

首先看一下页面结构,与上图的结构是完全对应的:

<div class="display">
  <div class="display-header"></div>

  <div class="display-body">
    <div class="display-body-left">
      <div class="display-item"></div>
      <div class="display-item"></div>
      <div class="display-item"></div>
    </div>

    <div class="display-body-middle"></div>

    <div class="display-body-right">
      <div class="display-item"></div>
      <div class="display-item"></div>
      <div class="display-item"></div>
    </div>
  </div>
</div>

1.视口单位

接着我们从整体到局部依次看一下它们的样式。

<style lang="scss">
.display {
  width: 100vw;
  height: 100vh;
  background: url("~@/assets/bg.png");
  background-size: 100%;
}
</style>

.display 是最外层的容器,大屏需要占据整个视口,所以我们使用视口单位vw和vh。这里视口指浏览器的可视区域,CSS3中的视口单位包括以下4个:

  • vw:1vw等于视口宽度的1%。
  • vh:1vh等于视口高度的1%。
  • vmin:取vw和vh中较小的那个。
  • vmax:取vw和vh中较大的那个。
  • vmin和vmax在移动端横屏、竖屏切换中非常有用。举个例子:
div { 
    height: 100vmin; 
    width: 100vmin; 
}

2.flex布局

接着往下看,整个页面分为上、下两部分,下面又分为左中右三部分。

.display-header {
  height: 74px;
  background: #C0C4CCCC;
}
.display-body {
  height: calc(100% - 74px);
  padding: 20px;
  display: flex;
  gap: 20px;
  .display-body-left, .display-body-right {
    width: 420px;
    background: #C0C4CCCC;
  }
  .display-body-middle {
    flex: 1;
    background: #C0C4CCCC;
  }
}

.display-body 使用弹性布局( display: flex; ),左右两块固定宽度( width: 420px; ),中间部分填充剩余空间( flex: 1; )。gap属性设置列之间的间隙大小( gap: 20px; )。

下面以图示的方式简要介绍一下flex布局中的相关属性。

flex布局中的容器存在主轴和交叉轴,默认情况下水平方向是主轴,垂直方向为交叉轴,子元素沿主轴排列。

容器属性

/* 测试 */
.test-flex-box {
  width: 170px;
  height: 200px;
  display: flex;
  flex-wrap: nowrap;
  flex-direction: row;
  justify-content: flex-start;
  align-items: stretch;
  align-content: stretch;
  background: #dedede;
  .test-flex-item {
    width: 40px;
    height: 40px;
    background: #ffe6e6;
    font-size: 12px;
    border: 1px solid;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}

1)flex-direction:定义主轴方向,决定了子元素如何排列。

  • row(默认):主轴为水平方向,起点在左端。
  • row-reverse:主轴为水平方向,起点在右端。
  • column:主轴为垂直方向,起点在上沿。
  • column-reverse:主轴为垂直方向,起点在下沿。

2)flex-wrap:定义子元素超出容器宽度或高度后是否换行,如果不换行,元素会缩小。

  • nowrap(默认):不换行。
  • wrap:换行,第一行在上方。
  • wrap-reverse:换行,第一行在下方

3)flex-flow:flex-direction属性和flex-wrap属性的简写形式,默认为row nowrap。

4)justify-content:定义子元素在主轴上的对齐方式,与主轴方向有关。

  • flex-start(默认):在容器的起点对齐。
  • flex-end:在容器的末端对齐。
  • center: 在容器的中心对齐。
  • space-between:两端对齐,子元素之间的间隔都相等。
  • space-around:每个子元素两侧的间隔都相等。

5)align-items:定义项目在交叉轴上的对齐方式,与交叉轴的方向有关。

  • stretch(默认):如果子元素未设置高度或设为auto,将占满整个容器的高度。
  • flex-start:交叉轴的起点对齐。
  • flex-end:交叉轴的末端对齐。
  • center:交叉轴的中心对齐。
  • baseline: 子元素的第一行文字的基线对齐。如果没有文字,则基线为子元素底部边缘。交叉轴为水平方向时,与flex-start一致。

6)align-content:定义多根轴的对齐方式。即存在多根主轴或多根交叉轴的情况下该属性才生效。

  • stretch(默认):轴占满整个交叉轴。
  • flex-start:与交叉轴的起点对齐。
  • flex-end:与交叉轴的末端对齐。
  • center: 与交叉轴的中心对齐
  • space-between:两端对齐,轴与轴之间的间隔都相等。
  • space-around:每根轴两侧的间隔都相等。

子元素属性

  • order:定义子元素的排列顺序。数值越小,排列越靠前,默认为0。
  • flex-grow:定义项目的放大比例,默认为0(即如果存在剩余空间也不放大)。
  • flex-shrink:定义项目的缩小比例,默认为1(即如果空间不足,该项目将缩小)。为0时不缩小。
  • flex-basis:定义了在分配多余空间之前,子元素占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即子元素的本来大小。
  • flex:是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。 该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。
  • align-self:允许单个子元素有与其他子元素不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。其他取值与align-items完全一致。

3.grid布局

现在布局还剩下最后一部分:左右两侧区域中的3个容器。

.display-body-left, .display-body-right {
  width: 420px;
  display: grid;
  grid-template-rows: repeat(3, 1fr);
  gap: 20px;
  .display-item {
    background: #C0C4CCCC;
  }
}

这里我使用了网格布局 display: grid; ,通过 grid-template-rows: repeat(3, 1fr); 将其高度分为3等份。当然,这里完全可以使用flex布局,我主要是想借此说明一下不同的布局方式,让不清楚网格布局的同学能对它有一定的认识。在这个案例的后面还会使用到这种布局。

/* 使用flex布局也可以达到同样的效果 */
.display-body-left, .display-body-right {
  width: 420px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  .display-item {
    flex: 1;
    background: #C0C4CCCC;
  }
}

flex布局是轴线布局,只能指定子元素针对轴线的位置,可以看作是一维布局,grid 布局则是将容器划分成“行"和“列”,产生单元格,然后指定子元素所在的单元格,可以看作是二维布局,grid布局比 flex布局更强大,相应的,涉及到的属性也很多。这里不再一一列举,只针对一些主要的属性进行介绍。

容器属性

1)grid-template-columns、grid-template-rows分别用于划分列和行。

grid-template-columns: 100px 100px 100px; /* 3列,每列宽100px */
grid-template-rows: repeat(3, 100px); /* 3行,每行高100px */
  • 使用 auto-fill 设置自动填充。
grid-template-columns: repeat(auto-fill, 100px);
grid-template-rows: repeat(auto-fill, 100px);

  • fr 表示比例关系。
grid-template-columns: repeat(3, 1fr); /* 平均分成3等份 */
grid-template-rows: 2fr 1fr; /* 行高分为3份,第一行占2份,第二行占1份 */

  • 使用 auto 由浏览器自动进行分配。
grid-template-columns: 100px auto 100px; /* 第1列和第3列 宽100px,第2列自动分配 */
grid-template-rows: 50px auto 50px; /* 第1行和第3列行 高50px,第2行自动分配 */

2)grid-column-gap、grid-row-gap、grid-gap 设置子元素之间的间距。

grid-gap是grid-column-gap和grid-row-gap的简写。另外,在最新标准中,这3个属性的前缀grid-已经删除,分别为:column-gap、row-gap、gap。

这个属性并不是只能用在grid布局中,在flex布局中也可以使用,在 .display-body 中就用了这个属性。

grid-template-columns: repeat(3, 1fr);;
grid-template-rows: repeat(3, 1fr);;
gap: 20px 10px;

3)align-items、justify-items、place-items

align-items和justify-items分别用于设置单元格内容的垂直和水平的对齐方式。

有4个取值:start、end、center、stretch。

place-items是这两个属性的简写。

justify-items: center;
align-items: start;
/* place-items: start center; */

子元素属性

1)grid-column-start、grid-column-end、grid-row-start、grid-row-end

用来指定子元素从哪根网格线开始,到哪根网格线结束。

grid-column是grid-column-start、grid-column-end的简写;grid-row是grid-row-start、grid-row-end的简写。

还可以进一步简写为:

grid-area: <grid-row-start> / <grid-column-start> / <grid-row-end> / <grid-column-end>

grid-column-start: 1; 
grid-column-end: 3;
grid-row-start: 1; 
grid-row-end: 3;
/* grid-column: 1 / 3; */
/* grid-row: 1 / 3; */
/* grid-area: 1 / 1 / 3 / 3 */

2)justify-self、align-self、place-self用于设置子元素自身的对齐方式。

与容器属性align-items、justify-items、place-items的用法一致。

align-self: center; 
justify-self: center;
/* place-self: center; */

三、其他CSS技巧

1.定位方式

接着来看一下顶部标题。

<div class="display-header">
  <div class="left">
    <span class="datetime">{{ curDate }} 星期{{ curDay }}</span>
  </div>
  <div class="title">
    <h1>大屏展示Demo</h1>
  </div>
  <div class="right">
    <span class="datetime">{{ curTime }}</span>
  </div>
</div>
.display-header {
  height: 74px;
  background: url("~@/assets/header.png") no-repeat center;
  background-size: auto 100%;
  position: relative;
  .title {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: #FFFFFFDD;
    font-size: 20px;
  }
  .left, .right {
    position: absolute;
    bottom: 10px;
  }
  .left {
    right: calc(50% + 260px);
  }
  .right {
    left: calc(50% + 260px);
  }
  .datetime {
    color: #08D1EA;
    font-size: 16px;
  }
}

标题、左右两边的日期、时间以它们的父级 .display-header 作为参照进行绝对定位。(父元素设置 position: relative; ,子元素设置 position: absolute;

标题元素在父容器的水平方向、垂直方向上都居中,元素是以左上角作为基准进行定位的。

top: 50%; left: 50%; 会将标题元素的左上角定位到父容器的中心点,

再对其进行平移变换 transform: translate(-50%, -50%); ,使其相对于自身分别向x轴负方向(向左)偏移自身宽度的一半,向y轴负方向(向上)偏移自身高度的一半,从而在父容器中居中。

左右两边的日期时间,则以父容器中心为基准分别向左、向右偏移。

 这里再介绍一下css中的四种定位方式:

  • relative:相对定位,相对于元素自身位置进行定位。相对定位不会脱离文档流,在文档流中不会影响其他元素。
  • absolute:绝对定位,相对于其最近的已定位(设置了position属性的)祖先元素进行定位,如果没有已定位的祖先元素,则相对于body元素进行定位。绝对定位的元素会脱离文档流。
  • fixed:固定定位,相对于浏览器窗口进行定位,元素的位置在屏幕内容滚动时不会改变位置。固定定位的元素会脱离文档流。
  • sticky:粘性定位,相对于其最近的滚动(设置了overflow相关属性的)祖先元素进行定位,固定在该祖先元素上,不会随之滚动。sticky定位非常适合用来做吸顶效果。
.box-father {
  position: relative;
  height: 400px;
  overflow-y: auto;
  .box-sticky {
    width: 100%;
    height: 30px;
    position: sticky;
    top: 10px;
  }
}

上个图看一下它们之间的区别:

2.图片边框

最后看一下用来放置图表的 .display-item 容器,它有个不规则的边框,一般的做法是使用background来设置,但是容器未必是固定宽高,可能长宽比跟图片不一致,这样就会导致拉伸变形。如下图所示,左边是容器长宽比跟图片一致的情况,右边是不一致的情况,可以看到图片拉伸变形了:

这里介绍一下border-image属性,它用于给边框设置图片,并且可以将其分成9个部分,使其缩放时不会变形。

.display-item {
  width: 100%;
  border: 30px solid;
  border-image: url("~@/assets/border.png") 30;
}

border-image是5个属性值的简写形式,它包括:

  • border-image-source:设置要用于绘制边框的图像的路径。
border-image-source: url("~@/assets/border.png");
  • border-image-slice:用来对source指定的图像进行切分。
  • 可以指定上、右、下、左四个方位来分割图像,将其分成 9 个部份。可以用数值或者百分比来指定。可以加上 fill 关键字来填充中间区域,不加的话中间区域就是透明的。

除了 fill 关键字,border-image-slice 属性可以接收 1~4 个参数值:

  • 如果提供全部四个参数值,那么将按上、右、下、左的顺序对图像进行分割
  • 如果提供三个参数,那么第一个参数用于上方,第二个参数用于左、右两侧,第三个参数用于下方
  • 如果提供两个参数,那么第一个参数用于上方和下方,第二个参数用于左、右两个
  • 如果只提供一个参数,那么上、右、下、左都将使用该值进行分割
border-image-slice: 30;
  • border-image-width:用于指定边框的宽度。默认为1(即 1 * border-width)。同样可以接收1~4 个参数值。
border: 30px solid;
border-image-width: 2; /* 60px */
  • border-image-outset:用于指定边框向外偏移的距离。
border-image-outset: 10px;

border-image-repeat:用于设置填充方式。有以下取值:

  • stretch(默认):将被分割的图像使用拉伸的方式来填充满边框区域;
  • repeat:将被分割的图像使用重复平铺的方式来填充满边框区域,当图像碰到边界时,超出的部分会被截断;
  • round:与 repeat 关键字类似,当背景图像不能以整数次平铺时,会根据情况缩放图像;
  • space:与 repeat 关键字类似,当背景图像不能以整数次平铺时,会用空白间隙填充在图像周围。

border-image-repeat属性可以接收 1~2 个参数值:

  • 如果提供两个参数,那么第一个参数将用于水平方向,第二个将用于垂直方向;
  • 如果只提供一个参数,那么将在水平和垂直方向都应用该值。

四、总结

本篇通过一个实例,介绍了适配、布局相关css、定位相关css以及使用border-image实现图片拉伸不变形的效果。熟练掌握并且灵活运用css可以使页面结构更加清晰简洁(没有多余的容器或元素),也可以使页面呈现更好的效果。