You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

344 lines
8.4 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view :class="`wd-circle ${customClass}`">
<canvas :width="canvasSize" :height="canvasSize" :style="style" :id="canvasId" :canvas-id="canvasId"></canvas>
<view v-if="!text" class="wd-circle__text">
<!-- 自定义提示内容 -->
<slot></slot>
</view>
<!-- #ifdef MP-WEIXIN -->
<cover-view v-else class="wd-circle__text">
{{ text }}
</cover-view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<!-- eslint-disable-next-line vue/valid-v-else -->
<text v-else class="wd-circle__text">
{{ text }}
</text>
<!-- #endif -->
</view>
</template>
<script lang="ts">
export default {
name: 'wd-circle',
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared'
}
}
</script>
<script lang="ts" setup>
// Circle 环形进度条
import { computed, getCurrentInstance, onBeforeMount, onMounted, onUnmounted, ref, watch } from 'vue'
import { addUnit, isObj, objToStyle, uuid } from '../common/util'
// 大于等于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
// 进度条端点的形状,可选值为 "butt" | "round" | "square"
type StrokeLinecapType = 'butt' | 'round' | 'square'
interface Props {
// 当前进度
modelValue: number
// 自定义class
customClass?: string
// 自定义style
customStyle?: string
// 目标进度
rate?: number | string
// 圆环直径,默认单位为 px
size?: number
// 进度条颜色,传入对象格式可以定义渐变色
color?: string | Record<string, string>
// 轨道颜色
layerColor?: string
// 填充颜色
fill?: string
// 动画速度(单位为 rate/s
speed?: number
// 文字
text?: string
// 进度条宽度 单位px
strokeWidth?: number
// 进度条端点的形状,可选值为 "butt" | "round" | "square"
strokeLinecap?: StrokeLinecapType
// 是否顺时针增加
clockwise?: boolean
}
const props = withDefaults(defineProps<Props>(), {
// 当前进度
modelValue: 0,
// 自定义class
customClass: '',
// 自定义style
customStyle: '',
// 目标进度
rate: 100,
// 圆环直径,默认单位为 px
size: 100,
// 进度条颜色,传入对象格式可以定义渐变色
color: '#4d80f0',
// 轨道颜色
layerColor: '#EBEEF5',
// 填充颜色
fill: '#ffffff',
// 动画速度(单位为 rate/s
speed: 50,
// 进度条宽度 默认单位px
strokeWidth: 10,
// 进度条端点的形状
strokeLinecap: 'round',
// 是否顺时针增加
clockwise: true
})
const progressColor = ref<string | CanvasGradient>('') // 进度条颜色
const pixel = ref<number>(1) // 设备像素比
const currentValue = ref<number>(0) // 当前值
const interval = ref<any>(null) // 定时器
const canvasId = ref<string>(uuid()) // canvasId
let ctx: UniApp.CanvasContext | null = null
// canvas渲染大小
const canvasSize = computed(() => {
return props.size * pixel.value
})
// Circle 样式
const style = computed(() => {
const style = {
width: addUnit(props.size),
height: addUnit(props.size)
}
return `${objToStyle(style)}; ${props.customStyle}`
})
// 监听目标数值变化
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 }
)
// 监听轨道颜色变化
watch(
() => props.layerColor,
() => {
drawCircle(currentValue.value)
},
{ immediate: false }
)
// 监听轨道宽度
watch(
() => props.strokeWidth,
() => {
drawCircle(currentValue.value)
},
{ immediate: false }
)
// 监听轨道展示方向
watch(
() => props.clockwise,
() => {
drawCircle(currentValue.value)
},
{ immediate: false }
)
// #ifdef MP-ALIPAY
onBeforeMount(() => {
pixel.value = uni.getSystemInfoSync().pixelRatio
})
// #endif
onMounted(() => {
currentValue.value = props.modelValue
drawCircle(currentValue.value)
})
onUnmounted(() => {
clearTimeInterval()
})
const { proxy } = getCurrentInstance() as any
/**
* 获取canvas上下文
*/
function getContext() {
if (!ctx) {
ctx = uni.createCanvasContext(canvasId.value, proxy)
}
return Promise.resolve(ctx)
}
/**
* 设置canvas
*/
function presetCanvas(context: any, strokeStyle: string | CanvasGradient, beginAngle: number, endAngle: number, fill?: string) {
const canvasSize = props.size * pixel.value
let strokeWidth = props.strokeWidth * pixel.value
const position = canvasSize / 2
if (!fill) {
strokeWidth = strokeWidth / 2
}
const radius = position - strokeWidth / 2
context.strokeStyle = strokeStyle
context.setLineWidth(strokeWidth)
context.setLineCap(props.strokeLinecap)
context.beginPath()
context.arc(position, position, radius, beginAngle, endAngle, !props.clockwise)
context.stroke()
if (fill) {
context.setLineWidth(strokeWidth)
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 canvasSize = props.size * pixel.value
// 结束角度
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, 0, 0, 0)
Object.keys(props.color)
.sort((a, b) => parseFloat(a) - parseFloat(b))
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, props.color[key]))
progressColor.value = LinearColor
} else {
progressColor.value = props.color
}
presetCanvas(context, progressColor.value, BEGIN_ANGLE, endAngle)
}
/**
* 渲染圆点
* 进度值为0时渲染一个圆点
*/
function renderDot(context: UniApp.CanvasContext) {
const canvasSize = props.size * pixel.value
const strokeWidth = props.strokeWidth * pixel.value // 管道宽度=小圆点直径
const position = canvasSize / 2 // 坐标
// 设置进度条颜色
if (isObj(props.color)) {
const LinearColor = context.createLinearGradient(canvasSize, 0, 0, 0)
Object.keys(props.color)
.sort((a, b) => parseFloat(a) - parseFloat(b))
.map((key) => LinearColor.addColorStop(parseFloat(key) / 100, props.color[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) {
const canvasSize = props.size * pixel.value
getContext().then((context) => {
context.clearRect(0, 0, canvasSize, canvasSize)
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>