news 2026/11/3 12:43:10

Vue 3 + SVG 实现 3/4 圆环信用分组件:从圆弧计算到动画与响应式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 + SVG 实现 3/4 圆环信用分组件:从圆弧计算到动画与响应式

信用分展示这个需求,我在好几个后台管理系统和数据看板项目里都碰到过。产品经理拿着竞品截图过来,说“就要这种四分之三圆环的效果,中间显示分数,颜色跟着分数变”。乍一看不就是画个圆环吗,但真动手做的时候,SVG 的圆弧路径怎么算、3/4 比例怎么控制、分数区间怎么映射到颜色、动画怎么做得顺滑,这些细节一个都绕不开。我用 Vue 做过至少五六个不同形态的信用分组件,踩过的坑从路径计算错误到响应式适配翻车都有,今天就把这套东西完整拆一遍。

这篇文章面向的是有一定 Vue 基础、需要在项目里落地数据可视化组件的开发者。不管你是刚接触 SVG 绘图的新手,还是想优化现有组件的老手,下面关于圆弧路径计算、分数映射策略、动画实现和响应式适配的内容都能直接拿去用。我会从设计思路讲起,把每个关键决策背后的原因说清楚,再给出可以直接复现的完整实现,最后把常见问题和排查方法整理成速查表。

1. 整体设计与技术选型拆解

1.1 为什么选 SVG 而不是 Canvas 或纯 CSS

做圆环进度条,市面上常见三条路:Canvas 绘制、纯 CSS 的 conic-gradient、SVG 的 stroke-dasharray 方案。我在不同项目里三种都用过,最后信用分这个场景我基本锁定 SVG,原因得掰开说。

Canvas 的问题是它是位图绘制,缩放会糊,而且要做响应式得手动监听容器尺寸重绘,代码量大。更关键的是 Canvas 里的元素不是 DOM 节点,想给圆环加个 hover 效果或者无障碍属性都很别扭。信用分组件通常要嵌入卡片里,卡片宽度随屏幕变化,Canvas 每次 resize 都要重新算一遍,维护成本高。

纯 CSS 的 conic-gradient 确实能画出圆环,代码也短,但它有个硬伤:渐变角度和颜色过渡不好精确控制,尤其是要做 3/4 圆环(也就是 270 度)的时候,得用遮罩或者多层叠加来“切掉”那 90 度,实现起来反而绕。而且 conic-gradient 在部分老版本浏览器上的表现不稳定,做企业级项目不敢赌。

SVG 的 stroke-dasharray 方案是我最终的选择。原理很简单:画一个圆,用虚线描边,通过控制虚线的“实线长度”和“间隔长度”来模拟进度。圆环的周长是固定的,进度百分比直接映射成实线段长度,数学关系清晰。SVG 是矢量图,任意缩放不失真,元素是 DOM 节点,加动画、加交互、加无障碍标签都方便。3/4 圆环的实现也直观——把整个圆的起点旋转一下,只画 270 度的弧就行。

提示:如果你的项目需要导出图片或者做复杂的数据可视化大屏,Canvas 可能更合适;但信用分这种单指标展示组件,SVG 的综合成本最低。

1.2 3/4 圆环的几何模型怎么建立

这是整个组件的地基,算错了后面全白搭。我先把这个几何模型讲透。

一个完整的圆是 360 度,3/4 圆环就是 270 度。我们希望在页面上看到的是一个开口朝下的“C”形,或者说像仪表盘那样从左上绕到右上,底部留空。这个开口位置决定了起始角度和旋转方向。

我的做法是:以圆心为原点,从时钟的 7 点半方向(也就是左下偏下)开始,顺时针画 270 度,到 4 点半方向结束。换算成标准角度,起点大约在 135 度位置(SVG 坐标系里 y 轴向下,角度计算要注意),终点在 45 度位置。实际实现时不用死记角度,用 SVG 的transform="rotate(135 cx cy)"把整个圆的起点转到左下,然后 stroke-dasharray 只画 3/4 周长就行。

这里有个关键参数:圆的半径 r 和描边宽度 strokeWidth。圆环的“可视半径”其实是 r,但描边是骑在路径上的,一半在内一半在外。所以如果容器尺寸是 size,描边宽度是 sw,那么半径应该取(size - sw) / 2,这样描边外沿刚好贴着容器边缘,不会溢出被裁掉。这个细节很多人第一次做会忽略,导致圆环被容器切掉一圈。

