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>
|