feature: 新增章节阅读器功能,支持在章节列表中直接阅读章节内容
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user