调整播放器UI,播放器组件化。

This commit is contained in:
陈乾 2026-01-30 23:45:58 +08:00
parent 2d0d0ff0fb
commit 62de130228
5 changed files with 1951 additions and 1006 deletions

View File

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

1073
src/AudioPlayerComponent.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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));
}
}
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.getElementById('loadAudio').addEventListener('click', () => {
const url = document.getElementById('audioUrl').value.trim();
if (url) {
open(url);
} else {
alert('请输入音频地址');
}
});
document.querySelectorAll('.test-source-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.getElementById('audioUrl').value = btn.dataset.url;
});
});

View File

@ -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;
@ -876,3 +1372,279 @@ button:focus-visible {
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;
}

View File

@ -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': '*',