周长公式是C = 2 * π * r。3/4 圆环的弧长就是0.75 * C。假设我们要显示 80% 的进度,那实线段长度就是0.8 * 0.75 * C,间隔段长度就是剩下的0.75 * C - 实线段长度,再加上那 1/4 不画的空白。这里要特别注意:dasharray 的第一个值是实线长度,第二个值是间隔长度,间隔要足够大以覆盖剩余弧长,通常直接给整个周长 C 就行,因为超出部分不会画出来。

1.3 分数到圆环的映射策略

信用分的范围通常是 0 到 100,也可能是 350 到 950 这种(类似某些信用体系)。组件不能写死范围,得做成可配置的。我设计的 props 里,min和max定义分数区间,score是当前值,内部计算ratio = (score - min) / (max - min),这个 ratio 就是圆环的填充比例。

为什么要做这个归一化?因为不同业务线的分数区间不一样,组件要复用就不能假设分数一定是百分制。归一化之后,圆环只认 0 到 1 的比例,逻辑就干净了。

颜色映射是另一个要考虑的点。信用分通常分档:低分红色警示、中等黄色提醒、高分绿色或蓝色表示良好。我一般用分段映射,比如 0-60 红色、60-80 橙色、80-100 绿色。但硬切颜色在临界点会突变,视觉上有点生硬。更好的做法是用渐变色,或者至少在档位之间做过渡。不过渐变圆环在 SVG 里要用 linearGradient 或 radialGradient,配合 stroke 使用,实现复杂度会上升。我的经验是:如果产品没明确要求渐变,分段纯色加过渡动画就够了,性价比最高。

1.4 组件 API 设计

一个可复用的组件,props 设计得好不好直接决定别人愿不愿意用。我最终定下来的 props 是这样的:

参数名类型默认值说明
scoreNumber0当前信用分
minNumber0分数下限
maxNumber100分数上限
sizeNumber200组件整体尺寸(px)
strokeWidthNumber12圆环描边宽度
colorsArray见下文分段颜色配置
animatedBooleantrue是否启用加载动画
durationNumber1000动画时长(ms)

colors 我设计成数组形式,每项包含threshold和color,按 threshold 升序排列。这样业务方可以自由配置分档,比如[{threshold: 0, color: '#f56c6c'}, {threshold: 60, color: '#e6a23c'}, {threshold: 80, color: '#67c23a'}]。组件内部根据 score 落在哪个区间取对应颜色。这种设计比写死三档灵活得多,不同项目直接传不同配置就行。

2. 核心细节解析与实操要点

2.1 SVG 圆弧路径的精确计算

虽然前面说了用 stroke-dasharray 方案,但理解圆弧路径的计算对排查问题很有帮助,而且有些场景(比如要做不规则开口)确实需要手写 path。

SVG 的 path 里画圆弧用A命令,格式是A rx ry x-axis-rotation large-arc-flag sweep-flag x y。画一个 3/4 圆环,起点和终点的坐标要用三角函数算。假设圆心在(cx, cy),半径 r,起点角度 θ1,终点角度 θ2,那么:

x = cx + r * cos(θ) y = cy + r * sin(θ)

注意 SVG 坐标系 y 轴向下,所以角度是从 x 轴正方向顺时针算的。3/4 圆环从 135 度到 405 度(即 45 度),跨度 270 度,大于 180 度,所以large-arc-flag要设为 1。sweep-flag设为 1 表示顺时针方向。

我实际项目里更常用的是<circle>配合 dasharray,因为代码更短、更好维护。但如果你要做那种开口角度可变的圆环(比如有的设计要 2/3 圆环),手写 path 反而更灵活,因为 dasharray 方案改开口角度要重新算旋转和弧长,path 方案直接改起止角度就行。

注意:用<circle>方案时,transform="rotate(135 cx cy)"的旋转中心必须是圆心坐标,写错了圆环会偏移。我见过有人把旋转中心写成容器中心,结果圆环整个歪掉。

2.2 stroke-dasharray 与 stroke-dashoffset 的配合

这是 SVG 圆环进度的核心机制,必须吃透。stroke-dasharray定义虚线的“实-空”模式,stroke-dashoffset定义虚线从哪里开始偏移。

