update:1.新增AI生成组织功能,扩展优化组织字段(所在地 代表颜色 格言/口号)

2.适配移动端项目管理-剧情分析UI页面
This commit is contained in:
xiamuceer
2025-11-05 16:22:14 +08:00
parent ff58548a79
commit 397ca30bcb
17 changed files with 1155 additions and 185 deletions
+257 -93
View File
@@ -6,6 +6,7 @@ import {
MenuOutlined,
LeftOutlined,
RightOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import { useParams } from 'react-router-dom';
import api from '../services/api';
@@ -71,8 +72,20 @@ const ChapterAnalysis: React.FC = () => {
const [showAnnotations, setShowAnnotations] = useState(true);
const [activeAnnotationId, setActiveAnnotationId] = useState<string | undefined>();
const [sidebarVisible, setSidebarVisible] = useState(false);
const [chapterListVisible, setChapterListVisible] = useState(false);
const [scrollToContentAnnotation, setScrollToContentAnnotation] = useState<string | undefined>();
const [scrollToSidebarAnnotation, setScrollToSidebarAnnotation] = useState<string | undefined>();
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
// 监听窗口大小变化
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 加载章节列表
useEffect(() => {
@@ -128,6 +141,9 @@ const ChapterAnalysis: React.FC = () => {
const handleChapterSelect = (chapterId: string) => {
loadChapterContent(chapterId);
if (isMobile) {
setChapterListVisible(false);
}
};
const handlePreviousChapter = () => {
@@ -151,7 +167,7 @@ const ChapterAnalysis: React.FC = () => {
// 清除滚动状态
setTimeout(() => setScrollToSidebarAnnotation(undefined), 100);
if (window.innerWidth < 768) {
if (isMobile) {
setSidebarVisible(true);
}
} else {
@@ -173,48 +189,102 @@ const ChapterAnalysis: React.FC = () => {
}
return (
<div style={{ display: 'flex', height: '100%', gap: 16 }}>
{/* 左侧章节列表 */}
<Card
title="章节列表"
style={{ width: 280, height: '100%', overflow: 'hidden' }}
bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => 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',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Card>
<div style={{
display: 'flex',
height: '100%',
gap: isMobile ? 0 : 16,
flexDirection: isMobile ? 'column' : 'row'
}}>
{/* 左侧章节列表 - 桌面端 */}
{!isMobile && (
<Card
title="章节列表"
style={{ width: 280, height: '100%', overflow: 'hidden' }}
bodyStyle={{ padding: 0, height: 'calc(100% - 57px)', overflow: 'auto' }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => 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',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Card>
)}
{/* 移动端章节列表抽屉 */}
{isMobile && (
<Drawer
title="章节列表"
placement="left"
onClose={() => setChapterListVisible(false)}
open={chapterListVisible}
width="85%"
styles={{ body: { padding: 0 } }}
>
{chapters.length === 0 ? (
<Empty description="暂无章节" style={{ marginTop: 60 }} />
) : (
<List
dataSource={chapters}
renderItem={(chapter) => (
<List.Item
key={chapter.id}
onClick={() => 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',
}}
>
<List.Item.Meta
title={
<span style={{ fontSize: 14, fontWeight: selectedChapter?.id === chapter.id ? 600 : 400 }}>
{chapter.chapter_number}: {chapter.title}
</span>
}
description={
<Space size={4}>
<Tag color={chapter.content && chapter.content.trim() !== '' ? 'success' : 'default'}>
{chapter.word_count || 0}
</Tag>
</Space>
}
/>
</List.Item>
)}
/>
)}
</Drawer>
)}
{/* 右侧内容区域 */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
@@ -225,54 +295,138 @@ const ChapterAnalysis: React.FC = () => {
) : (
<>
{/* 工具栏 */}
<Card size="small" style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
>
</Button>
<span style={{ fontSize: 16, fontWeight: 600 }}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
>
</Button>
</Space>
<Card size="small" style={{ marginBottom: isMobile ? 8 : 16 }}>
{isMobile ? (
// 移动端布局:两行显示
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{/* 第一行:标题和翻页按钮 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8
}}>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
size="small"
/>
<span style={{
fontSize: 14,
fontWeight: 600,
flex: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
padding: '0 8px'
}}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
size="small"
/>
</div>
<Space>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
/>
<span style={{ fontSize: 13, color: '#666' }}></span>
<Button
icon={<MenuOutlined />}
onClick={() => setSidebarVisible(true)}
style={{ display: window.innerWidth < 768 ? 'inline-block' : 'none' }}
>
</Button>
</>
)}
</Space>
</div>
{/* 第二行:章节、开关、分析按钮 */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: 8
}}>
<Button
icon={<UnorderedListOutlined />}
onClick={() => setChapterListVisible(true)}
size="small"
>
</Button>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
size="small"
style={{
flexShrink: 0,
height: 16,
minHeight: 16,
lineHeight: '16px'
}}
/>
<Button
icon={<MenuOutlined />}
onClick={() => setSidebarVisible(true)}
size="small"
>
</Button>
</>
)}
</div>
</div>
) : (
// 桌面端布局:保持原样
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Space>
<Button
icon={<LeftOutlined />}
onClick={handlePreviousChapter}
disabled={!navigation?.previous}
title={navigation?.previous ? `上一章: ${navigation.previous.title}` : '已是第一章'}
>
</Button>
<span style={{ fontSize: 16, fontWeight: 600 }}>
{selectedChapter.chapter_number}: {selectedChapter.title}
</span>
<Button
icon={<RightOutlined />}
onClick={handleNextChapter}
disabled={!navigation?.next}
title={navigation?.next ? `下一章: ${navigation.next.title}` : '已是最后一章'}
>
</Button>
</Space>
<Space>
{hasAnnotations && (
<>
<Switch
checked={showAnnotations}
onChange={setShowAnnotations}
checkedChildren={<EyeOutlined />}
unCheckedChildren={<EyeInvisibleOutlined />}
/>
<span style={{ fontSize: 13, color: '#666' }}></span>
</>
)}
</Space>
</div>
)}
{hasAnnotations && annotationsData && (
<div style={{ marginTop: 12, fontSize: 12, color: '#999' }}>
<div style={{
marginTop: 12,
fontSize: isMobile ? 11 : 12,
color: '#999',
lineHeight: 1.5
}}>
{annotationsData.summary.total_annotations}
{annotationsData.summary.hooks > 0 && ` 🎣${annotationsData.summary.hooks}个钩子`}
{annotationsData.summary.foreshadows > 0 &&
@@ -286,10 +440,16 @@ const ChapterAnalysis: React.FC = () => {
</Card>
{/* 内容区域 */}
<div style={{ flex: 1, display: 'flex', gap: 16, overflow: 'hidden' }}>
<div style={{
flex: 1,
display: 'flex',
gap: isMobile ? 0 : 16,
overflow: 'hidden'
}}>
{/* 章节内容 */}
<Card
style={{ flex: 1, overflow: 'auto' }}
bodyStyle={{ padding: isMobile ? '12px' : '24px' }}
loading={contentLoading}
>
{!contentLoading && (
@@ -311,12 +471,16 @@ const ChapterAnalysis: React.FC = () => {
onAnnotationClick={(annotation) => handleAnnotationClick(annotation, 'content')}
activeAnnotationId={activeAnnotationId}
scrollToAnnotation={scrollToContentAnnotation}
style={{
lineHeight: isMobile ? 1.8 : 2,
fontSize: isMobile ? 14 : 16,
}}
/>
) : (
<div
style={{
lineHeight: 2,
fontSize: 16,
lineHeight: isMobile ? 1.8 : 2,
fontSize: isMobile ? 14 : 16,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
@@ -329,7 +493,7 @@ const ChapterAnalysis: React.FC = () => {
</Card>
{/* 右侧记忆侧边栏(桌面端) */}
{hasAnnotations && annotationsData && window.innerWidth >= 768 && (
{hasAnnotations && annotationsData && !isMobile && (
<Card
style={{ width: 400, overflow: 'auto' }}
bodyStyle={{ padding: 0 }}
@@ -351,7 +515,7 @@ const ChapterAnalysis: React.FC = () => {
placement="right"
onClose={() => setSidebarVisible(false)}
open={sidebarVisible}
width="80%"
width={isMobile ? '90%' : '80%'}
>
<MemorySidebar
annotations={annotationsData.annotations}