兼容H5、APP、微信小程序
效果图
1.封装成组件 /components/CountdownTip/CountdownTip.vue
<template>
<view class="wrapper">
<!-- 右半边圆 -->
<view class="container container1">
<!-- 里面的半圆 -->
<view class="halfCir"
:style="[{ background: pathColor , 'animation-play-state':(timer===null)? 'paused':'running','animation-duration':`${animationDuration}s`,}]">
</view>
</view>
<!-- 左半边圆 -->
<view class="container container2">
<!-- 里面的半圆 -->
<view class="halfCir"
:style="[{ background: pathColor ,'animation-play-state':(timer===null)? 'paused':'running','animation-duration':`${animationDuration}s`,}]">
</view>
</view>
<!-- 这两个是控制圆角的小圆 -->
<view class="cir cir1" :style="{ background: pathColor }"></view>
<view class="cir cir2"
:style="[{ background: pathColor,'animation-play-state':(timer===null)? 'paused':'running','animation-duration':`${animationDuration}s`,}]">
</view>
<!-- 当前秒数 -->
<view class="label" :style="{ color: pathColor }">
<text class="num" :style="{fontSize:`${numSize}px`}">{{time}}</text><text class="unit"
:style="{fontSize:`${unitSize}px`}">s</text>
</view>
</view>
</template>
<script>
const FULL_DASH_ARRAY = 283;
const DEFAULT_COLOR = '#2e9a3c';
export default {
name: 'CountdownTip',
props: {
endTime: {
type: Number,
default: 0
}, // 结束时间,单位s
startTime: Number, // 开始时间,单位s
step: {
type: Number,
default: -1
}, // 间隔时间,单位s,倒计时为负数,正计时为正数
onFinished: Function, // 计时结束事件
thresholds: Array, // 阶段阈值,以及颜色变化。
wh: {
type: Number,
default: 220
}, // 圆圈宽度高度。
numSize: {
type: Number,
default: 80
}, // 数子字体大小
unitSize: {
type: Number,
default: 60
}, // 单位字体大小
isPausedTimer: Boolean,
},
data() {
return {
time: this.startTime,
animationDuration: this.startTime,
timer: null,
timeLimit: Math.abs(this.startTime - this.endTime),
}
},
computed: {
// 路径颜色
pathColor() {
let _this = this
let result = DEFAULT_COLOR
if (Array.isArray(this.thresholds)) {
_this.thresholds
.sort((a, b) => a.threshold - b.threshold)
.some(item => {
if (this.time <= this.timeLimit * item.threshold) {
// 根据当前时间获取距离最近的阈值的颜色
result = item.color;
return true;
}
return false;
});
}
return result
},
// stroke虚线数组
strokeDashArray() {
// 圆滑过渡
const rawTimeFraction = this.time / this.timeLimit;
const timeFraction = rawTimeFraction - (1 / this.timeLimit) * (1 - rawTimeFraction);
return `${(timeFraction * FULL_DASH_ARRAY).toFixed(
0,
)} ${FULL_DASH_ARRAY}`
}
},
mounted() {
this.timer = setInterval(() => {
// step>0正计时 和 step<0倒计时
if (
(this.step < 0 && this.time <= this.endTime) ||
(this.step > 0 && this.time >= this.endTime)
) {
this.onFinished?.();
clearInterval(this.timer);
} else {
const cur = this.time + this.step;
this.time = cur <= 0 ? 0 : cur;
}
}, Math.abs(this.step) * 1000);
},
beforeDestroye() {
clearInterval(this.timer);
this.timer = null
},
watch: {
isPausedTimer: {
handler(isTrue) {
console.log("isTrue===",isTrue);
if (isTrue) {
clearInterval(this.timer);
this.timer = null
}
},
deep: true
}
}
}
</script>
<style lang="scss" scoped>
.wrapper {
position: relative;
width: 220px;
height: 220px;
margin: auto;
/* 加上背景色和圆角 */
border-radius: 50%;
background: #f5f5f5;
}
.container {
position: absolute;
top: 0;
bottom: 0;
width: 110px;
overflow: hidden;
}
.halfCir {
width: 110px;
height: 220px;
background-color: #1A1A1A;
}
.container1 {
left: 110px;
}
.container1 .halfCir {
left: 0;
border-radius: 0 220px 220px 0;
transform-origin: 0 50%;
animation: halfCir1 60s 1 linear;
}
.container2 {
left: 0;
}
.container2 .halfCir {
border-radius: 220px 0 0 220px;
transform-origin: 110px 110px;
animation: halfCir2 60s 1 linear;
}
@keyframes halfCir1 {
50%,
100% {
transform: rotateZ(180deg);
}
}
@keyframes halfCir2 {
0%,
50% {
transform: rotateZ(0);
}
100% {
transform: rotateZ(180deg);
}
}
.wrapper::after {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 180px;
height: 180px;
border-radius: 50%;
content: "";
background-color: #fff;
}
.cir {
position: absolute;
top: 0;
right: 0;
left: 0;
width: 20px;
height: 20px;
margin: auto;
background: #FFD600;
border-radius: 50%;
}
.cir1 {
// left: -10px;
// border-radius: 0 20px 20px 0;
// background: #f5f5f5;
// z-index: -1;
}
.cir2 {
transform-origin: 50% 110px;
animation: cir2 60s 1 linear;
}
@keyframes cir2 {
100% {
transform: rotateZ(360deg);
}
}
.label {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
transform: translate(-50%, -50%);
font-size: 36px;
color: #FFD600;
}
</style>
2.页面引入
<template>
<view class="" style="margin-top: 200px;">
<CountdownTip :start-time="timeout" :step="-1" :isPausedTimer="isPausedTimer" :thresholds="thresholds">
</CountdownTip>
</view>
</template>
<script>
import CountdownTip from '@/components/CountdownTip/CountdownTip.vue'
export default {
components: {
CountdownTip
},
data() {
return {
timeout: 60,
isPausedTimer: false,
thresholds: [{
color: '#fed45f',
threshold: 0.5
},
{
color: '#FF1100',
threshold: 0.25
},
{
color: '#f5f5f5',
threshold: 0
}
]
}
},
onLoad() {},
mounted() {},
beforeDestroy() {
this.pausedCountdown()
},
// 使用hash路由刷新页面不会触发销毁函数 在该方法执行你的逻辑即可
beforeRouteLeave(to, from, next) {
this.pausedCountdown()
next()
},
methods: {
pausedCountdown() {
this.isPausedTimer = true
},
},
}
</script>
<style lang="scss" scoped>
</style>