guozhi-wechat/components/tui-circular-progress/tui-circular-progress.vue

264 lines
6.7 KiB
Vue
Raw Normal View History

2025-07-22 18:31:50 +08:00
<template>
<view class="tui-circular-container" :style="{ width: diam + 'px', height: (height || diam) + 'px' }">
<canvas
class="tui-circular-default"
:canvas-id="defaultCanvasId"
:id="defaultCanvasId"
:style="{ width: diam + 'px', height: (height || diam) + 'px' }"
v-if="defaultShow"
></canvas>
<canvas class="tui-circular-progress" :canvas-id="progressCanvasId" :id="progressCanvasId" :style="{ width: diam + 'px', height: (height || diam) + 'px' }"></canvas>
<slot />
</view>
</template>
<script>
export default {
name: 'tuiCircularProgress',
props: {
/*
传值需使用rpx进行转换保证各终端兼容
px = rpx / 750 * wx.getSystemInfoSync().windowWidth
圆形进度条(画布)宽度直径 [px]
*/
diam: {
type: Number,
default: 60
},
//圆形进度条(画布)高度默认取diam值[当画半弧时传值height有值时则取height]
height: {
type: Number,
default: 0
},
//进度条线条宽度[px]
lineWidth: {
type: Number,
default: 4
},
/*
线条的端点样式
butt向线条的每个末端添加平直的边缘
round 向线条的每个末端添加圆形线帽
square 向线条的每个末端添加正方形线帽
*/
lineCap: {
type: String,
default: 'round'
},
//圆环进度字体大小 [px]
fontSize: {
type: Number,
default: 12
},
//圆环进度字体颜色
fontColor: {
type: String,
default: '#1A73E8'
},
//是否显示进度文字
fontShow: {
type: Boolean,
default: true
},
/*
自定义显示文字[默认为空显示百分比fontShow=true时生效]
可以使用 slot自定义显示内容
*/
percentText: {
type: String,
default: ''
},
//是否显示默认(背景)进度条
defaultShow: {
type: Boolean,
default: true
},
//默认进度条颜色
defaultColor: {
type: String,
default: '#CCC'
},
//进度条颜色
progressColor: {
type: String,
default: '#1A73E8'
},
//进度条渐变颜色[结合progressColor使用默认为空]
gradualColor: {
type: String,
default: ''
},
//起始弧度,单位弧度
sAngle: {
type: Number,
default: -Math.PI / 2
},
//指定弧度的方向是逆时针还是顺时针。默认是false即顺时针
counterclockwise: {
type: Boolean,
default: false
},
//进度百分比 [10% 传值 10]
percentage: {
type: Number,
default: 0
},
//进度百分比缩放倍数[使用半弧为100%时则可传2]
multiple: {
type: Number,
default: 1
},
//动画执行时间[单位毫秒低于50无动画]
duration: {
type: Number,
default: 800
},
//backwards: 动画从头播forwards动画从上次结束点接着播
activeMode: {
type: String,
default: 'backwards'
}
},
watch: {
percentage(val) {
this.initDraw();
}
},
data() {
return {
// #ifdef MP-WEIXIN
progressCanvasId: 'progressCanvasId',
defaultCanvasId: 'defaultCanvasId',
// #endif
// #ifndef MP-WEIXIN
progressCanvasId: this.getCanvasId(),
defaultCanvasId: this.getCanvasId(),
// #endif
progressContext: null,
linearGradient: null,
//起始百分比
startPercentage: 0
// dpi
//pixelRatio: uni.getSystemInfoSync().pixelRatio
};
},
mounted() {
this.initDraw(true);
},
methods: {
//初始化绘制
initDraw(init) {
let start = this.activeMode === 'backwards' ? 0 : this.startPercentage;
start = start > this.percentage ? 0 : start;
if (this.defaultShow && init) {
this.drawDefaultCircular();
}
this.drawProgressCircular(start);
},
//默认(背景)圆环
drawDefaultCircular() {
let ctx = uni.createCanvasContext(this.defaultCanvasId, this);
ctx.setLineWidth(this.lineWidth);
ctx.setStrokeStyle(this.defaultColor);
//终止弧度
let eAngle = Math.PI * (this.height ? 1 : 2) + this.sAngle;
this.drawArc(ctx, eAngle);
},
//进度圆环
drawProgressCircular(startPercentage) {
let ctx = this.progressContext;
let gradient = this.linearGradient;
if (!ctx) {
ctx = uni.createCanvasContext(this.progressCanvasId, this);
//创建一个线性的渐变颜色 CanvasGradient对象
gradient = ctx.createLinearGradient(0, 0, this.diam, 0);
gradient.addColorStop('0', this.progressColor);
if (this.gradualColor) {
gradient.addColorStop('1', this.gradualColor);
}
// #ifdef APP-PLUS
const res = uni.getSystemInfoSync();
if (!this.gradualColor && res.platform.toLocaleLowerCase() == 'android') {
gradient.addColorStop('1', this.progressColor);
}
// #endif
this.progressContext = ctx;
this.linearGradient = gradient;
}
ctx.setLineWidth(this.lineWidth);
ctx.setStrokeStyle(gradient);
let time = this.percentage == 0 || this.duration < 50 ? 0 : this.duration / this.percentage;
if (this.percentage > 0) {
startPercentage = this.duration < 50 ? this.percentage - 1 : startPercentage;
startPercentage++;
}
if (this.fontShow) {
ctx.setFontSize(this.fontSize);
ctx.setFillStyle(this.fontColor);
ctx.setTextAlign('center');
ctx.setTextBaseline('middle');
let percentage = this.percentText;
if (!percentage) {
percentage = this.counterclockwise ? 100 - startPercentage * this.multiple : startPercentage * this.multiple;
percentage = `${percentage}%`;
}
let radius = this.diam / 2;
ctx.fillText(percentage, radius, radius);
}
if (this.percentage == 0 || (this.counterclockwise && startPercentage == 100)) {
ctx.draw();
}else{
let eAngle = ((2 * Math.PI) / 100) * startPercentage + this.sAngle;
this.drawArc(ctx, eAngle);
}
setTimeout(() => {
this.startPercentage = startPercentage;
if (startPercentage == this.percentage) {
this.$emit('end', {
canvasId: this.progressCanvasId,
percentage: startPercentage
});
} else {
this.drawProgressCircular(startPercentage);
}
this.$emit('change', {
percentage: startPercentage
});
}, time);
// #ifdef H5
// requestAnimationFrame(()=>{})
// #endif
},
//创建弧线
drawArc(ctx, eAngle) {
ctx.setLineCap(this.lineCap);
ctx.beginPath();
let radius = this.diam / 2; //x=y
ctx.arc(radius, radius, radius - this.lineWidth, this.sAngle, eAngle, this.counterclockwise);
ctx.stroke();
ctx.draw();
},
//生成canvasId
getCanvasId() {
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
return (c === 'x' ? (Math.random() * 16) | 0 : 'r&0x3' | '0x8').toString(16);
});
return uuid;
}
}
};
</script>
<style scoped>
.tui-circular-container,
.tui-circular-default {
position: relative;
}
.tui-circular-progress {
position: absolute;
left: 0;
top: 0;
z-index: 10;
}
</style>