[Vue组件]半环进度显示器

[Vue组件]半环进度显示器

纯svg实现,不需要其他第三方库,功能简单,理论上现代浏览器都能支持

  • 封装组件

所有参数都选填,进度都可选填

html 复制代码
<template>
  <div class="ys-semiring">
    <div class="svg-container">
      <svg viewBox="0 0 1000 1000">
        <!-- 半圆环背景 -->
        <path :d="path1" :fill="backgroundColor" />

        <!-- 进度环 -->
        <path :d="path2" :fill="progressColor" />

        <!-- 高亮指示器 -->
        <path v-if="isShowIndicator" :d="path4" :fill="highlightColor" />

        <!-- 白色遮挡条,将环分割成x部分 -->
        <line
          v-for="(divider, index) in dividers"
          :key="index"
          :x1="divider.x1"
          :y1="divider.y1"
          :x2="divider.x2"
          :y2="divider.y2"
          :stroke="intervalColor"
          :stroke-width="dividerWidth"
        />
      </svg>
    </div>
    <!-- 插槽 -->
    <div class="cu-slot">
      <slot></slot>
    </div>
  </div>
</template>

<script>
// svg绘制边界
const viewBoxWidth = 1000
// 生成半圆环的y坐标位置
const yPosition = 650
// 环的宽度(厚度)
const ringWidth = 160
// 分割线的内边距
const padding = 80

