本文参考作者:yyzclyang的文章进行的实现,由于原文章的实现不符合我自己的需求,所以在此基础上进行了一个二次创作的vue3.0版本。
最终效果
由两部分组成,底部的灰色和上面的蓝色。
<svg :width="size" :height="size" class="circle-rotate">
<circle
:r="radius"
:cx="cx"
:cy="cy"
fill="transparent"
:stroke="bgColor"
:stroke-width="strokeWidth"
/>
<circle
class="circle"
:style="`transition: stroke-dashoffset ${animationTime}s linear;font-size: ${fontSize}px`"
:r="radius"
:cx="cx"
:cy="cy"
fill="transparent"
:stroke="color"
:stroke-width="strokeWidth"
:stroke-dasharray="circumference"
:stroke-dashoffset="progress"
>
</circle>
</svg>
旋转90度让进度条从顶部开始
.circle-rotate {
transform: rotate(-90deg);
}
圆弧
circle 实现的是一个完整的圆圈,要实现圆弧效果该怎么做呢?
这里是利用的 stroke-dasharray 来实现的。
stroke-dasharray 在 svg 中表示描边的是虚线,它可以包含两个值,第一个值是虚线的宽度,第二个是虚线的间距,如果我们把虚线的宽度设置成圆弧的长度,间距设置为超过空白圆弧的长度即可实现圆弧效果。
比如说实现一个 50% 的圆弧,那么第一个值就为 50% * 2 * Math.PI * r,第二个值直接设置为 2 * Math.PI * r,这样不管怎么变,空白圆弧的长度是够的。
线条的圆角 由于线条的圆角是外切会导致进度条多出一块,内切圆角目前也没有实现,所以本文并未使用圆角。
stroke-linecap="round" 这个属性可以设置线条圆角
<circle
:r="radius"
:cx="cx"
:cy="cy"
fill="transparent"
:stroke="color"
:stroke-width="strokeWidth"
stroke-linecap="round"
:stroke-dasharray="circumference"
:stroke-dashoffset="progress"
/>
HTML部分
<template>
<div class="circle-main">
<div class="circle-main-box" :style="[{ width: size + 'px', height: size + 'px' }]">
<svg :width="size" :height="size" class="circle-rotate">
<circle
:r="radius"
:cx="cx"
:cy="cy"
fill="transparent"
:stroke="bgColor"
:stroke-width="strokeWidth"
/>
<circle
class="circle"
:style="`transition: stroke-dashoffset ${animationTime}s linear;font-size: ${fontSize}px`"
:r="radius"
:cx="cx"
:cy="cy"
fill="transparent"
:stroke="color"
:stroke-width="strokeWidth"
:stroke-dasharray="circumference"
:stroke-dashoffset="progress"
></circle>
</svg>
<span class="count-num" :style="[{ 'font-size': size * 0.3 + 'px' }]"
>{{ countNum }}%</span
>
</div>
</div>
</template>
JS+CSS部分
<script lang="ts" setup>
import { computed } from "vue";
const props = withDefaults(
defineProps<{
valueNum: number;
size: number;
strokeWidth: number;
color: string;
bgColor: string;
animationTimeMs: number;
fontSize: number;
}>(),
{
valueNum: 0,
size: 66,
strokeWidth: 10,
color: "#001E70",
bgColor: "#E8F2FF",
animationTimeMs: 500,
fontSize: 18,
}
);
const { valueNum, size, strokeWidth, color, animationTimeMs } = toRefs(props);
const animationTime = computed(() => {
return animationTimeMs.value / 1000;
}); // 动画执行时间
const cx = computed(() => {
return size.value / 2;
}); // 圆心x轴坐标
const cy = computed(() => {
return size.value / 2;
}); // 圆心y轴坐标
const radius = computed(() => {
return (size.value - strokeWidth.value) / 2;
}); // 半径
const circumference = computed(() => {
return 2 * Math.PI * radius.value;
}); // 圆周长
const progress = computed(() => {
return (1 - nowProgress.value / 100) * circumference.value;
}); // 进度长度
const nowProgress = ref(0);
const countNum = ref(0);
const run = () => {
let t = animationTimeMs.value / valueNum.value; // 通过动画时间来计算定时器执行的间隔
let timer = setInterval(() => {
if (countNum.value >= valueNum.value) {
clearTimeout(timer); // 清除定时器
} else {
countNum.value++; // 自增
}
}, t);
nextTick(() => {
// 通过nextTick设置进度条的过渡效果
nowProgress.value = valueNum.value;
});
};
onBeforeMount(() => {
run();
});
</script>
<style lang="scss" scoped>
.circle-main-box {
position: relative;
display: block;
}
.count-num {
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -50px;
align-items: center;
justify-content: center;
display: flex;
user-select: none;
font-size: 18px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #303133;
}
.circle-rotate {
transform: rotate(-90deg);
}
</style>