import React, { useState, useEffect } from 'react'; import { Card, List, Button, Space, Empty, Tag, Spin, Alert, Switch, Drawer, message } from 'antd'; import { EyeOutlined, EyeInvisibleOutlined, MenuOutlined, LeftOutlined, RightOutlined, UnorderedListOutlined, FundOutlined, } from '@ant-design/icons'; import { useParams } from 'react-router-dom'; import api from '../services/api'; import AnnotatedText, { type MemoryAnnotation } from '../components/AnnotatedText'; import MemorySidebar from '../components/MemorySidebar'; interface ChapterItem { id: string; chapter_number: number; title: string; content: string; word_count: number; status: string; } interface AnnotationsData { chapter_id: string; chapter_number: number; title: string; word_count: number; annotations: MemoryAnnotation[]; has_analysis: boolean; summary: { total_annotations: number; hooks: number; foreshadows: number; plot_points: number; character_events: number; }; } interface NavigationData { current: { id: string; chapter_number: number; title: string; }; previous: { id: string; chapter_number: number; title: string; } | null; next: { id: string; chapter_number: number; title: string; } | null; } /** * 项目内的章节剧情分析页面 * 显示章节列表和带标注的章节内容 */ const ChapterAnalysis: React.FC = () => { const { projectId } = useParams<{ projectId: string }>(); const [chapters, setChapters] = useState([]); const [selectedChapter, setSelectedChapter] = useState(null); const [annotationsData, setAnnotationsData] = useState(null); const [navigation, setNavigation] = useState(null); const [loading, setLoading] = useState(true); const [contentLoading, setContentLoading] = useState(false); const [showAnnotations, setShowAnnotations] = useState(true); const [activeAnnotationId, setActiveAnnotationId] = useState(); const [sidebarVisible, setSidebarVisible] = useState(false); const [chapterListVisible, setChapterListVisible] = useState(false); const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState(); const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState(); const [isMobile, setIsMobile] = useState(window.innerWidth < 768); // 监听窗口大小变化 useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth < 768); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // 加载章节列表 useEffect(() => { const loadChapters = async () => { if (!projectId) return; try { setLoading(true); const response = await api.get(`/chapters/project/${projectId}`); // API 拦截器已经解析了 response.data,所以直接使用 const data = response.data || response; const chapterList = data.items || []; setChapters(chapterList); // 自动选择第一个有内容的章节 const firstChapterWithContent = chapterList.find((ch: ChapterItem) => ch.content && ch.content.trim() !== ''); if (firstChapterWithContent) { loadChapterContent(firstChapterWithContent.id); } } catch (error) { console.error('加载章节列表失败:', error); message.error('加载章节列表失败'); } finally { setLoading(false); } }; loadChapters(); }, [projectId]); // 加载章节内容和标注 const loadChapterContent = async (chapterId: string) => { try { setContentLoading(true); const [chapterResponse, annotationsResponse, navigationResponse] = await Promise.all([ api.get(`/chapters/${chapterId}`), api.get(`/chapters/${chapterId}/annotations`).catch(() => null), api.get(`/chapters/${chapterId}/navigation`).catch(() => null), ]); // 提取 data 属性 setSelectedChapter(chapterResponse.data || chapterResponse); setAnnotationsData(annotationsResponse ? (annotationsResponse.data || annotationsResponse) : null); setNavigation(navigationResponse ? (navigationResponse.data || navigationResponse) : null); } catch (error) { console.error('加载章节内容失败:', error); message.error('加载章节内容失败'); } finally { setContentLoading(false); } }; const handleChapterSelect = (chapterId: string) => { loadChapterContent(chapterId); if (isMobile) { setChapterListVisible(false); } }; const handlePreviousChapter = () => { if (navigation?.previous) { loadChapterContent(navigation.previous.id); } }; const handleNextChapter = () => { if (navigation?.next) { loadChapterContent(navigation.next.id); } }; const handleAnnotationClick = (annotation: MemoryAnnotation, source: 'content' | 'sidebar' = 'content') => { setActiveAnnotationId(annotation.id); if (source === 'content') { // 从内容区点击,滚动到侧边栏 setScrollToSidebarAnnotation(annotation.id); // 清除滚动状态 setTimeout(() => setScrollToSidebarAnnotation(undefined), 100); if (isMobile) { setSidebarVisible(true); } } else { // 从侧边栏点击,滚动到内容区 setScrollToContentAnnotation(annotation.id); // 清除滚动状态 setTimeout(() => setScrollToContentAnnotation(undefined), 100); } }; const hasAnnotations = annotationsData && annotationsData.annotations.length > 0; if (loading) { return (
); } return (
{/* 页面标题 - 仅桌面端显示 */} {!isMobile && (

剧情分析

)}
{/* 左侧章节列表 - 桌面端 */} {!isMobile && ( {chapters.length === 0 ? ( ) : ( ( handleChapterSelect(chapter.id)} style={{ cursor: 'pointer', padding: '12px 16px', background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent', borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent', }} > 第{chapter.chapter_number}章: {chapter.title} } description={ {chapter.word_count || 0}字 } /> )} /> )} )} {/* 移动端章节列表抽屉 */} {isMobile && ( setChapterListVisible(false)} open={chapterListVisible} width="85%" styles={{ body: { padding: 0 } }} > {chapters.length === 0 ? ( ) : ( ( handleChapterSelect(chapter.id)} style={{ cursor: 'pointer', padding: '12px 16px', background: selectedChapter?.id === chapter.id ? '#e6f7ff' : 'transparent', borderLeft: selectedChapter?.id === chapter.id ? '3px solid #1890ff' : '3px solid transparent', }} > 第{chapter.chapter_number}章: {chapter.title} } description={ {chapter.word_count || 0}字 } /> )} /> )} )} {/* 右侧内容区域 */}
{!selectedChapter ? ( ) : ( <> {/* 工具栏 */} {isMobile ? ( // 移动端布局:两行显示
{/* 第一行:标题和翻页按钮 */}
{/* 第二行:章节、开关、分析按钮 */}
{hasAnnotations && ( <> } unCheckedChildren={} size="small" style={{ flexShrink: 0, height: 16, minHeight: 16, lineHeight: '16px' }} /> )}
) : ( // 桌面端布局:保持原样
第{selectedChapter.chapter_number}章: {selectedChapter.title} {hasAnnotations && ( <> } unCheckedChildren={} /> 显示标注 )}
)} {hasAnnotations && annotationsData && (
共有 {annotationsData.summary.total_annotations} 个标注: {annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`} {annotationsData.summary.foreshadows > 0 && ` 🌟${annotationsData.summary.foreshadows}个伏笔`} {annotationsData.summary.plot_points > 0 && ` 💎${annotationsData.summary.plot_points}个情节点`} {annotationsData.summary.character_events > 0 && ` 👤${annotationsData.summary.character_events}个角色事件`}
)}
{/* 内容区域 */}
{/* 章节内容 */} {!contentLoading && ( <> {!hasAnnotations && ( )} {showAnnotations && hasAnnotations && annotationsData ? ( handleAnnotationClick(annotation, 'content')} activeAnnotationId={activeAnnotationId} scrollToAnnotation={scrollToContentAnnotation} style={{ lineHeight: isMobile ? 1.8 : 2, fontSize: isMobile ? 14 : 16, }} /> ) : (
{selectedChapter.content}
)} )}
{/* 右侧记忆侧边栏(桌面端) */} {hasAnnotations && annotationsData && !isMobile && ( handleAnnotationClick(annotation, 'sidebar')} scrollToAnnotation={scrollToSidebarAnnotation} /> )}
{/* 移动端抽屉 */} {hasAnnotations && annotationsData && ( setSidebarVisible(false)} open={sidebarVisible} width={isMobile ? '90%' : '80%'} > { handleAnnotationClick(annotation, 'sidebar'); setSidebarVisible(false); }} scrollToAnnotation={scrollToSidebarAnnotation} /> )} )}
); }; export default ChapterAnalysis;