搜索框的功能比较简单,最基本的就是输入和提交两个逻辑。但是我们这个搜索框要额外的加一些细节,输入框分为输入状态和非输入状态,两种状态下输入框表现要有所不同。我们对这个搜索框组件会有以下要求
在非输入状态下,只有一个输入框样式,在输入框中有一个搜索图标和“搜索”两个提示用的文本。
当在输入的状态时,显示取消按钮和搜索提示,并且在输入框不为空的时候提供一键清空的按钮。
首先建立 /demo/search.html 文件:
<div class="tt-content">
<div class="tt-search">
<form class="tt-search-form" action="#">
<div class="tt-search-input-wrap">
<i class="fa fa-search tt-search-icon">i>
<input type="text" class="tt-search-input" placeholder="搜索" autocomplete="off" required/>
<i class="fa fa-close tt-search-clear">i>
div>
<span class="tt-search-cancel">取消span>
form>
<ul class="tt-search-suggest">
<li class="tt-suggest-item">手机li>
<li class="tt-suggest-item">iPhone XS Maxli>
<li class="tt-suggest-item">华为P30li>
<li class="tt-suggest-item">小米 MIX3li>
<li class="tt-suggest-item">诺基亚1110li>
ul>
div>
<p class="content">内容区p>
div>
搜索组件的容器一共有两个状态,分为输入状态和非输入状态。我们使用“on-focus”这个类来区分搜索框是不是在输入状态。在非输入状态下,搜索组件就是文档流里一个普通的盒子,可以随着页面进行滚动,而在输入状态下搜索组件需要覆盖住整个页面,所以我们要给 .tt-search 如下的样式:
/* 搜索框 */
.tt-search{
max-width: 640px;
margin: 0 auto;
background: #f8f8f8;
}
/* 搜索状态中,覆盖内容区 */
.tt-search.on-focus{
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow-y: auto;
}
/* 处理有标题栏的情况 */
.tt-header ~ .tt-content .tt-search.on-focus{
top: 2.3rem;
}
/* 处理有导航栏的情况 */
.tt-navbar ~ .tt-content .tt-search.on-focus{
bottom: 2.5rem;
}
搜索框里,我们使用了 form 做了最外层的容器,这是因为之前提到想在键盘上直接提交搜索操作,如果想达到这种效果,就必须使用下面这种结构:
<form action="#">
<input type="text" />
form>
也就是带有 action 属性的表单里包含的 input 元素,这样的结构就会被输入法认定为表单的功能,会在键盘上显示出“前往”的按钮.
@ Tips:
如果 input 的类型是 H5 中的搜索类型,也就是 ,那么在有些输入法中表单的提交按钮的文本就可以变成“搜索”。
/* 搜索栏中的表单 */
.tt-search > .tt-search-form{
display: flex;
height: 2.3rem;
align-items:center;
}
@ Tips:
align-items 属性就是用来标记弹性布局里的元素在侧轴(也就是和 flex-direction 方向相垂直的轴)上的对齐方式,form 里所有元素都是水平排列的,所以想竖直居中,就可以直接使用“align-items:center;”来实现。align-items 有以下可取的值:
- stretch(默认),元素进行抻拉来填满,如果盒子有指定大小,那么以指定的大小为准。
- center,元素会布局在容器侧轴的中间位置。
- flex-start,元素位于容器的开头。
- flex-end,元素位于容器的结尾。
- baseline,元素位于第一行文本的基准线位置,这里要注意是第一行的文本,而不是第一个元素。
接下来是 form 容器里的 4 个元素,因为搜索和一键清空的图标都是和 input 关联的,所以做了个 .tt-search-input-wrap 容器把这三个放了进去,最后的取消按钮和这个容器在同一层。先来处理 .tt-search-input-wrap 和取消按钮的关系:
/* 输入框的容器 */
.tt-search > .tt-search-form > .tt-search-input-wrap{
flex: 1;
position: relative;
padding: 0 .5rem;
}
/* 取消按钮 */
.tt-search > .tt-search-form > .tt-search-cancel{
flex: 0 0 2.2rem;
padding-right: .5rem;
text-align: center;
font-size: .7rem;
display: none;
}
/* 对取消按钮的控制 */
.tt-search.on-focus > .tt-search-form > .tt-search-cancel{
display: block;
}
这里面有几个点要注意下:
/* 搜索图标 */
.tt-search .tt-search-icon{
position: absolute;
height: .8rem;
line-height: .8rem;
font-size: .7rem;
left: 1rem;
top: 0;
bottom: 0;
margin: auto;
color: #ccc;
}
/* 输入框的样式 */
.tt-search .tt-search-input{
box-sizing: border-box;
width: 100%;
height: 1.6rem;
border: none;
font-size: .8rem;
padding-left: 1.5rem;
background: #fff;
border-radius: .2rem;
}
/* 清空按钮的样式 */
.tt-search .tt-search-clear{
position: absolute;
height: .8rem;
line-height: .8rem;
width: .8rem;
font-size: .6rem;
top: 0;
bottom: 0;
margin: auto 0;
right: 1rem;
border-radius: 50%;
color: #fff;
background: #ccc;
display: none;
}
/* 对清空按钮的控制 */
.tt-search.on-focus .tt-search-input:valid + .tt-search-clear{
display: block;
}
上面的代码中,图标的默认样式是没什么问题的,就是前面搜索图标的用法,只不过把位置放在了右边。这里面要注意的是第二条样式只有在搜索状态下并且输入框有内容的时候才让这个清空按钮出现,所以使用了一个 :valid 选择器来判断和它相邻的输入框的状态。这个 :valid是和 input 中的“required”属性对应的,input 元素有内容时“required”验证条件就会通过,这时候 :valid 选择器就会选中这个 input,从而后面的兄弟选择器才会选中 .tt-search-clear 元素。这种用法就可以直接使用 CSS 来控制清空按钮的显示了,省去了 JS 的工作。
一、简单列表
简单列表的设计就很简单了,比如在表单元素中存放每一条表单的容器。
这个列表的样式很好实现,这里面只需要注意为了和复杂列表共用每一行的容器,我们不要对每一行的元素做高度和行高的限制。
二、复杂列表
复杂列表的每一行因为要呈现多个信息,所以内容区要重新布局。公司的业务里主要是商品展示,所以在 UI 里我们使用商品信息作为复杂列表的填充内容,样式如下:
我们先来开发简单列表,简单列表的样式比较简单,我们之前也做过类似的样式,所以这里就直接贴代码了。
<ul class="tt-list">
<li class="tt-list-item">北京li>
<li class="tt-list-item">上海li>
<li class="tt-list-item">天津li>
<li class="tt-list-item">重庆li>
<li class="tt-list-item">厦门li>
<li class="tt-list-item">广州li>
<li class="tt-list-item">...li>
ul>
这里我们以城市列表为例,使用 ul 元素来实现这个列表。这个列表的样式也是给 .tt-list-item 加上一些简单样式就可以实现了,我们可以在 /src/list.css 中添加下列样式:
/* 列表里每一行的容器 */
.tt-list > .tt-list-item{
position: relative;
padding: .5rem 1rem;
font-size: .8rem;
background: #fff;
color: #333;
border-bottom: 1px solid #eee;
}
复杂列表中,每一行的外层容器可以和简单列表共用,比较麻烦的就是处理每一行里内容区的布局。按着示意图里的样式,图片和文本区域左右排列,如下图:
这个结构里,左侧图片固定宽度,右侧文本区域需要填充剩余空间的需求,所以这又是一个特别适合弹性布局的场景。左边的图片区域可以直接使用一个 img 元素,但我们这里会在 img 外层加一层容器,这样如果需要在图片位置加一些标签的话会更容易定位。右侧区域就是三个文本段落依次排列。
<ul class="tt-list">
<li class="tt-list-item">
<div class="item-img-wrap">
<img class="item-img" src="img/list-img.jpg" alt="机械键盘">
div>
<div class="item-content-wrap">
<h1 class="item-title">FILCO斐尔可 机械键盘87游戏无线圣手忍者二代红轴茶青黑蓝牙双模h1>
<p class="item-price">¥998p>
<p class="item-desc">已售1834件p>
div>
li>
<li class="tt-list-item">
<div class="item-img-wrap">
<img class="item-img" src="img/list-img.jpg" alt="机械键盘">
div>
<div class="item-content-wrap">
<h1 class="item-title">FILCO斐尔可 机械键盘87游戏无线圣手忍者二代红轴茶青黑蓝牙双模h1>
<p class="item-price">¥998p>
<p class="item-desc">已售1834件p>
div>
li>
ul>
为了缩短篇幅,这个列表里只包含了两条列表数据。在实现列表里内容区样式的时候,左右结构的处理和图片的处理都比较常规,只有右侧文本区的要求有些多。文本区分为标题、价格和销量这三部分内容,这三个元素要占满文本区域的高度,并且标题要靠顶,销量描述要贴底。对于这种需求能想到最简单的做法就是先固定每个元素的高度,然后通过计算出的内边距或外边距来撑开整个区域。但是我们这里准备用弹性布局里的 “justify-content” 属性来实现这个效果。下面先来介绍一下 “justify-content”。
@ Tips:
justify-content 属性是在弹性布局中,用来定义主轴上元素的排列方式的,这个属性要作用于弹性布局中的容器上。这个属性有以下可取的值:
flex-start:弹性盒子里的所有元素从容器起始位置开始依次排列。这个起始位置并不是固定的,它会受到 “flex-direction” 的影响。比如 “flex-direction” 的值如果是 “column-reverse”,那起始位置就在容器的最下面;如果 “flex-direction” 的值是默认的 “row”,那么起始位置就是容器的最左边。效果如下:
/* 列表里每一行的容器 */
.tt-list > .tt-list-item{
+ display: flex;
position: relative;
padding: .5rem 1rem;
font-size: .8rem;
background: #fff;
color: #333;
border-bottom: 1px solid #eee;
}
/* 列表项的图片容器 */
.tt-list .item-img-wrap{
flex: 0 0 5rem;
height: 5rem;
margin-right: .5rem;
border-radius: .2rem;
overflow: hidden;
}
/* 列表项的图片 */
.tt-list .item-img-wrap > .item-img{
width: 100%;
height: 100%;
}
/* 列表项的文字区域 */
.tt-list .item-content-wrap{
position: relative;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
再下来就是文本区域里三个元素的样式了,这里面标题部分需要两行的高度,并且超长的时候要截断,这里我们会使用下面这个固定的搭配来实现多行的折行截断效果:
div{
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
这三条固定的搭配就可以实现文本多行的截断,并且在截断处自动追加省略号。但需要注意的是,这几个属性并不是 CSS3 中标准的属性,并且只有在 webkit 内核的浏览器中且加上 - webkit - 前缀才有效。我们最需要的是对多行文本超出的部分做隐藏,而对添加省略号的需求不是很强,所以这几个元素可以配合 height 和 line-height 来使用,这样哪怕这三个属性失效了,也能保证正常的显示效果。
/* 列表项的标题 */
.tt-list .item-content-wrap > .item-title{
height: 2rem;
line-height: 1rem;
font-size: .8rem;
font-weight: normal;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 列表项的价格字段 */
.tt-list .item-content-wrap > .item-price{
font-size: .8rem;
font-weight: 600;
color: #e0652f;
}
/* 列表项的描述字段 */
.tt-list .item-content-wrap > .item-desc{
color: #999;
font-size: .6rem;
}
在实现网格组件的时候,我们把它分成两部分来设计。一部分是最常用的标准九宫格,另一部分就是在九宫格的基础上再延伸出其他多列的网格。
一、九宫格
我们先来分析九宫格的需求,我们需要做的九宫格样式如下:
对于这个九宫格,我们有如下的要求:
二、多列网格
在九宫格的基础上,我们要实现多列网格,样式如下:
对多列网格我们有如下要求;
在实现九宫格的时候,我们要考虑的就是多个格子怎么分行排布。这种多行多列并且要在水平方向上平分空间的布局,和 table 元素的特点十分吻合。但是,如果使用 table 布局的话,那么网格的列数要固定下来的,这样在后面做多列表格的时候就需要再去改动 HTML 的结构,不是很方便。
接下来可以考虑使用 float 来实现这个效果,这样可以给格子定义好宽度,然后每行排满了就会自动从下一行开始排列。这种方案是可以的,唯一的不好就是定义盒子的宽度上,如果是 3 格或者 6 格这种非整数的百分比,就有可能出现不能占满全屏的情况,另外宽度的比值也要自己去计算。
最后还有一种选择,就是使用弹性布局。我们之前用的弹性布局的例子都是排布在同一行,但是弹性布局也是支持多行排列的,使用弹性布局的 flex-wrap 即可实现。
@ Tips:
flex-wrap 属性是弹性布局中用来指定弹性盒子里的元素换行方式的,它有以下三个可取的属性值:
nowrap:这个属性表示弹性盒子里的元素在放不下的时候不换行,如果元素总宽度超过容器宽度,就会根据 flex-shrink 属性指定的方式进行压缩。
wrap:这个属性表示弹性盒子里的元素在放不下的时候自动进行折行。
wrap-reverse:这个属性也表示弹性盒子里的元素放不下时进行折行,但是这种折行方式比较特殊,折出来的多行是从下向上排列的。
关于 flex-wrap 属性要注意的是,它可以和 flex-grow 属性配合使用,能对盒子进行拉伸,保证每一行都是充满的。但是 flex-wrap 属性和 flex-shrink 属性是冲突的,当弹性盒子指定了可以换行的情况下,容器空间不足的情况下就会折行,而不会再去压缩盒子里的元素。
<div class="tt-grid">
<div class="tt-grid-item">
<i class="fa fa-area-chart tt-grid-icon">i>
<p class="tt-grid-label">格子1p>
div>
<div class="tt-grid-item">
<i class="fa fa-area-chart tt-grid-icon">i>
<p class="tt-grid-label">格子2p>
div>
<div class="tt-grid-item">
<i class="fa fa-bar-chart tt-grid-icon">i>
<p class="tt-grid-label">格子3p>
div>
<div class="tt-grid-item">
<i class="fa fa-area-chart tt-grid-icon">i>
<p class="tt-grid-label">格子4p>
div>
<div class="tt-grid-item">
<i class="fa fa-area-chart tt-grid-icon">i>
<p class="tt-grid-label">格子5p>
div>
<div class="tt-grid-item">
<i class="fa fa-bar-chart tt-grid-icon">i>
<p class="tt-grid-label">格子6p>
div>
<div class="tt-grid-item">
<i class="fa fa-area-chart tt-grid-icon">i>
<p class="tt-grid-label">格子7p>
div>
<div class="tt-grid-item">
<i class="fa fa-area-chart tt-grid-icon">i>
<p class="tt-grid-label">格子8p>
div>
<div class="tt-grid-item">
<i class="fa fa-bar-chart tt-grid-icon">i>
<p class="tt-grid-label">格子9p>
div>
div>
这里总共放上 9 个格子,每个格子里的包括了一个图标和一个格子的名称。在排布格子的时候,要注意的一个是刚才提到的 flex-wrap 的用法,另外一个就是盒子边框的设计。
首先在垂直方向上,每两个格子间的边框可以给每个格子下边框来实现。然后在水平方向上格子间的边框可以给每个盒子右边框来实现。但是这样九宫格最右侧就会多出一格边框,再把最右侧一列格子的边框取消。最后,就是给整个盒子容器加一个上边框,也就是黄色的边框部分,就可以实现需求里要求的边框样式。
/* 网格组件 */
.tt-grid{
display: flex;
flex-wrap: wrap;
border-top: 1px solid #ddd;
}
/* 网格中的格子 */
.tt-grid > .tt-grid-item{
position: relative;
flex: 1 1 33%;
box-sizing: border-box;
padding: 1.2rem 0;
text-align: center;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
background: #fff;
}
/* 默认是3列 */
.tt-grid .tt-grid-item:nth-child(3n){
border-right: none;
}
/* grid内容区 */
.tt-grid > .tt-grid-item > .tt-grid-icon{
font-size: 1.5rem;
color: #aaa;
margin-bottom: .5rem;
}
.tt-grid > .tt-grid-item > .tt-grid-label{
font-size: .6rem;
color: #333;
}
/* 不需要边框时取消容器上的border */
.tt-grid.no-border{
border: none;
}
/* 不需要边框时取消格子上所以的border */
.tt-grid.no-border > .tt-grid-item{
border: none;
}
这里有几点要注意:
在实现多列网格的时候,只需要在刚才的九宫格上进行改动就可以。这里我们要变动的地方有:
根据这些要求,我们就可以写出多列网格的样式了。2 列网格就可以用如下代码来实现:
/* 两列网格 */
.tt-grid.tt-grid-2 .tt-grid-item{
border-right: 1px solid #ddd;
flex-basis: 50%;
padding: 1.8rem 0;
}
.tt-grid.tt-grid-2 .tt-grid-item:nth-child(2n){
border-right: none;
}
/* 四列网格 */
.tt-grid.tt-grid-4 .tt-grid-item{
border-right: 1px solid #ddd;
flex-basis: 25%;
padding: .9rem 0;
}
.tt-grid.tt-grid-4 .tt-grid-item:nth-child(4n){
border-right: none;
}
/* 五列网格 */
.tt-grid.tt-grid-5 .tt-grid-item{
border-right: 1px solid #ddd;
flex-basis: 20%;
padding: .6rem 0;
}
.tt-grid.tt-grid-5 .tt-grid-item:nth-child(5n){
border-right: none;
}
在 PC 端中,因为页面空间充足,菜单和操作可以处在同一个页面中,所以 PC 端的菜单通常只是充当页面或者功能的引导。但是在移动端,有些复杂的操作会被放进菜单项引导的二级页面来做,为了方便查看操作结果,通常会把操作信息也展示在菜单上。而对于简单的操作,也可以直接放在菜单上来完成,比如上面的开关功能。所以移动端的菜单除了基本的引导作用以外,还要承担信息展示和完成简单操作的作用。下面我们来分析一下移动端的菜单组件都会有什么需求:
根据前面的需求,基础菜单里的分为菜单名称、菜单信息、菜单引导图标这三个内容。我们先按着这个要求把基本的 html 结构做出来:
<div class="tt-menu">
<a class="tt-menu-item">
<p class="tt-menu-name">用户IDp>
<span class="tt-menu-value">33581893span>
a>
<a class="tt-menu-item">
<p class="tt-menu-name">用户名p>
<span class="tt-menu-value">推推UIspan>
<i class="fa fa-chevron-right tt-menu-icon">i>
a>
<a class="tt-menu-item">
<p class="tt-menu-name">二维码p>
<i class="fa fa-qrcode tt-menu-value">i>
<i class="fa fa-chevron-right tt-menu-icon">i>
a>
<a class="tt-menu-item">
<p class="tt-menu-name">隐私设置p>
<i class="fa fa-chevron-right tt-menu-icon">i>
a>
div>
这里我们做了四个菜单,分别展示了菜单的不同用法;
根据这几种菜单的类型我们可以知道,菜单名称是一定有的,后面的菜单信息和引导图标都不是必须的。根据需求,我们要求菜单名称靠左,菜单信息和菜单引导图标在最右侧,且都和页面的边界有一定的空间。这种需求一看就应该很熟悉了,与前面完成的列表和输入框的样式又很相似,直接用弹性布局解决。
/* 菜单项 */
.tt-menu > .tt-menu-item{
display: flex;
height: 2.3rem;
padding: 0 1rem;
align-items: center;
border-bottom: 1px solid #eee;
font-size: .8rem;
}
/* 菜单名称 */
.tt-menu > .tt-menu-item > .tt-menu-name{
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #333;
}
/* 菜单信息 */
.tt-menu > .tt-menu-item > .tt-menu-value{
max-width: 5rem;
height: 1rem;
line-height: 1rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #999;
}
/* 下级操作引导图标 */
.tt-menu > .tt-menu-item > .tt-menu-icon{
margin-left: .3rem;
color: #999;
}
在这三个元素中,菜单名称使用了“flex:1;”用来占满剩余空间,以便把其他元素挤到容器的最右侧,如果菜单名称过长的话会自动截断。菜单信息这个元素没有指定宽度,但指定了最大宽度和超出自动截断,这样就不会在信息太长的时候挤占菜单名称的位置。最后的引导图标就是简单的设置了一个左边距,用来和菜单信息隔开一段距离。
对于基础模态框,我们要有如下要求:
对于海报形式模态框的要求,其实会更简单一些,它只是在基础模态框的基础上做了些变动。对海报形式的模态框有以下需求:
我们先来分析下模态框的结构。根据需求,模态框里大体上是分为两部分,一部分是半透明遮罩,另外一部分就是模态框的窗口部分。而模态框里面又分为内容区和操作区,内容区就是一些文本性质的内容,而操作区可以放按钮。
<div class="tt-modal show">
<div class="tt-mask">div>
<div class="tt-modal-wrap">
<div class="tt-modal-body">
<p>您的会员即将到期,请及时续费,以免影响您的权益。p>
div>
<div class="tt-modal-footer">
<a class="tt-btn">下次再说a>
<a class="tt-btn">立即续费a>
div>
div>
div>
/* 模态框 */
.tt-modal{
display: none;
}
/* 控制模态框的显示 */
.tt-modal.show{
display: block;
}
然后是下一层蒙版和模态框窗口的实现。蒙版层可以直接使用在第三章实现过的通用蒙版,这里只需要在 tt-modal 里加入一个 class 为“tt-mask”的 div 元素就可以了。
接下来是窗口的实现,窗口的定位就决定了模态框显示的位置,按着需求我们希望它中心在距顶部 45% 的位置。对于这种定位要求,我们之前用过的方法只有一种比较接近。就是在做绝对定位元素垂直居中的时候,可以通过设置“top: 45%;”值来定位元素的起始位置,再通过负的 margin-top 值来把整个容器向上移动容器高度的一半,从而达到需求中的要求。这种方法已经很接近了,但遗憾的是这种方式下我们要知道容器的确切高度,才能设置正确的 margin-top 值,而我们这里有另外一条要求就是弹窗的高度要随着内容自动调整,所以这种方法就没法用了。
下来我们要介绍一个新的东西,就是 CSS3 中的 transform 属性。
@ Tips:
transform 属性是 CSS3 中用来对盒模型做变化的,可以用它对元素的形状、大小、位置和旋转角度等进行更改。 transform 可以取的值有很多,这里由于篇幅限制只介绍几种常用的取值,同学们下去可以去w3school上了解下其他的转换方式。
translate(x,y),这个属性值是用来改变元素的位置,两个参数分别是在 x 轴和在 y 轴上的位移。其中 x 和 y 的值就是一般的长度值,可以使用 px、rem 和百分比等单位的值,如:
transform: translate(30px, 50px);
注:如果在 translate 中只有一个值,表示的是在 x 轴上的位移。
scale(x,y), 这个属性值是用来调整元素大小的。x 和 y 的取值是数字,分别表示在 x 轴和 y 轴上缩放的倍数,如果取值小于 1 就是缩小,大于 1 就是放大,如:
transform: scale(1.2, 1.5);
注:如果在 scale 中只有一个值,表示的是在 x 轴和 y 轴上都进行缩放这个参数指定的倍数。
rotate(angle),这个属性值用来调整元素的旋转角度。angle 的值代表的是顺时针旋转的角度,单位是 deg,如:
transform: rotate(45deg);
这里就介绍这三种最常见的用法。在刚使用 transform 属性的时候可能会有点不习惯,个人觉得应该把各种转换方法拿出来作为单独的属性使用会更方便。
有了 transform 属性,刚才的问题就能解决了。前面讲的那种方法里,margin-top 的值是以外层 .tt-modal 的高度为参照物的,所以没法使用百分比。但是现在有了 translate,这个属性做位移的时候参照物是盒子本身的高度,这样通过向上移动盒子自身高度的 50%,就可以把盒子的中心移到 top 值设定的地方。这样无论模态框窗口有多高,都可以保证窗口的中心距页面上边缘 45% 的需求。所以关于窗口容器这部分代码如下:
/* 模态框窗口容器 */
.tt-modal .tt-modal-wrap{
position: absolute;
width: 75%;
max-width: 480px;
top: 45%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: auto;
background: #fff;
border-radius: .4rem;
z-index: 301;
}
这里面我们使用的就是“transform: translateY(-50%);” 这条属性对窗口容器做的位移。这里还有一点要注意,我们使用了“max-width: 480px;”限制了模态框窗口的最大宽度,是防止在大屏幕下模态框过大,所以采用了页面内容区最大宽度 640px 的 75% 作为窗口的最大宽度。
最后就是窗口容器里面的样式了。里面分为内容区和操作区,分别用“tt-modal-body”和“tt-modal-footer”这两个 class 来表示这两部分,其中操作区为了在水平方向放多个按钮,会使用弹性布局。这两个容器的样式就可以按如下方式实现:
/* 模态框内容区 */
.tt-modal .tt-modal-body{
padding: 1.8rem .8rem 1.5rem;
text-align: center;
font-size: .8rem;
line-height: 1.2rem;
overflow: hidden;
}
/* 模态框尾部 */
.tt-modal .tt-modal-footer{
display: flex;
border-top: 1px solid #ddd;
}
/* 模态框尾部里按钮的样式 */
.tt-modal .tt-modal-footer .tt-btn{
border: none;
border-radius: 0;
width: 100%;
font-size: .8rem;
}
/* 模态框尾部中的按钮加上分隔 */
.tt-modal .tt-modal-footer .tt-btn + .tt-btn{
border-left: 1px solid #ddd;
}
三、海报样式模态框的设计与开发
<div class="tt-modal show">
<div class="tt-mask">div>
<div class="tt-modal-wrap">
<div class="tt-modal-body no-padding">
<img class="tt-modal-img" src="./img/modal-test.jpg" alt="">
div>
<i class="fa fa-close tt-modal-close">i>
div>
div>
这个 HTML 结构和刚才基础模态框很相似,只有以下三处的不同:
/* 控制模态框内容区的内边距 */
.tt-modal .tt-modal-body.no-padding{
padding: 0;
}
/* 图片形式的模态框样式 */
.tt-modal .tt-modal-body .tt-modal-img{
display: block;
width: 100%;
border-radius: .3rem;
}
/* 纯图片模态框里的关闭按钮 */
.tt-modal .tt-modal-close{
position: absolute;
left: 0;
right: 0;
width: 1.3rem;
line-height: 1.3rem;
margin: auto;
bottom: -3rem;
text-align: center;
font-size: .8rem;
font-weight: 100;
color: #eee;
border: 1px solid #eee;
border-radius: 50%;
}
这里要注意下,关闭按钮的位置是在外层容器边缘以外的,所以千万不要把外层 tt-modal-wrap 元素设置成“overflow: hidden;”,那样关闭按钮就会被隐藏掉了。
一、页面加载提示工具
加载提示组件并不是只有加载中的状态下才用的到,在页面加载出现异常情况下,也可以使用这个组件进行提示。页面如果加载成功了就会显示页面的内容,因此不需要有加载成功的信息的提示。
对于页面加载提示工具,我们有如下要求:
二、列表加载提示工具
对于列表加载提示工具,也是会有几种状态。最常见的就是加载中的状态。此外,还有当拉到页面底部,可能会用到提示操作的上拉加载更多的提示,还有在没有更多数据的时候显示已经到底的信息等。
对于列表中使用的单行加载提示工具,会有以下要求:
<h1 class="tt-panel-title">页面加载提示组件h1>
<div class="tt-panel-body">
<div class="tt-loading">
<i class="fa fa-circle-o-notch fa-spin tt-loading-icon">i>
<span class="tt-loading-info">用力载入中...span>
div>
<div class="tt-loading">
<i class="fa fa-refresh tt-loading-icon">i>
<span class="tt-loading-info">加载出错,点我重新加载span>
div>
div>
/* 页面加载提示组件 */
.tt-loading{
padding: 1rem 0;
text-align: center;
}
/* 页面加载提示组件的图标 */
.tt-loading > .tt-loading-icon{
font-size: 4.5rem;
color: rgba(0, 0, 0, .05);
}
/* 页面加载提示组件的提示信息 */
.tt-loading > .tt-loading-info{
display: block;
margin-top: .6rem;
font-size: .8rem;
color: #ccc;
}
单行的列表加载提示工具的结构也很简单,也是由文本和图标组成。这里面唯一要注意的就是文本两侧两条横线的实现方式。最直接的做法就是做两个盒子分别放在文本的左右两侧,但是还有更简单的方法,就是直接使用当前容器的边框来实现,再使用文本的背景色把实线中间的部分遮盖住。
<div class="tt-loading-inline">
<span class="tt-loading-info">
<i class="fa fa-circle-o-notch fa-spin tt-loading-icon">i>
用力加载中
span>
div>
/* 单行加载提示组件 */
.tt-loading-inline{
margin: 1.5rem auto 1rem;
width: 12.5rem;
position: relative;
box-sizing: border-box;
text-align: center;
color: #999;
height: 1rem;
border-top: 1px solid rgba(0, 0, 0, .1);
}
/* 单行加载提示组件的文本信息 */
.tt-loading-inline > .tt-loading-info{
display: inline-block;
padding: 0 .5rem;
position: relative;
top: -.7rem;
height: 1rem;
line-height: 1rem;
font-size: .7rem;
background: #fff;
}
/* 单行加载提示组件的图标 */
.tt-loading-inline > .tt-loading-info > .tt-loading-icon{
color: rgba(0, 0, 0, .2);
}
根据需求,在提示信息是上拉加载更多时,需要一个一直在上下振动的上箭头。这个箭头也是图标库里有现成的,我们只需要让它动起来就可以了。在实现效果之前,我们先来介绍一下 CSS3 中制作动画用的 animation 属性。
@Tips:
CSS3 中定义的 animation 属性,可以允许用户自己定义一些简单的动画效果。animation 的语法如下:
animation: name duration timing-function delay iteration-count direction;
animation 里有很多个属性,如果同学们仔细观察的话,动画里的参数和 transition 属性很相似。实际上,我们就可以把 animation 理解成更复杂的渐变效果。下面我们先来介绍下这几个参数:
animation-name(动画的名称),这个参数是用来指定使用哪个效果的动画,这里的名称是需要我们使用 @keyframes 定义出来的。我们先把 @keyframes 放一边,等介绍完 animation 的属性再来介绍这个东西。
animation-duration(动画持续的时间),这个就和渐变属性 transition 里的 duration 一样了,就是一轮动画持续的时间。
animation-timing-function(时间函数),定义了动画的播放速度曲线。
animation-delay(延迟时间),定义了动画播放的延迟时间,表示多长时间以后再开始播放。
animation-iteration-count(循环次数),这个参数式之前没见过的,它定义了动画执行几遍。这个值可以取正整数n,表示动画会播放n遍后停止。如果需要动画一直重复播放的话,把这个参数值设置成“infinite”就可以了。
animation-direction(动画播放方向),这个参数定义了动画播放的模式,它的取值可以是“normal”、“reverse”、“alternate”和“alternate-reverse”这四个值,默认的“normal”表示正常从前到后的播放;“reverse”表示动画从后往前播放;“alternate”表示先从前往后播放,然后再从后往前到播放;“alternate-reverse”表示的就是先从后往前播放,再从前往后播放。
这些就是关于 animation 的属性的介绍下来我们回到刚才的 @keyframes 的用法。如果有细心的同学翻看过 Font Awesome 的源码,看过 fa-spin 类的实现方法的话,就应该看过 @keyframes 的用法:
上面这种写法和之前那种百分比的写法效果是完全一样的,只有在有多个中间状态时,百分比形式的写法才有意义。
了解了 animation 这个属性后,就可以直接在图标的文件中加入需求中要求的动画效果了。
/* 垂直方向上振动 */
.fa-vibrate-y{
animation: fa-vibrate-y 1.5s infinite ease-in;
}
/* 振动轨迹 */
@keyframes fa-vibrate-y{
0% {
transform: translateY(-10%);
}
50% {
transform: translateY(10%);
}
100% {
transform: translateY(-10%);
}
}
我们对 Toast 组件有如下要求:
<body>
<div class="tt-content">
<h1 class="tt-panel-title">Toast提示组件h1>
<div class="tt-panel-body">
<a class="tt-btn btn-primary" id="js-show-toast-loading">加载中提示a>
<br>
<a class="tt-btn btn-primary" id="js-show-toast-success">成功提示a>
div>
div>
<div class="tt-toast" id="js-toast-loading">
<i class="fa fa-spinner fa-spin tt-toast-icon">i>
<p class="tt-toast-info">操作进行中p>
div>
<div class="tt-toast" id="js-toast-success">
<i class="fa fa-check tt-toast-icon">i>
<p class="tt-toast-info">操作成功p>
div>
body>
/* Toast提示工具 */
.tt-toast{
position: fixed;
width: 7rem;
top: 45%;
transform: translateY(-40%);
left: 0;
right: 0;
margin: auto;
padding: 1rem 0;
opacity: 0;
color: #fff;
text-align: center;
background: rgba(0, 0, 0, .6);
border-radius: .4rem;
transition: transform .3s, opacity .3s;
z-index: 301;
}
/* 显示Toast组件 */
.tt-toast.show{
opacity: 1;
transform: translateY(-50%);
}
/* Toast组件中的图标 */
.tt-toast > .tt-toast-icon{
font-size: 2.2rem;
}
/* Toast组件中的文本部分 */
.tt-toast > .tt-toast-info{
margin-top: .5rem;
font-size: .7rem;
}
Toast 组件的样式通过这四组样式就可以实现了,这个样式里的技巧都是我们之前用过的。第 1、3、4 组样式用来给Toast 添加静态样式,然后通过第 2 组 .tt-toast.show 的样式来控制组件是否显示。这里和模态框组件唯一不同的就是这里的显示和隐藏使用的 opacity 属性,而没有用 display 属性。这是因为如果使用“display: none;”属性,当“show“这个 class 一去掉,组件会立即消失,出场动画就没有办法实现了。所以这个组件在使用的时候会分四步进行:
这样做是因为 Toast 是一个一次性的组件,不使用的时候不需要让它停留在文档中。这样做还能避免当元素是“opacity: 0;”的时候会挡住底层内容区的操作。
<script>
window.onload = ()=>{
// 显示加载中的Toast
document.querySelector('#js-show-toast-loading').onclick = (e) => {
let toastEle = document.querySelector('#js-toast-loading');
toastEle.classList.add('show');
setTimeout(()=> {
toastEle.classList.remove('show');
}, 2e3);
};
// 显示操作成功的Toast
document.querySelector('#js-show-toast-success').onclick = (e) => {
let toastEle = document.querySelector('#js-toast-success');
toastEle.classList.add('show');
setTimeout(()=> {
toastEle.classList.remove('show');
}, 2e3);
};
};
</script>
这个组件的需求,一方面是菜单的静态效果,另一方面是菜单的进场和离场的效果。
先来说下弹出式菜单的静态效果:
然后是菜单一些动态的样式:
<body>
<div class="tt-content">
<h1 class="tt-panel-title">ActionSheet弹出式菜单h1>
<div class="tt-panel-body">
<a class="tt-btn" id="js-show">显示弹出式菜单a>
div>
div>
<div class="tt-action-sheet">
<div class="tt-mask">div>
<div class="tt-action-sheet-wrap">
<div class="tt-action-sheet-header">
<h1 class="tt-action-sheet-title">你需要做什么操作?h1>
div>
<div class="tt-action-sheet-body">
<a class="tt-action-sheet-menu">收藏a>
<a class="tt-action-sheet-menu">关注a>
<a class="tt-action-sheet-menu">分享给好友a>
div>
<div class="tt-action-sheet-footer">
<a class="tt-action-sheet-menu" id="js-close">取消a>
div>
div>
div>
body>
/* 弹出菜单容器,默认隐藏在屏幕的下面 */
.tt-action-sheet > .tt-action-sheet-wrap{
position: fixed;
bottom: 0;
left: 0;
right: 0;
max-width: 640px;
margin: auto;
background: #eee;
transition: transform .3s ease;
transform: translateY(100%);
z-index: 301;
}
/* 菜单弹出的时候,改变容器位移 */
.tt-action-sheet.show .tt-action-sheet-wrap{
transform: translateY(0);
}
在对容器的定位中,我们使用固定定位来安排菜单的位置,使用 bottom 值把菜单容器放在了页面的最下面,然后默认情况下又通过 transfrorm 属性把菜单向下移动了菜单高度,也就把菜单隐藏在了屏幕的下边缘。当容器显示的时候,再把位移值归零,这样容器就回到了页面中。这个过程我们还是用了“transition: transform .3s ease;”属性来添加菜单容器的入场和出场效果。
接下来要实现背景的控制,我们在做 Toast 提示组件的时候,是给元素设置了入场和出场动画后把元素移除掉了。但是弹出菜单这个组件通常是藏在页面下方,不会用的时候再加载,所以要让它一直存在于 DOM 中。这样就会造成一个问题,后面的蒙版层我们可以用透明度 opacity 属性来实现淡入淡出效果。当淡出以后蒙版的透明度是 0,但这个元素还是遮盖着后面的内容区的,导致内容区的操作不能进行。遇到这种情况,就要介绍一下“pointer-events”这个属性了。
@ Tips:
pointer-events 这个属性用来指定是否为某个元素触发鼠标点击事件。这个属性主要用于 SVG,但是在 HTML 中也是可以用的,只不过可以取的值只有“auto”和“none”这两个,下来说下这两个取值的含义;
auto,pointer-events 属性默认的取值就是 auto,使用这个属性值的情况下,HTML 元素就是正常的触发点击事件,通常只有为了覆盖不同取值的时候才会使用这个值。
none,给元素用上这个属性值的话,这个元素就变成点不中的了,无论这个元素是什么样式,点击事件都会忽略它而去触发它底层元素的点击事件。
/* 默认隐藏蒙版 */
.tt-action-sheet > .tt-mask{
opacity: 0;
/* 屏蔽元素的点击事件 */
pointer-events: none;
transition: opacity .3s ease;
}
/* 菜单弹出的时候显示蒙版 */
.tt-action-sheet.show > .tt-mask{
opacity: 1;
pointer-events: auto;
}
/* 弹出菜单头部 */
.tt-action-sheet .tt-action-sheet-header{
padding: 0 2rem;
display: flex;
align-items: center;
text-align: center;
height: 3rem;
background: #fff;
}
/* 头部标题,用来描述菜单作用 */
.tt-action-sheet .tt-action-sheet-header > .tt-action-sheet-title{
flex: 1;
font-size: .7rem;
line-height: 1rem;
font-weight: normal;
color: rgba(0, 0, 0, .3);
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
/* 中间主要内容区 */
.tt-action-sheet .tt-action-sheet-body{
border-top: 1px solid rgba(0, 0, 0, .1);
background: #fff;
}
/* 菜单尾部,通常用来放取消按钮 */
.tt-action-sheet .tt-action-sheet-footer{
margin-top: .3rem;
background: #fff;
}
/* 每个菜单项 */
.tt-action-sheet .tt-action-sheet-menu{
display: block;
height: 2.8rem;
line-height: 2.8rem;
font-size: .8rem;
text-align: center;
}
/* 菜单项的边框控制 */
.tt-action-sheet .tt-action-sheet-menu + .tt-action-sheet-menu{
display: block;
border-top: 1px solid rgba(0, 0, 0, .1);
}
window.onload = ()=>{
// 弹出菜单
document.querySelector('#js-show').onclick = (e) => {
document.querySelector('.tt-action-sheet').classList.add('show');
}
// 收回菜单
document.querySelector('#js-close').onclick = (e) => {
document.querySelector('.tt-action-sheet').classList.remove('show');
}
};
这个组件中包括文章标题、文章信息和文章内容这三大部分,其中文章内容里会包含小标题、文本和图片这几个主要的元素。下来我们分析一下我们对这个文本组件的要求。
<div class="tt-content">
<div class="tt-article fold">
<h1 class="tt-article-title">什么是UI框架h1>
<p class="tt-article-info">作者:Rosenp>
<div class="tt-article-content">
<h2 class="tt-article-subtitle">UI是什么?h2>
<p class="tt-article-paragraph">先来说下UI,这俩字母是User Interface的缩写,一般翻译成“用户界面”。UI最主要的功能就是建立用户和系统后台之间的联系,系统后台通过UI把数据转换成可视化的内容展示给用户,同时用户也要通过UI把操作指令(包括数据)传给系统后台。p>
<p class="tt-article-paragraph">对UI不太熟悉的同学一听到这个概念,可能会觉得它的作用就是怎么把一个产品做的漂亮,所以UI设计师经常被人叫成美工。而事实上UI应该是负责“交互”和“视觉”这两方面的工作,这两部分内容构成了产品的用户体验。p>
<p class="tt-article-paragraph">用户体验里最重要的应该是这个产品好不好用,也就是“交互”这部分,这其中包括产品功能是否完善,产品流程是否设计的合理,使用是否方便,操作是否流畅等。在一些大公司里,为了保证产品好用,还会专门设置交互设计师这个职位,专门做交互部分的设计工作。p>
<img class="tt-article-img" src="./img/modal-test.jpg" alt="测试图片">
div>
<div class="tt-article-unfold-btn" id="js-unfold">
<i class="fa fa-angle-double-down fa-vibrate-y">i>
div>
div>
<div class="tt-panel">
<div class="tt-panel-title">其他内容div>
<div class="tt-panel-body">
其他内容...
div>
div>
div>
/* 文章组件 */
.tt-article{
position: relative;
padding: 1rem;
margin-bottom: 1rem;
font-size: .8rem;
line-height: 1.6rem;
background: #fff;
border-bottom: 1px solid #eee;
color: #333;
}
/* 文章被折叠的情况 */
.tt-article.fold{
max-height: 100%;
overflow: hidden;
}
这个容器基础样式中就是边距、边框和字体的处理,然后在第二条样式中使用 .fold 来控制容器的高度,从而达到隐藏多余内容的目的。这里我们把折叠的时候的高度最高设置成100%,这样屏幕稍微往下滑动一点就能看到文本的底边了。这里用的 max-height 而没有用 height 是因为当文本内容占不满整屏高度的话,也不会留出空白。
接下来我们实现按钮的样式,这个按钮不是一般的按钮样式,而是一个带有渐变效果的区域,点击这个区域都可以达到展开文章的目的。这里我们要先处理一下这个渐变的效果,下面要介绍一下background-img属性里“linear-gradient”这个属性值的用法。
@ Tips:
linear-gradient是background-img属性中可取的一个属性值,也可以直接用于background属性中。这个属性值中包含三个主要信息:渐变的方向,渐变的颜色和渐变生效的位置。它完整的格式就是:
background: linear-gradient( angle/direction, color1 position1, color2 position2, …)
在这个结构中,第一个参数是渐变的方向,可以是一个角度值,也可以直接指明哪个方向;color的取值就是平时用的颜色值,position的取值是百分比,表示渐变色的起始或终止的位置。在这一节要实现的文本组件中,我们需要按钮的背景色从透明向纯白色渐变,就可以用如下样式:
background: linear-gradient(180deg, rgba(255,255,255,0) 0, #fff 100%);
这样就是竖直方向上,由上至下从白色透明渐变到白色不透明,也就达到了我们需要的样式。
/* 展开按钮 */
.tt-article .tt-article-unfold-btn{
display: none;
position: absolute;
box-sizing: border-box;
left: 0;
right: 0;
bottom: 0;
height: 5rem;
padding-top: 3rem;
text-align: center;
font-size: 1.5rem;
color: #e0652f;
background: linear-gradient(180deg, rgba(255,255,255,0) 0, #fff 100%);
}
/* 控制展开按钮的显示 */
.tt-article.fold .tt-article-unfold-btn{
display: block;
}
/* 文章大标题 */
.tt-article .tt-article-title{
margin: .5rem 0 1rem;
font-size: 1.4rem;
line-height: 2rem;
}
/* 文章信息,用来放作者、创作时间等 */
.tt-article .tt-article-info{
font-size: .8rem;
color: #aaa;
}
/* 文章小标题 */
.tt-article .tt-article-subtitle{
font-size: 1.1rem;
margin-top: 1rem;
}
/* 文章段落 */
.tt-article .tt-article-paragraph{
margin: .5rem 0 1rem;
}
/* 文章图片 */
.tt-article .tt-article-img{
max-width: 100%;
margin: auto;
}
window.onload = ()=>{
// 展开文章
document.querySelector('#js-unfold').onclick = (e) => {
document.querySelector('.tt-article').classList.remove('fold');
}
};