vue3 之 实现环形倒计时并解决真机运行下环形的层级问题
一、项目使用的技术以及框架:
技术栈: uniapp+vue3+vite+ts+uview-plus
框架地址:https://gitee.com/youyun-uu/uniapp-vue3-vue-cli/tree/master
二、使用canvas封装环形组件的问题
由于UI组件没有环形组件,使用canvas绘制环形真机运行出现如下问题:

基于以上问题进行修改,中途使用canvas-view换弹窗,这个方法是可以使用的,但是除了换掉当前弹窗,还有弹出层等样式也出现了该问题,所以最好换掉环形组件;
以下使用 hyh-progress来换之前的环形组件:
插件地址:https://ext.dcloud.net.cn/plugin?id=11196
三、利用hyh-progress插件实现环形
1、创建组件hyh-progress.vue
<template> <view class="circle-container" :style="boxSize"> <view class="circle" :style="circleStyle"> <view v-if="fillet" class="border-start" :style="borderStyle"></view> <view class="circle-left ab" :style="renderLeftRate(dispalyRate)"><view v-if="fillet && dispalyRate >= 50" class="border-left-end" :style="borderStyle"></view></view> <view class="circle-right ab" :style="renderRightRate(dispalyRate)"><view v-if="fillet && dispalyRate < 50" class="border-right-end" :style="borderStyle"></view></view> </view> <view class="text-area" :style="textareaStyle"><slot></slot></view> </view> </template> <script setup> import { computed, getCurrentInstance, onMounted, ref } from 'vue'; const props = defineProps({ rate: { type: Number, require: true }, width: { type: String, default: '20rpx' }, activeColor: { type: String, default: '#54c4fd' }, inactiveColor: { type: String, default: '#546063' }, startAngle: { type: Number, default: 0 }, fillet: { type: Boolean, default: false } }); const dispalyRate = computed(() => { if (props.rate <= 0) { return 0; } else if (props.rate >= 100) { return 100; } else if (props.rate <= 3) { return 1; } else { return props.rate - 3; } }); const circleStyle = computed(() => { const width = getNumberAndUnit(props.width).num - 0.3; const unit = getNumberAndUnit(props.width).unit; return `box-shadow: inset 0 0 0 ${width + unit} ${props.activeColor};transform:rotate(${props.startAngle}deg)`; }); const borderStyle = computed(() => { if (dispalyRate.value == 0) return ''; return `width:${props.width};height:${props.width};background-color:${props.activeColor};`; }); const textareaStyle = computed(() => { return `width:calc(100% - ${props.width});height:calc(100% - ${props.width});`; }); onMounted(() => { getSize(); }); const renderRightRate = rate => { const border = `border: ${props.width} solid ${props.inactiveColor};`; if (rate < 50) { return border + 'transform: rotate(' + 3.6 * rate + 'deg);'; } else { return border + `transform: rotate(0);border-color: ${props.activeColor};`; } }; const renderLeftRate = rate => { const border = `border: ${props.width} solid ${props.inactiveColor};`; if (rate >= 50) { return border + 'transform: rotate(' + 3.6 * (rate - 50) + 'deg);'; } else { return border; } }; const boxSize = ref(''); function getSize() { getWidth().then(res => { const { width, height } = res; const size = width < height ? width : height; boxSize.value = `width:${size}px;height:${size}px;`; }); } function getWidth() { return new Promise((resolve, reject) => { try { const { ctx } = getCurrentInstance(); uni.createSelectorQuery() .in(ctx) .select('.circle-container') .boundingClientRect(res => { resolve(res); }) .exec(); } catch (e) { //TODO handle the exception reject(e); } }); } function getNumberAndUnit(str) { const numReg = /\d+/g; const unitReg = /[a-z]+/; const num = str.match(numReg); const unit = str.match(unitReg); return { num: num[0], unit: unit[0] }; } </script> <style lang="scss" scoped> .circle-container { position: relative; width: 100%; height: 100%; .circle { position: relative; width: 100%; height: 100%; border-radius: 50%; .ab { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } .circle-left { border-radius: 50%; clip-path: polygon(0% 0%, 50% 0%, 50% 100%, 0% 100%); } .circle-right { border-radius: 50%; clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%); } .border-start { position: absolute; top: 0; left: 50%; transform: translateX(-50%); z-index: 1; border-radius: 50%; } .border-left-end { position: absolute; bottom: 0px; left: 50%; transform: translate(-50%, 100%); z-index: 1; border-radius: 50%; } .border-right-end { position: absolute; top: 0; left: 50%; transform: translate(-50%, -100%); z-index: 1; border-radius: 50%; } } .text-area { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; } } </style>
2、使用组件
1)引入组件
import * as hyhProgress from "@/components/hyhProgress/index.vue";
2)使用组件
<view class="circle-progress-area"> <view class="test-container"> <hyh-progress :rate="progressPercent" width="20rpx" :fillet="false" activeColor="#6161d2" inactiveColor="#e5e9ff" :startAngle="0" > <view class="timer-circle-content"> <text class="mode-text">{{ currentMode ? `${currentMode + "模式"}` : "未选择模式" }}</text> <text class="time-text">{{ modeDuration }}</text> <text class="total-time">{{ totalDuration }}</text> </view> </hyh-progress> </view> </view>
3)初始化每个模式的时长
const initModeDuration = (modeName: string) => { const targetMode = modeList.value.find((item) => item.name === modeName); if (targetMode) { // 时长转换:分钟 → 秒 const minutes = targetMode.times; totalSeconds.value = minutes * 60; remainingSeconds.value = minutes * 60; // 格式化时长为 "MM:SS" modeDuration.value = `${minutes.toString().padStart(2, "0")}:00`; totalDuration.value = `共${minutes}分钟`; } else { totalSeconds.value = 0; remainingSeconds.value = 0; modeDuration.value = "00:00"; totalDuration.value = "共00分钟"; } };
4)时间格式化,倒计时需要转成秒
// 秒转时分格式 const updateDurationDisplay = () => { const minutes = Math.floor(remainingSeconds.value / 60); const seconds = remainingSeconds.value % 60; modeDuration.value = `${minutes.toString().padStart(2, "0")}:${seconds .toString() .padStart(2, "0")}`; };
5)启用倒计时
// 启动倒计时 const startCountdown = () => { // 清除已有定时器,避免重复计时 if (countdownTimer.value) clearInterval(countdownTimer.value); updateDurationDisplay(); countdownTimer.value = setInterval(() => { remainingSeconds.value--; // 进度 = 剩余时间 / 总时间 * 100(从100%递减到0) progressPercent.value = totalSeconds.value > 0 ? (remainingSeconds.value / totalSeconds.value) * 100 : 0; updateDurationDisplay(); // 倒计时结束处理 if (remainingSeconds.value <= 0) { stopCountdown(); resetModeState(); } }, 1000); };
6)停止倒计时
// 停止倒计时 const stopCountdown = () => { if (countdownTimer.value) { clearInterval(countdownTimer.value); countdownTimer.value = null; } };
7)重置数据
// 重置模式状态 const resetModeState = () => { currentMode.value = null; // 回到未选择模式 modeDuration.value = "00:00"; totalDuration.value = "共00分钟"; progressPercent.value = 0; totalSeconds.value = 0; remainingSeconds.value = 0; };
8)选择不同的模式进行倒计时
// 模式选择核心逻辑(按你的需求重构) const handleModeSelect = async (val: any) => { if (!validateToken()) return; if (!isOpen.value) { uni.showToast({ title: "请先打开开关", icon: "none" }); return; } // 当前已有选中模式且和点击的模式一致,且倒计时正在进行 if (currentMode.value === val.name && countdownTimer.value) { uni.showToast({ title: "正在治疗中", icon: "none", }); return; } await getModeDuration(); currentMode.value = val.name; initModeDuration(val.name); const tempSelectedModelCode = modeList.value.find( (item) => item.name === val.name )?.code; const res = await sfmCalendarKeepRecord(tempSelectedModelCode); if (res.code !== 200) return // 自动启动倒计时(无暂停,直接开始) if (totalSeconds.value > 0) { progressPercent.value = 100; startCountdown(); } else { progressPercent.value = 0; stopCountdown(); // 自动模式无时长,停止定时器 } };
四、实现的倒计时

注:该文档为个人理解所写,有误可建议修改

浙公网安备 33010602011771号