Vue組件實(shí)現(xiàn)卡片動(dòng)畫倒計(jì)時(shí)示例詳解
前言
最近有朋友在做投票的項(xiàng)目,里面有用到一個(gè)倒計(jì)時(shí)的組件,還想要個(gè)動(dòng)畫效果。cv大法浸染多年的我,首先想到的是直接找個(gè)現(xiàn)有的組件。
通過一通搜索,看上的只有一個(gè) vue2-flip-countdown,但是當(dāng)我要修改大小和顏色的時(shí)候發(fā)現(xiàn)改不了,從而直接把源碼拉到項(xiàng)目里面,改起來也挺麻煩。

而且,在搜索大法運(yùn)行幾個(gè)周天以后,其實(shí)心理已經(jīng)有了一個(gè)倒計(jì)時(shí)開發(fā)整體思路,便決定自己封裝一個(gè)倒計(jì)時(shí)出來,最終實(shí)現(xiàn)效果如下:

需求拆解
- 開發(fā)一個(gè)開箱即用的組件,實(shí)現(xiàn)倒計(jì)時(shí)的效果,顯示模式為 01日01時(shí)01分01秒。
- 以終止時(shí)間為入?yún)?,?jì)算倒計(jì)時(shí)顯示的各個(gè)數(shù)據(jù)。
- 方便的控制顯示的文案,以及主題顏色,大小。
組件設(shè)計(jì)思路
- 組件分為兩個(gè)部分
- animate-clock,控制組件數(shù)據(jù)結(jié)構(gòu),例如:自定義文案、將中文時(shí)間單位改成英文,是否換行等等
- animate-card,動(dòng)畫卡片,即組件里面的具體數(shù)字部分,并加上動(dòng)畫。
- clock 組件中,對(duì)傳入的 props 進(jìn)行處理,最核心的為 終止時(shí)間(terminalTime),根據(jù)終止時(shí)間計(jì)算天、時(shí)、分、秒。
- 通過定時(shí)任務(wù),把計(jì)算出來的 天、時(shí)、分、秒 數(shù)據(jù)傳入 card 組件,觸發(fā)視圖更新。
- card 組件中,當(dāng)數(shù)據(jù)發(fā)生改變的時(shí)候觸發(fā)動(dòng)畫效果。
- 【升級(jí)】在此基礎(chǔ)上進(jìn)行擴(kuò)展,變?yōu)橐粋€(gè)倒計(jì)時(shí)通用解決方案:
- 反向倒計(jì)時(shí)
- 只有數(shù)字的倒計(jì)時(shí)
- 只有分、秒的倒計(jì)時(shí)
- 中文、英文字符串倒計(jì)時(shí)
- 動(dòng)畫抽獎(jiǎng)
- 專注時(shí)間計(jì)時(shí)器
- ...
具體開發(fā)
animate-clock.vue
首先設(shè)計(jì)視圖部分:
<template>
<div class="animate-clock">
<!-- <p>{{days}}{{hours}}{{minites}}{{seconds}}</p> -->
<span>距離結(jié)束還剩</span>
<animate-card :val="days" :size="16" :self-disabled="disabled" />
<span>天</span>
<animate-card :val="hours" :size="16" :self-disabled="disabled" />
<span>時(shí)</span>
<animate-card :val="minites" :size="16" :self-disabled="disabled" />
<span>分</span>
<animate-card :val="seconds" :size="16" :self-disabled="disabled" />
<span>秒</span>
</div>
</template>
<style lang="scss" scoped>
.animate-clock {
width: 100%;
text-align: center;
font-size: 16px;
font-weight: bold;
padding: 40px 0 ;
}
</style>
很簡(jiǎn)單的結(jié)構(gòu),現(xiàn)在版本為截圖所示的一行結(jié)構(gòu),可以看到,完全可以通過業(yè)務(wù)組件中通過傳入 props 的形式,修改每一個(gè)部分的文案,而且 樣式也可以隨時(shí)控制。
js 部分主要做這么幾件事:
- 接收 props,并聲明 data
- 聲明一個(gè) 工具函數(shù),用來 處理 小于 10 的數(shù)字,前面增加 0
- 聲明主要業(yè)務(wù)函數(shù),被定時(shí)任務(wù)調(diào)用的更新數(shù)據(jù)方法
<script>
import animateCard from './animate-card.vue'
export default {
components: { animateCard },
props: {
terminalTime: String,
},
data() {
return {
days: ['0', '0'],
hours: ['0', '0'],
minites: ['0', '0'],
seconds: ['0', '0'],
setIntVal: null,
disabled: false,
}
},
mounted() {
// 先調(diào)用一次
this.updateClock()
// 箭頭函數(shù)不修改當(dāng)前作用域下的 this 指向
this.setIntVal = setInterval(() => {
this.updateClock()
}, 1000)
},
methods: {
/**
* 更新計(jì)時(shí)器
* @result void
*/
updateClock() {
let now = new Date().getTime()
let stopTime = 0
// 錯(cuò)誤入?yún)?處理邏輯
try {
stopTime = new Date(this.terminalTime).getTime()
} catch (err) {
console.error(err)
return false
}
// 終止邏輯
const remainingTime = stopTime - now
if (remainingTime < 1000) {
clearInterval(this.setIntVal)
this.setIntVal = null
// 計(jì)時(shí)器 清零
this.days = this.hours = this.minites = this.seconds = ['0', '0']
this.disabled = true
console.log('時(shí)間到!')
return false
}
// 計(jì)算 日、時(shí)、分、秒
let days = parseInt(remainingTime / (24 * 60 * 60 * 1000))
let hours = parseInt(
(remainingTime - 24 * 60 * 60 * 1000 * days) / (60 * 60 * 1000)
)
let minites = parseInt(
(remainingTime - 24 * 60 * 60 * 1000 * days - 60 * 60 * 1000 * hours) /
(60 * 1000)
)
let seconds = parseInt(
(remainingTime -
24 * 60 * 60 * 1000 * days -
60 * 60 * 1000 * hours -
60 * 1000 * minites) /
1000
)
// 更新 data
this.days = this.toStringAndUnshiftZero(days)
this.hours = this.toStringAndUnshiftZero(hours)
this.minites = this.toStringAndUnshiftZero(minites)
this.seconds = this.toStringAndUnshiftZero(seconds)
},
/**
* 轉(zhuǎn)化數(shù)字為數(shù)組,并在 頭部填充 0
* @params num: numnber
* @result string[]
*/
toStringAndUnshiftZero(num) {
const val = num.toString().split('')
if (num < 10) {
val.unshift('0')
}
return val
},
},
}
</script>
這一塊根本沒有什么技術(shù)含量,主要是異常數(shù)據(jù)的處理,和停止邏輯。 其實(shí)計(jì)時(shí)器清零理論上是不需要出現(xiàn)的,但是在測(cè)試過程中發(fā)現(xiàn),會(huì)出現(xiàn)最后一幀為 01 的情況,就直接清零了。
animate-card
這個(gè)組件一開始考慮的很復(fù)雜,想著監(jiān)聽數(shù)據(jù)的變化,觸發(fā)一個(gè)動(dòng)畫的方法,然后這個(gè)方法支持重寫,提高組件的擴(kuò)展性,但是時(shí)間不允許,后面又覺得不太必要。
最后選擇的方案很簡(jiǎn)單,卻提供了一個(gè)可以實(shí)現(xiàn) css 能實(shí)現(xiàn)的所有效果的思路。
主要思路是通過 vue 的 transition-group 機(jī)制,將 0-9 所有的卡片都渲染好,隱藏起來,通過 v-show 來觸發(fā)綁定在 transition-group 上的動(dòng)畫效果,從而實(shí)現(xiàn)動(dòng)態(tài)監(jiān)聽數(shù)據(jù)變化的效果。
需要注意的是因?yàn)樗拗黜?xiàng)目中 引入了 animate.css,所以就直接使用 animate 的動(dòng)畫效果了。
有需要的,可以翻看文檔 【animate.css 官方文檔】進(jìn)行配置。
如果直接CV這套代碼的話,沒有動(dòng)畫效果。
代碼如下:
<template>
<div class="aimate-card">
<div class="card-group" v-for="(item,idx) in val" :key="idx" :style="{'font-size': size+'px'}">
<transition-group enter-active-class="animate__animated animate__bounceIn" leave-active-class="animate__animated animate__fadeOutDown">
<div class="card-item" :class="{'disabled': selfDisabled}" v-for="num in 10" :key="num" v-show="item== num-1">{{num-1}}</div>
</transition-group>
</div>
</div>
</template>
<script>
export default {
props: {
val: {
type: Array,
default: () => ['0', '0'],
},
size: {
type: Number,
default: 16,
},
selfDisabled: {
type: Boolean,
default: false,
},
},
mounted() {
console.log(this.selfDisabled)
},
}
</script>
<style lang="scss" scoped>
.aimate-card {
width: auto;
display: inline-block;
height: 100%;
.card-group {
display: inline-block;
position: relative;
width: 40px;
padding: 5px;
height: 100%;
vertical-align: middle;
.card-item {
position: absolute;
background: #3a7fe4;
color: #fff;
width: 30px;
height: 40px;
top: -20px;
line-height: 40px;
}
.disabled {
background: #ccc !important;
}
}
}
</style>
看完代碼以后很容易發(fā)現(xiàn),我設(shè)計(jì)的樣式其實(shí)一點(diǎn)都不好看,可以對(duì) card-item 寫一些前端比較炫的效果。而且動(dòng)畫效果也可以自定義。
項(xiàng)目中使用
粘貼上面兩個(gè) vue 文件
在業(yè)務(wù)頁(yè)面中 引入并使用
使用方法如下:
<div class="vote-clock">
<animate-clock :terminalTime="'2023-07-11 23:27:00'" />
</div>
CV大法只支持 terminalTime 這一個(gè)入?yún)ⅰ?/p>
后記
這個(gè)組件很簡(jiǎn)單,但是也有很多可以琢磨的地方,更重要的是整理出一個(gè)開發(fā)通用組件的思路。
另外,這套代碼其實(shí)只是一個(gè)基礎(chǔ)結(jié)構(gòu),擴(kuò)展性還是很強(qiáng)的,尤其是前面設(shè)計(jì)思路中第5條中提到的其他功能,在此基礎(chǔ)上可以很快速的進(jìn)行開發(fā),感興趣的道友可以簡(jiǎn)單琢磨一下。
大家在工作中不可避免的會(huì)遇到多種多樣的需要復(fù)用的代碼塊,有的道友會(huì)選擇提出為一個(gè)公共組件或者公共代碼塊,有的道友則選擇使用CV大法直接復(fù)用,也有的可能會(huì)另辟蹊徑在第一個(gè)場(chǎng)景的基礎(chǔ)上優(yōu)化為第二套代碼。
我以為這三種情況其實(shí)就是一個(gè)遞進(jìn)的關(guān)系,首先CV大法好,發(fā)功后發(fā)現(xiàn)場(chǎng)景不是完全一致,進(jìn)行些許優(yōu)化,進(jìn)而搞出一個(gè)兼容多個(gè)可能存在的場(chǎng)景的通用解決方案。
在設(shè)計(jì)一個(gè)組件的時(shí)候,需要盡可能的考慮多種場(chǎng)景,并考慮一下后續(xù)的升級(jí)方案。想兼容所有的場(chǎng)景肯定不可能,那么就要知道當(dāng)前組件的邊界在哪里。所以,在設(shè)計(jì)組件的時(shí)候不能只著眼于當(dāng)前業(yè)務(wù),更要考慮到其他場(chǎng)景。最簡(jiǎn)單的例如:一個(gè)Button組件,除了保證全局的按鈕風(fēng)格一致的前提下,也要考慮到按鈕禁用狀態(tài),大按鈕,帶圖標(biāo)按鈕,按鈕組這些方面的東西。
以上就是我關(guān)于做這個(gè)小組件之后進(jìn)行的一些淺顯的思考,共勉之,更多關(guān)于Vue卡片動(dòng)畫倒計(jì)時(shí)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決antd Form 表單校驗(yàn)方法無響應(yīng)的問題
這篇文章主要介紹了解決antd Form 表單校驗(yàn)方法無響應(yīng)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10
Vue關(guān)于Element UI中的文本域換行問題
這篇文章主要介紹了Vue關(guān)于Element UI中的文本域換行問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
詳解基于 axios 的 Vue 項(xiàng)目 http 請(qǐng)求優(yōu)化
這篇文章主要介紹了詳解基于 axios 的 Vue 項(xiàng)目 http 請(qǐng)求優(yōu)化,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-09-09

