diff --git a/index.html b/index.html index b4c255f..983401f 100644 --- a/index.html +++ b/index.html @@ -7,104 +7,28 @@ 网络音乐播放器 -
- -
- - -
- - -
-

测试音频源:

-
- - -
-
- - - - - - - - - - - -
- -
- - - -
-
- - -
-
- 00:00 - / - 00:00 -
-
-
-
-
-
-
-
- - -
- - -
- 🔊 - - 70% -
-
- - - +
+

网络音乐播放器

+ +
+ +
+ + +
+

测试音频源:

+
+ + + + +
+
+ + +
+
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 = ` +
+ + + + +
+ +
+ + + +
+
+ + +
+
+
+
+
+
+
+
+ 00:00 + / + 00:00 +
+
+ + +
+ +
+ +
+ + +
+ 🔊 + + 70% +
+
+ + + +
+ `; + } + + 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': '*',