first commit
This commit is contained in:
commit
2d0d0ff0fb
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
111
index.html
Normal file
111
index.html
Normal file
@ -0,0 +1,111 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>网络音乐播放器</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="audio-player">
|
||||
<!-- 音频URL输入 -->
|
||||
<div class="url-input-section">
|
||||
<input type="text" id="audioUrl" placeholder="输入网络音乐地址,如:http://music.163.com/song/media/outer/url?id=447925558.mp3" value="/DocServer/repository/file/view/73901-root/last/content?key=1769765152330.mp3">
|
||||
<button id="loadAudio">加载音乐</button>
|
||||
</div>
|
||||
|
||||
<!-- 备用音频源 -->
|
||||
<div class="test-audio-sources">
|
||||
<p style="margin: 10px 0 5px 0; font-size: 12px; color: #666;">测试音频源:</p>
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button class="test-source-btn" data-url="/DocServer/repository/file/view/73904-root/last/content?id=447925558.mp3">本地代理测试1</button>
|
||||
<button class="test-source-btn" data-url="/DocServer/repository/file/view/73901-root/last/content?key=1769765152330.mp3">本地代理测试2</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div id="errorMessage" class="error-message" style="display: none;"></div>
|
||||
|
||||
<!-- 加载遮罩层 -->
|
||||
<div id="loadingOverlay" class="loading-overlay" style="display: none;"></div>
|
||||
|
||||
<!-- 加载提示弹出层(简化版) -->
|
||||
<div id="loadingMessage" class="loading-message" style="display: none;">
|
||||
<!-- 主要加载动画 -->
|
||||
<div class="main-loader">
|
||||
<div class="circular-progress-simple">
|
||||
<svg width="80" height="80">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle class="progress-bg" cx="40" cy="40" r="35"></circle>
|
||||
<circle class="progress-bar" cx="40" cy="40" r="35" id="circularProgress"></circle>
|
||||
</svg>
|
||||
<div class="progress-text" id="circularProgressText">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载文字信息 -->
|
||||
<div class="loading-info">
|
||||
<div class="loading-title" id="loadingTitle">正在加载音频</div>
|
||||
<div class="loading-subtitle" id="loadingSubtitle">请稍候...</div>
|
||||
</div>
|
||||
|
||||
<!-- 简单的音乐频谱指示器 -->
|
||||
<div class="mini-spectrum">
|
||||
<div class="spectrum-bar"></div>
|
||||
<div class="spectrum-bar"></div>
|
||||
<div class="spectrum-bar"></div>
|
||||
<div class="spectrum-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 可视化效果 -->
|
||||
<div class="visualization-section">
|
||||
<canvas id="visualizer" width="100%" height="100%"></canvas>
|
||||
<div class="visualization-controls">
|
||||
<button class="viz-btn active" data-mode="bars">条形图</button>
|
||||
<button class="viz-btn" data-mode="wave">波形图</button>
|
||||
<button class="viz-btn" data-mode="circle">圆形频谱</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 播放控制 -->
|
||||
<div class="player-controls">
|
||||
<div class="time-display">
|
||||
<span id="currentTime">00:00</span>
|
||||
<span>/</span>
|
||||
<span id="duration">00:00</span>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" id="progressBar">
|
||||
<div class="progress" id="progress"></div>
|
||||
<div class="progress-handle" id="progressHandle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="control-buttons">
|
||||
<button id="playPause" class="control-btn">
|
||||
<span class="play-icon">▶️</span>
|
||||
<span class="pause-icon" style="display: none;">⏸️</span>
|
||||
</button>
|
||||
<button id="stop" class="control-btn">⏹️</button>
|
||||
<div class="volume-control">
|
||||
<span class="volume-icon">🔊</span>
|
||||
<input type="range" id="volumeSlider" min="0" max="100" value="70" class="volume-slider">
|
||||
<span id="volumeValue">70%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的音频元素 -->
|
||||
<audio id="audioElement" crossorigin="anonymous"></audio>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1105
package-lock.json
generated
Normal file
1105
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
Normal file
14
package.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "net-audio-player",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
9
src/counter.js
Normal file
9
src/counter.js
Normal file
@ -0,0 +1,9 @@
|
||||
export function setupCounter(element) {
|
||||
let counter = 0
|
||||
const setCounter = (count) => {
|
||||
counter = count
|
||||
element.innerHTML = `count is ${counter}`
|
||||
}
|
||||
element.addEventListener('click', () => setCounter(counter + 1))
|
||||
setCounter(0)
|
||||
}
|
||||
1
src/javascript.svg
Normal file
1
src/javascript.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>
|
||||
|
After Width: | Height: | Size: 995 B |
860
src/main.js
Normal file
860
src/main.js
Normal file
@ -0,0 +1,860 @@
|
||||
import './style.css'
|
||||
|
||||
class AudioPlayer {
|
||||
constructor() {
|
||||
this.audio = document.getElementById('audioElement');
|
||||
this.canvas = document.getElementById('visualizer');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.audioContext = null;
|
||||
this.analyser = null;
|
||||
this.source = null;
|
||||
this.dataArray = null;
|
||||
this.bufferLength = null;
|
||||
this.animationId = null;
|
||||
this.isPlaying = false;
|
||||
this.currentVisualization = 'bars';
|
||||
|
||||
this.initializeElements();
|
||||
this.setupEventListeners();
|
||||
this.setupCanvas();
|
||||
this.initializeAudioContext();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.elements = {
|
||||
audioUrl: document.getElementById('audioUrl'),
|
||||
loadAudio: document.getElementById('loadAudio'),
|
||||
playPause: document.getElementById('playPause'),
|
||||
stop: document.getElementById('stop'),
|
||||
progressBar: document.getElementById('progressBar'),
|
||||
progress: document.getElementById('progress'),
|
||||
progressHandle: document.getElementById('progressHandle'),
|
||||
currentTime: document.getElementById('currentTime'),
|
||||
duration: document.getElementById('duration'),
|
||||
volumeSlider: document.getElementById('volumeSlider'),
|
||||
volumeValue: document.getElementById('volumeValue'),
|
||||
vizButtons: document.querySelectorAll('.viz-btn'),
|
||||
playIcon: document.querySelector('.play-icon'),
|
||||
pauseIcon: document.querySelector('.pause-icon'),
|
||||
errorMessage: document.getElementById('errorMessage'),
|
||||
loadingMessage: document.getElementById('loadingMessage'),
|
||||
loadingTitle: document.getElementById('loadingTitle'),
|
||||
loadingSubtitle: document.getElementById('loadingSubtitle'),
|
||||
circularProgress: document.getElementById('circularProgress'),
|
||||
circularProgressText: document.getElementById('circularProgressText')
|
||||
};
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// 加载音频
|
||||
this.elements.loadAudio.addEventListener('click', () => this.loadAudio());
|
||||
|
||||
// 播放控制
|
||||
this.elements.playPause.addEventListener('click', () => this.togglePlayPause());
|
||||
this.elements.stop.addEventListener('click', () => this.stop());
|
||||
|
||||
// 进度条控制
|
||||
this.elements.progressBar.addEventListener('click', (e) => this.seekTo(e));
|
||||
this.elements.progressHandle.addEventListener('mousedown', () => this.startDragging());
|
||||
|
||||
// 音量控制
|
||||
this.elements.volumeSlider.addEventListener('input', () => this.updateVolume());
|
||||
|
||||
// 音频事件
|
||||
this.audio.addEventListener('loadedmetadata', () => this.updateDuration());
|
||||
this.audio.addEventListener('timeupdate', () => this.updateProgress());
|
||||
this.audio.addEventListener('ended', () => this.onAudioEnded());
|
||||
this.audio.addEventListener('error', (e) => this.handleAudioError(e));
|
||||
this.audio.addEventListener('loadstart', () => this.showLoadingState());
|
||||
this.audio.addEventListener('canplay', () => this.onAudioCanPlay());
|
||||
this.audio.addEventListener('canplaythrough', () => this.onAudioCanPlayThrough());
|
||||
this.audio.addEventListener('progress', () => this.onAudioProgress());
|
||||
|
||||
// 可视化切换
|
||||
this.elements.vizButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => this.switchVisualization(btn.dataset.mode));
|
||||
});
|
||||
|
||||
// 测试音频源按钮
|
||||
document.querySelectorAll('.test-source-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.elements.audioUrl.value = btn.dataset.url;
|
||||
this.loadAudio();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupCanvas() {
|
||||
const resizeCanvas = () => {
|
||||
const container = this.canvas.parentElement;
|
||||
const rect = container.getBoundingClientRect();
|
||||
this.canvas.width = Math.min(600, rect.width - 40);
|
||||
this.canvas.height = 300;
|
||||
};
|
||||
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
}
|
||||
|
||||
initializeAudioContext() {
|
||||
try {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
this.bufferLength = this.analyser.frequencyBinCount;
|
||||
this.dataArray = new Uint8Array(this.bufferLength);
|
||||
|
||||
// 连接音频源到分析器
|
||||
this.source = this.audioContext.createMediaElementSource(this.audio);
|
||||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
} catch (error) {
|
||||
console.warn('Web Audio API 不支持或初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadAudio() {
|
||||
const url = this.elements.audioUrl.value.trim();
|
||||
if (!url) {
|
||||
alert('请输入音频地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试多种跨域解决方案
|
||||
this.loadAudioWithCORS(url);
|
||||
}
|
||||
|
||||
loadAudioWithCORS(url) {
|
||||
this.hideLoadingState();
|
||||
|
||||
// 开始加载过程
|
||||
this.startLoadingAnimation();
|
||||
|
||||
// 直接尝试加载音频(跳过URL测试,提高响应速度)
|
||||
this.audio.crossOrigin = 'anonymous';
|
||||
this.audio.src = url;
|
||||
this.audio.load();
|
||||
|
||||
// 监听加载进度
|
||||
this.setupLoadingProgress();
|
||||
|
||||
// 如果音频上下文被暂停,恢复它
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
}
|
||||
|
||||
startLoadingAnimation() {
|
||||
this.showLoadingState();
|
||||
this.updateLoadingTitle('正在加载音频');
|
||||
this.updateLoadingSubtitle('请稍候...');
|
||||
this.updateLoadingProgress(0);
|
||||
|
||||
// 添加加载动画的延迟效果,让用户体验更自然
|
||||
setTimeout(() => {
|
||||
this.updateLoadingProgress(10);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updateLoadingText(text) {
|
||||
if (this.elements.loadingText) {
|
||||
this.elements.loadingText.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingProgress(percent) {
|
||||
if (this.elements.circularProgress) {
|
||||
// 圆形进度条计算
|
||||
const circumference = 2 * Math.PI * 25; // 半径25
|
||||
const offset = circumference - (percent / 100) * circumference;
|
||||
this.elements.circularProgress.style.strokeDashoffset = offset;
|
||||
}
|
||||
if (this.elements.circularProgressText) {
|
||||
this.elements.circularProgressText.textContent = Math.round(percent) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
setupLoadingProgress() {
|
||||
let lastProgress = 0;
|
||||
let loadStartTime = Date.now();
|
||||
let timeoutWarningShown = false;
|
||||
|
||||
const checkProgress = () => {
|
||||
const currentTime = Date.now();
|
||||
const elapsedTime = (currentTime - loadStartTime) / 1000; // 秒
|
||||
|
||||
// 超时警告(30秒)
|
||||
if (elapsedTime > 30 && !timeoutWarningShown) {
|
||||
timeoutWarningShown = true;
|
||||
this.updateLoadingTitle('加载时间较长');
|
||||
this.updateLoadingSubtitle('请耐心等待...');
|
||||
console.warn('音频加载超过30秒,可能存在网络问题');
|
||||
}
|
||||
|
||||
// 超时处理(60秒)
|
||||
if (elapsedTime > 60) {
|
||||
console.error('音频加载超时(60秒)');
|
||||
this.showError('音频加载超时\n\n可能原因:\n1. 网络连接缓慢\n2. 音频文件过大\n3. 服务器响应缓慢\n\n建议:\n1. 检查网络连接\n2. 尝试其他音频源\n3. 刷新页面重试');
|
||||
this.hideLoadingState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.audio.buffered.length > 0) {
|
||||
const bufferedEnd = this.audio.buffered.end(this.audio.buffered.length - 1);
|
||||
const duration = this.audio.duration || 0;
|
||||
|
||||
if (duration > 0) {
|
||||
const progress = (bufferedEnd / duration) * 100;
|
||||
this.updateLoadingProgress(progress);
|
||||
|
||||
// 更新加载文本 - 简化版
|
||||
if (progress < 30) {
|
||||
this.updateLoadingTitle('正在连接...');
|
||||
this.updateLoadingSubtitle('');
|
||||
} else if (progress < 60) {
|
||||
this.updateLoadingTitle(`已加载 ${Math.round(progress)}%`);
|
||||
this.updateLoadingSubtitle('');
|
||||
} else if (progress < 90) {
|
||||
this.updateLoadingTitle('正在缓冲...');
|
||||
this.updateLoadingSubtitle('');
|
||||
} else {
|
||||
this.updateLoadingTitle('即将播放');
|
||||
this.updateLoadingSubtitle('');
|
||||
}
|
||||
|
||||
lastProgress = progress;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续检查进度
|
||||
if (this.audio.readyState < 4 && this.audio.networkState !== 3) {
|
||||
setTimeout(checkProgress, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始检查进度
|
||||
setTimeout(checkProgress, 100);
|
||||
}
|
||||
|
||||
isValidAudioUrl(url) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
// 检查协议
|
||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
return false;
|
||||
}
|
||||
// 检查文件扩展名或路径模式
|
||||
const audioExtensions = ['.mp3', '.wav', '.ogg', '.m4a', '.flac'];
|
||||
const hasAudioExtension = audioExtensions.some(ext =>
|
||||
urlObj.pathname.toLowerCase().includes(ext)
|
||||
);
|
||||
// 或者包含音频相关的路径模式
|
||||
const hasAudioPattern = /(audio|music|song|media|sound)/i.test(urlObj.href);
|
||||
|
||||
return hasAudioExtension || hasAudioPattern;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
testAudioUrl(url) {
|
||||
return new Promise((resolve) => {
|
||||
const testAudio = new Audio();
|
||||
testAudio.crossOrigin = 'anonymous';
|
||||
|
||||
let resolved = false;
|
||||
|
||||
// 设置超时
|
||||
const timeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
resolve(false);
|
||||
}
|
||||
}, 8000); // 8秒超时
|
||||
|
||||
testAudio.addEventListener('loadedmetadata', () => {
|
||||
clearTimeout(timeout);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
testAudio.addEventListener('error', (e) => {
|
||||
clearTimeout(timeout);
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
console.log('测试音频失败:', e);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加加载事件监听
|
||||
testAudio.addEventListener('loadstart', () => {
|
||||
console.log('开始测试音频加载:', url);
|
||||
});
|
||||
|
||||
testAudio.src = url;
|
||||
testAudio.load();
|
||||
});
|
||||
}
|
||||
|
||||
tryProxyLoad(url) {
|
||||
// 尝试使用代理服务器加载音频
|
||||
const proxyUrl = `/DocServer/audio?url=${encodeURIComponent(url)}`;
|
||||
|
||||
console.log('尝试代理加载:', proxyUrl);
|
||||
|
||||
// 直接尝试代理URL
|
||||
this.audio.crossOrigin = 'anonymous';
|
||||
this.audio.src = proxyUrl;
|
||||
this.audio.load();
|
||||
|
||||
// 给代理加载设置超时
|
||||
setTimeout(() => {
|
||||
if (this.audio.networkState === 3) { // NETWORK_NO_SOURCE
|
||||
console.error('代理加载超时');
|
||||
this.showError('代理加载失败\n\n可能的原因:\n1. 代理服务器无法访问目标音频\n2. 目标音频需要认证\n3. 音频文件不存在\n\n建议:\n1. 使用支持CORS的音频源\n2. 下载音频到本地服务器\n3. 使用其他音频地址');
|
||||
}
|
||||
}, 10000); // 10秒超时
|
||||
}
|
||||
|
||||
togglePlayPause() {
|
||||
if (this.isPlaying) {
|
||||
this.pause();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.audio.src) {
|
||||
this.audio.play().then(() => {
|
||||
this.isPlaying = true;
|
||||
this.updatePlayButton();
|
||||
this.startVisualization();
|
||||
document.querySelector('.audio-player').classList.add('playing');
|
||||
|
||||
// 显示播放成功提示(短暂显示)
|
||||
console.log('🎵 音频播放开始');
|
||||
this.showSuccessMessage('音频加载完成,开始播放!');
|
||||
|
||||
}).catch(error => {
|
||||
console.error('播放失败:', error);
|
||||
this.showError('音频播放失败,请重试');
|
||||
this.isPlaying = false;
|
||||
this.updatePlayButton();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showSuccessMessage(message) {
|
||||
// 创建成功提示元素
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'success-message';
|
||||
successDiv.textContent = message;
|
||||
successDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
font-size: 14px;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(successDiv);
|
||||
|
||||
// 动画显示
|
||||
setTimeout(() => {
|
||||
successDiv.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
// 3秒后自动隐藏
|
||||
setTimeout(() => {
|
||||
successDiv.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (successDiv.parentNode) {
|
||||
successDiv.parentNode.removeChild(successDiv);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.audio.pause();
|
||||
this.isPlaying = false;
|
||||
this.updatePlayButton();
|
||||
this.stopVisualization();
|
||||
document.querySelector('.audio-player').classList.remove('playing');
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.audio.pause();
|
||||
this.audio.currentTime = 0;
|
||||
this.isPlaying = false;
|
||||
this.updatePlayButton();
|
||||
this.stopVisualization();
|
||||
this.updateProgress();
|
||||
document.querySelector('.audio-player').classList.remove('playing');
|
||||
}
|
||||
|
||||
updatePlayButton() {
|
||||
if (this.isPlaying) {
|
||||
this.elements.playIcon.style.display = 'none';
|
||||
this.elements.pauseIcon.style.display = 'inline';
|
||||
} else {
|
||||
this.elements.playIcon.style.display = 'inline';
|
||||
this.elements.pauseIcon.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
updateDuration() {
|
||||
const duration = this.audio.duration;
|
||||
this.elements.duration.textContent = this.formatTime(duration);
|
||||
|
||||
// 音频元数据加载完成,更新加载状态
|
||||
if (this.elements.loadingMessage.style.display !== 'none') {
|
||||
this.updateLoadingTitle('加载完成');
|
||||
this.updateLoadingSubtitle('');
|
||||
this.updateLoadingProgress(80);
|
||||
console.log('音频元数据加载完成,时长:', this.formatTime(duration));
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
const currentTime = this.audio.currentTime;
|
||||
const duration = this.audio.duration;
|
||||
|
||||
if (duration) {
|
||||
const progressPercent = (currentTime / duration) * 100;
|
||||
this.elements.progress.style.width = progressPercent + '%';
|
||||
this.elements.progressHandle.style.left = progressPercent + '%';
|
||||
}
|
||||
|
||||
this.elements.currentTime.textContent = this.formatTime(currentTime);
|
||||
}
|
||||
|
||||
seekTo(e) {
|
||||
const rect = this.elements.progressBar.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const width = rect.width;
|
||||
const percentage = clickX / width;
|
||||
|
||||
if (this.audio.duration) {
|
||||
this.audio.currentTime = percentage * this.audio.duration;
|
||||
}
|
||||
}
|
||||
|
||||
startDragging() {
|
||||
const handleMouseMove = (e) => {
|
||||
const rect = this.elements.progressBar.getBoundingClientRect();
|
||||
const clickX = e.clientX - rect.left;
|
||||
const width = rect.width;
|
||||
let percentage = clickX / width;
|
||||
|
||||
percentage = Math.max(0, Math.min(1, percentage));
|
||||
|
||||
if (this.audio.duration) {
|
||||
this.audio.currentTime = percentage * this.audio.duration;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
|
||||
updateVolume() {
|
||||
const volume = this.elements.volumeSlider.value / 100;
|
||||
this.audio.volume = volume;
|
||||
this.elements.volumeValue.textContent = Math.round(volume * 100) + '%';
|
||||
}
|
||||
|
||||
onAudioEnded() {
|
||||
this.isPlaying = false;
|
||||
this.updatePlayButton();
|
||||
this.stopVisualization();
|
||||
document.querySelector('.audio-player').classList.remove('playing');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const errorDiv = this.elements.errorMessage;
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.display = 'block';
|
||||
|
||||
// 5秒后自动隐藏错误信息
|
||||
setTimeout(() => {
|
||||
errorDiv.style.display = 'none';
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
showLoadingState() {
|
||||
// 显示遮罩层和加载弹出层
|
||||
if (this.elements.loadingOverlay) {
|
||||
this.elements.loadingOverlay.style.display = 'block';
|
||||
this.elements.loadingOverlay.classList.add('show');
|
||||
}
|
||||
|
||||
this.elements.loadingMessage.style.display = 'block';
|
||||
this.elements.loadingMessage.classList.add('fade-in');
|
||||
this.elements.errorMessage.style.display = 'none';
|
||||
|
||||
// 防止页面滚动
|
||||
document.body.classList.add('loading-active');
|
||||
|
||||
const loadBtn = this.elements.loadAudio;
|
||||
loadBtn.textContent = '加载中...';
|
||||
loadBtn.disabled = true;
|
||||
|
||||
// 重置进度
|
||||
this.updateLoadingProgress(0);
|
||||
this.updateLoadingTitle('正在加载音频');
|
||||
this.updateLoadingSubtitle('请稍候...');
|
||||
|
||||
// 启动加载进度监听
|
||||
this.setupLoadingProgress();
|
||||
}
|
||||
|
||||
hideLoadingState() {
|
||||
this.elements.loadingMessage.classList.add('fade-out');
|
||||
|
||||
setTimeout(() => {
|
||||
this.elements.loadingMessage.style.display = 'none';
|
||||
this.elements.loadingMessage.classList.remove('fade-in', 'fade-out');
|
||||
|
||||
if (this.elements.loadingOverlay) {
|
||||
this.elements.loadingOverlay.style.display = 'none';
|
||||
this.elements.loadingOverlay.classList.remove('show');
|
||||
}
|
||||
|
||||
// 恢复页面滚动
|
||||
document.body.classList.remove('loading-active');
|
||||
}, 300);
|
||||
|
||||
const loadBtn = this.elements.loadAudio;
|
||||
loadBtn.textContent = '加载音乐';
|
||||
loadBtn.disabled = false;
|
||||
}
|
||||
|
||||
updateLoadingTitle(text) {
|
||||
if (this.elements.loadingTitle) {
|
||||
this.elements.loadingTitle.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingSubtitle(text) {
|
||||
if (this.elements.loadingSubtitle) {
|
||||
this.elements.loadingSubtitle.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
onAudioProgress() {
|
||||
// 更新加载进度
|
||||
if (this.audio.buffered.length > 0 && this.elements.loadingMessage.style.display !== 'none') {
|
||||
const bufferedEnd = this.audio.buffered.end(this.audio.buffered.length - 1);
|
||||
const duration = this.audio.duration || 0;
|
||||
|
||||
if (duration > 0) {
|
||||
const progress = (bufferedEnd / duration) * 100;
|
||||
this.updateLoadingProgress(progress);
|
||||
|
||||
// 更新加载文本
|
||||
if (progress < 30) {
|
||||
this.updateLoadingText('正在连接音频服务器...');
|
||||
} else if (progress < 60) {
|
||||
this.updateLoadingText('正在接收音频数据...');
|
||||
} else if (progress < 90) {
|
||||
this.updateLoadingText('正在缓冲音频...');
|
||||
} else {
|
||||
this.updateLoadingText('即将开始播放...');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onAudioCanPlay() {
|
||||
console.log('音频可以开始播放');
|
||||
// 音频已经有足够数据开始播放,但继续显示加载状态
|
||||
if (this.elements.loadingMessage.style.display !== 'none') {
|
||||
this.updateLoadingText('音频缓冲完成,准备播放...');
|
||||
this.updateLoadingProgress(95);
|
||||
}
|
||||
}
|
||||
|
||||
onAudioCanPlayThrough() {
|
||||
console.log('音频可以流畅播放');
|
||||
// 音频可以流畅播放,隐藏加载状态并自动播放
|
||||
|
||||
// 显示加载完成状态
|
||||
if (this.elements.loadingMessage.style.display !== 'none') {
|
||||
this.updateLoadingTitle('加载完成');
|
||||
this.updateLoadingSubtitle('');
|
||||
this.updateLoadingProgress(100);
|
||||
}
|
||||
|
||||
// 延迟隐藏加载状态,让用户看到完成状态
|
||||
setTimeout(() => {
|
||||
this.hideLoadingState();
|
||||
|
||||
// 自动开始播放
|
||||
if (this.audio.src && !this.isPlaying) {
|
||||
console.log('自动开始播放');
|
||||
this.play();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
handleAudioError(error) {
|
||||
console.error('音频加载错误详情:', {
|
||||
error: error,
|
||||
audioError: this.audio.error,
|
||||
currentSrc: this.audio.currentSrc,
|
||||
networkState: this.audio.networkState,
|
||||
readyState: this.audio.readyState
|
||||
});
|
||||
|
||||
let errorMessage = '音频加载失败';
|
||||
let solutions = [];
|
||||
|
||||
// 获取详细的错误信息
|
||||
const audioError = this.audio.error;
|
||||
const networkState = this.audio.networkState;
|
||||
const readyState = this.audio.readyState;
|
||||
|
||||
console.log('网络状态:', this.getNetworkStateText(networkState));
|
||||
console.log('就绪状态:', this.getReadyStateText(readyState));
|
||||
|
||||
if (audioError) {
|
||||
switch (audioError.code) {
|
||||
case 1:
|
||||
errorMessage = '音频下载过程中被中断';
|
||||
solutions = ['检查网络连接', '重新加载页面', '尝试其他音频地址'];
|
||||
break;
|
||||
case 2:
|
||||
errorMessage = '网络错误 - 音频文件无法下载';
|
||||
solutions = ['检查音频地址是否有效', '确认音频文件存在', '检查网络连接'];
|
||||
break;
|
||||
case 3:
|
||||
errorMessage = '音频解码错误 - 文件格式不支持或已损坏';
|
||||
solutions = ['确认音频格式为MP3/WAV/OGG', '尝试其他音频文件', '检查文件是否完整'];
|
||||
break;
|
||||
case 4:
|
||||
errorMessage = '音频格式不支持或跨域访问被拒绝';
|
||||
solutions = ['检查音频文件格式', '确认服务器支持跨域', '尝试使用代理服务器'];
|
||||
break;
|
||||
default:
|
||||
errorMessage = '未知错误';
|
||||
solutions = ['检查音频地址', '确认网络连接', '尝试其他音频源'];
|
||||
}
|
||||
} else {
|
||||
// 没有具体错误码,根据状态判断
|
||||
if (networkState === 3) {
|
||||
errorMessage = '网络错误 - 无法下载音频文件';
|
||||
solutions = ['检查音频地址是否有效', '确认音频文件存在', '检查网络连接'];
|
||||
} else if (readyState === 0) {
|
||||
errorMessage = '音频文件无法加载';
|
||||
solutions = ['检查音频地址', '确认文件可访问', '检查跨域设置'];
|
||||
} else {
|
||||
errorMessage = '音频加载失败';
|
||||
solutions = ['检查音频地址', '确认网络连接', '尝试其他音频源'];
|
||||
}
|
||||
}
|
||||
|
||||
// 显示详细的错误信息
|
||||
this.showError(`音频加载失败:${errorMessage}\n\n解决方案:\n${solutions.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n调试信息:\n网络状态: ${this.getNetworkStateText(networkState)}\n就绪状态: ${this.getReadyStateText(readyState)}\n错误码: ${audioError?.code || '无'}`);
|
||||
|
||||
this.hideLoadingState();
|
||||
}
|
||||
|
||||
getNetworkStateText(state) {
|
||||
const states = {
|
||||
0: 'NETWORK_EMPTY - 网络状态未知',
|
||||
1: 'NETWORK_IDLE - 未使用网络',
|
||||
2: 'NETWORK_LOADING - 正在加载',
|
||||
3: 'NETWORK_NO_SOURCE - 未找到音频源'
|
||||
};
|
||||
return states[state] || `未知状态(${state})`;
|
||||
}
|
||||
|
||||
getReadyStateText(state) {
|
||||
const states = {
|
||||
0: 'HAVE_NOTHING - 没有音频信息',
|
||||
1: 'HAVE_METADATA - 只有元数据',
|
||||
2: 'HAVE_CURRENT_DATA - 当前播放位置有数据',
|
||||
3: 'HAVE_FUTURE_DATA - 当前及未来数据可用',
|
||||
4: 'HAVE_ENOUGH_DATA - 足够数据开始播放'
|
||||
};
|
||||
return states[state] || `未知状态(${state})`;
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
switchVisualization(mode) {
|
||||
this.currentVisualization = mode;
|
||||
|
||||
// 更新按钮状态
|
||||
this.elements.vizButtons.forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.mode === mode);
|
||||
});
|
||||
}
|
||||
|
||||
startVisualization() {
|
||||
if (!this.animationId) {
|
||||
this.animate();
|
||||
}
|
||||
}
|
||||
|
||||
stopVisualization() {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
this.animationId = null;
|
||||
this.clearCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
clearCanvas() {
|
||||
this.ctx.fillStyle = '#000';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
animate() {
|
||||
if (!this.analyser) return;
|
||||
|
||||
this.animationId = requestAnimationFrame(() => this.animate());
|
||||
|
||||
this.analyser.getByteFrequencyData(this.dataArray);
|
||||
|
||||
switch (this.currentVisualization) {
|
||||
case 'bars':
|
||||
this.drawBars();
|
||||
break;
|
||||
case 'wave':
|
||||
this.drawWave();
|
||||
break;
|
||||
case 'circle':
|
||||
this.drawCircle();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
drawBars() {
|
||||
this.clearCanvas();
|
||||
|
||||
const barWidth = (this.canvas.width / this.bufferLength) * 2.5;
|
||||
let barHeight;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < this.bufferLength; i++) {
|
||||
barHeight = (this.dataArray[i] / 255) * this.canvas.height * 0.8;
|
||||
|
||||
const gradient = this.ctx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height);
|
||||
gradient.addColorStop(0, `hsl(${(i / this.bufferLength) * 360}, 100%, 50%)`);
|
||||
gradient.addColorStop(1, `hsl(${(i / this.bufferLength) * 360}, 100%, 30%)`);
|
||||
|
||||
this.ctx.fillStyle = gradient;
|
||||
this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
|
||||
|
||||
x += barWidth + 1;
|
||||
}
|
||||
}
|
||||
|
||||
drawWave() {
|
||||
this.clearCanvas();
|
||||
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.strokeStyle = '#00ff88';
|
||||
this.ctx.beginPath();
|
||||
|
||||
const sliceWidth = this.canvas.width / this.bufferLength;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < this.bufferLength; i++) {
|
||||
const v = this.dataArray[i] / 128.0;
|
||||
const y = (v * this.canvas.height) / 2;
|
||||
|
||||
if (i === 0) {
|
||||
this.ctx.moveTo(x, y);
|
||||
} else {
|
||||
this.ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
x += sliceWidth;
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
drawCircle() {
|
||||
this.clearCanvas();
|
||||
|
||||
const centerX = this.canvas.width / 2;
|
||||
const centerY = this.canvas.height / 2;
|
||||
const radius = Math.min(centerX, centerY) - 50;
|
||||
|
||||
for (let i = 0; i < this.bufferLength; i++) {
|
||||
const angle = (i / this.bufferLength) * Math.PI * 2;
|
||||
const amplitude = (this.dataArray[i] / 255) * radius;
|
||||
|
||||
const x1 = centerX + Math.cos(angle) * (radius * 0.5);
|
||||
const y1 = centerY + Math.sin(angle) * (radius * 0.5);
|
||||
const x2 = centerX + Math.cos(angle) * (radius * 0.5 + amplitude);
|
||||
const y2 = centerY + Math.sin(angle) * (radius * 0.5 + amplitude);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(x1, y1);
|
||||
this.ctx.lineTo(x2, y2);
|
||||
this.ctx.strokeStyle = `hsl(${(i / this.bufferLength) * 360}, 100%, 50%)`;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// 绘制中心圆
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(centerX, centerY, 30, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = 'rgba(102, 126, 234, 0.8)';
|
||||
this.ctx.fill();
|
||||
|
||||
// 中心文字
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '12px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText('🎵', centerX, centerY + 4);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化播放器
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 检查浏览器兼容性
|
||||
if (!window.AudioContext && !window.webkitAudioContext) {
|
||||
alert('您的浏览器不支持Web Audio API,部分功能可能无法正常使用。\n\n建议使用:\n• Chrome 10+\n• Firefox 25+\n• Safari 6+\n• Edge 12+');
|
||||
}
|
||||
|
||||
// 检查是否支持音频
|
||||
if (!document.createElement('audio').canPlayType) {
|
||||
alert('您的浏览器不支持HTML5音频播放。\n\n请升级浏览器或使用现代浏览器。');
|
||||
return;
|
||||
}
|
||||
|
||||
const player = new AudioPlayer();
|
||||
|
||||
// 显示初始化提示
|
||||
console.log('🎵 音频播放器初始化完成');
|
||||
console.log('💡 提示:如果遇到跨域问题,请尝试:');
|
||||
console.log(' 1. 使用上方的测试音频按钮');
|
||||
console.log(' 2. 确保音频地址支持CORS');
|
||||
console.log(' 3. 检查网络连接');
|
||||
});
|
||||
878
src/style.css
Normal file
878
src/style.css
Normal file
@ -0,0 +1,878 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
width: 800px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
/* URL输入区域 */
|
||||
.url-input-section {
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#audioUrl {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
#audioUrl:focus {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
#loadAudio {
|
||||
padding: 12px 20px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
#loadAudio:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 可视化区域 */
|
||||
.visualization-section {
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#visualizer {
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 15px;
|
||||
background: #000;
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.visualization-controls {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.viz-btn {
|
||||
padding: 8px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.viz-btn.active {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.viz-btn:hover {
|
||||
border-color: #667eea;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 播放控制 */
|
||||
.player-controls {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 4px;
|
||||
width: 0%;
|
||||
transition: width 0.1s ease;
|
||||
}
|
||||
|
||||
.progress-handle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
left: 0%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.progress-handle:hover {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
|
||||
/* 控制按钮 */
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#playPause {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 音量控制 */
|
||||
.volume-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.volume-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
width: 100px;
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.volume-slider::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#volumeValue {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
/* 加载完成动画 */
|
||||
@keyframes loading-complete {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载遮罩层 - 简洁设计 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(1px);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-overlay.show {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 加载弹出层悬浮效果 */
|
||||
.loading-message {
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(102, 126, 234, 0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
/* 加载弹出层悬浮效果 */
|
||||
.loading-message:hover {
|
||||
transform: translate(-50%, -50%) scale(1.02);
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 1px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
/* 加载弹出层响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.loading-message {
|
||||
min-width: 180px;
|
||||
max-width: 240px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.circular-progress {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.loading-text .loading-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-text .loading-subtitle {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载弹出层细节优化 */
|
||||
.loading-message {
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.circular-progress .progress-bar {
|
||||
will-change: stroke-dashoffset;
|
||||
}
|
||||
|
||||
.loading-text .loading-title {
|
||||
will-change: contents;
|
||||
}
|
||||
|
||||
/* 防止加载时页面滚动 */
|
||||
body.loading-active {
|
||||
overflow: hidden;
|
||||
} 加载遮罩层 - 简洁设计 */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(1px);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-overlay.show {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 加载弹出层 - 简洁优雅 */
|
||||
.loading-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
display: none;
|
||||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* 成功消息动画 */
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-out-right {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.audio-player {
|
||||
padding: 20px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.url-input-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#visualizer {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
margin-left: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 测试音频源 */
|
||||
.test-audio-sources {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-source-btn {
|
||||
padding: 6px 12px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.test-source-btn:hover {
|
||||
background: #e0e0e0;
|
||||
border-color: #667eea;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 加载弹出层 - 简洁优雅 */
|
||||
.loading-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
display: none;
|
||||
border: 1px solid rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* 圆形加载动画 */
|
||||
.loading-animation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.circular-loader {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.circular-loader .loader-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.circular-loader .loader-inner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 3px solid transparent;
|
||||
border-top: 3px solid #764ba2;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.5s linear infinite reverse;
|
||||
}
|
||||
|
||||
/* 加载信息 - 简洁 */
|
||||
.loading-info {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.loading-info .loading-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.loading-info .loading-subtitle {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* 迷你频谱 - 简洁 */
|
||||
.mini-spectrum {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
height: 20px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.spectrum-bar {
|
||||
width: 3px;
|
||||
background: linear-gradient(to top, #667eea, #764ba2);
|
||||
border-radius: 1.5px;
|
||||
animation: spectrum-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.spectrum-bar:nth-child(1) {
|
||||
height: 12px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.spectrum-bar:nth-child(2) {
|
||||
height: 16px;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.spectrum-bar:nth-child(3) {
|
||||
height: 14px;
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
.spectrum-bar:nth-child(4) {
|
||||
height: 18px;
|
||||
animation-delay: 0.6s;
|
||||
}
|
||||
|
||||
@keyframes spectrum-pulse {
|
||||
0%, 100% {
|
||||
transform: scaleY(0.7);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 主要加载器 - 简洁 */
|
||||
.main-loader {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 简化圆形进度条 */
|
||||
.circular-progress-simple {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.circular-progress-simple svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.circular-progress-simple .progress-bg {
|
||||
fill: none;
|
||||
stroke: #f5f5f5;
|
||||
stroke-width: 6;
|
||||
}
|
||||
|
||||
.circular-progress-simple .progress-bar {
|
||||
fill: none;
|
||||
stroke: url(#gradient);
|
||||
stroke-width: 6;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 220; /* 2 * π * 35 */
|
||||
stroke-dashoffset: 220;
|
||||
transition: stroke-dashoffset 0.4s ease;
|
||||
}
|
||||
|
||||
.circular-progress-simple .progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading-text .loading-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.loading-text .loading-subtitle {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 加载状态切换动画 - 更平滑 */
|
||||
.loading-message.fade-in {
|
||||
animation: fadeInScale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.loading-message.fade-out {
|
||||
animation: fadeOutScale 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.7);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutScale {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.7);
|
||||
}
|
||||
}
|
||||
|
||||
/* 圆形进度条填充动画 */
|
||||
@keyframes progress-fill {
|
||||
from {
|
||||
stroke-dashoffset: 220;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.circular-progress-simple .progress-bar.loading {
|
||||
animation: progress-fill 2s ease-out;
|
||||
}
|
||||
|
||||
/* 加载弹出层响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.loading-message {
|
||||
min-width: 180px;
|
||||
max-width: 240px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.circular-progress-simple {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.loading-info .loading-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-info .loading-subtitle {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 错误提示和加载状态 */
|
||||
.error-message {
|
||||
background: #ffebee;
|
||||
border: 1px solid #f44336;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 15px 0;
|
||||
color: #c62828;
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #2196f3;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 15px 0;
|
||||
color: #1565c0;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(102, 126, 234, 0.1);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.loading-message:hover {
|
||||
transform: translate(-50%, -50%) scale(1.02);
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 1px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
/* 圆形进度条动画 */
|
||||
@keyframes progress-glow {
|
||||
0%, 100% {
|
||||
filter: drop-shadow(0 0 2px rgba(102, 126, 234, 0.3));
|
||||
}
|
||||
50% {
|
||||
filter: drop-shadow(0 0 8px rgba(102, 126, 234, 0.6));
|
||||
}
|
||||
}
|
||||
|
||||
.circular-progress .progress-bar {
|
||||
animation: progress-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 音乐频谱脉冲效果 */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.music-bars-small {
|
||||
animation: pulse-glow 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 加载文字动画 */
|
||||
@keyframes text-shimmer {
|
||||
0% {
|
||||
background-position: -100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
background: linear-gradient(90deg, #667eea, #764ba2, #667eea);
|
||||
background-size: 200% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: text-shimmer 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-message span {
|
||||
display: inline-block;
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-message::before {
|
||||
content: "🎵";
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.playing .control-btn {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
24
vite.config.js
Normal file
24
vite.config.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true,
|
||||
cors: true,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Range',
|
||||
'Access-Control-Allow-Credentials': 'true',
|
||||
'Access-Control-Expose-Headers': 'Content-Length, Content-Range'
|
||||
},
|
||||
proxy: {
|
||||
'/DocServer': {
|
||||
target: 'http://192.168.1.201:8080',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
rewrite: (path) => path.replace(/^\/DocServer/, '/DocServer')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user