feature: 新增章节阅读器功能,支持在章节列表中直接阅读章节内容
This commit is contained in:
+138
-89
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
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 { 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 } from '@ant-design/icons';
|
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 { useStore } from '../store';
|
||||||
import { useChapterSync } from '../store/hooks';
|
import { useChapterSync } from '../store/hooks';
|
||||||
import { projectApi, writingStyleApi, chapterApi } from '../services/api';
|
import { projectApi, writingStyleApi, chapterApi } from '../services/api';
|
||||||
@@ -10,6 +10,7 @@ import ExpansionPlanEditor from '../components/ExpansionPlanEditor';
|
|||||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||||
import { SSEProgressModal } from '../components/SSEProgressModal';
|
import { SSEProgressModal } from '../components/SSEProgressModal';
|
||||||
import FloatingIndexPanel from '../components/FloatingIndexPanel';
|
import FloatingIndexPanel from '../components/FloatingIndexPanel';
|
||||||
|
import ChapterReader from '../components/ChapterReader';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
@@ -39,6 +40,10 @@ export default function Chapters() {
|
|||||||
const pollingIntervalsRef = useRef<Record<string, number>>({});
|
const pollingIntervalsRef = useRef<Record<string, number>>({});
|
||||||
const [isIndexPanelVisible, setIsIndexPanelVisible] = useState(false);
|
const [isIndexPanelVisible, setIsIndexPanelVisible] = useState(false);
|
||||||
|
|
||||||
|
// 阅读器状态
|
||||||
|
const [readerVisible, setReaderVisible] = useState(false);
|
||||||
|
const [readingChapter, setReadingChapter] = useState<Chapter | null>(null);
|
||||||
|
|
||||||
// 规划编辑状态
|
// 规划编辑状态
|
||||||
const [planEditorVisible, setPlanEditorVisible] = useState(false);
|
const [planEditorVisible, setPlanEditorVisible] = useState(false);
|
||||||
const [editingPlanChapter, setEditingPlanChapter] = useState<Chapter | null>(null);
|
const [editingPlanChapter, setEditingPlanChapter] = useState<Chapter | null>(null);
|
||||||
@@ -1170,11 +1175,9 @@ export default function Chapters() {
|
|||||||
);
|
);
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return (
|
return (
|
||||||
<Tooltip title={task.error_message}>
|
<Tag icon={<CloseCircleOutlined />} color="error" title={task.error_message || undefined}>
|
||||||
<Tag icon={<CloseCircleOutlined />} color="error">
|
分析失败
|
||||||
分析失败
|
</Tag>
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
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 (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
@@ -1561,6 +1582,15 @@ export default function Chapters() {
|
|||||||
alignItems: isMobile ? 'flex-start' : 'center',
|
alignItems: isMobile ? 'flex-start' : 'center',
|
||||||
}}
|
}}
|
||||||
actions={isMobile ? undefined : [
|
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
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -1574,23 +1604,20 @@ export default function Chapters() {
|
|||||||
const hasContent = item.content && item.content.trim() !== '';
|
const hasContent = item.content && item.content.trim() !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
||||||
|
onClick={() => handleShowAnalysis(item.id)}
|
||||||
|
disabled={!hasContent || isAnalyzing}
|
||||||
|
loading={isAnalyzing}
|
||||||
title={
|
title={
|
||||||
!hasContent ? '请先生成章节内容' :
|
!hasContent ? '请先生成章节内容' :
|
||||||
isAnalyzing ? '分析进行中,请稍候...' :
|
isAnalyzing ? '分析进行中,请稍候...' :
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
{isAnalyzing ? '分析中' : '查看分析'}
|
||||||
type="text"
|
</Button>
|
||||||
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
|
||||||
onClick={() => handleShowAnalysis(item.id)}
|
|
||||||
disabled={!hasContent || isAnalyzing}
|
|
||||||
loading={isAnalyzing}
|
|
||||||
>
|
|
||||||
{isAnalyzing ? '分析中' : '查看分析'}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
<Button
|
<Button
|
||||||
@@ -1621,11 +1648,9 @@ export default function Chapters() {
|
|||||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: 'var(--color-success)' }} />
|
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: 'var(--color-success)' }} />
|
||||||
{renderAnalysisStatus(item.id)}
|
{renderAnalysisStatus(item.id)}
|
||||||
{!canGenerateChapter(item) && (
|
{!canGenerateChapter(item) && (
|
||||||
<Tooltip title={getGenerateDisabledReason(item)}>
|
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
||||||
<Tag icon={<LockOutlined />} color="warning">
|
需前置章节
|
||||||
需前置章节
|
</Tag>
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
@@ -1644,6 +1669,14 @@ export default function Chapters() {
|
|||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
|
<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
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -1657,22 +1690,19 @@ export default function Chapters() {
|
|||||||
const hasContent = item.content && item.content.trim() !== '';
|
const hasContent = item.content && item.content.trim() !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
||||||
|
onClick={() => handleShowAnalysis(item.id)}
|
||||||
|
size="small"
|
||||||
|
disabled={!hasContent || isAnalyzing}
|
||||||
|
loading={isAnalyzing}
|
||||||
title={
|
title={
|
||||||
!hasContent ? '请先生成章节内容' :
|
!hasContent ? '请先生成章节内容' :
|
||||||
isAnalyzing ? '分析中' :
|
isAnalyzing ? '分析中' :
|
||||||
'查看分析'
|
'查看分析'
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
|
||||||
onClick={() => handleShowAnalysis(item.id)}
|
|
||||||
size="small"
|
|
||||||
disabled={!hasContent || isAnalyzing}
|
|
||||||
loading={isAnalyzing}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<Button
|
<Button
|
||||||
@@ -1737,6 +1767,15 @@ export default function Chapters() {
|
|||||||
alignItems: isMobile ? 'flex-start' : 'center',
|
alignItems: isMobile ? 'flex-start' : 'center',
|
||||||
}}
|
}}
|
||||||
actions={isMobile ? undefined : [
|
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
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -1750,23 +1789,20 @@ export default function Chapters() {
|
|||||||
const hasContent = item.content && item.content.trim() !== '';
|
const hasContent = item.content && item.content.trim() !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
||||||
|
onClick={() => handleShowAnalysis(item.id)}
|
||||||
|
disabled={!hasContent || isAnalyzing}
|
||||||
|
loading={isAnalyzing}
|
||||||
title={
|
title={
|
||||||
!hasContent ? '请先生成章节内容' :
|
!hasContent ? '请先生成章节内容' :
|
||||||
isAnalyzing ? '分析进行中,请稍候...' :
|
isAnalyzing ? '分析进行中,请稍候...' :
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
{isAnalyzing ? '分析中' : '查看分析'}
|
||||||
type="text"
|
</Button>
|
||||||
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
|
||||||
onClick={() => handleShowAnalysis(item.id)}
|
|
||||||
disabled={!hasContent || isAnalyzing}
|
|
||||||
loading={isAnalyzing}
|
|
||||||
>
|
|
||||||
{isAnalyzing ? '分析中' : '查看分析'}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
<Button
|
<Button
|
||||||
@@ -1816,33 +1852,29 @@ export default function Chapters() {
|
|||||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: 'var(--color-success)' }} />
|
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: 'var(--color-success)' }} />
|
||||||
{renderAnalysisStatus(item.id)}
|
{renderAnalysisStatus(item.id)}
|
||||||
{!canGenerateChapter(item) && (
|
{!canGenerateChapter(item) && (
|
||||||
<Tooltip title={getGenerateDisabledReason(item)}>
|
<Tag icon={<LockOutlined />} color="warning" title={getGenerateDisabledReason(item)}>
|
||||||
<Tag icon={<LockOutlined />} color="warning">
|
需前置章节
|
||||||
需前置章节
|
</Tag>
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
{item.expansion_plan && (
|
{item.expansion_plan && (
|
||||||
<Tooltip title="查看展开详情">
|
<InfoCircleOutlined
|
||||||
<InfoCircleOutlined
|
title="查看展开详情"
|
||||||
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }}
|
style={{ color: 'var(--color-primary)', cursor: 'pointer', fontSize: 16 }}
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
showExpansionPlanModal(item);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<Tooltip title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}>
|
|
||||||
<FormOutlined
|
|
||||||
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleOpenPlanEditor(item);
|
showExpansionPlanModal(item);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
)}
|
||||||
|
<FormOutlined
|
||||||
|
title={item.expansion_plan ? "编辑规划信息" : "创建规划信息"}
|
||||||
|
style={{ color: 'var(--color-success)', cursor: 'pointer', fontSize: 16 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleOpenPlanEditor(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
@@ -1861,6 +1893,14 @@ export default function Chapters() {
|
|||||||
|
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
|
<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
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -1874,22 +1914,19 @@ export default function Chapters() {
|
|||||||
const hasContent = item.content && item.content.trim() !== '';
|
const hasContent = item.content && item.content.trim() !== '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
||||||
|
onClick={() => handleShowAnalysis(item.id)}
|
||||||
|
size="small"
|
||||||
|
disabled={!hasContent || isAnalyzing}
|
||||||
|
loading={isAnalyzing}
|
||||||
title={
|
title={
|
||||||
!hasContent ? '请先生成章节内容' :
|
!hasContent ? '请先生成章节内容' :
|
||||||
isAnalyzing ? '分析中' :
|
isAnalyzing ? '分析中' :
|
||||||
'查看分析'
|
'查看分析'
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
|
|
||||||
onClick={() => handleShowAnalysis(item.id)}
|
|
||||||
size="small"
|
|
||||||
disabled={!hasContent || isAnalyzing}
|
|
||||||
loading={isAnalyzing}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<Button
|
<Button
|
||||||
@@ -2045,19 +2082,18 @@ export default function Chapters() {
|
|||||||
const disabledReason = currentChapter ? getGenerateDisabledReason(currentChapter) : '';
|
const disabledReason = currentChapter ? getGenerateDisabledReason(currentChapter) : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={!canGenerate ? disabledReason : '根据大纲和前置章节内容创作'}>
|
<Button
|
||||||
<Button
|
type="primary"
|
||||||
type="primary"
|
icon={canGenerate ? <ThunderboltOutlined /> : <LockOutlined />}
|
||||||
icon={canGenerate ? <ThunderboltOutlined /> : <LockOutlined />}
|
onClick={() => currentChapter && showGenerateModal(currentChapter)}
|
||||||
onClick={() => currentChapter && showGenerateModal(currentChapter)}
|
loading={isContinuing}
|
||||||
loading={isContinuing}
|
disabled={!canGenerate}
|
||||||
disabled={!canGenerate}
|
danger={!canGenerate}
|
||||||
danger={!canGenerate}
|
style={{ fontWeight: 'bold' }}
|
||||||
style={{ fontWeight: 'bold' }}
|
title={!canGenerate ? disabledReason : '根据大纲和前置章节内容创作'}
|
||||||
>
|
>
|
||||||
{isMobile ? 'AI' : 'AI创作'}
|
{isMobile ? 'AI' : 'AI创作'}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
@@ -2556,6 +2592,19 @@ export default function Chapters() {
|
|||||||
onChapterSelect={handleChapterSelect}
|
onChapterSelect={handleChapterSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 章节阅读器 */}
|
||||||
|
{readingChapter && (
|
||||||
|
<ChapterReader
|
||||||
|
visible={readerVisible}
|
||||||
|
chapter={readingChapter}
|
||||||
|
onClose={() => {
|
||||||
|
setReaderVisible(false);
|
||||||
|
setReadingChapter(null);
|
||||||
|
}}
|
||||||
|
onChapterChange={handleReaderChapterChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 规划编辑器 */}
|
{/* 规划编辑器 */}
|
||||||
{editingPlanChapter && currentProject && (() => {
|
{editingPlanChapter && currentProject && (() => {
|
||||||
let parsedPlanData = null;
|
let parsedPlanData = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user