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

news2025/7/21 6:34:28

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

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

在这里插入图片描述

  • 封装组件

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

<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>
引用
<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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2392363.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

科技赋能建筑行业,智能楼宇自控系统崭露头角成发展新势力

在科技浪潮席卷全球的时代背景下&#xff0c;传统建筑行业正面临着前所未有的变革压力。随着城市化进程加快&#xff0c;建筑规模与复杂度不断攀升&#xff0c;能源消耗、运营效率、用户体验等问题日益凸显。智能楼宇自控系统凭借物联网、大数据、人工智能等前沿技术&#xff0…

白杨SEO:做AI搜索优化的DeepSeek、豆包、Kimi、百度文心一言、腾讯元宝、通义、智谱、天工等AI生成内容信息采集主要来自哪?占比是多少?

大家好&#xff0c;我是白杨SEO&#xff0c;专注SEO十年以上&#xff0c;全网SEO流量实战派&#xff0c;AI搜索优化研究者。 在开始写之前&#xff0c;先说个抱歉。 上周在上海客户以及线下聚会AI搜索优化分享说各大AI模型的联网搜索是关闭的&#xff0c;最开始上来确实是的。…

显示docker桌面,vnc远程连接docker

目录 相关概念&#xff1a; 实现步骤&#xff1a; 1.启动docker容器 2.安装x11 3.Docker 容器中安装一个完整的图形桌面&#xff08;XFCE&#xff09;和 VNC 远程桌面服务器&#xff08;TightVNC&#xff09; 4.配置vncservice 5.本地安装VNC Viewer连接VNC Viewer下载地…

腾讯云国际站性能调优

全球化业务扩张中&#xff0c;云端性能直接决定用户体验与商业成败。腾讯云国际站通过资源适配、网络优化与存储革新&#xff0c;为企业提供全链路调优方案。 ​​资源精准适配​​ 实例选型需与业务场景深度耦合&#xff0c;计算优化型实例加速AI训练效率3倍&#xff0c;内存…

深入解析操作系统内核与用户空间以及内核态与用户态转换

用户空间和内核空间的划分是现代操作系统的基础&#xff0c;对应用程序网络模型的设计和优化有着深远的影响。 内核空间与用户空间的分工 现代操作系统为了保证系统的稳定性和安全性&#xff0c;将虚拟内存空间划分为用户空间和内核空间。 一、用户空间 用户空间是用户程序…

每日一题洛谷P8662 [蓝桥杯 2018 省 AB] 全球变暖c++

P8662 [蓝桥杯 2018 省 AB] 全球变暖 - 洛谷 (luogu.com.cn) DFS #include<iostream> using namespace std; int n, res; char a[1005][1005]; bool vis[1005][1005]; bool flag; int dx[4] { 0,0,1,-1 }; int dy[4] { 1,-1,0,0 }; void dfs(int x, int y) {vis[x][y]…

【JVM】初识JVM 从字节码文件到类的生命周期

初识JVM JVM&#xff08;Java Virtual Machine&#xff09;即 Java 虚拟机&#xff0c;是 Java 技术的核心组件之一。JVM的本质就是运行在计算机上的一个程序&#xff0c;通过软件模拟实现了一台抽象的计算机的功能。JVM是Java程序的运行环境&#xff0c;负责加载字节码文件&a…

多级体验体系构建:基于开源AI智能客服与AI智能名片的S2B2C商城小程序体验升级路径研究

摘要&#xff1a;在体验经济时代&#xff0c;传统企业单一的总部体验模式难以覆盖全链路用户需求。本文针对B端与C端体验深度差异&#xff0c;提出“一级总部体验—二级区域体验—三级终端体验”的分层架构&#xff0c;并引入“开源AI智能客服”与“AI智能名片”技术&#xff0…

Git切换历史版本及Gitee云绑定

1、git介绍 Git是目前世界上最先进的分布式版本控制系统 Linux <- BitKeeper&#xff08;不是开源的&#xff0c;但免费的&#xff0c;后来要收费&#xff09; Linus Torvalds(林纳斯托瓦兹) 两周时间吧&#xff0c;弄了个 Git&#xff1b;大约一个月就把Linux代码从BitK…

服务器的IP是什么东西?

