``
<div class="circle-process">
<svg :key="useSvgRender" class="svg-ele" :viewBox="`0, 0, ${pointX * 2}, ${pointX * 2}`">
<defs>
<linearGradient x1="1" y1="0" x2="0" y2="0" :id="`outGradient` + useSvgRender">
<stop offset="0%" :stop-color="circleAminationOption.outStarColor" />
<stop offset="100%" :stop-color="circleAminationOption.outEndColor" />
</linearGradient>
<linearGradient x1="1" y1="0" x2="0" y2="0" :id="`innerGradient` + useSvgRender">
<stop offset="0%" :stop-color="circleAminationOption.innerStarColor" />
<stop offset="100%" :stop-color="circleAminationOption.innerEndColor" />
</linearGradient>
</defs>
<circle
class="circle-bg"
:cx="pointX"
:cy="pointX"
:r="circleConfig.cr"
:stroke="circleConfig.bgColor"
:stroke-width="circleConfig.bgWidth"
/>
<circle
ref="circleRef"
class="circle-color"
:cx="pointX"
:cy="pointX"
:r="circleConfig.cr"
:stroke="`url(#innerGradient${useSvgRender})`"
:stroke-width="circleConfig.cWidth"
:stroke-dasharray="`0, 1000000`"
>
<animate
:to="`${circleAminationOption.innerArcLength},1000000`"
:begin="circleAminationOption.outDurtion"
:dur="circleAminationOption.innerDurtion"
:from="`${circleAminationOption.innerInitArcLength},1000000`"
calcMode="linear"
keyTimes="0;0.5;1"
attributeName="stroke-dasharray"
fill="freeze"
/>
</circle>
<circle
ref="circleRef"
class="circle-color"
:cx="pointX"
:cy="pointX"
:r="circleConfig.cr"
:stroke="`url(#outGradient${useSvgRender})`"
:stroke-width="circleConfig.cWidth"
:stroke-dasharray="`${circleAminationOption.outArcLength},1000000`"
>
<animate
:to="`${circleAminationOption.outArcLength},1000000`"
begin="0s"
:dur="circleAminationOption.outDurtion"
from="0,1000000"
attributeName="stroke-dasharray"
calcMode="linear"
keyTimes="0;0.5;1"
fill="freeze"
/>
</circle>
<g>
<line
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-dasharray="null"
stroke-linejoin="null"
stroke-linecap="round"
:x1="pointX"
y1="0"
:x2="pointX"
:y2="circleConfig.cWidth * 2"
:transform="`rotate(${360 * Number(count)} ${pointX} ${pointX})`"
/>
<animateTransform
attributeName="transform"
begin="0s"
:dur="circleConfig.duration"
type="rotate"
calcMode="linear"
keyTimes="0;0.5;1"
:from="`${360 - 360 * Number(count)} ${pointX} ${pointX}`"
:to="`360 ${pointX} ${pointX}`"
/>
</g>
</svg>
<div class="circle-count">
<slot>{{ count * 100 }}</slot>
</div>
</div>
</template>
<script setup>
/**
* fill: none; // 圆的填充色
* stroke: #7c83fd;
* stroke-width: 10; // 画笔的宽度
* stroke-dasharray: 314; // 圆的周长
* stroke-dashoffset: 314;
* stroke-linecap: round; // 圆的头部
* cx: '80', // 圆环在svg中的坐标x
* cy: '80', // 圆环在svg中的坐标y
* cr: '50', // 圆环的半径
* :transform="`rotate(${360 * Number(count)})`"
*/
const props = defineProps({
circleConfig: {
type: Object,
default: () => {
return {
cr: '30',
cWidth: '4', // 进度圆弧的宽度
color: ['rgba(244,112,33)', 'rgba(255,207,93)'], // 进度圆弧的颜色,可以为字符,数组,fn, 颜色支持rbga和十六进制
bgColor: 'rgba(255, 255, 255, 0.4)', //背景圆的颜色,颜色支持rbga和十六进制
bgWidth: '2', // 背景圆的宽度
lineSize: '20', // 指针的高度大小:暂时没有用到
duration: '2s' // 动画间隔
}
}
},
// repeat: {
// type: String,
// default: 'indefinite'
// },
count: {
type: [Number, String],
default: 0.9
}
})
// 环的周长
const circleLength = computed(() => {
return Math.floor(2 * Math.PI * props.circleConfig.cr)
})
// // 环的stroke-dasharray的值
// const circleProgressLen = computed(() => {
// let progressLength = Number(props.count) * circleLength.value
// return `${progressLength},1000000`
// })
const pointX = computed(() => {
return Number(props.circleConfig.cr) + Number(props.circleConfig.cWidth)
})
// 计算圆环动画相关的值
// const circleAminationOption = ref({
// outArcLength: 0, // 外层圆弧渲染长度
// outDurtion: 0, // 外层动画执行时间
// innerArcLength: 0, // 内层圆弧渲染长度
// innerInitArcLength: 0, // 内层圆弧开始渲染的其实位置
// innerDurtion: 0, // 内层动画执行时间
// outStarColor: '', // 外层圆弧渲染起始颜色
// outEndColor: '', // 外层圆弧渲染结束颜色
// innerStarColor: '', // 内层圆弧渲染起始颜色
// innerEndColor: '' // 内层圆弧渲染结束颜色
// })
const circleAminationOption = computed(() => {
/**
* 1、如果进度小于一半,渐变色不需要两个圆实现
* 2、如果进度条不需要渐变色,也可以不需要两个圆实现
*/
// 一个环分为两半,inner— 上部分半环的数据(内层圆弧), out- 下部分半环的数据(最外层圆弧)
let circleAminationOption = {
outArcLength: 0, // 外层圆弧渲染长度
outDurtion: 0, // 外层动画执行时间
innerArcLength: 0, // 内层圆弧渲染长度
innerInitArcLength: 0, // 内层圆弧开始渲染的其实位置
innerDurtion: 0, // 内层动画执行时间
outStarColor: '', // 外层圆弧渲染起始颜色
outEndColor: '', // 外层圆弧渲染结束颜色
innerStarColor: '', // 内层圆弧渲染起始颜色
innerEndColor: '' // 内层圆弧渲染结束颜色
}
let circleProgressColor = props.circleConfig.color || ''
if (!circleProgressColor) return circleAminationOption
if (typeof circleProgressColor === 'function') {
circleProgressColor = circleProgressColor()
}
// 单个色值环不用分上下半部分
if (
typeof circleProgressColor === 'string' ||
(circleProgressColor instanceof Array && circleProgressColor.length === 1)
) {
circleAminationOption.outArcLength = Number(props.count) * circleLength.value
circleAminationOption.outDurtion = props.circleConfig.duration
circleAminationOption.innerArcLength = 0
circleAminationOption.innerInitArcLength = 0 // 为动画做准备
circleAminationOption.innerDurtion = 0
circleAminationOption.outEndColor = circleAminationOption.outStarColor =
typeof circleProgressColor === 'string' ? circleProgressColor : circleProgressColor[0]
return circleAminationOption
}
// 进度小于0.5时不用分上下半部分
if (Number(props.count) < 0.5) {
circleAminationOption.outArcLength = Number(props.count) * circleLength.value
circleAminationOption.outDurtion = props.circleConfig.duration
circleAminationOption.outStarColor = circleProgressColor[0]
circleAminationOption.outEndColor = circleProgressColor[1]
} else {
// 动画:
// 由下部分颜色值从0s开始渲染,下部分的渲染时长为 ((0.5 / 进度值) * 总渲染时长);渲染长度为 0.5 * 进度值 * 环的周长
// 下部分的渲染位置从 0 到 下半部分总渲染长度:渲染长度为 0.5 * 进度值 * 环的周长
// 上半部分 从 ((0.5 / 进度值) * 总渲染时长) 开始,即上半部分渲染完成才开始,结束为设置的总渲染时长
// 上部分的渲染位置从 0.5 * 进度值 * 环的周长 到 总渲染长度 进度值 * 环的周长
const time = props.circleConfig.duration.split('s')[0]
if (!time) return circleAminationOption
const outAnimationTime = (0.5 / Number(props.count)) * time
circleAminationOption.outArcLength = 0.5 * circleLength.value
circleAminationOption.outDurtion = outAnimationTime + 's'
circleAminationOption.innerArcLength = Number(props.count) * circleLength.value
circleAminationOption.innerInitArcLength = 0.5 * circleLength.value // 为动画做准备 此时从中间开始
circleAminationOption.innerDurtion = time - outAnimationTime + 's' // 为动画做准备 此时从中间开始
const halfColor = gradientColor(circleProgressColor[0], circleProgressColor[1], 2)[0]
circleAminationOption.outStarColor = circleProgressColor[0]
circleAminationOption.outEndColor = halfColor
circleAminationOption.innerStarColor = circleProgressColor[1]
circleAminationOption.innerEndColor = halfColor
}
return circleAminationOption
})
// 获取每步长结束时的rgba值
const gradientColor = (startcolor, endColor, step) => {
if (step < 2) return false
if (startcolor.includes('#')) startcolor = stringToRgb(startcolor)
if (endColor.includes('#')) endColor = stringToRgb(endColor)
let startColorList = startcolor.slice(5, -1).split(',')
let endColorList = endColor.slice(5, -1).split(',')
let startR = Number(startColorList[0])
let startG = Number(startColorList[1])
let startB = Number(startColorList[2])
let endR = Number(endColorList[0])
let endG = Number(endColorList[1])
let endB = Number(endColorList[2])
let sR = (endR - startR) / step // 总差值
let sG = (endG - startG) / step
let sB = (endB - startB) / step
let colorArr = []
for (let i = 1; i < step; i++) {
let color =
'rgb(' + parseInt(sR * i + startR) + ',' + parseInt(sG * i + startG) + ',' + parseInt(sB * i + startB) + ')'
colorArr.push(color)
}
return colorArr
}
// 16进制色值转换为rgba
const stringToRgb = (str, mode = 'string') => {
const template = str.toLowerCase()
let result = ''
if (template.indexOf('rgb(') === 0) {
result = template
} else if (template.indexOf('rgba(') === 0) {
const colors = template
.replace(/rgba\(/g, '')
.replace(/\)/g, '')
.split(',')
const r = colors[0]
const g = colors[1]
const b = colors[2]
result = `rgb(${r},${g},${b})`
} else if (template.indexOf('#') === 0) {
let colors = template.replace(/#/g, '')
let resultArr = []
if (colors.length === 3) {
colors = colors.replace(/[0-9a-f]/g, (str) => {
return str + str
})
}
for (let i = 0; i < colors.length; i += 2) {
resultArr.push(parseInt(colors[i] + colors[i + 1], 16))
}
result = `rgb(${resultArr.join(',')})`
}
if (mode === 'string') {
return result
} else if (mode === 'array') {
return result.replace(/rgb\(/g, '').replace(/\)/g, '').split(',')
}
}
const useSvgRender = ref(0)
watch(
() => props.count,
() => {
useSvgRender.value = Math.random()
}
)
</script>
<style lang="scss" scoped>
.circle-process {
width: 200px;
height: 200px;
position: relative;
.svg-ele {
width: 100%;
height: 100%;
}
.circle-count {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: default;
font-size: 20px;
}
}
.circle-bg {
fill: none;
}
.circle-color {
fill: none;
// stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
transform-box: fill-box;
position: relative;
::after {
content: '|';
width: 1px;
height: 20px;
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: #fff;
}
}
</style>