前言
最近有朋友在做投票的项目,里面有用到一个倒计时的组件,还想要个动画效果。cv大法浸染多年的我,首先想到的是直接找个现有的组件。
通过一通搜索,看上的只有一个 vue2-flip-countdown,但是当我要修改大小和颜色的时候发现改不了,从而直接把源码拉到项目里面,改起来也挺麻烦。
而且,在搜索大法运行几个周天以后,其实心理已经有了一个倒计时开发整体思路,便决定自己封装一个倒计时出来,最终实现效果如下:
需求拆解
- 开发一个开箱即用的组件,实现倒计时的效果,显示模式为 01日01时01分01秒。
- 以终止时间为入参,计算倒计时显示的各个数据。
- 方便的控制显示的文案,以及主题颜色,大小。
组件设计思路
- 组件分为两个部分
- animate-clock,控制组件数据结构,例如:自定义文案、将中文时间单位改成英文,是否换行等等
- animate-card,动画卡片,即组件里面的具体数字部分,并加上动画。
- clock 组件中,对传入的 props 进行处理,最核心的为 终止时间(terminalTime),根据终止时间计算天、时、分、秒。
- 通过定时任务,把计算出来的 天、时、分、秒 数据传入 card 组件,触发视图更新。
- card 组件中,当数据发生改变的时候触发动画效果。
- 【升级】在此基础上进行扩展,变为一个倒计时通用解决方案:
- 反向倒计时
- 只有数字的倒计时
- 只有分、秒的倒计时
- 中文、英文字符串倒计时
- 动画抽奖
- 专注时间计时器
- ...
具体开发
animate-clock.vue
首先设计视图部分:
<template> <div class="animate-clock"> <!-- <p>{{days}}{{hours}}{{minites}}{{seconds}}</p> --> <span>距离结束还剩</span> <animate-card :val="days" :size="16" :self-disabled="disabled" /> <span>天</span> <animate-card :val="hours" :size="16" :self-disabled="disabled" /> <span>时</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>
很简单的结构,现在版本为截图所示的一行结构,可以看到,完全可以通过业务组件中通过传入 props 的形式,修改每一个部分的文案,而且 样式也可以随时控制。
js 部分主要做这么几件事:
- 接收 props,并声明 data
- 声明一个 工具函数,用来 处理 小于 10 的数字,前面增加 0
- 声明主要业务函数,被定时任务调用的更新数据方法
<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() { // 先调用一次 this.updateClock() // 箭头函数不修改当前作用域下的 this 指向 this.setIntVal = setInterval(() => { this.updateClock() }, 1000) }, methods: { /** * 更新计时器 * @result void */ updateClock() { let now = new Date().getTime() let stopTime = 0 // 错误入参 处理逻辑 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 // 计时器 清零 this.days = this.hours = this.minites = this.seconds = ['0', '0'] this.disabled = true console.log('时间到!') return false } // 计算 日、时、分、秒 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) }, /** * 转化数字为数组,并在 头部填充 0 * @params num: numnber * @result string[] */ toStringAndUnshiftZero(num) { const val = num.toString().split('') if (num < 10) { val.unshift('0') } return val }, }, } </script>
这一块根本没有什么技术含量,主要是异常数据的处理,和停止逻辑。 其实计时器清零理论上是不需要出现的,但是在测试过程中发现,会出现最后一帧为 01 的情况,就直接清零了。
animate-card
这个组件一开始考虑的很复杂,想着监听数据的变化,触发一个动画的方法,然后这个方法支持重写,提高组件的扩展性,但是时间不允许,后面又觉得不太必要。
最后选择的方案很简单,却提供了一个可以实现 css 能实现的所有效果的思路。
主要思路是通过 vue 的 transition-group 机制,将 0-9 所有的卡片都渲染好,隐藏起来,通过 v-show 来触发绑定在 transition-group 上的动画效果,从而实现动态监听数据变化的效果。
需要注意的是因为宿主项目中 引入了 animate.css,所以就直接使用 animate 的动画效果了。
有需要的,可以翻看文档 【animate.css 官方文档】进行配置。
如果直接CV这套代码的话,没有动画效果。
代码如下:
<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>
看完代码以后很容易发现,我设计的样式其实一点都不好看,可以对 card-item 写一些前端比较炫的效果。而且动画效果也可以自定义。
项目中使用
粘贴上面两个 vue 文件
在业务页面中 引入并使用
使用方法如下:
<div class="vote-clock"> <animate-clock :terminalTime="'2023-07-11 23:27:00'" /> </div>
CV大法只支持 terminalTime 这一个入参。
后记
这个组件很简单,但是也有很多可以琢磨的地方,更重要的是整理出一个开发通用组件的思路。
另外,这套代码其实只是一个基础结构,扩展性还是很强的,尤其是前面设计思路中第5条中提到的其他功能,在此基础上可以很快速的进行开发,感兴趣的道友可以简单琢磨一下。
大家在工作中不可避免的会遇到多种多样的需要复用的代码块,有的道友会选择提出为一个公共组件或者公共代码块,有的道友则选择使用CV大法直接复用,也有的可能会另辟蹊径在第一个场景的基础上优化为第二套代码。
我以为这三种情况其实就是一个递进的关系,首先CV大法好,发功后发现场景不是完全一致,进行些许优化,进而搞出一个兼容多个可能存在的场景的通用解决方案。
在设计一个组件的时候,需要尽可能的考虑多种场景,并考虑一下后续的升级方案。想兼容所有的场景肯定不可能,那么就要知道当前组件的边界在哪里。所以,在设计组件的时候不能只着眼于当前业务,更要考虑到其他场景。最简单的例如:一个Button组件,除了保证全局的按钮风格一致的前提下,也要考虑到按钮禁用状态,大按钮,带图标按钮,按钮组这些方面的东西。
以上就是我关于做这个小组件之后进行的一些浅显的思考,共勉之,更多关于Vue卡片动画倒计时的资料请关注阿兔在线工具其它相关文章!