Files
MoonTVPlus/src/app/music/page.tsx
T
2026-02-01 22:07:28 +08:00

1636 lines
69 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { useRouter } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import {
getAllMusicPlayRecords,
saveMusicPlayRecord,
MusicPlayRecord,
deleteMusicPlayRecord,
clearAllMusicPlayRecords,
} from '@/lib/db.client';
interface Song {
id: string;
name: string;
artist: string;
album?: string;
pic?: string;
platform?: 'netease' | 'qq' | 'kuwo'; // 添加平台信息
}
interface PlayRecord {
platform: 'netease' | 'qq' | 'kuwo';
id: string;
playTime: number; // 播放时间(秒)
duration: number; // 总时长(秒)
timestamp: number; // 添加时间戳
}
interface LyricLine {
time: number;
text: string;
}
interface Playlist {
id: string;
name: string;
pic: string;
updateFrequency?: string;
}
export default function MusicPage() {
const router = useRouter();
const [currentSource, setCurrentSource] = useState<'netease' | 'qq' | 'kuwo'>('netease');
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [songs, setSongs] = useState<Song[]>([]);
const [currentView, setCurrentView] = useState<'playlists' | 'songs'>('playlists');
const [currentPlaylistTitle, setCurrentPlaylistTitle] = useState('');
const [searchKeyword, setSearchKeyword] = useState('');
const [currentSong, setCurrentSong] = useState<Song | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(100);
const [quality, setQuality] = useState<'128k' | '320k' | 'flac' | 'flac24bit'>('320k');
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 [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 [showVolumeSlider, setShowVolumeSlider] = useState(false); // 音量滑块显示状态
const [pendingSongToPlay, setPendingSongToPlay] = useState<{ platform: string; id: string } | null>(null); // 待播放的歌曲信息
const audioRef = useRef<HTMLAudioElement>(null);
const lyricsContainerRef = useRef<HTMLDivElement>(null);
const lastSaveTimeRef = useRef<number>(0);
const restoredTimeRef = useRef<number>(0);
const songStartTimeRef = useRef<number>(0); // 歌曲开始播放的时间戳
// 工具函数:处理图片 URL(在 HTTPS 环境下代理 HTTP 图片)
const processImageUrl = (url: string | undefined, platform: string): string | undefined => {
if (!url) return url;
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
// 只对酷我音乐的 HTTP 图片在 HTTPS 环境下进行代理
if (platform === 'kuwo' && isHttps && url.startsWith('http://')) {
return `/api/music/proxy?url=${encodeURIComponent(url)}`;
}
return url;
};
// 保存播放状态到 localStorage
const savePlayState = () => {
if (!currentSong) return;
const playState = {
currentSong,
currentSongIndex,
songs,
currentPlaylistTitle,
currentSource,
currentView,
quality,
playMode,
volume,
currentTime: audioRef.current?.currentTime || 0,
currentSongUrl,
lyrics,
playRecords, // 只保存播放记录(平台+ID+播放信息)
playlist, // 保存完整歌曲信息(用于显示)
playlistIndex,
};
localStorage.setItem('musicPlayState', JSON.stringify(playState));
};
// 从 localStorage 恢复播放状态
const restorePlayState = async () => {
try {
const saved = localStorage.getItem('musicPlayState');
if (!saved) return;
const playState = JSON.parse(saved);
setCurrentSong(playState.currentSong);
setCurrentSongIndex(playState.currentSongIndex);
setSongs(playState.songs || []);
setCurrentPlaylistTitle(playState.currentPlaylistTitle || '');
setCurrentSource(playState.currentSource || 'netease');
setCurrentView(playState.currentView || 'playlists');
setQuality(playState.quality || '320k');
setPlayMode(playState.playMode || 'loop');
setVolume(playState.volume || 100);
setLyrics(playState.lyrics || []);
setPlayRecords(playState.playRecords || []);
setPlaylist(playState.playlist || []); // 恢复播放列表
// 恢复 playlistIndex,如果没有则设置为 -1
const restoredIndex = playState.playlistIndex ?? -1;
setPlaylistIndex(restoredIndex);
// 保存需要恢复的时间点
restoredTimeRef.current = playState.currentTime || 0;
if (playState.currentSong) {
setShowPlayer(true);
// 记录歌曲开始播放的时间(恢复时也需要设置)
songStartTimeRef.current = Date.now();
// 获取歌曲的平台信息
const platform = playState.currentSong.platform || playState.currentSource || 'netease';
// 重新解析歌曲获取新的播放链接
try {
const response = await fetch('/api/music', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'parse',
platform: platform,
ids: playState.currentSong.id,
quality: playState.quality || '320k',
}),
});
const data = await response.json();
if (data.code === 0 && data.data?.data && data.data.data.length > 0) {
const songData = data.data.data[0];
if (songData.url && songData.success) {
// 对于酷我音乐,使用代理
let playUrl = songData.url;
if (platform === 'kuwo') {
playUrl = `/api/music/proxy?url=${encodeURIComponent(songData.url)}`;
}
setCurrentSongUrl(songData.url);
// 延迟设置音频源,等待 audio 元素加载
setTimeout(() => {
if (audioRef.current) {
audioRef.current.src = playUrl;
// 监听多个事件以确保进度恢复
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);
}
}
} catch (error) {
console.error('重新解析歌曲失败:', error);
}
}
} catch (error) {
console.error('恢复播放状态失败:', error);
}
};
// 页面加载时恢复播放状态和数据库记录
useEffect(() => {
const initializePlayState = async () => {
// 先恢复 localStorage 中的播放状态
restorePlayState();
// 从数据库加载播放记录
try {
const dbRecords = await getAllMusicPlayRecords();
// 将数据库记录转换为前端格式
const records: PlayRecord[] = [];
const songs: Song[] = [];
Object.entries(dbRecords).forEach(([key, record]) => {
records.push({
platform: record.platform,
id: record.id,
playTime: record.play_time,
duration: record.duration,
timestamp: record.save_time,
});
songs.push({
id: record.id,
name: record.name,
artist: record.artist,
album: record.album,
pic: record.pic,
platform: record.platform, // 添加平台信息
});
});
// 更新状态
if (records.length > 0) {
setPlayRecords(records);
setPlaylist(songs);
// 如果当前有正在播放的歌曲,找到它在记录中的索引
const savedPlayState = localStorage.getItem('musicPlayState');
if (savedPlayState) {
const playState = JSON.parse(savedPlayState);
if (playState.currentSong) {
const platform = playState.currentSong.platform || playState.currentSource || 'netease';
const currentIndex = records.findIndex(
r => r.platform === platform && r.id === playState.currentSong.id
);
if (currentIndex >= 0) {
setPlaylistIndex(currentIndex);
}
}
}
}
} catch (error) {
console.error('加载播放记录失败:', error);
}
};
initializePlayState();
}, []);
// 监听播放状态变化,自动保存
useEffect(() => {
if (currentSong) {
savePlayState();
}
}, [currentSong, currentSongIndex, songs, currentPlaylistTitle, currentSource, currentView, quality, playMode, volume, currentSongUrl, lyrics, playRecords, playlistIndex]);
// 监听 playRecords 变化,更新 playlistIndex
useEffect(() => {
if (pendingSongToPlay) {
const index = playRecords.findIndex(
r => r.platform === pendingSongToPlay.platform && r.id === pendingSongToPlay.id
);
setPlaylistIndex(index);
setPendingSongToPlay(null);
}
}, [playRecords, pendingSongToPlay]);
// 加载排行榜列表
const loadPlaylists = async (source: string) => {
setLoading(true);
try {
const response = await fetch(
`/api/music?action=toplists&platform=${source}`
);
const data = await response.json();
// 确保返回的是数组
setPlaylists(Array.isArray(data) ? data : []);
} catch (error) {
console.error('加载排行榜失败:', error);
setPlaylists([]);
} finally {
setLoading(false);
}
};
// 加载歌单详情
const loadPlaylist = async (playlistId: string, playlistName: string) => {
setLoading(true);
try {
const response = await fetch(
`/api/music?action=toplist&platform=${currentSource}&id=${playlistId}`
);
const data = await response.json();
// 确保返回的是数组
setSongs(Array.isArray(data) ? data : []);
setCurrentPlaylistTitle(playlistName);
setCurrentView('songs');
} catch (error) {
console.error('加载歌单失败:', error);
setSongs([]);
} finally {
setLoading(false);
}
};
// 搜索歌曲
const searchSongs = async () => {
if (!searchKeyword.trim()) return;
setLoading(true);
try {
const response = await fetch(
`/api/music?action=search&platform=${currentSource}&keyword=${encodeURIComponent(searchKeyword)}&page=1&pageSize=20`
);
const data = await response.json();
// 确保返回的是数组
setSongs(Array.isArray(data) ? data : []);
setCurrentPlaylistTitle(`搜索: ${searchKeyword}`);
setCurrentView('songs');
} catch (error) {
console.error('搜索失败:', error);
setSongs([]);
} finally {
setLoading(false);
}
};
// 播放歌曲
const playSong = async (song: Song, index: number) => {
try {
// 使用歌曲自己的平台信息,如果没有则使用当前选择的平台
const platform = song.platform || currentSource;
// 记录歌曲开始播放的时间
songStartTimeRef.current = Date.now();
// 先设置当前歌曲和显示播放器
setCurrentSong(song);
setCurrentSongIndex(index);
setShowPlayer(true);
setLyrics([]); // 清空旧歌词
// 添加到播放记录和播放列表
const record: PlayRecord = {
platform: platform,
id: song.id,
playTime: 0, // 初始播放时间
duration: 0, // 将在音频加载后更新
timestamp: Date.now(),
};
// 设置待播放歌曲信息,用于在 playRecords 更新后找到索引
setPendingSongToPlay({ platform, id: song.id });
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(),
};
return updated;
} else {
// 新记录,添加到列表末尾
return [...prev, record];
}
});
setPlaylist(prev => {
const existingIndex = prev.findIndex(s => s.id === song.id && s.platform === platform);
if (existingIndex >= 0) {
return prev;
} else {
return [...prev, { ...song, platform }];
}
});
// 调用解析接口获取播放链接
const response = await fetch('/api/music', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'parse',
platform: platform, // 使用歌曲的平台
ids: song.id,
quality: quality,
}),
});
const data = await response.json();
// TuneHub 返回格式: { code: 0, data: { data: [...] } }
if (data.code === 0 && data.data?.data && data.data.data.length > 0) {
const songData = data.data.data[0];
if (songData.url && songData.success) {
// 处理封面图片(在 HTTPS 环境下代理 HTTP 图片)
const coverUrl = processImageUrl(songData.cover, platform);
// 更新歌曲信息,包括封面
if (coverUrl) {
setCurrentSong({
...song,
pic: coverUrl,
platform,
});
}
// 解析歌词
if (songData.lyrics) {
const parsedLyrics = parseLyric(songData.lyrics);
setLyrics(parsedLyrics);
}
// 保存原始 URL 用于下载
setCurrentSongUrl(songData.url);
// 对于酷我音乐,使用代理
let playUrl = songData.url;
if (platform === 'kuwo') {
playUrl = `/api/music/proxy?url=${encodeURIComponent(songData.url)}`;
}
if (audioRef.current) {
audioRef.current.src = playUrl;
audioRef.current.load();
audioRef.current.play().catch(err => {
console.error('播放失败:', err);
});
setIsPlaying(true);
}
} else {
console.error('无法获取播放链接,songData:', songData);
}
} else {
console.error('解析失败,完整响应:', data);
}
} catch (error) {
console.error('播放失败:', error);
}
};
// 解析歌词文本
const parseLyric = (lyricText: string): LyricLine[] => {
if (!lyricText) return [];
const lines = lyricText.split('\n');
const lyricLines: LyricLine[] = [];
// 匹配 [mm:ss.xx] 或 [mm:ss] 格式
const timeRegex = /\[(\d{2}):(\d{2})(?:\.(\d{2,3}))?\]/g;
lines.forEach(line => {
const matches = Array.from(line.matchAll(timeRegex));
if (matches.length > 0) {
// 提取歌词文本(去掉所有时间标签)
const text = line.replace(timeRegex, '').trim();
if (text) {
// 一行可能有多个时间标签
matches.forEach(match => {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0;
const time = minutes * 60 + seconds + milliseconds / 1000;
lyricLines.push({ time, text });
});
}
}
});
// 按时间排序
return lyricLines.sort((a, b) => a.time - b.time);
};
// 切换播放/暂停
const togglePlay = () => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
setIsPlaying(false);
// 暂停时保存状态到 localStorage 和数据库
savePlayState();
// 前5秒不保存(避免加载时的跳转触发保存)
if (Date.now() - songStartTimeRef.current < 5000) {
return;
}
// 保存到数据库
if (currentSong && playlistIndex >= 0 && playRecords[playlistIndex]) {
const record = playRecords[playlistIndex];
const dbRecord: MusicPlayRecord = {
platform: record.platform,
id: record.id,
name: currentSong.name,
artist: currentSong.artist,
album: currentSong.album,
pic: currentSong.pic,
play_time: audioRef.current.currentTime,
duration: audioRef.current.duration || 0,
save_time: Date.now(),
};
saveMusicPlayRecord(record.platform, record.id, dbRecord).catch(err => {
console.error('暂停时保存播放记录失败:', err);
});
}
} else {
audioRef.current.play().catch(err => {
console.error('播放失败:', err);
});
setIsPlaying(true);
}
}
};
// 上一曲
const playPrev = () => {
// 优先从播放列表切换
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 (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);
}
};
// 切换音质
const cycleQuality = () => {
const qualities: Array<'128k' | '320k' | 'flac' | 'flac24bit'> = ['128k', '320k', 'flac', 'flac24bit'];
const currentIndex = qualities.indexOf(quality);
const nextIndex = (currentIndex + 1) % qualities.length;
setQuality(qualities[nextIndex]);
};
// 切换播放模式
const toggleMode = () => {
const modes: Array<'loop' | 'single' | 'random'> = ['loop', 'single', 'random'];
const currentIndex = modes.indexOf(playMode);
const nextIndex = (currentIndex + 1) % modes.length;
setPlayMode(modes[nextIndex]);
};
// 返回
const goBack = () => {
if (currentView === 'songs') {
setCurrentView('playlists');
setSongs([]);
} else {
router.back();
}
};
// 下载歌曲
const downloadSong = () => {
if (!currentSongUrl || !currentSong) return;
// 创建一个临时的 a 标签来触发下载
const link = document.createElement('a');
link.href = currentSongUrl;
link.download = `${currentSong.name} - ${currentSong.artist}.mp3`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 切换平台
const switchSource = (source: 'netease' | 'qq' | 'kuwo') => {
setCurrentSource(source);
setCurrentView('playlists');
setSongs([]);
setSearchKeyword('');
};
// 音频事件监听
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const handleTimeUpdate = () => {
setCurrentTime(audio.currentTime);
// 更新当前歌词索引
if (lyrics.length > 0) {
let index = -1;
for (let i = 0; i < lyrics.length; i++) {
if (lyrics[i].time <= audio.currentTime) {
index = i;
} else {
break;
}
}
setCurrentLyricIndex(index);
}
// 每20秒保存一次播放进度和播放时间
const now = Date.now();
if (now - lastSaveTimeRef.current > 20000) {
lastSaveTimeRef.current = now;
// 前5秒不保存(避免加载时的跳转触发保存)
if (Date.now() - songStartTimeRef.current < 5000) {
return;
}
// 更新当前播放记录的播放时间
if (currentSong && playlistIndex >= 0) {
setPlayRecords(prev => {
const updated = [...prev];
if (updated[playlistIndex]) {
updated[playlistIndex] = {
...updated[playlistIndex],
playTime: audio.currentTime,
};
// 保存到数据库
const record = updated[playlistIndex];
const dbRecord: MusicPlayRecord = {
platform: record.platform,
id: record.id,
name: currentSong.name,
artist: currentSong.artist,
album: currentSong.album,
pic: currentSong.pic,
play_time: audio.currentTime,
duration: audio.duration || 0,
save_time: Date.now(),
};
saveMusicPlayRecord(record.platform, record.id, dbRecord).catch(err => {
console.error('保存播放记录到数据库失败:', err);
});
}
return updated;
});
}
savePlayState();
}
};
const handleLoadedMetadata = () => {
// 恢复播放进度
if (restoredTimeRef.current > 0) {
audio.currentTime = restoredTimeRef.current;
restoredTimeRef.current = 0; // 清除标记
}
};
const handleDurationChange = () => {
setDuration(audio.duration);
// 前5秒不保存(避免加载时的跳转触发保存)
if (Date.now() - songStartTimeRef.current < 5000) {
return;
}
// 更新当前播放记录的总时长
if (currentSong && playlistIndex >= 0) {
setPlayRecords(prev => {
const updated = [...prev];
if (updated[playlistIndex]) {
updated[playlistIndex] = {
...updated[playlistIndex],
duration: audio.duration,
};
// 保存到数据库(包含时长信息)
const record = updated[playlistIndex];
const dbRecord: MusicPlayRecord = {
platform: record.platform,
id: record.id,
name: currentSong.name,
artist: currentSong.artist,
album: currentSong.album,
pic: currentSong.pic,
play_time: record.playTime,
duration: audio.duration,
save_time: Date.now(),
};
saveMusicPlayRecord(record.platform, record.id, dbRecord).catch(err => {
console.error('保存播放记录到数据库失败:', err);
});
}
return updated;
});
}
};
const handleEnded = () => {
if (playMode === 'single') {
audio.currentTime = 0;
audio.play();
} else if (playMode === 'random') {
const randomIndex = Math.floor(Math.random() * songs.length);
playSong(songs[randomIndex], randomIndex);
} else {
playNext();
}
};
audio.addEventListener('timeupdate', handleTimeUpdate);
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
audio.addEventListener('durationchange', handleDurationChange);
audio.addEventListener('ended', handleEnded);
return () => {
audio.removeEventListener('timeupdate', handleTimeUpdate);
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
audio.removeEventListener('durationchange', handleDurationChange);
audio.removeEventListener('ended', handleEnded);
};
}, [playMode, songs, currentSongIndex, lyrics, currentSong, playlistIndex, playRecords]);
// 初始加载
useEffect(() => {
loadPlaylists(currentSource);
}, [currentSource]);
// 歌词自动滚动
useEffect(() => {
if (lyricsContainerRef.current && currentLyricIndex >= 0) {
const container = lyricsContainerRef.current;
const activeLine = container.querySelector(`[data-index="${currentLyricIndex}"]`);
if (activeLine) {
activeLine.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}
}, [currentLyricIndex]);
// 搜索框回车
const handleSearchKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
searchSongs();
}
};
// 进度条拖动
const handleProgressChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTime = (parseFloat(e.target.value) / 100) * duration;
if (audioRef.current) {
audioRef.current.currentTime = newTime;
}
};
// 音量调节
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseInt(e.target.value);
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume / 100;
}
};
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
const getQualityLabel = () => {
switch (quality) {
case '128k': return '标准';
case '320k': return 'HQ';
case 'flac': return 'SQ';
case 'flac24bit': return 'HR';
}
};
const getSourceLabel = () => {
switch (currentSource) {
case 'netease': return '网易云';
case 'qq': return 'QQ音乐';
case 'kuwo': return '酷我';
}
};
const formatTime = (seconds: number) => {
if (isNaN(seconds) || seconds === 0) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="min-h-screen bg-zinc-950 text-white">
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-40 bg-zinc-950/95 backdrop-blur-md border-b border-white/10 px-4 md:px-6">
<div className="w-full mx-auto flex flex-col md:flex-row md:items-center md:justify-between gap-3 md:gap-4 py-3">
<div className="flex items-center justify-between md:justify-start md:gap-6 w-full md:w-auto">
<div className="flex items-center gap-3">
<button
onClick={() => router.push('/')}
className="w-8 h-8 rounded-full flex items-center justify-center bg-white/10 hover:bg-white/20 text-white transition-colors"
title="返回首页"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
<div className="w-8 h-8 rounded-full flex items-center justify-center bg-white/10 text-green-500">
<svg className="w-5 h-5" 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>
<span className="font-bold text-lg text-white">音乐</span>
</div>
<div className="flex bg-white/5 rounded-lg p-1 gap-1 border border-white/5">
<button
onClick={() => switchSource('netease')}
className={`px-3 py-1 md:px-4 rounded text-[10px] font-bold tracking-wider transition-all ${
currentSource === 'netease'
? 'bg-green-500 text-white border border-white/30 shadow-lg shadow-green-500/50'
: 'text-zinc-400 border border-transparent'
}`}
>
NET
</button>
<button
onClick={() => switchSource('qq')}
className={`px-3 py-1 md:px-4 rounded text-[10px] font-bold tracking-wider transition-all ${
currentSource === 'qq'
? 'bg-green-500 text-white border border-white/30 shadow-lg shadow-green-500/50'
: 'text-zinc-400 border border-transparent'
}`}
>
QQ
</button>
<button
onClick={() => switchSource('kuwo')}
className={`px-3 py-1 md:px-4 rounded text-[10px] font-bold tracking-wider transition-all ${
currentSource === 'kuwo'
? 'bg-green-500 text-white border border-white/30 shadow-lg shadow-green-500/50'
: 'text-zinc-400 border border-transparent'
}`}
>
酷我
</button>
</div>
</div>
<div className="flex items-center w-full md:flex-1 md:max-w-md md:ml-auto h-10 md:h-9 gap-2">
{currentView === 'songs' && (
<button
onClick={goBack}
className="w-10 h-full rounded-lg bg-white/10 hover:bg-white/20 flex items-center justify-center text-white border border-white/10"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M15 19l-7-7 7-7" />
</svg>
</button>
)}
<div className="relative group w-full h-full">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<svg className="w-4 h-4 text-zinc-500 group-focus-within:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<input
type="text"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={handleSearchKeyDown}
className="w-full h-full bg-black/30 border border-white/10 rounded-lg pl-9 pr-4 text-sm text-white focus:outline-none focus:border-green-500 focus:ring-2 focus:ring-green-500/50 font-mono placeholder-zinc-500"
placeholder="搜索歌曲或艺术家..."
/>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="pt-[120px] md:pt-[96px] pb-32 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
{loading && (
<div className="text-center text-zinc-500 py-8">加载中...</div>
)}
{/* Playlists View */}
{currentView === 'playlists' && !loading && (
<div>
<div className="flex items-center justify-between mb-6 border-b border-white/5 pb-2">
<h2 className="text-xs font-mono text-white/50 tracking-widest">排行榜</h2>
<span className="text-[10px] font-bold bg-white/10 px-2 py-0.5 rounded text-white">
{getSourceLabel()}
</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{playlists.map((playlist) => (
<div
key={playlist.id}
onClick={() => loadPlaylist(playlist.id, playlist.name)}
className="cursor-pointer group"
>
<div className="relative aspect-square rounded-lg overflow-hidden mb-2 bg-white/5">
{playlist.pic && (
<img
src={playlist.pic}
alt={playlist.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
)}
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<svg className="w-12 h-12 text-white" 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>
<h3 className="text-sm font-medium text-white/80 truncate">{playlist.name}</h3>
{playlist.updateFrequency && (
<p className="text-xs text-zinc-500 mt-1">{playlist.updateFrequency}</p>
)}
</div>
))}
</div>
</div>
)}
{/* Songs View */}
{currentView === 'songs' && !loading && (
<div>
<div className="flex items-center justify-between mb-6 border-b border-white/5 pb-2">
<h2 className="text-xl font-bold text-white/80 tracking-tight truncate max-w-md">
{currentPlaylistTitle}
</h2>
<span className="text-[10px] font-bold bg-white/10 px-2 py-0.5 rounded text-white shrink-0">
{songs.length} 首歌曲
</span>
</div>
<div className="space-y-1">
{songs.map((song, index) => (
<div
key={`${song.id}-${index}`}
onClick={() => playSong(song, index)}
className={`grid grid-cols-[40px_1fr_auto] md:grid-cols-[50px_2fr_1fr_auto] gap-2 px-3 py-3 rounded-lg cursor-pointer transition-all ${
currentSongIndex === index
? 'bg-white/12 border-l-2 border-green-500'
: 'hover:bg-white/5'
}`}
>
<div className="text-center text-zinc-500 text-sm">{index + 1}</div>
<div className="min-w-0">
<div className="text-sm font-medium text-white truncate">{song.name}</div>
<div className="text-xs text-zinc-500 truncate md:hidden">{song.artist}</div>
</div>
<div className="hidden md:block text-sm text-zinc-400 truncate">{song.artist}</div>
<div className="text-xs text-zinc-600">{getSourceLabel()}</div>
</div>
))}
</div>
</div>
)}
</div>
</main>
{/* Player */}
{showPlayer && currentSong && (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 w-[95%] max-w-3xl z-50">
<div className="bg-zinc-900/95 backdrop-blur-md rounded-xl p-4 border border-white/10 shadow-2xl">
{/* Progress Bar */}
<div className="absolute top-0 left-0 right-0 h-1 bg-white/10 rounded-t-xl overflow-hidden">
<div
className="h-full bg-green-500 transition-all pointer-events-none"
style={{ width: `${progress}%` }}
/>
<input
type="range"
min="0"
max="100"
value={progress}
onChange={handleProgressChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<div className="flex items-center justify-between gap-4 mt-2">
{/* Song Info */}
<div className="flex items-center gap-3 min-w-0 flex-1">
<div
className="w-12 h-12 rounded-lg bg-zinc-800 overflow-hidden shrink-0 flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setShowLyrics(true)}
>
{currentSong.pic ? (
<img
src={currentSong.pic}
alt={currentSong.name}
className="w-full h-full object-cover"
onError={(e) => {
// 图片加载失败时显示默认图标
e.currentTarget.style.display = 'none';
}}
/>
) : (
<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 className="min-w-0">
<div className="text-sm font-bold text-white truncate">{currentSong.name}</div>
<div className="text-xs text-zinc-500 truncate">{currentSong.artist}</div>
</div>
</div>
{/* Controls */}
<div className="flex items-center gap-4">
<button onClick={playPrev} className="text-zinc-500 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
</svg>
</button>
<button
onClick={togglePlay}
className="w-10 h-10 rounded-full bg-green-500 text-white flex items-center justify-center hover:bg-green-600 transition-colors"
>
{isPlaying ? (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
) : (
<svg className="w-4 h-4 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
)}
</button>
<button onClick={playNext} className="text-zinc-500 hover:text-white transition-colors">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
</svg>
</button>
</div>
{/* Right Controls */}
<div className="flex items-center gap-3">
<div className="hidden sm:flex items-center gap-2">
<input
type="range"
value={volume}
onChange={handleVolumeChange}
className="w-16 h-1 bg-white/10 rounded-full appearance-none cursor-pointer"
/>
</div>
<button
onClick={downloadSong}
className="text-zinc-500 hover:text-white transition-colors"
title="下载歌曲"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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={toggleMode}
className="text-zinc-500 hover:text-white transition-colors"
title={playMode === 'loop' ? '列表循环' : playMode === 'single' ? '单曲循环' : '随机播放'}
>
{playMode === 'loop' && (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)}
{playMode === 'single' && (
<div className="relative w-4 h-4">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span className="absolute inset-0 flex items-center justify-center text-[8px] font-bold">1</span>
</div>
)}
{playMode === 'random' && (
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/>
</svg>
)}
</button>
</div>
</div>
</div>
</div>
)}
{/* Audio Element */}
<audio ref={audioRef} className="hidden" />
{/* Lyrics Modal */}
{showLyrics && currentSong && (
<div
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-[100] flex items-center justify-center p-4"
onClick={(e) => {
// 点击背景关闭音量条
if (e.target === e.currentTarget) {
setShowVolumeSlider(false);
}
}}
>
<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"
onClick={() => setShowVolumeSlider(false)}
>
{/* Header */}
<div className="relative h-32 md:h-48 bg-gradient-to-b from-zinc-800 to-zinc-900 shrink-0">
{currentSong.pic && (
<div className="absolute inset-0">
<img
src={currentSong.pic}
alt={currentSong.name}
className="w-full h-full object-cover opacity-30 blur-xl"
/>
</div>
)}
<div className="relative h-full flex flex-col items-center justify-center p-4 md:p-6">
<button
onClick={() => setShowLyrics(false)}
className="absolute top-2 right-2 md:top-4 md:right-4 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 className="w-16 h-16 md:w-24 md:h-24 rounded-xl overflow-hidden shadow-2xl mb-2 md:mb-4">
{currentSong.pic ? (
<img
src={currentSong.pic}
alt={currentSong.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-zinc-800 flex items-center justify-center">
<svg className="w-8 h-8 md:w-12 md:h-12 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>
<h2 className="text-base md:text-xl font-bold text-white text-center mb-1 line-clamp-1">{currentSong.name}</h2>
<p className="text-xs md:text-sm text-zinc-400 line-clamp-1">{currentSong.artist}</p>
</div>
</div>
{/* Lyrics Content */}
<div ref={lyricsContainerRef} className="flex-1 overflow-y-auto p-4 md:p-6">
{lyrics.length > 0 ? (
<div className="space-y-4">
{lyrics.map((line, index) => (
<div
key={index}
data-index={index}
className={`text-center transition-all duration-300 ${
index === currentLyricIndex
? 'text-white text-lg font-bold scale-110'
: index === currentLyricIndex - 1 || index === currentLyricIndex + 1
? 'text-zinc-400 text-base'
: 'text-zinc-600 text-sm'
}`}
>
{line.text}
</div>
))}
</div>
) : (
<div className="text-center space-y-4">
<p className="text-zinc-500 text-sm">暂无歌词</p>
<p className="text-zinc-600 text-xs">纯音乐或歌词获取失败</p>
</div>
)}
</div>
{/* Mini Player Controls */}
<div className="border-t border-white/5 p-3 md:p-4 shrink-0">
{/* 上排:播放控制按钮 */}
<div className="flex items-center justify-center gap-4 md:gap-6 mb-2 md:mb-3">
<button onClick={playPrev} className="text-zinc-500 hover:text-white transition-colors">
<svg className="w-5 h-5 md:w-6 md:h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" />
</svg>
</button>
<button
onClick={togglePlay}
className="w-10 h-10 md:w-12 md:h-12 rounded-full bg-green-500 text-white flex items-center justify-center hover:bg-green-600 transition-colors"
>
{isPlaying ? (
<svg className="w-4 h-4 md:w-5 md:h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
</svg>
) : (
<svg className="w-4 h-4 md:w-5 md:h-5 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
)}
</button>
<button onClick={playNext} className="text-zinc-500 hover:text-white transition-colors">
<svg className="w-5 h-5 md:w-6 md:h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" />
</svg>
</button>
</div>
{/* 下排:其他按钮(小一号) */}
<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"
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="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"
title={playMode === 'loop' ? '列表循环' : playMode === 'single' ? '单曲循环' : '随机播放'}
>
{playMode === 'loop' && (
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)}
{playMode === 'single' && (
<div className="relative w-4 h-4 md:w-5 md:h-5">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span className="absolute inset-0 flex items-center justify-center text-[7px] md:text-[8px] font-bold">1</span>
</div>
)}
{playMode === 'random' && (
<svg className="w-4 h-4 md:w-5 md:h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/>
</svg>
)}
</button>
{/* 音量控制 */}
<div className="relative group">
<button
onClick={(e) => {
e.stopPropagation();
setShowVolumeSlider(!showVolumeSlider);
}}
className="text-zinc-500 hover:text-white transition-colors"
title="音量"
>
<svg className="w-4 h-4 md:w-5 md:h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clipRule="evenodd" />
</svg>
</button>
{/* 垂直音量条 - 桌面悬浮/移动端点击 */}
<div
className={`absolute bottom-full left-1/2 -translate-x-1/2 pb-2 transition-opacity md:opacity-0 md:group-hover:opacity-100 md:pointer-events-auto ${showVolumeSlider ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
onClick={(e) => e.stopPropagation()}
>
<div className="bg-zinc-800/95 backdrop-blur-sm rounded-lg p-3 shadow-xl border border-white/10">
<div className="flex flex-col items-center gap-2">
<span className="text-xs text-zinc-400 font-mono">{volume}</span>
<div className="h-24 w-1 bg-white/10 rounded-full relative">
<div
className="absolute bottom-0 left-0 right-0 bg-green-500 rounded-full transition-all pointer-events-none"
style={{ height: `${volume}%` }}
/>
<input
type="range"
min="0"
max="100"
value={volume}
onChange={handleVolumeChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer [writing-mode:bt-lr] [-webkit-appearance:slider-vertical]"
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 进度条 */}
<div>
<div className="flex items-center gap-2 text-xs text-zinc-500">
<span>{formatTime(currentTime)}</span>
<div className="flex-1 h-1 bg-white/10 rounded-full overflow-hidden relative">
<div
className="h-full bg-green-500 transition-all pointer-events-none"
style={{ width: `${progress}%` }}
/>
<input
type="range"
min="0"
max="100"
value={progress}
onChange={handleProgressChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<span>{formatTime(duration)}</span>
</div>
</div>
</div>
</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">
<div className="flex items-center gap-3">
<h2 className="text-lg font-bold text-white">播放列表</h2>
<span className="text-xs text-zinc-500">({playlist.length})</span>
</div>
<div className="flex items-center gap-2">
{playlist.length > 0 && (
<button
onClick={async () => {
if (confirm('确定要清空全部播放记录吗?')) {
try {
await clearAllMusicPlayRecords();
setPlaylist([]);
setPlayRecords([]);
setPlaylistIndex(-1);
} catch (error) {
console.error('清空播放记录失败:', error);
}
}
}}
className="px-3 py-1 text-xs rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors border border-red-500/50"
title="清空全部"
>
清空
</button>
)}
<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>
</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}`}
className={`flex items-center gap-3 p-3 rounded-lg transition-colors group ${
index === playlistIndex
? 'bg-green-500/20 border border-green-500/50'
: 'bg-white/5 hover:bg-white/10'
}`}
>
<div
onClick={() => {
setPlaylistIndex(index);
playSong(song, -1);
setShowPlaylist(false);
}}
className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer"
>
<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>
<button
onClick={async (e) => {
e.stopPropagation();
try {
const platform = song.platform || 'netease';
await deleteMusicPlayRecord(platform, song.id);
// 更新本地状态
const newPlaylist = playlist.filter((_, i) => i !== index);
const newRecords = playRecords.filter((_, i) => i !== index);
setPlaylist(newPlaylist);
setPlayRecords(newRecords);
// 如果删除的是当前播放的歌曲,调整索引
if (index === playlistIndex) {
setPlaylistIndex(-1);
} else if (index < playlistIndex) {
setPlaylistIndex(playlistIndex - 1);
}
} catch (error) {
console.error('删除播放记录失败:', error);
}
}}
className="w-8 h-8 rounded-lg bg-red-500/20 hover:bg-red-500/30 flex items-center justify-center transition-colors opacity-0 group-hover:opacity-100 shrink-0"
title="删除"
>
<svg className="w-4 h-4 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</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>
);
}