feat(music): 实现黑胶唱片播放器与歌词同步功能
- 重构黑胶唱片样式,添加旋转动画与脉冲波效果 - 移除机械臂元素,改用点击唱片控制播放/暂停 - 新增歌词显示区域,实现歌词随播放进度高亮 - 解析并加载 LRC 歌词文件,支持异步获取远程歌词 - 使用 requestAnimationFrame 实现高性能歌词滚动 - 更新移动端悬浮按钮状态以反映音乐播放状态 - 简化主题判断逻辑,优化语言切换功能 - 隐藏 APlayer 默认界面,保留核心音频控制功能
This commit is contained in:
@@ -196,11 +196,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="vinyl-player">
|
<div class="vinyl-player">
|
||||||
<div class="vinyl-disc" id="vinyl-disc"></div>
|
<div class="vinyl-disc" id="vinyl-disc"></div>
|
||||||
<div class="vinyl-arm"></div>
|
|
||||||
<div class="vinyl-info">
|
|
||||||
<span class="vinyl-title" data-i18n="music.playlist">My Playlist</span>
|
|
||||||
<button id="music-toggle" class="link-btn" aria-label="Toggle Music">Play</button>
|
|
||||||
<span id="music-status" class="vinyl-status" style="display:none;" data-i18n="music.unavailable">Player under maintenance</span>
|
<span id="music-status" class="vinyl-status" style="display:none;" data-i18n="music.unavailable">Player under maintenance</span>
|
||||||
|
<div class="vinyl-lyrics" id="vinyl-lyrics">
|
||||||
|
<div id="lyric-current">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="aplayer" class="aplayer" style="flex:1;"></div>
|
<div id="aplayer" class="aplayer" style="flex:1;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1580,15 +1580,24 @@ body {
|
|||||||
.social-dock .s-icon i { background: none !important; -webkit-background-clip: initial !important; background-clip: initial !important; -webkit-text-fill-color: initial !important; color: currentColor !important; text-shadow: none !important; }
|
.social-dock .s-icon i { background: none !important; -webkit-background-clip: initial !important; background-clip: initial !important; -webkit-text-fill-color: initial !important; color: currentColor !important; text-shadow: none !important; }
|
||||||
.area-music { padding: 20px; display: flex; flex-direction: column; }
|
.area-music { padding: 20px; display: flex; flex-direction: column; }
|
||||||
.vinyl-player { position: relative; display: flex; align-items: center; gap: 16px; }
|
.vinyl-player { position: relative; display: flex; align-items: center; gap: 16px; }
|
||||||
.vinyl-disc { width: 140px; height: 140px; border-radius: 50%; background: radial-gradient(#222 0%, #111 30%, #000 60%, #111 100%); box-shadow: inset 0 0 20px rgba(0,0,0,0.6), 0 8px 20px rgba(0,0,0,0.2); position: relative; }
|
.vinyl-disc { width: clamp(110px, 26vw, 160px); height: clamp(110px, 26vw, 160px); border-radius: 50%; background:
|
||||||
|
radial-gradient(#222 0%, #111 30%, #000 60%, #111 100%),
|
||||||
|
repeating-radial-gradient(circle at 50% 50%, rgba(255,255,255,0.06) 0px, rgba(255,255,255,0.06) 2px, transparent 3px, transparent 6px),
|
||||||
|
repeating-conic-gradient(from 0deg, rgba(255,215,0,0.05) 0deg, rgba(255,215,0,0.0) 8deg);
|
||||||
|
box-shadow: inset 0 0 20px rgba(0,0,0,0.6), 0 8px 20px rgba(0,0,0,0.2);
|
||||||
|
position: relative; }
|
||||||
.vinyl-disc::after { content: ""; position: absolute; inset: 50% auto auto 50%; width: 32px; height: 32px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); }
|
.vinyl-disc::after { content: ""; position: absolute; inset: 50% auto auto 50%; width: 32px; height: 32px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); }
|
||||||
.vinyl-arm { width: 80px; height: 8px; background: #888; border-radius: 8px; transform-origin: left center; transform: rotate(12deg); box-shadow: 0 2px 8px rgba(0,0,0,0.2); }
|
.vinyl-disc::before { content: ""; position: absolute; inset: -6px; border-radius: 50%; box-shadow: 0 0 0 2px rgba(108,92,231,0.15); animation: pulseWave 2.4s ease-out infinite; }
|
||||||
|
.vinyl-arm { display: none !important; }
|
||||||
.vinyl-info { display: flex; flex-direction: column; gap: 6px; }
|
.vinyl-info { display: flex; flex-direction: column; gap: 6px; }
|
||||||
.vinyl-title { font-weight: 700; background: var(--gradient-4); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
.vinyl-title { font-weight: 700; background: var(--gradient-4); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||||
.vinyl-status { font-size: 0.85rem; color: var(--text-secondary); }
|
.vinyl-status { font-size: 0.85rem; color: var(--text-secondary); }
|
||||||
.netease-embed { position: absolute; right: 20px; bottom: 10px; width: 340px; height: 86px; border-radius: 12px; overflow: hidden; }
|
|
||||||
.spinning { animation: spinDisc 8s linear infinite; }
|
.spinning { animation: spinDisc 8s linear infinite; }
|
||||||
@keyframes spinDisc { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
@keyframes spinDisc { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
|
.vinyl-lyrics { flex: 1; min-width: 160px; max-width: 240px; padding: 8px 12px; }
|
||||||
|
.vinyl-lyrics #lyric-current { font-size: 0.95rem; line-height: 1.6; background: var(--gradient-3); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||||||
|
@keyframes pulseWave { 0% { box-shadow: 0 0 0 2px rgba(108,92,231,0.0); } 50% { box-shadow: 0 0 0 10px rgba(108,92,231,0.25); } 100% { box-shadow: 0 0 0 2px rgba(108,92,231,0.0); } }
|
||||||
|
.aplayer { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
|
||||||
.mobile-fab { position: fixed; right: 16px; bottom: 88px; z-index: 1100; }
|
.mobile-fab { position: fixed; right: 16px; bottom: 88px; z-index: 1100; }
|
||||||
.fab-main { display: flex; align-items: center; gap: 6px; background: var(--accent); color: #fff; border: none; border-radius: 22px; padding: 10px 14px; box-shadow: 0 8px 18px rgba(0,0,0,0.25); }
|
.fab-main { display: flex; align-items: center; gap: 6px; background: var(--accent); color: #fff; border: none; border-radius: 22px; padding: 10px 14px; box-shadow: 0 8px 18px rgba(0,0,0,0.25); }
|
||||||
.fab-label { font-size: 12px; }
|
.fab-label { font-size: 12px; }
|
||||||
|
|||||||
77
js/about.js
77
js/about.js
@@ -616,17 +616,34 @@ class UIManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initMusic() {
|
initMusic() {
|
||||||
const embed = null; // 不再使用 iframe 嵌入
|
|
||||||
const disc = document.getElementById('vinyl-disc');
|
const disc = document.getElementById('vinyl-disc');
|
||||||
const btn = document.getElementById('music-toggle');
|
|
||||||
const statusEl = document.getElementById('music-status');
|
const statusEl = document.getElementById('music-status');
|
||||||
if (!disc || !btn) return;
|
const lyricEl = document.getElementById('lyric-current');
|
||||||
|
if (!disc) return;
|
||||||
let playing = true;
|
let playing = true;
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
|
let rafId = null; let lyricIdx = 0; this._lyricLines = [];
|
||||||
|
const stopLyricLoop = () => { if (rafId) cancelAnimationFrame(rafId); rafId = null; };
|
||||||
|
const startLyricLoop = () => {
|
||||||
|
stopLyricLoop();
|
||||||
|
const loop = () => {
|
||||||
|
if (!this.aplayer || !playing) return;
|
||||||
|
const ct = this.aplayer.audio.currentTime || 0;
|
||||||
|
while (lyricIdx + 1 < this._lyricLines.length && this._lyricLines[lyricIdx + 1].t <= ct) { lyricIdx++; }
|
||||||
|
const line = this._lyricLines[lyricIdx];
|
||||||
|
if (line && lyricEl && lyricEl.dataset.last !== String(lyricIdx)) {
|
||||||
|
lyricEl.textContent = line.text || '';
|
||||||
|
lyricEl.dataset.last = String(lyricIdx);
|
||||||
|
}
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
rafId = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
const fail = () => {
|
const fail = () => {
|
||||||
playing = false;
|
playing = false;
|
||||||
embed.style.display = 'none';
|
|
||||||
if (statusEl) statusEl.style.display = 'inline';
|
if (statusEl) statusEl.style.display = 'inline';
|
||||||
|
stopLyricLoop();
|
||||||
|
if (lyricEl) { lyricEl.textContent = '--'; lyricEl.dataset.last = ''; }
|
||||||
updateUI();
|
updateUI();
|
||||||
};
|
};
|
||||||
const updateUI = () => {
|
const updateUI = () => {
|
||||||
@@ -638,7 +655,8 @@ class UIManager {
|
|||||||
btn.textContent = this._t('music.play') || 'Play';
|
btn.textContent = this._t('music.play') || 'Play';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
btn.addEventListener('click', () => {
|
disc.addEventListener('click', () => {
|
||||||
|
if (this.aplayer) { this.aplayer.toggle(); return; }
|
||||||
playing = !playing;
|
playing = !playing;
|
||||||
updateUI();
|
updateUI();
|
||||||
});
|
});
|
||||||
@@ -654,17 +672,35 @@ class UIManager {
|
|||||||
listFolded: true,
|
listFolded: true,
|
||||||
audio
|
audio
|
||||||
});
|
});
|
||||||
this.aplayer.on('play', () => {
|
const parseLrc = (lrc) => {
|
||||||
playing = true;
|
if (!lrc) return [];
|
||||||
updateUI();
|
const lines = String(lrc).split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
||||||
});
|
const out = [];
|
||||||
this.aplayer.on('pause', () => {
|
for (const s of lines) {
|
||||||
playing = false;
|
const m = s.match(/\[(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?\](.*)/);
|
||||||
updateUI();
|
if (!m) continue;
|
||||||
});
|
const mm = parseInt(m[1], 10), ss = parseInt(m[2], 10), xx = parseInt(m[3] || '0', 10);
|
||||||
btn.addEventListener('click', () => {
|
const t = mm * 60 + ss + (xx / 1000);
|
||||||
this.aplayer.toggle();
|
const text = m[4].trim();
|
||||||
|
out.push({ t, text });
|
||||||
|
}
|
||||||
|
out.sort((a, b) => a.t - b.t);
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
const ensureLrc = async (track) => {
|
||||||
|
if (track && track.lrc && /^https?:/.test(track.lrc)) {
|
||||||
|
try { const r = await fetch(track.lrc); const s = await r.text(); return parseLrc(s); } catch { return []; }
|
||||||
|
}
|
||||||
|
return parseLrc(track?.lrc);
|
||||||
|
};
|
||||||
|
this.aplayer.on('play', async () => {
|
||||||
|
playing = true; updateUI();
|
||||||
|
const cur = this.aplayer.list.audios[this.aplayer.list.index];
|
||||||
|
this._lyricLines = await ensureLrc(cur);
|
||||||
|
lyricIdx = 0; startLyricLoop();
|
||||||
});
|
});
|
||||||
|
this.aplayer.on('pause', () => { playing = false; updateUI(); stopLyricLoop(); });
|
||||||
|
this.aplayer.on('ended', () => { stopLyricLoop(); });
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}).catch(() => fail());
|
}).catch(() => fail());
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -685,16 +721,11 @@ class UIManager {
|
|||||||
if (!main || !menu || !fLang || !fTheme || !fMusic) return;
|
if (!main || !menu || !fLang || !fTheme || !fMusic) return;
|
||||||
const updateLabels = () => {
|
const updateLabels = () => {
|
||||||
const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en');
|
const lang = localStorage.getItem('lang') || (navigator.language && navigator.language.startsWith('zh') ? 'zh' : 'en');
|
||||||
const cacheKey = window.SiteConfig?.cacheKeys?.theme?.key || 'theme-v2';
|
const theme = (localStorage.getItem('theme') === 'night') ? 'night' : 'day';
|
||||||
const themeBean = localStorage.getItem(cacheKey);
|
|
||||||
let theme = 'day';
|
|
||||||
if (themeBean != null && themeBean.value != null) {
|
|
||||||
theme = themeBean.value;
|
|
||||||
}
|
|
||||||
fLang.querySelector('.fab-text').textContent = lang === 'zh' ? '中文' : 'English';
|
fLang.querySelector('.fab-text').textContent = lang === 'zh' ? '中文' : 'English';
|
||||||
fTheme.querySelector('.fab-text').textContent = theme === 'night' ? 'Night' : 'Day';
|
fTheme.querySelector('.fab-text').textContent = theme === 'night' ? 'Night' : 'Day';
|
||||||
const mt = document.getElementById('music-toggle');
|
const playing = (this.aplayer && !this.aplayer.audio.paused);
|
||||||
fMusic.querySelector('.fab-text').textContent = mt ? mt.textContent : 'Music';
|
fMusic.querySelector('.fab-text').textContent = playing ? (this._t('music.pause') || 'Pause') : (this._t('music.play') || 'Play');
|
||||||
};
|
};
|
||||||
main.addEventListener('click', () => {
|
main.addEventListener('click', () => {
|
||||||
menu.classList.toggle('open');
|
menu.classList.toggle('open');
|
||||||
|
|||||||
Reference in New Issue
Block a user