实现一个交互式的圆形进度条,包含动画效果和用户交互功能。
完整实现代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>交互式圆形进度条</title>
<style>
body {
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
}
.progress-container {
text-align: center;
position: relative;
}
.progress-circle {
width: 200px;
height: 200px;
position: relative;
}
.progress-bg {
fill: none;
stroke: rgba(255, 255, 255, 0.2);
stroke-width: 12;
}
.progress-fill {
fill: none;
stroke: #ffffff;
stroke-width: 12;
stroke-linecap: round;
transition: stroke-dasharray 0.3s ease;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
font-weight: bold;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.controls {
margin-top: 30px;
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 25px;
background: rgba(255, 255, 255, 0.2);
color: white;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
}
.btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
.range-control {
margin-top: 20px;
width: 80%;
max-width: 300px;
}
.range-control input {
width: 100%;
height: 6px;
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
outline: none;
}
.range-control input::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.range-control input::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.pulse-animation {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.success {
stroke: #4CAF50 !important;
animation: success-pulse 0.5s ease-in-out;
}
@keyframes success-pulse {
0% { stroke-width: 12; }
50% { stroke-width: 16; }
100% { stroke-width: 12; }
}
.warning {
stroke: #FFC107 !important;
}
.danger {
stroke: #F44336 !important;
}
</style>
</head>
<body>
<div class="progress-container">
<div class="progress-circle">
<svg width="200" height="200" viewBox="0 0 200 200">
<circle class="progress-bg" cx="100" cy="100" r="80" />
<circle class="progress-fill" cx="100" cy="100" r="80" />
</svg>
<div class="progress-text">0%</div>
</div>
<div class="range-control">
<input type="range" min="0" max="100" value="0" step="1">
</div>
<div class="controls">
<button class="btn" id="btn-25">25%</button>
<button class="btn" id="btn-50">50%</button>
<button class="btn" id="btn-75">75%</button>
<button class="btn" id="btn-100">100%</button>
<button class="btn" id="btn-reset">重置</button>
<button class="btn" id="btn-animate">动画演示</button>
</div>
</div>
<script>
class CircularProgressBar {
constructor() {
this.progress = 0;
this.maxProgress = 100;
this.radius = 80;
this.circumference = 2 * Math.PI * this.radius;
this.animationId = null;
this.initElements();
this.initEvents();
this.updateProgress(0);
}
initElements() {
this.svg = document.querySelector('svg');
this.progressFill = document.querySelector('.progress-fill');
this.progressText = document.querySelector('.progress-text');
this.rangeInput = document.querySelector('.range-control input');
this.buttons = {
'25': document.getElementById('btn-25'),
'50': document.getElementById('btn-50'),
'75': document.getElementById('btn-75'),
'100': document.getElementById('btn-100'),
'reset': document.getElementById('btn-reset'),
'animate': document.getElementById('btn-animate')
};
}
initEvents() {
// 滑块事件
this.rangeInput.addEventListener('input', (e) => {
this.updateProgress(parseInt(e.target.value));
});
// 按钮事件
this.buttons['25'].addEventListener('click', () => this.setPercentage(25));
this.buttons['50'].addEventListener('click', () => this.setPercentage(50));
this.buttons['75'].addEventListener('click', () => this.setPercentage(75));
this.buttons['100'].addEventListener('click', () => this.setPercentage(100));
this.buttons['reset'].addEventListener('click', () => this.reset());
this.buttons['animate'].addEventListener('click', () => this.animateDemo());
// 点击圆圈事件
this.svg.addEventListener('click', (e) => {
this.handleCircleClick(e);
});
}
updateProgress(percentage) {
if (percentage < 0) percentage = 0;
if (percentage > 100) percentage = 100;
this.progress = percentage;
this.rangeInput.value = percentage;
this.progressText.textContent = `${Math.round(percentage)}%`;
// 计算stroke-dasharray
const dashArray = (percentage / 100) * this.circumference;
this.progressFill.style.strokeDasharray = `${dashArray} ${this.circumference}`;
// 旋转圆圈以从顶部开始
this.progressFill.style.transform = 'rotate(-90deg)';
this.progressFill.style.transformOrigin = 'center';
// 根据进度改变颜色
this.updateProgressColor();
// 触发自定义事件
this.dispatchEvent('progressChange', { percentage });
}
setPercentage(percentage) {
this.updateProgress(percentage);
}
reset() {
this.animateTo(0);
}
animateTo(targetPercentage, duration = 1000) {
// 取消之前的动画
if (this.animationId) {
cancelAnimationFrame(this.animationId);
}
const startPercentage = this.progress;
const startTime = performance.now();
const change = targetPercentage - startPercentage;
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用缓动函数
const easeProgress = this.easeOutCubic(progress);
const currentPercentage = startPercentage + (change * easeProgress);
this.updateProgress(currentPercentage);
if (progress < 1) {
this.animationId = requestAnimationFrame(animate);
} else {
// 动画完成后的效果
if (targetPercentage === 100) {
this.showSuccessAnimation();
}
this.animationId = null;
}
};
this.animationId = requestAnimationFrame(animate);
}
easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
showSuccessAnimation() {
this.progressFill.classList.add('success');
setTimeout(() => {
this.progressFill.classList.remove('success');
}, 1000);
}
animateDemo() {
// 重置并演示完整动画
this.reset();
setTimeout(() => {
this.animateTo(100, 3000);
}, 500);
}
handleCircleClick(e) {
// 获取点击位置相对于SVG中心的坐标
const rect = this.svg.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const clickX = e.clientX - centerX;
const clickY = e.clientY - centerY;
// 计算角度
let angle = Math.atan2(clickY, clickX) * (180 / Math.PI);
if (angle < 0) angle += 360;
// 将角度转换为百分比 (0-360度对应0-100%)
const percentage = (angle / 360) * 100;
// 动画到目标百分比
this.animateTo(percentage);
}
updateProgressColor() {
// 根据进度改变颜色
if (this.progress < 30) {
this.progressFill.classList.remove('warning', 'success');
this.progressFill.classList.add('danger');
} else if (this.progress < 70) {
this.progressFill.classList.remove('danger', 'success');
this.progressFill.classList.add('warning');
} else {
this.progressFill.classList.remove('danger', 'warning');
this.progressFill.classList.add('success');
}
}
dispatchEvent(eventName, data) {
const event = new CustomEvent(eventName, {
detail: data,
bubbles: true,
cancelable: true
});
this.progressText.dispatchEvent(event);
}
// 获取当前进度
getProgress() {
return this.progress;
}
// 设置进度(带动画)
setProgress(percentage, animate = true) {
if (animate) {
this.animateTo(percentage);
} else {
this.updateProgress(percentage);
}
}
}
// 初始化进度条
document.addEventListener('DOMContentLoaded', () => {
const progressBar = new CircularProgressBar();
// 监听进度变化事件
document.querySelector('.progress-text').addEventListener('progressChange', (e) => {
console.log('进度变化:', e.detail.percentage + '%');
});
});
</script>
</body>
</html>
主要特性
1. 多种交互方式
- 滑块控制:通过range input精确控制进度
- 按钮控制:预设25%、50%、75%、100%等快捷按钮
- 点击圆圈:点击圆圈的任意位置,根据角度自动计算进度
- 动画控制:支持平滑动画过渡
2. 视觉效果
- 渐变背景:美观的渐变背景
- 颜色变化:根据进度自动改变颜色(红色→黄色→绿色)
- 成功动画:达到100%时有特殊动画效果
- 悬停效果:按钮有悬停和点击动画
3. 核心功能
// 主要方法
progressBar.setProgress(75); // 设置进度
progressBar.animateTo(100, 2000); // 动画到指定进度
progressBar.reset(); // 重置到0%
progressBar.getProgress(); // 获取当前进度
4. 动画系统
- 缓动函数:使用easeOutCubic缓动函数使动画更自然
- requestAnimationFrame:使用高性能的动画帧控制
- 可取消动画:支持取消正在进行的动画
5. 响应式设计
- 适配不同屏幕尺寸
- 移动端友好的触摸控制
使用方法
- 直接复制代码到HTML文件中运行
- 自定义样式:修改CSS部分调整外观
- API调用:通过JavaScript对象方法控制进度条