import {dialog} from "../dialog.js";
/**
* 封装的音乐播放器组件
* */
export class MusicPlayer {
constructor() {
// Singleton check
if (window.__music_player_instance) {
return window.__music_player_instance;
}
window.__music_player_instance = this;
this.audio = new Audio();
this.playlist = [];
this.currentIndex = 0;
this.isPlaying = false;
this.isShuffled = false;
this.playMode = 'order'; // order, random, single
this.lyrics = [];
this.showLyrics = false;
this.lyricsOnBackground = false; // 歌词是否显示在页面背景
this.showList = window.innerWidth > 768;
this.isSeeking = false;
this.currentLyricIndex = -1;
// BEM Elements cache
this.el = {};
this.init();
}
async init() {
// Initialize Elements
this.initElements();
// Bind events
this.bindEvents();
// Load data
await this.fetchMusic();
// 监听hash变化,支持浏览器前进后退
this._isHandlingHashChange = false;
window.addEventListener('hashchange', () => {
if (this._isHandlingHashChange) return; // 避免循环调用
const isOpen = window.location.hash === '#music-player';
const panelIsOpen = this.el.panel.classList.contains('music-player__panel--open');
if (isOpen && !panelIsOpen) {
this._isHandlingHashChange = true;
this.togglePanel(true);
this._isHandlingHashChange = false;
} else if (!isOpen && panelIsOpen) {
this._isHandlingHashChange = true;
this.togglePanel(false);
this._isHandlingHashChange = false;
}
});
// 初始化时检查hash
if (window.location.hash === '#music-player') {
this.togglePanel(true);
}
}
initElements() {
// Cache elements (HTML is now in _Layout.cshtml via _MusicPlayer.cshtml)
this.el.mini = document.getElementById('mp-mini');
this.el.miniCover = document.getElementById('mp-mini-cover');
this.el.miniStatus = document.getElementById('mp-mini-status');
this.el.miniProgress = document.getElementById('mp-mini-progress');
this.el.panel = document.getElementById('mp-panel');
this.el.backdrop = document.getElementById('mp-backdrop');
this.el.close = document.getElementById('mp-close');
this.el.title = document.getElementById('mp-title');
this.el.artist = document.getElementById('mp-artist');
this.el.cover = document.getElementById('mp-cover');
this.el.play = document.getElementById('mp-play');
this.el.prev = document.getElementById('mp-prev');
this.el.next = document.getElementById('mp-next');
this.el.seek = document.getElementById('mp-seek');
this.el.timeCurrent = document.getElementById('mp-time-current');
this.el.timeTotal = document.getElementById('mp-time-total');
this.el.mode = document.getElementById('mp-mode');
this.el.lrcToggle = document.getElementById('mp-lrc-toggle');
this.el.listToggle = document.getElementById('mp-list-toggle');
this.el.list = document.getElementById('mp-list');
this.el.listItems = document.getElementById('mp-list-items');
this.el.listCloseMobile = document.getElementById('mp-list-close-mobile');
this.el.lyricsContainer = document.getElementById('mp-lyrics-container');
this.el.lyricsInner = document.getElementById('mp-lyrics-inner');
this.el.bgLyrics = document.getElementById('mp-bg-lyrics');
this.el.bgLyricsInner = document.getElementById('mp-bg-lyrics-inner');
// Progress ring circumference
// r=34 -> 2*PI*34 = 213.628
this.circumference = 213.6;
// Initialize state
if (this.showList) {
this.toggleList(true);
}
}
bindEvents() {
// Mini Player Interactions
let clickTimer = null;
// Click: Toggle Play
this.el.mini.addEventListener('click', () => {
if (clickTimer) clearTimeout(clickTimer);
clickTimer = setTimeout(() => {
this.togglePlay();
clickTimer = null;
}, 300);
});
// Double Click: Open Panel
this.el.mini.addEventListener('dblclick', (e) => {
e.preventDefault();
if (clickTimer) {
clearTimeout(clickTimer);
clickTimer = null;
}
this.togglePanel(true);
});
// Touch Long Press: Open Panel
let touchTimer = null;
const startTouch = () => {
touchTimer = setTimeout(() => {
this.togglePanel(true);
touchTimer = null;
}, 600); // 600ms long press
};
const endTouch = () => {
if (touchTimer) {
clearTimeout(touchTimer);
touchTimer = null;
}
};
this.el.mini.addEventListener('touchstart', startTouch, {passive: true});
this.el.mini.addEventListener('touchend', endTouch);
this.el.mini.addEventListener('touchmove', endTouch);
this.el.close.addEventListener('click', () => this.togglePanel(false));
// Backdrop click only closes panel on mobile (or if we decide to show backdrop)
this.el.backdrop.addEventListener('click', () => this.togglePanel(false));
// Play Controls
this.el.play.addEventListener('click', () => this.togglePlay());
this.el.prev.addEventListener('click', () => this.skip('prev'));
this.el.next.addEventListener('click', () => this.skip('next'));
// Seek
this.el.seek.addEventListener('input', (e) => {
const pct = e.target.value / 100;
this.el.timeCurrent.textContent = this.formatTime(this.audio.duration * pct);
this.isSeeking = true;
});
this.el.seek.addEventListener('change', (e) => {
const pct = e.target.value / 100;
if (this.audio.duration) {
this.audio.currentTime = this.audio.duration * pct;
}
this.isSeeking = false;
});
// Modes
this.el.mode.addEventListener('click', () => this.cycleMode());
this.el.lrcToggle.addEventListener('click', () => {
// 三种状态循环:关闭 -> 面板内 -> 页面背景 -> 关闭
if (!this.showLyrics && !this.lyricsOnBackground) {
// 关闭 -> 面板内
this.showLyrics = true;
this.lyricsOnBackground = false;
} else if (this.showLyrics && !this.lyricsOnBackground) {
// 面板内 -> 页面背景
this.showLyrics = false;
this.lyricsOnBackground = true;
} else {
// 页面背景 -> 关闭
this.showLyrics = false;
this.lyricsOnBackground = false;
}
// 更新UI
this.updateLyricsDisplay();
// 保存状态
this.saveState();
});
this.el.listToggle.addEventListener('click', () => {
this.toggleList(!this.showList);
});
// Mobile list close button
if (this.el.listCloseMobile) {
this.el.listCloseMobile.addEventListener('click', () => {
this.toggleList(false);
});
}
// Audio Events
this.audio.addEventListener('timeupdate', () => this.onTimeUpdate());
this.audio.addEventListener('ended', () => this.onEnded());
this.audio.addEventListener('durationchange', () => {
this.el.timeTotal.textContent = this.formatTime(this.audio.duration);
});
this.audio.addEventListener('play', () => this.updatePlayState(true));
this.audio.addEventListener('pause', () => this.updatePlayState(false));
this.audio.addEventListener('error', (e) => {
console.error("Audio error", e);
});
// Media Session
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => this.play());
navigator.mediaSession.setActionHandler('pause', () => this.pause());
navigator.mediaSession.setActionHandler('previoustrack', () => this.skip('prev'));
navigator.mediaSession.setActionHandler('nexttrack', () => this.skip('next'));
}
}
async fetchMusic() {
try {
const res = await fetch('/Music/Recommend/v2');
if (!res.ok) {
dialog.toast('Failed to load music');
return;
}
this.playlist = await res.json();
if (this.playlist.length > 0) {
this.renderList();
this.restoreState();
}
} catch (e) {
console.error(e);
this.el.title.textContent = '加载失败';
}
}
renderList() {
this.el.listItems.innerHTML = this.playlist.map((track, index) => `
<div class="music-player__item" data-index="${index}">
<div class="music-player__item-index">${index + 1}</div>
<div class="music-player__item-meta">
<span class="music-player__item-title">${track.name}</span>
<span class="music-player__item-artist">${track.singer}</span>
</div>
</div>
`).join('');
this.el.listItems.querySelectorAll('.music-player__item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.dataset.index);
this.loadTrack(index, true);
});
});
}
loadTrack(index, autoPlay = true, skipSave = false) {
if (!this.playlist[index]) return;
this.currentIndex = index;
const track = this.playlist[index];
this.audio.src = track.musicSrc;
// Update UI
this.el.title.textContent = track.name;
this.el.artist.textContent = track.singer;
this.el.cover.style.backgroundImage = `url('${track.cover}')`;
this.el.miniCover.style.backgroundImage = `url('${track.cover}')`;
// Active item in list
const active = this.el.list.querySelector('.music-player__item--active');
if (active) active.classList.remove('music-player__item--active');
const newItem = this.el.list.querySelector(`.music-player__item[data-index="${index}"]`);
if (newItem) {
newItem.classList.add('music-player__item--active');
if (this.showList) {
newItem.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}
// Lyrics
this.parseLyrics(track.lyric);
this.currentLyricIndex = -1;
if (autoPlay) {
this.play();
} else {
this.updatePlayState(false);
}
// Update MediaSession
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: track.name,
artist: track.singer,
artwork: [
{src: track.cover, sizes: '512x512', type: 'image/jpeg'}
]
});
}
if (!skipSave) {
this.saveState();
}
}
play() {
const p = this.audio.play();
if (p) {
p.catch(e => console.warn("Playback prevented:", e));
}
}
pause() {
this.audio.pause();
}
togglePlay() {
if (this.audio.paused) this.play();
else this.pause();
}
updatePlayState(isPlaying) {
this.isPlaying = isPlaying;
const iconClass = isPlaying ? 'fa-pause' : 'fa-play';
// Main button
this.el.play.innerHTML = `<i class="fa ${iconClass}"></i>`;
// Mini status icon (just indicates status, no longer a button)
this.el.miniStatus.innerHTML = `<i class="fa ${iconClass}"></i>`;
if (isPlaying) {
this.el.cover.classList.add('music-player__cover--rotating');
this.el.miniCover.classList.add('music-player__cover--rotating');
} else {
this.el.cover.classList.remove('music-player__cover--rotating');
this.el.miniCover.classList.remove('music-player__cover--rotating');
}
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused";
}
}
skip(direction) {
let nextIndex = this.currentIndex;
if (this.playMode === 'random') {
if (this.playlist.length > 1) {
do {
nextIndex = Math.floor(Math.random() * this.playlist.length);
} while (nextIndex === this.currentIndex);
}
} else {
if (direction === 'next') {
nextIndex = (this.currentIndex + 1) % this.playlist.length;
} else {
nextIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
}
}
this.loadTrack(nextIndex, true);
}
onEnded() {
if (this.playMode === 'single') {
this.audio.currentTime = 0;
this.play();
} else {
this.skip('next');
}
}
onTimeUpdate() {
if (!this.isSeeking) {
const cur = this.audio.currentTime || 0;
const dur = this.audio.duration || 1;
const pct = (cur / dur) * 100;
this.el.seek.value = isFinite(pct) ? pct : 0;
this.el.timeCurrent.textContent = this.formatTime(cur);
// Mini progress ring
if (isFinite(dur) && dur > 0) {
const offset = this.circumference - (cur / dur) * this.circumference;
this.el.miniProgress.style.strokeDashoffset = offset;
}
}
this.syncLyrics(this.audio.currentTime);
// 定期自动保存进度(每5秒保存一次,避免过于频繁)
const now = Date.now();
if (!this._lastAutoSaveTime) {
this._lastAutoSaveTime = now;
}
if (now - this._lastAutoSaveTime >= 5000) {
this.saveState();
this._lastAutoSaveTime = now;
}
}
cycleMode() {
const modes = ['order', 'random', 'single'];
const modeNames =
{'order': '顺序播放', 'random': '随机播放', 'single': '单曲循环'};
const icons = {'order': 'fa-sort-amount-asc', 'random': 'fa-random', 'single': 'fa-retweet'};
let idx = modes.indexOf(this.playMode);
idx = (idx + 1) % modes.length;
this.playMode = modes[idx];
const newTitle = modeNames[this.playMode];
this.el.mode.innerHTML = `<i class="fa ${icons[this.playMode]}"></i>`;
// Update tooltip logic
this.el.mode.setAttribute('data-original-title', newTitle);
this.el.mode.removeAttribute('title');
dialog.showTooltip(this.el.mode, newTitle);
this.saveState();
}
toggleList(show) {
this.showList = show;
this.el.list.classList.toggle('music-player__list--visible', this.showList);
this.el.listToggle.classList.toggle('music-player__text-btn--active', this.showList);
// Show/Hide mobile close button
if (this.el.listCloseMobile) {
// Check if mobile
if (window.innerWidth <= 768) {
this.el.listCloseMobile.style.display = show ? 'flex' : 'none';
} else {
this.el.listCloseMobile.style.display = 'none';
}
}
if (this.showList) {
const activeItem = this.el.list.querySelector('.music-player__item--active');
if (activeItem) {
setTimeout(() => {
activeItem.scrollIntoView({behavior: 'smooth', block: 'center'});
}, 100);
}
}
}
togglePanel(show) {
if (show) {
this.el.panel.classList.add('music-player__panel--open');
// 添加hash(如果不是由hashchange事件触发的)
if (!this._isHandlingHashChange && window.location.hash !== '#music-player') {
window.location.hash = '#music-player';
}
// 移动端禁用滚动
if (window.innerWidth <= 768) {
document.body.style.overflow = 'hidden';
}
} else {
this.el.panel.classList.remove('music-player__panel--open');
// 清除hash(如果不是由hashchange事件触发的)
if (!this._isHandlingHashChange && window.location.hash === '#music-player') {
window.history.replaceState(null, '', window.location.pathname + window.location.search);
}
// 恢复滚动
document.body.style.overflow = '';
}
}
saveState() {
const currentTrack = this.playlist[this.currentIndex];
const state = {
mode: this.playMode,
trackId: currentTrack ? currentTrack.id : null,
time: this.audio.currentTime,
showLyrics: this.showLyrics,
lyricsOnBackground: this.lyricsOnBackground
};
localStorage.setItem('dpz_music_player_state', JSON.stringify(state));
}
restoreState() {
try {
const saved = localStorage.getItem('dpz_music_player_state');
if (saved) {
const state = JSON.parse(saved);
// Restore mode
if (state.mode && ['order', 'random', 'single'].includes(state.mode)) {
this.playMode = state.mode;
const icons = {'order': 'fa-sort-amount-asc', 'random': 'fa-random', 'single': 'fa-retweet'};
const modeNames = {'order': '顺序播放', 'random': '随机播放', 'single': '单曲循环'};
this.el.mode.innerHTML = `<i class="fa ${icons[this.playMode]}"></i>`;
this.el.mode.setAttribute('title', modeNames[this.playMode]);
// Update data-original-title if it exists (for tooltip consistency)
if (this.el.mode.hasAttribute('data-original-title')) {
this.el.mode.setAttribute('data-original-title', modeNames[this.playMode]);
}
}
// Restore lyrics display state
if (typeof state.showLyrics === 'boolean') {
this.showLyrics = state.showLyrics;
}
if (typeof state.lyricsOnBackground === 'boolean') {
this.lyricsOnBackground = state.lyricsOnBackground;
}
// Restore track by ID
let index = 0;
let time = 0;
if (state.trackId) {
const foundIndex = this.playlist.findIndex(t => t.id === state.trackId);
if (foundIndex !== -1) {
index = foundIndex;
// Restore time only if track is found
const savedTime = parseFloat(state.time);
if (isFinite(savedTime) && savedTime > 0) {
time = savedTime;
}
}
}
// If trackId not found or not present, index defaults to 0 and time to 0 (as initialized variables)
// 恢复状态时跳过保存,避免保存错误的进度
this.loadTrack(index, false, true);
// 等待音频加载完成后再设置播放时间
const restoreTime = () => {
if (time > 0) {
// 确保音频已加载元数据
if (this.audio.readyState >= 2) {
try {
this.audio.currentTime = time;
// 重置自动保存时间,避免立即保存恢复的时间
this._lastAutoSaveTime = Date.now();
} catch (e) {
console.warn('Failed to restore time:', e);
}
} else {
// 如果还没加载完成,等待加载完成
const onLoadedMetadata = () => {
try {
if (time > 0 && this.audio.duration && time < this.audio.duration) {
this.audio.currentTime = time;
// 重置自动保存时间,避免立即保存恢复的时间
this._lastAutoSaveTime = Date.now();
}
} catch (e) {
console.warn('Failed to restore time on loadedmetadata:', e);
}
this.audio.removeEventListener('loadedmetadata', onLoadedMetadata);
};
this.audio.addEventListener('loadedmetadata', onLoadedMetadata);
}
}
};
// 尝试立即恢复时间,如果音频还没加载则等待
restoreTime();
// Restore lyrics display after track is loaded
this.updateLyricsDisplay();
} else {
this.loadTrack(0, false);
}
} catch (e) {
console.error('Failed to restore music player state', e);
this.loadTrack(0, false);
}
}
formatTime(seconds) {
if (!isFinite(seconds)) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s < 10 ? '0' : ''}${s}`;
}
parseLyrics(lrcString) {
this.lyrics = [];
this.el.lyricsInner.innerHTML = '';
this.el.bgLyricsInner.innerHTML = ''; // Reset background lyrics too
this.el.bgLyricsInner.style.transform = 'translateY(0)'; // Reset scroll
if (!lrcString) {
const html = '<div class="music-player__lyric-line">暂无歌词</div>';
this.el.lyricsInner.innerHTML = html;
this.el.bgLyricsInner.innerHTML = html;
return;
}
const lines = lrcString.split('\n');
const regex = /\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/;
for (const line of lines) {
const match = regex.exec(line);
if (match) {
const min = parseInt(match[1]);
const sec = parseInt(match[2]);
const ms = match[3].length === 3 ? parseInt(match[3]) : parseInt(match[3]) * 10;
const time = min * 60 + sec + ms / 1000;
const text = match[4].trim();
if (text) {
this.lyrics.push({time, text});
}
}
}
const noLyricsHtml = '<div class="music-player__lyric-line">纯音乐 / 暂无歌词</div>';
const lyricsHtml = this.lyrics.length === 0
? noLyricsHtml
: this.lyrics.map((l, i) =>
`<div class="music-player__lyric-line" data-index="${i}">${l.text}</div>`
).join('');
// 更新面板内歌词(显示全部)
this.el.lyricsInner.innerHTML = lyricsHtml;
// 更新背景歌词(显示全部,通过CSS transform滚动)
this.el.bgLyricsInner.innerHTML = lyricsHtml;
// 更新显示状态
this.updateLyricsDisplay();
}
updateLyricsDisplay() {
// 更新面板内歌词显示
this.el.lyricsContainer.style.display = (this.showLyrics && !this.lyricsOnBackground) ? 'block' : 'none';
this.el.cover.style.display = (this.showLyrics && !this.lyricsOnBackground) ? 'none' : 'block';
// 更新页面背景歌词显示
this.el.bgLyrics.classList.toggle('music-player__bg-lyrics--visible', this.lyricsOnBackground);
// 更新按钮状态
const isActive = this.showLyrics || this.lyricsOnBackground;
this.el.lrcToggle.classList.toggle('music-player__text-btn--active', isActive);
// 如果显示背景歌词,立即同步一次以显示初始歌词
if (this.lyricsOnBackground) {
this.syncLyrics(this.audio.currentTime || 0);
} else if (isActive) {
// 如果显示面板内歌词,同步当前时间
this.syncLyrics(this.audio.currentTime);
}
}
syncLyrics(time) {
if ((!this.showLyrics && !this.lyricsOnBackground) || !this.lyrics.length) return;
let activeIdx = -1;
for (let i = 0; i < this.lyrics.length; i++) {
if (time >= this.lyrics[i].time) {
activeIdx = i;
} else {
break;
}
}
if (activeIdx !== this.currentLyricIndex) {
this.currentLyricIndex = activeIdx;
// 同步面板内歌词
if (this.showLyrics && !this.lyricsOnBackground) {
const lines = this.el.lyricsInner.children;
for (let line of lines) line.classList.remove('music-player__lyric-line--current');
if (activeIdx >= 0 && lines[activeIdx]) {
const activeLine = lines[activeIdx];
activeLine.classList.add('music-player__lyric-line--current');
activeLine.scrollIntoView({behavior: 'smooth', block: 'center'});
}
}
// 同步页面背景歌词
if (this.lyricsOnBackground) {
const container = this.el.bgLyricsInner;
const lines = container.children;
// Clear active classes
for (let line of lines) {
line.classList.remove('music-player__lyric-line--current');
}
if (activeIdx >= 0 && lines[activeIdx]) {
const activeLine = lines[activeIdx];
activeLine.classList.add('music-player__lyric-line--current');
// Calculate transform to align active line to top
// We want activeLine to be at the top of the container
const offset = activeLine.offsetTop;
container.style.transform = `translateY(-${offset}px)`;
}
}
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面是对这段音乐播放器代码的逐步说明与主要功能点的总结(中文):
总体概述
- 这是一个封装好的单例式前端音乐播放器组件(类名 MusicPlayer),使用 HTMLAudioElement(new Audio())播放音频,并负责 UI 同步、播放列表、歌词显示、播放模式、进度保存与恢复、以及与浏览器媒体会话 API 的集成。
- 通过把实例保存到 window.__music_player_instance,实现单例(再次 new 时返回已有实例)。
初始化与元素缓存
- constructor 中初始化各种状态变量(播放列表、当前索引、播放模式、歌词相关、UI 显示状态等),并调用 init()。
- init(): 初始化 DOM 元素(initElements)、绑定事件(bindEvents)、从后端加载推荐音乐(fetchMusic),并监听 URL hash(#music-player)以控制面板打开/关闭,支持浏览器前进后退控制面板状态。
- initElements(): 缓存常用 DOM 节点(迷你播放器、面板、控制按钮、进度、歌词容器等),并计算环形进度条周长(circumference),根据屏幕宽度决定是否默认显示列表。
事件绑定(bindEvents)
- 迷你播放器:单击切换播放/暂停(通过延迟 300ms 判断单击),双击或触摸长按打开面板(touch 长按 600ms)。
- Panel 控制:关闭按钮、遮罩点击(移动端可关闭),列表开关,歌词显示模式切换(三态:关闭 -> 面板内 -> 页面背景 -> 关闭)。
- 播放控制:播放/暂停、上一首/下一首、播放模式循环切换(顺序/随机/单曲),以及进度条 seek(input 临时显示,change 时设置 audio.currentTime)。
- audio 事件监听:timeupdate、ended、durationchange、play、pause、error 等,用于 UI 更新、歌词同步、自动保存进度等。
- 支持 navigator.mediaSession:设置 play/pause/next/previous 操作,以及在 loadTrack 时设置媒体元数据(title/artist/artwork)和在 updatePlayState 设置 playbackState。
获取与渲染播放列表(fetchMusic、renderList)
- fetchMusic(): 使用 fetch('/Music/Recommend/v2') 获取播放列表 JSON,失败时显示提示(dialog.toast),成功后渲染列表并恢复上次状态 restoreState。
- renderList(): 把播放列表渲染为 DOM 列表项,并为每项绑定点击事件以加载对应曲目。
加载曲目与 UI 更新(loadTrack)
- loadTrack(index, autoPlay = true, skipSave = false)
- 设置 this.currentIndex、audio.src、更新标题/歌手/封面(面板和迷你封面)。
- 在列表中高亮当前项并滚动可见。
- 解析歌词(parseLyrics),重置当前歌词索引。
- 如果 autoPlay 为 true,调用 play() 启动播放;否则更新播放状态为暂停。
- 更新 MediaSession metadata。
- 除非 skipSave 为 true,会调用 saveState 保存当前状态。
播放控制与播放模式
- play()/pause()/togglePlay():对 audio.play()/audio.pause() 的封装;play() 捕获 play 返回的 Promise 的异常(浏览器策略可能阻止自动播放)。
- updatePlayState(isPlaying):更新按钮图标、迷你状态图标、封面旋转样式、mediaSession playbackState。
- skip(direction):根据 playMode(order、random、single)决定下一首或上一首;random 会随机选择不等于当前的索引(若列表长度 > 1)。
- onEnded(): 单曲循环模式会把 currentTime 设为 0 并重播,否则跳到下一首。
进度与自动保存
- onTimeUpdate(): 更新面板进度条、当前时间文本,更新迷你圆环进度(通过 strokeDashoffset),并调用 syncLyrics 同步歌词。
- 每 5 秒自动保存一次播放进度(saveState),用 _lastAutoSaveTime 限制频率。
歌词解析与同步(parseLyrics、syncLyrics、updateLyricsDisplay)
- parseLyrics(lrcString): 使用正则解析 LRC 格式的歌词行 [mm:ss.xx]text,并把解析结果填入 this.lyrics(每项包含 time 和 text),然后把歌词渲染到面板内(el.lyricsInner)和背景歌词容器(el.bgLyricsInner)。
- updateLyricsDisplay(): 根据 this.showLyrics 与 this.lyricsOnBackground 控制面板内歌词与背景歌词的显示与隐藏,并在必要时立即同步当前时间对应的歌词。
- syncLyrics(time): 根据当前时间计算应高亮的歌词索引,对面板和背景分别处理:
- 面板内:为当前行添加当前样式并滚动到可见(scrollIntoView)。
- 背景:给当前行加样式,并通过 transform translateY(-offset) 把该行移动到容器顶部(实现背景歌词滚动效果)。
状态持久化(saveState / restoreState)
- saveState(): 将播放模式、当前曲目 ID、当前时间、歌词显示状态保存到 localStorage(key dpz_music_player_state)。
- restoreState(): 从 localStorage 读取并恢复模式、歌词显示状态、根据保存的 trackId 在 playlist 中找到对应索引并 loadTrack(skipSave),然后尽可能恢复保存的播放时间(考虑 readyState / loadedmetadata)。若没有保存则加载第一首。
UI 面板与列表控制
- togglePanel(show):打开或关闭主面板;在打开时设置 hash 为 #music-player(除非由 hashchange 处理),移动端打开时禁止页面滚动;关闭时移除 hash(使用 replaceState 避免触发 history 记录)。
- 通过监听 window.hashchange,支持浏览器前进后退控制面板打开/关闭,并用 _isHandlingHashChange 防止循环触发。
- toggleList(show):显示/隐藏播放列表,控制移动端的关闭按钮显示,并滚动到高亮项。
其他细节/注意点
- 使用 dialog.showTooltip / dialog.toast 与外部 dialog 模块集成(文件顶部 import)。
- 迷你播放器单击/双击区分通过 setTimeout+clearTimeout 实现(避免将双击误判为单击)。
- lyrics 正则处理了毫秒为 2 或 3 位的情况,并把两位 ms 扩展为毫秒(*10)。
- 部分操作在加载期间会考虑 readyState 和 loadedmetadata 以确保设置 currentTime 不会抛错。
- 假设页面中存在对应 id 的 DOM 元素(HTML 在后端模板 _MusicPlayer.cshtml 中注入)。
总结
- 这是一个功能完善的前端音乐播放器实现,包含播放/暂停、上一/下一、三种播放模式、播放列表渲染、歌词解析与面板/背景展示、播放进度与环形迷你进度、媒体会话集成、以及本地状态持久化与页面 hash 控制面板的打开关闭。代码在可用性细节(单击/双击、触摸长按、自动保存频率、恢复进度时机)上也做了较多考虑。
评论加载中...