394 lines
9.7 KiB
Plaintext
394 lines
9.7 KiB
Plaintext
<script lang="ts">
|
|
import { getUid, rpx2px } from "../../core/util/xCoreUtil.uts"
|
|
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
|
import { checkIsCssUnit,getUnit } from "../../core/util/xCoreUtil.uts"
|
|
import { xConfig } from "../../config/xConfig.uts"
|
|
|
|
type OPTIONS = {
|
|
start : number,
|
|
end : number,
|
|
duration : number,
|
|
run : (current : number) => void,
|
|
complete : () => void
|
|
}
|
|
type ANIMATECALLBACK = {
|
|
stop : () => void
|
|
}
|
|
|
|
/**
|
|
* @name 圆形进度环 xCircleProgress
|
|
* @page /pages/index/circle-progress
|
|
* @category 展示组件
|
|
* @description 样式灵活多变。
|
|
* @constant 平台兼容
|
|
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
|
*/
|
|
export default {
|
|
data() {
|
|
return {
|
|
id: 'xCircleProgeress-' + getUid(),
|
|
nowValue: 0,
|
|
tid: 0,
|
|
oladValue: 0,
|
|
nowback: {
|
|
stop: () => { },
|
|
} as ANIMATECALLBACK
|
|
}
|
|
},
|
|
props: {
|
|
/**
|
|
* 可以是px,rpx,纯数字符串。默认以rpx为单位。
|
|
*/
|
|
size: {
|
|
type: String,
|
|
default: "60"
|
|
},
|
|
/**
|
|
* 可以是px,rpx,纯数字符串。默认以rpx为单位。
|
|
*/
|
|
lineWidth: {
|
|
type: String,
|
|
default: "3"
|
|
},
|
|
/**
|
|
* 圆环背景颜色,暗黑时取inputDarkColor
|
|
*/
|
|
color: {
|
|
type: String,
|
|
default: "info"
|
|
},
|
|
|
|
/**
|
|
* 当前激活的进度颜色,空值读取全局值。
|
|
*/
|
|
activeColor: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
/**
|
|
* 当前的值,以百分比为值0-100
|
|
* 等同v-model=""
|
|
* 您直接:model-value="xx"也是一样可以改变值。
|
|
*/
|
|
modelValue: {
|
|
type: Number,
|
|
default: 30
|
|
},
|
|
/**
|
|
* 中间文本字号
|
|
*/
|
|
labelFontSize: {
|
|
type: String,
|
|
default: "16"
|
|
},
|
|
/**
|
|
* 数字的单位。
|
|
*/
|
|
labelUnit: {
|
|
type: String,
|
|
default: "%"
|
|
},
|
|
/**
|
|
* 是否显示中间文本。
|
|
*/
|
|
showLabel: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
/**
|
|
* 中间文本的颜色。
|
|
*/
|
|
labelColor: {
|
|
type: String,
|
|
default: "#333333"
|
|
},
|
|
/**
|
|
* 中间文本的暗黑颜色。空值是取白色
|
|
*/
|
|
darkLabelColor: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
/**
|
|
* 进度条动画的时间,单位ms。
|
|
* 请不要设置的过慢,否则会有停顿感。
|
|
*/
|
|
duration: {
|
|
type: Number,
|
|
default: 300
|
|
},
|
|
},
|
|
computed: {
|
|
_labelFontSize() : string {
|
|
let fontSize = checkIsCssUnit(this.labelFontSize, xConfig.unit);
|
|
if(xConfig.fontScale==1) return fontSize;
|
|
let sizeNumber = parseInt(fontSize)
|
|
if(isNaN(sizeNumber)){
|
|
sizeNumber = 14
|
|
}
|
|
return (sizeNumber*xConfig.fontScale).toString() + getUnit(fontSize)
|
|
},
|
|
_labelUnit() : string {
|
|
return this.labelUnit
|
|
},
|
|
_labelColor() : string {
|
|
if(xConfig.dark=='dark'){
|
|
if(this.darkLabelColor!='') return getDefaultColor(this.darkLabelColor)
|
|
return "#ffffff"
|
|
}
|
|
return getDefaultColor(this.labelColor)
|
|
},
|
|
_showLabel() : boolean {
|
|
return this.showLabel
|
|
},
|
|
_width() : number {
|
|
let p = parseInt(this.size);
|
|
if (this.size.lastIndexOf('rpx') > -1) {
|
|
p = rpx2px(p);
|
|
}
|
|
return p
|
|
},
|
|
_lineWidth() : number {
|
|
let p = parseInt(this.lineWidth);
|
|
if (this.lineWidth.lastIndexOf('rpx') > -1) {
|
|
p = rpx2px(p);
|
|
}
|
|
return Math.floor(p)
|
|
},
|
|
_color() : string {
|
|
if(xConfig.dark=='dark') return getDefaultColor(xConfig.inputDarkColor)
|
|
return getDefaultColor(this.color)
|
|
},
|
|
_activeColor() : string {
|
|
if (this.activeColor == "") return getDefaultColor(xConfig.color)
|
|
return getDefaultColor(this.activeColor)
|
|
},
|
|
},
|
|
watch: {
|
|
modelValue(newval : number) {
|
|
|
|
if (newval != this.nowValue) {
|
|
this.oladValue = this.nowValue
|
|
this.nowValue = newval
|
|
this.play();
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
this.nowValue = this.modelValue
|
|
this.play();
|
|
},
|
|
beforeUnmount() {
|
|
clearTimeout(this.tid)
|
|
},
|
|
methods: {
|
|
play() {
|
|
this.nowback.stop()
|
|
clearTimeout(this.tid)
|
|
this.clear();
|
|
let t = this;
|
|
this.nowback = this.animate({
|
|
start: 0,
|
|
end: 100,
|
|
duration: t.duration,
|
|
complete() {
|
|
|
|
},
|
|
run(cureent : number) {
|
|
let angle = Math.PI * 2;
|
|
if (t.nowValue >= t.oladValue) {
|
|
let startAngle = (t.oladValue / 100) * angle
|
|
let endAngle = ((cureent / 100) * (t.nowValue - t.oladValue) / 100) * angle
|
|
endAngle = Math.max(endAngle, 0)
|
|
|
|
t.dreawer(startAngle, endAngle + startAngle)
|
|
} else {
|
|
let startAngle = (t.nowValue / 100) * angle
|
|
let endAngle = ((cureent / 100) * (t.oladValue - t.nowValue) / 100) * angle
|
|
let diff = (t.oladValue - t.nowValue) / 100 * angle
|
|
t.dreawer(startAngle, diff + startAngle - endAngle)
|
|
}
|
|
}
|
|
} as OPTIONS)
|
|
},
|
|
animate(options : OPTIONS) : ANIMATECALLBACK {
|
|
let tid = this.tid;
|
|
const start = options.start; // 初始值,默认为0
|
|
const end = options.end; // 目标值,默认为100
|
|
const duration = options.duration; // 动画时长,默认为1000毫秒
|
|
let current = 0; // 当前值
|
|
let startTime = 0; // 动画开始时间
|
|
let isRunning = true; // 动画运行状态
|
|
function run() {
|
|
if (startTime <= 0) {
|
|
startTime = Date.now(); // 记录动画开始时间
|
|
}
|
|
const progress = Math.min((Date.now() - startTime) / duration, 1); // 计算当前进度
|
|
current = start + (end - start) * progress; // 根据进度计算当前值
|
|
if (isRunning) {
|
|
options.run(current); // 执行传入的运行中状态回调函数
|
|
}
|
|
if (progress < 1 && isRunning) {
|
|
tid = setTimeout(() => {
|
|
run()
|
|
}, 16); // 递归调用自身,实现动画效果
|
|
} else {
|
|
options.complete(); // 动画结束时执行回调函数
|
|
}
|
|
}
|
|
run(); // 开始执行动画
|
|
return {
|
|
stop: () => {
|
|
// 设置动画运行状态为false
|
|
isRunning = false;
|
|
}
|
|
} as ANIMATECALLBACK;
|
|
},
|
|
async dreawer(start : number, end : number):Promise<any|null> {
|
|
this.clear()
|
|
let canvas = uni.getElementById(this.id as string) as UniElement
|
|
let ctx = null as DrawableContext | null
|
|
let ratio = 1;
|
|
// #ifdef APP
|
|
ctx = canvas.getDrawableContext()!
|
|
// #endif
|
|
|
|
// #ifdef WEB
|
|
let crect = canvas.getBoundingClientRect();
|
|
let dom = canvas as HTMLElement;
|
|
canvas = dom.querySelector('canvas')
|
|
ratio = window.devicePixelRatio;
|
|
let w = crect.width
|
|
let h = crect.height
|
|
|
|
canvas.width = w * ratio
|
|
canvas.height = h * ratio
|
|
|
|
ctx = canvas.getContext('2d')!
|
|
// #endif
|
|
|
|
// #ifdef MP-WEIXIN
|
|
let ctxnode = await uni.createCanvasContextAsync({
|
|
id: this.id,
|
|
component: this,
|
|
})
|
|
const canvasContext = ctxnode.getContext('2d')!;
|
|
canvas = canvasContext.canvas;
|
|
// 处理高清屏逻辑
|
|
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
|
|
canvas.width = canvas.offsetWidth * dpr;
|
|
canvas.height = canvas.offsetHeight * dpr;
|
|
canvasContext.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
|
|
ctx = canvasContext
|
|
// #endif
|
|
|
|
|
|
if (ctx == null) return Promise.resolve(null);
|
|
let cx = (this._width) / 2
|
|
let cy = cx
|
|
let cr = (this._width - this._lineWidth) / 2
|
|
ctx!.beginPath()
|
|
ctx!.strokeStyle = this._color
|
|
ctx!.lineWidth = this._lineWidth
|
|
ctx!.lineCap = "round"
|
|
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, 0, Math.PI * 2)
|
|
ctx!.closePath()
|
|
ctx!.stroke()
|
|
// 绘制起始的角度。
|
|
ctx!.beginPath()
|
|
ctx!.strokeStyle = this._activeColor
|
|
ctx!.lineWidth = this._lineWidth
|
|
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, 0, start)
|
|
ctx!.stroke()
|
|
// ctx.update()
|
|
|
|
|
|
ctx!.beginPath()
|
|
ctx!.strokeStyle = this._activeColor
|
|
ctx!.lineWidth = this._lineWidth
|
|
ctx!.arc(cx * ratio, cy * ratio, cr * ratio, start, end)
|
|
ctx!.stroke()
|
|
// #ifdef APP
|
|
ctx!.update()
|
|
// #endif
|
|
|
|
return Promise.resolve(null)
|
|
},
|
|
clear(){
|
|
let canvas = uni.getElementById(this.id as string) as UniElement
|
|
let ctx = null as DrawableContext | null
|
|
let ratio = 1;
|
|
// #ifdef APP
|
|
ctx = canvas.getDrawableContext()!
|
|
// #endif
|
|
// #ifdef WEB
|
|
let dom = canvas as HTMLElement;
|
|
canvas = dom.querySelector('canvas')
|
|
ctx = canvas.getContext('2d')!
|
|
ratio = window.devicePixelRatio;
|
|
// #endif
|
|
|
|
|
|
|
|
|
|
if (canvas == null || ctx == null) return
|
|
|
|
// #ifndef APP
|
|
ctx!.reset()
|
|
// #endif
|
|
|
|
ctx!.fillStyle = 'rgba(0,0,0,0)'
|
|
ctx!.fillRect(0, 0, this._width * ratio, this._width * ratio)
|
|
ctx!.fill()
|
|
// #ifdef APP
|
|
ctx!.update()
|
|
// #endif
|
|
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
|
|
<view :style="{width:_width+'px',height:_width+'px',position: 'relative'}">
|
|
<!-- #ifdef APP -->
|
|
<view class="xCircleProgress" :id="id" :style="{width:_width+'px',height:_width+'px'}"></view>
|
|
<!-- #endif -->
|
|
<!-- #ifdef WEB || MP -->
|
|
<canvas class="xCircleProgress" :id="id" :style="{width:_width+'px',height:_width+'px'}"></canvas>
|
|
<!-- #endif -->
|
|
<view v-if="_showLabel" class="xCircleProgressLabel">
|
|
<!--
|
|
@slot 默认文本插槽
|
|
@prop {number} current - 当前的进度值。
|
|
-->
|
|
<slot current="nowValue">
|
|
<text :style="{
|
|
fontSize:_labelFontSize,
|
|
color:_labelColor
|
|
}">{{nowValue}}{{_labelUnit}}</text>
|
|
</slot>
|
|
</view>
|
|
|
|
</view>
|
|
</template>
|
|
<style scoped>
|
|
.xCircleProgress {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.xCircleProgressLabel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: absolute;
|
|
left: 0px;
|
|
top: 0px;
|
|
width: 100%;
|
|
height: 100%;
|
|
|
|
}
|
|
</style> |