From 39d829eab61c8f60d095b32e52ca317ccb5917bd Mon Sep 17 00:00:00 2001 From: mtvpls Date: Thu, 26 Mar 2026 11:07:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=BC=B9=E5=B9=95=E9=80=89?= =?UTF-8?q?=E9=9B=86=E9=9D=A2=E6=9D=BF=EF=BC=8C=E5=B9=B6=E4=B8=94=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=89=8B=E5=8A=A8=E9=80=89=E6=8B=A9=E5=BC=B9=E5=B9=95?= =?UTF-8?q?=E5=9B=A0=E7=BC=93=E5=AD=98=E9=97=AE=E9=A2=98=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E5=BC=B9=E5=B9=95=E9=9B=86=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/play/page.tsx | 12 +- src/components/DanmakuPanel.tsx | 275 ++++++++++++++++++++++++-------- src/lib/danmaku/api.ts | 7 +- 3 files changed, 228 insertions(+), 66 deletions(-) diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index d0b8968..2284e28 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -4299,6 +4299,7 @@ function PlayPageClient() { episodeTitle?: string; searchKeyword?: string; danmakuCount?: number; + bypassCache?: boolean; }) => { if (!danmakuPluginRef.current) { console.warn('弹幕插件未初始化'); @@ -4328,7 +4329,13 @@ function PlayPageClient() { console.log(`[弹幕加载] episodeId=${episodeId}, title="${title}", episodeIndex=${episodeIndex}`); - const comments = await getDanmakuById(episodeId, title, episodeIndex, metadata); + const comments = await getDanmakuById( + episodeId, + title, + episodeIndex, + { bypassCache: metadata?.bypassCache === true }, + metadata + ); if (comments.length === 0) { console.warn('未获取到弹幕数据'); @@ -4491,6 +4498,7 @@ function PlayPageClient() { episode.episodeId, title, nextEpisodeIndex, + undefined, { animeId: savedAnimeId, animeTitle: episodesResult.bangumi.animeTitle, @@ -4542,6 +4550,7 @@ function PlayPageClient() { episode.episodeId, title, nextEpisodeIndex, + undefined, { animeId: selectedAnime.animeId, animeTitle: selectedAnime.animeTitle, @@ -4683,6 +4692,7 @@ function PlayPageClient() { episodeTitle: selection.episodeTitle, searchKeyword: selection.searchKeyword, danmakuCount: selection.danmakuCount, + bypassCache: isManual, }); }; diff --git a/src/components/DanmakuPanel.tsx b/src/components/DanmakuPanel.tsx index 3265d59..e4eeed4 100644 --- a/src/components/DanmakuPanel.tsx +++ b/src/components/DanmakuPanel.tsx @@ -1,7 +1,7 @@ 'use client'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getEpisodes, searchAnime } from '@/lib/danmaku/api'; import type { @@ -35,6 +35,10 @@ export default function DanmakuPanel({ const [searchError, setSearchError] = useState(null); const initializedRef = useRef(false); // 标记是否已初始化过 const fileInputRef = useRef(null); + const [episodeGroupIndex, setEpisodeGroupIndex] = useState(0); + const [episodeDescending, setEpisodeDescending] = useState(false); + const [episodeViewMode, setEpisodeViewMode] = useState<'list' | 'grid'>('list'); + const episodesPerGroup = 50; // 搜索弹幕 const handleSearch = useCallback(async (keyword: string) => { @@ -112,6 +116,7 @@ export default function DanmakuPanel({ const handleBackToResults = useCallback(() => { setSelectedAnime(null); setEpisodes([]); + setEpisodeGroupIndex(0); }, []); // 判断当前剧集是否已选中 @@ -162,6 +167,69 @@ export default function DanmakuPanel({ } }, [videoTitle]); + useEffect(() => { + if (episodes.length > 0) { + setEpisodeGroupIndex(Math.floor(currentEpisodeIndex / episodesPerGroup)); + } else { + setEpisodeGroupIndex(0); + } + }, [episodes, currentEpisodeIndex]); + + const episodeGroupCount = Math.ceil(episodes.length / episodesPerGroup); + + const episodeGroups = useMemo(() => { + return Array.from({ length: episodeGroupCount }, (_, idx) => { + const start = idx * episodesPerGroup + 1; + const end = Math.min((idx + 1) * episodesPerGroup, episodes.length); + return `${start}-${end}`; + }); + }, [episodeGroupCount, episodes.length]); + + const displayEpisodeGroupIndex = useMemo(() => { + if (episodeDescending) { + return episodeGroupCount - 1 - episodeGroupIndex; + } + return episodeGroupIndex; + }, [episodeDescending, episodeGroupCount, episodeGroupIndex]); + + const currentGroupEpisodes = useMemo(() => { + if (episodes.length === 0) return []; + + const start = episodeGroupIndex * episodesPerGroup; + const end = Math.min(start + episodesPerGroup, episodes.length); + const groupEpisodes = episodes.slice(start, end); + const withEpisodeNumber = groupEpisodes.map((episode, index) => ({ + ...episode, + episodeNumber: start + index + 1, + })); + + return episodeDescending ? [...withEpisodeNumber].reverse() : withEpisodeNumber; + }, [episodes, episodeDescending, episodeGroupIndex]); + + const getEpisodeDisplayLabel = useCallback((episodeTitle: string, episodeNumber: number) => { + if (!episodeTitle) { + return String(episodeNumber); + } + + if (episodeTitle.match(/^OVA\s+\d+/i)) { + return episodeTitle; + } + + const sxxexxMatch = episodeTitle.match(/[Ss](\d+)[Ee](\d{1,4}(?:\.\d+)?)/); + if (sxxexxMatch) { + const season = sxxexxMatch[1].padStart(2, '0'); + const episode = sxxexxMatch[2]; + return `S${season}E${episode}`; + } + + const match = episodeTitle.match(/(?:第)?(\d+(?:\.\d+)?)(?:集|话)/); + if (match) { + return match[1]; + } + + return String(episodeNumber); + }, []); + return (
{/* 搜索区域 - 固定在顶部 */} @@ -278,72 +346,151 @@ export default function DanmakuPanel({ {/* 剧集列表 */} {!isLoadingEpisodes && episodes.length > 0 && ( -
- {episodes.map((episode, index) => { - const isSelected = isEpisodeSelected(episode.episodeId); - return ( -
-
- - {/* 选中标记 */} - {isSelected && ( -
- - - -
- )} - - {/* 未选中时的箭头 */} - {!isSelected && ( -
- - - -
- )} + {label} + {isActive && ( +
+ )} + + ); + })} + - ); - })} +
+ + +
+
+ + + {episodeViewMode === 'grid' ? ( +
+ {currentGroupEpisodes.map((episode) => { + const isSelected = isEpisodeSelected(episode.episodeId); + return ( + + ); + })} +
+ ) : ( +
+ {currentGroupEpisodes.map((episode) => { + const isSelected = isEpisodeSelected(episode.episodeId); + return ( + + ); + })} +
+ )} )} diff --git a/src/lib/danmaku/api.ts b/src/lib/danmaku/api.ts index 51cc001..1a497d0 100644 --- a/src/lib/danmaku/api.ts +++ b/src/lib/danmaku/api.ts @@ -142,6 +142,9 @@ export async function getDanmakuById( episodeId: number, title?: string, episodeIndex?: number, + options?: { + bypassCache?: boolean; + }, metadata?: { animeId?: number; animeTitle?: string; @@ -152,13 +155,15 @@ export async function getDanmakuById( ): Promise { try { // 1. 如果提供了 title 和 episodeIndex,先尝试从缓存读取 - if (title && episodeIndex !== undefined) { + if (title && episodeIndex !== undefined && !options?.bypassCache) { const cachedData = await getDanmakuFromCache(title, episodeIndex); if (cachedData) { console.log(`[弹幕缓存] 使用缓存: title=${title}, episodeIndex=${episodeIndex}, 数量=${cachedData.comments.length}`); return cachedData.comments; } console.log(`[弹幕缓存] 缓存未命中,从 API 获取: title=${title}, episodeIndex=${episodeIndex}`); + } else if (title && episodeIndex !== undefined && options?.bypassCache) { + console.log(`[弹幕缓存] 手动选择,跳过缓存读取: title=${title}, episodeIndex=${episodeIndex}, episodeId=${episodeId}`); } else { console.log(`[弹幕缓存] 未提供 title/episodeIndex,跳过缓存: episodeId=${episodeId}`); }