Files
Mymusic2/index.html
2026-01-06 09:46:52 +08:00

1188 lines
50 KiB
HTML
Raw Permalink 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.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#ff8c42">
<meta name="description" content="MyMusic - 现代化音乐搜索播放器">
<link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiTXlNdXNpYyIsInNob3J0X25hbWUiOiJNeU11c2ljIiwic3RhcnRfdXJsIjoiLiIsImRpc3BsYXkiOiJzdGFuZGFsb25lIiwiYmFja2dyb3VuZF9jb2xvciI6IiNmZmY4ZjIiLCJ0aGVtZV9jb2xvciI6IiNmZjhjNDIifQ==">
<title>MyMusic - 音乐搜索播放器</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome CDN -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- React 和 ReactDOM CDN -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel Standalone for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
/* 自定义滚动条样式 - 苹果风格 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #ff8c42;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #ff7722;
}
/* 毛玻璃效果 */
.glass-effect {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
/* 播放器固定底部 */
.player-fixed {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 50;
}
/* 平滑过渡 */
.smooth-transition {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 封面旋转动画 */
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.rotating {
animation: rotate 20s linear infinite;
}
.paused {
animation-play-state: paused;
}
/* 苹果风格按钮 */
.apple-button {
background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
border: none;
box-shadow: 0 4px 15px rgba(255, 140, 66, 0.3);
}
.apple-button:hover {
box-shadow: 0 6px 20px rgba(255, 140, 66, 0.4);
transform: translateY(-2px);
}
/* 卡片阴影 */
.card-shadow {
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.08);
}
/* 隐藏默认音频控件 */
audio {
display: none;
}
/* Toast通知动画 */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.toast-enter {
animation: slideInRight 0.3s ease-out;
}
.toast-exit {
animation: slideOutRight 0.3s ease-in;
}
/* 加载动画 */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* 按钮点击涟漪效果 */
.ripple {
position: relative;
overflow: hidden;
}
.ripple::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.ripple:active::after {
width: 200px;
height: 200px;
}
/* 移动端优化 */
@media (max-width: 768px) {
/* 播放器在移动端的优化 */
.player-fixed .max-w-7xl {
padding-left: 1rem;
padding-right: 1rem;
}
/* 缩小按钮间距 */
.player-fixed .flex.gap-2 {
gap: 0.25rem;
}
/* 隐藏音量控制在小屏幕 */
.volume-control {
display: none;
}
/* Toast在移动端位置调整 */
.fixed.top-20.right-4 {
top: 5rem;
right: 0.5rem;
left: 0.5rem;
}
/* 搜索栏移动端优化 */
.search-wrapper {
flex-direction: column;
}
/* 卡片在移动端的优化 */
.music-card {
padding: 0.75rem;
}
/* 专辑封面在移动端缩小 */
.album-cover-mobile {
width: 3.5rem;
height: 3.5rem;
}
}
/* 加载骨架屏 */
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 优化滚动性能 */
.scroll-smooth {
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
</style>
</head>
<body class="bg-gradient-to-br from-orange-50 to-pink-50 min-h-screen">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef, useCallback, useMemo } = React;
// ==================== API 配置 ====================
const API_BASE = 'https://music-dl.sayqz.com';
// ==================== API 请求函数 ====================
/**
* 聚合搜索音乐
* @param {string} keyword - 搜索关键词
* @returns {Promise<Array>} 音乐列表
*/
const aggregateSearch = async (keyword) => {
try {
const response = await fetch(`${API_BASE}/api/?type=aggregateSearch&keyword=${encodeURIComponent(keyword)}`);
const data = await response.json();
if (data.code === 200) {
return data.data.results || [];
}
return [];
} catch (error) {
console.error('搜索失败:', error);
return [];
}
};
/**
* 获取歌曲详细信息
* @param {string} source - 平台
* @param {string} id - 歌曲ID
* @returns {Promise<Object>} 歌曲信息
*/
const getSongInfo = async (source, id) => {
try {
const response = await fetch(`${API_BASE}/api/?source=${source}&id=${id}&type=info`);
const data = await response.json();
if (data.code === 200) {
return data.data;
}
return null;
} catch (error) {
console.error('获取歌曲信息失败:', error);
return null;
}
};
/**
* 下载单个音乐文件
* @param {Object} song - 歌曲对象
* @param {Function} showToast - Toast通知函数可选
*/
const downloadSong = (song, showToast = null) => {
const url = `${API_BASE}/api/?source=${song.platform}&id=${song.id}&type=url&br=320k`;
const a = document.createElement('a');
a.href = url;
a.download = `${song.name} - ${song.artist}.mp3`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
if (showToast) {
showToast(`开始下载: ${song.name}`, 'success');
}
};
/**
* 批量下载专辑
* @param {Array} songs - 歌曲列表
* @param {string} albumName - 专辑名
* @param {Function} showToast - Toast通知函数可选
*/
const downloadAlbum = (songs, albumName, showToast = null) => {
if (showToast) {
showToast(`开始下载专辑: ${albumName} (${songs.length}首)`, 'success');
}
songs.forEach((song, index) => {
setTimeout(() => {
downloadSong(song);
}, index * 1000); // 每秒下载一首,避免过快
});
};
const formatTime = (seconds) => {
if (!Number.isFinite(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')}`;
};
// ==================== 主应用组件 ====================
function MusicApp() {
// 状态管理
const [searchKeyword, setSearchKeyword] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const [playlist, setPlaylist] = useState([]); // 播放列表
const [currentSong, setCurrentSong] = useState(null); // 当前播放歌曲
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(0.7);
const [activeTab, setActiveTab] = useState('search'); // search | playlist
const [albumView, setAlbumView] = useState(null); // 查看专辑详情
const [playMode, setPlayMode] = useState('loop'); // loop | single | shuffle
const [toasts, setToasts] = useState([]); // Toast通知列表
const [showLyrics, setShowLyrics] = useState(false); // 显示歌词
const [lyrics, setLyrics] = useState(''); // 歌词内容
const audioRef = useRef(null);
const currentTimeRef = useRef(0);
const durationRef = useRef(0);
const progressRef = useRef(null);
const currentTimeTextRef = useRef(null);
const durationTextRef = useRef(null);
// 进度与时间显示改为直接操作 DOM避免频繁渲染
const updateCurrentTimeUI = useCallback((nextTime) => {
const safeTime = Number.isFinite(nextTime) && nextTime >= 0 ? nextTime : 0;
const safeDuration = Number.isFinite(durationRef.current) && durationRef.current >= 0
? durationRef.current
: 0;
const cappedTime = safeDuration > 0 ? Math.min(safeTime, safeDuration) : safeTime;
currentTimeRef.current = cappedTime;
if (currentTimeTextRef.current) {
currentTimeTextRef.current.textContent = formatTime(cappedTime);
}
const progress = progressRef.current;
if (progress) {
progress.max = String(safeDuration);
progress.value = String(safeDuration > 0 ? Math.min(cappedTime, safeDuration) : 0);
}
}, []);
const updateDurationUI = useCallback((nextDuration) => {
const safeDuration = Number.isFinite(nextDuration) && nextDuration >= 0 ? nextDuration : 0;
durationRef.current = safeDuration;
const safeTime = Number.isFinite(currentTimeRef.current) && currentTimeRef.current >= 0
? currentTimeRef.current
: 0;
const cappedTime = safeDuration > 0 ? Math.min(safeTime, safeDuration) : safeTime;
currentTimeRef.current = cappedTime;
if (currentTimeTextRef.current) {
currentTimeTextRef.current.textContent = formatTime(cappedTime);
}
if (durationTextRef.current) {
durationTextRef.current.textContent = formatTime(safeDuration);
}
const progress = progressRef.current;
if (progress) {
progress.max = String(safeDuration);
progress.value = String(safeDuration > 0 ? Math.min(cappedTime, safeDuration) : 0);
}
}, []);
const resetTimeUI = useCallback(() => {
currentTimeRef.current = 0;
durationRef.current = 0;
if (currentTimeTextRef.current) {
currentTimeTextRef.current.textContent = formatTime(0);
}
if (durationTextRef.current) {
durationTextRef.current.textContent = formatTime(0);
}
const progress = progressRef.current;
if (progress) {
progress.max = '0';
progress.value = '0';
}
}, []);
// ==================== Toast通知系统 ====================
/**
* 显示Toast通知
*/
const showToast = useCallback((message, type = 'info') => {
const id = Date.now();
const toast = { id, message, type };
setToasts(prev => [...prev, toast]);
// 3秒后自动移除
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
}, []);
/**
* 获取歌词
*/
const fetchLyrics = useCallback(async (song) => {
try {
const response = await fetch(`${API_BASE}/api/?source=${song.platform}&id=${song.id}&type=lrc`);
const lrcText = await response.text();
setLyrics(lrcText || '暂无歌词');
} catch (error) {
setLyrics('歌词加载失败');
}
}, []);
// ==================== 搜索处理 ====================
const handleSearch = useCallback(async () => {
if (!searchKeyword.trim()) {
showToast('请输入搜索关键词', 'warning');
return;
}
setIsSearching(true);
try {
const results = await aggregateSearch(searchKeyword);
setSearchResults(results);
if (results.length === 0) {
showToast('未找到相关音乐', 'info');
} else {
showToast(`找到 ${results.length} 首歌曲`, 'success');
}
} catch (error) {
showToast('搜索失败,请重试', 'error');
} finally {
setIsSearching(false);
}
}, [searchKeyword, showToast]);
// 回车搜索
const handleKeyPress = useCallback((e) => {
if (e.key === 'Enter') {
handleSearch();
}
}, [handleSearch]);
// ==================== 播放控制 ====================
/**
* 播放指定歌曲
*/
const playSong = useCallback((song) => {
setCurrentSong(song);
setIsPlaying(true);
showToast(`正在播放: ${song.name}`, 'success');
// 获取歌词
fetchLyrics(song);
}, [showToast, fetchLyrics]);
/**
* 添加到播放列表
*/
const addToPlaylist = useCallback((song) => {
// 检查是否已存在
setPlaylist(prev => {
const exists = prev.some(s => s.id === song.id && s.platform === song.platform);
if (!exists) {
showToast(`已添加: ${song.name}`, 'success');
return [...prev, song];
} else {
showToast('歌曲已在播放列表中', 'info');
return prev;
}
});
}, [showToast]);
/**
* 从播放列表移除
*/
const removeFromPlaylist = useCallback((song) => {
setPlaylist(prev => prev.filter(s => !(s.id === song.id && s.platform === song.platform)));
showToast('已从播放列表移除', 'info');
}, [showToast]);
/**
* 清空播放列表
*/
const clearPlaylist = useCallback(() => {
setPlaylist(prev => {
if (prev.length === 0) {
showToast('播放列表已经是空的', 'info');
return prev;
}
showToast('播放列表已清空', 'success');
return [];
});
}, [showToast]);
/**
* 播放整个播放列表
*/
const playPlaylist = useCallback(() => {
setPlaylist(prev => {
if (prev.length > 0) {
setCurrentSong(prev[0]);
setIsPlaying(true);
}
return prev;
});
}, []);
/**
* 上一曲
*/
const playPrevious = useCallback(() => {
if (!currentSong || playlist.length === 0) return;
const currentIndex = playlist.findIndex(s => s.id === currentSong.id && s.platform === currentSong.platform);
const prevIndex = currentIndex > 0 ? currentIndex - 1 : playlist.length - 1;
const nextSong = playlist[prevIndex];
setCurrentSong(nextSong);
setIsPlaying(true);
fetchLyrics(nextSong);
}, [currentSong, playlist, fetchLyrics]);
/**
* 下一曲 - 支持不同播放模式
*/
const playNext = useCallback(() => {
if (!currentSong || playlist.length === 0) return;
const currentIndex = playlist.findIndex(s => s.id === currentSong.id && s.platform === currentSong.platform);
let nextSong;
if (playMode === 'single') {
// 单曲循环 - 重新播放当前歌曲
nextSong = currentSong;
} else if (playMode === 'shuffle') {
// 随机播放
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * playlist.length);
} while (randomIndex === currentIndex && playlist.length > 1);
nextSong = playlist[randomIndex];
} else {
// 列表循环
const nextIndex = (currentIndex + 1) % playlist.length;
nextSong = playlist[nextIndex];
}
setCurrentSong(nextSong);
setIsPlaying(true);
fetchLyrics(nextSong);
}, [currentSong, playlist, playMode, fetchLyrics]);
/**
* 切换播放模式
*/
const togglePlayMode = useCallback(() => {
const modes = ['loop', 'single', 'shuffle'];
const currentModeIndex = modes.indexOf(playMode);
const nextMode = modes[(currentModeIndex + 1) % modes.length];
setPlayMode(nextMode);
const modeNames = { loop: '列表循环', single: '单曲循环', shuffle: '随机播放' };
showToast(`播放模式: ${modeNames[nextMode]}`, 'info');
}, [playMode, showToast]);
/**
* 切换播放/暂停
*/
const togglePlayPause = useCallback(() => {
if (!currentSong) return;
setIsPlaying(prev => !prev);
}, [currentSong]);
// ==================== 音频事件处理 ====================
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const updateDuration = () => {
updateDurationUI(audio.duration);
};
const handleTimeUpdate = () => {
updateCurrentTimeUI(audio.currentTime);
};
const handleEnded = () => {
playNext(); // 自动播放下一曲
};
audio.addEventListener('loadedmetadata', updateDuration);
audio.addEventListener('durationchange', updateDuration);
audio.addEventListener('timeupdate', handleTimeUpdate);
audio.addEventListener('ended', handleEnded);
return () => {
audio.removeEventListener('loadedmetadata', updateDuration);
audio.removeEventListener('durationchange', updateDuration);
audio.removeEventListener('timeupdate', handleTimeUpdate);
audio.removeEventListener('ended', handleEnded);
};
}, [playNext, updateCurrentTimeUI, updateDurationUI]);
// 当前歌曲变化时更新音频源
useEffect(() => {
if (currentSong && audioRef.current) {
const url = `${API_BASE}/api/?source=${currentSong.platform}&id=${currentSong.id}&type=url&br=320k`;
const audio = audioRef.current;
audio.src = url;
audio.currentTime = 0;
audio.volume = volume;
resetTimeUI();
audio.load();
}
}, [currentSong, resetTimeUI]);
// 控制播放/暂停
useEffect(() => {
if (!audioRef.current) return;
if (isPlaying) {
audioRef.current.play().catch(err => console.error('播放失败:', err));
} else {
audioRef.current.pause();
}
}, [isPlaying, currentSong]);
// 音量控制
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = volume;
}
}, [volume]);
// ==================== 专辑功能 ====================
/**
* 查看专辑歌曲列表
*/
const viewAlbum = useCallback((albumName) => {
const albumSongs = searchResults.filter(s => s.album === albumName);
setAlbumView({ name: albumName, songs: albumSongs });
}, [searchResults]);
/**
* 播放专辑所有歌曲
*/
const playAlbum = useCallback((albumSongs) => {
albumSongs.forEach(song => addToPlaylist(song));
if (albumSongs.length > 0) {
setCurrentSong(albumSongs[0]);
setIsPlaying(true);
}
}, [addToPlaylist]);
/**
* 获取平台图标
*/
const getPlatformIcon = (platform) => {
const icons = {
netease: '🎵',
qq: '🎶',
kuwo: '🎸'
};
return icons[platform] || '🎧';
};
// ==================== 渲染组件 ====================
/**
* 搜索栏组件
*/
const SearchBar = useMemo(() => (
<div className="mb-6">
<div className="flex gap-2">
<div className="flex-1 relative">
<input
type="text"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="搜索歌曲、歌手或专辑..."
className="w-full px-4 py-3 pl-12 rounded-2xl border-0 glass-effect focus:outline-none focus:ring-2 focus:ring-orange-400 text-gray-700 placeholder-gray-400"
/>
<i className="fas fa-search absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
<button
onClick={handleSearch}
disabled={isSearching}
className="apple-button text-white px-8 py-3 rounded-2xl font-medium smooth-transition disabled:opacity-50 ripple"
>
{isSearching ? <i className="fas fa-spinner fa-spin"></i> : ''}
</button>
</div>
</div>
), [searchKeyword, isSearching, handleSearch, handleKeyPress]);
/**
* 音乐卡片组件
*/
const MusicCard = ({ song, showActions = true }) => (
<div className="glass-effect rounded-xl p-4 card-shadow smooth-transition hover:shadow-lg mb-3">
<div className="flex items-center gap-4">
{/* 封面 */}
<img
src={song.pic || 'https://via.placeholder.com/80?text=No+Cover'}
alt={song.name}
className="w-20 h-20 rounded-lg object-cover"
onError={(e) => e.target.src = 'https://via.placeholder.com/80?text=No+Cover'}
/>
{/* 歌曲信息 */}
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-800 truncate">{song.name}</h3>
<p className="text-sm text-gray-500 truncate">{song.artist}</p>
<div className="flex items-center gap-2 mt-1">
<span
className="text-xs text-orange-600 hover:underline cursor-pointer"
onClick={() => viewAlbum(song.album)}
>
{song.album}
</span>
<span className="text-xs">{getPlatformIcon(song.platform)}</span>
</div>
</div>
{/* 操作按钮 */}
{showActions && (
<div className="flex gap-2">
<button
onClick={() => playSong(song)}
className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-orange-500 text-white flex items-center justify-center smooth-transition hover:scale-110"
title="播放"
>
<i className="fas fa-play text-sm"></i>
</button>
<button
onClick={() => addToPlaylist(song)}
className="w-10 h-10 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200"
title="添加到播放列表"
>
<i className="fas fa-plus"></i>
</button>
<button
onClick={() => downloadSong(song, showToast)}
className="w-10 h-10 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200 ripple"
title="下载"
>
<i className="fas fa-download"></i>
</button>
</div>
)}
</div>
</div>
);
/**
* 专辑视图组件
*/
const AlbumView = useMemo(() => {
if (!albumView) return null;
return (
<div className="glass-effect rounded-2xl p-6 card-shadow mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-gray-800">
<i className="fas fa-compact-disc mr-2 text-orange-500"></i>
{albumView.name}
</h2>
<button
onClick={() => setAlbumView(null)}
className="text-gray-500 hover:text-gray-700"
>
<i className="fas fa-times text-xl"></i>
</button>
</div>
<div className="flex gap-3 mb-4">
<button
onClick={() => playAlbum(albumView.songs)}
className="apple-button text-white px-6 py-2 rounded-xl smooth-transition ripple"
>
<i className="fas fa-play mr-2"></i>
</button>
<button
onClick={() => downloadAlbum(albumView.songs, albumView.name, showToast)}
className="bg-gray-100 text-gray-700 px-6 py-2 rounded-xl smooth-transition hover:bg-gray-200 ripple"
>
<i className="fas fa-download mr-2"></i>
</button>
</div>
<div className="max-h-96 overflow-y-auto scroll-smooth">
{albumView.songs.map((song, index) => (
<MusicCard key={`${song.platform}-${song.id}-${index}`} song={song} />
))}
</div>
</div>
);
}, [albumView, playAlbum, showToast]);
/**
* 加载骨架屏组件
*/
const LoadingSkeleton = useMemo(() => (
<div className="glass-effect rounded-2xl p-6 card-shadow">
<div className="h-6 w-32 skeleton rounded mb-4"></div>
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="flex items-center gap-4 mb-3 p-4">
<div className="w-20 h-20 skeleton rounded-lg"></div>
<div className="flex-1">
<div className="h-4 skeleton rounded mb-2"></div>
<div className="h-3 w-3/4 skeleton rounded"></div>
</div>
<div className="flex gap-2">
<div className="w-10 h-10 skeleton rounded-full"></div>
<div className="w-10 h-10 skeleton rounded-full"></div>
<div className="w-10 h-10 skeleton rounded-full"></div>
</div>
</div>
))}
</div>
), []);
/**
* 搜索结果列表
*/
const SearchResults = useMemo(() => (
<div>
{AlbumView}
{isSearching ? (
LoadingSkeleton
) : searchResults.length > 0 ? (
<div className="glass-effect rounded-2xl p-6 card-shadow">
<h2 className="text-xl font-bold text-gray-800 mb-4">
搜索结果 ({searchResults.length})
</h2>
<div className="max-h-[calc(100vh-400px)] overflow-y-auto scroll-smooth">
{searchResults.map((song, index) => (
<MusicCard key={`${song.platform}-${song.id}-${index}`} song={song} />
))}
</div>
</div>
) : (
<div className="glass-effect rounded-2xl p-12 card-shadow text-center text-gray-400">
<i className="fas fa-music text-6xl mb-4 loading-pulse"></i>
<p className="text-lg">搜索你喜欢的音乐</p>
<p className="text-sm mt-2">支持网易云QQ音乐酷我音乐</p>
</div>
)}
</div>
), [AlbumView, isSearching, searchResults, LoadingSkeleton]);
/**
* 播放列表视图
*/
const PlaylistView = useMemo(() => (
<div className="glass-effect rounded-2xl p-6 card-shadow">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-800">
我的播放列表 ({playlist.length})
</h2>
<div className="flex gap-2">
{playlist.length > 0 && (
<>
<button
onClick={playPlaylist}
className="apple-button text-white px-4 py-2 rounded-xl text-sm smooth-transition ripple"
>
<i className="fas fa-play mr-2"></i>
</button>
<button
onClick={clearPlaylist}
className="bg-red-100 text-red-600 px-4 py-2 rounded-xl text-sm smooth-transition hover:bg-red-200 ripple"
>
<i className="fas fa-trash mr-2"></i>
</button>
</>
)}
</div>
</div>
{playlist.length > 0 ? (
<div className="max-h-[calc(100vh-400px)] overflow-y-auto scroll-smooth">
{playlist.map((song, index) => (
<div key={`${song.platform}-${song.id}-${index}`} className="relative">
<MusicCard song={song} showActions={false} />
<button
onClick={() => removeFromPlaylist(song)}
className="absolute top-1/2 right-4 transform -translate-y-1/2 w-8 h-8 rounded-full bg-red-100 text-red-600 flex items-center justify-center smooth-transition hover:bg-red-200"
>
<i className="fas fa-times text-sm"></i>
</button>
</div>
))}
</div>
) : (
<div className="text-center text-gray-400 py-12">
<i className="fas fa-list-music text-6xl mb-4"></i>
<p className="text-lg">播放列表为空</p>
<p className="text-sm mt-2">添加你喜欢的歌曲到播放列表</p>
</div>
)}
</div>
), [playlist, playPlaylist, clearPlaylist, removeFromPlaylist]);
/**
* Toast通知组件
*/
const ToastContainer = useMemo(() => (
<div className="fixed top-20 right-4 z-50 space-y-2">
{toasts.map(toast => {
const colors = {
success: 'bg-green-500',
error: 'bg-red-500',
warning: 'bg-yellow-500',
info: 'bg-blue-500'
};
return (
<div
key={toast.id}
className={`${colors[toast.type]} text-white px-6 py-3 rounded-xl shadow-lg toast-enter flex items-center gap-2 max-w-sm`}
>
<i className={`fas fa-${
toast.type === 'success' ? 'check-circle' :
toast.type === 'error' ? 'exclamation-circle' :
toast.type === 'warning' ? 'exclamation-triangle' :
'info-circle'
}`}></i>
<span>{toast.message}</span>
</div>
);
})}
</div>
), [toasts]);
/**
* 歌词显示组件
*/
const LyricsPanel = () => {
if (!showLyrics || !currentSong) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex items-center justify-center p-4" onClick={() => setShowLyrics(false)}>
<div className="glass-effect rounded-2xl p-6 max-w-2xl w-full max-h-[80vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold text-gray-800">
<i className="fas fa-music mr-2 text-orange-500"></i>
{currentSong.name}
</h3>
<button
onClick={() => setShowLyrics(false)}
className="w-8 h-8 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200"
>
<i className="fas fa-times"></i>
</button>
</div>
<p className="text-sm text-gray-500 mb-4">{currentSong.artist}</p>
<div className="whitespace-pre-line text-gray-700 leading-relaxed">
{lyrics || '加载中...'}
</div>
</div>
</div>
);
};
/**
* 底部播放器组件
*/
const Player = () => {
if (!currentSong) return null;
return (
<div className="player-fixed glass-effect shadow-2xl border-t border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center gap-4">
{/* 封面和歌曲信息 */}
<div className="flex items-center gap-3 flex-1 min-w-0">
<img
src={currentSong.pic || 'https://via.placeholder.com/60?text=Playing'}
alt={currentSong.name}
className={`w-14 h-14 rounded-lg object-cover ${isPlaying ? 'rotating' : 'paused'}`}
onError={(e) => e.target.src = 'https://via.placeholder.com/60?text=Playing'}
/>
<div className="min-w-0">
<h4 className="font-semibold text-gray-800 truncate">{currentSong.name}</h4>
<p className="text-sm text-gray-500 truncate">{currentSong.artist}</p>
</div>
</div>
{/* 控制按钮 */}
<div className="flex items-center gap-2">
{/* 播放模式 */}
<button
onClick={togglePlayMode}
className="w-9 h-9 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200"
title={`当前模式: ${playMode === 'loop' ? '列表循环' : playMode === 'single' ? '单曲循环' : '随机播放'}`}
>
<i className={`fas fa-${
playMode === 'loop' ? 'repeat' :
playMode === 'single' ? 'redo' :
'random'
}`}></i>
</button>
<button
onClick={playPrevious}
className="w-10 h-10 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200 ripple"
>
<i className="fas fa-step-backward"></i>
</button>
<button
onClick={togglePlayPause}
className="w-12 h-12 rounded-full apple-button text-white flex items-center justify-center smooth-transition hover:scale-110 ripple"
>
<i className={`fas fa-${isPlaying ? 'pause' : 'play'}`}></i>
</button>
<button
onClick={playNext}
className="w-10 h-10 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200 ripple"
>
<i className="fas fa-step-forward"></i>
</button>
{/* 歌词按钮 */}
<button
onClick={() => setShowLyrics(true)}
className="w-9 h-9 rounded-full bg-gray-100 text-gray-600 flex items-center justify-center smooth-transition hover:bg-gray-200"
title="显示歌词"
>
<i className="fas fa-align-left"></i>
</button>
</div>
{/* 进度条 */}
<div className="flex-1 flex items-center gap-3">
<span
ref={currentTimeTextRef}
className="text-xs text-gray-500 w-10 text-right"
>
{formatTime(currentTimeRef.current)}
</span>
<input
ref={progressRef}
type="range"
min="0"
max={durationRef.current}
defaultValue={0}
onChange={(e) => {
const nextTime = Number(e.target.value);
if (!Number.isFinite(nextTime)) return;
if (audioRef.current) {
audioRef.current.currentTime = nextTime;
}
updateCurrentTimeUI(nextTime);
}}
className="flex-1 h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-orange-500"
/>
<span
ref={durationTextRef}
className="text-xs text-gray-500 w-10"
>
{formatTime(durationRef.current)}
</span>
</div>
{/* 音量控制 */}
<div className="flex items-center gap-2 volume-control">
<i className="fas fa-volume-up text-gray-500"></i>
<input
type="range"
min="0"
max="1"
step="0.01"
value={volume}
onChange={(e) => setVolume(parseFloat(e.target.value))}
className="w-24 h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-orange-500"
/>
</div>
</div>
</div>
</div>
);
};
// ==================== 主渲染 ====================
return (
<div className="min-h-screen pb-32">
{/* Toast通知 */}
{ToastContainer}
{/* 歌词面板 */}
<LyricsPanel />
{/* 顶部导航 */}
<div className="glass-effect sticky top-0 z-40 border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between mb-4">
<h1 className="text-3xl font-bold bg-gradient-to-r from-orange-500 to-pink-500 bg-clip-text text-transparent">
<i className="fas fa-music mr-2"></i>
MyMusic
</h1>
<div className="flex gap-2">
<button
onClick={() => setActiveTab('search')}
className={`px-6 py-2 rounded-xl smooth-transition font-medium ripple ${
activeTab === 'search'
? 'apple-button text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<i className="fas fa-search mr-2"></i>
</button>
<button
onClick={() => setActiveTab('playlist')}
className={`px-6 py-2 rounded-xl smooth-transition font-medium ripple ${
activeTab === 'playlist'
? 'apple-button text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<i className="fas fa-list mr-2"></i>
{playlist.length > 0 && (
<span className="ml-1 px-2 py-0.5 bg-white bg-opacity-30 rounded-full text-xs">
{playlist.length}
</span>
)}
</button>
</div>
</div>
{activeTab === 'search' && SearchBar}
</div>
</div>
{/* 主内容区 */}
<div className="max-w-7xl mx-auto px-4 py-6">
{activeTab === 'search' ? SearchResults : PlaylistView}
</div>
{/* 底部播放器 */}
<Player />
{/* 隐藏的音频元素 */}
<audio ref={audioRef} preload="metadata" />
</div>
);
}
// ==================== 渲染到DOM ====================
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<MusicApp />);
</script>
</body>
</html>