hldy_app_mini/pages/material/component/gress.vue

169 lines
3.5 KiB
Vue

<template>
<view
class="circle-progress"
:style="{
width: '5vw',
height: '5vw',
}"
>
<view class="circle-bg" :style="{ borderColor: bgColor }"></view>
<view class="circle-bar-wrapper">
<view
class="circle-bar"
:style="{
background: `conic-gradient(${color} ${animateProgress}%, transparent ${animateProgress}%)`,
'--thickness': `${thickness}px`,
'--half-thickness': `${thickness / 2}px`,
}"
></view>
</view>
<!-- 中间文字 -->
<view class="circle-content" v-if="showText">
<view class="progress-number">{{ Math.round(animateProgress) }}%</view>
<view class="progress-label">进度</view>
</view>
</view>
</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,
color: '#409EFF',
bgColor: '#E5E5E5',
thickness: 1,
showText: true,
duration: 1.5,
});
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;
z-index: 3;
}
.progress-number {
font-size: 1.2vw;
font-weight: 600;
color: #333;
line-height: 1;
margin-bottom: 0.2vw;
}
.progress-label {
font-size: 1.1vw;
color: #666;
line-height: 1;
}
</style>