| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>云音乐 - 在线音乐播放器</title> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; background: #f8f9fa; color: #333; overflow-x: hidden; line-height: 1.6; } |
| .navbar { background: #ffffff; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); padding: 15px 0; position: sticky; top: 0; z-index: 100; } |
| .nav-container { max-width: 1400px; margin: 0 auto; padding: 0 30px; display: flex; align-items: center; justify-content: space-between; } |
| .logo { display: flex; align-items: center; gap: 12px; font-size: 24px; font-weight: bold; color: #333; } |
| .logo i { color: #4a90e2; font-size: 28px; } |
| .search-container { flex: 1; max-width: 600px; margin: 0 40px; position: relative; } |
| .search-wrapper { display: flex; background: #f5f7fa; border-radius: 25px; overflow: hidden; border: 1px solid #e1e5eb; transition: all 0.3s ease; } |
| .search-wrapper:focus-within { background: #ffffff; border-color: #4a90e2; box-shadow: 0 0 15px rgba(74, 144, 226, 0.2); } |
| .search-input { flex: 1; padding: 12px 20px; background: transparent; border: none; color: #333; font-size: 16px; outline: none; } |
| .search-input::placeholder { color: #adb5bd; } |
| .source-select { background: #f0f3f7; border: none; color: #495057; padding: 12px 15px; outline: none; cursor: pointer; border-left: 1px solid #e1e5eb; } |
| .source-select option { background: #ffffff; color: #333; padding: 8px; } |
| .search-btn { background: #4a90e2; border: none; color: #fff; padding: 12px 20px; cursor: pointer; transition: all 0.3s ease; } |
| .search-btn:hover { background: #3a7bc8; } |
| .main-container { max-width: 1200px; margin: 0 auto; padding: 20px; min-height: calc(100vh - 170px); } |
| .content-panel { background: #ffffff; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); padding: 25px; margin-bottom: 20px; height: 100%; } |
| .panel-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; color: #333; display: flex; align-items: center; gap: 10px; } |
| .panel-title i { color: #4a90e2; } |
| .search-results { max-height: 500px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ced4da transparent; display: grid; grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); gap: 12px; padding-right: 10px; } |
| .search-results::-webkit-scrollbar { width: 6px; } |
| .search-results::-webkit-scrollbar-track { background: transparent; } |
| .search-results::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 3px; } |
| .song-card { display: flex; align-items: flex-start; padding: 12px 15px; border-radius: 12px; cursor: pointer; transition: all 0.3s ease; background: #f8f9fa; border: 1px solid #e9ecef; } |
| .song-card:hover { background: #ffffff; transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); } |
| .song-card.active { background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(74, 144, 226, 0.05)); border: 1px solid rgba(74, 144, 226, 0.3); } |
| .song-index { width: 32px; height: 32px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 14px; font-weight: 600; color: #495057; flex-shrink: 0; } |
| .song-card.active .song-index { background: linear-gradient(135deg, #4a90e2, #3a7bc8); color: #fff; } |
| .song-info { flex: 1; min-width: 0; padding: 2px 0; } |
| .song-name { font-weight: 600; margin-bottom: 3px; font-size: 15px; color: #333; white-space: normal; line-height: 1.4; } |
| .song-artist { color: #6c757d; font-size: 13px; white-space: normal; line-height: 1.4; } |
| .song-duration { color: #adb5bd; font-size: 13px; margin-left: 10px; white-space: nowrap; align-self: center; } |
| .song-actions { display: flex; gap: 8px; margin-left: 10px; align-self: center; } |
| .action-btn { width: 32px; height: 32px; border-radius: 50%; background: #e9ecef; border: none; color: #6c757d; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; font-size: 13px; } |
| .action-btn:hover { background: #dee2e6; color: #4a90e2; transform: scale(1.1); } |
| .player-panel { text-align: center; padding: 30px 25px; } |
| .current-song { margin-bottom: 30px; } |
| .current-cover-container { position: relative; display: inline-block; margin-bottom: 25px; } |
| .current-cover { width: 220px; height: 220px; border-radius: 50%; object-fit: cover; box-shadow: 0 15px 35px rgba(74, 144, 226, 0.2); transition: all 0.3s ease; border: 6px solid #f8f9fa; } |
| .current-cover.playing { animation: rotate 20s linear infinite; } |
| @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| .current-info h3 { font-size: 22px; font-weight: 600; margin-bottom: 8px; color: #333; } |
| .current-info p { color: #6c757d; font-size: 16px; } |
| .player-controls { display: flex; justify-content: center; align-items: center; gap: 25px; margin-bottom: 30px; } |
| .control-btn { background: #f1f3f5; border: none; border-radius: 50%; color: #495057; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; } |
| .control-btn:hover { background: #e9ecef; transform: scale(1.1); } |
| .control-btn.small { width: 50px; height: 50px; font-size: 18px; } |
| .play-btn { width: 70px; height: 70px; font-size: 28px; background: linear-gradient(135deg, #4a90e2, #3a7bc8); color: white; box-shadow: 0 8px 25px rgba(74, 144, 226, 0.3); } |
| .play-btn:hover { background: linear-gradient(135deg, #3a7bc8, #2d6bc1); box-shadow: 0 12px 35px rgba(74, 144, 226, 0.4); } |
| .progress-container { margin-bottom: 30px; padding: 0 20px; } |
| .progress-bar { width: 100%; height: 6px; background: #e9ecef; border-radius: 3px; cursor: pointer; margin-bottom: 10px; position: relative; } |
| .progress-fill { height: 100%; background: linear-gradient(90deg, #4a90e2, #6aa8e6); border-radius: 3px; width: 0%; transition: width 0.1s ease; position: relative; } |
| .progress-fill::after { content: ''; position: absolute; right: -2px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; background: #4a90e2; border-radius: 50%; box-shadow: 0 2px 8px rgba(74, 144, 226, 0.5); } |
| .time-info { display: flex; justify-content: space-between; font-size: 14px; color: #6c757d; } |
| .volume-container { display: flex; align-items: center; gap: 12px; margin-bottom: 30px; justify-content: center; } |
| .volume-icon { color: #6c757d; font-size: 18px; } |
| .volume-slider { width: 150px; height: 4px; background: #e9ecef; border-radius: 2px; outline: none; cursor: pointer; -webkit-appearance: none; } |
| .volume-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: #4a90e2; border-radius: 50%; cursor: pointer; } |
| .quality-container { display: flex; align-items: center; justify-content: space-between; margin-bottom: 25px; padding: 15px 20px; background: #f8f9fa; border-radius: 12px; border: 1px solid #e9ecef; } |
| .quality-label { display: flex; align-items: center; gap: 8px; color: #495057; font-size: 14px; } |
| .quality-select { background: #ffffff; border: 1px solid #ced4da; border-radius: 8px; color: #333; padding: 8px 12px; outline: none; cursor: pointer; font-size: 14px; } |
| .quality-select option { background: #ffffff; color: #333; padding: 8px; } |
| .download-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px; } |
| .download-btn { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 15px; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 12px; color: #495057; cursor: pointer; transition: all 0.3s ease; font-size: 14px; } |
| .download-btn:hover:not(:disabled) { background: #eef2f7; border-color: #4a90e2; color: #4a90e2; } |
| .download-btn:disabled { opacity: 0.5; cursor: not-allowed; } |
| .lyrics-container { max-height: 500px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ced4da transparent; padding-right: 10px; position: relative; } |
| .lyrics-container::-webkit-scrollbar { width: 6px; } |
| .lyrics-container::-webkit-scrollbar-track { background: transparent; } |
| .lyrics-container::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 3px; } |
| .lyric-line { padding: 8px 15px; transition: all 0.3s ease; cursor: pointer; border-radius: 8px; margin-bottom: 6px; color: #6c757d; line-height: 1.6; text-align: center; font-size: 15px; } |
| .lyric-line:hover { background: #f8f9fa; color: #4a90e2; } |
| .lyric-line.active { color: #4a90e2; font-weight: 600; background: rgba(74, 144, 226, 0.08); transform: scale(1.02); } |
| .loading, .error, .empty-state { text-align: center; padding: 60px 20px; color: #6c757d; } |
| .loading i, .error i, .empty-state i { font-size: 48px; margin-bottom: 15px; display: block; color: #adb5bd; } |
| .loading i { animation: spin 1s linear infinite; color: #4a90e2; } |
| @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| .error i { color: #e74c3c; } |
| .mobile-nav { display: none; position: fixed; bottom: 0; left: 0; width: 100%; background: #ffffff; box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.05); z-index: 99; } |
| .mobile-nav-items { display: flex; justify-content: space-around; } |
| .mobile-nav-item { flex: 1; text-align: center; padding: 15px 0; color: #6c757d; text-decoration: none; font-size: 12px; transition: all 0.3s ease; } |
| .mobile-nav-item.active { color: #4a90e2; } |
| .mobile-nav-item i { font-size: 20px; margin-bottom: 5px; display: block; } |
| .view-container { display: block; } |
| @media (min-width: 1200px) { .desktop-layout { display: grid; grid-template-columns: 350px 450px 350px; gap: 25px; } .mobile-nav { display: none !important; } } |
| @media (max-width: 1199px) { .mobile-nav { display: block; } .main-container { padding-bottom: 80px; } .desktop-layout { display: block; } .view-container { display: none; } .view-container.active { display: block; } .song-name { font-size: 14px; line-height: 1.4; } .song-artist { font-size: 13px; line-height: 1.4; } .song-card { padding: 10px 12px; gap: 10px; } .song-index { width: 28px; height: 28px; font-size: 13px; } .action-btn { width: 28px; height: 28px; font-size: 12px; } } |
| @media (max-width: 768px) { .nav-container { flex-direction: column; gap: 15px; padding: 0 15px; } .search-container { margin: 0; max-width: none; width: 100%; } .current-cover { width: 180px; height: 180px; } .player-controls { gap: 15px; } .content-panel { padding: 15px 12px; } .panel-title { font-size: 17px; margin-bottom: 15px; } .search-results { gap: 10px; } } |
| ::-webkit-scrollbar { width: 8px; } |
| ::-webkit-scrollbar-track { background: #f8f9fa; border-radius: 4px; } |
| ::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; } |
| ::-webkit-scrollbar-thumb:hover { background: #adb5bd; } |
| </style> |
| </head> |
| <body> |
| <nav class="navbar"> |
| <div class="nav-container"> |
| <div class="logo"><i class="fas fa-music"></i><span>云音乐</span></div> |
| <div class="search-container"> |
| <div class="search-wrapper"> |
| <input type="text" class="search-input" placeholder="搜索音乐、歌手、专辑..." id="searchInput"> |
| <select class="source-select" id="sourceSelect"> |
| <option value="netease">网易云音乐</option> |
| <option value="tencent">QQ音乐</option> |
| <option value="kuwo">酷我音乐</option> |
| <option value="joox">JOOX</option> |
| <option value="kugou">酷狗音乐</option> |
| <option value="migu">咪咕音乐</option> |
| <option value="deezer">Deezer</option> |
| <option value="spotify">Spotify</option> |
| <option value="apple">Apple Music</option> |
| <option value="ytmusic">YouTube Music</option> |
| <option value="tidal">TIDAL</option> |
| <option value="qobuz">Qobuz</option> |
| <option value="ximalaya">喜马拉雅</option> |
| </select> |
| <button class="search-btn" onclick="searchMusic()"><i class="fas fa-search"></i></button> |
| </div> |
| </div> |
| </div> |
| </nav> |
| <div class="main-container"> |
| <div class="desktop-layout"> |
| <div class="view-container" id="searchView"> |
| <div class="content-panel"> |
| <h2 class="panel-title"><i class="fas fa-search"></i> 搜索结果</h2> |
| <div class="search-results" id="searchResults"> |
| <div class="empty-state"><i class="fas fa-music"></i><div>在上方搜索框输入关键词开始搜索音乐</div></div> |
| </div> |
| </div> |
| </div> |
| <div class="view-container" id="playerView"> |
| <div class="content-panel player-panel"> |
| <div class="current-song"> |
| <div class="current-cover-container"><img class="current-cover" id="currentCover" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+" alt="专辑封面"></div> |
| <div class="current-info"><h3 id="currentTitle">未选择歌曲</h3><p id="currentArtist">请搜索并选择要播放的歌曲</p></div> |
| </div> |
| <div class="player-controls"> |
| <button class="control-btn small" onclick="previousSong()"><i class="fas fa-step-backward"></i></button> |
| <button class="control-btn play-btn" id="playBtn" onclick="togglePlay()"><i class="fas fa-play"></i></button> |
| <button class="control-btn small" onclick="nextSong()"><i class="fas fa-step-forward"></i></button> |
| </div> |
| <div class="progress-container"> |
| <div class="progress-bar" onclick="seekTo(event)"><div class="progress-fill" id="progressFill"></div></div> |
| <div class="time-info"><span id="currentTime">0:00</span><span id="totalTime">0:00</span></div> |
| </div> |
| <div class="quality-container"> |
| <div class="quality-label"><i class="fas fa-music"></i><span>音质</span></div> |
| <select class="quality-select" id="qualitySelect"> |
| <option value="128">标准 128K</option> |
| <option value="192">较高 192K</option> |
| <option value="320" selected>高品质 320K</option> |
| <option value="740">无损 FLAC</option> |
| <option value="999">Hi-Res</option> |
| </select> |
| </div> |
| <div class="volume-container"> |
| <i class="fas fa-volume-up volume-icon"></i> |
| <input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="80" onchange="setVolume(this.value)"> |
| </div> |
| <div class="download-container"> |
| <button class="download-btn" onclick="downloadCurrentSong()" id="downloadSongBtn" disabled><i class="fas fa-download"></i><span>下载音乐</span></button> |
| <button class="download-btn" onclick="downloadCurrentLyric()" id="downloadLyricBtn" disabled><i class="fas fa-file-text"></i><span>下载歌词</span></button> |
| </div> |
| <audio id="audioPlayer" preload="metadata"></audio> |
| </div> |
| </div> |
| <div class="view-container" id="lyricsView"> |
| <div class="content-panel"> |
| <h2 class="panel-title"><i class="fas fa-align-left"></i> 歌词</h2> |
| <div class="lyrics-container" id="lyricsContainer"><div class="lyric-line">暂无歌词</div></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div class="mobile-nav"> |
| <div class="mobile-nav-items"> |
| <a href="#searchView" class="mobile-nav-item active" onclick="switchView('searchView', this)"><i class="fas fa-search"></i><span>搜索</span></a> |
| <a href="#playerView" class="mobile-nav-item" onclick="switchView('playerView', this)"><i class="fas fa-play-circle"></i><span>播放</span></a> |
| <a href="#lyricsView" class="mobile-nav-item" onclick="switchView('lyricsView', this)"><i class="fas fa-align-left"></i><span>歌词</span></a> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const API_BASE = 'https://music-api.gdstudio.xyz/api.php'; |
| const PLAYER_STATE_KEY = 'cloudMusicPlayerState_v3'; |
| let currentPlaylist = []; |
| let currentIndex = -1; |
| let isPlaying = false; |
| let timeToRestore = 0; |
| |
| |
| const audioPlayer = document.getElementById('audioPlayer'); |
| const playBtn = document.getElementById('playBtn'); |
| const progressFill = document.getElementById('progressFill'); |
| const currentTimeSpan = document.getElementById('currentTime'); |
| const totalTimeSpan = document.getElementById('totalTime'); |
| const lyricsContainer = document.getElementById('lyricsContainer'); |
| const currentCover = document.getElementById('currentCover'); |
| const qualitySelect = document.getElementById('qualitySelect'); |
| const sourceSelect = document.getElementById('sourceSelect'); |
| const volumeSlider = document.getElementById('volumeSlider'); |
| const searchInput = document.getElementById('searchInput'); |
| |
| function savePlayerState() { |
| if (currentIndex === -1) return; |
| const state = { |
| playlist: currentPlaylist, |
| index: currentIndex, |
| volume: audioPlayer.volume, |
| quality: qualitySelect.value, |
| source: sourceSelect.value, |
| currentTime: audioPlayer.currentTime > 1 ? audioPlayer.currentTime : 0 |
| }; |
| try { |
| localStorage.setItem(PLAYER_STATE_KEY, JSON.stringify(state)); |
| } catch (e) { console.error("保存状态失败:", e); } |
| } |
| |
| async function loadPlayerState() { |
| try { |
| const savedState = localStorage.getItem(PLAYER_STATE_KEY); |
| if (!savedState) return; |
| |
| const state = JSON.parse(savedState); |
| |
| currentPlaylist = state.playlist || []; |
| currentIndex = state.index !== undefined ? state.index : -1; |
| timeToRestore = state.currentTime || 0; |
| |
| qualitySelect.value = state.quality || '320'; |
| sourceSelect.value = state.source || 'netease'; |
| |
| const volumeValue = (state.volume !== undefined ? state.volume : 0.8) * 100; |
| volumeSlider.value = volumeValue; |
| setVolume(volumeValue, false); |
| |
| if (currentPlaylist.length > 0) { |
| displaySearchResults(currentPlaylist); |
| } |
| |
| if (currentIndex > -1 && currentPlaylist[currentIndex]) { |
| const song = currentPlaylist[currentIndex]; |
| await updateCurrentSongInfo(song); |
| updateActiveItem(); |
| |
| lyricsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在加载歌词...</div></div>`; |
| await loadLyrics(song); |
| |
| const quality = qualitySelect.value; |
| const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`); |
| const urlData = await urlResponse.json(); |
| if(urlData && urlData.url) { |
| audioPlayer.src = urlData.url; |
| } |
| |
| document.getElementById('downloadSongBtn').disabled = false; |
| document.getElementById('downloadLyricBtn').disabled = false; |
| } |
| } catch (e) { |
| console.error("加载状态失败, 数据已损坏:", e); |
| localStorage.removeItem(PLAYER_STATE_KEY); |
| } |
| } |
| |
| async function initializeApp() { |
| if (window.innerWidth >= 1200) { |
| document.getElementById('searchView').classList.add('active'); |
| document.getElementById('playerView').classList.add('active'); |
| document.getElementById('lyricsView').classList.add('active'); |
| } else { |
| switchView('searchView', document.querySelectorAll('.mobile-nav-item')[0]); |
| } |
| |
| await loadPlayerState(); |
| |
| qualitySelect.onchange = savePlayerState; |
| sourceSelect.onchange = savePlayerState; |
| window.addEventListener('beforeunload', savePlayerState); |
| setInterval(() => { |
| if (isPlaying) savePlayerState(); |
| }, 5000); |
| } |
| |
| function setVolume(value, shouldSave = true) { |
| audioPlayer.volume = value / 100; |
| const volumeIcon = document.querySelector('.volume-icon'); |
| if (value == 0) volumeIcon.className = 'fas fa-volume-mute volume-icon'; |
| else if (value < 50) volumeIcon.className = 'fas fa-volume-down volume-icon'; |
| else volumeIcon.className = 'fas fa-volume-up volume-icon'; |
| if (shouldSave) { |
| savePlayerState(); |
| } |
| } |
| |
| async function playSong(index) { |
| if (index < 0 || index >= currentPlaylist.length) return; |
| currentIndex = index; |
| timeToRestore = 0; |
| const song = currentPlaylist[index]; |
| await updateCurrentSongInfo(song); |
| updateActiveItem(); |
| |
| try { |
| showNotification('正在加载音乐...', 'info'); |
| lyricsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在加载歌词...</div></div>`; |
| await loadLyrics(song); |
| |
| const quality = qualitySelect.value; |
| const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`); |
| const urlData = await urlResponse.json(); |
| |
| if (urlData && urlData.url) { |
| audioPlayer.src = urlData.url; |
| audioPlayer.load(); |
| document.getElementById('downloadSongBtn').disabled = false; |
| document.getElementById('downloadLyricBtn').disabled = false; |
| |
| const playPromise = audioPlayer.play(); |
| if (playPromise !== undefined) { |
| playPromise.catch(error => { |
| showNotification('自动播放失败,请手动点击播放', 'warning'); |
| }); |
| } |
| } else { |
| showNotification('无法获取音乐链接', 'error'); |
| } |
| } catch (error) { |
| console.error('播放失败:', error); |
| showNotification('播放失败,请检查网络连接', 'error'); |
| } |
| } |
| |
| audioPlayer.addEventListener('loadedmetadata', () => { |
| totalTimeSpan.textContent = formatTime(audioPlayer.duration); |
| if (timeToRestore > 0 && timeToRestore < audioPlayer.duration) { |
| audioPlayer.currentTime = timeToRestore; |
| timeToRestore = 0; |
| updateProgress(); |
| showNotification('已恢复上次播放进度', 'info'); |
| } |
| }); |
| |
| audioPlayer.addEventListener('play', () => { isPlaying = true; updatePlayButton(); currentCover.classList.add('playing'); }); |
| audioPlayer.addEventListener('pause', () => { isPlaying = false; updatePlayButton(); currentCover.classList.remove('playing'); savePlayerState(); }); |
| audioPlayer.addEventListener('ended', nextSong); |
| setInterval(updateProgress, 500); |
| searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') searchMusic(); }); |
| document.addEventListener('keydown', (e) => { if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return; const keyMap = { ' ': togglePlay, 'ArrowRight': () => { if(audioPlayer.duration) audioPlayer.currentTime = Math.min(audioPlayer.currentTime + 5, audioPlayer.duration); }, 'ArrowLeft': () => { if(audioPlayer.duration) audioPlayer.currentTime = Math.max(audioPlayer.currentTime - 5, 0); }, 'ArrowUp': () => { const v = Math.min(parseInt(volumeSlider.value) + 5, 100); volumeSlider.value = v; setVolume(v); }, 'ArrowDown': () => { const v = Math.max(parseInt(volumeSlider.value) - 5, 0); volumeSlider.value = v; setVolume(v); }, 'n': nextSong, 'N': nextSong, 'p': previousSong, 'P': previousSong }; if (keyMap[e.key]) { e.preventDefault(); keyMap[e.key](); } }); |
| |
| function switchView(viewId, navItem) { |
| document.querySelectorAll('.view-container').forEach(view => view.classList.remove('active')); |
| document.getElementById(viewId).classList.add('active'); |
| if (navItem) { |
| document.querySelectorAll('.mobile-nav-item').forEach(item => item.classList.remove('active')); |
| navItem.classList.add('active'); |
| } |
| } |
| |
| async function searchMusic() { |
| const keyword = searchInput.value.trim(); |
| const source = sourceSelect.value; |
| if (!keyword) { showNotification('请输入搜索关键词', 'warning'); return; } |
| const resultsContainer = document.getElementById('searchResults'); |
| resultsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在搜索音乐...</div></div>`; |
| try { |
| const response = await fetch(`${API_BASE}?types=search&source=${source}&name=${encodeURIComponent(keyword)}&count=30`); |
| const data = await response.json(); |
| if (data && data.length > 0) { |
| currentPlaylist = data; |
| currentIndex = -1; |
| displaySearchResults(data); |
| savePlayerState(); |
| } else { |
| resultsContainer.innerHTML = `<div class="error"><i class="fas fa-exclamation-triangle"></i><div>未找到相关歌曲</div></div>`; |
| } |
| } catch (error) { |
| console.error('搜索失败:', error); |
| resultsContainer.innerHTML = `<div class="error"><i class="fas fa-wifi"></i><div>网络连接失败</div></div>`; |
| } |
| } |
| |
| async function getAlbumCoverUrl(song, size = 300) { |
| if (!song || !song.pic_id) { return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+'; } |
| try { |
| const response = await fetch(`${API_BASE}?types=pic&source=${song.source}&id=${song.pic_id}&size=${size}`); |
| const data = await response.json(); |
| return (data && data.url) ? data.url : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+'; |
| } catch (error) { |
| console.error('获取专辑图失败:', error); |
| return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+'; |
| } |
| } |
| |
| function displaySearchResults(songs) { |
| const resultsContainer = document.getElementById('searchResults'); |
| resultsContainer.innerHTML = ''; |
| songs.forEach((song, index) => { |
| const songCard = document.createElement('div'); |
| songCard.className = 'song-card'; |
| songCard.onclick = () => playSong(index); |
| const artistText = Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist; |
| songCard.innerHTML = ` <div class="song-index">${(index + 1).toString().padStart(2, '0')}</div> <div class="song-info"> <div class="song-name">${song.name}</div> <div class="song-artist">${artistText} ${song.album ? '· ' + song.album : ''}</div> </div> <div class="song-actions"> <button class="action-btn" onclick="event.stopPropagation(); downloadSong(${index})" title="下载音乐"><i class="fas fa-download"></i></button> <button class="action-btn" onclick="event.stopPropagation(); downloadLyric(${index})" title="下载歌词"><i class="fas fa-file-text"></i></button> </div>`; |
| resultsContainer.appendChild(songCard); |
| }); |
| } |
| |
| async function downloadSong(index) { const song = currentPlaylist[index]; const quality = qualitySelect.value; try { showNotification('正在获取下载链接...', 'info'); const response = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`); const data = await response.json(); if (data && data.url) { const link = document.createElement('a'); link.href = data.url; link.download = `${song.name} - ${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}.mp3`; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification('开始下载音乐文件', 'success'); } else { showNotification('无法获取下载链接', 'error'); } } catch (error) { console.error('下载失败:', error); showNotification('下载失败,请稍后重试', 'error'); } } |
| async function downloadLyric(index) { const song = currentPlaylist[index]; try { showNotification('正在获取歌词...', 'info'); const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`); const data = await response.json(); if (data && data.lyric) { let lyricContent = `歌曲:${song.name}\r\n歌手:${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}\r\n专辑:${song.album}\r\n\r\n${data.lyric}`; if (data.tlyric) lyricContent += `\r\n\r\n=== 翻译 ===\r\n${data.tlyric}`; const blob = new Blob([lyricContent], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${song.name} - ${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}.lrc`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); showNotification('歌词下载完成', 'success'); } else { showNotification('该歌曲暂无歌词', 'warning'); } } catch (error) { console.error('下载歌词失败:', error); showNotification('下载歌词失败,请稍后重试', 'error'); } } |
| async function downloadCurrentSong() { if (currentIndex === -1) { showNotification('请先选择歌曲', 'warning'); return; } await downloadSong(currentIndex); } |
| async function downloadCurrentLyric() { if (currentIndex === -1) { showNotification('请先选择歌曲', 'warning'); return; } await downloadLyric(currentIndex); } |
| async function updateCurrentSongInfo(song) { document.getElementById('currentTitle').textContent = song.name; const artistText = Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist; document.getElementById('currentArtist').textContent = `${artistText}${song.album ? ' · ' + song.album : ''}`; currentCover.src = await getAlbumCoverUrl(song, 500); } |
| function updateActiveItem() { document.querySelectorAll('.song-card').forEach((item, index) => { item.classList.toggle('active', index === currentIndex); }); } |
| function updatePlayButton() { playBtn.querySelector('i').className = isPlaying ? 'fas fa-pause' : 'fas fa-play'; } |
| function togglePlay() { if (!audioPlayer.src) { showNotification('请先选择一首歌曲', 'warning'); return; } if (isPlaying) { audioPlayer.pause(); } else { audioPlayer.play().catch(e => console.error("播放命令被拒绝", e)); } } |
| function seekTo(event) { if(!audioPlayer.duration) return; const progressBar = event.currentTarget; const rect = progressBar.getBoundingClientRect(); audioPlayer.currentTime = ((event.clientX - rect.left) / rect.width) * audioPlayer.duration; } |
| |
| async function loadLyrics(song) { |
| try { |
| const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`); |
| const data = await response.json(); |
| let lyrics = []; |
| if (data && data.lyric) { |
| lyrics = parseLyrics(data.lyric); |
| if (data.tlyric) { |
| const tLyrics = parseLyrics(data.tlyric); |
| mergeLyrics(lyrics, tLyrics); |
| } |
| } |
| displayLyrics(lyrics); |
| } catch (error) { |
| console.error('加载歌词失败:', error); |
| lyricsContainer.innerHTML = '<div class="lyric-line">加载歌词失败</div>'; |
| } |
| } |
| |
| function parseLyrics(lyricText) { |
| const lines = lyricText.split('\n'); |
| const lyrics = []; |
| const timeRegex = /\[(\d{2}):(\d{2})[.:](\d{2,3})\]/g; |
| for (const line of lines) { |
| if (!line.trim()) continue; |
| let text = line.replace(timeRegex, '').trim(); |
| if (text) { |
| let match; |
| timeRegex.lastIndex = 0; |
| while ((match = timeRegex.exec(line)) !== null) { |
| const timeInSeconds = parseInt(match[1]) * 60 + parseInt(match[2]) + parseInt(match[3]) / (match[3].length === 3 ? 1000 : 100); |
| lyrics.push({ time: timeInSeconds, text: text }); |
| } |
| } |
| } |
| return lyrics.sort((a, b) => a.time - b.time); |
| } |
| |
| function mergeLyrics(originalLyrics, translatedLyrics) { |
| const translatedMap = new Map(translatedLyrics.map(l => [Math.round(l.time * 10), l.text])); |
| originalLyrics.forEach(l => { |
| const key = Math.round(l.time * 10); |
| const translatedText = translatedMap.get(key); |
| if (translatedText) { |
| l.text += `<br><span style="color: #adb5bd; font-size: 0.9em;">${translatedText}</span>`; |
| } |
| }); |
| } |
| |
| function displayLyrics(lyrics) { |
| lyricsContainer.innerHTML = lyrics.length === 0 ? '<div class="lyric-line">暂无歌词</div>' : ''; |
| lyrics.forEach(lyric => { |
| const line = document.createElement('div'); |
| line.className = 'lyric-line'; |
| line.innerHTML = lyric.text; |
| line.setAttribute('data-time', lyric.time); |
| line.onclick = () => { if(audioPlayer.duration) audioPlayer.currentTime = lyric.time; }; |
| lyricsContainer.appendChild(line); |
| }); |
| } |
| |
| function updateProgress() { |
| if (audioPlayer.duration) { |
| progressFill.style.width = `${(audioPlayer.currentTime / audioPlayer.duration) * 100}%`; |
| currentTimeSpan.textContent = formatTime(audioPlayer.currentTime); |
| highlightCurrentLyric(); |
| } |
| } |
| |
| function formatTime(seconds) { |
| const min = Math.floor(seconds / 60); |
| const sec = Math.floor(seconds % 60); |
| return `${min}:${sec.toString().padStart(2, '0')}`; |
| } |
| |
| function highlightCurrentLyric() { |
| const currentTime = audioPlayer.currentTime; |
| const lyricLines = document.querySelectorAll('.lyric-line'); |
| if (lyricLines.length === 0 || lyricsContainer.offsetParent === null) return; |
| let activeIndex = -1; |
| for (let i = lyricLines.length - 1; i >= 0; i--) { |
| if (parseFloat(lyricLines[i].getAttribute('data-time')) <= currentTime + 0.2) { |
| activeIndex = i; |
| break; |
| } |
| } |
| lyricLines.forEach((line, index) => { |
| if (line.classList.contains('active') !== (index === activeIndex)) { |
| line.classList.toggle('active', index === activeIndex); |
| } |
| }); |
| if (activeIndex > -1) { |
| const activeLine = lyricLines[activeIndex]; |
| const scrollPosition = activeLine.offsetTop - lyricsContainer.clientHeight / 2 + activeLine.clientHeight / 2; |
| lyricsContainer.scrollTo({ top: scrollPosition, behavior: 'smooth' }); |
| } |
| } |
| |
| function showNotification(message, type = 'info') { |
| const existing = document.querySelector('.custom-notification'); |
| if (existing) existing.remove(); |
| const notification = document.createElement('div'); |
| notification.className = `custom-notification notification-${type}`; |
| const icons = { info: 'info-circle', success: 'check-circle', error: 'exclamation-circle', warning: 'exclamation-triangle' }; |
| notification.innerHTML = `<i class="fas fa-${icons[type]}"></i><span>${message}</span>`; |
| document.body.appendChild(notification); |
| Object.assign(notification.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '12px 20px', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', display: 'flex', alignItems: 'center', gap: '10px', fontSize: '14px', zIndex: '9999', transition: 'all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)', transform: 'translateY(100px)', opacity: '0' }); |
| const colors = { success: { bg: 'rgba(40, 167, 69, 0.9)', color: '#fff' }, error: { bg: 'rgba(220, 53, 69, 0.9)', color: '#fff' }, warning: { bg: 'rgba(255, 193, 7, 0.9)', color: '#212529' }, info: { bg: 'rgba(74, 144, 226, 0.9)', color: '#fff' } }; |
| notification.style.backgroundColor = colors[type].bg; |
| notification.style.color = colors[type].color; |
| setTimeout(() => { |
| notification.style.transform = 'translateY(0)'; |
| notification.style.opacity = '1'; |
| }, 50); |
| setTimeout(() => { |
| notification.style.transform = 'translateY(100px)'; |
| notification.style.opacity = '0'; |
| setTimeout(() => notification.remove(), 400); |
| }, 3000); |
| } |
| |
| function previousSong() { |
| if (currentPlaylist.length === 0) return; |
| currentIndex = (currentIndex - 1 + currentPlaylist.length) % currentPlaylist.length; |
| playSong(currentIndex); |
| } |
| |
| function nextSong() { |
| if (currentPlaylist.length === 0) return; |
| currentIndex = (currentIndex + 1) % currentPlaylist.length; |
| playSong(currentIndex); |
| } |
| |
| initializeApp(); |
| </script> |
| </body> |
| </html> |