vue3 之 实现环形倒计时并解决真机运行下环形的层级问题

一、项目使用的技术以及框架:

技术栈: uniapp+vue3+vite+ts+uview-plus

框架地址:https://gitee.com/youyun-uu/uniapp-vue3-vue-cli/tree/master

 

二、使用canvas封装环形组件的问题

由于UI组件没有环形组件,使用canvas绘制环形真机运行出现如下问题:

image

基于以上问题进行修改,中途使用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(); // 自动模式无时长,停止定时器
  }
};

 

四、实现的倒计时

image

 

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

 

posted @ 2026-01-20 14:39  小栗子_persist  阅读(3)  评论(0)    收藏  举报