一、什么是服务器的IP地址&#xff1f; 服务器的IP地址是互联网协议&#xff08;Internet Protocol&#xff09;的缩写&#xff0c;是服务器在网络中的唯一数字标识符。它类似于现实生活中的门牌号&#xff0c;用于标识服务器在网络中的位置&#xff0c;使其他设备能够通过它与…

[问题解决]:Unable to find image ‘containrrr/watchtower:latest‘ locally

一&#xff0c;问题 在使用docker安装部署新应用的时候&#xff0c;报错&#xff1a;Unable to find image containrrr/watchtower:latest locally 分析认为是当前docker的资源库里找不到这个软件的镜像&#xff0c;需要配置一个包含这个软件镜像的新的资源库。 二&#xff0…

【文件上传】阿里云对象存储服务实现文件上传

一、基础 上传到本地&#xff1a; package org.example.controller;import lombok.extern.slf4j.Slf4j; import org.example.pojo.Result; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; imp…

IPv6代理如何引领下一代网络未来

随着互联网技术的不断发展&#xff0c;IPv6逐渐成为下一代网络协议的核心&#xff0c;替代IPv4已是大势所趋。IPv6代理作为IPv6网络环境下的重要工具&#xff0c;为用户提供了更高效、更安全的网络解决方案。 IPv6代理的定义 IPv6代理是在IPv6网络环境中为处理IPv4转换和其他网…

Linux——数据链路层

1. 认识以太网 认知&#xff1a;以太网是用于局域网数据通信的协议标准&#xff0c;定义了同一局域网内通过电缆/无线怎么在设备之间传输数据帧。 注&#xff1a;整个网络世界可以具象看出由许许多多的局域网组成&#xff0c; • 家庭中的设备A and 家庭中的设备B and 家庭路由…

游戏引擎学习第310天:利用网格划分完成排序加速优化

回顾并为今天的内容做个铺垫 昨天我们完成了一个用于排序的空间划分系统&#xff0c;但还没有机会真正利用它。昨天的工作刚好在结束时才完成&#xff0c;所以今天我们打算正式使用这个空间划分来加速排序。 现在我们在渲染代码中&#xff0c;可以看到在代码底部隐藏着一个“…

数据结构 - 树的遍历

一、二叉树的遍历 对于二叉树&#xff0c;常用的遍历方式包括&#xff1a;先序遍历、中序遍历、后序遍历和层次遍历 。 1、先序遍历&#xff08;PreOrder&#xff09; 先序遍历的操作过程如下&#xff1a; 若二叉树为空&#xff0c;则什么也不做&#xff1b;否则&#xff0…

时序模型介绍

一.整体介绍 1.单变量 vs 多变量时序数据 单变量就是只根据时间预测&#xff0c;多变量还要考虑用户 2.为什么不能用机器学习预测&#xff1a; a.时间不是影响标签的关键因素 b.时间与标签之间的联系过于弱/过于复杂&#xff0c;因此时序模型依赖于时间与时间的相关性来进行预…

【第4章 图像与视频】4.4 离屏 canvas

文章目录 前言为什么要使用 offscreenCanvas为什么要使用 OffscreenCanvas如何使用 OffscreenCanvas第一种使用方式第二种使用方式 计算时长超过多长时间适合用Web Worker 前言 在 Canvas 开发中&#xff0c;我们经常需要处理复杂的图形和动画&#xff0c;这些操作可能会影响页…

尚硅谷redis7 74-85 redis集群分片之集群是什么

74 redis集群分片之集群是什么 如果主机宕机&#xff0c;那么写操作就被暂时中断&#xff0c;后面就要由哨兵进行投票和选举。那么一瞬间若有大量的数据修改&#xff0c;由于写操作中断就会导致数据流失。 由于数据量过大,单个Master复制集难以承担,因此需要对多个复制集进行…

WPF的基础控件:布局控件(StackPanel DockPanel)

布局控件&#xff08;StackPanel & DockPanel&#xff09; 1 StackPanel的Orientation属性2 DockPanel的LastChildFill3 嵌套布局示例4 性能优化建议5 常见问题排查 在WPF开发中&#xff0c;布局控件是构建用户界面的基石。StackPanel和DockPanel作为两种最基础的布局容器&…