2026-04-20 08:57:36 +08:00
|
|
|
<template>
|
2026-04-20 17:22:42 +08:00
|
|
|
<view
|
2026-04-20 08:57:36 +08:00
|
|
|
class="circle-progress"
|
|
|
|
|
:style="{
|
|
|
|
|
width: '5vw',
|
|
|
|
|
height: '5vw',
|
|
|
|
|
}"
|
|
|
|
|
>
|
2026-04-20 17:22:42 +08:00
|
|
|
<view class="circle-bg" :style="{ borderColor: bgColor }"></view>
|
|
|
|
|
<view class="circle-bar-wrapper">
|
|
|
|
|
<view
|
2026-04-20 08:57:36 +08:00
|
|
|
class="circle-bar"
|
|
|
|
|
:style="{
|
|
|
|
|
background: `conic-gradient(${color} ${animateProgress}%, transparent ${animateProgress}%)`,
|
|
|
|
|
'--thickness': `${thickness}px`,
|
|
|
|
|
'--half-thickness': `${thickness / 2}px`,
|
|
|
|
|
}"
|
2026-04-20 17:22:42 +08:00
|
|
|
></view>
|
|
|
|
|
</view>
|
2026-04-20 08:57:36 +08:00
|
|
|
|
|
|
|
|
<!-- 中间文字 -->
|
2026-04-20 17:22:42 +08:00
|
|
|
<view class="circle-content" v-if="showText">
|
|
|
|
|
<view class="progress-number">{{ Math.round(animateProgress) }}%</view>
|
|
|
|
|
<view class="progress-label">进度</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
2026-04-20 08:57:36 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, ref, onMounted, watch } from 'vue';
|
|
|
|
|
interface ProgressProps {
|
|
|
|
|
progress?: number;
|
|
|
|
|
color?: string;
|
|
|
|
|
bgColor?: string;
|
|
|
|
|
thickness?: number;
|
|
|
|
|
showText?: boolean;
|
|
|
|
|
duration?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<ProgressProps>(), {
|
|
|
|
|
progress: 0,
|
2026-04-20 17:22:42 +08:00
|
|
|
color: '#409EFF',
|
|
|
|
|
bgColor: '#E5E5E5',
|
|
|
|
|
thickness: 1,
|
2026-04-20 08:57:36 +08:00
|
|
|
showText: true,
|
2026-04-20 17:22:42 +08:00
|
|
|
duration: 1.5,
|
2026-04-20 08:57:36 +08:00
|
|
|
});
|
|
|
|
|
const realProgress = computed(() => {
|
|
|
|
|
const p = Number(props.progress);
|
|
|
|
|
return isNaN(p) ? 0 : Math.max(0, Math.min(100, p));
|
|
|
|
|
});
|
|
|
|
|
const animateProgress = ref(0);
|
|
|
|
|
const startProgressAnimation = () => {
|
|
|
|
|
const target = realProgress.value;
|
|
|
|
|
const duration = props.duration * 1000;
|
|
|
|
|
const frameRate = 60;
|
|
|
|
|
const totalFrames = duration / (1000 / frameRate);
|
|
|
|
|
const increment = target / totalFrames;
|
|
|
|
|
|
|
|
|
|
let currentFrame = 0;
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
|
currentFrame++;
|
|
|
|
|
animateProgress.value += increment;
|
|
|
|
|
|
|
|
|
|
if (currentFrame >= totalFrames || animateProgress.value >= target) {
|
|
|
|
|
animateProgress.value = target;
|
|
|
|
|
clearInterval(timer);
|
|
|
|
|
}
|
|
|
|
|
}, 1000 / frameRate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
watch(realProgress, () => {
|
|
|
|
|
animateProgress.value = 0;
|
|
|
|
|
startProgressAnimation();
|
|
|
|
|
}, { immediate: false });
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
startProgressAnimation();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.circle-progress {
|
|
|
|
|
position: relative;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
aspect-ratio: 1/1;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #fff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.circle-bg {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border: 3px solid #E8E9EA;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
opacity: 1;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.circle-bar-wrapper {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
z-index: 3;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.circle-bar {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
-webkit-mask: radial-gradient(
|
|
|
|
|
transparent calc(50% - 3px),
|
|
|
|
|
black calc(50% - 3px)
|
|
|
|
|
);
|
|
|
|
|
mask: radial-gradient(
|
|
|
|
|
transparent calc(50% - 3px),
|
|
|
|
|
black calc(50% - 3px)
|
|
|
|
|
);
|
|
|
|
|
&::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 3px;
|
|
|
|
|
left: 3px;
|
|
|
|
|
right: 3px;
|
|
|
|
|
bottom: 3px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
box-shadow: inset 0 0 0 13px rgba(255, 255, 255, 1);
|
|
|
|
|
}
|
|
|
|
|
clip-path: circle(50% at center);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.circle-content {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
width: 80%;
|
|
|
|
|
text-align: center;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
2026-04-20 17:22:42 +08:00
|
|
|
z-index: 3;
|
2026-04-20 08:57:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-number {
|
2026-04-20 17:22:42 +08:00
|
|
|
font-size: 1.2vw;
|
2026-04-20 08:57:36 +08:00
|
|
|
font-weight: 600;
|
|
|
|
|
color: #333;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
margin-bottom: 0.2vw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.progress-label {
|
2026-04-20 17:22:42 +08:00
|
|
|
font-size: 1.1vw;
|
2026-04-20 08:57:36 +08:00
|
|
|
color: #666;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
</style>
|