feature: 新增章节阅读器功能,支持在章节列表中直接阅读章节内容

This commit is contained in:
xiamuceer
2026-01-01 17:26:38 +08:00
parent fba6922a5c
commit fb42aa9874
+98 -49
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useMemo } from 'react';
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, FloatButton } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined } from '@ant-design/icons';
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, InputNumber, Alert, Radio, Descriptions, Collapse, Popconfirm, FloatButton } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined, BookOutlined, FormOutlined, PlusOutlined, ReadOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import { useChapterSync } from '../store/hooks';
import { projectApi, writingStyleApi, chapterApi } from '../services/api';
@@ -10,6 +10,7 @@ import ExpansionPlanEditor from '../components/ExpansionPlanEditor';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
import { SSEProgressModal } from '../components/SSEProgressModal';
import FloatingIndexPanel from '../components/FloatingIndexPanel';
import ChapterReader from '../components/ChapterReader';
const { TextArea } = Input;
@@ -39,6 +40,10 @@ export default function Chapters() {
const pollingIntervalsRef = useRef<Record<string, number>>({});
const [isIndexPanelVisible, setIsIndexPanelVisible] = useState(false);
// 阅读器状态
const [readerVisible, setReaderVisible] = useState(false);
const [readingChapter, setReadingChapter] = useState<Chapter | null>(null);
// 规划编辑状态
const [planEditorVisible, setPlanEditorVisible] = useState(false);
const [editingPlanChapter, setEditingPlanChapter] = useState<Chapter | null>(null);
@@ -1170,11 +1175,9 @@ export default function Chapters() {
);
case 'failed':
return (
<Tooltip title={task.error_message}>
<Tag icon={<CloseCircleOutlined />} color="error">
<Tag icon={<CloseCircleOutlined />} color="error" title={task.error_message || undefined}>
</Tag>
</Tooltip>
);
default:
return null;
@@ -1478,6 +1481,24 @@ export default function Chapters() {
}
};
// 打开阅读器
const handleOpenReader = (chapter: Chapter) => {
setReadingChapter(chapter);
setReaderVisible(true);
};
// 阅读器切换章节
const handleReaderChapterChange = async (chapterId: string) => {
try {
const response = await fetch(`/api/chapters/${chapterId}`);
if (!response.ok) throw new Error('获取章节失败');
const newChapter = await response.json();
setReadingChapter(newChapter);
} catch {
message.error('加载章节失败');
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{contextHolder}
@@ -1561,6 +1582,15 @@ export default function Chapters() {
alignItems: isMobile ? 'flex-start' : 'center',
}}
actions={isMobile ? undefined : [
<Button
type="text"
icon={<ReadOutlined />}
onClick={() => handleOpenReader(item)}
disabled={!item.content || item.content.trim() === ''}
title={!item.content || item.content.trim() === '' ? '暂无内容' : '沉浸式阅读'}
>
</Button>,
<Button
type="text"
icon={<EditOutlined />}
@@ -1574,23 +1604,20 @@ export default function Chapters() {
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析进行中,请稍候...' :
''
}
>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析进行中,请稍候...' :
''
}
>
{isAnalyzing ? '分析中' : '查看分析'}
</Button>
</Tooltip>
);
})(),
<Button
@@ -1621,11 +1648,9 @@ export default function Chapters() {
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: 'var(--color-success)' }} />
{renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && (
<Tooltip title={getGenerateDisabledReason(item)}>
<Tag icon={<LockOutlined />} color="warning">
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
</Tag>
</Tooltip>
)}
</Space>
</div>
@@ -1644,6 +1669,14 @@ export default function Chapters() {
{isMobile && (
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
<Button
type="text"
icon={<ReadOutlined />}
onClick={() => handleOpenReader(item)}
size="small"
disabled={!item.content || item.content.trim() === ''}
title={!item.content || item.content.trim() === '' ? '暂无内容' : '阅读'}
/>
<Button
type="text"
icon={<EditOutlined />}
@@ -1657,13 +1690,6 @@ export default function Chapters() {
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
@@ -1671,8 +1697,12 @@ export default function Chapters() {
size="small"
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
/>
</Tooltip>
);
})()}
<Button
@@ -1737,6 +1767,15 @@ export default function Chapters() {
alignItems: isMobile ? 'flex-start' : 'center',
}}
actions={isMobile ? undefined : [
<Button
type="text"
icon={<ReadOutlined />}
onClick={() => handleOpenReader(item)}
disabled={!item.content || item.content.trim() === ''}
title={!item.content || item.content.trim() === '' ? '暂无内容' : '沉浸式阅读'}
>
</Button>,
<Button
type="text"
icon={<EditOutlined />}
@@ -1750,23 +1789,20 @@ export default function Chapters() {
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析进行中,请稍候...' :
''
}
>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析进行中,请稍候...' :
''
}
>
{isAnalyzing ? '分析中' : '查看分析'}
</Button>
</Tooltip>
);
})(),
<Button
@@ -1816,33 +1852,29 @@ export default function Chapters() {
<Badge count={`${item.word_count || 0}`} style={{ backgroundColor: 'var(--color-success)' }} />
{renderAnalysisStatus(item.id)}
{!canGenerateChapter(item) && (
<Tooltip title={getGenerateDisabledReason(item)}>
<Tag icon={<LockOutlined />} color="warning">
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
</Tag>
</Tooltip>
)}
<Space size={4}>
{item.expansion_plan && (
<Tooltip title="查看展开详情">
<InfoCircleOutlined
title="查看展开详情"
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }}
onClick={(e) => {
e.stopPropagation();
showExpansionPlanModal(item);
}}
/>
</Tooltip>
)}
<Tooltip title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}>
<FormOutlined
title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }}
onClick={(e) => {
e.stopPropagation();
handleOpenPlanEditor(item);
}}
/>
</Tooltip>
</Space>
</Space>
</div>
@@ -1861,6 +1893,14 @@ export default function Chapters() {
{isMobile && (
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
<Button
type="text"
icon={<ReadOutlined />}
onClick={() => handleOpenReader(item)}
size="small"
disabled={!item.content || item.content.trim() === ''}
title={!item.content || item.content.trim() === '' ? '暂无内容' : '阅读'}
/>
<Button
type="text"
icon={<EditOutlined />}
@@ -1874,13 +1914,6 @@ export default function Chapters() {
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
@@ -1888,8 +1921,12 @@ export default function Chapters() {
size="small"
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
/>
</Tooltip>
);
})()}
<Button
@@ -2045,7 +2082,6 @@ export default function Chapters() {
const disabledReason = currentChapter ? getGenerateDisabledReason(currentChapter) : '';
return (
<Tooltip title={!canGenerate ? disabledReason : '根据大纲和前置章节内容创作'}>
<Button
type="primary"
icon={canGenerate ? <ThunderboltOutlined /> : <LockOutlined />}
@@ -2054,10 +2090,10 @@ export default function Chapters() {
disabled={!canGenerate}
danger={!canGenerate}
style={{ fontWeight: 'bold' }}
title={!canGenerate ? disabledReason : '根据大纲和前置章节内容创作'}
>
{isMobile ? 'AI' : 'AI创作'}
</Button>
</Tooltip>
);
})()}
</Space.Compact>
@@ -2556,6 +2592,19 @@ export default function Chapters() {
onChapterSelect={handleChapterSelect}
/>
{/* 章节阅读器 */}
{readingChapter && (
<ChapterReader
visible={readerVisible}
chapter={readingChapter}
onClose={() => {
setReaderVisible(false);
setReadingChapter(null);
}}
onChapterChange={handleReaderChapterChange}
/>
)}
{/* 规划编辑器 */}
{editingPlanChapter && currentProject && (() => {
let parsedPlanData = null;