
本文详解如何使用 SVG + CSS + JavaScript 构建可精确控制完成度的圆形进度条,支持自定义目标值、颜色与动画,并修复原代码中 stroke-dashoffset 计算错误、DOM 查询范围混乱及旋转方向不一致等核心问题。
本文详解如何使用 svg + css + javascript 构建可精确控制完成度的圆形进度条,支持自定义目标值、颜色与动画,并修复原代码中 `stroke-dashoffset` 计算错误、dom 查询范围混乱及旋转方向不一致等核心问题。
要实现一个真正按设定数值(如 75/100 或 135/200)精准填充的 SVG 圆形进度条,关键在于正确理解 SVG 圆形路径的绘制原理与 stroke-dasharray / stroke-dashoffset 的协同机制。
✅ 核心原理说明
- SVG 圆的周长 =
2 × π × r。示例中r = 135→ 周长 ≈2 × 3.1416 × 135 ≈ 848.23,因此stroke-dasharray: 848是合理近似。 -
stroke-dasharray: 848表示“实线长度 = 848,间隙 = 0”,即整圈为单段实线。 -
stroke-dashoffset控制该实线起始位置的偏移量:值越大,未填充部分越长;当offset = 0时,圆完全填满(从起点开始画满整圈);当offset = 848时,圆完全为空(起点被偏移到终点,无可见绘制)。 - 因此,填充百分比 =
(targetDegree / maxDegree) × 100%,对应offset = circumference × (1 − percentage)。
⚠️ 原代码错误点:
percentage = degree / targetDegree后误用progressCalc = 848 - (848 * targetDegree) / 100(恒定计算,未随degree变化);- 使用
conic-gradient覆盖 SVG 原生描边,失去矢量精度与性能优势;document.querySelector('.text')全局查询,导致多个进度条时仅操作第一个;svg { transform: rotate(90deg) }与动画方向不匹配,应统一为-90deg实现顺时针填充。
✅ 正确实现代码(含结构优化)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SVG Circular Progress Bar</title>
<style>
body {
margin: 0;
height: 100vh;
background: #191932;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Poppins', sans-serif;
}
.progress-container {
position: relative;
width: 400px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.progress-ring {
transform: rotate(-90deg); /* 关键:使0°在顶部,顺时针增长 */
}
.progress-ring circle {
fill: none;
stroke-linecap: round;
}
.progress-ring .bg-circle {
stroke: #f0f8ff;
stroke-width: 12px;
}
.progress-ring .fill-circle {
stroke: #0b60e9;
stroke-width: 16px;
stroke-dasharray: 848; /* 周长近似值 */
stroke-dashoffset: 848; /* 初始全空 */
transition: stroke-dashoffset 0.3s ease-out; /* 平滑动画 */
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #4e5bcd;
font-size: 60px;
font-weight: 500;
line-height: 1;
}
.progress-text span {
display: block;
font-size: 14px;
margin-top: 8px;
color: #4e5bcd;
}
</style>
</head>
<body>
<div class="progress-container">
<svg class="progress-ring" data-degree="75" data-color="#0b60e9" width="300" height="300" viewBox="0 0 300 300">
<circle class="bg-circle" cx="150" cy="150" r="135" />
<circle class="fill-circle" cx="150" cy="150" r="135" />
</svg>
<div class="progress-text">
<span class="progress-value">0</span>
<span>Completed</span>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const progressBars = document.querySelectorAll('.progress-ring');
const circumference = 848; // 2 * Math.PI * 135 ≈ 848.23
progressBars.forEach(bar => {
const targetDegree = parseInt(bar.getAttribute('data-degree')) || 0;
const color = bar.getAttribute('data-color') || '#0b60e9';
const fillCircle = bar.querySelector('.fill-circle');
const valueEl = bar.closest('.progress-container').querySelector('.progress-value');
// 初始化样式
fillCircle.style.stroke = color;
// 动态更新函数(支持任意目标值,不限于100)
const animateProgress = () => {
let current = 0;
const increment = targetDegree > 100 ? Math.ceil(targetDegree / 100) : 1;
const duration = 1000;
const steps = 100;
const interval = Math.floor(duration / steps);
const timer = setInterval(() => {
current += increment;
if (current >= targetDegree) {
current = targetDegree;
clearInterval(timer);
}
const percentage = current / targetDegree;
const offset = circumference - (circumference * percentage);
fillCircle.style.strokeDashoffset = offset;
valueEl.textContent = Math.round(current);
}, interval);
};
animateProgress();
});
});
</script>
</body>
</html>? 关键改进点总结
-
精准数学映射:
offset = circumference × (1 − current/target)确保任意targetDegree(如135、200)均可准确填充; -
模块化 DOM 操作:每个进度条独立查找其子元素(
.fill-circle,.progress-value),避免多实例冲突; -
CSS 驱动平滑过渡:
transition: stroke-dashoffset 0.3s ease-out替代 JS 逐帧设置,性能更优; -
语义化结构:分离 SVG 容器(
.progress-container)、环形图(.progress-ring)与文本层,便于复用与响应式扩展; -
健壮性增强:添加
|| 0和|| '#0b60e9'默认值,防止属性缺失报错。
? 进阶提示:若需支持「百分比输入」(如
data-degree="75%")或「双刻度显示」(如75/100),可在valueEl.textContent中扩展逻辑,例如:const maxVal = parseInt(bar.getAttribute('data-max')) || 100; valueEl.innerHTML = `${Math.round(current)}<span>/${maxVal}</span>`;
通过以上实现,你将获得一个轻量、可配置、高性能且符合 W3C 标准的 SVG 圆形进度条,完美适配数据仪表盘、加载状态、技能展示等多种场景。

















