
上个月给一个老后台系统加模块需求很简单首页要放一个半圆形的完成率仪表盘数据从接口拉刷新要顺滑。我习惯性地先搜了一圈jQuery插件结果不是体积太大就是样式死活套不进现有设计最后只能自己动手。折腾完我才意识到“jQuery画半圆”这个说法本身就容易把人带偏——真正负责“画”的是CSS和SVGjQuery在里面只是干调度和控制的活。这篇文章就把我这几天的完整实践拆开来讲把CSS方案和SVG方案从原理到实操彻底说透最后再用一个半圆进度条的实战案例把三者的分工串起来。不管你是刚接触前端的新人还是要在老项目里维护这种组件的老人应该都能找到能直接拿去用的东西。1. 为什么“用jQuery画半圆”本身是个伪命题很多人在后台系统里待久了遇到“画个图形”的第一反应就是“找个jQuery插件”。这个思路不能说错但很容易踩进坑里。我先说说jQuery的边界在哪里再说清楚CSS和SVG在这个任务里到底各自扮演什么角色。1.1 jQuery擅长什么不擅长什么jQuery最擅长的事情从来不是“画”而是“操作DOM”。选择器、事件绑定、Ajax请求、链式修改元素样式和属性这些是它的主场。你给它一个页面它能非常方便地找到元素、改内容、绑交互。但图形绘制本身不是它的能力范围它的底层没有Canvas、没有2D渲染上下文、没有任何跟像素打交道的东西。市面上那些所谓的jQuery画图插件本质上都是包装了Canvas或者SVGjQuery只是在外面做了一层“方便调用”的外壳。这就带来一个后果你为了画一个简单的半圆去引一个几十KB甚至上百KB的插件里面有大量你可能永远用不到的图表配置项最后渲染出来的效果还不一定符合你的设计稿。更麻烦的是这类插件很多已经停止维护在老项目里埋雷。1.2 在“画半圆”这个任务里三者应该怎么分工我的实践经验是这种任务应该按下面的思路拆静态的形状和视觉样式交给CSS复杂的路径和动态数据图形交给SVGjQuery只负责“在合适的时机把数据写进DOM和SVG属性”。CSS负责的是“形状感”的东西比如一个装饰性的半圆色块、一个半圆形的按钮背景、一个拱门的视觉样式。这类需求不涉及数据变化不需要路径计算用CSS圆角就能搞定。SVG负责的是“路径精确”的东西比如半圆环的进度条、仪表盘的刻度弧、需要跟随数据变化而改变角度的弧线。SVG的Path天然支持精确坐标而且作为DOM节点可以被JavaScript直接控制这正好是jQuery发挥作用的舞台。jQuery在这里面有点像一个施工现场的调度员不搬砖、不和水泥但它知道什么时候该让哪个工人上。具体来说它负责三件事测量容器尺寸、切换CSS类名、更新SVG属性。这三件事分别对应半圆组件落地时最常见的三个需求自适应、交互态切换、数据刷新。1.3 先按场景选路线再动手写代码我做这个半圆仪表盘之前先列了一张简单的判断表逼着自己不跳进“抄代码”的思维里场景推荐方案理由纯装饰半圆、静态色块CSS border-radius零JS开销浏览器渲染快需要描边、渐变、阴影的半圆轮廓SVG路径精确样式表达能力更强动态数据驱动的半圆弧线SVG jQuery数据变化时需要改Path或者stroke参数复杂交互动画、粒子效果Canvas本文不展开图形元素多、帧率高时才值得用Canvas这条决策线走完下面就开始动手。先看CSS方案再看SVG方案最后用jQuery把SVG方案串起来做一个能交付的组件。2. CSS实现半圆三套思路按场景选型CSS实现半圆的方案不止一种但很多人只会其中一种遇到特殊场景就抓瞎。我平时常用的有三套写法各自适合不同的场景。下面逐个拆开讲包括原理和常见的坑。2.1 border-radius切圆法最经典也最容易栽在圆角值上先看最经典的做法。半圆本质上是一个矩形让上面两个角变成“半圆轮廓”下面两个角保持直角。写起来很直观div classsemi-circle/div.semi-circle { width: 200px; height: 100px; background: #4a90d9; border-radius: 100px 100px 0 0; }这个代码的含义是一个宽200px、高100px的矩形左上角和右上角的圆角半径设为100px。因为矩形高度只有100px而圆角半径也是100px上面两个角就成了两个完美的半圆连起来就是上半圆。关键点就在于圆角半径要等于容器高度而不是漫无目的地写border-radius: 50%。我在代码评审里见过太多人写border-radius: 50%结果椭圆不是椭圆、半圆不是半圆整个形状像个鸡蛋。border-radius: 50%这个写法有一个常见的误解百分比是相对元素自身宽高的。对一个宽200px、高100px的元素来说50%圆角的水平半径是100px、垂直半径是50px两个半径不相等渲染出来是椭圆角不是正圆的四分之一。只有宽高相等的正方形border-radius: 50%才能裁出正圆。所以做半圆的时候要用具体的px值去控制圆角半径并且这个值必须等于高度。如果要做下半圆就把圆角反过来.semi-circle-bottom { width: 200px; height: 100px; background: #4a90d9; border-radius: 0 0 100px 100px; }这个方案的优点是简单直观缺点是样式比较“死”它填充的是一个实心半圆想在里面放文字得另加定位想做出描边半圆要用border属性而不是background并且要把下方边框去掉.semi-circle-outline { width: 200px; height: 100px; border: 10px solid #4a90d9; border-bottom: none; border-radius: 110px 110px 0 0; }注意这里的圆角半径变成了110px因为border-radius的半径是相对于border盒子的外边缘计算的元素本身宽200px加上左右各10px border之后外宽是220px上面两个角如果还是100px圆弧就不够饱满。你实际测试时可能发现110px也未必完全贴合因为圆角半径的计算起点在不同浏览器里有细微差异。这个只能靠肉眼微调。我的建议是凡是要做描边半圆优先考虑SVG别在CSS的border上跟自己较劲后面第三章会讲为什么。2.2 拆盒法利用border-bottom做扁半圆和拱形第二种方案是“拆盒法”它的核心思路是把元素的某一条边框当成弧线来画。比如一个元素高100px、宽200px给它设置下边框和上下圆角就能得到一个拱形div classarch/div.arch { width: 200px; height: 100px; border-bottom: 24px solid #f5a623; border-radius: 100px 100px 0 0; }这个写法的视觉效果是下半部分是背景色这里没设置背景默认透明底部有一条24px粗的边框边框上面两个角被圆角半径“顶”成了拱形弧线。它和border-radius切圆法的区别在于它只描了边没有填充整个半圆。这种形状非常适合做拱门、分割线、仪表盘下方的那条“弧线装饰”。拆盒法还有一个变体可以做出扁半圆半椭圆.flat-semi { width: 240px; height: 90px; background: #50b96a; border-radius: 240px 240px 0 0 / 90px 90px 0 0; }这里用了border-radius的斜杠语法斜杠前面是水平半径斜杠后面是垂直半径。水平半径设为宽度的一半垂直半径设为高度渲染出来就是一个标准的半椭圆。有些UI场景需要这种扁半圆来做数据可视化装饰比正半圆更柔和。拆盒法的坑在于视觉效果依赖border-width和border-radius的配合这两个值不是随便写的。比如border-bottom: 24px配合border-radius: 100px圆弧的曲率中心会随着边框宽度发生偏移你调完在普通屏幕上看刚好换一个缩放比例又可能胖一圈。所以拆盒法我一般只用于不追求像素级精确的装饰性元素。2.3 clip-path裁切法最灵活但注意裁剪边界第三种方案是用clip-path它跟前面两种思路完全不同先准备一个方形或圆形元素然后按半圆的几何边界去裁剪它。CSS的clip-path: circle()可以指定圆心和半径天然适合做半圆裁剪。div classclipped-semi/div.clipped-semi { width: 200px; height: 200px; background: linear-gradient(180deg, #9b59b6 0%, #6c3483 100%); clip-path: circle(100px at 100px 0); }这段代码的意思是以一个200px乘200px的元素为基础圆心的位置在(100px, 0)也就是元素上边缘的中点半径100px。裁出来之后下半部分全部被裁掉只保留上半圆。这个方案最大的好处是裁剪不改变元素本身的盒模型你可以在里面继续放文本、放图标甚至放一个独立的子元素来分别控制样式。clip-path方案还有一个很实用的延伸配合CSS动画做“涟漪扩散”效果。很多人搜过的“css涟漪光圈扩散”有一种实现方式就是多个半圆或圆形元素叠加用clip-path控制初始形状再通过animation放大和淡出。虽然完整涟漪是用整圆做的但思路是一样的——裁剪区域 变换 透明度可以组合出很多看似复杂的形状。这个方案的坑主要在兼容性。比较老的浏览器比如IE完全不支持clip-path: circle()部分老版Chrome需要加-webkit-前缀。另外clip-path裁掉的区域不会响应鼠标事件如果你需要用户点击半圆内部触发交互得在裁剪区域之外补一个透明的、覆盖半圆范围的点击层或者干脆用SVG。还有一个细节clip-path: circle()的百分比半径计算方式跟border-radius不一样它是基于元素的对角线来计算的容易产生“半径到底有多长”的困惑。我的建议是写固定px值位置也写px这样在各个浏览器里表现更稳定。2.4 渐变、阴影这些“半圆美化”怎么补CSS半圆不是只能填纯色。背景渐变可以直接写在background上跟border-radius方案完全兼容.gradient-semi { width: 200px; height: 100px; background: linear-gradient(90deg, #ff6b6b, #ee5a24); border-radius: 100px 100px 0 0; }但阴影有一个大坑box-shadow是跟着盒模型的矩形走的。一个宽200px、高100px、上面是圆弧的梯形你给它加box-shadow: 0 10px 20px rgba(0,0,0,0.3)阴影会从下面直着延伸出去底部是一条直线而不是圆弧看起来像是在圆拱下面挂了块板子。想要阴影贴合半圆的形状应该用filter: drop-shadow().gradient-semi-shadow { width: 200px; height: 100px; background: linear-gradient(90deg, #ff6b6b, #ee5a24); border-radius: 100px 100px 0 0; filter: drop-shadow(0 8px 12px rgba(0, 0, 0, 0.25)); }drop-shadow是按元素实际渲染出来的像素形状去计算阴影的圆弧的部分会得到圆滑的阴影底部直角的部分也是直角阴影非常贴合。这个技巧代价是filter本身比box-shadow更消耗性能如果页面上有成百上千个带阴影的半圆要注意别滥用。到这里CSS方案大概够用了。但如果你要做的不是装饰色块而是能跟随数据变化的半圆弧那CSS就有点力不从心了——圆弧的起止点、弧度大小、描边宽度这些都是动态的CSS写死一套样式无法应对。这时候就该SVG上场了。3. SVG实现半圆的核心Path弧线参数绕不开SVG实现半圆的入门姿势很多有人用circle元素加clip-path有人直接写path。我的建议是把path的弧线命令彻底搞懂因为半圆只是圆弧的一个特例你迟早会遇到“画一个从30度到150度的弧”这种需求。到了那种场景circle clip-path做不了只能靠path。3.1 SVG坐标系统y轴向下的世界视觉顺时针才是正方向先建立坐标系的概念。SVG的坐标原点在左上角x轴向右y轴向下。跟我们在数学课上学到的直角坐标系y轴向上相比y轴方向是反的。这个反掉的y轴引发了一个特别常见的混乱角度方向。在数学坐标系里从x轴正方向逆时针旋转是正角度但在SVG这种y向下的坐标系里正角度方向变成了视觉上的顺时针。也就是说SVG中的sweep-flag 1表示“正角度方向”画出来是顺时针sweep-flag 0表示负角度方向画出来是逆时针。我之前见过不少人在这一步栽跟头明明想画上半圆结果画出来一个下半圆以为是终点坐标写错了其实是sweep-flag这个方向位搞反了。下面用一个例子把这块彻底讲明白。3.2 A命令拆解六个参数逐个说清path里的圆弧命令是A也可以小写a区别在于大写用绝对坐标、小写用相对坐标。完整的语法长这样A rx ry x-axis-rotation large-arc-flag sweep-flag x y六个参数分别是rx和ry椭圆弧在x轴和y轴方向的半径。画正圆弧时rx ry。x-axis-rotation椭圆弧整体旋转的角度。对半圆来说一般写0。large-arc-flag取0还是1。两个点之间用给定半径可以画出两条弧一条大于180度一条小于180度。取1表示画大弧取0表示画小弧。恰好半圆时两条弧长度相同取0还是1视觉结果一样但规范上建议按“是否超过180度”来算。sweep-flag方向位取0或1。1表示沿正角度方向视觉顺时针0表示沿负角度方向视觉逆时针。x和y终点坐标。起点由当前画笔位置决定一般紧跟着一个M命令来设置。这一堆参数光看定义记不住。我自己的记忆方法是把起点和终点想象成钟表上的两个刻度起点是钟表指针站的位置终点是它要走到哪里sweep-flag 1就按顺时针方向从起点走到终点sweep-flag 0就按逆时针方向走。3.3 一个上半圆的手算过程以我之前画的半圆为例起点(30, 100)终点(170, 100)半径70圆心自然就是(100, 100)。画上半圆的path是这样svg viewBox0 0 200 120 xmlnshttp://www.w3.org/2000/svg path dM 30 100 A 70 70 0 0 1 170 100 fillnone stroke#4a90d9 stroke-width10 / /svg我们来逐一对应M 30 100把画笔移动到起点A 70 70 0 0 1 170 100表示以半径70画弧不旋转取小弧sweep-flag 1终点是(170, 100)。现在用钟表思维验证方向起点(30, 100)在圆心(100, 100)的正左方相当于钟表的9点位置终点(170, 100)是正右方相当于3点位置。从9点沿顺时针走到3点会经过12点方向也就是屏幕正上方(100, 30)所以画出来是上凸的半圆弧。如果我把sweep-flag改成0从9点沿逆时针走到3点会经过6点方向也就是屏幕正下方(100, 170)画出来就是下半圆弧。这个例子能帮你把坐标和方向一次性理清。这里还要提醒一个新手常踩的坑SVG的path默认是闭合的实心图形。如果你写了一句path dM 30 100 A 70 70 0 0 1 170 100不写fillnone浏览器会自动补一条从终点回到起点的线然后填充内部区域你会看到一个类似“吃豆人”的扇形。画线条的时候一定记得写fillnone要的只是描边效果就用stroke来控制。3.4 任意角度的弧手写一个Path生成函数实际项目中很少会每次手算Path更多时候是给一个圆心、半径和起止角度让程序自动生成。我自己写一个辅助函数function getArcPath(cx, cy, r, startAngle, endAngle) { // 约定0度在12点方向角度顺时针增加符合视觉直觉 var startRad ((startAngle - 90) * Math.PI) / 180; var endRad ((endAngle - 90) * Math.PI) / 180; var x1 cx r * Math.cos(startRad); var y1 cy r * Math.sin(startRad); var x2 cx r * Math.cos(endRad); var y2 cy r * Math.sin(endRad); var largeArc endAngle - startAngle 180 ? 1 : 0; var sweep 1; // 视觉顺时针 return M x1 y1 A r r 0 largeArc sweep x2 y2; }角度规则是0度在12点方向90度在3点方向顺时针增加。这个约定跟钟表一致写业务逻辑的时候非常顺手。函数内部通过(角度 - 90)把12点方向转换成数学上的标准0度方向然后换算成弧度、算终点的x和y坐标。用这个函数画一个“从0度到180度”的上半圆var d getArcPath(100, 100, 70, 0, 180); $(#arc).attr(d, d);得到的d属性就是M 30 100 A 70 70 0 0 1 170 100跟我手写的完全一致。以后要画“从30度到120度”的弧只需要改角度参数坐标和sweep-flag都不用管。3.5 半圆环可以更简单radius stroke-dasharray方案如果你要的不是一条实线弧而是半圆环进度条还有一条更简单的路直接用circle元素然后用stroke-dasharray和stroke-dashoffset控制可见部分的长度。虽然画的是整圆但通过进度控制只显示其中一段在很多场景下比手动算Path更方便。svg viewBox0 0 200 120 xmlnshttp://www.w3.org/2000/svg circle cx100 cy100 r70 fillnone stroke#e6e9ed stroke-width12 / circle idprogress cx100 cy100 r70 fillnone stroke#4a90d9 stroke-width12 stroke-linecapround stroke-dasharray219.9 stroke-dashoffset219.9 transformrotate(180 100 100) / /svg解释一下第一个circle是底环第二个是进度环。stroke-dasharray设为圆弧周长半径为70的半圆弧周长 π × 70 ≈ 219.9stroke-dashoffset初始设为同样的值表示偏移掉全部长度进度为0。将circle旋转180度是为了让进度从左侧开始、往右侧增长。然后通过JavaScript把stroke-dashoffset改小进度环就会逐渐显示出来。如果我只想显示半圈的进度为什么用完整circle而不是直接画半圆Path因为circle计算周长简单而且进度动画只需要改一个属性不用重新生成Path。但有一个代价circle画的是整圆你最终看到的那“一段”到底是不是严格的半圆要看你把进度控制在什么区间进度条两端还会因为stroke-linecap: round多出半截圆头。所以这个方案适合“半圆环 动态进度”的组合如果只画静态半圆直接用Path更精确。4. jQuery在两条技术路线里的角色不是画手而是调度前面说了jQuery在这个任务里不负责“画”。那它到底怎么用这一章讲我在这个半圆仪表盘项目里实际用到的几个jQuery能力每一块都对应一种真实需求。4.1 用jQuery拿容器尺寸做自适应半圆CSS方案和SVG方案都有一个共同问题容器宽度可能不是固定的。老后台系统的左侧菜单栏伸缩、右侧面板宽度变化都会影响半圆大小。如果写死200px宽一刷新就变形。CSS方案的自适应可以用百分比宽度但高度没法跟着宽度走CSS里高度百分比不依赖宽度。SVG方案好处理一些因为可以用viewBox统一描述坐标系但如果你希望图形的物理大小跟随容器宽度变化还是要动态调整viewBox的宽度或者图形的最大尺寸。我用得最多的jQuery尺寸方法是outerWidth()。它返回元素包含padding和border的完整宽度配合resize事件可以做一个自适应更新function resizeSemi() { var width $(#semi-wrapper).outerWidth(); var height width / 2; $(#semi-wrapper).css(height, height px); // 如果使用SVG可以在这一步更新viewBox或半径 $(#semi-svg).attr(viewBox, 0 0 width (width / 2)); } $(window).on(resize, function() { clearTimeout(window.__resizeTimer); window.__resizeTimer setTimeout(resizeSemi, 200); }); resizeSemi();resize事件加了一个简单的防抖200毫秒内连续触发只执行一次。这种写法在老的jQuery项目里非常实用不需要引入额外工具库。4.2 用jQuery切换class控制CSS半圆的交互态CSS半圆本身不带交互但半圆作为UI组件经常需要“选中态”“高亮态”“禁用态”的切换。朴素写法是直接改style但那样样式和逻辑混在一起维护性很差。更好的方式是用class管理状态// 假设有多个半圆卡片点击一个点亮它熄灭其他 $(.semi-card).on(click, function() { $(this) .addClass(is-active) .siblings() .removeClass(is-active); });.siblings()会选中当前元素的所有兄弟节点然后统一移除is-active类。这比遍历所有.semi-card逐个判断简单得多。如果你要精确选中第一个半圆可以用:first-child选择器如果半圆上有name属性也可以按name取对象这些都是jQuery选择器的基本功但在组件里非常管用。CSS侧只需要定义好.semi-card { opacity: 0.6; transition: opacity 0.3s ease; } .semi-card.is-active { opacity: 1; }这种“用类名表达状态”的做法比直接操作颜色值好调得多。以后设计稿改色只动CSS不动JavaScript。4.3 用jQuery更新SVG属性驱动数据变化SVG图形是DOM节点jQuery操作DOM的老本行在这里派上了大用场。两个常用方法.attr()和.css()。.attr()主要用来更新SVG的路径参数和形状属性比如直接改Path的d属性var newD getArcPath(100, 100, 70, 0, currentPercent * 1.8); $(#dynamic-arc).attr(d, newD);.css()则用来更新stroke相关的样式可以实现平滑过渡。这里有一个细节stroke-dashoffset既是SVG属性也是CSS属性。用.attr(stroke-dashoffset, value)设置时很多浏览器不会触发CSS的transition过渡改用.css(stroke-dashoffset, value)设置内联样式过渡就正常了。所以我的习惯是描边动画相关的都用.css()Path数据相关的用.attr()。来看一个更新进度环的例子var $bar $(#progress); var fullLength 219.9; function setProgress(percent) { var offset fullLength * (1 - percent / 100); $bar.css(stroke-dashoffset, offset); $(#percent-text).text(Math.round(percent) %); } setProgress(72);配合CSS#progress { transition: stroke-dashoffset 0.6s ease; }进度从旧值跳到新值时stroke-dashoffset会从当前值平滑动画到目标值。我第一次跑通这个效果时还挺意外没想到SVG的描边属性在老式jQuery项目里也能这么顺滑地做动画。4.4 与Ajax数据联动数据回来再更新图形实际业务里百分比不是写死的要从接口拿。jQuery的$.ajax在这里承担数据拉取任务$.ajax({ url: /api/completion-rate, method: GET, dataType: json }).done(function(res) { if (res typeof res.percent number) { setProgress(res.percent); } else { setProgress(0); $(#percent-text).text(--); } }).fail(function() { setProgress(0); $(#percent-text).text(--); });接口失败时不要把页面搞成一个报错大弹窗直接把进度归零、百分比文本显示为--用户至少知道这个区域“暂时没有有效数据”。这是很多后台组件做不好的细节。5. 实战案例一个半圆形完成率仪表盘理论讲了三大板块现在把完整代码串起来做一个半圆形的完成率仪表盘。这个案例我会直接给出可以在页面里跑起来的代码并把每一块的取舍讲清楚。5.1 需求拆解和技术选型需求是页面头部展示一个“本月任务完成率”的半圆仪表盘数值从接口获取要求刷新时有平滑动画容器宽度变化时自适应。选型结论外层用SVG的circlestroke-dasharray方案做半圆环因为数据是动态的、描边动画要顺滑。底环和进度环都用circle进度环加transformrotate(180 100 100)让起点从左下角开始。jQuery负责获取容器宽度、刷新百分比、监听resize。CSS负责transition过渡和字体样式。5.2 搭建SVG骨架先写HTML结构div idgauge-wrapper svg idgauge viewBox0 0 200 120 xmlnshttp://www.w3.org/2000/svg circle cx100 cy100 r70 fillnone stroke#e6e9ed stroke-width12 / circle idprogress cx100 cy100 r70 fillnone stroke#4a90d9 stroke-width12 stroke-linecapround stroke-dasharray219.9 stroke-dashoffset219.9 transformrotate(180 100 100) / text idpercent-text x100 y80 text-anchormiddle font-familyArial, sans-serif font-size28 font-weightbold fill#3330%/text /svg /div这里有几个细节值得解释viewBox0 0 200 120给了一个宽200、高120的坐标系。半径70的圆圆心在(100, 100)圆会向下延伸到底部边界之外但半圆部分落在120高度的可视范围内刚好呈现上凸半圆。底环stroke#e6e9ed浅灰色代表“总量”。进度环初始stroke-dashoffset219.9等于半圆弧周长表示进度为0。stroke-linecapround会让进度环两端圆润视觉更友好但也会让端点略微超出理论弧长。如果你后面发现动画最后那一点点“串”出去了可以考虑把fullLength微调小一点或者改用stroke-linecapbutt。5.3 CSS控制过渡和容器自适应#gauge-wrapper { width: 100%; max-width: 300px; margin: 0 auto; } #gauge { display: block; width: 100%; height: auto; } #progress { transition: stroke-dashoffset 0.6s ease; }#gauge { width: 100%; height: auto; }是SVG自适应的关键。因为我设置过viewBoxSVG的宽高比是固定的宽度一撑开高度会按比例自动调整。放在#gauge-wrapper这个宽度容器里就实现了跟随父容器宽度自适应。父容器限制max-width: 300px是为了防止半圆在宽屏上太大。5.4 jQuery初始化与刷新逻辑JavaScript部分$(function() { var $progress $(#progress); var $percentText $(#percent-text); var fullLength 219.9; function setProgress(percent) { var safePercent Math.max(0, Math.min(100, percent)); var offset fullLength * (1 - safePercent / 100); $progress.css(stroke-dashoffset, offset); $percentText.text(Math.round(safePercent) %); } // 模拟接口数据实际项目换成$.ajax function loadData() { // 此处模拟随机数据便于演示 var randomPercent 35 Math.floor(Math.random() * 60); setProgress(randomPercent); } // 自适应容器尺寸重设viewBox或半径简单场景下viewBox固定即可 function resizeGauge() { var wrapperWidth $(#gauge-wrapper).outerWidth(); // 如果需要严格等比例缩放viewBox不需要改这里留给读者按需使用 } $(window).on(resize, function() { clearTimeout(window.__gaugeResizeTimer); window.__gaugeResizeTimer setTimeout(resizeGauge, 200); }); loadData(); });这里我把setProgress写成了一个独立函数好处是后面接真实接口时只需要在loadData里把$.ajax的返回值传进来调用处不用动。5.5 进度更新动画的两个注意点第一CSStransition要写在初始状态上。我在#progress上直接写了transition: stroke-dashoffset 0.6s ease意味着任何一次stroke-dashoffset变化都会触发动画。如果数据每隔几秒刷新一次动画每次都会播这不一定是你想要的。如果只希望首次加载动画而后续刷新静默更新可以改成这样// 首次加载后移除过渡避免每次都播放动画 $progress.css(transition, none);第二用.css()而不是.attr()去更新stroke-dashoffset。前面已经解释过.attr()在很多浏览器里不会触发CSS过渡动画会变成“瞬移”。这种细节不实际跑一遍很难发现。5.6 进阶多数据源动态更新一个仪表盘往往不止一个半圆。比如“销售目标完成率”“库存周转率”并列展示每个半圆都要独立绑定数据。这里我把组件抽成函数function initGauge(svgSelector, textSelector) { var $svg $(svgSelector); var $progress $svg.find(.progress); var $text $svg.find(.percent-text); var fullLength 219.9; return { setProgress: function(percent) { var offset fullLength * (1 - percent / 100); $progress.css(stroke-dashoffset, offset); $text.text(Math.round(percent) %); } }; } var salesGauge initGauge(#sales-gauge, #sales-text); var stockGauge initGauge(#stock-gauge, #stock-text); salesGauge.setProgress(86); stockGauge.setProgress(42);多个半圆互不干扰这是我在实际项目里比较得意的写法。老系统里如果再遇到类似的仪表盘需求直接复用这个initGauge就可以。6. 绕不开的布局、对齐和兼容性坑半圆图形画出来了不等于组件能直接用。布局、对齐、兼容性这些边角问题往往才是验收时被提bug的重灾区。我把这段时间实测踩过的坑集中整理在这里。6.1 SVG元素的对齐baseline问题比你想的麻烦svg作为行内元素时默认是按文本基线对齐的。你把它放在一个高度不定的容器里经常会出现底部多出几像素空白或者SVG跟旁边的文字错位。最省心的解决办法是让SVG变成块级元素#gauge { display: block; }如果它要和旁边的文本排在一行更推荐用flex容器统一对齐.gauge-wrapper { display: flex; align-items: center; gap: 12px; }flex会把SVG和文本按中心对齐视觉上最稳。我很少用vertical-align: middle去微调SVG因为不同字体、不同字号下基线位置的差异很大调完这个浏览器换个浏览器又歪了。6.2 半圆边缘的毛边和高清屏显示CSS半圆在普通屏幕上看着还行但放到高清屏或放大缩小的场景里圆弧边缘容易出现锯齿感尤其是用border-radius做出来的半圆配合filter阴影时。SVG在这方面有天生的优势默认抗锯齿弧线边缘非常平滑。如果CSS半圆在放大时发虚可以做一个散列技巧给元素加transform: translateZ(0)或者will-change: transform把它提升到自己的合成层不少情况下能改善渲染。但这个方法不是万能的如果仍然发虚说明这种形状就不适合CSS硬扛换成SVG是更靠谱的方案。6.3 无障碍和文本可读性SVG图形对屏幕阅读器来说默认是不可读的。半圆仪表盘的数值视觉上在中心显示了但读屏用户可能只能听到一个没有意义的“圆形”。我做的半圆仪表盘加了这些无障碍补充svg idgauge roleimg aria-label本月任务完成率当前72% !-- ... -- /svgroleimg告诉读屏软件这是一个独立图形aria-label直接把关键信息暴露给读屏用户。这个做法成本极低但对无障碍体验提升很大。另外如果页面里有text节点显示百分比要注意SVG里的文本跟HTML文本不一样它不会被普通CSS的字体选择器命中需要单独用font-family、font-size等SVG属性去控制或者通过CSS属性选择器写[class^gauge-text]这类规则。6.4 兼容性老浏览器和跨端复用CSS半圆的老兼容性其实比很多人想象的好border-radius从IE9开始就没问题。但如果用的是clip-path: circle()要注意加-webkit-前缀以覆盖旧版Chrome和Safari。SVG在老IEIE8及以下完全不支持这种环境建议直接用CSS做静态半圆降级样式丑一点没关系至少内容能看。IE9到IE11对SVG基本可用stroke-dashoffset动画也勉强能跑但过渡效果要打折做好心理预期。另外如果你所在团队有用Compose开发移动端的趋势SVG图形其实是很好的素材中间格式很多工具链都能把SVG转换成Compose的ImageVector。这意味着一个半圆仪表盘的视觉稿放在Web端是SVG将来迁移到移动端时转换成本也低。我在做这个仪表盘的时候特意保留了SVG源文件就是这个原因。最后再补一句实操体会这次做半圆仪表盘我最大的收获是重新理解了工具选择的优先级能用CSS写出来的静态形状就不要引入JS能用SVG表达的数据图形就不要一上来就搬Canvas。jQuery在2024年之后虽然不再是新项目的第一选择但在存量系统里它依然是连接数据和界面的称手工具。我在排错过程中还有一个习惯动手写Path之前先在纸上画一个坐标系把起点、终点、圆心标注出来再决定sweep-flag和large-arc-flag。这个习惯帮我少走了很多弯路也分享给正在跟圆弧命令搏斗的你。