- 响应式设计,支持桌面端和手机端自适应 - 首页展示轮播图、推荐歌单、热门歌曲、新碟上架、热门歌手 - 歌曲/歌手/专辑/歌单详情页 - 底部播放器:播放/暂停、上下首、进度拖动、音量控制 - 歌词同步滚动显示 - 搜索功能 - 后台管理 /admin - JSON文件存储
821 lines
27 KiB
JavaScript
821 lines
27 KiB
JavaScript
// ============ 全局状态 ============
|
|
const state = {
|
|
currentPage: 'home',
|
|
currentSong: null,
|
|
playlist: [],
|
|
playlistIndex: -1,
|
|
isPlaying: false,
|
|
lyricsData: [],
|
|
currentLyricIndex: -1
|
|
};
|
|
|
|
// ============ 路由系统 ============
|
|
const router = {
|
|
go(path) {
|
|
history.pushState(null, '', path);
|
|
this.handleRoute();
|
|
},
|
|
|
|
handleRoute() {
|
|
const path = window.location.pathname;
|
|
|
|
// 更新导航激活状态
|
|
document.querySelectorAll('.header-nav a').forEach(a => {
|
|
a.classList.remove('active');
|
|
const page = a.dataset.page;
|
|
if (path === '/' && page === 'home') a.classList.add('active');
|
|
else if (path.startsWith('/' + page)) a.classList.add('active');
|
|
});
|
|
|
|
// 路由匹配
|
|
if (path === '/') {
|
|
this.renderHome();
|
|
} else if (path === '/discover') {
|
|
this.renderDiscover();
|
|
} else if (path === '/artists') {
|
|
this.renderArtists();
|
|
} else if (path === '/playlists') {
|
|
this.renderPlaylists();
|
|
} else if (path.startsWith('/song/')) {
|
|
const id = path.split('/')[2];
|
|
this.renderSongDetail(id);
|
|
} else if (path.startsWith('/artist/')) {
|
|
const id = path.split('/')[2];
|
|
this.renderArtistDetail(id);
|
|
} else if (path.startsWith('/album/')) {
|
|
const id = path.split('/')[2];
|
|
this.renderAlbumDetail(id);
|
|
} else if (path.startsWith('/playlist/')) {
|
|
const id = path.split('/')[2];
|
|
this.renderPlaylistDetail(id);
|
|
} else if (path.startsWith('/search')) {
|
|
const q = new URLSearchParams(window.location.search).get('q');
|
|
this.renderSearch(q);
|
|
} else {
|
|
this.renderHome();
|
|
}
|
|
},
|
|
|
|
// 首页
|
|
async renderHome() {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const data = await api.get('/api/home');
|
|
|
|
let html = '';
|
|
|
|
// 轮播图
|
|
if (data.banners.length > 0) {
|
|
html += '<div class="banner-section" id="bannerSlider">';
|
|
data.banners.forEach((banner, i) => {
|
|
html += `
|
|
<div class="banner-slide ${i === 0 ? 'active' : ''}" data-link="${banner.link}">
|
|
<img src="${banner.image || '/images/banner-placeholder.jpg'}" alt="${banner.title}" onerror="this.style.display='none'">
|
|
<div class="banner-overlay">
|
|
<h2>${banner.title}</h2>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '<div class="banner-dots">';
|
|
data.banners.forEach((_, i) => {
|
|
html += `<span class="${i === 0 ? 'active' : ''}" onclick="banner.goTo(${i})"></span>`;
|
|
});
|
|
html += '</div></div>';
|
|
}
|
|
|
|
// 推荐歌单
|
|
if (data.featuredPlaylists.length > 0) {
|
|
html += '<div class="section-title"><h3>推荐歌单</h3><span class="more-link" onclick="router.go(\'/playlists\')">更多 <i class="fas fa-chevron-right"></i></span></div>';
|
|
html += '<div class="playlist-grid">';
|
|
data.featuredPlaylists.forEach(pl => {
|
|
html += `
|
|
<div class="playlist-card" onclick="router.go('/playlist/${pl.id}')">
|
|
<div class="cover">
|
|
<img src="${pl.cover || ''}" alt="${pl.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-list\\'></i></div>'">
|
|
<div class="play-btn"><i class="fas fa-play"></i></div>
|
|
</div>
|
|
<div class="title">${pl.title}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// 热门歌曲
|
|
if (data.hotSongs.length > 0) {
|
|
html += '<div class="section-title"><h3>热门推荐</h3></div>';
|
|
html += '<div class="song-list">';
|
|
data.hotSongs.slice(0, 10).forEach((song, i) => {
|
|
html += renderSongItem(song, i + 1);
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// 最新专辑
|
|
if (data.albums.length > 0) {
|
|
html += '<div class="section-title"><h3>新碟上架</h3><span class="more-link" onclick="router.go(\'/discover\')">更多 <i class="fas fa-chevron-right"></i></span></div>';
|
|
html += '<div class="album-grid">';
|
|
data.albums.slice(0, 6).forEach(album => {
|
|
html += `
|
|
<div class="album-card" onclick="router.go('/album/${album.id}')">
|
|
<div class="cover">
|
|
<img src="${album.cover || ''}" alt="${album.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-compact-disc\\'></i></div>'">
|
|
</div>
|
|
<div class="title">${album.title}</div>
|
|
<div class="artist">${album.artist_name || ''}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
// 热门歌手
|
|
if (data.artists.length > 0) {
|
|
html += '<div class="section-title"><h3>热门歌手</h3><span class="more-link" onclick="router.go(\'/artists\')">更多 <i class="fas fa-chevron-right"></i></span></div>';
|
|
html += '<div class="artist-grid">';
|
|
data.artists.slice(0, 6).forEach(artist => {
|
|
html += `
|
|
<div class="artist-card" onclick="router.go('/artist/${artist.id}')">
|
|
<div class="avatar">
|
|
<img src="${artist.avatar || ''}" alt="${artist.name}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-user\\'></i></div>'">
|
|
</div>
|
|
<div class="name">${artist.name}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
app.innerHTML = html;
|
|
|
|
// 初始化轮播
|
|
if (data.banners.length > 0) {
|
|
banner.init(data.banners.length);
|
|
}
|
|
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
console.error(err);
|
|
}
|
|
},
|
|
|
|
// 发现页
|
|
async renderDiscover() {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const [songsData, albumsData] = await Promise.all([
|
|
api.get('/api/songs?limit=50'),
|
|
api.get('/api/albums')
|
|
]);
|
|
|
|
let html = '<div class="section-title"><h3>全部歌曲</h3></div>';
|
|
html += '<div class="song-list">';
|
|
songsData.songs.forEach((song, i) => {
|
|
html += renderSongItem(song, i + 1);
|
|
});
|
|
html += '</div>';
|
|
|
|
html += '<div class="section-title"><h3>全部专辑</h3></div>';
|
|
html += '<div class="album-grid">';
|
|
albumsData.forEach(album => {
|
|
html += `
|
|
<div class="album-card" onclick="router.go('/album/${album.id}')">
|
|
<div class="cover">
|
|
<img src="${album.cover || ''}" alt="${album.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-compact-disc\\'></i></div>'">
|
|
</div>
|
|
<div class="title">${album.title}</div>
|
|
<div class="artist">${album.artist_name || ''}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 歌手列表页
|
|
async renderArtists() {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const artists = await api.get('/api/artists');
|
|
|
|
let html = '<div class="section-title"><h3>全部歌手</h3></div>';
|
|
html += '<div class="artist-grid">';
|
|
artists.forEach(artist => {
|
|
html += `
|
|
<div class="artist-card" onclick="router.go('/artist/${artist.id}')">
|
|
<div class="avatar">
|
|
<img src="${artist.avatar || ''}" alt="${artist.name}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-user\\'></i></div>'">
|
|
</div>
|
|
<div class="name">${artist.name}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 歌单列表页
|
|
async renderPlaylists() {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const playlists = await api.get('/api/playlists');
|
|
|
|
let html = '<div class="section-title"><h3>全部歌单</h3></div>';
|
|
html += '<div class="playlist-grid">';
|
|
playlists.forEach(pl => {
|
|
html += `
|
|
<div class="playlist-card" onclick="router.go('/playlist/${pl.id}')">
|
|
<div class="cover">
|
|
<img src="${pl.cover || ''}" alt="${pl.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-list\\'></i></div>'">
|
|
<div class="play-btn"><i class="fas fa-play"></i></div>
|
|
</div>
|
|
<div class="title">${pl.title}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 歌曲详情
|
|
async renderSongDetail(id) {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const song = await api.get(`/api/songs/${id}`);
|
|
|
|
let html = `
|
|
<div class="detail-header slide-up">
|
|
<div class="detail-cover">
|
|
<img src="${song.cover || ''}" alt="${song.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-music\\'></i></div>'">
|
|
</div>
|
|
<div class="detail-info">
|
|
<div class="type">单曲</div>
|
|
<h1>${song.title}</h1>
|
|
<div class="meta">
|
|
<span onclick="router.go('/artist/${song.artist_id}')" style="cursor:pointer">${song.artist_name || '未知歌手'}</span>
|
|
${song.album_title ? ` · <span onclick="router.go('/album/${song.album_id}')" style="cursor:pointer">${song.album_title}</span>` : ''}
|
|
· 播放 ${formatNumber(song.play_count)} 次
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn-primary" onclick="player.playSong(${JSON.stringify(song).replace(/"/g, '"')})">
|
|
<i class="fas fa-play"></i> 播放
|
|
</button>
|
|
<button class="btn-secondary" onclick="lyrics.toggle()">
|
|
<i class="fas fa-align-left"></i> 歌词
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
app.innerHTML = html;
|
|
|
|
// 设置歌词
|
|
if (song.lyrics) {
|
|
lyrics.parse(song.lyrics);
|
|
}
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 歌手详情
|
|
async renderArtistDetail(id) {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const artist = await api.get(`/api/artists/${id}`);
|
|
|
|
let html = `
|
|
<div class="detail-header slide-up">
|
|
<div class="detail-cover" style="border-radius: 50%;">
|
|
<img src="${artist.avatar || ''}" alt="${artist.name}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-user\\'></i></div>'">
|
|
</div>
|
|
<div class="detail-info">
|
|
<div class="type">歌手</div>
|
|
<h1>${artist.name}</h1>
|
|
<div class="meta">${artist.bio || ''}</div>
|
|
<div class="actions">
|
|
<button class="btn-primary" onclick="player.playAll(${JSON.stringify(artist.songs).replace(/"/g, '"')})">
|
|
<i class="fas fa-play"></i> 播放全部
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (artist.albums.length > 0) {
|
|
html += '<div class="section-title"><h3>专辑</h3></div>';
|
|
html += '<div class="album-grid">';
|
|
artist.albums.forEach(album => {
|
|
html += `
|
|
<div class="album-card" onclick="router.go('/album/${album.id}')">
|
|
<div class="cover">
|
|
<img src="${album.cover || ''}" alt="${album.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-compact-disc\\'></i></div>'">
|
|
</div>
|
|
<div class="title">${album.title}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (artist.songs.length > 0) {
|
|
html += '<div class="section-title"><h3>热门歌曲</h3></div>';
|
|
html += '<div class="song-list">';
|
|
artist.songs.forEach((song, i) => {
|
|
html += renderSongItem(song, i + 1);
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 专辑详情
|
|
async renderAlbumDetail(id) {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const album = await api.get(`/api/albums/${id}`);
|
|
|
|
let html = `
|
|
<div class="detail-header slide-up">
|
|
<div class="detail-cover">
|
|
<img src="${album.cover || ''}" alt="${album.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-compact-disc\\'></i></div>'">
|
|
</div>
|
|
<div class="detail-info">
|
|
<div class="type">专辑</div>
|
|
<h1>${album.title}</h1>
|
|
<div class="meta">
|
|
<span onclick="router.go('/artist/${album.artist_id}')" style="cursor:pointer">${album.artist_name || '未知歌手'}</span>
|
|
${album.release_date ? ` · ${album.release_date}` : ''}
|
|
</div>
|
|
<div class="meta">${album.description || ''}</div>
|
|
<div class="actions">
|
|
<button class="btn-primary" onclick="player.playAll(${JSON.stringify(album.songs).replace(/"/g, '"')})">
|
|
<i class="fas fa-play"></i> 播放全部
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (album.songs.length > 0) {
|
|
html += '<div class="section-title"><h3>歌曲列表</h3></div>';
|
|
html += '<div class="song-list">';
|
|
album.songs.forEach((song, i) => {
|
|
html += renderSongItem(song, i + 1);
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 歌单详情
|
|
async renderPlaylistDetail(id) {
|
|
const app = document.getElementById('app');
|
|
app.innerHTML = '<div class="loading">加载中</div>';
|
|
|
|
try {
|
|
const playlist = await api.get(`/api/playlists/${id}`);
|
|
|
|
let html = `
|
|
<div class="detail-header slide-up">
|
|
<div class="detail-cover">
|
|
<img src="${playlist.cover || ''}" alt="${playlist.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-list\\'></i></div>'">
|
|
</div>
|
|
<div class="detail-info">
|
|
<div class="type">歌单</div>
|
|
<h1>${playlist.title}</h1>
|
|
<div class="meta">${playlist.description || ''}</div>
|
|
<div class="meta">${playlist.songs.length} 首歌曲</div>
|
|
<div class="actions">
|
|
<button class="btn-primary" onclick="player.playAll(${JSON.stringify(playlist.songs).replace(/"/g, '"')})">
|
|
<i class="fas fa-play"></i> 播放全部
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (playlist.songs.length > 0) {
|
|
html += '<div class="section-title"><h3>歌曲列表</h3></div>';
|
|
html += '<div class="song-list">';
|
|
playlist.songs.forEach((song, i) => {
|
|
html += renderSongItem(song, i + 1);
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">加载失败</div>';
|
|
}
|
|
},
|
|
|
|
// 搜索结果
|
|
async renderSearch(q) {
|
|
const app = document.getElementById('app');
|
|
if (!q) {
|
|
app.innerHTML = '<div class="section-title"><h3>请输入搜索关键词</h3></div>';
|
|
return;
|
|
}
|
|
|
|
app.innerHTML = '<div class="loading">搜索中</div>';
|
|
|
|
try {
|
|
const data = await api.get(`/api/search?q=${encodeURIComponent(q)}`);
|
|
|
|
let html = `<div class="search-results">`;
|
|
html += `<div class="section-title"><h3>搜索: "${q}"</h3></div>`;
|
|
|
|
if (data.songs.length > 0) {
|
|
html += '<div class="section-title"><h3>歌曲</h3></div>';
|
|
html += '<div class="song-list">';
|
|
data.songs.forEach((song, i) => {
|
|
html += renderSongItem(song, i + 1);
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (data.artists.length > 0) {
|
|
html += '<div class="section-title"><h3>歌手</h3></div>';
|
|
html += '<div class="artist-grid">';
|
|
data.artists.forEach(artist => {
|
|
html += `
|
|
<div class="artist-card" onclick="router.go('/artist/${artist.id}')">
|
|
<div class="avatar">
|
|
<img src="${artist.avatar || ''}" alt="${artist.name}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-user\\'></i></div>'">
|
|
</div>
|
|
<div class="name">${artist.name}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (data.albums.length > 0) {
|
|
html += '<div class="section-title"><h3>专辑</h3></div>';
|
|
html += '<div class="album-grid">';
|
|
data.albums.forEach(album => {
|
|
html += `
|
|
<div class="album-card" onclick="router.go('/album/${album.id}')">
|
|
<div class="cover">
|
|
<img src="${album.cover || ''}" alt="${album.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-compact-disc\\'></i></div>'">
|
|
</div>
|
|
<div class="title">${album.title}</div>
|
|
<div class="artist">${album.artist_name || ''}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
if (data.songs.length === 0 && data.artists.length === 0 && data.albums.length === 0) {
|
|
html += '<div class="loading">未找到相关结果</div>';
|
|
}
|
|
|
|
html += '</div>';
|
|
app.innerHTML = html;
|
|
} catch (err) {
|
|
app.innerHTML = '<div class="loading">搜索失败</div>';
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============ API 工具 ============
|
|
const api = {
|
|
async get(url) {
|
|
const res = await fetch(url);
|
|
return res.json();
|
|
},
|
|
async post(url, data) {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return res.json();
|
|
}
|
|
};
|
|
|
|
// ============ 播放器 ============
|
|
const player = {
|
|
audio: null,
|
|
|
|
init() {
|
|
this.audio = document.getElementById('audio');
|
|
|
|
this.audio.addEventListener('timeupdate', () => this.onTimeUpdate());
|
|
this.audio.addEventListener('ended', () => this.next());
|
|
this.audio.addEventListener('loadedmetadata', () => {
|
|
document.getElementById('totalTime').textContent = formatTime(this.audio.duration);
|
|
});
|
|
},
|
|
|
|
playSong(song) {
|
|
state.currentSong = song;
|
|
|
|
// 更新UI
|
|
document.getElementById('playerTitle').textContent = song.title;
|
|
document.getElementById('playerArtist').textContent = song.artist_name || '未知歌手';
|
|
|
|
const coverEl = document.getElementById('playerCover');
|
|
if (song.cover) {
|
|
coverEl.innerHTML = `<img src="${song.cover}" alt="${song.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover><i class=\\'fas fa-music\\'></i></div>'">`;
|
|
} else {
|
|
coverEl.innerHTML = '<div class="placeholder-cover"><i class="fas fa-music"></i></div>';
|
|
}
|
|
|
|
// 设置音频源
|
|
this.audio.src = song.file_path;
|
|
this.audio.play();
|
|
state.isPlaying = true;
|
|
this.updatePlayBtn();
|
|
|
|
// 解析歌词
|
|
if (song.lyrics) {
|
|
lyrics.parse(song.lyrics);
|
|
} else {
|
|
lyrics.clear();
|
|
}
|
|
|
|
// 增加播放次数
|
|
api.post(`/api/songs/${song.id}/play`, {});
|
|
|
|
// 高亮当前播放
|
|
document.querySelectorAll('.song-item').forEach(el => el.classList.remove('playing'));
|
|
const currentEl = document.querySelector(`.song-item[data-id="${song.id}"]`);
|
|
if (currentEl) currentEl.classList.add('playing');
|
|
},
|
|
|
|
playAll(songs) {
|
|
if (songs.length === 0) return;
|
|
state.playlist = songs;
|
|
state.playlistIndex = 0;
|
|
this.playSong(songs[0]);
|
|
},
|
|
|
|
toggle() {
|
|
if (!state.currentSong) return;
|
|
|
|
if (state.isPlaying) {
|
|
this.audio.pause();
|
|
state.isPlaying = false;
|
|
} else {
|
|
this.audio.play();
|
|
state.isPlaying = true;
|
|
}
|
|
this.updatePlayBtn();
|
|
},
|
|
|
|
prev() {
|
|
if (state.playlist.length === 0) return;
|
|
state.playlistIndex = (state.playlistIndex - 1 + state.playlist.length) % state.playlist.length;
|
|
this.playSong(state.playlist[state.playlistIndex]);
|
|
},
|
|
|
|
next() {
|
|
if (state.playlist.length === 0) return;
|
|
state.playlistIndex = (state.playlistIndex + 1) % state.playlist.length;
|
|
this.playSong(state.playlist[state.playlistIndex]);
|
|
},
|
|
|
|
seek(e) {
|
|
if (!this.audio.duration) return;
|
|
const bar = e.currentTarget;
|
|
const rect = bar.getBoundingClientRect();
|
|
const percent = (e.clientX - rect.left) / rect.width;
|
|
this.audio.currentTime = percent * this.audio.duration;
|
|
},
|
|
|
|
setVolume(e) {
|
|
const bar = e.currentTarget;
|
|
const rect = bar.getBoundingClientRect();
|
|
const percent = (e.clientX - rect.left) / rect.width;
|
|
this.audio.volume = Math.max(0, Math.min(1, percent));
|
|
document.getElementById('volumeLevel').style.width = (percent * 100) + '%';
|
|
},
|
|
|
|
toggleMute() {
|
|
this.audio.muted = !this.audio.muted;
|
|
const btn = document.getElementById('volumeBtn');
|
|
btn.innerHTML = this.audio.muted ? '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>';
|
|
},
|
|
|
|
updatePlayBtn() {
|
|
const btn = document.getElementById('playBtn');
|
|
btn.innerHTML = state.isPlaying ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>';
|
|
},
|
|
|
|
onTimeUpdate() {
|
|
const current = this.audio.currentTime;
|
|
const duration = this.audio.duration || 0;
|
|
|
|
// 更新进度条
|
|
const percent = duration > 0 ? (current / duration) * 100 : 0;
|
|
document.getElementById('progress').style.width = percent + '%';
|
|
document.getElementById('currentTime').textContent = formatTime(current);
|
|
|
|
// 更新歌词高亮
|
|
lyrics.update(current);
|
|
}
|
|
};
|
|
|
|
// ============ 歌词模块 ============
|
|
const lyrics = {
|
|
toggle() {
|
|
const panel = document.getElementById('lyricsPanel');
|
|
panel.classList.toggle('open');
|
|
},
|
|
|
|
parse(lyricsText) {
|
|
const lines = lyricsText.split('\n');
|
|
state.lyricsData = [];
|
|
|
|
lines.forEach(line => {
|
|
const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/);
|
|
if (match) {
|
|
const min = parseInt(match[1]);
|
|
const sec = parseInt(match[2]);
|
|
const ms = parseInt(match[3].padEnd(3, '0'));
|
|
const time = min * 60 + sec + ms / 1000;
|
|
const text = match[4].trim();
|
|
if (text) {
|
|
state.lyricsData.push({ time, text });
|
|
}
|
|
}
|
|
});
|
|
|
|
this.render();
|
|
},
|
|
|
|
render() {
|
|
const content = document.getElementById('lyricsContent');
|
|
|
|
if (state.lyricsData.length === 0) {
|
|
content.innerHTML = '<div class="lyrics-empty">暂无歌词</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div style="padding-top: 40%"></div>';
|
|
state.lyricsData.forEach((line, i) => {
|
|
html += `<div class="lyrics-line" data-index="${i}" onclick="player.audio.currentTime=${line.time}">${line.text}</div>`;
|
|
});
|
|
html += '<div style="padding-bottom: 40%"></div>';
|
|
|
|
content.innerHTML = html;
|
|
state.currentLyricIndex = -1;
|
|
},
|
|
|
|
update(currentTime) {
|
|
if (state.lyricsData.length === 0) return;
|
|
|
|
let newIndex = -1;
|
|
for (let i = state.lyricsData.length - 1; i >= 0; i--) {
|
|
if (currentTime >= state.lyricsData[i].time) {
|
|
newIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newIndex !== state.currentLyricIndex) {
|
|
state.currentLyricIndex = newIndex;
|
|
|
|
// 更新高亮
|
|
document.querySelectorAll('.lyrics-line').forEach(el => el.classList.remove('active'));
|
|
if (newIndex >= 0) {
|
|
const activeLine = document.querySelector(`.lyrics-line[data-index="${newIndex}"]`);
|
|
if (activeLine) {
|
|
activeLine.classList.add('active');
|
|
// 滚动到可视区域
|
|
activeLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
clear() {
|
|
state.lyricsData = [];
|
|
state.currentLyricIndex = -1;
|
|
document.getElementById('lyricsContent').innerHTML = '<div class="lyrics-empty">暂无歌词</div>';
|
|
}
|
|
};
|
|
|
|
// ============ 轮播图 ============
|
|
const banner = {
|
|
timer: null,
|
|
currentIndex: 0,
|
|
total: 0,
|
|
|
|
init(total) {
|
|
this.total = total;
|
|
this.currentIndex = 0;
|
|
this.startAutoPlay();
|
|
},
|
|
|
|
goTo(index) {
|
|
this.currentIndex = index;
|
|
const slides = document.querySelectorAll('.banner-slide');
|
|
const dots = document.querySelectorAll('.banner-dots span');
|
|
|
|
slides.forEach((s, i) => s.classList.toggle('active', i === index));
|
|
dots.forEach((d, i) => d.classList.toggle('active', i === index));
|
|
},
|
|
|
|
next() {
|
|
this.goTo((this.currentIndex + 1) % this.total);
|
|
},
|
|
|
|
startAutoPlay() {
|
|
if (this.timer) clearInterval(this.timer);
|
|
this.timer = setInterval(() => this.next(), 5000);
|
|
}
|
|
};
|
|
|
|
// ============ 辅助函数 ============
|
|
function renderSongItem(song, index) {
|
|
return `
|
|
<div class="song-item" data-id="${song.id}" onclick='player.playSong(${JSON.stringify(song).replace(/'/g, "\\'")})'>
|
|
<div class="index">${index}</div>
|
|
<div class="cover">
|
|
<img src="${song.cover || ''}" alt="${song.title}" onerror="this.parentElement.innerHTML='<div class=placeholder-cover style=\\'width:46px;height:46px;font-size:16px\\'><i class=\\'fas fa-music\\'></i></div>'">
|
|
</div>
|
|
<div class="info">
|
|
<div class="name">${song.title}</div>
|
|
<div class="artist">${song.artist_name || '未知歌手'}</div>
|
|
</div>
|
|
<div class="duration">${formatTime(song.duration)}</div>
|
|
<div class="actions">
|
|
<i class="far fa-heart"></i>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
if (!seconds || isNaN(seconds)) return '0:00';
|
|
const min = Math.floor(seconds / 60);
|
|
const sec = Math.floor(seconds % 60);
|
|
return `${min}:${sec.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function formatNumber(num) {
|
|
if (!num) return '0';
|
|
if (num >= 10000) return (num / 10000).toFixed(1) + '万';
|
|
return num.toString();
|
|
}
|
|
|
|
function handleSearch(e) {
|
|
if (e.key === 'Enter') {
|
|
const q = e.target.value.trim();
|
|
if (q) {
|
|
router.go(`/search?q=${encodeURIComponent(q)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============ 初始化 ============
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
player.init();
|
|
router.handleRoute();
|
|
|
|
// 处理浏览器后退
|
|
window.addEventListener('popstate', () => {
|
|
router.handleRoute();
|
|
});
|
|
});
|