diff --git a/index.html b/index.html
index b4c255f..983401f 100644
--- a/index.html
+++ b/index.html
@@ -7,104 +7,28 @@
-
-
-
-
-
-
-
-
-
测试音频源:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
0%
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 00:00
- /
- 00:00
-
-
-
-
-
-
-
-
-
+
+
网络音乐播放器
+
+
+
+
+
+
+
+
测试音频源:
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AudioPlayerComponent.js b/src/AudioPlayerComponent.js
new file mode 100644
index 0000000..9f3ceb1
--- /dev/null
+++ b/src/AudioPlayerComponent.js
@@ -0,0 +1,1073 @@
+export class AudioPlayerComponent {
+ constructor(container, options = {}) {
+ this.container = container;
+ this.options = {
+ autoPlay: true,
+ showVisualization: true,
+ visualizationMode: 'bars',
+ onError: null,
+ ...options
+ };
+
+ this.audio = null;
+ this.canvas = null;
+ this.ctx = null;
+ this.audioContext = null;
+ this.analyser = null;
+ this.source = null;
+ this.dataArray = null;
+ this.bufferLength = null;
+ this.animationId = null;
+ this.isPlaying = false;
+ this.currentVisualization = this.options.visualizationMode;
+ this.currentAudioUrl = null;
+
+ this.initializeComponent();
+ }
+
+ initializeComponent() {
+ this.createPlayerHTML();
+ this.initializeElements();
+ this.setupEventListeners();
+ this.setupCanvas();
+ this.initializeAudioContext();
+ }
+
+ createPlayerHTML() {
+ this.container.innerHTML = `
+
+
+
+
+
+
+
0%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 00:00
+ /
+ 00:00
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ initializeElements() {
+ this.elements = {
+ audio: this.container.querySelector('#audioElement'),
+ canvas: this.container.querySelector('#visualizer'),
+ playPause: this.container.querySelector('#playPause'),
+ progressBar: this.container.querySelector('#progressBar'),
+ progress: this.container.querySelector('#progress'),
+ progressHandle: this.container.querySelector('#progressHandle'),
+ currentTime: this.container.querySelector('#currentTime'),
+ duration: this.container.querySelector('#duration'),
+ volumeSlider: this.container.querySelector('#volumeSlider'),
+ volumeValue: this.container.querySelector('#volumeValue'),
+ vizButtons: this.container.querySelectorAll('.viz-btn'),
+ playIcon: this.container.querySelector('.play-icon'),
+ pauseIcon: this.container.querySelector('.pause-icon'),
+ loadingMessage: this.container.querySelector('.loading-message'),
+ loadingTitle: this.container.querySelector('#loadingTitle'),
+ loadingSubtitle: this.container.querySelector('#loadingSubtitle'),
+ circularProgress: this.container.querySelector('#circularProgress'),
+ circularProgressText: this.container.querySelector('#circularProgressText'),
+ loadingOverlay: this.container.querySelector('.loading-overlay')
+ };
+
+ this.audio = this.elements.audio;
+ this.canvas = this.elements.canvas;
+ this.ctx = this.canvas ? this.canvas.getContext('2d') : null;
+ }
+
+ setupEventListeners() {
+ // 播放控制
+ this.elements.playPause.addEventListener('click', () => this.togglePlayPause());
+
+ // 进度条控制
+ 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));
+ });
+ }
+
+ setupCanvas() {
+ if (!this.canvas) return;
+
+ 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(url) {
+ if (!url || url === this.currentAudioUrl) return;
+
+ this.currentAudioUrl = url;
+ this.stop();
+
+ // 尝试多种跨域解决方案
+ this.loadAudioWithCORS(url);
+ }
+
+ loadAudioWithCORS(url) {
+ this.hideLoadingState();
+
+ // 开始加载过程
+ this.startLoadingAnimation();
+
+ // 释放旧音频资源
+ this.audio.pause();
+ this.audio.currentTime = 0;
+ this.audio.src = '';
+ this.audio.srcObject = null;
+
+ // 直接尝试加载音频(跳过URL测试,提高响应速度)
+ this.audio.crossOrigin = 'anonymous';
+ this.audio.src = url;
+ this.audio.load();
+
+ // 监听加载进度
+ this.setupLoadingProgress();
+
+ // 如果音频上下文被暂停,恢复它
+ if (this.audioContext && this.audioContext.state === 'suspended') {
+ this.audioContext.resume().catch(error => {
+ console.error('恢复音频上下文失败:', error);
+ // 不显示错误,因为用户可以手动点击播放
+ });
+ }
+ }
+
+ startLoadingAnimation() {
+ this.showLoadingState();
+ this.updateLoadingTitle('正在加载音频');
+ this.updateLoadingSubtitle('请稍候...');
+ this.updateLoadingProgress(0);
+
+ // 添加加载动画的延迟效果,让用户体验更自然
+ setTimeout(() => {
+ this.updateLoadingProgress(10);
+ }, 300);
+ }
+
+ 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.onError('音频加载超时\\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);
+ }
+
+ 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();
+ this.container.classList.add('playing');
+
+ // 显示播放成功提示(短暂显示)
+ console.log('🎵 音频播放开始');
+ this.showSuccessMessage('音频加载完成,开始播放!');
+
+ }).catch(error => {
+ console.error('播放失败:', error);
+ this.onError('音频播放失败,请重试');
+ 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();
+ this.container.classList.remove('playing');
+ }
+
+ stop() {
+ this.isPlaying = false;
+ this.updatePlayButton();
+ this.stopVisualization();
+ this.updateProgress();
+ this.container.classList.remove('playing');
+
+ if (this.audio) {
+ this.audio.pause();
+ this.audio.currentTime = 0;
+ // 释放音频资源
+ this.audio.src = '';
+ this.audio.srcObject = null;
+ }
+ }
+
+ release(){
+ // 清理AudioContext资源
+ if (this.audioContext && this.audioContext.state !== 'closed') {
+ this.audioContext.close();
+ this.audioContext = null;
+ }
+ }
+
+ 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();
+ this.container.classList.remove('playing');
+ }
+
+ onError(message) {
+ // 触发错误回调
+ if (this.options.onError) {
+ this.options.onError(message);
+ }
+
+ console.error('播放器错误:', message);
+ }
+
+ showLoadingState() {
+ // 创建全屏加载遮罩层(如果不存在)
+ let loadingOverlay = document.getElementById('global-loading-overlay');
+ if (!loadingOverlay) {
+ loadingOverlay = document.createElement('div');
+ loadingOverlay.id = 'global-loading-overlay';
+ loadingOverlay.className = 'loading-overlay';
+ document.body.appendChild(loadingOverlay);
+ }
+
+ // 显示遮罩层和加载弹出层
+ loadingOverlay.style.display = 'block';
+ loadingOverlay.classList.add('show');
+
+ this.elements.loadingMessage.style.display = 'block';
+ this.elements.loadingMessage.classList.add('fade-in');
+
+ // 防止页面滚动
+ document.body.classList.add('loading-active');
+
+ // 重置进度
+ 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');
+
+ // 移除全局加载遮罩层
+ const loadingOverlay = document.getElementById('global-loading-overlay');
+ if (loadingOverlay) {
+ loadingOverlay.style.display = 'none';
+ loadingOverlay.classList.remove('show');
+ document.body.removeChild(loadingOverlay);
+ }
+
+ // 恢复页面滚动
+ document.body.classList.remove('loading-active');
+ }, 300);
+ }
+
+ 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.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('');
+ }
+ }
+ }
+ }
+
+ onAudioCanPlay() {
+ console.log('音频可以开始播放');
+ // 音频已经有足够数据开始播放,但继续显示加载状态
+ if (this.elements.loadingMessage.style.display !== 'none') {
+ this.updateLoadingTitle('音频缓冲完成,准备播放...');
+ this.updateLoadingProgress(95);
+ }
+ }
+
+ onAudioCanPlayThrough() {
+ console.log('音频可以流畅播放');
+ // 音频可以流畅播放,隐藏加载状态并自动播放
+
+ // 显示加载完成状态
+ if (this.elements.loadingMessage.style.display !== 'none') {
+ this.updateLoadingTitle('加载完成');
+ this.updateLoadingSubtitle('');
+ this.updateLoadingProgress(100);
+ }
+
+ // 更新歌曲标题
+ this.updateSongTitle();
+
+ // 延迟隐藏加载状态,让用户看到完成状态
+ setTimeout(() => {
+ this.hideLoadingState();
+
+ // 自动开始播放
+ if (this.audio.src && !this.isPlaying && this.options.autoPlay) {
+ console.log('自动开始播放', this.audio.src, this.options.autoPlay);
+
+ // 确保音频上下文处于运行状态
+ if (this.audioContext && this.audioContext.state === 'suspended') {
+ console.log('恢复音频上下文');
+ this.audioContext.resume().then(() => {
+ console.log('音频上下文已恢复,开始播放');
+ this.play();
+ }).catch(error => {
+ console.error('恢复音频上下文失败:', error);
+ this.onError('无法恢复音频上下文,请点击播放按钮重试');
+ });
+ } else {
+ this.play();
+ }
+ }
+ }, 500);
+ }
+
+ // 更新歌曲标题的方法
+ updateSongTitle() {
+ if (!this.currentAudioUrl) return;
+
+ // 从URL中提取歌曲信息
+ let title = '未知歌曲';
+ try {
+ const urlParts = this.currentAudioUrl.split('/');
+ const filename = urlParts[urlParts.length - 1];
+ const decodedFilename = decodeURIComponent(filename);
+ const nameWithoutQuery = decodedFilename.split('?')[0];
+ title = nameWithoutQuery.split('.')[0].replace(/[-_]/g, ' ').trim();
+ } catch (e) {
+ console.error('Failed to extract song title:', e);
+ }
+
+ // 触发自定义事件,通知外部更新标题
+ const event = new CustomEvent('songLoaded', {
+ detail: {
+ title: title,
+ url: this.currentAudioUrl
+ }
+ });
+
+ // 从播放器容器向上冒泡事件
+ if (this.container && this.container.parentNode) {
+ this.container.parentNode.dispatchEvent(event);
+ }
+ }
+
+ handleAudioError(error) {
+ console.error('音频加载错误详情:', {
+ error: error,
+ audioError: this.audio.error,
+ currentSrc: this.audio.currentSrc,
+ networkState: this.audio.networkState,
+ readyState: this.audio.readyState
+ });
+
+ let errorMessage = '音频加载失败';
+
+ // 获取详细的错误信息
+ 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 = '音频下载过程中被中断';
+ break;
+ case 2:
+ errorMessage = '网络错误,请检查网络连接';
+ break;
+ case 3:
+ errorMessage = '音频解码错误,文件可能已损坏';
+ break;
+ case 4:
+ errorMessage = '音频格式不支持';
+ break;
+ default:
+ errorMessage = '未知错误';
+ }
+ }
+
+ // 网络状态分析
+ if (networkState === 3) { // NETWORK_NO_SOURCE
+ errorMessage += '\n\n无法获取音频资源,可能原因:\n';
+ errorMessage += '1. 音频文件不存在\n';
+ errorMessage += '2. 服务器拒绝访问\n';
+ errorMessage += '3. 跨域访问被阻止\n';
+ errorMessage += '4. 音频文件格式不支持\n\n';
+ errorMessage += '建议解决方案:\n';
+ errorMessage += '1. 检查音频文件是否存在\n';
+ errorMessage += '2. 尝试使用支持CORS的音频源\n';
+ errorMessage += '3. 下载音频到本地服务器\n';
+ errorMessage += '4. 使用其他音频地址';
+ }
+
+ this.onError(errorMessage);
+ this.hideLoadingState();
+ }
+
+ getNetworkStateText(state) {
+ const states = {
+ 0: 'NETWORK_EMPTY',
+ 1: 'NETWORK_IDLE',
+ 2: 'NETWORK_LOADING',
+ 3: 'NETWORK_NO_SOURCE'
+ };
+ return states[state] || 'UNKNOWN';
+ }
+
+ 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] || 'UNKNOWN';
+ }
+
+ // 可视化相关方法
+ switchVisualization(mode) {
+ this.currentVisualization = mode;
+
+ // 更新按钮状态
+ this.elements.vizButtons.forEach(btn => {
+ btn.classList.remove('active');
+ if (btn.dataset.mode === mode) {
+ btn.classList.add('active');
+ }
+ });
+ }
+
+ startVisualization() {
+ if (!this.analyser || !this.canvas) return;
+
+ this.stopVisualization();
+ this.animate();
+ }
+
+ stopVisualization() {
+ if (this.animationId) {
+ cancelAnimationFrame(this.animationId);
+ this.animationId = null;
+ }
+
+ if (this.ctx && this.canvas) {
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ }
+ }
+
+ animate() {
+ if (!this.analyser || !this.ctx || !this.canvas) 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() {
+ if (!this.ctx || !this.canvas) return;
+
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ 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, '#667eea');
+ gradient.addColorStop(1, '#764ba2');
+
+ this.ctx.fillStyle = gradient;
+ this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
+
+ x += barWidth + 1;
+ }
+ }
+
+ drawWave() {
+ if (!this.ctx || !this.canvas) return;
+
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ this.analyser.getByteTimeDomainData(this.dataArray);
+
+ this.ctx.lineWidth = 2;
+ this.ctx.strokeStyle = '#667eea';
+ this.ctx.beginPath();
+
+ const sliceWidth = this.canvas.width * 1.0 / 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.lineTo(this.canvas.width, this.canvas.height / 2);
+ this.ctx.stroke();
+ }
+
+ drawCircle() {
+ if (!this.ctx || !this.canvas) return;
+
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
+
+ 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 barHeight = (this.dataArray[i] / 255) * radius;
+
+ const x1 = centerX + Math.cos(angle) * radius;
+ const y1 = centerY + Math.sin(angle) * radius;
+ const x2 = centerX + Math.cos(angle) * (radius + barHeight);
+ const y2 = centerY + Math.sin(angle) * (radius + barHeight);
+
+ const gradient = this.ctx.createLinearGradient(x1, y1, x2, y2);
+ gradient.addColorStop(0, '#667eea');
+ gradient.addColorStop(1, '#764ba2');
+
+ this.ctx.strokeStyle = gradient;
+ this.ctx.lineWidth = 3;
+ this.ctx.beginPath();
+ this.ctx.moveTo(x1, y1);
+ this.ctx.lineTo(x2, y2);
+ this.ctx.stroke();
+ }
+ }
+
+ // 工具方法
+ formatTime(seconds) {
+ if (isNaN(seconds) || seconds < 0) return '00:00';
+
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = Math.floor(seconds % 60);
+
+ return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+ }
+
+ // 公共API方法
+ getCurrentTime() {
+ return this.audio.currentTime;
+ }
+
+ getDuration() {
+ return this.audio.duration;
+ }
+
+ getVolume() {
+ return this.audio.volume;
+ }
+
+ setVolume(volume) {
+ this.audio.volume = Math.max(0, Math.min(1, volume));
+ this.elements.volumeSlider.value = volume * 100;
+ this.elements.volumeValue.textContent = Math.round(volume * 100) + '%';
+ }
+
+ isPlaying() {
+ return this.isPlaying;
+ }
+
+ getCurrentUrl() {
+ return this.currentAudioUrl;
+ }
+
+ // 销毁方法
+ destroy() {
+ this.stop();
+ this.stopVisualization();
+
+ // 释放音频上下文资源
+ if (this.audioContext) {
+ this.audioContext.close();
+ this.audioContext = null;
+ }
+
+ // 释放音频元素资源
+ if (this.audio) {
+ this.audio.pause();
+ this.audio.currentTime = 0;
+ this.audio.src = '';
+ this.audio.srcObject = null;
+ this.audio = null;
+ }
+
+ // 清理Canvas和上下文
+ if (this.canvas) {
+ this.canvas.width = 0;
+ this.canvas.height = 0;
+ this.canvas = null;
+ this.ctx = null;
+ }
+
+ // 清理DOM元素
+ if (this.container) {
+ this.container.innerHTML = '';
+ this.container = null;
+ }
+
+ // 清理事件监听器和引用
+ this.elements = {};
+ this.currentAudioUrl = null;
+ }
+}
+
+// 导出对话框包装器
+export function createAudioPlayerDialog(options = {}) {
+ // 默认启用自动播放
+ const defaultOptions = {
+ autoPlay: true,
+ showVisualization: true,
+ visualizationMode: 'bars',
+ onError: null
+ };
+
+ const mergedOptions = { ...defaultOptions, ...options };
+
+ const dialog = document.createElement('dialog');
+ dialog.className = 'audio-player-dialog';
+
+ // 创建标题栏
+ const titleBar = document.createElement('div');
+ titleBar.className = 'dialog-title-bar';
+
+ // 创建歌曲信息区域
+ const songInfo = document.createElement('div');
+ songInfo.className = 'song-info';
+ songInfo.innerHTML = '
正在播放
';
+ titleBar.appendChild(songInfo);
+
+ // 创建关闭按钮
+ const closeBtn = document.createElement('button');
+ closeBtn.className = 'close-btn';
+ closeBtn.innerHTML = '×';
+ titleBar.appendChild(closeBtn);
+
+ dialog.appendChild(titleBar);
+
+ // 创建播放器容器
+ const playerContainer = document.createElement('div');
+ dialog.appendChild(playerContainer);
+
+ document.body.appendChild(dialog);
+
+ // 使用合并后的选项创建播放器
+ const player = new AudioPlayerComponent(playerContainer, mergedOptions);
+
+ // 更新歌曲信息的方法
+ const updateSongInfo = (url, title = null) => {
+ const songTitle = songInfo.querySelector('.song-title');
+ const songUrl = songInfo.querySelector('.song-url');
+
+ // 从URL中提取歌曲名称(如果可能)
+ let finalTitle = title || '未知歌曲';
+ if (!title && url) {
+ try {
+ const urlParts = url.split('/');
+ const filename = urlParts[urlParts.length - 1];
+ const decodedFilename = decodeURIComponent(filename);
+ const nameWithoutQuery = decodedFilename.split('?')[0];
+ finalTitle = nameWithoutQuery.split('.')[0].replace(/[-_]/g, ' ').trim();
+ } catch (e) {
+ console.error('Failed to extract song title:', e);
+ }
+ }
+
+ songTitle.textContent = finalTitle;
+ songUrl.textContent = url || '';
+ };
+
+ // 监听加载事件更新歌曲信息
+ const originalLoadAudio = player.loadAudio.bind(player);
+ player.loadAudio = function(url) {
+ updateSongInfo(url);
+ return originalLoadAudio(url);
+ };
+
+ // 监听歌曲加载完成事件,更新更准确的标题信息
+ dialog.addEventListener('songLoaded', (e) => {
+ const { title, url } = e.detail;
+ updateSongInfo(url, title);
+ });
+
+ closeBtn.addEventListener('click', () => {
+ player.stop();
+ dialog.close();
+ });
+
+ return {
+ dialog,
+ player,
+ open: (url) => {
+ if (url) player.loadAudio(url);
+ dialog.showModal();
+ },
+ close: () => dialog.close()
+ };
+}
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 42f9528..6441e31 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,860 +1,36 @@
-import './style.css'
+import { createAudioPlayerDialog } from './AudioPlayerComponent.js';
+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%)';
+// 创建播放器对话框
+const { open } = createAudioPlayerDialog({
+ autoPlay: true,
+ showVisualization: true,
+ onError: (message) => {
+ const errorMessage = document.getElementById('errorMessage');
+ if (errorMessage) {
+ errorMessage.textContent = message;
+ errorMessage.style.display = 'block';
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';
+ errorMessage.style.display = 'none';
+ }, 5000);
} else {
- this.elements.playIcon.style.display = 'inline';
- this.elements.pauseIcon.style.display = 'none';
+ alert(message);
}
}
+});
- 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));
- }
+// 设置测试按钮
+document.getElementById('loadAudio').addEventListener('click', () => {
+ const url = document.getElementById('audioUrl').value.trim();
+ if (url) {
+ open(url);
+ } else {
+ alert('请输入音频地址');
}
+});
- 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. 检查网络连接');
+document.querySelectorAll('.test-source-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.getElementById('audioUrl').value = btn.dataset.url;
+ });
});
\ No newline at end of file
diff --git a/src/style.css b/src/style.css
index 6c3469c..b457473 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,61 +1,50 @@
-: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;
}
+body {
+ margin: 0;
+ font-family: 'Arial', sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ color: #333;
+}
+
+/* 主容器样式 */
+.container {
+ text-align: center;
+ padding: 40px;
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 20px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+}
+
+.container h1 {
+ margin-bottom: 30px;
+ font-size: 2.5em;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
.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;
+ width: 100%;
+ max-width: 800px;
margin: 20px;
+ box-sizing: border-box;
}
/* URL输入区域 */
@@ -166,6 +155,7 @@ h1 {
cursor: pointer;
position: relative;
overflow: hidden;
+ transform: none; /* 确保没有旋转变换 */
}
.progress {
@@ -224,12 +214,6 @@ h1 {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
}
-#playPause {
- width: 60px;
- height: 60px;
- font-size: 24px;
-}
-
/* 音量控制 */
.volume-control {
display: flex;
@@ -279,6 +263,204 @@ h1 {
min-width: 30px;
}
+/* 加载动画样式 */
+.loading-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.8);
+ z-index: 2000;
+ display: none;
+ backdrop-filter: blur(5px);
+ transition: opacity 0.3s ease;
+ opacity: 0;
+}
+
+.loading-overlay.show {
+ opacity: 1;
+}
+
+.loading-message {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: white;
+ border-radius: 20px;
+ padding: 40px;
+ text-align: center;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ z-index: 2001;
+ min-width: 300px;
+ display: none;
+}
+
+.loading-message.fade-in {
+ animation: fadeInScale 0.3s ease-out;
+}
+
+.loading-message.fade-out {
+ animation: fadeOutScale 0.3s ease-in;
+}
+
+@keyframes fadeInScale {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -50%) scale(0.8);
+ }
+ 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.8);
+ }
+}
+
+.main-loader {
+ margin-bottom: 30px;
+}
+
+.circular-progress-simple {
+ position: relative;
+ display: inline-block;
+}
+
+.progress-bg {
+ fill: none;
+ stroke: #e0e0e0;
+ stroke-width: 4;
+}
+
+.circular-progress-bar {
+ fill: none;
+ stroke: url(#gradient);
+ stroke-width: 4;
+ stroke-linecap: round;
+ stroke-dasharray: 220;
+ stroke-dashoffset: 220;
+ transform: rotate(-90deg);
+ transform-origin: center;
+ transition: stroke-dashoffset 0.3s ease;
+}
+
+.progress-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 18px;
+ font-weight: bold;
+ color: #667eea;
+}
+
+.loading-info {
+ margin-bottom: 20px;
+}
+
+.loading-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 5px;
+}
+
+.loading-subtitle {
+ font-size: 14px;
+ color: #666;
+}
+
+.mini-spectrum {
+ display: flex;
+ justify-content: center;
+ gap: 4px;
+ margin-top: 20px;
+}
+
+.spectrum-bar {
+ width: 4px;
+ height: 20px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 2px;
+ animation: spectrumDance 1s infinite ease-in-out;
+}
+
+.spectrum-bar:nth-child(2) {
+ animation-delay: 0.1s;
+}
+
+.spectrum-bar:nth-child(3) {
+ animation-delay: 0.2s;
+}
+
+.spectrum-bar:nth-child(4) {
+ animation-delay: 0.3s;
+}
+
+@keyframes spectrumDance {
+ 0%, 100% {
+ transform: scaleY(1);
+ }
+ 50% {
+ transform: scaleY(1.5);
+ }
+}
+
+/* 错误提示样式 */
+.error-message {
+ background: #ffebee;
+ color: #c62828;
+ padding: 15px 20px;
+ border-radius: 10px;
+ border-left: 4px solid #f44336;
+ margin: 20px 0;
+ font-size: 14px;
+ line-height: 1.5;
+ white-space: pre-line;
+ box-shadow: 0 2px 8px rgba(244, 67, 54, 0.2);
+ animation: slideInFromTop 0.3s ease-out;
+}
+
+@keyframes slideInFromTop {
+ from {
+ transform: translateY(-20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* 成功提示样式 */
+.success-message {
+ animation: slideInFromRight 0.3s ease-out;
+}
+
+@keyframes slideInFromRight {
+ from {
+ transform: translateX(100%);
+ }
+ to {
+ transform: translateX(0);
+ }
+}
+
+/* 防止页面滚动 */
+body.loading-active {
+ overflow: hidden;
+}
+
/* 加载完成动画 */
@keyframes loading-complete {
0% {
@@ -331,7 +513,7 @@ h1 {
}
/* 加载弹出层响应式设计 */
-@media (max-width: 480px) {
+@media (max-width: 800px) {
.loading-message {
min-width: 180px;
max-width: 240px;
@@ -365,10 +547,98 @@ h1 {
will-change: contents;
}
-/* 防止加载时页面滚动 */
+/* 防止页面滚动 */
body.loading-active {
overflow: hidden;
-} 加载遮罩层 - 简洁设计 */
+}
+
+/* URL输入区域样式 */
+.url-input-section {
+ margin-bottom: 30px;
+ display: flex;
+ gap: 15px;
+ align-items: center;
+}
+
+#audioUrl {
+ flex: 1;
+ padding: 15px 20px;
+ border: 2px solid #e0e0e0;
+ border-radius: 12px;
+ font-size: 16px;
+ outline: none;
+ transition: all 0.3s ease;
+ background: white;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+#audioUrl:focus {
+ border-color: #667eea;
+ box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
+ transform: translateY(-2px);
+}
+
+#loadAudio {
+ padding: 15px 25px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white;
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: bold;
+ transition: all 0.3s ease;
+ box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
+ min-width: 120px;
+}
+
+#loadAudio:hover {
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
+}
+
+#loadAudio:active {
+ transform: translateY(-1px) scale(0.98);
+}
+
+#loadAudio:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
+}
+
+/* 测试音频源样式 */
+.test-audio-sources {
+ margin-bottom: 20px;
+}
+
+.test-source-btn {
+ padding: 10px 15px;
+ background: #f8f9fa;
+ color: #667eea;
+ border: 2px solid #e9ecef;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ margin: 2px;
+}
+
+.test-source-btn:hover {
+ background: #667eea;
+ color: white;
+ border-color: #667eea;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.test-source-btn:active {
+ transform: translateY(0);
+}
+
+/*加载遮罩层 - 简洁设计 */
.loading-overlay {
position: fixed;
top: 0;
@@ -453,9 +723,17 @@ body.loading-active {
gap: 15px;
}
+ .playback-controls {
+ flex-direction: row;
+ justify-content: center;
+ gap: 10px;
+ }
+
.volume-control {
margin-left: 0;
margin-top: 10px;
+ width: 100%;
+ justify-content: center;
}
}
@@ -630,7 +908,7 @@ body.loading-active {
stroke-width: 6;
}
-.circular-progress-simple .progress-bar {
+.circular-progress-simple .circular-progress-bar {
fill: none;
stroke: url(#gradient);
stroke-width: 6;
@@ -708,7 +986,7 @@ body.loading-active {
}
}
-.circular-progress-simple .progress-bar.loading {
+.circular-progress-simple .circular-progress-bar.loading {
animation: progress-fill 2s ease-out;
}
@@ -780,7 +1058,7 @@ body.loading-active {
}
}
-.circular-progress .progress-bar {
+.circular-progress .circular-progress-bar {
animation: progress-glow 2s ease-in-out infinite;
}
@@ -845,6 +1123,224 @@ body.loading-active {
animation: pulse 2s infinite;
}
+/* 音频播放器组件样式 */
+.audio-player-component {
+ background: rgba(255, 255, 255, 0.95);
+ padding: 20px;
+ width: 100%;
+ margin: 0;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* 可视化区域 */
+.visualization-section {
+ margin-bottom: 30px;
+}
+
+#visualizer {
+ width: 100%;
+ height: 300px;
+ border-radius: 15px;
+ background: linear-gradient(135deg, #1a1a1a, #2d2d2d);
+ box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.3);
+}
+
+.visualization-controls {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ margin-top: 15px;
+}
+
+.viz-btn {
+ padding: 8px 16px;
+ background: #f0f0f0;
+ border: none;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.3s ease;
+}
+
+.viz-btn:hover {
+ background: #e0e0e0;
+ transform: translateY(-1px);
+}
+
+.viz-btn.active {
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white;
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+/* 播放控制 */
+.player-controls {
+ margin-bottom: 5px;
+}
+
+.time-display {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 15px;
+ font-family: 'Courier New', monospace;
+ font-size: 14px;
+ color: #666;
+}
+
+.progress-container {
+ position: relative;
+ margin-bottom: 15px;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 8px;
+ background: #e0e0e0;
+ border-radius: 4px;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ transform: none; /* 确保没有旋转变换 */
+}
+
+.progress {
+ height: 100%;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ border-radius: 4px;
+ width: 0%;
+ transition: width 0.1s ease;
+ position: relative;
+}
+
+.progress::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ animation: progressShine 2s infinite;
+}
+
+@keyframes progressShine {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
+}
+
+.progress-handle {
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 16px;
+ height: 16px;
+ background: white;
+ border: 3px solid #667eea;
+ border-radius: 50%;
+ cursor: grab;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+ transition: all 0.2s ease;
+ left: 0%;
+}
+
+.progress-handle:hover {
+ transform: translate(-50%, -50%) scale(1.2);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.progress-handle:active {
+ cursor: grabbing;
+}
+
+/* 控制按钮 */
+.control-buttons {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 15px;
+ margin-bottom: 20px;
+}
+
+/* 播放控制按钮组 - 左对齐 */
+.playback-controls {
+ display: flex;
+ gap: 15px;
+ align-items: center;
+}
+
+.control-btn {
+ width: 50px;
+ height: 50px;
+ border: none;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white;
+ font-size: 20px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.control-btn:hover {
+ transform: translateY(-2px) scale(1.05);
+ box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
+}
+
+.control-btn:active {
+ transform: translateY(0) scale(0.95);
+}
+
+.volume-control {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-left: auto;
+}
+
+.volume-icon {
+ font-size: 18px;
+ color: #666;
+}
+
+.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 6px rgba(0, 0, 0, 0.2);
+ transition: all 0.2s ease;
+}
+
+.volume-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
+ box-shadow: 0 4px 10px rgba(102, 126, 234, 0.4);
+}
+
+#volumeValue {
+ font-size: 12px;
+ color: #666;
+ min-width: 30px;
+ font-weight: 500;
+}
+
button {
border-radius: 8px;
border: 1px solid transparent;
@@ -875,4 +1371,280 @@ button:focus-visible {
button {
background-color: #f9f9f9;
}
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .audio-player {
+ padding: 20px;
+ margin: 20px;
+ }
+
+ .audio-player-dialog {
+ width: 95vw;
+ margin: 0 auto;
+ }
+
+ .url-input-section {
+ flex-direction: column;
+ }
+
+ #loadAudio {
+ width: 100%;
+ margin-top: 10px;
+ }
+
+ .control-buttons {
+ flex-wrap: wrap;
+ gap: 10px;
+ }
+
+ .volume-control {
+ margin-left: 0;
+ margin-top: 10px;
+ width: 100%;
+ justify-content: center;
+ }
+
+ .volume-slider {
+ flex: 1;
+ max-width: 150px;
+ }
+
+ #visualizer {
+ height: 200px;
+ }
+
+ .visualization-controls {
+ flex-wrap: wrap;
+ gap: 5px;
+ }
+
+ .viz-btn {
+ padding: 6px 12px;
+ font-size: 11px;
+ }
+}
+
+@media (max-width: 480px) {
+ .audio-player {
+ padding: 15px;
+ margin: 10px;
+ }
+
+ .loading-message {
+ padding: 30px 20px;
+ min-width: 250px;
+ }
+
+ .test-audio-sources p {
+ font-size: 11px !important;
+ }
+
+ .test-source-btn {
+ padding: 8px 12px;
+ font-size: 11px;
+ }
+}
+
+/* 深色模式支持 */
+@media (prefers-color-scheme: dark) {
+ .audio-player {
+ background: rgba(30, 30, 30, 0.95);
+ color: #e0e0e0;
+ }
+
+ #audioUrl {
+ background: #3d3d3d;
+ border-color: #555;
+ color: #e0e0e0;
+ }
+
+ #audioUrl:focus {
+ border-color: #8a9bff;
+ }
+
+ .test-source-btn {
+ background: #3d3d3d;
+ border-color: #555;
+ color: #8a9bff;
+ }
+
+ .test-source-btn:hover {
+ background: #8a9bff;
+ color: white;
+ }
+
+ .viz-btn {
+ background: #3d3d3d;
+ color: #e0e0e0;
+ }
+
+ .viz-btn:hover {
+ background: #4d4d4d;
+ }
+
+ .viz-btn.active {
+ background: linear-gradient(135deg, #8a9bff, #b388ff);
+ }
+
+ .time-display {
+ color: #e0e0e0;
+ }
+
+ .progress-bar {
+ background: #3d3d3d;
+ }
+
+ .volume-slider {
+ background: #3d3d3d;
+ }
+
+ .volume-icon {
+ color: #e0e0e0;
+ }
+
+ #volumeValue {
+ color: #e0e0e0;
+ }
+
+ .error-message {
+ background: #4d1a1a;
+ color: #ff8a80;
+ border-color: #f44336;
+ }
+}
+
+/* 高对比度模式 */
+@media (prefers-contrast: high) {
+ .control-btn,
+ .viz-btn.active,
+ #loadAudio {
+ border: 2px solid currentColor;
+ }
+
+ .progress-bar {
+ border: 1px solid #333;
+ }
+
+ .volume-slider::-webkit-slider-thumb {
+ border: 2px solid white;
+ }
+}
+
+/* 基础样式 */
+/* 对话框样式 */
+.audio-player-dialog {
+ border: none;
+ border-radius: 20px;
+ padding: 0;
+ background: white;
+ max-width: 95vw;
+ max-height: 95vh;
+ overflow: hidden;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ width: 800px;
+ height: 600px;
+ min-width: 320px;
+}
+
+.audio-player-dialog::backdrop {
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(5px);
+}
+
+/* 对话框标题栏 */
+.dialog-title-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ color: white;
+ border-radius: 20px 20px 0 0;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+/* 歌曲信息 */
+.song-info {
+ max-width: 80%;
+ overflow: hidden;
+}
+
+.song-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 3px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.song-url {
+ font-size: 12px;
+ opacity: 0.9;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* 关闭按钮 */
+.close-btn {
+ background: rgba(250, 250, 250, 0.6);
+ border: 1px solid #eee;
+ color: white;
+ font-size: 20px;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ cursor: pointer;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(255, 255, 255, 0.3);
+ margin-left: 15px;
+ font-size: 24px;
+ cursor: pointer;
+ color: #fff;
+}
+
+.url-input-section {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+.url-input-section input {
+ flex: 1;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+}
+
+.url-input-section button {
+ padding: 10px 15px;
+ background: #667eea;
+ color: white;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.test-audio-sources {
+ margin-top: 20px;
+}
+
+.test-source-btn {
+ margin: 5px;
+ padding: 8px 12px;
+ background: #f0f0f0;
+ border: 1px solid #ddd;
+ border-radius: 5px;
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index 5de32dc..f6ec446 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -3,7 +3,7 @@ import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000,
- host: true,
+ host: false,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',