mrr.sj.front/components/circle-progress/circle-progress.vue

276 lines
6.1 KiB
Vue
Raw Normal View History

2026-03-24 11:45:13 +08:00
<template>
<view class="circle-progress" :style="{ width: size + 'rpx', height: size + 'rpx' }">
<!-- 背景圆环 -->
<canvas
:type="canvasType"
class="progress-canvas"
:style="{ width: size + 'rpx', height: size + 'rpx' }"
:id="canvasId"
:canvas-id="canvasId"
@ready="onCanvasReady"
></canvas>
<!-- 进度文字 -->
<view class="progress-text" v-if="showText">
<slot>
<text class="text-value">{{ progress }}%</text>
</slot>
</view>
</view>
</template>
<script>
export default {
name: 'CircleProgress',
props: {
// 进度值0-100
progress: {
type: Number,
default: 0,
validator: value => value >= 0 && value <= 100
},
// 圆环大小
size: {
type: Number,
default: 200
},
// 圆环宽度
strokeWidth: {
type: Number,
default: 10
},
// 圆环颜色
color: {
type: String,
2026-06-02 11:39:23 +08:00
default: '#FF4767'
2026-03-24 11:45:13 +08:00
},
// 背景圆环颜色
backgroundColor: {
type: String,
default: '#F5F5F5'
},
// 是否显示进度文字
showText: {
type: Boolean,
default: true
},
// 是否显示动画
animation: {
type: Boolean,
default: true
},
// 动画时长(毫秒)
duration: {
type: Number,
default: 1000
}
},
data() {
return {
canvasId: 'circleProgress_' + Math.random().toString(36).substr(2, 9),
ctx: null,
canvas: null,
animationFrame: null,
currentProgress: 0,
canvasType: '2d'
}
},
watch: {
progress: {
handler(newVal) {
if (this.animation) {
this.animateProgress(newVal)
} else {
this.currentProgress = newVal
this.drawProgress()
}
},
immediate: true
}
},
mounted() {
// #ifdef H5
this.canvasType = ''
// #endif
this.initCanvas()
},
beforeDestroy() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame)
}
},
methods: {
// 初始化画布
async initCanvas() {
const query = uni.createSelectorQuery().in(this)
query.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
if (!res[0]) return
// #ifdef H5
this.canvas = res[0].node || res[0].canvas
if (!this.canvas) {
this.canvas = document.getElementById(this.canvasId)
}
// #endif
// #ifndef H5
this.canvas = res[0].node
// #endif
if (!this.canvas) return
this.ctx = this.canvas.getContext('2d')
if (!this.ctx) return
// 设置画布大小
const dpr = uni.getSystemInfoSync().pixelRatio
this.canvas.width = this.size * dpr
this.canvas.height = this.size * dpr
this.ctx.scale(dpr, dpr)
// 设置初始进度
this.currentProgress = this.progress
// 绘制初始进度
this.drawProgress()
})
},
// 绘制进度
drawProgress() {
if (!this.ctx) return
const center = this.size / 2
const radius = (this.size - this.strokeWidth) / 2
// 清空画布
this.ctx.clearRect(0, 0, this.size, this.size)
// 绘制背景圆环
this.ctx.beginPath()
this.ctx.arc(center, center, radius, 0, Math.PI * 2)
this.ctx.strokeStyle = this.backgroundColor
this.ctx.lineWidth = this.strokeWidth
this.ctx.lineCap = 'round'
this.ctx.stroke()
// 绘制进度圆环
if (this.currentProgress > 0) {
const endAngle = (this.currentProgress / 100) * Math.PI * 2
// 创建渐变色
const gradient = this.ctx.createLinearGradient(0, 0, this.size, 0)
gradient.addColorStop(0, this.color)
gradient.addColorStop(1, this.color)
this.ctx.beginPath()
this.ctx.arc(center, center, radius, -Math.PI / 2, endAngle - Math.PI / 2)
this.ctx.strokeStyle = gradient
this.ctx.lineWidth = this.strokeWidth
this.ctx.lineCap = 'round'
this.ctx.stroke()
// 添加发光效果
this.ctx.shadowColor = this.color
this.ctx.shadowBlur = 10
this.ctx.stroke()
this.ctx.shadowBlur = 0
}
},
// 动画效果
animateProgress(targetProgress) {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame)
}
const startProgress = this.currentProgress
const startTime = Date.now()
// 使用缓动函数
const easeOutQuart = (x) => {
return 1 - Math.pow(1 - x, 4)
}
const animate = () => {
const currentTime = Date.now()
const elapsed = currentTime - startTime
if (elapsed >= this.duration) {
this.currentProgress = targetProgress
this.drawProgress()
return
}
const progress = easeOutQuart(elapsed / this.duration)
this.currentProgress = startProgress + (targetProgress - startProgress) * progress
this.drawProgress()
this.animationFrame = requestAnimationFrame(animate)
}
animate()
}
}
}
</script>
<style>
.circle-progress {
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
}
.progress-canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.progress-text {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.text-value {
font-size: 32rpx;
color: #333333;
font-weight: 500;
}
/* H5适配 */
/* #ifdef H5 */
.progress-canvas {
transform: scale(1);
}
/* #endif */
/* APP适配 */
/* #ifdef APP-PLUS */
.progress-canvas {
transform: scale(1);
}
/* #endif */
/* 小程序适配 */
/* #ifdef MP-WEIXIN */
.progress-canvas {
transform: scale(1);
}
/* #endif */
</style>