我的实现思路是:先算出 3/4 圆环的总弧长arcLength = 0.75 * 2 * π * r。然后设置stroke-dasharray = "${arcLength} ${circumference}",这样实线段是整段弧长,间隔足够大。进度通过stroke-dashoffset控制:offset 从arcLength逐渐减到arcLength * (1 - ratio),视觉上就是圆环从空到满。

为什么用 dashoffset 而不是直接改 dasharray?因为改 dasharray 会导致虚线模式重新计算,动画过程中可能出现跳变;而 dashoffset 是平移虚线起点,动画更平滑,而且配合 CSS transition 或 requestAnimationFrame 都好做。

具体数值举例:size=200,strokeWidth=12,则 r=(200-12)/2=94,周长 C=2π94≈590.6,3/4 弧长≈442.9。如果 ratio=0.8,那么 offset 从 442.9 变到 442.9*(1-0.8)=88.6。这个计算过程我在代码里封装成 computed,score 变化时自动重算。

2.3 分数区间的颜色映射实现

颜色映射我写了一个方法,遍历 colors 数组,找到最后一个 threshold 小于等于 score 的项,取它的 color。这里有个边界要处理:如果 score 小于第一个 threshold,取第一个颜色;如果大于最后一个 threshold,取最后一个颜色。

function getColor(score, colors) { let result = colors[0].color; for (const item of colors) { if (score >= item.threshold) { result = item.color; } else { break; } } return result; }

这个逻辑简单但容易写错。我见过有人用find方法,结果找到第一个满足条件的就返回了,没考虑后面的档位。用 for 循环配合 break 是最稳的,因为数组已经按 threshold 升序排好了,一旦遇到不满足的就说明后面都不满足。

颜色切换时加个 CSS transition 会更自然:transition: stroke 0.3s ease。这样分数变化导致颜色档位切换时,颜色是渐变过去的,不会突兀。

2.4 动画的两种实现路径

动画这块我试过两种方案,各有适用场景。

第一种是 CSS transition。给 circle 的stroke-dashoffset加 transition,score 变化时浏览器自动补间。优点是代码极简,性能好(走合成层);缺点是无法在动画过程中做回调,而且初始加载时的动画需要额外处理(比如先设 offset 为满值,下一帧再改)。

第二种是 requestAnimationFrame 手动补间。自己写缓动函数,每帧更新 offset。优点是控制力强,可以做回调、可以自定义缓动曲线;缺点是要手动管理动画生命周期,组件销毁时要 cancelAnimationFrame,否则内存泄漏。

我最终用的是组合方案:初始加载用 rAF 做入场动画(从 0 涨到目标值),后续 score 变化用 CSS transition。这样既有入场效果,又保持代码简洁。入场动画的缓动函数我用 easeOutCubic,公式是1 - Math.pow(1 - t, 3),视觉上先快后慢,符合“分数增长”的心理预期。

提示:rAF 动画里记得用performance.now()而不是Date.now(),前者精度更高,动画更顺滑。

2.5 响应式适配的处理

组件尺寸不能写死,得跟着容器走。我的做法是 size 支持传入数字(px)或字符串(如 '100%')。如果是百分比,用 ResizeObserver 监听容器尺寸变化,动态重算 r 和弧长。

ResizeObserver 的兼容性现在很好了,但要注意在组件卸载时 disconnect,否则会持续监听导致泄漏。另外,尺寸变化时如果正在做动画,要先把动画停掉再重算,不然会出现数值错乱。

还有一种更轻量的方案:用 SVG 的 viewBox 配合 preserveAspectRatio,让 SVG 自动缩放。这样不用监听尺寸,但 strokeWidth 也会跟着缩放,可能导致细屏上圆环太细。我的经验是:如果设计稿要求描边宽度固定,就用 ResizeObserver 方案;如果允许整体等比缩放,viewBox 方案更省事。

3. 完整实操过程与核心代码实现

3.1 项目初始化与组件骨架

先建一个 Vue 3 的单文件组件,用 Composition API。如果你还在用 Vue 2,逻辑完全一样,只是把 setup 换成 data/computed/methods 的写法。

npm create vite@latest credit-ring -- --template vue cd credit-ring npm install

组件文件叫CreditRing.vue,骨架先搭出来:

