開(kāi)始之前
今天公司的一個(gè)項(xiàng)目里面有一個(gè)步驟條要實(shí)現(xiàn),這個(gè)項(xiàng)目前端是基于 Vue 和 ElementUI開(kāi)發(fā)。餓了么UI里面是有步驟條(step)這個(gè)組件的,之前的一個(gè)項(xiàng)目是有用過(guò)這個(gè)東西,當(dāng)時(shí)是基于餓了么的那個(gè)組件封裝了一層,做那個(gè)的時(shí)候也挺費(fèi)勁的。今天這個(gè)組件還復(fù)雜一些,再去封裝感覺(jué)還要費(fèi)很長(zhǎng)時(shí)間,衡量了一下還是自己寫(xiě)一個(gè)來(lái)的比較直接,以后自己修改也方便一些。
代碼開(kāi)擼
開(kāi)擼之前先放一張項(xiàng)目里步驟條的圖看看長(zhǎng)什么樣子。

步驟條大致就是這個(gè)樣子里,上面的標(biāo)記大家可以忽略,上面是用一個(gè)前端開(kāi)發(fā)輔助軟件標(biāo)注的,我會(huì)在末尾的時(shí)候告訴大家這個(gè)軟件。
步驟條的功能大家都知到,比如說(shuō)一個(gè)審批流程,創(chuàng)建人發(fā)起,然后到直屬領(lǐng)導(dǎo)審批,直屬領(lǐng)導(dǎo)審批完成了直屬領(lǐng)導(dǎo)的節(jié)點(diǎn)就會(huì)由白色變成一個(gè)藍(lán)色,連接創(chuàng)建人和直屬領(lǐng)導(dǎo)之間的連線(xiàn)也會(huì)由虛線(xiàn)變成實(shí)線(xiàn),后面的節(jié)點(diǎn)依次類(lèi)推,步驟條大致就是這些功能了。
因?yàn)椴煌?xiàng)目對(duì)與生成步驟條的接口的數(shù)據(jù)結(jié)構(gòu)不一樣,所以先不考慮通過(guò)拿到后臺(tái)接口的數(shù)據(jù)去渲染步驟條,咱們先放一張靜態(tài)的頁(yè)面看看怎么去實(shí)現(xiàn)。
<ul class="steps">
<li class="active">
<el-row class="text">
<el-col :span="12">步驟一</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
<li class="active">
<el-row class="text">
<el-col :span="12">步驟一</el-col>
<el-col :span="12">
<div>
<el-avatar :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
<li class="">
<el-row class="text">
<el-col :span="12">步驟三</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
<li class="">
<el-row class="text">
<el-col :span="12">步驟二</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
</ul>
通過(guò)這個(gè)DOM樹(shù)的結(jié)構(gòu)相信做過(guò)前端的朋友應(yīng)該大致猜到了要實(shí)現(xiàn)我們項(xiàng)目的要求應(yīng)該如何控制渲染。步驟條節(jié)點(diǎn)數(shù)據(jù)必定要有一個(gè)表示狀態(tài)的標(biāo)志,我們通過(guò)這個(gè)標(biāo)志來(lái)判斷是否該給 li 節(jié)點(diǎn)添加 active 這個(gè)類(lèi),達(dá)到點(diǎn)亮節(jié)點(diǎn)的效果。而節(jié)點(diǎn)數(shù)據(jù)的其他部分就是負(fù)責(zé)渲染步驟條下面的信息。我們的項(xiàng)目中有一個(gè)?,這個(gè)就是觸發(fā)一個(gè)事件,至于什么樣的事件,咱們?cè)诶锩嫣砑右粋€(gè)圖標(biāo),然后再在圖標(biāo)上放上一個(gè) click 事件,根據(jù)業(yè)務(wù)需求執(zhí)行相關(guān)操作就可以了。
那么我們?cè)趺慈?shí)現(xiàn)節(jié)點(diǎn)點(diǎn)亮和連線(xiàn)的效果呢?那當(dāng)然還是靠 CSS 樣式去渲染它。那么我們把 CSS 的代碼放上來(lái)。
<style lang="scss" scoped>
.steps{
position: relative;
list-style: none;
text-align: center;
counter-reset: step;
}
.steps li{
float: left;
display: inline-block;
width: 20%;
text-align: center;
height: auto;
position: relative;
}
/* 控制節(jié)點(diǎn)那個(gè)圓圈的形成 */
.steps li:before{
counter-increment: step;
content: counter(step);
display: block;
height: 20px;
width: 20px;
font-size:0;
border-radius: 20px;
border:1px solid #6d99ff;
background-color: #fff;
margin: 0 auto;
line-height: 20px;
text-align: center;
margin-bottom: 10px;
}
/* 控制節(jié)點(diǎn)之間的連線(xiàn) */
.steps li ~ li:after{
content: "";
width: 100%;
border: 1px dotted #8db0ff;
position: absolute;
top:10px;
left: calc(-40% - 25px);
z-index: -1;
}
/* 控制節(jié)點(diǎn)點(diǎn)亮的效果渲染 */
.steps li.active:before{
background-color: #1a5fff;
box-shadow:0 0 0 2px #bacfff;
}
.steps li.active:after{
border: 1px solid #8db0ff
}
/* 控制節(jié)點(diǎn)下面的信息樣式渲染 */
.steps .text{
position: relative;
}
.steps .text .el-col, .steps .text .el-col div{
height: 28px !important;
}
.steps .text .el-col{
position: relative;
left: -14px;
}
.steps .text .el-col:first-child{
text-align: right;
height: 28px;
line-height: 28px;
padding-right: 10px;
}
.steps .text .el-col:last-child {
& > div {
margin-bottom: 10px;
& > span {
float: left;
&:last-child {
height: 28px;
line-height: 28px;
padding-left: 10px;
}
}
}
}
</style>
關(guān)于步驟條這塊其實(shí)主要還是樣式的問(wèn)題。寫(xiě)過(guò)前端的一看這些代碼肯定是都了解,但是如果有沒(méi)做過(guò)前端或者對(duì)前端還沒(méi)那么熟悉的不知什么機(jī)緣巧合看到了這篇日記(表示自己很幸運(yùn),感謝)我班門(mén)弄斧的稍微說(shuō)一我當(dāng)時(shí)的思路,ul li 的那一部分是前端常見(jiàn)清除原生樣式,自己手寫(xiě)過(guò)菜單生成代碼的朋友對(duì)這塊應(yīng)該很熟悉。這一部分就是通過(guò) list-style清除無(wú)序列表前面的那個(gè)小黑點(diǎn),然后將 li 做成行內(nèi)塊級(jí)元素,加一個(gè)浮動(dòng),使原來(lái)的豎排列變成行排列。
然后生成節(jié)點(diǎn)的時(shí)候我們使用 :before 偽元素,這個(gè)屬性的作用是在目標(biāo)元素的內(nèi)容前面插入一個(gè)虛擬的元素,它其實(shí)不是真實(shí)存在的,不會(huì)出現(xiàn)DOM中,不會(huì)改變文檔內(nèi)容,不可復(fù)制,僅僅是在CSS渲染出加入。所以它經(jīng)常用做修飾行的內(nèi)容,比如說(shuō)添加個(gè)圖標(biāo)什么的。它必須通過(guò)content屬性來(lái)添加內(nèi)容,然后根據(jù)設(shè)置不同元素形式(塊級(jí),行內(nèi)元素等),來(lái)設(shè)置其它的屬性。這個(gè)地方我設(shè)置了 content ,然后將這個(gè)偽類(lèi)設(shè)置成一個(gè)塊級(jí)元素,設(shè)置寬高。對(duì)于怎么成一個(gè)圓形哪就是 border-radius。我這里設(shè)置的是相同的寬高,是一個(gè)正方形,我們做成一個(gè)圓只要將 broder-radius 設(shè)置成超過(guò)正方形邊長(zhǎng)的一半即可?,F(xiàn)在說(shuō)說(shuō)這個(gè)content值的設(shè)置,其實(shí)相關(guān)的counter-reset 和 counter-increment 這兩個(gè)屬性我平時(shí)很少用到,上次做步驟條看 element-ui 樣式表的時(shí)候發(fā)現(xiàn)了這兩個(gè)屬性。這兩個(gè)屬性的作用創(chuàng)建計(jì)數(shù)器和遞增計(jì)數(shù)器,常用來(lái)排序替代用有序表來(lái)排序。咱們這里其實(shí)是用來(lái)生成節(jié)點(diǎn)的 1 2 3 4 數(shù)字,這樣如果要顯示這些數(shù)字的我們就不用程序去控制了。最后面我會(huì)放上頁(yè)面的最終渲染效果,發(fā)現(xiàn)沒(méi)有顯示這些數(shù)字是因?yàn)槲以谏蓤A圈那段CSS代碼中把font-size設(shè)置成了0,大家注掉會(huì)發(fā)現(xiàn)里面的數(shù)字。
其他地方需要需要注意的屬性是 box-shadow,生成盒子的陰影,我們的很多項(xiàng)目都會(huì)帶有陰影效果(我也不知道為什么??),我會(huì)經(jīng)常用到,要在周?chē)纬梢蝗﹃幱?,大家只要把這個(gè)屬性前三個(gè)屬性都設(shè)置為0 ,第四個(gè)屬性設(shè)置一下想要陰影的大小,然后給個(gè)顏色就可以了。后面的連線(xiàn)就是使用 :after 位元素,生成一個(gè)沒(méi)有寬高,只有邊框的。寬度為整個(gè)父級(jí)寬度一跟線(xiàn),它其實(shí)是連接兩個(gè)圓心的。連線(xiàn)這里有一個(gè)算是比較巧妙的地方,就是你看到我的代碼里設(shè)置 :after 偽類(lèi)的時(shí)候我是從第二個(gè) li 元素開(kāi)始的, 就是 li ~ li:after這個(gè)代碼,如果我們不這樣設(shè)置,當(dāng)我們給第二個(gè) li元素添加 active 類(lèi)名時(shí),節(jié)點(diǎn)后面的線(xiàn)也會(huì)點(diǎn)亮,而且最后一個(gè)節(jié)點(diǎn)還會(huì)有一個(gè)尾巴,那就不是一個(gè)步驟條正確的效果。既然從第二條開(kāi)始那怎么連接第一個(gè)節(jié)點(diǎn)呢,就是改變定位方式,然后加一個(gè)偏移量就搞定了。因?yàn)楦讣?jí)li元素是一個(gè)相對(duì)定位,我們這個(gè)偽元素設(shè)置成絕對(duì)定位時(shí)參照的是相對(duì)父級(jí)的絕對(duì)定位。
關(guān)于下面信息的展示,就沒(méi)啥可以說(shuō)的了,借助element的柵格,然后計(jì)算一下位置的偏移量,最終到達(dá)我們想要的效果就可以了。下面例子靜態(tài)頁(yè)面想象了最多有5個(gè)節(jié)點(diǎn),所以每個(gè)li元素寬度是20%,如果需要更多的節(jié)點(diǎn)就需要?jiǎng)討B(tài)調(diào)整這個(gè)寬度。下面放上最終的效果圖:

