Skip to content

刻度进度仪表盘

概要

提示:uniapp自定义进度刻度仪表盘

在最近的 app 应用开发过程中,我遇到了许多图表展示的问题。为什么 ECharts 不是开发应用的首选?我们不得不使用不支持 TypeScriptuCharts 。uCharts也是依托答辩。尽管uCharts是开源的,但它的可视化配置参数需要收费。尽管如此,我们仍应怀着感恩的心去使用它(我开玩笑的🙃)。

效果展示

纯手搓,肯定有杂七杂八的bug

起始角度的0为3点钟位置,结束角度为2即完整一圈

致文档维护者:下面有效果展示代码,因为打包问题勿开放勿删!

技术细节

1. 声明组件

vue
<template>
  <view class="gauge">
    <canvas :id="canvasId" ref="canvas" :canvas-id="canvasId" />
    <view class="maskGure">
      <view class="percentage" :style="{color: color}">
        {{ threshold * 100 }}<text class="unit">%</text>
      </view>
      <view class="title">{{ title }}</view>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { customAlphabet } from 'nanoid'

const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 4)

const props = defineProps({
  width: Number,
  height: Number,
  tickCount: {
    type: Number,
    default: 20 // 格子数
  },
  threshold: {
    type: Number,
    default: 0 // 完成百分比
  },
  duration: {
    type: Number,
    default: 300 // 动画持续时间,单位为毫秒
  },
  startAngle: {
    type: Number,
    default: 0.85 // 起始角度,0为3点钟位置
  },
  endAngle: {
    type: Number,
    default: 2.15 // 结束角度,2为2 * Math.PI,即完整一圈
  },
  color: {
    type: String,
    default: '#1890FF' // 颜色
  },
  title: {
    type: String,
    default: '' // 标题
  }
})

const canvas = ref(null)
const canvasId = ref(nanoid())
let animationFrameId = null

const drawGauge = (highlightedTicks, canvasWidth, canvasHeight) => {
  const ctx = uni.createCanvasContext(canvasId.value)
  const { tickCount, startAngle, endAngle } = props
  const radius = canvasWidth / 2
  const tickLength = 10
  const angleRange = (endAngle - startAngle) * Math.PI
  const angleStep = angleRange / tickCount
  const thresholdIndex = Math.floor(tickCount * props.threshold)

  ctx.clearRect(0, 0, canvasWidth, canvasHeight)
  ctx.setLineWidth(2)

  for (let i = 0; i <= tickCount; i++) {
    const angle = startAngle * Math.PI + i * angleStep
    const startX = radius + Math.cos(angle) * (radius - tickLength)
    const startY = radius + Math.sin(angle) * (radius - tickLength)
    const endX = radius + Math.cos(angle) * radius
    const endY = radius + Math.sin(angle) * radius

    // 设置不同的刻度颜色
    if (thresholdIndex > 0 && i <= highlightedTicks && i <= thresholdIndex) {
      ctx.setStrokeStyle(props.color) // 自定义颜色
    } else {
      ctx.setStrokeStyle('#CFCFCF') // 默认颜色
    }

    ctx.beginPath()
    ctx.moveTo(startX, startY)
    ctx.lineTo(endX, endY)
    ctx.stroke()
  }

  ctx.draw()
}

const animateGauge = (canvasWidth, canvasHeight) => {
  const { duration, tickCount } = props
  const thresholdIndex = Math.floor(tickCount * props.threshold)
  const startTime = Date.now()

  const animate = () => {
    const currentTime = Date.now()
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)
    const highlightedTicks = Math.floor(thresholdIndex * progress)

    drawGauge(highlightedTicks, canvasWidth, canvasHeight)

    if (progress < 1) {
      // #ifdef APP-PLUS
      animationFrameId = animate()
      // #endif

      // #ifndef APP-PLUS
      animationFrameId = requestAnimationFrame(animate)
      // #endif
    }
  }
  // #ifdef APP-PLUS
  animationFrameId = animate()
  // #endif

  // #ifndef APP-PLUS
  animationFrameId = requestAnimationFrame(animate)
  // #endif
}

onMounted(() => {
  uni.createSelectorQuery()
    .select(`#${canvasId.value}`)
    .boundingClientRect(data => {
      const canvasWidth = props.width || data.width
      const canvasHeight = props.height || data.height

      canvas.value.width = canvasWidth
      canvas.value.height = canvasHeight

      animateGauge(canvasWidth, canvasHeight)
    })
    .exec()
})

onBeforeUnmount(() => {
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId)
  }
})
</script>
scss
<style scoped lang="scss">
.gauge {
  position: relative;
  width: 100%;
  height: 100%;

  canvas {
    width: 100%;
    height: 100%;
  }

  .maskGure {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: center;

    .percentage {
      padding-top: 20rpx;
      font-size: 40rpx;
      line-height: 40rpx;

      .unit {
        margin-left: 8rpx;
        font-size: 24rpx;
        line-height: 24rpx;
      }
    }

    .title {
      position: absolute;
      bottom: 0;
      font-size: 28rpx;
      line-height: 28rpx;
      color: #666;
    }
  }
}
</style>

2. 使用该组件

vue
<Gauge title="完成" :threshold="0.64" color="#FF6021" />

小结

提示:刻度进度仪表盘

小小棱镜,无限可能