276 lines
6.1 KiB
Vue
276 lines
6.1 KiB
Vue
<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,
|
||
default: '#E8101E'
|
||
},
|
||
// 背景圆环颜色
|
||
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> |