Files
taimed-international-app/uni_modules/wot-design-uni/components/wd-circle/wd-circle.vue
2025-11-04 12:37:04 +08:00

297 lines
7.4 KiB
Vue

<template>
<view :class="`wd-circle ${customClass}`" :style="customStyle">
<!-- #ifdef MP-WEIXIN -->
<canvas :style="canvasStyle" :id="canvasId" :canvas-id="canvasId" type="2d"></canvas>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<canvas :width="canvasSize" :height="canvasSize" :style="canvasStyle" :id="canvasId" :canvas-id="canvasId"></canvas>
<!-- #endif -->
<view v-if="!text" class="wd-circle__text">
<!-- 自定义提示内容 -->
<slot></slot>
</view>
<text v-else class="wd-circle__text">
{{ text }}
</text>
</view>
</template>
<script lang="ts">
export default {
name: 'wd-circle',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
import { computed, getCurrentInstance, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
import { addUnit, isObj, objToStyle, uuid } from '../common/util'
import { circleProps } from './types'
// #ifdef MP-WEIXIN
import { canvas2dAdapter } from '../common/canvasHelper'
// #endif
// 大于等于0且小于等于100
function format(rate: number) {
return Math.min(Math.max(rate, 0), 100)
}
// 结束角度
const PERIMETER = 2 * Math.PI
// 开始角度
const BEGIN_ANGLE = -Math.PI / 2
const STEP = 1
const props = defineProps(circleProps)
const { proxy } = getCurrentInstance() as any
const progressColor = ref<string | CanvasGradient>('') // 进度条颜色
const currentValue = ref<number>(0) // 当前值
const interval = ref<any>(null) // 定时器
const pixelRatio = ref<number>(1) // 像素比
const canvasId = ref<string>(`wd-circle${uuid()}`) // canvasId
let ctx: UniApp.CanvasContext | null = null
// canvas渲染大小
const canvasSize = computed(() => {
let size = props.size
// #ifdef MP-ALIPAY
size = size * pixelRatio.value
// #endif
return size
})
// 进度条宽度
const sWidth = computed(() => {
let sWidth = props.strokeWidth
// #ifdef MP-ALIPAY
sWidth = sWidth * pixelRatio.value
// #endif
return sWidth
})
// Circle 样式
const canvasStyle = computed(() => {
const style = {
width: addUnit(props.size),
height: addUnit(props.size)
}
return `${objToStyle(style)}`
})
// 监听目标数值变化
watch(
() => props.modelValue,
() => {
reRender()
},
{ immediate: true }
)
// 监听Circle大小变化
watch(
() => props.size,
() => {
let timer = setTimeout(() => {
drawCircle(currentValue.value)
clearTimeout(timer)
}, 50)
},
{ immediate: false }
)
// 监听进度条颜色变化
watch(
() => props.color,
() => {
drawCircle(currentValue.value)
},
{ immediate: false, deep: true }
)
onBeforeMount(() => {
pixelRatio.value = uni.getSystemInfoSync().pixelRatio
})
onMounted(() => {
currentValue.value = props.modelValue
drawCircle(currentValue.value)
})
onUnmounted(() => {
clearTimeInterval()
})
/**
* 获取canvas上下文
*/
function getContext() {
return new Promise<UniApp.CanvasContext>((resolve) => {
if (ctx) {
return resolve(ctx)
}
// #ifndef MP-WEIXIN
ctx = uni.createCanvasContext(canvasId.value, proxy)
resolve(ctx)
// #endif
// #ifdef MP-WEIXIN
uni
.createSelectorQuery()
.in(proxy)
.select(`#${canvasId.value}`)
.node((res) => {
if (res && res.node) {
const canvas = res.node
ctx = canvas2dAdapter(canvas.getContext('2d') as CanvasRenderingContext2D)
canvas.width = props.size * pixelRatio.value
canvas.height = props.size * pixelRatio.value
ctx.scale(pixelRatio.value, pixelRatio.value)
resolve(ctx)
}
})
.exec()
// #endif
})
}
/**
* 设置canvas
*/
function presetCanvas(context: any, strokeStyle: string | CanvasGradient, beginAngle: number, endAngle: number, fill?: string) {
let width = sWidth.value
const position = canvasSize.value / 2
if (!fill) {
width = width / 2
}
const radius = position - width / 2
context.strokeStyle = strokeStyle
context.setStrokeStyle(strokeStyle)
context.setLineWidth(width)
context.setLineCap(props.strokeLinecap)
context.beginPath()
context.arc(position, position, radius, beginAngle, endAngle, !props.clockwise)
context.stroke()
if (fill) {
context.setLineWidth(width)
context.setFillStyle(fill)
context.fill()
}
}
/**
* 渲染管道
*/
function renderLayerCircle(context: UniApp.CanvasContext) {
presetCanvas(context, props.layerColor, 0, PERIMETER, props.fill)
}
/**
* 渲染进度条
*/
function renderHoverCircle(context: UniApp.CanvasContext, formatValue: number) {
// 结束角度
const progress = PERIMETER * (formatValue / 100)
const endAngle = props.clockwise ? BEGIN_ANGLE + progress : 3 * Math.PI - (BEGIN_ANGLE + progress)
// 设置进度条颜色
if (isObj(props.color)) {
const LinearColor = context.createLinearGradient(canvasSize.value, 0, 0, 0)
Object.keys(props.color)
.sort((a, b) => parseFloat(a) - parseFloat(b))
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, (props.color as Record<string, any>)[key]))
progressColor.value = LinearColor
} else {
progressColor.value = props.color
}
presetCanvas(context, progressColor.value, BEGIN_ANGLE, endAngle)
}
/**
* 渲染圆点
* 进度值为0时渲染一个圆点
*/
function renderDot(context: UniApp.CanvasContext) {
const strokeWidth = sWidth.value // 管道宽度=小圆点直径
const position = canvasSize.value / 2 // 坐标
// 设置进度条颜色
if (isObj(props.color)) {
const LinearColor = context.createLinearGradient(canvasSize.value, 0, 0, 0)
Object.keys(props.color)
.sort((a, b) => parseFloat(a) - parseFloat(b))
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, (props.color as Record<string, any>)[key]))
progressColor.value = LinearColor
} else {
progressColor.value = props.color
}
context.beginPath()
context.arc(position, strokeWidth / 4, strokeWidth / 4, 0, PERIMETER)
context.setFillStyle(progressColor.value)
context.fill()
}
/**
* 画圆
*/
function drawCircle(currentValue: number) {
getContext().then((context) => {
context.clearRect(0, 0, canvasSize.value, canvasSize.value)
renderLayerCircle(context)
const formatValue = format(currentValue)
if (formatValue !== 0) {
renderHoverCircle(context, formatValue)
} else {
renderDot(context)
}
context.draw()
})
}
/**
* Circle组件渲染
* 当前进度值变化时重新渲染Circle组件
*/
function reRender() {
// 动画通过定时器渲染
if (props.speed <= 0 || props.speed > 1000) {
drawCircle(props.modelValue)
return
}
clearTimeInterval()
currentValue.value = currentValue.value || 0
const run = () => {
interval.value = setTimeout(() => {
if (currentValue.value !== props.modelValue) {
if (Math.abs(currentValue.value - props.modelValue) < STEP) {
currentValue.value = props.modelValue
} else if (currentValue.value < props.modelValue) {
currentValue.value += STEP
} else {
currentValue.value -= STEP
}
drawCircle(currentValue.value)
run()
} else {
clearTimeInterval()
}
}, 1000 / props.speed)
}
run()
}
/**
* 清除定时器
*/
function clearTimeInterval() {
interval.value && clearTimeout(interval.value)
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>