update:1.优化向导生成逻辑,最后一步不再展开大纲,避免等待时间过久 2.新增章节跳转功能,搜索功能
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import { Drawer, Input, List, Typography, Empty, Tag } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import type { Chapter } from '../types';
|
||||
|
||||
const { Link } = Typography;
|
||||
|
||||
interface GroupedChapters {
|
||||
outlineId: string | null;
|
||||
outlineTitle: string;
|
||||
chapters: Chapter[];
|
||||
}
|
||||
|
||||
interface FloatingIndexPanelProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
groupedChapters: GroupedChapters[];
|
||||
onChapterSelect: (chapterId: string) => void;
|
||||
}
|
||||
|
||||
export default function FloatingIndexPanel({
|
||||
visible,
|
||||
onClose,
|
||||
groupedChapters,
|
||||
onChapterSelect,
|
||||
}: FloatingIndexPanelProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
if (!searchTerm) {
|
||||
return groupedChapters;
|
||||
}
|
||||
return groupedChapters
|
||||
.map(group => {
|
||||
const filteredChapters = group.chapters.filter(chapter =>
|
||||
chapter.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
return { ...group, chapters: filteredChapters };
|
||||
})
|
||||
.filter(group => group.chapters.length > 0);
|
||||
}, [searchTerm, groupedChapters]);
|
||||
|
||||
const handleChapterClick = (chapterId: string) => {
|
||||
onChapterSelect(chapterId);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
title="章节目录"
|
||||
placement="right"
|
||||
onClose={onClose}
|
||||
open={visible}
|
||||
width={320}
|
||||
styles={{
|
||||
body: { padding: 0 },
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '16px', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Input
|
||||
placeholder="搜索章节标题"
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredGroups.length > 0 ? (
|
||||
<List
|
||||
dataSource={filteredGroups}
|
||||
renderItem={group => (
|
||||
<List.Item style={{ padding: '0 16px', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<div style={{ padding: '12px 0', fontWeight: 'bold' }}>
|
||||
<Tag color={group.outlineId ? 'blue' : 'default'}>
|
||||
{group.outlineTitle}
|
||||
</Tag>
|
||||
</div>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={group.chapters}
|
||||
renderItem={chapter => (
|
||||
<List.Item style={{ paddingLeft: 16, borderBlockStart: 'none' }}>
|
||||
<Link onClick={() => handleChapterClick(chapter.id)}>
|
||||
{`第${chapter.chapter_number}章: ${chapter.title}`}
|
||||
</Link>
|
||||
</List.Item>
|
||||
)}
|
||||
split={false}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
style={{ height: 'calc(100vh - 120px)', overflowY: 'auto' }}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="没有找到匹配的章节" style={{ marginTop: 48 }} />
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, Alert, Radio, Descriptions, Collapse, Popconfirm } from 'antd';
|
||||
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, 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 } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useChapterSync } from '../store/hooks';
|
||||
import { projectApi, writingStyleApi } from '../services/api';
|
||||
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types';
|
||||
import ChapterAnalysis from '../components/ChapterAnalysis';
|
||||
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
|
||||
import FloatingIndexPanel from '../components/FloatingIndexPanel';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -29,6 +30,7 @@ export default function Chapters() {
|
||||
// 分析任务状态管理
|
||||
const [analysisTasksMap, setAnalysisTasksMap] = useState<Record<string, AnalysisTask>>({});
|
||||
const pollingIntervalsRef = useRef<Record<string, number>>({});
|
||||
const [isIndexPanelVisible, setIsIndexPanelVisible] = useState(false);
|
||||
|
||||
// 单章节生成进度状态
|
||||
const [singleChapterProgress, setSingleChapterProgress] = useState(0);
|
||||
@@ -847,9 +849,22 @@ export default function Chapters() {
|
||||
message.error('删除章节失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
|
||||
const handleChapterSelect = (chapterId: string) => {
|
||||
const element = document.getElementById(`chapter-item-${chapterId}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Optional: add a visual highlight effect
|
||||
element.style.transition = 'background-color 0.5s ease';
|
||||
element.style.backgroundColor = '#e6f7ff';
|
||||
setTimeout(() => {
|
||||
element.style.backgroundColor = '';
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
@@ -891,7 +906,7 @@ export default function Chapters() {
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
||||
{chapters.length === 0 ? (
|
||||
<Empty description="还没有章节,开始创作吧!" />
|
||||
) : (
|
||||
@@ -933,6 +948,7 @@ export default function Chapters() {
|
||||
dataSource={group.chapters}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
id={`chapter-item-${item.id}`}
|
||||
style={{
|
||||
padding: '16px 0',
|
||||
borderRadius: 8,
|
||||
@@ -1000,38 +1016,46 @@ export default function Chapters() {
|
||||
<List.Item.Meta
|
||||
avatar={!isMobile && <FileTextOutlined style={{ fontSize: 32, color: '#1890ff' }} />}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: isMobile ? 4 : 8, flexWrap: 'wrap', fontSize: isMobile ? 14 : 16 }}>
|
||||
<span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
alignItems: isMobile ? 'flex-start' : 'center',
|
||||
gap: isMobile ? 6 : 12,
|
||||
width: '100%'
|
||||
}}>
|
||||
<span style={{ fontSize: isMobile ? 14 : 16, fontWeight: 500, flexShrink: 0 }}>
|
||||
第{item.chapter_number}章:{item.title}
|
||||
</span>
|
||||
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: '#52c41a' }} />
|
||||
{renderAnalysisStatus(item.id)}
|
||||
{item.expansion_plan && (
|
||||
<Tooltip title="已有展开规划,点击信息图标查看详情">
|
||||
<Tag icon={<CheckCircleOutlined />} color="blue">
|
||||
已展开
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!canGenerateChapter(item) && (
|
||||
<Tooltip title={getGenerateDisabledReason(item)}>
|
||||
<Tag icon={<LockOutlined />} color="warning">
|
||||
需前置章节
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{item.expansion_plan && (
|
||||
<Tooltip title="查看展开规划详情">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: '#1890ff', cursor: 'pointer', fontSize: 16 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showExpansionPlanModal(item);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Space wrap size={isMobile ? 4 : 8}>
|
||||
<Tag color={getStatusColor(item.status)}>{getStatusText(item.status)}</Tag>
|
||||
<Badge count={`${item.word_count || 0}字`} style={{ backgroundColor: '#52c41a' }} />
|
||||
{renderAnalysisStatus(item.id)}
|
||||
{item.expansion_plan && (
|
||||
<Tooltip title="已有展开规划,点击信息图标查看详情">
|
||||
<Tag icon={<CheckCircleOutlined />} color="blue">
|
||||
已展开
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!canGenerateChapter(item) && (
|
||||
<Tooltip title={getGenerateDisabledReason(item)}>
|
||||
<Tag icon={<LockOutlined />} color="warning">
|
||||
需前置章节
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{item.expansion_plan && (
|
||||
<Tooltip title="查看展开规划详情">
|
||||
<InfoCircleOutlined
|
||||
style={{ color: '#1890ff', cursor: 'pointer', fontSize: 16 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
showExpansionPlanModal(item);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
@@ -1630,6 +1654,21 @@ export default function Chapters() {
|
||||
progress={singleChapterProgress}
|
||||
message={singleChapterProgressMessage}
|
||||
/>
|
||||
|
||||
<FloatButton
|
||||
icon={<BookOutlined />}
|
||||
type="primary"
|
||||
tooltip="章节目录"
|
||||
onClick={() => setIsIndexPanelVisible(true)}
|
||||
style={{ right: isMobile ? 24 : 48, bottom: isMobile ? 80 : 48 }}
|
||||
/>
|
||||
|
||||
<FloatingIndexPanel
|
||||
visible={isIndexPanelVisible}
|
||||
onClose={() => setIsIndexPanelVisible(false)}
|
||||
groupedChapters={groupedChapters}
|
||||
onChapterSelect={handleChapterSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,7 +140,7 @@ export default function ProjectWizardNew() {
|
||||
await wizardStreamApi.generateCompleteOutlineStream(
|
||||
{
|
||||
project_id: createdProjectId,
|
||||
chapter_count: 5, // 开局5章
|
||||
chapter_count: 3, // 生成3个大纲节点(不展开)
|
||||
narrative_perspective: values.narrative_perspective,
|
||||
target_words: values.target_words,
|
||||
},
|
||||
@@ -190,7 +190,7 @@ export default function ProjectWizardNew() {
|
||||
创建新项目
|
||||
</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 32 }}>
|
||||
填写基本信息后,AI将自动为您生成世界观、角色和开局大纲
|
||||
填写基本信息后,AI将自动为您生成世界观、角色和大纲节点(大纲可在项目内手动展开为章节)
|
||||
</Paragraph>
|
||||
|
||||
<Form
|
||||
@@ -432,7 +432,13 @@ export default function ProjectWizardNew() {
|
||||
marginTop: isMobile ? 16 : 24,
|
||||
marginBottom: isMobile ? 32 : 48,
|
||||
}}>
|
||||
《{projectTitle}》已成功创建,包含完整的世界观、角色和开局大纲
|
||||
《{projectTitle}》已成功创建,包含完整的世界观、角色和大纲节点
|
||||
</Paragraph>
|
||||
<Paragraph type="secondary" style={{
|
||||
fontSize: isMobile ? 12 : 14,
|
||||
marginTop: 8,
|
||||
}}>
|
||||
💡 提示:进入项目后,可在"大纲"页面将大纲节点展开为详细章节
|
||||
</Paragraph>
|
||||
|
||||
<Space
|
||||
|
||||
Reference in New Issue
Block a user