diff --git a/src/app/music/MusicClient.tsx b/src/app/music/MusicClient.tsx
index b582412..ae51846 100644
--- a/src/app/music/MusicClient.tsx
+++ b/src/app/music/MusicClient.tsx
@@ -1,12 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
-import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import AddToPlaylistModal from '@/components/AddToPlaylistModal';
import Toast, { ToastProps } from '@/components/Toast';
import LyricsPiPWindow from '@/components/LyricsPiPWindow';
+import MusicSidebarDrawer from '@/components/music/MusicSidebarDrawer';
+import { getSourceDisplayLabel, normalizeSource, SourcePill } from '@/lib/music/shared';
+import type { MusicQuality, MusicSource, Song } from '@/lib/music/types';
const SPECTRUM_BIN_COUNT = 96;
const SPECTRUM_IDLE_LEVEL = 0.02;
@@ -15,30 +18,6 @@ const SPECTRUM_REFERENCE_VOLUME = 10;
const SPECTRUM_MIN_VOLUME = 5;
const SPECTRUM_MAX_REFERENCE_VOLUME = 15;
-function getApiErrorMessage(error: unknown, fallback: string): string {
- if (typeof error === 'string') return error;
- if (error && typeof error === 'object' && 'message' in error) {
- const message = (error as { message?: unknown }).message;
- if (typeof message === 'string' && message.trim()) return message;
- }
- return fallback;
-}
-
-export type MusicSource = 'wy' | 'tx' | 'kw' | 'kg' | 'mg';
-export type MusicQuality = '128k' | '320k' | 'flac' | 'flac24bit';
-
-export interface Song {
- id: string;
- name: string;
- artist: string;
- album?: string;
- pic?: string;
- platform: MusicSource;
- duration?: number;
- durationText?: string;
- songmid?: string;
-}
-
interface PlayRecord {
platform: MusicSource;
id: string;
@@ -53,14 +32,6 @@ interface LyricLine {
translation?: string;
}
-export interface Playlist {
- id: string;
- name: string;
- pic?: string;
- source?: MusicSource;
- updateFrequency?: string;
-}
-
interface DbRecord {
source: MusicSource;
songId: string;
@@ -77,38 +48,6 @@ interface DbRecord {
songmid?: string;
}
-export function MusicLoadingIndicator({
- text,
- size = 'md',
- className = '',
-}: {
- text?: string;
- size?: 'sm' | 'md';
- className?: string;
-}) {
- const iconSize = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5';
- const textSize = size === 'sm' ? 'text-xs' : 'text-sm';
-
- return (
-
-
- {[0, 1, 2].map((index) => (
-
- ))}
-
- {text ?
{text} : null}
-
- );
-}
-
function AudioSpectrumCanvas({
bars,
compact = false,
@@ -274,17 +213,7 @@ declare global {
export default function MusicClient({ children: _children }: { children?: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
- const searchParams = useSearchParams();
const [currentSource, setCurrentSource] = useState('wy');
- const [playlists, setPlaylists] = useState([]);
- const [songs, setSongs] = useState([]);
- const [currentView, setCurrentView] = useState<'playlists' | 'songs' | 'myPlaylists' | 'search'>('playlists');
- const [currentPlaylistTitle, setCurrentPlaylistTitle] = useState('');
- const [searchKeyword, setSearchKeyword] = useState('');
- const [activeSearchKeyword, setActiveSearchKeyword] = useState('');
- const [searchPage, setSearchPage] = useState(1);
- const [searchHasMore, setSearchHasMore] = useState(false);
- const [loadingMoreSearch, setLoadingMoreSearch] = useState(false);
const [currentSong, setCurrentSong] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [isBuffering, setIsBuffering] = useState(false);
@@ -295,7 +224,6 @@ export default function MusicClient({ children: _children }: { children?: React.
const [playMode, setPlayMode] = useState<'loop' | 'single' | 'random'>('loop');
const [currentSongIndex, setCurrentSongIndex] = useState(-1);
const [showPlayer, setShowPlayer] = useState(false);
- const [loading, setLoading] = useState(false);
const [showLyrics, setShowLyrics] = useState(false);
const [mobileLyricsView, setMobileLyricsView] = useState<'lyrics' | 'vinyl'>('lyrics');
const [musicProxyEnabled, setMusicProxyEnabled] = useState(() => {
@@ -310,7 +238,6 @@ export default function MusicClient({ children: _children }: { children?: React.
const [showPlaylist, setShowPlaylist] = useState(false);
const [playlistIndex, setPlaylistIndex] = useState(-1); // 当前在播放列表中的索引
const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单
- const [showSourceMenu, setShowSourceMenu] = useState(false); // 移动端音源菜单
const [showSidebarDrawer, setShowSidebarDrawer] = useState(false); // 左侧抽屉菜单
const [showVolumeSlider, setShowVolumeSlider] = useState(false); // 音量滑块显示状态
const [pendingSongToPlay, setPendingSongToPlay] = useState<{ platform: string; id: string } | null>(null); // 待播放的歌曲信息
@@ -318,16 +245,6 @@ export default function MusicClient({ children: _children }: { children?: React.
const [showAddToPlaylistModal, setShowAddToPlaylistModal] = useState(false); // 添加到歌单弹窗
const [songToAddToPlaylist, setSongToAddToPlaylist] = useState(null); // 要添加到歌单的歌曲
- // 我的歌单相关状态
- const [userPlaylists, setUserPlaylists] = useState([]);
- const [selectedUserPlaylist, setSelectedUserPlaylist] = useState(null);
- const [userPlaylistSongs, setUserPlaylistSongs] = useState([]);
- const [loadingUserPlaylists, setLoadingUserPlaylists] = useState(false);
- const [loadingUserPlaylistSongs, setLoadingUserPlaylistSongs] = useState(false);
- const [loadingPlayAll, setLoadingPlayAll] = useState(false); // 播放全部加载状态
- const [loadingCurrentPlayAll, setLoadingCurrentPlayAll] = useState(false); // 当前排行榜/详情页播放全部加载状态
- const [deletingPlaylistId, setDeletingPlaylistId] = useState(null); // 正在删除的歌单ID
-
useEffect(() => {
if (typeof window !== 'undefined' && !(window as any).RUNTIME_CONFIG?.MUSIC_ENABLED) {
router.replace('/');
@@ -364,7 +281,6 @@ export default function MusicClient({ children: _children }: { children?: React.
const audioRef = useRef(null);
const lyricsContainerRef = useRef(null);
- const searchLoadMoreRef = useRef(null);
const lastSaveTimeRef = useRef(0);
const restoredTimeRef = useRef(0);
const songStartTimeRef = useRef(0); // 歌曲开始播放的时间戳
@@ -380,66 +296,6 @@ export default function MusicClient({ children: _children }: { children?: React.
const currentSongRef = useRef(null);
const currentSourceRef = useRef(currentSource);
- const mapSong = (song: any): Song => ({
- id: song.songId || song.id,
- name: song.name,
- artist: song.artist,
- album: song.album,
- pic: song.cover || song.pic,
- platform: normalizeSource(song.source || song.platform),
- duration: song.durationSec || song.duration,
- durationText: song.durationText || song.interval,
- songmid: song.songmid,
- });
-
- const normalizeSource = (source: string | undefined): MusicSource => {
- switch (source) {
- case 'netease': return 'wy';
- case 'qq': return 'tx';
- case 'kuwo': return 'kw';
- case 'wy':
- case 'tx':
- case 'kw':
- case 'kg':
- case 'mg':
- return source;
- default:
- return 'wy';
- }
- };
-
- const musicSources: Array<{ key: MusicSource; label: string }> = [
- { key: 'wy', label: '网易云' },
- { key: 'tx', label: 'QQ' },
- { key: 'kw', label: '酷我' },
- { key: 'kg', label: '酷狗' },
- { key: 'mg', label: '咪咕' },
- ];
-
- const getSourceDisplayLabel = (source?: MusicSource | string, compact = true) => {
- switch (normalizeSource(source)) {
- case 'wy': return compact ? '网易' : '网易云';
- case 'tx': return compact ? 'QQ' : 'QQ音乐';
- case 'kw': return '酷我';
- case 'kg': return '酷狗';
- case 'mg': return '咪咕';
- }
- };
-
- const SourcePill = ({
- source,
- className = '',
- }: {
- source?: MusicSource | string;
- className?: string;
- }) => (
-
- {getSourceDisplayLabel(source)}
-
- );
-
const buildStreamUrl = (song: Song, source: MusicSource, songQuality: MusicQuality) => {
const params = new URLSearchParams({
songId: song.id,
@@ -548,10 +404,7 @@ export default function MusicClient({ children: _children }: { children?: React.
const playState = {
currentSong,
currentSongIndex,
- songs,
- currentPlaylistTitle,
currentSource,
- currentView,
quality,
playMode,
volume,
@@ -645,10 +498,7 @@ export default function MusicClient({ children: _children }: { children?: React.
const playState = savedPlayState ? JSON.parse(savedPlayState) : {};
// 恢复配置状态(不包括歌曲)
- setSongs(playState.songs || []);
- setCurrentPlaylistTitle(playState.currentPlaylistTitle || '');
setCurrentSource(normalizeSource(playState.currentSource));
- setCurrentView(playState.currentView || 'playlists');
setQuality(playState.quality || '320k');
setPlayMode(playState.playMode || 'loop');
setVolume(playState.volume || 100);
@@ -763,7 +613,7 @@ export default function MusicClient({ children: _children }: { children?: React.
if (currentSong) {
savePlayState();
}
- }, [currentSong, currentSongIndex, songs, currentPlaylistTitle, currentSource, currentView, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]);
+ }, [currentSong, currentSongIndex, currentSource, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]);
useEffect(() => {
currentSongRef.current = currentSong;
@@ -792,518 +642,7 @@ export default function MusicClient({ children: _children }: { children?: React.
}
}, [volume]);
- // 加载排行榜列表
- const loadPlaylists = async (source: string) => {
- setLoading(true);
- try {
- const boardsResponse = await fetch(`/api/music/v2/discovery/boards?source=${source}`);
- const boardsData = await boardsResponse.json();
-
- if (boardsResponse.ok && boardsData.success) {
- setPlaylists((boardsData.data?.list || []).map((item: any) => ({
- id: item.id,
- name: item.name,
- source: normalizeSource(item.source || boardsData.data?.source || source),
- updateFrequency: item.updateFrequency || item.description || '',
- })));
- } else {
- console.error('加载排行榜失败:', boardsData);
- setPlaylists([]);
- }
- } catch (error) {
- console.error('加载排行榜失败:', error);
- setPlaylists([]);
- } finally {
- setLoading(false);
- }
- };
-
- // 加载歌单详情
- const loadPlaylist = async (playlistId: string, playlistName: string, playlistSource?: MusicSource, navigate = true) => {
- const source = playlistSource || currentSource;
- if (navigate) {
- router.push(`/music/rankings/${source}/${encodeURIComponent(playlistId)}?name=${encodeURIComponent(playlistName)}`);
- return;
- }
-
- setLoading(true);
- try {
- const response = await fetch(
- `/api/music/v2/discovery/board-songs?source=${source}&boardId=${encodeURIComponent(playlistId)}`
- );
- const data = await response.json();
- setSongs((data.data?.list || []).map(mapSong));
- setCurrentPlaylistTitle(playlistName);
- setActiveSearchKeyword('');
- setSearchPage(1);
- setSearchHasMore(false);
- setCurrentView('songs');
- } catch (error) {
- console.error('加载歌单失败:', error);
- setSongs([]);
- } finally {
- setLoading(false);
- }
- };
-
- // 当前排行榜歌单:播放全部
- const handlePlayAllCurrentSongs = async () => {
- setLoadingCurrentPlayAll(true);
-
- try {
- if (songs.length === 0) {
- setToast({
- message: '当前歌单为空',
- type: 'error',
- onClose: () => setToast(null),
- });
- return;
- }
-
- await fetch('/api/music/v2/history', { method: 'DELETE' });
-
- const baseTime = Date.now();
- const recordsToAdd = songs.map((song, i) => ({
- song: {
- songId: song.id,
- source: song.platform,
- songmid: song.songmid,
- name: song.name,
- artist: song.artist,
- album: song.album,
- cover: song.pic,
- durationSec: song.duration || 0,
- durationText: song.durationText,
- },
- playProgressSec: 0,
- lastPlayedAt: baseTime + i,
- playCount: 1,
- lastQuality: quality,
- createdAt: baseTime + i,
- }));
-
- await fetch('/api/music/v2/history', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ records: recordsToAdd }),
- });
-
- const newRecords: PlayRecord[] = songs.map((song, i) => ({
- platform: song.platform,
- id: song.id,
- playTime: 0,
- duration: song.duration || 0,
- timestamp: baseTime + i,
- }));
-
- setPlayRecords(newRecords);
- setPlaylist(songs);
- setPlaylistIndex(0);
- await playSong(songs[0], 0);
-
- setToast({
- message: `已开始播放 ${currentPlaylistTitle || '当前歌单'}`,
- type: 'success',
- onClose: () => setToast(null),
- });
- } catch (error) {
- console.error('排行榜播放全部失败:', error);
- setToast({
- message: '播放全部失败',
- type: 'error',
- onClose: () => setToast(null),
- });
- } finally {
- setLoadingCurrentPlayAll(false);
- }
- };
-
- // 搜索歌曲
- const searchSongs = async (keywordArg?: string, navigate = true) => {
- const keyword = (keywordArg ?? searchKeyword).trim();
- if (!keyword) return;
-
- if (navigate) {
- router.push(`/music/search?source=${currentSource}&q=${encodeURIComponent(keyword)}`);
- return;
- }
-
- setLoading(true);
- try {
- const response = await fetch(
- `/api/music/v2/search?source=${currentSource}&q=${encodeURIComponent(keyword)}&page=1&limit=20`
- );
- const data = await response.json();
- setSongs((data.data?.list || []).map(mapSong));
- setActiveSearchKeyword(keyword);
- setSearchKeyword(keyword);
- setSearchPage(1);
- setSearchHasMore(Boolean(data.data?.hasMore));
- setCurrentPlaylistTitle(`搜索: ${keyword}`);
- setCurrentView('search');
- } catch (error) {
- console.error('搜索失败:', error);
- setSongs([]);
- setActiveSearchKeyword('');
- setSearchPage(1);
- setSearchHasMore(false);
- } finally {
- setLoading(false);
- }
- };
-
- const loadMoreSearchSongs = async () => {
- const keyword = activeSearchKeyword.trim();
- if (!keyword || loadingMoreSearch || !searchHasMore) return;
-
- const nextPage = searchPage + 1;
- setLoadingMoreSearch(true);
- try {
- const response = await fetch(
- `/api/music/v2/search?source=${currentSource}&q=${encodeURIComponent(keyword)}&page=${nextPage}&limit=20`
- );
- const data = await response.json();
-
- if (response.ok && data.success) {
- const nextSongs = (data.data?.list || []).map(mapSong);
- setSongs((prev) => [...prev, ...nextSongs]);
- setSearchPage(nextPage);
- setSearchHasMore(Boolean(data.data?.hasMore));
- } else {
- setToast({
- message: data.error?.message || '加载更多失败',
- type: 'error',
- onClose: () => setToast(null),
- });
- }
- } catch (error) {
- console.error('加载更多搜索结果失败:', error);
- setToast({
- message: '加载更多失败',
- type: 'error',
- onClose: () => setToast(null),
- });
- } finally {
- setLoadingMoreSearch(false);
- }
- };
-
- // 打开添加到歌单弹窗
- const handleAddToPlaylist = (song: Song, e: React.MouseEvent) => {
- e.stopPropagation(); // 阻止事件冒泡,避免触发播放
- setSongToAddToPlaylist(song);
- setShowAddToPlaylistModal(true);
- };
-
- // 稍后播放:追加到当前播放列表末尾,不立即播放
- const handlePlayLater = (song: Song, e: React.MouseEvent) => {
- e.stopPropagation();
-
- if (!currentSong && playlist.length === 0 && playRecords.length === 0) {
- playSong(song, -1);
- return;
- }
-
- const platform = song.platform || currentSource;
- const exists = playlist.some((item) => item.id === song.id && item.platform === platform);
-
- if (exists) {
- setToast({
- message: '歌曲已在播放列表中',
- type: 'info',
- onClose: () => setToast(null),
- });
- return;
- }
-
- const record: PlayRecord = {
- platform,
- id: song.id,
- playTime: 0,
- duration: song.duration || 0,
- timestamp: Date.now(),
- };
-
- setPlayRecords((prev) => [...prev, record]);
- setPlaylist((prev) => [...prev, { ...song, platform }]);
- saveHistoryRecordSafely(record, { ...song, platform }, 0, song.duration || 0, 0);
- setToast({
- message: '已加入稍后播放',
- type: 'success',
- onClose: () => setToast(null),
- });
- };
-
- // 加载用户歌单列表
- const loadUserPlaylists = async () => {
- try {
- setLoadingUserPlaylists(true);
- const response = await fetch('/api/music/v2/playlists');
- if (response.ok) {
- const data = await response.json();
- setUserPlaylists(data.data?.playlists || []);
- }
- } catch (error) {
- console.error('加载歌单失败:', error);
- } finally {
- setLoadingUserPlaylists(false);
- }
- };
-
- // 加载歌单中的歌曲
- const loadUserPlaylistSongs = async (playlistId: string) => {
- try {
- setLoadingUserPlaylistSongs(true);
- const response = await fetch(`/api/music/v2/playlists/${playlistId}/songs`);
- if (response.ok) {
- const data = await response.json();
- setUserPlaylistSongs((data.data?.songs || []).map((song: any) => ({
- ...song,
- id: song.songId,
- platform: song.source,
- pic: song.cover,
- duration: song.durationSec,
- })));
- }
- } catch (error) {
- console.error('加载歌单歌曲失败:', error);
- } finally {
- setLoadingUserPlaylistSongs(false);
- }
- };
-
- // 选择歌单
- const handleSelectUserPlaylist = (playlist: any) => {
- setSelectedUserPlaylist(playlist);
- loadUserPlaylistSongs(playlist.id);
- };
-
- // 播放全部歌单歌曲
- const handlePlayAllPlaylist = async () => {
- if (!selectedUserPlaylist || userPlaylistSongs.length === 0) {
- setToast({
- message: '歌单为空',
- type: 'error',
- onClose: () => setToast(null),
- });
- return;
- }
-
- setLoadingPlayAll(true);
- try {
- // 1. 清空所有播放历史
- await fetch('/api/music/v2/history', { method: 'DELETE' });
-
- // 2. 清空本地状态
- setPlayRecords([]);
- setPlaylist([]);
-
- // 3. 批量添加歌单中的所有歌曲到播放历史
- const baseTime = Date.now();
- const recordsToAdd = userPlaylistSongs.map((song, i) => ({
- song: {
- songId: song.id,
- source: song.platform,
- songmid: song.songmid,
- name: song.name,
- artist: song.artist,
- album: song.album,
- cover: song.pic,
- durationSec: song.duration || 0,
- durationText: song.durationText,
- },
- playProgressSec: 0,
- lastPlayedAt: baseTime + i,
- playCount: 1,
- lastQuality: quality,
- createdAt: baseTime + i,
- }));
-
- // 一次性批量添加所有歌曲
- const response = await fetch('/api/music/v2/history', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- records: recordsToAdd,
- }),
- });
-
- if (!response.ok) {
- throw new Error('批量添加歌曲失败');
- }
-
- // 4. 立即更新本地状态
- const newRecords: PlayRecord[] = userPlaylistSongs.map((song, i) => ({
- platform: song.platform,
- id: song.id,
- playTime: 0,
- duration: song.duration || 0,
- timestamp: baseTime + i,
- }));
-
- const newPlaylist: Song[] = userPlaylistSongs.map((song) => ({
- id: song.id,
- name: song.name,
- artist: song.artist,
- album: song.album,
- pic: song.pic,
- platform: song.platform,
- duration: song.duration,
- durationText: song.durationText,
- songmid: song.songmid,
- }));
-
- setPlayRecords(newRecords);
- setPlaylist(newPlaylist);
-
- // 5. 直接播放第一首歌
- if (userPlaylistSongs.length > 0) {
- setPlaylistIndex(0);
- await playSong(userPlaylistSongs[0], 0);
- }
-
- setToast({
- message: `已将 ${userPlaylistSongs.length} 首歌曲添加到播放列表`,
- type: 'success',
- onClose: () => setToast(null),
- });
- } catch (error) {
- console.error('播放全部失败:', error);
- setToast({
- message: '播放全部失败',
- type: 'error',
- onClose: () => setToast(null),
- });
- } finally {
- setLoadingPlayAll(false);
- }
- };
-
- // 删除歌单
- const handleDeleteUserPlaylist = async (playlistId: string) => {
- setConfirmModal({
- isOpen: true,
- title: '确认删除',
- message: '确定要删除这个歌单吗?',
- onConfirm: async () => {
- // 先关闭确认框
- setConfirmModal({
- isOpen: false,
- title: '',
- message: '',
- onConfirm: () => {},
- onCancel: () => {},
- });
-
- // 然后执行删除
- setDeletingPlaylistId(playlistId);
- try {
- const response = await fetch(`/api/music/v2/playlists/${playlistId}`, { method: 'DELETE' });
-
- if (response.ok) {
- setToast({
- message: '删除成功',
- type: 'success',
- onClose: () => setToast(null),
- });
- if (selectedUserPlaylist?.id === playlistId) {
- setSelectedUserPlaylist(null);
- setUserPlaylistSongs([]);
- }
- loadUserPlaylists();
- } else {
- const data = await response.json();
- setToast({
- message: getApiErrorMessage(data.error, '删除失败'),
- type: 'error',
- onClose: () => setToast(null),
- });
- }
- } catch (error) {
- console.error('删除歌单失败:', error);
- setToast({
- message: '删除歌单失败',
- type: 'error',
- onClose: () => setToast(null),
- });
- } finally {
- setDeletingPlaylistId(null);
- }
- },
- onCancel: () => {
- setConfirmModal({
- isOpen: false,
- title: '',
- message: '',
- onConfirm: () => {},
- onCancel: () => {},
- });
- },
- });
- };
-
- // 从歌单中移除歌曲
- const handleRemoveSongFromUserPlaylist = async (song: any) => {
- if (!selectedUserPlaylist) return;
-
- setConfirmModal({
- isOpen: true,
- title: '确认移除',
- message: `确定要从歌单中移除 "${song.name}" 吗?`,
- onConfirm: async () => {
- try {
- const response = await fetch(
- `/api/music/v2/playlists/${selectedUserPlaylist.id}/songs?songId=${encodeURIComponent(song.id)}`,
- { method: 'DELETE' }
- );
-
- if (response.ok) {
- setToast({
- message: '移除成功',
- type: 'success',
- onClose: () => setToast(null),
- });
- loadUserPlaylistSongs(selectedUserPlaylist.id);
- } else {
- const data = await response.json();
- setToast({
- message: getApiErrorMessage(data.error, '移除失败'),
- type: 'error',
- onClose: () => setToast(null),
- });
- }
- } catch (error) {
- console.error('移除歌曲失败:', error);
- setToast({
- message: '移除歌曲失败',
- type: 'error',
- onClose: () => setToast(null),
- });
- }
- setConfirmModal({
- isOpen: false,
- title: '',
- message: '',
- onConfirm: () => {},
- onCancel: () => {},
- });
- },
- onCancel: () => {
- setConfirmModal({
- isOpen: false,
- title: '',
- message: '',
- onConfirm: () => {},
- onCancel: () => {},
- });
- },
- });
- };
-
-
const handlePlayAllCurrentSongsWith = async (targetSongs: Song[], title: string) => {
- setLoadingCurrentPlayAll(true);
-
try {
if (targetSongs.length === 0) {
setToast({ message: '当前列表为空', type: 'error', onClose: () => setToast(null) });
@@ -1350,8 +689,6 @@ export default function MusicClient({ children: _children }: { children?: React.
} catch (error) {
console.error('播放全部失败:', error);
setToast({ message: '播放全部失败', type: 'error', onClose: () => setToast(null) });
- } finally {
- setLoadingCurrentPlayAll(false);
}
};
@@ -1588,27 +925,19 @@ export default function MusicClient({ children: _children }: { children?: React.
// 上一曲
const playPrev = () => {
- // 优先从播放列表切换
if (playlist.length > 0) {
- // 如果已经是第一首,循环到最后一首
const prevIndex = playlistIndex > 0 ? playlistIndex - 1 : playlist.length - 1;
setPlaylistIndex(prevIndex);
playSong(playlist[prevIndex], -1);
- } else if (currentSongIndex > 0) {
- playSong(songs[currentSongIndex - 1], currentSongIndex - 1);
}
};
// 下一曲
const playNext = () => {
- // 优先从播放列表切换
if (playlist.length > 0) {
- // 如果已经是最后一首,循环到第一首
const nextIndex = playlistIndex < playlist.length - 1 ? playlistIndex + 1 : 0;
setPlaylistIndex(nextIndex);
playSong(playlist[nextIndex], -1);
- } else if (currentSongIndex < songs.length - 1) {
- playSong(songs[currentSongIndex + 1], currentSongIndex + 1);
}
};
@@ -1821,15 +1150,6 @@ export default function MusicClient({ children: _children }: { children?: React.
setPlayMode(modes[nextIndex]);
};
- // 返回
- const goBack = () => {
- if (currentView === 'songs' || currentView === 'search' || currentView === 'myPlaylists') {
- router.push('/music/rankings');
- } else {
- router.back();
- }
- };
-
// 下载歌曲
const downloadSong = () => {
if (!currentSongUrl || !currentSong) return;
@@ -1843,64 +1163,6 @@ export default function MusicClient({ children: _children }: { children?: React.
document.body.removeChild(link);
};
- // 切换平台
- const switchSource = (source: MusicSource) => {
- setCurrentSource(source);
- setSongs([]);
- setSearchKeyword('');
- setActiveSearchKeyword('');
- setSearchPage(1);
- setSearchHasMore(false);
- if (pathname?.startsWith('/music/search')) {
- router.push(`/music/search?source=${source}`);
- } else {
- router.push('/music/rankings');
- }
- };
-
-
- // 路由同步:/music 下多页面切换时保留本组件和播放器状态
- useEffect(() => {
- if (!pathname) return;
-
- if (pathname === '/music' || pathname === '/music/rankings') {
- setCurrentView('playlists');
- setSongs([]);
- setCurrentPlaylistTitle('');
- setActiveSearchKeyword('');
- setSearchPage(1);
- setSearchHasMore(false);
- return;
- }
-
- const rankingMatch = pathname.match(/^\/music\/rankings\/([^/]+)\/([^/]+)$/);
- if (rankingMatch) {
- const source = normalizeSource(decodeURIComponent(rankingMatch[1]));
- const playlistId = decodeURIComponent(rankingMatch[2]);
- const playlistName = searchParams.get('name') || currentPlaylistTitle || '排行榜';
- setCurrentSource(source);
- void loadPlaylist(playlistId, playlistName, source, false);
- return;
- }
-
- if (pathname === '/music/search') {
- const keyword = searchParams.get('q') || '';
- setCurrentView('search');
- setCurrentPlaylistTitle(keyword ? `搜索: ${keyword}` : '搜索');
- setSearchKeyword(keyword);
- setSongs([]);
- setActiveSearchKeyword('');
- setSearchPage(1);
- setSearchHasMore(false);
- return;
- }
-
- if (pathname === '/music/my-playlists') {
- setCurrentView('myPlaylists');
- return;
- }
- }, [pathname, searchParams]);
-
// 音频事件监听
useEffect(() => {
const audio = audioRef.current;
@@ -2008,14 +1270,10 @@ export default function MusicClient({ children: _children }: { children?: React.
audio.currentTime = 0;
audio.play();
} else if (playMode === 'random') {
- // 优先从播放列表中随机选择
if (playlist.length > 0) {
const randomIndex = Math.floor(Math.random() * playlist.length);
setPlaylistIndex(randomIndex);
playSong(playlist[randomIndex], -1);
- } else if (songs.length > 0) {
- const randomIndex = Math.floor(Math.random() * songs.length);
- playSong(songs[randomIndex], randomIndex);
}
} else {
playNext();
@@ -2049,19 +1307,7 @@ export default function MusicClient({ children: _children }: { children?: React.
audio.removeEventListener('durationchange', handleDurationChange);
audio.removeEventListener('ended', handleEnded);
};
- }, [playMode, songs, currentSongIndex, lyrics, currentSong, playlistIndex, playRecords, quality]);
-
- // 初始加载
- useEffect(() => {
- loadPlaylists(currentSource);
- }, [currentSource]);
-
- // 当切换到我的歌单视图时加载歌单列表
- useEffect(() => {
- if (currentView === 'myPlaylists') {
- loadUserPlaylists();
- }
- }, [currentView]);
+ }, [playMode, currentSongIndex, lyrics, currentSong, playlistIndex, playRecords, quality]);
// 歌词自动滚动
useEffect(() => {
@@ -2077,34 +1323,6 @@ export default function MusicClient({ children: _children }: { children?: React.
}
}, [currentLyricIndex]);
- // 搜索框回车
- const handleSearchKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- searchSongs();
- }
- };
-
- useEffect(() => {
- const sentinel = searchLoadMoreRef.current;
- if (!sentinel || !activeSearchKeyword || !searchHasMore) return;
-
- const observer = new IntersectionObserver(
- (entries) => {
- if (entries[0]?.isIntersecting) {
- loadMoreSearchSongs();
- }
- },
- {
- root: null,
- rootMargin: '240px 0px 240px 0px',
- threshold: 0,
- }
- );
-
- observer.observe(sentinel);
- return () => observer.disconnect();
- }, [activeSearchKeyword, searchHasMore, loadingMoreSearch, searchPage]);
-
// 进度条拖动
const handleProgressChange = (e: React.ChangeEvent) => {
const newTime = (parseFloat(e.target.value) / 100) * duration;
@@ -2229,13 +1447,7 @@ export default function MusicClient({ children: _children }: { children?: React.
};
const getSourceLabel = () => {
- switch (currentSource) {
- case 'wy': return '网易云';
- case 'tx': return 'QQ音乐';
- case 'kw': return '酷我';
- case 'kg': return '酷狗';
- case 'mg': return '咪咕';
- }
+ return getSourceDisplayLabel(currentSource, false);
};
const formatTime = (seconds: number) => {
@@ -2377,9 +1589,6 @@ export default function MusicClient({ children: _children }: { children?: React.
const handlePlayAllEvent = (event: Event) => {
const detail = (event as CustomEvent<{ songs: Song[]; title?: string }>).detail;
if (!detail?.songs?.length) return;
- setSongs(detail.songs);
- setCurrentPlaylistTitle(detail.title || '当前列表');
- setActiveSearchKeyword('');
void handlePlayAllCurrentSongsWith(detail.songs, detail.title || '当前列表');
};
@@ -2407,7 +1616,7 @@ export default function MusicClient({ children: _children }: { children?: React.
window.removeEventListener('music:add-to-playlist', handleAddToPlaylistEvent);
window.removeEventListener('music:play-later', handlePlayLaterEvent);
};
- }, [songs, playlist, playRecords, currentSong, quality, currentSource]);
+ }, [playlist, playRecords, currentSong, quality, currentSource]);
return (
@@ -2702,7 +1911,7 @@ export default function MusicClient({ children: _children }: { children?: React.
@@ -2871,7 +2080,7 @@ export default function MusicClient({ children: _children }: { children?: React.
{currentSong.name}
-
+
{currentSong.artist}
@@ -3234,7 +2443,7 @@ export default function MusicClient({ children: _children }: { children?: React.
}`}>
{song.name}
-
+
{song.artist}
@@ -3422,95 +2631,13 @@ export default function MusicClient({ children: _children }: { children?: React.
)}
- {showSidebarDrawer && (
-
-
- )}
+ setShowSidebarDrawer(false)}
+ onNavigate={(href) => router.push(href)}
+ />
{/* Add to Playlist Modal */}
diff --git a/src/app/music/my-playlists/page.tsx b/src/app/music/my-playlists/page.tsx
index d00f140..46eb24d 100644
--- a/src/app/music/my-playlists/page.tsx
+++ b/src/app/music/my-playlists/page.tsx
@@ -2,7 +2,8 @@
import { useCallback, useEffect, useState } from 'react';
import { playMusicList, playMusicSong } from '@/lib/music/actions';
-import { MusicLoadingIndicator } from '../MusicClient';
+import MusicLoadingIndicator from '@/components/music/MusicLoadingIndicator';
+import { getApiErrorMessage } from '@/lib/music/errors';
import { mapSong, SourcePill } from '@/lib/music/shared';
export default function MusicMyPlaylistsPage() {
@@ -14,14 +15,6 @@ export default function MusicMyPlaylistsPage() {
const [deletingPlaylistId, setDeletingPlaylistId] = useState(null);
const [removingSongId, setRemovingSongId] = useState(null);
- const getApiErrorMessage = (error: unknown, fallback: string) => {
- if (typeof error === 'string') return error;
- if (error && typeof error === 'object' && 'message' in error && typeof (error as { message?: unknown }).message === 'string') {
- return (error as { message: string }).message;
- }
- return fallback;
- };
-
const loadUserPlaylists = useCallback(() => {
setLoadingUserPlaylists(true);
fetch('/api/music/v2/playlists')
diff --git a/src/app/music/playlists/page.tsx b/src/app/music/playlists/page.tsx
deleted file mode 100644
index 69f30d4..0000000
--- a/src/app/music/playlists/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from 'next/navigation';
-
-export default function LegacyMusicPlaylistsPage() {
- redirect('/music/rankings');
-}
diff --git a/src/app/music/rankings/[source]/[playlistId]/page.tsx b/src/app/music/rankings/[source]/[playlistId]/page.tsx
index 4dad364..496938f 100644
--- a/src/app/music/rankings/[source]/[playlistId]/page.tsx
+++ b/src/app/music/rankings/[source]/[playlistId]/page.tsx
@@ -3,9 +3,10 @@
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'next/navigation';
import { playMusicList } from '@/lib/music/actions';
-import { MusicLoadingIndicator, type Song } from '../../../MusicClient';
-import SongList from '../../../SongList';
+import MusicLoadingIndicator from '@/components/music/MusicLoadingIndicator';
+import SongList from '@/components/music/SongList';
import { mapSong, normalizeSource } from '@/lib/music/shared';
+import type { Song } from '@/lib/music/types';
export default function MusicRankingDetailPage() {
const params = useParams<{ source: string; playlistId: string }>();
diff --git a/src/app/music/rankings/page.tsx b/src/app/music/rankings/page.tsx
index ae1c918..ed340ca 100644
--- a/src/app/music/rankings/page.tsx
+++ b/src/app/music/rankings/page.tsx
@@ -2,8 +2,9 @@
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
-import { MusicLoadingIndicator, type Playlist } from '../MusicClient';
+import MusicLoadingIndicator from '@/components/music/MusicLoadingIndicator';
import { musicSources, normalizeSource } from '@/lib/music/shared';
+import type { Playlist } from '@/lib/music/types';
export default function MusicRankingsPage() {
const router = useRouter();
diff --git a/src/app/music/search/page.tsx b/src/app/music/search/page.tsx
index c7ec6fb..7566e3d 100644
--- a/src/app/music/search/page.tsx
+++ b/src/app/music/search/page.tsx
@@ -3,9 +3,10 @@
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { playMusicList } from '@/lib/music/actions';
-import { MusicLoadingIndicator, type Song } from '../MusicClient';
-import SongList from '../SongList';
+import MusicLoadingIndicator from '@/components/music/MusicLoadingIndicator';
+import SongList from '@/components/music/SongList';
import { mapSong, musicSources, normalizeSource } from '@/lib/music/shared';
+import type { Song } from '@/lib/music/types';
export default function MusicSearchPage() {
const router = useRouter();
diff --git a/src/components/music/MusicLoadingIndicator.tsx b/src/components/music/MusicLoadingIndicator.tsx
new file mode 100644
index 0000000..c97a53b
--- /dev/null
+++ b/src/components/music/MusicLoadingIndicator.tsx
@@ -0,0 +1,31 @@
+export default function MusicLoadingIndicator({
+ text,
+ size = 'md',
+ className = '',
+}: {
+ text?: string;
+ size?: 'sm' | 'md';
+ className?: string;
+}) {
+ const iconSize = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5';
+ const textSize = size === 'sm' ? 'text-xs' : 'text-sm';
+
+ return (
+
+
+ {[0, 1, 2].map((index) => (
+
+ ))}
+
+ {text ?
{text} : null}
+
+ );
+}
diff --git a/src/components/music/MusicSidebarDrawer.tsx b/src/components/music/MusicSidebarDrawer.tsx
new file mode 100644
index 0000000..ac4bd31
--- /dev/null
+++ b/src/components/music/MusicSidebarDrawer.tsx
@@ -0,0 +1,111 @@
+import type { MusicSource } from '@/lib/music/types';
+
+interface MusicSidebarDrawerProps {
+ currentSource: MusicSource;
+ isOpen: boolean;
+ pathname: string | null;
+ onClose: () => void;
+ onNavigate: (href: string) => void;
+}
+
+const musicNavItems = [
+ { key: 'rankings', label: '排行榜', href: '/music/rankings', icon: 'M3 4h18M8 8h13M3 12h18M8 16h13M3 20h18' },
+ { key: 'search', label: '搜索', icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' },
+ { key: 'my-playlists', label: '我的歌单', href: '/music/my-playlists', icon: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z' },
+];
+
+export default function MusicSidebarDrawer({
+ currentSource,
+ isOpen,
+ pathname,
+ onClose,
+ onNavigate,
+}: MusicSidebarDrawerProps) {
+ if (!isOpen) return null;
+
+ const navigate = (href: string) => {
+ onClose();
+ onNavigate(href);
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/music/SongList.tsx b/src/components/music/SongList.tsx
similarity index 98%
rename from src/app/music/SongList.tsx
rename to src/components/music/SongList.tsx
index 372f107..aed51f8 100644
--- a/src/app/music/SongList.tsx
+++ b/src/components/music/SongList.tsx
@@ -1,8 +1,8 @@
'use client';
import { addMusicSongToPlaylist, playMusicLater, playMusicSong } from '@/lib/music/actions';
-import type { Song } from './MusicClient';
import { SourcePill } from '@/lib/music/shared';
+import type { Song } from '@/lib/music/types';
export default function SongList({ songs }: { songs: Song[] }) {
return (
diff --git a/src/lib/music/actions.ts b/src/lib/music/actions.ts
index 44bcf42..273905d 100644
--- a/src/lib/music/actions.ts
+++ b/src/lib/music/actions.ts
@@ -1,6 +1,6 @@
'use client';
-import type { Song } from '@/app/music/MusicClient';
+import type { Song } from '@/lib/music/types';
export function playMusicSong(song: Song, index = -1) {
window.dispatchEvent(new CustomEvent('music:play-song', { detail: { song, index } }));
diff --git a/src/lib/music/errors.ts b/src/lib/music/errors.ts
new file mode 100644
index 0000000..2c58047
--- /dev/null
+++ b/src/lib/music/errors.ts
@@ -0,0 +1,8 @@
+export function getApiErrorMessage(error: unknown, fallback: string): string {
+ if (typeof error === 'string') return error;
+ if (error && typeof error === 'object' && 'message' in error) {
+ const message = (error as { message?: unknown }).message;
+ if (typeof message === 'string' && message.trim()) return message;
+ }
+ return fallback;
+}
diff --git a/src/lib/music/shared.ts b/src/lib/music/shared.ts
index 331c1c6..3bdb10c 100644
--- a/src/lib/music/shared.ts
+++ b/src/lib/music/shared.ts
@@ -1,5 +1,5 @@
import React from 'react';
-import type { MusicSource, Song } from '@/app/music/MusicClient';
+import type { MusicSource, Song } from '@/lib/music/types';
export const musicSources: Array<{ key: MusicSource; label: string }> = [
{ key: 'wy', label: '网易云' },
@@ -10,12 +10,15 @@ export const musicSources: Array<{ key: MusicSource; label: string }> = [
];
export function normalizeSource(source: string | undefined | null): MusicSource {
+ if (source === 'netease') return 'wy';
+ if (source === 'qq') return 'tx';
+ if (source === 'kuwo') return 'kw';
if (source === 'wy' || source === 'tx' || source === 'kw' || source === 'kg' || source === 'mg') return source;
return 'wy';
}
export function mapSong(song: any): Song {
- const rawSource = song.platform || song.source || song.vendor || song.origin;
+ const rawSource = song.source || song.platform || song.vendor || song.origin;
return {
id: String(song.id ?? song.songId ?? song.rid ?? song.mid ?? ''),
name: song.name || song.title || '未知歌曲',
@@ -23,19 +26,48 @@ export function mapSong(song: any): Song {
album: song.album || song.albumName,
pic: song.pic || song.cover || song.img,
platform: normalizeSource(rawSource),
- duration: song.duration,
- durationText: song.durationText,
+ duration: song.durationSec || song.duration,
+ durationText: song.durationText || song.interval,
songmid: song.songmid || song.mid,
};
}
-export function SourcePill({ source, className = '' }: { source?: MusicSource | string; className?: string }) {
+export function getSourceDisplayLabel(source?: MusicSource | string, compact = true): string {
+ switch (normalizeSource(source)) {
+ case 'wy':
+ return compact ? '网易' : '网易云';
+ case 'tx':
+ return compact ? 'QQ' : 'QQ音乐';
+ case 'kw':
+ return '酷我';
+ case 'kg':
+ return '酷狗';
+ case 'mg':
+ return '咪咕';
+ }
+}
+
+export function SourcePill({
+ source,
+ className = '',
+ variant = 'subtle',
+}: {
+ source?: MusicSource | string;
+ className?: string;
+ variant?: 'subtle' | 'accent';
+}) {
const normalized = normalizeSource(source);
- const label = musicSources.find((item) => item.key === normalized)?.label || source || '未知';
+ const label = variant === 'accent'
+ ? getSourceDisplayLabel(normalized)
+ : musicSources.find((item) => item.key === normalized)?.label || source || '未知';
+ const variantClass = variant === 'accent'
+ ? 'rounded-lg border-red-500/50 bg-red-500/20 leading-none text-red-400'
+ : 'rounded-full border-white/10 bg-white/5 text-zinc-400';
+
return React.createElement(
'span',
{
- className: `inline-flex shrink-0 items-center rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] font-medium text-zinc-400 ${className}`,
+ className: `inline-flex shrink-0 items-center border px-2 py-0.5 text-[10px] font-medium ${variantClass} ${className}`,
},
label
);
diff --git a/src/lib/music/types.ts b/src/lib/music/types.ts
new file mode 100644
index 0000000..d457fe4
--- /dev/null
+++ b/src/lib/music/types.ts
@@ -0,0 +1,23 @@
+export type MusicSource = 'wy' | 'tx' | 'kw' | 'kg' | 'mg';
+
+export type MusicQuality = '128k' | '320k' | 'flac' | 'flac24bit';
+
+export interface Song {
+ id: string;
+ name: string;
+ artist: string;
+ album?: string;
+ pic?: string;
+ platform: MusicSource;
+ duration?: number;
+ durationText?: string;
+ songmid?: string;
+}
+
+export interface Playlist {
+ id: string;
+ name: string;
+ pic?: string;
+ source?: MusicSource;
+ updateFrequency?: string;
+}