调整播放器UI,播放器组件化。
This commit is contained in:
parent
2d0d0ff0fb
commit
62de130228
118
index.html
118
index.html
@ -7,104 +7,28 @@
|
||||
<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>
|
||||
|
||||
1073
src/AudioPlayerComponent.js
Normal file
1073
src/AudioPlayerComponent.js
Normal file
File diff suppressed because it is too large
Load Diff
878
src/main.js
878
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;
|
||||
});
|
||||
});
|
||||
886
src/style.css
886
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;
|
||||
}
|
||||
@ -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': '*',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user