<template> <div class="credit-ring" :style="{ width: sizePx, height: sizePx }"> <svg :width="sizePx" :height="sizePx" :viewBox="`0 0 ${sizePx} ${sizePx}`"> <!-- 背景圆环 --> <circle :cx="center" :cy="center" :r="radius" fill="none" :stroke="trackColor" :stroke-width="strokeWidth" :stroke-dasharray="trackDash" :transform="`rotate(135 ${center} ${center})`" stroke-linecap="round" /> <!-- 进度圆环 --> <circle :cx="center" :cy="center" :r="radius" fill="none" :stroke="currentColor" :stroke-width="strokeWidth" :stroke-dasharray="progressDash" :stroke-dashoffset="progressOffset" :transform="`rotate(135 ${center} ${center})`" stroke-linecap="round" class="progress-circle" /> </svg> <div class="center-content"> <span class="score-value">{{ displayScore }}</span> <span class="score-label">信用分</span> </div> </div> </template>

这里有两个 circle:背景圆环画满 3/4 作为底轨,进度圆环叠在上面。两个都用rotate(135)把起点转到左下。stroke-linecap="round"让圆环两端是圆头,视觉上更柔和,这也是信用分组件常见的样式。

3.2 核心计算逻辑的 setup 实现

接下来把计算逻辑写进 setup。先定义 props:

import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue' const props = defineProps({ score: { type: Number, default: 0 }, min: { type: Number, default: 0 }, max: { type: Number, default: 100 }, size: { type: [Number, String], default: 200 }, strokeWidth: { type: Number, default: 12 }, colors: { type: Array, default: () => [ { threshold: 0, color: '#f56c6c' }, { threshold: 60, color: '#e6a23c' }, { threshold: 80, color: '#67c23a' } ] }, animated: { type: Boolean, default: true }, duration: { type: Number, default: 1000 } })

然后算尺寸相关的基础量。这里要处理 size 是数字还是百分比字符串的情况:

const containerSize = ref(200) const sizePx = computed(() => { return typeof props.size === 'number' ? `${props.size}px` : props.size }) const actualSize = computed(() => { return typeof props.size === 'number' ? props.size : containerSize.value }) const center = computed(() => actualSize.value / 2) const radius = computed(() => (actualSize.value - props.strokeWidth) / 2) const circumference = computed(() => 2 * Math.PI * radius.value) const arcLength = computed(() => 0.75 * circumference.value)

actualSize是实际用于计算的像素值,百分比情况下从 containerSize 取。radius用(size - strokeWidth) / 2保证描边不溢出,这个前面讲过原因。

进度相关的计算:

const ratio = computed(() => { const range = props.max - props.min if (range <= 0) return 0 const r = (props.score - props.min) / range return Math.min(1, Math.max(0, r)) }) const trackDash = computed(() => `${arcLength.value} ${circumference.value}`) const progressDash = computed(() => `${arcLength.value} ${circumference.value}`) const progressOffset = computed(() => arcLength.value * (1 - ratio.value))

ratio做了 clamp,防止传入越界分数导致圆环画错。trackDash和progressDash其实一样,都是实线段为整段弧长、间隔为整个周长。区别在 offset:背景圆环 offset 为 0(画满),进度圆环 offset 根据比例算。

颜色计算:

const currentColor = computed(() => { let color = props.colors[0].color for (const item of props.colors) { if (props.score >= item.threshold) { color = item.color } else { break } } return color })

3.3 入场动画的 rAF 实现

入场动画我单独抽了一个逻辑。核心是维护一个animatedRatioref,从 0 涨到目标 ratio,圆环的 offset 基于 animatedRatio 算而不是直接基于 ratio。

const animatedRatio = ref(0) let rafId = null function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3) } function runAnimation(targetRatio) { if (rafId) cancelAnimationFrame(rafId) if (!props.animated) { animatedRatio.value = targetRatio return } const startTime = performance.now() const startRatio = animatedRatio.value const delta = targetRatio - startRatio function step(now) { const elapsed = now - startTime const progress = Math.min(elapsed / props.duration, 1) animatedRatio.value = startRatio + delta * easeOutCubic(progress) if (progress < 1) { rafId = requestAnimationFrame(step) } else { rafId = null } } rafId = requestAnimationFrame(step) }

这里有个细节:动画起点用animatedRatio.value而不是固定 0,这样如果动画中途 score 变了,新动画从当前值继续,不会跳回 0 重来。这个处理让交互更顺滑,尤其是分数频繁更新的场景。