寫(xiě)在最后
這篇日記寫(xiě)完,發(fā)現(xiàn)跟我開(kāi)發(fā)這個(gè)組件用的時(shí)間幾乎差不多。再寫(xiě)一遍這個(gè)思路的時(shí)候腦子會(huì)重現(xiàn)出當(dāng)時(shí)寫(xiě)出來(lái)的BUG,然后對(duì)自己不了解的一些屬性會(huì)重新鞏固一遍。上面寫(xiě)的不一定很好,朋友們?nèi)绻l(fā)現(xiàn)不合適的地方還請(qǐng)?jiān)谙旅媪粞裕戎x謝大家的批評(píng)指正了。
最后說(shuō)一下開(kāi)始項(xiàng)目截圖里面用的軟件,是一個(gè)叫 PxCook 的軟件,它分為設(shè)計(jì)和開(kāi)發(fā)模式,只要我們把高保真圖放進(jìn)去就可以測(cè)量出每個(gè)區(qū)域的寬度,取色也很方便,會(huì)生成一些代碼輔助開(kāi)發(fā)。最后的最后附上這個(gè)實(shí)例的全部代碼。
<template>
<ul class="steps">
<li class="active">
<el-row class="text">
<el-col :span="12">步驟一</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
<li class="active">
<el-row class="text">
<el-col :span="12">步驟一</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
<li class="">
<el-row class="text">
<el-col :span="12">步驟三</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
<li class="">
<el-row class="text">
<el-col :span="12">步驟二</el-col>
<el-col :span="12">
<div>
<el-avatar size="small" :src="imgUrl"></el-avatar>
<span>部門(mén)經(jīng)理</span>
</div>
</el-col>
</el-row>
</li>
</ul>
</template>
<script>
export default {
name: "Steps",
data(){
return {
imgUrl: 'https://cube.elemecdn.com/9/c2/f0ee8a3c7c9638a54940382568c9dpng.png'
}
}
}
</script>
<style lang="scss" scoped>
.steps{
position: relative;
list-style: none;
text-align: center;
counter-reset: step;
}
.steps li{
float: left;
display: inline-block;
width: 20%;
text-align: center;
height: auto;
position: relative;
}
.steps li:before{
counter-increment: step;
content: counter(step);
display: block;
height: 20px;
width: 20px;
font-size:0;
border-radius: 20px;
border:1px solid #6d99ff;
background-color: #fff;
margin: 0 auto;
line-height: 20px;
text-align: center;
margin-bottom: 10px;
}
.steps li ~ li:after{
content: "";
width: 100%;
border: 1px dotted #8db0ff;
position: absolute;
top:10px;
left: calc(-40% - 25px);
z-index: -1;
}
.steps li.active:before{
background-color: #1a5fff;
box-shadow:0 0 0 2px #bacfff;
}
.steps li.active:after{
border: 1px solid #8db0ff
}
.steps .text{
position: relative;
}
.steps .text .el-col, .steps .text .el-col div{
height: 28px !important;
}
.steps .text .el-col{
position: relative;
left: -14px;
}
.steps .text .el-col:first-child{
text-align: right;
height: 28px;
line-height: 28px;
padding-right: 10px;
}
.steps .text .el-col:last-child {
& > div {
margin-bottom: 10px;
& > span {
float: left;
&:last-child {
height: 28px;
line-height: 28px;
padding-left: 10px;
}
}
}
}
</style>