From 68b1332df1dde6d79fe82b2d598766ca63c60c30 Mon Sep 17 00:00:00 2001 From: mtvpls Date: Mon, 2 Feb 2026 12:44:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=8C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/002_add_music_play_records.sql | 43 +- src/app/api/music/playlists/route.ts | 200 ++++++ src/app/api/music/playlists/songs/route.ts | 187 ++++++ src/app/api/music/playrecords/route.ts | 123 ++-- src/app/music/page.tsx | 670 ++++++++++++++++++++- src/components/AddToPlaylistModal.tsx | 280 +++++++++ src/lib/d1.db.ts | 301 +++++++++ src/lib/db.ts | 103 ++++ src/lib/redis-base.db.ts | 228 +++++++ src/lib/types.ts | 1 + 10 files changed, 2089 insertions(+), 47 deletions(-) create mode 100644 src/app/api/music/playlists/route.ts create mode 100644 src/app/api/music/playlists/songs/route.ts create mode 100644 src/components/AddToPlaylistModal.tsx diff --git a/migrations/002_add_music_play_records.sql b/migrations/002_add_music_play_records.sql index 4771147..1647afa 100644 --- a/migrations/002_add_music_play_records.sql +++ b/migrations/002_add_music_play_records.sql @@ -1,7 +1,8 @@ -- ============================================ --- MoonTV Plus - 音乐播放记录表 --- 版本: 1.1.0 +-- MoonTV Plus - 音乐模块数据表 +-- 版本: 1.2.0 -- 创建时间: 2026-02-01 +-- 更新时间: 2026-02-02 -- ============================================ -- 音乐播放记录表 @@ -25,3 +26,41 @@ CREATE TABLE IF NOT EXISTS music_play_records ( CREATE INDEX IF NOT EXISTS idx_music_play_records_username ON music_play_records(username); CREATE INDEX IF NOT EXISTS idx_music_play_records_save_time ON music_play_records(username, save_time DESC); CREATE INDEX IF NOT EXISTS idx_music_play_records_platform ON music_play_records(username, platform); + +-- ============================================ +-- 音乐歌单表 +-- ============================================ + +-- 音乐歌单表 +CREATE TABLE IF NOT EXISTS music_playlists ( + id TEXT NOT NULL, -- 歌单ID (UUID) + username TEXT NOT NULL, -- 用户名 + name TEXT NOT NULL, -- 歌单名称 + description TEXT, -- 歌单描述(可选) + cover TEXT, -- 歌单封面(可选,使用第一首歌的封面) + created_at INTEGER NOT NULL, -- 创建时间戳 + updated_at INTEGER NOT NULL, -- 更新时间戳 + PRIMARY KEY (id), + FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE +); + +-- 音乐歌单歌曲关联表 +CREATE TABLE IF NOT EXISTS music_playlist_songs ( + playlist_id TEXT NOT NULL, -- 歌单ID + platform TEXT NOT NULL CHECK(platform IN ('netease', 'qq', 'kuwo')), -- 音乐平台 + song_id TEXT NOT NULL, -- 歌曲ID + name TEXT NOT NULL, -- 歌曲名 + artist TEXT NOT NULL, -- 艺术家 + album TEXT, -- 专辑(可选) + pic TEXT, -- 封面图URL(可选) + duration REAL NOT NULL DEFAULT 0, -- 总时长(秒) + added_at INTEGER NOT NULL, -- 添加时间戳 + sort_order INTEGER NOT NULL DEFAULT 0, -- 排序顺序 + PRIMARY KEY (playlist_id, platform, song_id), + FOREIGN KEY (playlist_id) REFERENCES music_playlists(id) ON DELETE CASCADE +); + +-- 创建索引以提高查询性能 +CREATE INDEX IF NOT EXISTS idx_music_playlists_username ON music_playlists(username, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_music_playlist_songs_playlist ON music_playlist_songs(playlist_id, sort_order ASC); +CREATE INDEX IF NOT EXISTS idx_music_playlist_songs_added_at ON music_playlist_songs(playlist_id, added_at DESC); diff --git a/src/app/api/music/playlists/route.ts b/src/app/api/music/playlists/route.ts new file mode 100644 index 0000000..7150d4c --- /dev/null +++ b/src/app/api/music/playlists/route.ts @@ -0,0 +1,200 @@ +/* eslint-disable no-console */ + +import { NextRequest, NextResponse } from 'next/server'; +import { randomUUID } from 'crypto'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export const runtime = 'nodejs'; + +// GET - 获取用户的所有歌单 +export async function GET(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const playlists = await db.getUserMusicPlaylists(authInfo.username); + + return NextResponse.json({ playlists }); + } catch (error) { + console.error('GET /api/music/playlists error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// POST - 创建新歌单 +export async function POST(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const body = await request.json(); + const { name, description } = body; + + if (!name || typeof name !== 'string' || name.trim().length === 0) { + return NextResponse.json( + { error: '歌单名称不能为空' }, + { status: 400 } + ); + } + + const playlistId = randomUUID(); + + await db.createMusicPlaylist(authInfo.username, { + id: playlistId, + name: name.trim(), + description: description?.trim(), + }); + + const playlist = await db.getMusicPlaylist(playlistId); + + return NextResponse.json({ playlist }); + } catch (error) { + console.error('POST /api/music/playlists error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// PUT - 更新歌单信息 +export async function PUT(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const body = await request.json(); + const { playlistId, name, description, cover } = body; + + if (!playlistId) { + return NextResponse.json( + { error: '歌单ID不能为空' }, + { status: 400 } + ); + } + + // 检查歌单是否存在且属于当前用户 + const playlist = await db.getMusicPlaylist(playlistId); + if (!playlist) { + return NextResponse.json({ error: '歌单不存在' }, { status: 404 }); + } + if (playlist.username !== authInfo.username) { + return NextResponse.json({ error: '无权限操作此歌单' }, { status: 403 }); + } + + const updates: any = {}; + if (name !== undefined) updates.name = name.trim(); + if (description !== undefined) updates.description = description?.trim(); + if (cover !== undefined) updates.cover = cover; + + await db.updateMusicPlaylist(playlistId, updates); + + const updatedPlaylist = await db.getMusicPlaylist(playlistId); + + return NextResponse.json({ playlist: updatedPlaylist }); + } catch (error) { + console.error('PUT /api/music/playlists error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// DELETE - 删除歌单 +export async function DELETE(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const { searchParams } = new URL(request.url); + const playlistId = searchParams.get('playlistId'); + + if (!playlistId) { + return NextResponse.json( + { error: '歌单ID不能为空' }, + { status: 400 } + ); + } + + // 检查歌单是否存在且属于当前用户 + const playlist = await db.getMusicPlaylist(playlistId); + if (!playlist) { + return NextResponse.json({ error: '歌单不存在' }, { status: 404 }); + } + if (playlist.username !== authInfo.username) { + return NextResponse.json({ error: '无权限操作此歌单' }, { status: 403 }); + } + + await db.deleteMusicPlaylist(playlistId); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('DELETE /api/music/playlists error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/music/playlists/songs/route.ts b/src/app/api/music/playlists/songs/route.ts new file mode 100644 index 0000000..f460bb0 --- /dev/null +++ b/src/app/api/music/playlists/songs/route.ts @@ -0,0 +1,187 @@ +/* eslint-disable no-console */ + +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { db } from '@/lib/db'; + +export const runtime = 'nodejs'; + +// GET - 获取歌单中的所有歌曲 +export async function GET(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const { searchParams } = new URL(request.url); + const playlistId = searchParams.get('playlistId'); + + if (!playlistId) { + return NextResponse.json( + { error: '歌单ID不能为空' }, + { status: 400 } + ); + } + + // 检查歌单是否存在且属于当前用户 + const playlist = await db.getMusicPlaylist(playlistId); + if (!playlist) { + return NextResponse.json({ error: '歌单不存在' }, { status: 404 }); + } + if (playlist.username !== authInfo.username) { + return NextResponse.json({ error: '无权限访问此歌单' }, { status: 403 }); + } + + const songs = await db.getPlaylistSongs(playlistId); + + return NextResponse.json({ songs }); + } catch (error) { + console.error('GET /api/music/playlists/songs error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// POST - 添加歌曲到歌单 +export async function POST(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const body = await request.json(); + const { playlistId, song } = body; + + if (!playlistId) { + return NextResponse.json( + { error: '歌单ID不能为空' }, + { status: 400 } + ); + } + + if (!song || !song.platform || !song.id || !song.name || !song.artist) { + return NextResponse.json( + { error: '歌曲信息不完整' }, + { status: 400 } + ); + } + + // 检查歌单是否存在且属于当前用户 + const playlist = await db.getMusicPlaylist(playlistId); + if (!playlist) { + return NextResponse.json({ error: '歌单不存在' }, { status: 404 }); + } + if (playlist.username !== authInfo.username) { + return NextResponse.json({ error: '无权限操作此歌单' }, { status: 403 }); + } + + // 检查歌曲是否已在歌单中 + const exists = await db.isSongInPlaylist(playlistId, song.platform, song.id); + if (exists) { + return NextResponse.json( + { error: '歌曲已在歌单中' }, + { status: 400 } + ); + } + + await db.addSongToPlaylist(playlistId, { + platform: song.platform, + id: song.id, + name: song.name, + artist: song.artist, + album: song.album, + pic: song.pic, + duration: song.duration || 0, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('POST /api/music/playlists/songs error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// DELETE - 从歌单中移除歌曲 +export async function DELETE(request: NextRequest) { + try { + // 从 cookie 获取用户信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 检查用户状态 + if (authInfo.username !== process.env.USERNAME) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { + return NextResponse.json({ error: '用户不存在' }, { status: 401 }); + } + if (userInfoV2.banned) { + return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); + } + } + + const { searchParams } = new URL(request.url); + const playlistId = searchParams.get('playlistId'); + const platform = searchParams.get('platform'); + const songId = searchParams.get('songId'); + + if (!playlistId || !platform || !songId) { + return NextResponse.json( + { error: '参数不完整' }, + { status: 400 } + ); + } + + // 检查歌单是否存在且属于当前用户 + const playlist = await db.getMusicPlaylist(playlistId); + if (!playlist) { + return NextResponse.json({ error: '歌单不存在' }, { status: 404 }); + } + if (playlist.username !== authInfo.username) { + return NextResponse.json({ error: '无权限操作此歌单' }, { status: 403 }); + } + + await db.removeSongFromPlaylist(playlistId, platform, songId); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('DELETE /api/music/playlists/songs error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/music/playrecords/route.ts b/src/app/api/music/playrecords/route.ts index 8ecc0bc..d27090c 100644 --- a/src/app/api/music/playrecords/route.ts +++ b/src/app/api/music/playrecords/route.ts @@ -81,44 +81,95 @@ export async function POST(request: NextRequest) { } const body = await request.json(); - const { key, record }: { key: string; record: MusicPlayRecord } = body; - if (!key || !record) { - return NextResponse.json( - { error: 'Missing key or record' }, - { status: 400 } - ); + // 检查是否是批量添加 + if (Array.isArray(body.records)) { + // 批量添加 + const records: Array<{ platform: string; id: string; record: MusicPlayRecord }> = []; + + for (const item of body.records) { + const { key, record } = item; + if (!key || !record) { + return NextResponse.json( + { error: 'Missing key or record in batch item' }, + { status: 400 } + ); + } + + // 验证音乐播放记录数据 + if (!record.platform || !record.id || !record.name || !record.artist) { + return NextResponse.json( + { error: 'Invalid record data in batch item' }, + { status: 400 } + ); + } + + // 从key中解析platform和id + const [platform, id] = key.split('+'); + if (!platform || !id) { + return NextResponse.json( + { error: 'Invalid key format in batch item' }, + { status: 400 } + ); + } + + records.push({ platform, id, record }); + + // 缓存歌曲信息到服务器内存 + setCachedSong(platform, id, { + id: record.id, + name: record.name, + artist: record.artist, + album: record.album, + pic: record.pic, + }); + } + + // 批量保存到数据库 + await db.batchSaveMusicPlayRecords(authInfo.username, records); + + return NextResponse.json({ success: true, count: records.length }, { status: 200 }); + } else { + // 单个添加(保持向后兼容) + const { key, record }: { key: string; record: MusicPlayRecord } = body; + + if (!key || !record) { + return NextResponse.json( + { error: 'Missing key or record' }, + { status: 400 } + ); + } + + // 验证音乐播放记录数据 + if (!record.platform || !record.id || !record.name || !record.artist) { + return NextResponse.json( + { error: 'Invalid record data' }, + { status: 400 } + ); + } + + // 从key中解析platform和id + const [platform, id] = key.split('+'); + if (!platform || !id) { + return NextResponse.json( + { error: 'Invalid key format' }, + { status: 400 } + ); + } + + await db.saveMusicPlayRecord(authInfo.username, platform, id, record); + + // 缓存歌曲信息到服务器内存 + setCachedSong(platform, id, { + id: record.id, + name: record.name, + artist: record.artist, + album: record.album, + pic: record.pic, + }); + + return NextResponse.json({ success: true }, { status: 200 }); } - - // 验证音乐播放记录数据 - if (!record.platform || !record.id || !record.name || !record.artist) { - return NextResponse.json( - { error: 'Invalid record data' }, - { status: 400 } - ); - } - - // 从key中解析platform和id - const [platform, id] = key.split('+'); - if (!platform || !id) { - return NextResponse.json( - { error: 'Invalid key format' }, - { status: 400 } - ); - } - - await db.saveMusicPlayRecord(authInfo.username, platform, id, record); - - // 缓存歌曲信息到服务器内存 - setCachedSong(platform, id, { - id: record.id, - name: record.name, - artist: record.artist, - album: record.album, - pic: record.pic, - }); - - return NextResponse.json({ success: true }, { status: 200 }); } catch (err) { console.error('保存音乐播放记录失败', err); return NextResponse.json( diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx index a3a1d9d..f14f056 100644 --- a/src/app/music/page.tsx +++ b/src/app/music/page.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; import { getAllMusicPlayRecords, saveMusicPlayRecord, @@ -10,6 +11,8 @@ import { deleteMusicPlayRecord, clearAllMusicPlayRecords, } from '@/lib/db.client'; +import AddToPlaylistModal from '@/components/AddToPlaylistModal'; +import Toast, { ToastProps } from '@/components/Toast'; interface Song { id: string; @@ -17,7 +20,7 @@ interface Song { artist: string; album?: string; pic?: string; - platform?: 'netease' | 'qq' | 'kuwo'; // 添加平台信息 + platform: 'netease' | 'qq' | 'kuwo'; // 添加平台信息 } interface PlayRecord { @@ -57,7 +60,7 @@ export default function MusicPage() { const [currentSource, setCurrentSource] = useState<'netease' | 'qq' | 'kuwo'>('netease'); const [playlists, setPlaylists] = useState([]); const [songs, setSongs] = useState([]); - const [currentView, setCurrentView] = useState<'playlists' | 'songs'>('playlists'); + const [currentView, setCurrentView] = useState<'playlists' | 'songs' | 'myPlaylists'>('playlists'); const [currentPlaylistTitle, setCurrentPlaylistTitle] = useState(''); const [searchKeyword, setSearchKeyword] = useState(''); const [currentSong, setCurrentSong] = useState(null); @@ -81,6 +84,33 @@ export default function MusicPage() { const [showQualityMenu, setShowQualityMenu] = useState(false); // 音质选择菜单 const [showVolumeSlider, setShowVolumeSlider] = useState(false); // 音量滑块显示状态 const [pendingSongToPlay, setPendingSongToPlay] = useState<{ platform: string; id: string } | null>(null); // 待播放的歌曲信息 + const [showAddToPlaylistModal, setShowAddToPlaylistModal] = useState(false); // 添加到歌单弹窗 + const [songToAddToPlaylist, setSongToAddToPlaylist] = useState(null); // 要添加到歌单的歌曲 + + // 我的歌单相关状态 + const [userPlaylists, setUserPlaylists] = useState([]); + const [selectedUserPlaylist, setSelectedUserPlaylist] = useState(null); + const [userPlaylistSongs, setUserPlaylistSongs] = useState([]); + const [loadingUserPlaylists, setLoadingUserPlaylists] = useState(false); + const [loadingUserPlaylistSongs, setLoadingUserPlaylistSongs] = useState(false); + const [loadingPlayAll, setLoadingPlayAll] = useState(false); // 播放全部加载状态 + const [deletingPlaylistId, setDeletingPlaylistId] = useState(null); // 正在删除的歌单ID + + // Toast 和 Confirm Modal 状态 + const [toast, setToast] = useState(null); + const [confirmModal, setConfirmModal] = useState<{ + isOpen: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; + }>({ + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + onCancel: () => {}, + }); const audioRef = useRef(null); const lyricsContainerRef = useRef(null); @@ -358,8 +388,272 @@ export default function MusicPage() { } }; + // 打开添加到歌单弹窗 + const handleAddToPlaylist = (song: Song, e: React.MouseEvent) => { + e.stopPropagation(); // 阻止事件冒泡,避免触发播放 + setSongToAddToPlaylist(song); + setShowAddToPlaylistModal(true); + }; + + // 加载用户歌单列表 + const loadUserPlaylists = async () => { + try { + setLoadingUserPlaylists(true); + const response = await fetch('/api/music/playlists'); + if (response.ok) { + const data = await response.json(); + setUserPlaylists(data.playlists || []); + } + } catch (error) { + console.error('加载歌单失败:', error); + } finally { + setLoadingUserPlaylists(false); + } + }; + + // 加载歌单中的歌曲 + const loadUserPlaylistSongs = async (playlistId: string) => { + try { + setLoadingUserPlaylistSongs(true); + const response = await fetch(`/api/music/playlists/songs?playlistId=${playlistId}`); + if (response.ok) { + const data = await response.json(); + setUserPlaylistSongs(data.songs || []); + } + } catch (error) { + console.error('加载歌单歌曲失败:', error); + } finally { + setLoadingUserPlaylistSongs(false); + } + }; + + // 选择歌单 + const handleSelectUserPlaylist = (playlist: any) => { + setSelectedUserPlaylist(playlist); + loadUserPlaylistSongs(playlist.id); + }; + + // 播放全部歌单歌曲 + const handlePlayAllPlaylist = async () => { + if (!selectedUserPlaylist || userPlaylistSongs.length === 0) { + setToast({ + message: '歌单为空', + type: 'error', + onClose: () => setToast(null), + }); + return; + } + + setLoadingPlayAll(true); + try { + // 1. 清空所有播放历史 + await clearAllMusicPlayRecords(); + + // 2. 清空本地状态 + setPlayRecords([]); + setPlaylist([]); + + // 3. 批量添加歌单中的所有歌曲到播放历史 + const baseTime = Date.now(); + const recordsToAdd = userPlaylistSongs.map((song, i) => ({ + key: `${song.platform}+${song.id}`, + record: { + platform: song.platform, + id: song.id, + name: song.name, + artist: song.artist, + album: song.album, + pic: song.pic, + play_time: 0, + duration: song.duration || 0, + save_time: baseTime + i, // 使用递增的时间戳 + }, + })); + + // 一次性批量添加所有歌曲 + const response = await fetch('/api/music/playrecords', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + records: recordsToAdd, + }), + }); + + if (!response.ok) { + throw new Error('批量添加歌曲失败'); + } + + // 4. 立即更新本地状态 + const newRecords: PlayRecord[] = userPlaylistSongs.map((song, i) => ({ + platform: song.platform, + id: song.id, + playTime: 0, + duration: song.duration || 0, + timestamp: baseTime + i, + })); + + const newPlaylist: Song[] = userPlaylistSongs.map((song) => ({ + id: song.id, + name: song.name, + artist: song.artist, + album: song.album, + pic: song.pic, + platform: song.platform, + })); + + setPlayRecords(newRecords); + setPlaylist(newPlaylist); + + // 5. 直接播放第一首歌 + if (userPlaylistSongs.length > 0) { + console.log('播放全部: 准备播放第一首歌', userPlaylistSongs[0]); + setPlaylistIndex(0); + await playSong(userPlaylistSongs[0], 0); + } + + setToast({ + message: `已将 ${userPlaylistSongs.length} 首歌曲添加到播放列表`, + type: 'success', + onClose: () => setToast(null), + }); + } catch (error) { + console.error('播放全部失败:', error); + setToast({ + message: '播放全部失败', + type: 'error', + onClose: () => setToast(null), + }); + } finally { + setLoadingPlayAll(false); + } + }; + + // 删除歌单 + const handleDeleteUserPlaylist = async (playlistId: string) => { + setConfirmModal({ + isOpen: true, + title: '确认删除', + message: '确定要删除这个歌单吗?', + onConfirm: async () => { + // 先关闭确认框 + setConfirmModal({ + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + onCancel: () => {}, + }); + + // 然后执行删除 + setDeletingPlaylistId(playlistId); + try { + const response = await fetch(`/api/music/playlists?playlistId=${playlistId}`, { + method: 'DELETE', + }); + + if (response.ok) { + setToast({ + message: '删除成功', + type: 'success', + onClose: () => setToast(null), + }); + if (selectedUserPlaylist?.id === playlistId) { + setSelectedUserPlaylist(null); + setUserPlaylistSongs([]); + } + loadUserPlaylists(); + } else { + const data = await response.json(); + setToast({ + message: data.error || '删除失败', + type: 'error', + onClose: () => setToast(null), + }); + } + } catch (error) { + console.error('删除歌单失败:', error); + setToast({ + message: '删除歌单失败', + type: 'error', + onClose: () => setToast(null), + }); + } finally { + setDeletingPlaylistId(null); + } + }, + onCancel: () => { + setConfirmModal({ + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + onCancel: () => {}, + }); + }, + }); + }; + + // 从歌单中移除歌曲 + const handleRemoveSongFromUserPlaylist = async (song: any) => { + if (!selectedUserPlaylist) return; + + setConfirmModal({ + isOpen: true, + title: '确认移除', + message: `确定要从歌单中移除 "${song.name}" 吗?`, + onConfirm: async () => { + try { + const response = await fetch( + `/api/music/playlists/songs?playlistId=${selectedUserPlaylist.id}&platform=${song.platform}&songId=${song.id}`, + { method: 'DELETE' } + ); + + if (response.ok) { + setToast({ + message: '移除成功', + type: 'success', + onClose: () => setToast(null), + }); + loadUserPlaylistSongs(selectedUserPlaylist.id); + } else { + const data = await response.json(); + setToast({ + message: data.error || '移除失败', + type: 'error', + onClose: () => setToast(null), + }); + } + } catch (error) { + console.error('移除歌曲失败:', error); + setToast({ + message: '移除歌曲失败', + type: 'error', + onClose: () => setToast(null), + }); + } + setConfirmModal({ + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + onCancel: () => {}, + }); + }, + onCancel: () => { + setConfirmModal({ + isOpen: false, + title: '', + message: '', + onConfirm: () => {}, + onCancel: () => {}, + }); + }, + }); + }; + // 播放歌曲 const playSong = async (song: Song, index: number) => { + console.log('playSong 被调用:', song, 'index:', index); try { // 使用歌曲自己的平台信息,如果没有则使用当前选择的平台 const platform = song.platform || currentSource; @@ -373,6 +667,8 @@ export default function MusicPage() { setShowPlayer(true); setLyrics([]); // 清空旧歌词 + console.log('已设置 showPlayer=true, currentSong=', song); + // 添加到播放记录和播放列表 const record: PlayRecord = { platform: platform, @@ -596,6 +892,10 @@ export default function MusicPage() { if (currentView === 'songs') { setCurrentView('playlists'); setSongs([]); + } else if (currentView === 'myPlaylists') { + setCurrentView('playlists'); + setSelectedUserPlaylist(null); + setUserPlaylistSongs([]); } else { router.back(); } @@ -774,6 +1074,13 @@ export default function MusicPage() { loadPlaylists(currentSource); }, [currentSource]); + // 当切换到我的歌单视图时加载歌单列表 + useEffect(() => { + if (currentView === 'myPlaylists') { + loadUserPlaylists(); + } + }, [currentView]); + // 歌词自动滚动 useEffect(() => { if (lyricsContainerRef.current && currentLyricIndex >= 0) { @@ -840,6 +1147,7 @@ export default function MusicPage() { return (
+ <> {/* Header */}
@@ -895,7 +1203,7 @@ export default function MusicPage() {
- {currentView === 'songs' && ( + {(currentView === 'songs' || currentView === 'myPlaylists') && (
+ @@ -986,29 +1303,230 @@ export default function MusicPage() { {songs.map((song, index) => (
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 ${ + className={`grid grid-cols-[40px_1fr_auto_auto] md:grid-cols-[50px_2fr_1fr_auto_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' }`} > -
{index + 1}
-
+
playSong(song, index)} + > + {index + 1} +
+
playSong(song, index)} + >
{song.name}
{song.artist}
-
{song.artist}
-
{getSourceLabel()}
+
playSong(song, index)} + > + {song.artist} +
+
playSong(song, index)} + > + {getSourceLabel()} +
+
))}
)} + + {/* My Playlists View */} + {currentView === 'myPlaylists' && ( +
+ {/* Playlists List */} +
+
+

歌单列表

+ {loadingUserPlaylists ? ( +
加载中...
+ ) : userPlaylists.length === 0 ? ( +
+ 还没有歌单 +
+ +
+ ) : ( +
+ {userPlaylists.map((playlist) => ( +
handleSelectUserPlaylist(playlist)} + > +
+ {playlist.cover ? ( + {playlist.name} + ) : ( +
+ + + +
+ )} +
+
{playlist.name}
+ {playlist.description && ( +
{playlist.description}
+ )} +
+
+
+ ))} +
+ )} +
+
+ + {/* Playlist Songs */} +
+ {selectedUserPlaylist ? ( +
+
+
+

{selectedUserPlaylist.name}

+ {selectedUserPlaylist.description && ( +

{selectedUserPlaylist.description}

+ )} +
+
+ + +
+
+ + {loadingUserPlaylistSongs ? ( +
加载中...
+ ) : userPlaylistSongs.length === 0 ? ( +
歌单为空
+ ) : ( +
+ {userPlaylistSongs.map((song, index) => ( +
+
{index + 1}
+ {song.pic && ( + {song.name} + )} +
+
{song.name}
+
{song.artist}
+
+ + +
+ ))} +
+ )} +
+ ) : ( +
+
+ + + +

选择一个歌单查看详情

+
+
+ )} +
+
+ )} {/* Player */} + {console.log('渲染检查: showPlayer=', showPlayer, 'currentSong=', currentSong)} {showPlayer && currentSong && (
@@ -1095,6 +1613,21 @@ export default function MusicPage() { className="w-16 h-1 bg-white/10 rounded-full appearance-none cursor-pointer" />
+
+ {/* 添加到歌单按钮 */} + {/* 进度条 */} @@ -1651,6 +2200,109 @@ export default function MusicPage() { )} + + {/* Add to Playlist Modal */} + { + setShowAddToPlaylistModal(false); + setSongToAddToPlaylist(null); + }} + onSuccess={() => { + setToast({ + message: '已添加到歌单', + type: 'success', + onClose: () => setToast(null), + }); + }} + onError={(message) => { + setToast({ + message, + type: 'error', + onClose: () => setToast(null), + }); + }} + /> + + {/* Toast */} + {toast && } + + {/* Confirm Modal */} + {confirmModal.isOpen && + createPortal( +
+
e.stopPropagation()} + > +
+
+

+ {confirmModal.title} +

+ +
+ +
+

+ {confirmModal.message} +

+
+ +
+ + +
+
+
+
, + document.body + )} + ); } diff --git a/src/components/AddToPlaylistModal.tsx b/src/components/AddToPlaylistModal.tsx new file mode 100644 index 0000000..d7d9e98 --- /dev/null +++ b/src/components/AddToPlaylistModal.tsx @@ -0,0 +1,280 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; + +import { useEffect, useState } from 'react'; + +interface Song { + id: string; + name: string; + artist: string; + album?: string; + pic?: string; + platform: 'netease' | 'qq' | 'kuwo'; + duration?: number; +} + +interface MusicPlaylist { + id: string; + username: string; + name: string; + description?: string; + cover?: string; + created_at: number; + updated_at: number; +} + +interface AddToPlaylistModalProps { + song: Song | null; + isOpen: boolean; + onClose: () => void; + onSuccess?: () => void; + onError?: (message: string) => void; +} + +export default function AddToPlaylistModal({ + song, + isOpen, + onClose, + onSuccess, + onError, +}: AddToPlaylistModalProps) { + const [playlists, setPlaylists] = useState([]); + const [loading, setLoading] = useState(false); + const [showCreateForm, setShowCreateForm] = useState(false); + const [newPlaylistName, setNewPlaylistName] = useState(''); + const [newPlaylistDescription, setNewPlaylistDescription] = useState(''); + const [creating, setCreating] = useState(false); + const [addingToPlaylistId, setAddingToPlaylistId] = useState(null); // 正在添加的歌单ID + + // 加载用户的歌单列表 + useEffect(() => { + if (isOpen) { + loadPlaylists(); + } + }, [isOpen]); + + const loadPlaylists = async () => { + try { + setLoading(true); + const response = await fetch('/api/music/playlists'); + if (response.ok) { + const data = await response.json(); + setPlaylists(data.playlists || []); + } + } catch (error) { + console.error('加载歌单失败:', error); + } finally { + setLoading(false); + } + }; + + const handleCreatePlaylist = async () => { + if (!newPlaylistName.trim()) { + onError?.('请输入歌单名称'); + return; + } + + try { + setCreating(true); + const response = await fetch('/api/music/playlists', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: newPlaylistName.trim(), + description: newPlaylistDescription.trim(), + }), + }); + + if (response.ok) { + setNewPlaylistName(''); + setNewPlaylistDescription(''); + setShowCreateForm(false); + await loadPlaylists(); + } else { + const data = await response.json(); + onError?.(data.error || '创建歌单失败'); + } + } catch (error) { + console.error('创建歌单失败:', error); + onError?.('创建歌单失败'); + } finally { + setCreating(false); + } + }; + + const handleAddToPlaylist = async (playlistId: string) => { + if (!song) return; + + try { + setAddingToPlaylistId(playlistId); + const response = await fetch('/api/music/playlists/songs', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + playlistId, + song: { + platform: song.platform, + id: song.id, + name: song.name, + artist: song.artist, + album: song.album, + pic: song.pic, + duration: song.duration || 0, + }, + }), + }); + + if (response.ok) { + onSuccess?.(); + onClose(); + } else { + const data = await response.json(); + onError?.(data.error || '添加失败'); + } + } catch (error) { + console.error('添加到歌单失败:', error); + onError?.('添加到歌单失败'); + } finally { + setAddingToPlaylistId(null); + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+

添加到歌单

+ +
+ {song && ( +
+ {song.name} - {song.artist} +
+ )} +
+ + {/* Content */} +
+ {/* Create New Playlist Button */} + {!showCreateForm && ( + + )} + + {/* Create Form */} + {showCreateForm && ( +
+ setNewPlaylistName(e.target.value)} + className="w-full px-3 py-2 bg-zinc-800 text-white rounded-lg border border-white/10 focus:border-green-500 focus:outline-none mb-2" + /> +