优化music

This commit is contained in:
mtvpls
2026-02-01 16:25:22 +08:00
parent 19be34c6fe
commit 2ef04bc434

View File

@@ -12,6 +12,14 @@ interface Song {
pic?: string;
}
interface PlayRecord {
platform: 'netease' | 'qq' | 'kuwo';
id: string;
playTime: number; // 播放时间(秒)
duration: number; // 总时长(秒)
timestamp: number; // 添加时间戳
}
interface LyricLine {
time: number;
text: string;
@@ -46,6 +54,11 @@ export default function MusicPage() {
const [lyrics, setLyrics] = useState<LyricLine[]>([]);
const [currentLyricIndex, setCurrentLyricIndex] = useState(-1);
const [currentSongUrl, setCurrentSongUrl] = useState('');
const [playRecords, setPlayRecords] = useState<PlayRecord[]>([]); // 播放记录只存平台和ID
const [playlist, setPlaylist] = useState<Song[]>([]); // 完整歌曲信息(用于显示)
const [showPlaylist, setShowPlaylist] = useState(false);
const [playlistIndex, setPlaylistIndex] = useState(-1); // 当前在播放列表中的索引
const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单
const audioRef = useRef<HTMLAudioElement>(null);
const lyricsContainerRef = useRef<HTMLDivElement>(null);
@@ -69,6 +82,9 @@ export default function MusicPage() {
currentTime: audioRef.current?.currentTime || 0,
currentSongUrl,
lyrics,
playRecords, // 只保存播放记录(平台+ID+播放信息)
playlist, // 保存完整歌曲信息(用于显示)
playlistIndex,
};
localStorage.setItem('musicPlayState', JSON.stringify(playState));
@@ -93,6 +109,9 @@ export default function MusicPage() {
setVolume(playState.volume || 100);
setCurrentSongUrl(playState.currentSongUrl || '');
setLyrics(playState.lyrics || []);
setPlayRecords(playState.playRecords || []);
setPlaylist(playState.playlist || []); // 恢复播放列表
setPlaylistIndex(playState.playlistIndex || -1);
// 保存需要恢复的时间点
restoredTimeRef.current = playState.currentTime || 0;
@@ -103,7 +122,21 @@ export default function MusicPage() {
setTimeout(() => {
if (audioRef.current && playState.currentSongUrl) {
audioRef.current.src = playState.currentSongUrl;
// currentTime 会在 loadedmetadata 事件中设置
// 监听多个事件以确保进度恢复
const restoreTime = () => {
if (audioRef.current && restoredTimeRef.current > 0) {
audioRef.current.currentTime = restoredTimeRef.current;
restoredTimeRef.current = 0;
}
};
// 监听加载完成事件
audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true });
audioRef.current.addEventListener('canplay', restoreTime, { once: true });
// 调用 load() 触发音频加载
audioRef.current.load();
}
}, 100);
}
@@ -122,7 +155,7 @@ export default function MusicPage() {
if (currentSong) {
savePlayState();
}
}, [currentSong, currentSongIndex, songs, currentPlaylistTitle, currentSource, currentView, quality, playMode, volume, currentSongUrl, lyrics]);
}, [currentSong, currentSongIndex, songs, currentPlaylistTitle, currentSource, currentView, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]);
// 加载排行榜列表
const loadPlaylists = async (source: string) => {
@@ -298,6 +331,43 @@ export default function MusicPage() {
setShowPlayer(true);
setLyrics([]); // 清空旧歌词
// 添加到播放记录和播放列表
const record: PlayRecord = {
platform: currentSource,
id: song.id,
playTime: 0, // 初始播放时间
duration: 0, // 将在音频加载后更新
timestamp: Date.now(),
};
setPlayRecords(prev => {
const existingIndex = prev.findIndex(r => r.platform === record.platform && r.id === record.id);
if (existingIndex >= 0) {
// 记录已存在,更新时间戳
const updated = [...prev];
updated[existingIndex] = {
...updated[existingIndex],
timestamp: Date.now(),
};
setPlaylistIndex(existingIndex);
return updated;
} else {
// 新记录,添加到列表末尾
const newRecords = [...prev, record];
setPlaylistIndex(newRecords.length - 1);
return newRecords;
}
});
setPlaylist(prev => {
const existingIndex = prev.findIndex(s => s.id === song.id);
if (existingIndex >= 0) {
return prev;
} else {
return [...prev, song];
}
});
// 调用解析接口获取播放链接
const response = await fetch('/api/music', {
method: 'POST',
@@ -404,14 +474,24 @@ export default function MusicPage() {
// 上一曲
const playPrev = () => {
if (currentSongIndex > 0) {
// 优先从播放列表切换
if (playlist.length > 0 && playlistIndex > 0) {
const prevIndex = playlistIndex - 1;
setPlaylistIndex(prevIndex);
playSong(playlist[prevIndex], -1);
} else if (currentSongIndex > 0) {
playSong(songs[currentSongIndex - 1], currentSongIndex - 1);
}
};
// 下一曲
const playNext = () => {
if (currentSongIndex < songs.length - 1) {
// 优先从播放列表切换
if (playlist.length > 0 && playlistIndex < playlist.length - 1) {
const nextIndex = playlistIndex + 1;
setPlaylistIndex(nextIndex);
playSong(playlist[nextIndex], -1);
} else if (currentSongIndex < songs.length - 1) {
playSong(songs[currentSongIndex + 1], currentSongIndex + 1);
}
};
@@ -484,10 +564,25 @@ export default function MusicPage() {
setCurrentLyricIndex(index);
}
// 每20秒保存一次播放进度
// 每20秒保存一次播放进度和播放时间
const now = Date.now();
if (now - lastSaveTimeRef.current > 20000) {
lastSaveTimeRef.current = now;
// 更新当前播放记录的播放时间
if (currentSong && playlistIndex >= 0) {
setPlayRecords(prev => {
const updated = [...prev];
if (updated[playlistIndex]) {
updated[playlistIndex] = {
...updated[playlistIndex],
playTime: audio.currentTime,
};
}
return updated;
});
}
savePlayState();
}
};
@@ -500,7 +595,23 @@ export default function MusicPage() {
}
};
const handleDurationChange = () => setDuration(audio.duration);
const handleDurationChange = () => {
setDuration(audio.duration);
// 更新当前播放记录的总时长
if (currentSong && playlistIndex >= 0) {
setPlayRecords(prev => {
const updated = [...prev];
if (updated[playlistIndex]) {
updated[playlistIndex] = {
...updated[playlistIndex],
duration: audio.duration,
};
}
return updated;
});
}
};
const handleEnded = () => {
if (playMode === 'single') {
audio.currentTime = 0;
@@ -861,12 +972,6 @@ export default function MusicPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
<button
onClick={cycleQuality}
className="px-2 py-0.5 rounded border text-amber-400 border-amber-500/50 bg-amber-900/20 text-[9px] font-mono min-w-[32px] text-center"
>
{getQualityLabel()}
</button>
<button
onClick={toggleMode}
className="text-zinc-500 hover:text-white transition-colors"
@@ -1004,6 +1109,20 @@ export default function MusicPage() {
{/* 下排:其他按钮(小一号) */}
<div className="flex items-center justify-center gap-3 md:gap-4 mb-2 md:mb-3">
<button
onClick={() => setShowPlaylist(true)}
className="text-zinc-500 hover:text-white transition-colors relative"
title="播放列表"
>
<svg className="w-4 h-4 md:w-5 md:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
{playlist.length > 0 && (
<span className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full text-[8px] flex items-center justify-center font-bold">
{playlist.length > 9 ? '9+' : playlist.length}
</span>
)}
</button>
<button
onClick={downloadSong}
className="text-zinc-500 hover:text-white transition-colors"
@@ -1013,6 +1132,13 @@ export default function MusicPage() {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
<button
onClick={() => setShowQualityMenu(true)}
className="px-2 py-0.5 rounded border text-amber-400 border-amber-500/50 bg-amber-900/20 text-[9px] md:text-[10px] font-mono min-w-[32px] text-center hover:bg-amber-900/30 transition-colors"
title="音质选择"
>
{getQualityLabel()}
</button>
<button
onClick={toggleMode}
className="text-zinc-500 hover:text-white transition-colors"
@@ -1064,6 +1190,221 @@ export default function MusicPage() {
</div>
</div>
)}
{/* Playlist Modal */}
{showPlaylist && (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
<div className="w-full max-w-2xl h-[90vh] md:h-auto max-h-[90vh] bg-zinc-900/95 rounded-2xl overflow-hidden border border-white/10 shadow-2xl flex flex-col">
{/* Header */}
<div className="relative h-16 bg-gradient-to-b from-zinc-800 to-zinc-900 shrink-0 flex items-center justify-between px-6">
<h2 className="text-lg font-bold text-white"></h2>
<button
onClick={() => setShowPlaylist(false)}
className="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors"
>
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Playlist */}
<div className="flex-1 overflow-y-auto p-4 md:p-6">
{playlist.length > 0 ? (
<div className="space-y-2">
{playlist.map((song, index) => (
<div
key={`${song.id}-${index}`}
onClick={() => {
setPlaylistIndex(index);
playSong(song, -1);
setShowPlaylist(false);
}}
className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors group ${
index === playlistIndex
? 'bg-green-500/20 border border-green-500/50'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div className="w-12 h-12 rounded-lg bg-zinc-800 overflow-hidden shrink-0">
{song.pic ? (
<img
src={song.pic}
alt={song.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<svg className="w-6 h-6 text-zinc-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />
</svg>
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-medium truncate transition-colors ${
index === playlistIndex ? 'text-green-400' : 'text-white group-hover:text-green-400'
}`}>
{song.name}
</div>
<div className="text-xs text-zinc-500 truncate">{song.artist}</div>
</div>
{index === playlistIndex ? (
<svg className="w-5 h-5 text-green-400 shrink-0 animate-pulse" fill="currentColor" viewBox="0 0 20 20">
<path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2V3z" />
</svg>
) : (
<svg className="w-5 h-5 text-zinc-600 group-hover:text-white transition-colors shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
</svg>
)}
</div>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center h-full text-center">
<svg className="w-16 h-16 text-zinc-700 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
</svg>
<p className="text-zinc-500 text-sm"></p>
<p className="text-zinc-600 text-xs mt-2"></p>
</div>
)}
</div>
</div>
</div>
)}
{/* Quality Selection Menu */}
{showQualityMenu && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[100] flex items-end justify-center"
onClick={() => setShowQualityMenu(false)}
>
<div
className="w-full max-w-md bg-zinc-900 rounded-t-2xl border-t border-white/10 shadow-2xl animate-slide-up"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="p-4 border-b border-white/10">
<h3 className="text-lg font-bold text-white text-center"></h3>
</div>
{/* Quality Options */}
<div className="p-4 space-y-2">
<button
onClick={() => {
setQuality('128k');
setShowQualityMenu(false);
}}
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
quality === '128k'
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${quality === '128k' ? 'bg-amber-400' : 'bg-zinc-600'}`} />
<div className="text-left">
<div className="text-white font-medium"></div>
<div className="text-xs text-zinc-500">128kbps</div>
</div>
</div>
{quality === '128k' && (
<svg className="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
<button
onClick={() => {
setQuality('320k');
setShowQualityMenu(false);
}}
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
quality === '320k'
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${quality === '320k' ? 'bg-amber-400' : 'bg-zinc-600'}`} />
<div className="text-left">
<div className="text-white font-medium"> HQ</div>
<div className="text-xs text-zinc-500">320kbps</div>
</div>
</div>
{quality === '320k' && (
<svg className="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
<button
onClick={() => {
setQuality('flac');
setShowQualityMenu(false);
}}
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
quality === 'flac'
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${quality === 'flac' ? 'bg-amber-400' : 'bg-zinc-600'}`} />
<div className="text-left">
<div className="text-white font-medium"> SQ</div>
<div className="text-xs text-zinc-500">FLAC</div>
</div>
</div>
{quality === 'flac' && (
<svg className="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
<button
onClick={() => {
setQuality('flac24bit');
setShowQualityMenu(false);
}}
className={`w-full p-4 rounded-lg flex items-center justify-between transition-colors ${
quality === 'flac24bit'
? 'bg-amber-500/20 border border-amber-500/50'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${quality === 'flac24bit' ? 'bg-amber-400' : 'bg-zinc-600'}`} />
<div className="text-left">
<div className="text-white font-medium">Hi-Res音质 HR</div>
<div className="text-xs text-zinc-500">FLAC 24bit</div>
</div>
</div>
{quality === 'flac24bit' && (
<svg className="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
</div>
{/* Cancel Button */}
<div className="p-4 pt-0">
<button
onClick={() => setShowQualityMenu(false)}
className="w-full p-3 rounded-lg bg-white/5 hover:bg-white/10 text-white transition-colors"
>
</button>
</div>
</div>
</div>
)}
</div>
);
}