当前位置: 首页 > news >正文

[Vue组件]比例环形编辑

环形编辑组件简介

这是一个基于 Vue.js 的交互式环形编辑组件(YsCircularEdit),用于可视化和动态调整比例分配。核心功能包括:

  • 动态扇形渲染:根据传入的比例数组(默认[0.375, 0.125, 0.5])和颜色数组,生成可定制的环形扇形图。
  • 交互式拖拽:用户可拖动分隔线调整扇形比例,最小比例限制为5%,确保操作有效。
  • 精确比例管理:自动校正比例总和为1,保留两位小数,解决浮点精度问题。
  • 视觉反馈:拖拽时显示辅助圆圈,分隔线支持悬停(绿色高亮、阴影效果)和拖拽(加粗、增强阴影)状态。
  • 响应式布局:自适应容器尺寸,保持1:1宽高比,禁用文本选择优化交互。
  • 事件通知:比例调整后通过 ratios-change 事件向父组件传递新比例数据。

适合用于数据分配、资源管理等需要直观比例调整的场景。

  • 组件效果

在这里插入图片描述

<template><div class="ys-circular-edit" :class="isDragging && 'dragging'"><div ref="container" class="container"><svg :width="size" :height="size" viewBox="0 0 200 200" @mousemove="handleMouseMove" @mouseup="handleMouseUp" @mouseleave="handleMouseUp"><!-- 动态渲染扇形区域 --><pathv-for="(segment, index) in computedSegments":key="`path_${index}`":d="createArcPath(segment.startAngle, segment.endAngle)":fill="segment.color"/><!-- 分隔线 --><linev-for="(angle, index) in separatorAngles":key="`line_${index}`"class="separator-line":class="{ dragging: draggingIndex === index }":x1="getSeparatorLine(angle).startX":y1="getSeparatorLine(angle).startY":x2="getSeparatorLine(angle).endX":y2="getSeparatorLine(angle).endY"stroke="#F2F6FC"stroke-width="4"@mousedown="handleMouseDown($event, index)"/><!-- 拖拽时的辅助圆圈 --><circle v-if="isDragging" :cx="dragPreview.x" :cy="dragPreview.y" r="4" fill="#FF6B6B" opacity="0.8" /></svg></div></div>
</template><script>
const centerX = 100
const centerY = 100
const innerRadius = 55
const outerRadius = 80export default {name: 'YsCircularEdit',props: {// 比例数组,三个小数加起来应该等于1ratios: {type: Array,default: () => [0.375, 0.125, 0.5] // 对应原来的 135/360, 45/360, 180/360},// 颜色数组colors: {type: Array,default: () => ['#eab170', '#6290ea', '#ea5e5e']}},data() {return {size: 300,isDragging: false,draggingIndex: -1,currentRatios: [...this.ratios], // 内部维护的比例状态dragPreview: { x: 0, y: 0 }}},computed: {// 根据比例计算每个扇形的角度范围computedSegments() {let currentAngle = 0const segments = []this.currentRatios.forEach((ratio, index) => {const startAngle = currentAngleconst angleSpan = ratio * 360const endAngle = currentAngle + angleSpansegments.push({startAngle: startAngle,endAngle: endAngle,color: this.colors[index] || '#CCCCCC'})currentAngle = endAngle})return segments},// 计算分隔线角度separatorAngles() {const angles = []let currentAngle = 0// 添加每个扇形结束的角度作为分隔线(不包括最后一个360度)this.currentRatios.forEach((ratio, index) => {if (index < this.currentRatios.length - 1) {// 不包括最后一个分隔线currentAngle += ratio * 360angles.push(currentAngle)}})return angles}},watch: {ratios: {handler(newRatios) {this.currentRatios = [...newRatios]},immediate: true}},mounted() {this.init()},methods: {init() {const dom = this.$refs.containerconst rect = dom.getBoundingClientRect()this.size = rect.width},// 创建弧形路径createArcPath(startDeg, endDeg) {const startAngle = ((startDeg - 90) * Math.PI) / 180const endAngle = ((endDeg - 90) * Math.PI) / 180const outerStartX = centerX + outerRadius * Math.cos(startAngle)const outerStartY = centerY + outerRadius * Math.sin(startAngle)const outerEndX = centerX + outerRadius * Math.cos(endAngle)const outerEndY = centerY + outerRadius * Math.sin(endAngle)const innerStartX = centerX + innerRadius * Math.cos(startAngle)const innerStartY = centerY + innerRadius * Math.sin(startAngle)const innerEndX = centerX + innerRadius * Math.cos(endAngle)const innerEndY = centerY + innerRadius * Math.sin(endAngle)const largeArcFlag = endDeg - startDeg > 180 ? 1 : 0return [`M ${outerStartX} ${outerStartY}`,`A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${outerEndX} ${outerEndY}`,`L ${innerEndX} ${innerEndY}`,`A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${innerStartX} ${innerStartY}`,'Z'].join(' ')},// 获取分隔线坐标getSeparatorLine(angleDeg) {const angleRad = ((angleDeg - 90) * Math.PI) / 180return {startX: centerX + Math.cos(angleRad) * innerRadius,startY: centerY + Math.sin(angleRad) * innerRadius,endX: centerX + Math.cos(angleRad) * outerRadius,endY: centerY + Math.sin(angleRad) * outerRadius}},// 鼠标按下开始拖拽handleMouseDown(event, index) {event.preventDefault()this.isDragging = truethis.draggingIndex = index},// 鼠标移动时更新角度handleMouseMove(event) {if (!this.isDragging) returnconst svgRect = event.currentTarget.getBoundingClientRect()const svgX = ((event.clientX - svgRect.left) / svgRect.width) * 200const svgY = ((event.clientY - svgRect.top) / svgRect.height) * 200// 更新拖拽预览位置this.dragPreview.x = svgXthis.dragPreview.y = svgY// 计算鼠标相对于圆心的角度const angle = this.calculateAngle(svgX, svgY)// 更新比例this.updateRatios(angle)},// 鼠标松开结束拖拽handleMouseUp() {if (this.isDragging) {this.isDragging = falsethis.draggingIndex = -1const result = this.formatRatiosToSum1(this.currentRatios)// 触发比例变化事件this.$emit('ratios-change', result)}},// 计算鼠标位置相对于圆心的角度calculateAngle(x, y) {const dx = x - centerXconst dy = y - centerYlet angle = (Math.atan2(dy, dx) * 180) / Math.PI// 调整角度,让0度在顶部,顺时针增加angle = (angle + 90 + 360) % 360return angle},// 根据新角度更新比例updateRatios(newAngle) {const newRatios = [...this.currentRatios]if (this.draggingIndex === 0) {// 拖拽第一条分隔线,影响第一个和第二个扇形const ratio1 = newAngle / 360const ratio2 = this.currentRatios[1] + this.currentRatios[0] - ratio1// 确保比例有效if (ratio1 > 0.05 && ratio2 > 0.05) {// 最小5%的限制newRatios[0] = ratio1newRatios[1] = ratio2}} else if (this.draggingIndex === 1) {// 拖拽第二条分隔线,影响第二个和第三个扇形const totalRatio12 = newAngle / 360const ratio2 = totalRatio12 - this.currentRatios[0]const ratio3 = 1 - totalRatio12// 确保比例有效if (ratio2 > 0.05 && ratio3 > 0.05) {newRatios[1] = ratio2newRatios[2] = ratio3}}this.currentRatios = newRatios},// 格式化比例数组,确保总和为1且保留两位小数formatRatiosToSum1(ratios) {// 先将所有比例四舍五入到两位小数const formattedRatios = ratios.map(ratio => Math.round(ratio * 100) / 100)// 计算当前总和let currentSum = formattedRatios.reduce((sum, ratio) => sum + ratio, 0)currentSum = Math.round(currentSum * 100) / 100 // 避免浮点精度问题// 如果总和不是1,需要调整if (currentSum !== 1) {const difference = Math.round((1 - currentSum) * 100) / 100// 找到可以调整的索引(不是最小值的那个)let adjustIndex = 0let maxValue = formattedRatios[0]// 找到数值最大的项来承担差值for (let i = 1; i < formattedRatios.length; i++) {if (formattedRatios[i] > maxValue) {maxValue = formattedRatios[i]adjustIndex = i}}// 调整最大值项formattedRatios[adjustIndex] = Math.round((formattedRatios[adjustIndex] + difference) * 100) / 100// 确保调整后的值不小于0.01if (formattedRatios[adjustIndex] < 0.01) {formattedRatios[adjustIndex] = 0.01// 重新分配剩余的比例const remaining = Math.round((1 - 0.01) * 100) / 100const otherIndices = formattedRatios.map((_, index) => index).filter(i => i !== adjustIndex)const otherSum = otherIndices.reduce((sum, i) => sum + formattedRatios[i], 0)if (otherSum > 0) {const scale = remaining / otherSumotherIndices.forEach(i => {formattedRatios[i] = Math.round(formattedRatios[i] * scale * 100) / 100})}}}// 最终验证并微调(处理可能的舍入误差)let finalSum = formattedRatios.reduce((sum, ratio) => sum + ratio, 0)finalSum = Math.round(finalSum * 100) / 100if (finalSum !== 1) {const finalDiff = Math.round((1 - finalSum) * 100) / 100// 将最终差值加到最后一个元素上formattedRatios[formattedRatios.length - 1] = Math.round((formattedRatios[formattedRatios.length - 1] + finalDiff) * 100) / 100}return formattedRatios}}
}
</script><style scoped>
/* 禁用文本选择 */
.ys-circular-edit {position: relative;width: 100%;height: 100%;user-select: none;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;
}
.ys-circular-edit.dragging {cursor: none !important;
}.container {margin: auto;max-height: 100%;aspect-ratio: 1 / 1;
}.container > svg {width: 100%;height: 100%;
}/* 分隔线样式 */
.separator-line {cursor: grab;transition: all 0.3s ease;stroke-linecap: round;
}.separator-line:hover {stroke: #67c23a !important;stroke-width: 4;filter: drop-shadow(0 0 4px rgba(103, 194, 58, 0.6));
}.separator-line.dragging {cursor: grabbing;stroke: #67c23a !important;stroke-width: 5;filter: drop-shadow(0 0 6px rgba(103, 194, 58, 0.8));
}
</style>

相关文章:

  • 【前端】【Vue3】vue3性能优化总结
  • 关于Oracle LATCH:LIBRARY CACHE
  • 【Vue Vapor Mode :技术突破与性能优化的可能性】
  • 《棒球特长生》棒球升学途径·棒球1号位
  • 深度学习能取代机器学习吗?
  • 给定数组 nums,判断是否存在三个元素 a, b, c 使得 a + b + c = 0。
  • 基于vue框架的动物园饲养管理系统a7s60(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 四足机器人环境监测系统相关问题
  • 鸿蒙OSUniApp 实现登录状态管理与持久化#三方框架 #Uniapp
  • 深度学习常用概念详解:从生活理解到技术原理
  • 调不好分布式锁?HarmonyOS + Redis 分布式锁失效排查全路径
  • 32.第二阶段x64游戏实战-封包-公共call
  • [yolov11改进系列]基于yolov11引入感受野注意力卷积RFAConv的python源码+训练源码
  • 【刷题】数组拼接(超聚变暑期实习笔试)
  • GitCode镜像仓库批量下载开发实录
  • 一,关键字class和typename的区别
  • 线代第四章线性方程组第二节:线性方程组有解判断
  • 模型协同构建智能流程体
  • linux kernel 内存回收水位线调整方法
  • 《独立开发工具 • 半月刊》 第 003 期
  • 网站建设的软件介绍/网站查询关键词排名软件
  • wordpress 首页打开慢/seo搜索引擎优化工具
  • 短视频制作软件app/搜索引擎关键词seo优化公司
  • 如何创建网站和域名/网络营销策划包括哪些内容
  • 电商网站系统建设考试/销售策略和营销策略
  • 前端做项目网站/杭州seo外包