first commit

This commit is contained in:
陈乾 2026-01-30 19:00:58 +08:00
commit 2d0d0ff0fb
10 changed files with 3027 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

111
index.html Normal file
View File

@ -0,0 +1,111 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>网络音乐播放器</title>
</head>
<body>
<div class="audio-player">
<!-- 音频URL输入 -->
<div class="url-input-section">
<input type="text" id="audioUrl" placeholder="输入网络音乐地址http://music.163.com/song/media/outer/url?id=447925558.mp3" value="/DocServer/repository/file/view/73901-root/last/content?key=1769765152330.mp3">
<button id="loadAudio">加载音乐</button>
</div>
<!-- 备用音频源 -->
<div class="test-audio-sources">
<p style="margin: 10px 0 5px 0; font-size: 12px; color: #666;">测试音频源:</p>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button class="test-source-btn" data-url="/DocServer/repository/file/view/73904-root/last/content?id=447925558.mp3">本地代理测试1</button>
<button class="test-source-btn" data-url="/DocServer/repository/file/view/73901-root/last/content?key=1769765152330.mp3">本地代理测试2</button>
</div>
</div>
<!-- 错误提示 -->
<div id="errorMessage" class="error-message" style="display: none;"></div>
<!-- 加载遮罩层 -->
<div id="loadingOverlay" class="loading-overlay" style="display: none;"></div>
<!-- 加载提示弹出层(简化版) -->
<div id="loadingMessage" class="loading-message" style="display: none;">
<!-- 主要加载动画 -->
<div class="main-loader">
<div class="circular-progress-simple">
<svg width="80" height="80">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
</linearGradient>
</defs>
<circle class="progress-bg" cx="40" cy="40" r="35"></circle>
<circle class="progress-bar" cx="40" cy="40" r="35" id="circularProgress"></circle>
</svg>
<div class="progress-text" id="circularProgressText">0%</div>
</div>
</div>
<!-- 加载文字信息 -->
<div class="loading-info">
<div class="loading-title" id="loadingTitle">正在加载音频</div>
<div class="loading-subtitle" id="loadingSubtitle">请稍候...</div>
</div>
<!-- 简单的音乐频谱指示器 -->
<div class="mini-spectrum">
<div class="spectrum-bar"></div>
<div class="spectrum-bar"></div>
<div class="spectrum-bar"></div>
<div class="spectrum-bar"></div>
</div>
</div>
<!-- 可视化效果 -->
<div class="visualization-section">
<canvas id="visualizer" width="100%" height="100%"></canvas>
<div class="visualization-controls">
<button class="viz-btn active" data-mode="bars">条形图</button>
<button class="viz-btn" data-mode="wave">波形图</button>
<button class="viz-btn" data-mode="circle">圆形频谱</button>
</div>
</div>
<!-- 播放控制 -->
<div class="player-controls">
<div class="time-display">
<span id="currentTime">00:00</span>
<span>/</span>
<span id="duration">00:00</span>
</div>
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div class="progress" id="progress"></div>
<div class="progress-handle" id="progressHandle"></div>
</div>
</div>
</div>
<!-- 控制按钮 -->
<div class="control-buttons">
<button id="playPause" class="control-btn">
<span class="play-icon">▶️</span>
<span class="pause-icon" style="display: none;">⏸️</span>
</button>
<button id="stop" class="control-btn">⏹️</button>
<div class="volume-control">
<span class="volume-icon">🔊</span>
<input type="range" id="volumeSlider" min="0" max="100" value="70" class="volume-slider">
<span id="volumeValue">70%</span>
</div>
</div>
<!-- 隐藏的音频元素 -->
<audio id="audioElement" crossorigin="anonymous"></audio>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1105
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "net-audio-player",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^7.2.4"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

9
src/counter.js Normal file
View File