export default {
  name: 'YsSemiring',
  props: {
    // 进度百分比(0-1)
    percentage: {
      type: Number,
      default: 0.1,
      validator: value => value >= 0 && value <= 1
    },
    // 是否显示高亮指示器
    isShowIndicator: {
      type: Boolean,
      default: false
    },
    // 指示器大小
    indicatorSize: {
      type: Number,
      default: 80
    },
    // 指示器偏移量 0-160
    indicatorOffset: {
      type: Number,
      default: 0
    },
    // 分割段数
    divider: {
      type: Number,
      default: 5
    },
    // 分割线宽度
    dividerWidth: {
      type: Number,
      default: 10
    },
    // 背景颜色
    backgroundColor: {
      type: String,
      default: '#ededf5'
    },
    // 进度颜色
    progressColor: {
      type: String,
      default: '#3570f8'
    },
    // 高亮指示器颜色
    highlightColor: {
      type: String,
      default: '#f8ba49'
    },
    // 分割线颜色
    intervalColor: {
      type: String,
      default: '#ffffff'
    }
  },
  data() {
    return {
      path1: '',
      path2: '',
      path4: '',
      dividers: []
    }
  },
  created() {
    const path1 = this.generateSemiRingPath(yPosition, ringWidth)
    this.path1 = path1

    const path2 = this.generateProgressPath(yPosition, ringWidth, this.percentage)
    this.path2 = path2

    // 显示指示器
    if (this.isShowIndicator) {
      // 获取当前分段索引
      const i = this.getCurrentSegmentIndex(this.percentage, this.divider)
      // 获取当前分段中间点的坐标
      const { midX, midY } = this.getSegmentMidPoint(i, this.divider)
      // 生成指示器的三角形路径
      this.path4 = this.generateTrianglePath(midX, midY)
    }

    // 生成分割线
    if (this.divider >= 1) {
      this.dividers = this.generateDividers(yPosition, ringWidth, this.divider)
    }
  },
  methods: {
    /**
     * 生成半圆环的SVG路径
     * @param {number} yPosition - 水平线的y坐标位置
     * @param {number} ringWidth - 环的宽度(厚度)
     * @returns {string} SVG路径字符串
     */
    generateSemiRingPath(yPosition, ringWidth) {
      const centerX = viewBoxWidth / 2
      const outerRadius = (viewBoxWidth - padding * 2) / 2
      const innerRadius = outerRadius - ringWidth

      const outerStartX = centerX - outerRadius
      const outerEndX = centerX + outerRadius
      const innerStartX = centerX - innerRadius
      const innerEndX = centerX + innerRadius

      return `M ${outerStartX} ${yPosition} A ${outerRadius} ${outerRadius} 0 0 1 ${outerEndX} ${yPosition} L ${innerEndX} ${yPosition} A ${innerRadius} ${innerRadius} 0 0 0 ${innerStartX} ${yPosition} Z`
    },

    /**
     * 生成进度环的SVG路径(0-180度基于percentage)
     * @param {number} yPosition - 水平线的y坐标位置
     * @param {number} ringWidth - 环的宽度
     * @param {number} percentage - 进度比例(0-1)
     * @returns {string} SVG路径字符串
     */
    generateProgressPath(yPosition, ringWidth, percentage) {
      const centerX = viewBoxWidth / 2
      const outerRadius = (viewBoxWidth - padding * 2) / 2
      const innerRadius = outerRadius - ringWidth

      // 将percentage(0-1)转换为角度(180-0度)
      const angle = Math.PI * (1 - percentage)
      const outerEndX = centerX + outerRadius * Math.cos(angle)
      const outerEndY = yPosition - outerRadius * Math.sin(angle)
      const innerEndX = centerX + innerRadius * Math.cos(angle)
      const innerEndY = yPosition - innerRadius * Math.sin(angle)
      const outerStartX = centerX - outerRadius
      const innerStartX = centerX - innerRadius

      // large-arc-flag 设置为0,因为我们总是绘制小于180度的弧
      return `M ${outerStartX} ${yPosition} A ${outerRadius} ${outerRadius} 0 0 1 ${outerEndX} ${outerEndY} L ${innerEndX} ${innerEndY} A ${innerRadius} ${innerRadius} 0 0 0 ${innerStartX} ${yPosition} Z`
    },

    /**
     * 获取当前进度所在的分段索引
     * @param {number} percentage - 进度百分比(0-1)
     * @param {number} segments - 分段数
     * @returns {number} 当前分段索引
     */
    getCurrentSegmentIndex(percentage, segments) {
      // 计算每个分段的起始百分比
      const segmentStartPercentage = percentage => percentage / segments
      // 确定当前进度所在的分段
      let currentIndex = 0
      for (let i = 1; i < segments; i++) {
        if (percentage > segmentStartPercentage(i)) {
          currentIndex = i
        }
      }
      // 确保索引在合理范围内
      currentIndex = Math.min(currentIndex, segments - 1)
      return currentIndex
    },

    // 获取所在分段,向上兼容
    getCurrentSegmentIndex2(percentage, segments) {
      // 计算每个分段所代表的百分比
      const segmentPercentage = 1 / segments
      // 确定当前进度所在的分段
      let currentIndex = Math.floor(percentage / segmentPercentage)
      // 确保索引在合理范围内
      currentIndex = Math.max(0, Math.min(currentIndex, segments - 1))
      return currentIndex
    },

    /**
     * 获取当前分段中间点的坐标
     * @param {number} segmentIndex - 分段索引
     * @param {number} segments - 分段总数
     * @returns {Object} 中间点的坐标 { midX, midY }
     */
    getSegmentMidPoint(segmentIndex, segments) {
      const centerX = viewBoxWidth / 2
      const centerY = yPosition
      const radius = (viewBoxWidth - padding * 2) / 2 - this.indicatorOffset
      const totalAngle = Math.PI
      const segmentAngle = totalAngle / segments

      // 计算当前分段中间的角度
      const midAngle = (segments - segmentIndex - 1) * segmentAngle + segmentAngle / 2

      // 计算中间点的坐标
      const midX = centerX + radius * Math.cos(midAngle)
      const midY = centerY - radius * Math.sin(midAngle)

      return { midX, midY }
    },

    /**
     * 生成指示器的三角形路径
     * @param {number} triangleTopX - 三角形顶点的x坐标
     * @param {number} triangleTopY - 三角形顶点的y坐标
     * @returns {string} SVG路径字符串
     */
    generateTrianglePath(triangleTopX, triangleTopY) {
      const centerX = viewBoxWidth / 2
      const centerY = yPosition
      const angle = Math.PI * 1.5 - Math.atan2(triangleTopX - centerX, triangleTopY - centerY)

      const halfBase = this.indicatorSize * 0.45
      const baseAngle = Math.atan(halfBase / this.indicatorSize)
      const baseLength = Math.sqrt(halfBase * halfBase + this.indicatorSize * this.indicatorSize)

      const triangleLeftX = triangleTopX - baseLength * Math.cos(angle - baseAngle)
      const triangleLeftY = triangleTopY - baseLength * Math.sin(angle - baseAngle)
      const triangleRightX = triangleTopX - baseLength * Math.cos(angle + baseAngle)
      const triangleRightY = triangleTopY - baseLength * Math.sin(angle + baseAngle)

      return `M ${triangleTopX} ${triangleTopY} L ${triangleLeftX} ${triangleLeftY} L ${triangleRightX} ${triangleRightY} Z`
    },

    /**
     * 生成分割线的坐标
     * @param {number} yPosition - 水平线的y坐标位置
     * @param {number} ringWidth - 环的宽度
     * @param {number} segments - 分割段数
     * @returns {Array} 分割线坐标数组
     */
    generateDividers(yPosition, ringWidth, segments) {
      const centerX = viewBoxWidth / 2
      const outerRadius = (viewBoxWidth - padding * 2) / 2
      const innerRadius = outerRadius - ringWidth

      const dividers = []

      // 计算每个分割点的角度(从π到0)
      for (let i = 1; i < segments; i++) {
        const angle = (Math.PI * (segments - i)) / segments

        // 计算内圆和外圆上该角度对应的坐标
        const outerX = centerX + outerRadius * Math.cos(angle)
        const outerY = yPosition - outerRadius * Math.sin(angle)
        const innerX = centerX + innerRadius * Math.cos(angle)
        const innerY = yPosition - innerRadius * Math.sin(angle)

        dividers.push({
          x1: innerX,
          y1: innerY,
          x2: outerX,
          y2: outerY
        })
      }

      return dividers
    }
  }
}
</script>

