'use client'; import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { getEpisodes, searchAnime } from '@/lib/danmaku/api'; import type { DanmakuAnime, DanmakuComment, DanmakuEpisode, DanmakuSelection, } from '@/lib/danmaku/types'; interface DanmakuPanelProps { videoTitle: string; currentEpisodeIndex: number; onDanmakuSelect: (selection: DanmakuSelection) => void; currentSelection: DanmakuSelection | null; onUploadDanmaku?: (comments: DanmakuComment[]) => void; } export default function DanmakuPanel({ videoTitle, currentEpisodeIndex, onDanmakuSelect, currentSelection, onUploadDanmaku, }: DanmakuPanelProps) { const [searchKeyword, setSearchKeyword] = useState(''); const [searchResults, setSearchResults] = useState([]); const [selectedAnime, setSelectedAnime] = useState(null); const [episodes, setEpisodes] = useState([]); const [isSearching, setIsSearching] = useState(false); const [isLoadingEpisodes, setIsLoadingEpisodes] = useState(false); const [searchError, setSearchError] = useState(null); const initializedRef = useRef(false); // 标记是否已初始化过 const fileInputRef = useRef(null); const episodeGroupContainerRef = useRef(null); const episodeGroupButtonRefs = useRef<(HTMLButtonElement | null)[]>([]); const [episodeGroupIndex, setEpisodeGroupIndex] = useState(0); const [episodeDescending, setEpisodeDescending] = useState(false); const [episodeViewMode, setEpisodeViewMode] = useState<'list' | 'grid'>('list'); const [isEpisodeGroupHovered, setIsEpisodeGroupHovered] = useState(false); const episodesPerGroup = 50; // 搜索弹幕 const handleSearch = useCallback(async (keyword: string) => { if (!keyword.trim()) { setSearchError('请输入搜索关键词'); return; } setIsSearching(true); setSearchError(null); try { const response = await searchAnime(keyword.trim()); if (response.success && response.animes.length > 0) { setSearchResults(response.animes); setSearchError(null); } else { setSearchResults([]); setSearchError( response.errorMessage || '未找到匹配的剧集,请尝试其他关键词' ); } } catch (error) { console.error('搜索失败:', error); setSearchError('搜索失败,请检查弹幕 API 服务是否正常运行'); setSearchResults([]); } finally { setIsSearching(false); } }, []); // 选择动漫,加载剧集列表 const handleAnimeSelect = useCallback(async (anime: DanmakuAnime) => { setSelectedAnime(anime); setIsLoadingEpisodes(true); try { const response = await getEpisodes(anime.animeId); if (response.success && response.bangumi.episodes.length > 0) { setEpisodes(response.bangumi.episodes); } else { setEpisodes([]); setSearchError('该剧集暂无弹幕信息'); } } catch (error) { console.error('获取剧集失败:', error); setEpisodes([]); setSearchError('获取剧集失败'); } finally { setIsLoadingEpisodes(false); } }, []); // 选择剧集 const handleEpisodeSelect = useCallback( (episode: DanmakuEpisode) => { if (!selectedAnime) return; const selection: DanmakuSelection = { animeId: selectedAnime.animeId, episodeId: episode.episodeId, animeTitle: selectedAnime.animeTitle, episodeTitle: episode.episodeTitle, searchKeyword: searchKeyword.trim() || undefined, // 使用当前搜索框的关键词 }; onDanmakuSelect(selection); }, [selectedAnime, searchKeyword, onDanmakuSelect] ); // 回到搜索结果 const handleBackToResults = useCallback(() => { setSelectedAnime(null); setEpisodes([]); setEpisodeGroupIndex(0); }, []); // 判断当前剧集是否已选中 const isEpisodeSelected = useCallback( (episodeId: number) => { return currentSelection?.episodeId === episodeId; }, [currentSelection] ); // 处理文件上传 const handleFileUpload = useCallback(async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; if (!file.name.endsWith('.xml')) { setSearchError('请上传XML格式的弹幕文件'); return; } try { const text = await file.text(); const { parseXmlDanmaku } = await import('@/lib/danmaku/xml-parser'); const comments = parseXmlDanmaku(text); if (comments.length === 0) { setSearchError('弹幕文件解析失败或文件为空'); return; } onUploadDanmaku?.(comments); setSearchError(null); } catch (error) { console.error('上传弹幕失败:', error); setSearchError('弹幕文件解析失败'); } finally { if (fileInputRef.current) { fileInputRef.current.value = ''; } } }, [onUploadDanmaku]); // 当视频标题首次加载时,初始化搜索关键词(仅执行一次) useEffect(() => { if (videoTitle && !initializedRef.current) { setSearchKeyword(videoTitle); initializedRef.current = true; // 标记已初始化,防止后续自动填充 } }, [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); }, []); const preventPageScroll = useCallback((e: WheelEvent) => { if (isEpisodeGroupHovered) { e.preventDefault(); } }, [isEpisodeGroupHovered]); const handleEpisodeGroupWheel = useCallback((e: WheelEvent) => { if (!isEpisodeGroupHovered || !episodeGroupContainerRef.current) { return; } const container = episodeGroupContainerRef.current; if (container.scrollWidth <= container.clientWidth) { return; } e.preventDefault(); container.scrollBy({ left: e.deltaY * 2, behavior: 'smooth', }); }, [isEpisodeGroupHovered]); useEffect(() => { if (isEpisodeGroupHovered) { document.addEventListener('wheel', preventPageScroll, { passive: false }); document.addEventListener('wheel', handleEpisodeGroupWheel, { passive: false }); } else { document.removeEventListener('wheel', preventPageScroll); document.removeEventListener('wheel', handleEpisodeGroupWheel); } return () => { document.removeEventListener('wheel', preventPageScroll); document.removeEventListener('wheel', handleEpisodeGroupWheel); }; }, [handleEpisodeGroupWheel, isEpisodeGroupHovered, preventPageScroll]); useEffect(() => { const btn = episodeGroupButtonRefs.current[displayEpisodeGroupIndex]; const container = episodeGroupContainerRef.current; if (!btn || !container) { return; } const containerRect = container.getBoundingClientRect(); const btnRect = btn.getBoundingClientRect(); const btnLeft = btnRect.left - containerRect.left + container.scrollLeft; const targetScrollLeft = btnLeft - (containerRect.width - btnRect.width) / 2; container.scrollTo({ left: targetScrollLeft, behavior: 'smooth', }); }, [displayEpisodeGroupIndex]); return (
{/* 搜索区域 - 固定在顶部 */}
setSearchKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { handleSearch(searchKeyword); } }} placeholder='输入剧集名称搜索弹幕...' autoComplete='off' autoCorrect='off' autoCapitalize='off' spellCheck='false' data-form-type='other' data-lpignore='true' className='flex-1 min-w-[220px] rounded-lg border border-gray-300 px-3 py-2 text-sm transition-colors focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-white sm:px-4' disabled={isSearching} />
{/* 错误提示 */} {searchError && (
{searchError}
)}
{/* 可滚动内容区域 */}
{/* 当前选择的弹幕信息 */} {currentSelection && (

当前弹幕

{currentSelection.animeTitle}

{currentSelection.episodeTitle}

{currentSelection.danmakuCount !== undefined && (

弹幕数量: {currentSelection.danmakuCount} {currentSelection.danmakuOriginalCount && ` (原始 ${currentSelection.danmakuOriginalCount} 条)`}

)}
)} {/* 内容区域 */}
{/* 显示剧集列表 */} {selectedAnime && (
{/* 返回按钮 */} {/* 动漫标题 */}

{selectedAnime.animeTitle}

{/* 加载中 */} {isLoadingEpisodes && (
)} {/* 剧集列表 */} {!isLoadingEpisodes && episodes.length > 0 && (
setIsEpisodeGroupHovered(true)} onMouseLeave={() => setIsEpisodeGroupHovered(false)} > {episodeGroups.map((label, idx) => { const isActive = idx === displayEpisodeGroupIndex; return ( ); })}
{episodeViewMode === 'grid' ? (
{currentGroupEpisodes.map((episode) => { const isSelected = isEpisodeSelected(episode.episodeId); return ( ); })}
) : (
{currentGroupEpisodes.map((episode) => { const isSelected = isEpisodeSelected(episode.episodeId); return ( ); })}
)}
)} {!isLoadingEpisodes && episodes.length === 0 && (
暂无剧集信息
)}
)} {/* 显示搜索结果 */} {!selectedAnime && searchResults.length > 0 && (
{searchResults.map((anime) => (
handleAnimeSelect(anime)} className='flex cursor-pointer items-start gap-3 rounded-lg bg-gray-100 p-3 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700' > {/* 封面 */} {anime.imageUrl && (
{anime.animeTitle} { e.currentTarget.style.display = 'none'; }} />
)} {/* 信息 */}

{anime.animeTitle}

{/* 自定义 tooltip */}
{anime.animeTitle}
{anime.typeDescription || anime.type} {anime.episodeCount && ( {anime.episodeCount} 集 )} {anime.startDate && {anime.startDate}}
))}
)} {/* 空状态 */} {!selectedAnime && searchResults.length === 0 && !isSearching && (

输入剧集名称搜索弹幕

)}
{/* 上传弹幕区域 - 移动端:在滚动容器内 */} {onUploadDanmaku && (
)}
{/* 上传弹幕区域 - PC端:固定在底部 */} {onUploadDanmaku && (
)}
); }