@ -0,0 +1,9 @@
export function setupCounter(element) {
let counter = 0
const setCounter = (count) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

1
src/javascript.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>

After

Width:  |  Height:  |  Size: 995 B

860
src/main.js Normal file
View File

@ -0,0 +1,860 @@
import './style.css'
class AudioPlayer {
constructor() {
this.audio = document.getElementById('audioElement');
this.canvas = document.getElementById('visualizer');
this.ctx = this.canvas.getContext('2d');
this.audioContext = null;
this.analyser = null;
this.source = null;
this.dataArray = null;
this.bufferLength = null;
this.animationId = null;
this.isPlaying = false;
this.currentVisualization = 'bars';
this.initializeElements();
this.setupEventListeners();
this.setupCanvas();
this.initializeAudioContext();
}
initializeElements() {
this.elements = {
audioUrl: document.getElementById('audioUrl'),
loadAudio: document.getElementById('loadAudio'),
playPause: document.getElementById('playPause'),
stop: document.getElementById('stop'),
progressBar: document.getElementById('progressBar'),
progress: document.getElementById('progress'),
progressHandle: document.getElementById('progressHandle'),
currentTime: document.getElementById('currentTime'),
duration: document.getElementById('duration'),
volumeSlider: document.getElementById('volumeSlider'),
volumeValue: document.getElementById('volumeValue'),
vizButtons: document.querySelectorAll('.viz-btn'),
playIcon: document.querySelector('.play-icon'),
pauseIcon: document.querySelector('.pause-icon'),
errorMessage: document.getElementById('errorMessage'),
loadingMessage: document.getElementById('loadingMessage'),
loadingTitle: document.getElementById('loadingTitle'),
loadingSubtitle: document.getElementById('loadingSubtitle'),
circularProgress: document.getElementById('circularProgress'),
circularProgressText: document.getElementById('circularProgressText')
};
}
setupEventListeners() {
// 加载音频
this.elements.loadAudio.addEventListener('click', () => this.loadAudio());
// 播放控制
this.elements.playPause.addEventListener('click', () => this.togglePlayPause());
this.elements.stop.addEventListener('click', () => this.stop());
// 进度条控制
this.elements.progressBar.addEventListener('click', (e) => this.seekTo(e));
this.elements.progressHandle.addEventListener('mousedown', () => this.startDragging());
// 音量控制
this.elements.volumeSlider.addEventListener('input', () => this.updateVolume());
// 音频事件
this.audio.addEventListener('loadedmetadata', () => this.updateDuration());
this.audio.addEventListener('timeupdate', () => this.updateProgress());
this.audio.addEventListener('ended', () => this.onAudioEnded());
this.audio.addEventListener('error', (e) => this.handleAudioError(e));
this.audio.addEventListener('loadstart', () => this.showLoadingState());
this.audio.addEventListener('canplay', () => this.onAudioCanPlay());
this.audio.addEventListener('canplaythrough', () => this.onAudioCanPlayThrough());
this.audio.addEventListener('progress', () => this.onAudioProgress());
// 可视化切换
this.elements.vizButtons.forEach(btn => {
btn.addEventListener('click', () => this.switchVisualization(btn.dataset.mode));
});
// 测试音频源按钮
document.querySelectorAll('.test-source-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.elements.audioUrl.value = btn.dataset.url;
this.loadAudio();
});
});
}
setupCanvas() {
const resizeCanvas = () => {
const container = this.canvas.parentElement;
const rect = container.getBoundingClientRect();
this.canvas.width = Math.min(600, rect.width - 40);
this.canvas.height = 300;
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
}
initializeAudioContext() {
try {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 256;
this.bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
// 连接音频源到分析器
this.source = this.audioContext.createMediaElementSource(this.audio);
this.source.connect(this.analyser);
this.analyser.connect(this.audioContext.destination);
} catch (error) {
console.warn('Web Audio API 不支持或初始化失败:', error);
}
}
loadAudio() {
const url = this.elements.audioUrl.value.trim();
if (!url) {
alert('请输入音频地址');
return;
}
// 尝试多种跨域解决方案
this.loadAudioWithCORS(url);
}
loadAudioWithCORS(url) {
this.hideLoadingState();
// 开始加载过程
this.startLoadingAnimation();
// 直接尝试加载音频跳过URL测试提高响应速度
this.audio.crossOrigin = 'anonymous';
this.audio.src = url;
this.audio.load();
// 监听加载进度
this.setupLoadingProgress();
// 如果音频上下文被暂停,恢复它
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
}
startLoadingAnimation() {
this.showLoadingState();
this.updateLoadingTitle('正在加载音频');
this.updateLoadingSubtitle('请稍候...');
this.updateLoadingProgress(0);
// 添加加载动画的延迟效果,让用户体验更自然
setTimeout(() => {
this.updateLoadingProgress(10);
}, 300);
}
updateLoadingText(text) {
if (this.elements.loadingText) {
this.elements.loadingText.textContent = text;
}
}
updateLoadingProgress(percent) {
if (this.elements.circularProgress) {
// 圆形进度条计算
const circumference = 2 * Math.PI * 25; // 半径25
const offset = circumference - (percent / 100) * circumference;
this.elements.circularProgress.style.strokeDashoffset = offset;
}
if (this.elements.circularProgressText) {
this.elements.circularProgressText.textContent = Math.round(percent) + '%';
}
}
setupLoadingProgress() {
let lastProgress = 0;
let loadStartTime = Date.now();
let timeoutWarningShown = false;
const checkProgress = () => {
const currentTime = Date.now();
const elapsedTime = (currentTime - loadStartTime) / 1000; // 秒
// 超时警告30秒
if (elapsedTime > 30 && !timeoutWarningShown) {
timeoutWarningShown = true;
this.updateLoadingTitle('加载时间较长');
this.updateLoadingSubtitle('请耐心等待...');
console.warn('音频加载超过30秒可能存在网络问题');
}
// 超时处理60秒
if (elapsedTime > 60) {
console.error('音频加载超时60秒');
this.showError('音频加载超时\n\n可能原因\n1. 网络连接缓慢\n2. 音频文件过大\n3. 服务器响应缓慢\n\n建议\n1. 检查网络连接\n2. 尝试其他音频源\n3. 刷新页面重试');
this.hideLoadingState();
return;
}
if (this.audio.buffered.length > 0) {
const bufferedEnd = this.audio.buffered.end(this.audio.buffered.length - 1);
const duration = this.audio.duration || 0;
if (duration > 0) {
const progress = (bufferedEnd / duration) * 100;
this.updateLoadingProgress(progress);
// 更新加载文本 - 简化版
if (progress < 30) {
this.updateLoadingTitle('正在连接...');
this.updateLoadingSubtitle('');
} else if (progress < 60) {
this.updateLoadingTitle(`已加载 ${Math.round(progress)}%`);
this.updateLoadingSubtitle('');
} else if (progress < 90) {
this.updateLoadingTitle('正在缓冲...');
this.updateLoadingSubtitle('');
} else {
this.updateLoadingTitle('即将播放');
this.updateLoadingSubtitle('');
}
lastProgress = progress;
}
}
// 继续检查进度
if (this.audio.readyState < 4 && this.audio.networkState !== 3) {
setTimeout(checkProgress, 500);
}
};
// 开始检查进度
setTimeout(checkProgress, 100);
}
isValidAudioUrl(url) {
try {
const urlObj = new URL(url);
// 检查协议
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return false;
}
// 检查文件扩展名或路径模式
const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.flac'];
const hasAudioExtension = audioExtensions.some(ext =>
urlObj.pathname.toLowerCase().includes(ext)
);
// 或者包含音频相关的路径模式
const hasAudioPattern = /(audio|music|song|media|sound)/i.test(urlObj.href);
return hasAudioExtension || hasAudioPattern;
} catch (e) {
return false;
}
}
testAudioUrl(url) {
return new Promise((resolve) => {
const testAudio = new Audio();
testAudio.crossOrigin = 'anonymous';
let resolved = false;
// 设置超时
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
resolve(false);
}
}, 8000); // 8秒超时
testAudio.addEventListener('loadedmetadata', () => {
clearTimeout(timeout);
if (!resolved) {
resolved = true;
resolve(true);
}
});
testAudio.addEventListener('error', (e) => {
clearTimeout(timeout);
if (!resolved) {
resolved = true;
console.log('测试音频失败:', e);
resolve(false);
}
});
// 添加加载事件监听
testAudio.addEventListener('loadstart', () => {
console.log('开始测试音频加载:', url);
});
testAudio.src = url;
testAudio.load();
});
}
tryProxyLoad(url) {
// 尝试使用代理服务器加载音频
const proxyUrl = `/DocServer/audio?url=${encodeURIComponent(url)}`;
console.log('尝试代理加载:', proxyUrl);
// 直接尝试代理URL
this.audio.crossOrigin = 'anonymous';
this.audio.src = proxyUrl;
this.audio.load();
// 给代理加载设置超时
setTimeout(() => {
if (this.audio.networkState === 3) { // NETWORK_NO_SOURCE
console.error('代理加载超时');
this.showError('代理加载失败\n\n可能的原因\n1. 代理服务器无法访问目标音频\n2. 目标音频需要认证\n3. 音频文件不存在\n\n建议\n1. 使用支持CORS的音频源\n2. 下载音频到本地服务器\n3. 使用其他音频地址');
}
}, 10000); // 10秒超时
}
togglePlayPause() {
if (this.isPlaying) {
this.pause();
} else {
this.play();
}
}
play() {
if (this.audio.src) {
this.audio.play().then(() => {
this.isPlaying = true;
this.updatePlayButton();
this.startVisualization();
document.querySelector('.audio-player').classList.add('playing');
// 显示播放成功提示(短暂显示)
console.log('🎵 音频播放开始');
this.showSuccessMessage('音频加载完成,开始播放!');
}).catch(error => {
console.error('播放失败:', error);
this.showError('音频播放失败,请重试');
this.isPlaying = false;
this.updatePlayButton();
});
}
}
showSuccessMessage(message) {
// 创建成功提示元素
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
successDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4caf50;
color: white;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
font-size: 14px;
transform: translateX(100%);
transition: transform 0.3s ease;
`;
document.body.appendChild(successDiv);
// 动画显示
setTimeout(() => {
successDiv.style.transform = 'translateX(0)';
}, 100);
// 3秒后自动隐藏
setTimeout(() => {
successDiv.style.transform = 'translateX(100%)';
setTimeout(() => {
if (successDiv.parentNode) {
successDiv.parentNode.removeChild(successDiv);
}
}, 300);
}, 3000);
}
pause() {
this.audio.pause();
this.isPlaying = false;
this.updatePlayButton();
this.stopVisualization();
document.querySelector('.audio-player').classList.remove('playing');
}
stop() {
this.audio.pause();
this.audio.currentTime = 0;
this.isPlaying = false;
this.updatePlayButton();
this.stopVisualization();
this.updateProgress();
document.querySelector('.audio-player').classList.remove('playing');
}
updatePlayButton() {
if (this.isPlaying) {
this.elements.playIcon.style.display = 'none';
this.elements.pauseIcon.style.display = 'inline';
} else {
this.elements.playIcon.style.display = 'inline';
this.elements.pauseIcon.style.display = 'none';
}
}
updateDuration() {
const duration = this.audio.duration;
this.elements.duration.textContent = this.formatTime(duration);
// 音频元数据加载完成,更新加载状态
if (this.elements.loadingMessage.style.display !== 'none') {
this.updateLoadingTitle('加载完成');
this.updateLoadingSubtitle('');
this.updateLoadingProgress(80);
console.log('音频元数据加载完成,时长:', this.formatTime(duration));
}
}
updateProgress() {
const currentTime = this.audio.currentTime;
const duration = this.audio.duration;
if (duration) {
const progressPercent = (currentTime / duration) * 100;
this.elements.progress.style.width = progressPercent + '%';
this.elements.progressHandle.style.left = progressPercent + '%';
}
this.elements.currentTime.textContent = this.formatTime(currentTime);
}
seekTo(e) {
const rect = this.elements.progressBar.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const width = rect.width;
const percentage = clickX / width;
if (this.audio.duration) {
this.audio.currentTime = percentage * this.audio.duration;
}
}
startDragging() {
const handleMouseMove = (e) => {
const rect = this.elements.progressBar.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const width = rect.width;
let percentage = clickX / width;
percentage = Math.max(0, Math.min(1, percentage));
if (this.audio.duration) {
this.audio.currentTime = percentage * this.audio.duration;
}
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
updateVolume() {
const volume = this.elements.volumeSlider.value / 100;
this.audio.volume = volume;
this.elements.volumeValue.textContent = Math.round(volume * 100) + '%';
}
onAudioEnded() {
this.isPlaying = false;
this.updatePlayButton();
this.stopVisualization();
document.querySelector('.audio-player').classList.remove('playing');
}
showError(message) {
const errorDiv = this.elements.errorMessage;
errorDiv.textContent = message;
errorDiv.style.display = 'block';
// 5秒后自动隐藏错误信息
setTimeout(() => {
errorDiv.style.display = 'none';
}, 8000);
}
showLoadingState() {
// 显示遮罩层和加载弹出层
if (this.elements.loadingOverlay) {
this.elements.loadingOverlay.style.display = 'block';
this.elements.loadingOverlay.classList.add('show');
}
this.elements.loadingMessage.style.display = 'block';
this.elements.loadingMessage.classList.add('fade-in');
this.elements.errorMessage.style.display = 'none';
// 防止页面滚动
document.body.classList.add('loading-active');
const loadBtn = this.elements.loadAudio;
loadBtn.textContent = '加载中...';
loadBtn.disabled = true;
// 重置进度
this.updateLoadingProgress(0);
this.updateLoadingTitle('正在加载音频');
this.updateLoadingSubtitle('请稍候...');
// 启动加载进度监听
this.setupLoadingProgress();
}
hideLoadingState() {
this.elements.loadingMessage.classList.add('fade-out');
setTimeout(() => {
this.elements.loadingMessage.style.display = 'none';
this.elements.loadingMessage.classList.remove('fade-in', 'fade-out');
if (this.elements.loadingOverlay) {
this.elements.loadingOverlay.style.display = 'none';
this.elements.loadingOverlay.classList.remove('show');
}
// 恢复页面滚动
document.body.classList.remove('loading-active');
}, 300);
const loadBtn = this.elements.loadAudio;
loadBtn.textContent = '加载音乐';
loadBtn.disabled = false;
}
updateLoadingTitle(text) {
if (this.elements.loadingTitle) {
this.elements.loadingTitle.textContent = text;
}
}
updateLoadingSubtitle(text) {
if (this.elements.loadingSubtitle) {
this.elements.loadingSubtitle.textContent = text;
}
}
onAudioProgress() {
// 更新加载进度
if (this.audio.buffered.length > 0 && this.elements.loadingMessage.style.display !== 'none') {
const bufferedEnd = this.audio.buffered.end(this.audio.buffered.length - 1);
const duration = this.audio.duration || 0;
if (duration > 0) {
const progress = (bufferedEnd / duration) * 100;
this.updateLoadingProgress(progress);
// 更新加载文本
if (progress < 30) {
this.updateLoadingText('正在连接音频服务器...');
} else if (progress < 60) {
this.updateLoadingText('正在接收音频数据...');
} else if (progress < 90) {
this.updateLoadingText('正在缓冲音频...');
} else {
this.updateLoadingText('即将开始播放...');
}
}
}
}
onAudioCanPlay() {
console.log('音频可以开始播放');
// 音频已经有足够数据开始播放,但继续显示加载状态
if (this.elements.loadingMessage.style.display !== 'none') {
this.updateLoadingText('音频缓冲完成,准备播放...');
this.updateLoadingProgress(95);
}
}
onAudioCanPlayThrough() {
console.log('音频可以流畅播放');
// 音频可以流畅播放,隐藏加载状态并自动播放
// 显示加载完成状态
if (this.elements.loadingMessage.style.display !== 'none') {
this.updateLoadingTitle('加载完成');
this.updateLoadingSubtitle('');
this.updateLoadingProgress(100);
}
// 延迟隐藏加载状态,让用户看到完成状态
setTimeout(() => {
this.hideLoadingState();
// 自动开始播放
if (this.audio.src && !this.isPlaying) {
console.log('自动开始播放');
this.play();
}
}, 500);
}
handleAudioError(error) {
console.error('音频加载错误详情:', {
error: error,
audioError: this.audio.error,
currentSrc: this.audio.currentSrc,
networkState: this.audio.networkState,
readyState: this.audio.readyState
});
let errorMessage = '音频加载失败';
let solutions = [];
// 获取详细的错误信息
const audioError = this.audio.error;
const networkState = this.audio.networkState;
const readyState = this.audio.readyState;
console.log('网络状态:', this.getNetworkStateText(networkState));
console.log('就绪状态:', this.getReadyStateText(readyState));
if (audioError) {
switch (audioError.code) {
case 1:
errorMessage = '音频下载过程中被中断';
solutions = ['检查网络连接', '重新加载页面', '尝试其他音频地址'];
break;
case 2:
errorMessage = '网络错误 - 音频文件无法下载';
solutions = ['检查音频地址是否有效', '确认音频文件存在', '检查网络连接'];
break;
case 3:
errorMessage = '音频解码错误 - 文件格式不支持或已损坏';
solutions = ['确认音频格式为MP3/WAV/OGG', '尝试其他音频文件', '检查文件是否完整'];
break;
case 4:
errorMessage = '音频格式不支持或跨域访问被拒绝';
solutions = ['检查音频文件格式', '确认服务器支持跨域', '尝试使用代理服务器'];
break;
default:
errorMessage = '未知错误';
solutions = ['检查音频地址', '确认网络连接', '尝试其他音频源'];
}
} else {
// 没有具体错误码,根据状态判断
if (networkState === 3) {
errorMessage = '网络错误 - 无法下载音频文件';
solutions = ['检查音频地址是否有效', '确认音频文件存在', '检查网络连接'];
} else if (readyState === 0) {
errorMessage = '音频文件无法加载';
solutions = ['检查音频地址', '确认文件可访问', '检查跨域设置'];
} else {
errorMessage = '音频加载失败';
solutions = ['检查音频地址', '确认网络连接', '尝试其他音频源'];
}
}
// 显示详细的错误信息
this.showError(`音频加载失败:${errorMessage}\n\n解决方案:\n${solutions.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n调试信息:\n网络状态: ${this.getNetworkStateText(networkState)}\n就绪状态: ${this.getReadyStateText(readyState)}\n错误码: ${audioError?.code || '无'}`);
this.hideLoadingState();
}
getNetworkStateText(state) {
const states = {
0: 'NETWORK_EMPTY - 网络状态未知',
1: 'NETWORK_IDLE - 未使用网络',
2: 'NETWORK_LOADING - 正在加载',
3: 'NETWORK_NO_SOURCE - 未找到音频源'
};
return states[state] || `未知状态(${state})`;
}
getReadyStateText(state) {
const states = {
0: 'HAVE_NOTHING - 没有音频信息',
1: 'HAVE_METADATA - 只有元数据',
2: 'HAVE_CURRENT_DATA - 当前播放位置有数据',
3: 'HAVE_FUTURE_DATA - 当前及未来数据可用',
4: 'HAVE_ENOUGH_DATA - 足够数据开始播放'
};
return states[state] || `未知状态(${state})`;
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
switchVisualization(mode) {
this.currentVisualization = mode;
// 更新按钮状态
this.elements.vizButtons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
}
startVisualization() {
if (!this.animationId) {
this.animate();
}
}
stopVisualization() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
this.clearCanvas();
}
}
clearCanvas() {
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
animate() {
if (!this.analyser) return;
this.animationId = requestAnimationFrame(() => this.animate());
this.analyser.getByteFrequencyData(this.dataArray);
switch (this.currentVisualization) {
case 'bars':
this.drawBars();
break;
case 'wave':
this.drawWave();
break;
case 'circle':
this.drawCircle();
break;
}
}
drawBars() {
this.clearCanvas();
const barWidth = (this.canvas.width / this.bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < this.bufferLength; i++) {
barHeight = (this.dataArray[i] / 255) * this.canvas.height * 0.8;
const gradient = this.ctx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height);
gradient.addColorStop(0, `hsl(${(i / this.bufferLength) * 360}, 100%, 50%)`);
gradient.addColorStop(1, `hsl(${(i / this.bufferLength) * 360}, 100%, 30%)`);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
drawWave() {
this.clearCanvas();
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = '#00ff88';
this.ctx.beginPath();
const sliceWidth = this.canvas.width / this.bufferLength;
let x = 0;
for (let i = 0; i < this.bufferLength; i++) {
const v = this.dataArray[i] / 128.0;
const y = (v * this.canvas.height) / 2;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
x += sliceWidth;
}
this.ctx.stroke();
}
drawCircle() {
this.clearCanvas();
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
const radius = Math.min(centerX, centerY) - 50;
for (let i = 0; i < this.bufferLength; i++) {
const angle = (i / this.bufferLength) * Math.PI * 2;
const amplitude = (this.dataArray[i] / 255) * radius;
const x1 = centerX + Math.cos(angle) * (radius * 0.5);
const y1 = centerY + Math.sin(angle) * (radius * 0.5);
const x2 = centerX + Math.cos(angle) * (radius * 0.5 + amplitude);
const y2 = centerY + Math.sin(angle) * (radius * 0.5 + amplitude);
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.strokeStyle = `hsl(${(i / this.bufferLength) * 360}, 100%, 50%)`;
this.ctx.lineWidth = 2;
this.ctx.stroke();
}
// 绘制中心圆
this.ctx.beginPath();
this.ctx.arc(centerX, centerY, 30, 0, Math.PI * 2);
this.ctx.fillStyle = 'rgba(102, 126, 234, 0.8)';
this.ctx.fill();
// 中心文字
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText('🎵', centerX, centerY + 4);
}
}
// 初始化播放器
document.addEventListener('DOMContentLoaded', () => {
// 检查浏览器兼容性
if (!window.AudioContext && !window.webkitAudioContext) {
alert('您的浏览器不支持Web Audio API部分功能可能无法正常使用。\n\n建议使用\n• Chrome 10+\n• Firefox 25+\n• Safari 6+\n• Edge 12+');
}
// 检查是否支持音频
if (!document.createElement('audio').canPlayType) {
alert('您的浏览器不支持HTML5音频播放。\n\n请升级浏览器或使用现代浏览器。');
return;
}
const player = new AudioPlayer();
// 显示初始化提示
console.log('🎵 音频播放器初始化完成');
console.log('💡 提示:如果遇到跨域问题,请尝试:');
console.log(' 1. 使用上方的测试音频按钮');
console.log(' 2. 确保音频地址支持CORS');
console.log(' 3. 检查网络连接');
});

878
src/style.css Normal file
View File

@ -0,0 +1,878 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
width: 100%;
height: 100%;
min-width: 320px;
min-height: 100vh;
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
justify-content: center;
align-items: center;
color: #333;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.audio-player {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
width: 800px;
margin: 20px;
}
/* URL输入区域 */
.url-input-section {
margin-bottom: 30px;
display: flex;
gap: 10px;
}
#audioUrl {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
outline: none;
transition: border-color 0.3s ease;
}
#audioUrl:focus {
border-color: #667eea;
}
#loadAudio {
padding: 12px 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: transform 0.2s ease;
}
#loadAudio:hover {
transform: translateY(-2px);
}
/* 可视化区域 */
.visualization-section {
margin-bottom: 30px;
text-align: center;
}
#visualizer {
border: 2px solid #e0e0e0;
border-radius: 15px;
background: #000;
width: 100%;
height: 320px;
}
.visualization-controls {
margin-top: 15px;
display: flex;
justify-content: center;
gap: 10px;
}
.viz-btn {
padding: 8px 16px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 20px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.viz-btn.active {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border-color: #667eea;
}
.viz-btn:hover {
border-color: #667eea;
transform: translateY(-1px);
}
/* 播放控制 */
.player-controls {
margin-bottom: 25px;
}
.time-display {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-bottom: 15px;
font-size: 14px;
font-weight: bold;
color: #666;
}
.progress-container {
position: relative;
margin-bottom: 20px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.progress {
height: 100%;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 4px;
width: 0%;
transition: width 0.1s ease;
}
.progress-handle {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
background: white;
border: 3px solid #667eea;
border-radius: 50%;
cursor: pointer;
left: 0%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease;
}
.progress-handle:hover {
transform: translate(-50%, -50%) scale(1.2);
}
/* 控制按钮 */
.control-buttons {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
}
.control-btn {
width: 50px;
height: 50px;
border: none;
border-radius: 50%;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
#playPause {
width: 60px;
height: 60px;
font-size: 24px;
}
/* 音量控制 */
.volume-control {
display: flex;
align-items: center;
gap: 10px;
margin-left: 20px;
}
.volume-icon {
font-size: 20px;
}
.volume-slider {
width: 100px;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
outline: none;
cursor: pointer;
-webkit-appearance: none;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.volume-slider::-moz-range-thumb {
width: 16px;
height: 16px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
#volumeValue {
font-size: 12px;
font-weight: bold;
color: #666;
min-width: 30px;
}
/* 加载完成动画 */
@keyframes loading-complete {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
/* 加载遮罩层 - 简洁设计 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(1px);
z-index: 999;
display: none;
}
.loading-overlay.show {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 加载弹出层悬浮效果 */
.loading-message {
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(102, 126, 234, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
/* 加载弹出层悬浮效果 */
.loading-message:hover {
transform: translate(-50%, -50%) scale(1.02);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(102, 126, 234, 0.2);
}
/* 加载弹出层响应式设计 */
@media (max-width: 480px) {
.loading-message {
min-width: 180px;
max-width: 240px;
padding: 20px;
}
.circular-progress {
width: 60px;
height: 60px;
}
.loading-text .loading-title {
font-size: 14px;
}
.loading-text .loading-subtitle {
font-size: 11px;
}
}
/* 加载弹出层细节优化 */
.loading-message {
will-change: transform, opacity;
}
.circular-progress .progress-bar {
will-change: stroke-dashoffset;
}
.loading-text .loading-title {
will-change: contents;
}
/* 防止加载时页面滚动 */
body.loading-active {
overflow: hidden;
} 加载遮罩层 - 简洁设计 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(1px);
z-index: 999;
display: none;
}
.loading-overlay.show {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 加载弹出层 - 简洁优雅 */
.loading-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
z-index: 1000;
text-align: center;
min-width: 200px;
max-width: 280px;
display: none;
border: 1px solid rgba(102, 126, 234, 0.1);
}
/* 成功消息动画 */
@keyframes slide-in-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slide-out-right {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.audio-player {
padding: 20px;
margin: 10px;
}
.url-input-section {
flex-direction: column;
}
#visualizer {
height: 200px;
}
.control-buttons {
flex-direction: column;
gap: 15px;
}
.volume-control {
margin-left: 0;
margin-top: 10px;
}
}
/* 测试音频源 */
.test-audio-sources {
margin-bottom: 20px;
}
.test-source-btn {
padding: 6px 12px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 15px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.test-source-btn:hover {
background: #e0e0e0;
border-color: #667eea;
transform: translateY(-1px);
}
/* 加载弹出层 - 简洁优雅 */
.loading-message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 24px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
z-index: 1000;
text-align: center;
min-width: 200px;
max-width: 280px;
display: none;
border: 1px solid rgba(102, 126, 234, 0.1);
}
/* 圆形加载动画 */
.loading-animation {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.circular-loader {
position: relative;
width: 80px;
height: 80px;
}
.circular-loader .loader-circle {
width: 100%;
height: 100%;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.circular-loader .loader-inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
border: 3px solid transparent;
border-top: 3px solid #764ba2;
border-radius: 50%;
animation: spin 1.5s linear infinite reverse;
}
/* 加载信息 - 简洁 */
.loading-info {
margin-bottom: 12px;
}
.loading-info .loading-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.loading-info .loading-subtitle {
font-size: 12px;
color: #888;
}
/* 迷你频谱 - 简洁 */
.mini-spectrum {
display: flex;
justify-content: center;
align-items: flex-end;
height: 20px;
gap: 2px;
}
.spectrum-bar {
width: 3px;
background: linear-gradient(to top, #667eea, #764ba2);
border-radius: 1.5px;
animation: spectrum-pulse 1s ease-in-out infinite;
}
.spectrum-bar:nth-child(1) {
height: 12px;
animation-delay: 0s;
}
.spectrum-bar:nth-child(2) {
height: 16px;
animation-delay: 0.2s;
}
.spectrum-bar:nth-child(3) {
height: 14px;
animation-delay: 0.4s;
}
.spectrum-bar:nth-child(4) {
height: 18px;
animation-delay: 0.6s;
}
@keyframes spectrum-pulse {
0%, 100% {
transform: scaleY(0.7);
opacity: 0.7;
}
50% {
transform: scaleY(1);
opacity: 1;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 主要加载器 - 简洁 */
.main-loader {
display: flex;
justify-content: center;
margin-bottom: 16px;
}
/* 简化圆形进度条 */
.circular-progress-simple {
position: relative;
width: 80px;
height: 80px;
margin: 0 auto;
}
.circular-progress-simple svg {
width: 100%;
height: 100%;
transform: rotate(-90deg);
}
.circular-progress-simple .progress-bg {
fill: none;
stroke: #f5f5f5;
stroke-width: 6;
}
.circular-progress-simple .progress-bar {
fill: none;
stroke: url(#gradient);
stroke-width: 6;
stroke-linecap: round;
stroke-dasharray: 220; /* 2 * π * 35 */
stroke-dashoffset: 220;
transition: stroke-dashoffset 0.4s ease;
}
.circular-progress-simple .progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
font-weight: 600;
color: #667eea;
}
.loading-text {
text-align: center;
margin-bottom: 10px;
}
.loading-text .loading-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 5px;
}
.loading-text .loading-subtitle {
font-size: 12px;
color: #666;
}
/* 加载状态切换动画 - 更平滑 */
.loading-message.fade-in {
animation: fadeInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.loading-message.fade-out {
animation: fadeOutScale 0.3s ease-in;
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.7);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes fadeOutScale {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -50%) scale(0.7);
}
}
/* 圆形进度条填充动画 */
@keyframes progress-fill {
from {
stroke-dashoffset: 220;
}
to {
stroke-dashoffset: 0;
}
}
.circular-progress-simple .progress-bar.loading {
animation: progress-fill 2s ease-out;
}
/* 加载弹出层响应式设计 */
@media (max-width: 480px) {
.loading-message {
min-width: 180px;
max-width: 240px;
padding: 20px;
}
.circular-progress-simple {
width: 60px;
height: 60px;
}
.loading-info .loading-title {
font-size: 14px;
}
.loading-info .loading-subtitle {
font-size: 11px;
}
}
/* 错误提示和加载状态 */
.error-message {
background: #ffebee;
border: 1px solid #f44336;
border-radius: 8px;
padding: 12px;
margin: 15px 0;
color: #c62828;
font-size: 14px;
display: none;
white-space: pre-line;
}
.loading-message {
background: #e3f2fd;
border: 1px solid #2196f3;
border-radius: 8px;
padding: 12px;
margin: 15px 0;
color: #1565c0;
font-size: 14px;
text-align: center;
display: none;
box-shadow:
0 20px 40px rgba(0, 0, 0, 0.15),
0 0 0 1px rgba(102, 126, 234, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.loading-message:hover {
transform: translate(-50%, -50%) scale(1.02);
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(102, 126, 234, 0.2);
}
/* 圆形进度条动画 */
@keyframes progress-glow {
0%, 100% {
filter: drop-shadow(0 0 2px rgba(102, 126, 234, 0.3));
}
50% {
filter: drop-shadow(0 0 8px rgba(102, 126, 234, 0.6));
}
}
.circular-progress .progress-bar {
animation: progress-glow 2s ease-in-out infinite;
}
/* 音乐频谱脉冲效果 */
@keyframes pulse-glow {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
.music-bars-small {
animation: pulse-glow 1.5s ease-in-out infinite;
}
/* 加载文字动画 */
@keyframes text-shimmer {
0% {
background-position: -100% 0;
}
100% {
background-position: 100% 0;
}
}
.loading-title {
background: linear-gradient(90deg, #667eea, #764ba2, #667eea);
background-size: 200% 100%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: text-shimmer 3s ease-in-out infinite;
}
.loading-message span {
display: inline-block;
animation: pulse 1.5s infinite;
}
/* 动画效果 */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-message::before {
content: "🎵";
display: inline-block;
margin-right: 8px;
animation: spin 2s linear infinite;
}
.playing .control-btn {
animation: pulse 2s infinite;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

24
vite.config.js Normal file
View File

@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000,
host: true,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Range',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range'
},
proxy: {
'/DocServer': {
target: 'http://192.168.1.201:8080',
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/DocServer/, '/DocServer')
}
}
}
})