<style scoped>
.ys-semiring {
  position: relative;
  height: 100%;
  width: 100%;
}

.svg-container {
  margin: auto;
  height: 100%;
  aspect-ratio: 1 / 1;
}

.svg-container > svg {
  width: 100%;
  height: 100%;
  background-color: #ffffff;
}

/* 调整节点位置 */
.cu-slot {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translateX(-50%);
}
</style>
引用
html 复制代码
<template>
  <div>
    <div class="container">
      <div v-for="(item, i) in list" :key="i" class="item">
        <h3>{{ item.title }}</h3>
        <div class="box">
          <YsSemiring
            :percentage="item.percentage"
            :divider="item.divider"
            :isShowIndicator="item.isShowIndicator"
            :dividerWidth="item.dividerWidth"
            :indicatorSize="item.indicatorSize"
            :indicatorOffset="item.indicatorOffset"
            :backgroundColor="item.backgroundColor"
            :progressColor="item.progressColor"
            :highlightColor="item.highlightColor"
            :intervalColor="item.intervalColor"
          >
            <template v-if="item.hasSlot">
              <div v-if="item.hasSlot === '节点A'" class="aaa">节点A</div>
              <div v-else class="bbb">{{ item.hasSlot }}</div>
            </template>
          </YsSemiring>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import YsSemiring from './components/ys-semiring.vue'

export default {
  name: 'SvgRender',
  components: { YsSemiring },
  data() {
    return {
      // 进度
      list: [
        { title: '基础使用', percentage: 0.1 },
        { title: '两段分割', percentage: 0.2, divider: 2 },
        { title: '七段分割', percentage: 0.3, divider: 7, isShowIndicator: true },
        { title: '切换颜色', percentage: 0.4, backgroundColor: '#B6B6B6', progressColor: '#67C23A' },
        { title: '开指示器', percentage: 0.5, isShowIndicator: true },
        { title: '三段分割开指示器', percentage: 0.6, divider: 3, isShowIndicator: true },
        { title: '指示器变色', percentage: 0.6, isShowIndicator: true, highlightColor: '#F56C6C' },
        { title: '分割线变色', percentage: 0.7, isShowIndicator: true, intervalColor: '#000000' },
        { title: '指示器偏移', percentage: 0.8, isShowIndicator: true, indicatorOffset: 40 },
        { title: '分割线加宽', percentage: 0.9, isShowIndicator: true, dividerWidth: 30, intervalColor: '#E6A23C' },
        { title: '指示器大偏', percentage: 0.95, isShowIndicator: true, indicatorOffset: 160 },
        { title: '指示器放大', percentage: 0.5, isShowIndicator: true, indicatorSize: 120 },
        { title: '指示器缩小', percentage: 0.44, isShowIndicator: true, indicatorSize: 60 },
        { title: '添加节点1', percentage: 0.12, hasSlot: '节点A' },
        { title: '添加节点2', percentage: 0.75, hasSlot: '节点B' }
      ]
    }
  },
  methods: {},
  mounted() {}
}
</script>

<style scoped>
.container {
  padding: 0.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, 300px);
  gap: 1rem;
}

.item {
  border: 1px solid #ccc;
}

.box {
  width: 300px;
  height: 200px;
}

.aaa {
  width: 50px;
  height: 50px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: yellowgreen;
}

.bbb {
  font-weight: bold;
  color: red;
}
</style>
相关推荐
仟濹5 小时前
【HTML】基础学习【数据分析全栈攻略:爬虫+处理+可视化+报告】
大数据·前端·爬虫·数据挖掘·数据分析·html
小小小小宇6 小时前
前端WebWorker笔记总结
前端
小小小小宇6 小时前
前端监控用户停留时长
前端
小小小小宇6 小时前
前端性能监控笔记
前端
烛阴7 小时前
Date-fns教程:现代JavaScript日期处理从入门到精通
前端·javascript
全栈小57 小时前
【前端】Vue3+elementui+ts,TypeScript Promise<string>转string错误解析,习惯性请出DeepSeek来解答
前端·elementui·typescript·vue3·同步异步
穗余7 小时前
NodeJS全栈开发面试题讲解——P6安全与鉴权
前端·sql·xss
小蜜蜂嗡嗡8 小时前
flutter项目迁移空安全
javascript·安全·flutter
穗余9 小时前
NodeJS全栈开发面试题讲解——P2Express / Nest 后端开发
前端·node.js
航Hang*9 小时前
WEBSTORM前端 —— 第3章:移动 Web —— 第4节:移动适配-VM
前端·笔记·edge·less·css3·html5·webstorm