进度 offset 改成基于 animatedRatio:

const progressOffset = computed(() => arcLength.value * (1 - animatedRatio.value))

然后监听 score 变化触发动画:

watch(() => props.score, () => { runAnimation(ratio.value) }) onMounted(() => { runAnimation(ratio.value) }) onBeforeUnmount(() => { if (rafId) cancelAnimationFrame(rafId) })

onBeforeUnmount里取消 rAF 是必须的,否则组件销毁后动画还在跑,回调里访问已销毁的响应式数据会报错。

3.4 响应式尺寸监听

百分比尺寸的情况,用 ResizeObserver 监听容器:

let resizeObserver = null onMounted(() => { if (typeof props.size === 'string') { resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { const { width, height } = entry.contentRect containerSize.value = Math.min(width, height) } }) resizeObserver.observe(containerRef.value) } }) onBeforeUnmount(() => { if (resizeObserver) { resizeObserver.disconnect() resizeObserver = null } })

取Math.min(width, height)是因为圆环要正方形,容器可能是长方形,取短边保证圆环完整显示。containerRef是外层 div 的 ref,记得在模板里加上ref="containerRef"。

3.5 样式与中心内容布局

中心显示分数和标签,用绝对定位居中:

.credit-ring { position: relative; display: inline-block; } .center-content { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; } .score-value { font-size: 2em; font-weight: 700; line-height: 1.2; color: #303133; } .score-label { font-size: 0.75em; color: #909399; margin-top: 4px; } .progress-circle { transition: stroke 0.3s ease; }

pointer-events: none让中心内容不拦截鼠标事件,避免影响圆环上的交互(如果有的话)。transition: stroke让颜色切换平滑。

分数显示我加了个小优化:用displayScore而不是直接绑 score,配合动画让数字也滚动增长。这个可以用同样的 rAF 逻辑,把 animatedRatio 映射回分数值:

const displayScore = computed(() => { return Math.round(props.min + animatedRatio.value * (props.max - props.min)) })

这样数字和圆环同步增长,视觉一致性很好。实测下来这个细节很加分,用户会觉得组件“活”了。

3.6 完整调用示例

在父组件里用起来:

<template> <div class="dashboard"> <CreditRing :score="85" :min="0" :max="100" :size="240" :stroke-width="14" :colors="colorConfig" /> </div> </template> <script setup> import CreditRing from './components/CreditRing.vue' const colorConfig = [ { threshold: 0, color: '#f56c6c' }, { threshold: 60, color: '#e6a23c' }, { threshold: 80, color: '#67c23a' } ] </script>

如果要做分数动态变化的场景(比如从接口拉数据后更新),直接改 score 就行,组件内部 watch 会自动触发动画。

4. 常见问题与排查技巧实录

4.1 圆环显示不全或被裁切

这是最高频的问题。表现是圆环边缘被容器切掉一块,或者圆头端点被截断。

根因通常是半径计算没考虑描边宽度。如果 r 取了 size/2,描边会有一半溢出容器。正确做法是r = (size - strokeWidth) / 2。另外stroke-linecap="round"会让端点向外延伸半个描边宽度,如果圆环刚好贴边,圆头也会被裁。解决办法是给 SVG 留点内边距,或者半径再减一点。

还有一种情况是父容器有overflow: hidden,而组件尺寸算大了。检查一下外层样式,必要时给组件加overflow: visible。

4.2 动画卡顿或跳变

动画不流畅,八成是这几个原因:

第一,动画过程中同时改了 dasharray 和 dashoffset。dasharray 变化会触发虚线模式重算,和 offset 动画叠加就乱了。记住:动画只改 offset,dasharray 保持固定。

第二,rAF 回调里做了重计算。比如每帧都重新算 circumference,虽然计算量不大,但能提到外面就提外面。我的做法是所有几何量都用 computed 缓存,rAF 里只做数值插值。

第三,duration 设得太短。低于 300ms 的动画人眼几乎看不清过程,反而显得突兀。信用分这种需要“仪式感”的展示,800-1200ms 比较合适。

4.3 分数边界值处理异常

传入 score 等于 min 或 max 时,圆环应该分别是空和满。但实际测试发现,score=min 时圆环可能还残留一小段,score=max 时圆头可能超出。

原因是 stroke-linecap="round" 的圆头有额外长度。当 offset 等于 arcLength 时,理论上实线段长度为 0,但圆头会画出一个点。解决办法是在 ratio 为 0 时把进度圆环隐藏(v-show 或 opacity),或者接受这个小圆点(有些设计反而喜欢这个效果)。

score=max 时圆头超出,是因为圆头半径是 strokeWidth/2,会超出弧长端点。如果视觉上不能接受,可以把 arcLength 稍微减一点,比如减掉 strokeWidth,给圆头留空间。

4.4 响应式下尺寸抖动

用 ResizeObserver 时,如果容器尺寸频繁变化(比如拖拽窗口),圆环会跟着抖动。这是因为每次 resize 都触发重算和可能的动画。

优化思路:加个防抖,resize 停止 100ms 后再更新尺寸。或者判断尺寸变化幅度,小于 2px 的忽略。另外,resize 时如果正在动画,先 cancel 掉,等尺寸稳定后再重新触发。

4.5 常见问题速查表

问题现象可能原因排查方法解决方案
圆环被裁切半径未减描边宽度检查 r 计算r=(size-strokeWidth)/2
圆环偏移不居中rotate 中心写错检查 transform 参数旋转中心用圆心坐标
动画跳变dasharray 参与动画检查动画属性只动 dashoffset
颜色不切换映射逻辑用了 find检查遍历逻辑用 for+break 取最后匹配
尺寸变化错乱未取消旧动画检查 resize 处理resize 前 cancel rAF
内存泄漏rAF/observer 未清理检查卸载钩子onBeforeUnmount 里清理
分数为 0 有残留圆头端点检查 linecapratio=0 时隐藏进度环

4.6 几个踩坑心得

第一个坑是关于transform的。SVG 的 rotate 和 CSS 的 rotate 参数顺序不一样,SVG 是rotate(angle cx cy),角度在前。我一开始按 CSS 习惯写成rotate(cx cy angle),结果圆环纹丝不动,排查了半天。

第二个坑是 Vue 的响应式。props 里的 colors 数组如果父组件直接改内部对象,子组件的 computed 可能不更新。因为 Vue 3 的响应式是浅层的,数组元素变化不一定触发。解决办法是让父组件整体替换数组,或者在组件内用 watch 深度监听。我一般建议业务方传新数组,别原地改。

第三个坑是 SSR 场景。如果项目用了服务端渲染,performance.now()和requestAnimationFrame在服务端不存在,会报错。要在 onMounted 里才调用动画逻辑,因为 onMounted 只在客户端执行。这个我吃过亏,页面直接白屏。

第四个坑是数字字体。分数用大字号显示时,如果字体不是等宽的,数字变化会导致宽度跳动,中心内容左右晃。解决办法是给 score-value 设font-variant-numeric: tabular-nums,让数字等宽。

5. 组件扩展与复用思路

5.1 支持多段颜色渐变圆环

有些设计要圆环本身是渐变的,不是单色。这个用 SVG 的 linearGradient 实现:

<defs> <linearGradient id="ringGradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#67c23a" /> <stop offset="100%" stop-color="#409eff" /> </linearGradient> </defs>

然后进度圆环的 stroke 用url(#ringGradient)。注意 gradient 的 id 要唯一,多个组件实例在同一页面会冲突,可以用useId或者随机字符串生成。

渐变圆环的视觉效果确实更高级,但有个问题:颜色不再和分数档位对应,用户没法通过颜色快速判断分数高低。所以渐变方案适合“展示型”场景,分档纯色适合“警示型”场景,看业务需求选。

5.2 开口角度可配置

虽然标题是 3/4 圆环,但实际项目里经常遇到要 2/3 或者半圆的需求。把开口角度做成 prop 不难:

const arcRatio = computed(() => props.arcRatio || 0.75) const arcLength = computed(() => arcRatio.value * circumference.value) const startAngle = computed(() => 90 + (360 * (1 - arcRatio.value)) / 2)

startAngle 的计算逻辑是:开口在底部居中,所以起点角度是 90 度加上半个开口角度。3/4 圆环开口 90 度,起点就是 90+45=135 度,和前面一致。这样改完,组件就能支持任意开口比例了。

5.3 无障碍与语义化

信用分是重要信息,无障碍不能忽略。给 SVG 加role="img"和aria-label:

<svg role="img" :aria-label="`信用分 ${score} 分,满分 ${max} 分`">

中心数字用<span>而不是纯文本,方便屏幕阅读器识别。如果分数有档位含义(如“优秀”“良好”),可以在 label 里体现。这些细节平时不起眼,但做企业级产品时是硬性要求。

5.4 性能优化建议

组件本身很轻,但如果一个页面放几十个信用分圆环(比如列表页),要注意几点:动画错开启动,避免同时几十个 rAF 在跑;ResizeObserver 可以多个组件共用一个实例;SVG 的 defs 里的 gradient 尽量复用。

实测下来,单页面 50 个圆环同时做入场动画,低端机上会掉帧。解决办法是列表项进入视口才启动动画,用 IntersectionObserver 控制。这个优化在数据大屏场景很实用。

我在实际项目里把这套组件沉淀成了团队的基础组件库,props 设计经过几轮迭代才稳定下来。最开始 colors 是写死三档的,后来业务方要四档、五档,才改成数组配置。size 一开始只支持数字,做移动端适配时发现百分比更灵活,又补了 ResizeObserver。这些演进过程说明一件事:组件设计要留扩展余地,别一上来就把逻辑写死。你现在如果照着这套代码落地,建议先把 colors 和 size 的灵活性保留住,后面改需求会省很多事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 12:42:15

RLCD反射式液晶显示技术全解析:无背光,靠环境光成像

RLCD&#xff0c;全称 Reflective Liquid Crystal Display&#xff0c;也就是反射式液晶显示&#xff0c;说直白点就是一块没有背光的液晶屏。它自己不发光&#xff0c;靠反射环境光成像&#xff0c;所以光线越强画面越清楚&#xff0c;观感和纸张非常接近。过去很多人觉得它只…

作者头像 李华
网站建设 2026/11/3 12:41:16

工业智能体操作系统:工厂AI落地的统一底座

这些年我在制造业里跑了不少项目&#xff0c;从汽车零部件、化工到电子装配都碰过。说实话&#xff0c;大多数工厂对AI的态度已经从“要不要上”变成了“怎么上”&#xff0c;但真正落地的时候&#xff0c;很多人发现事情没那么简单&#xff1a;买几个大模型API、接两个对话机器…

作者头像 李华
网站建设 2026/11/3 12:40:50

Chroma 1905x耐压测试仪手册拆解:接地、参数设置与故障排查

简介&#xff1a;致茂 Chroma 1905x 系列耐压测试仪使用手册&#xff0c;面向电气安全测试工程师、质检与设备维护人员&#xff0c;用于查阅19053、19054、19055等型号的操作规范、安全注意事项、保修条款及环保合规说明。压缩包共1个PDF文件&#xff0c;大小约3.11MB&#xff…

作者头像 李华
网站建设 2026/11/3 12:40:26

AI工程从零开始:生产级AI交付方法论

1. 这不是“搭个LLM API”——AI工程从零开始的真实含义 很多人看到“AI Engineering from Scratch”&#xff0c;第一反应是&#xff1a;不就是用LangChain写个RAG链、调个OpenAI接口、再套个Streamlit前端&#xff1f;——这叫AI应用开发&#xff0c;不是AI工程。真正的“fro…

作者头像 李华
网站建设 2026/11/3 12:40:18

VS2017封装C语言DLL的工程实践与跨语言调用指南

1. 为什么非得用VS2017封装C语言动态库&#xff1f;——不是版本怀旧&#xff0c;而是工程现实 很多人看到“VS2017”第一反应是&#xff1a;都2024年了&#xff0c;怎么还在用五年前的IDE&#xff1f;是不是过时了&#xff1f;其实恰恰相反——在工业控制、嵌入式上位机、老旧…

作者头像 李华
网站建设 2026/11/3 12:40:14

Odoo 16实施入门:从系统安装到采购销售闭环

简介&#xff1a;面向 Odoo 16 初学者与中小企业实施人员的中文实施指南&#xff0c;以开源智造的 GoodERP&#xff08;Odoo 中国版&#xff09;为参考样例&#xff0c;可帮助读者从零搭建并跑通一套可用的 ERP 系统。压缩包内含 1 个 PDF 文件&#xff0c;体积约 9.01MB&#…

作者头像 李华