update:1.更新大纲细化功能

This commit is contained in:
xiamuceer
2025-11-18 22:14:55 +08:00
parent a2229f7780
commit 9b17774e13
14 changed files with 3285 additions and 370 deletions
+263 -90
View File
@@ -1,11 +1,10 @@
import { useState, useEffect, useRef } from 'react';
import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge, Tag, Card, Tooltip, InputNumber, Progress, Alert, Radio } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined } from '@ant-design/icons';
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 } from 'antd';
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined, FundOutlined, SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, RocketOutlined, StopOutlined, InfoCircleOutlined, CaretRightOutlined } 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 } from '../types';
import { cardStyles } from '../components/CardStyles';
import type { Chapter, ChapterUpdate, ApiError, WritingStyle, AnalysisTask, ExpansionPlanData } from '../types';
import ChapterAnalysis from '../components/ChapterAnalysis';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
@@ -479,6 +478,34 @@ export default function Chapters() {
const sortedChapters = [...chapters].sort((a, b) => a.chapter_number - b.chapter_number);
// 按大纲分组章节
const groupedChapters = useMemo(() => {
const groups: Record<string, {
outlineId: string | null;
outlineTitle: string;
outlineOrder: number;
chapters: Chapter[];
}> = {};
sortedChapters.forEach(chapter => {
const key = chapter.outline_id || 'uncategorized';
if (!groups[key]) {
groups[key] = {
outlineId: chapter.outline_id || null,
outlineTitle: chapter.outline_title || '未分类章节',
outlineOrder: chapter.outline_order ?? 999,
chapters: []
};
}
groups[key].chapters.push(chapter);
});
// 转换为数组并按大纲顺序排序
return Object.values(groups).sort((a, b) => a.outlineOrder - b.outlineOrder);
}, [sortedChapters]);
const handleExport = () => {
if (chapters.length === 0) {
message.warning('当前项目没有章节,无法导出');
@@ -709,6 +736,97 @@ export default function Chapters() {
}
};
// 显示展开规划详情
const showExpansionPlanModal = (chapter: Chapter) => {
if (!chapter.expansion_plan) return;
try {
const planData: ExpansionPlanData = JSON.parse(chapter.expansion_plan);
Modal.info({
title: (
<Space>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<span>{chapter.chapter_number}</span>
</Space>
),
width: 800,
content: (
<div style={{ marginTop: 16 }}>
<Descriptions column={1} size="small" bordered>
<Descriptions.Item label="章节标题">
<strong>{chapter.title}</strong>
</Descriptions.Item>
<Descriptions.Item label="情感基调">
<Tag color="blue">{planData.emotional_tone}</Tag>
</Descriptions.Item>
<Descriptions.Item label="冲突类型">
<Tag color="orange">{planData.conflict_type}</Tag>
</Descriptions.Item>
<Descriptions.Item label="预估字数">
<Tag color="green">{planData.estimated_words}</Tag>
</Descriptions.Item>
<Descriptions.Item label="叙事目标">
{planData.narrative_goal}
</Descriptions.Item>
<Descriptions.Item label="关键事件">
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{planData.key_events.map((event, idx) => (
<div key={idx} style={{ padding: '4px 0' }}>
<Tag color="purple">{idx + 1}</Tag> {event}
</div>
))}
</Space>
</Descriptions.Item>
<Descriptions.Item label="涉及角色">
<Space wrap>
{planData.character_focus.map((char, idx) => (
<Tag key={idx} color="cyan">{char}</Tag>
))}
</Space>
</Descriptions.Item>
{planData.scenes && planData.scenes.length > 0 && (
<Descriptions.Item label="场景规划">
<Space direction="vertical" size="small" style={{ width: '100%' }}>
{planData.scenes.map((scene, idx) => (
<Card key={idx} size="small" style={{ backgroundColor: '#fafafa' }}>
<div style={{ marginBottom: 4 }}>
<strong>📍 </strong>{scene.location}
</div>
<div style={{ marginBottom: 4 }}>
<strong>👥 </strong>
<Space size="small" wrap style={{ marginLeft: 8 }}>
{scene.characters.map((char, charIdx) => (
<Tag key={charIdx}>{char}</Tag>
))}
</Space>
</div>
<div>
<strong>🎯 </strong>{scene.purpose}
</div>
</Card>
))}
</Space>
</Descriptions.Item>
)}
</Descriptions>
<Alert
message="提示"
description="这些是AI在大纲展开时生成的规划信息,可以作为创作章节内容时的参考。"
type="info"
showIcon
style={{ marginTop: 16 }}
/>
</div>
),
okText: '关闭',
});
} catch (error) {
console.error('解析展开规划失败:', error);
message.error('展开规划数据格式错误');
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{
@@ -754,21 +872,54 @@ export default function Chapters() {
<div style={{ flex: 1, overflowY: 'auto' }}>
{chapters.length === 0 ? (
<Empty description="还没有章节,开始创作吧!" />
) : (
<Card style={cardStyles.base}>
<List
dataSource={sortedChapters}
renderItem={(item) => (
<List.Item
<Empty description="还没有章节,开始创作吧!" />
) : (
<Collapse
bordered={false}
defaultActiveKey={groupedChapters.map((_, idx) => idx.toString())}
expandIcon={({ isActive }) => <CaretRightOutlined rotate={isActive ? 90 : 0} />}
style={{ background: 'transparent' }}
>
{groupedChapters.map((group, groupIndex) => (
<Collapse.Panel
key={groupIndex.toString()}
header={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<Tag color={group.outlineId ? 'blue' : 'default'} style={{ margin: 0 }}>
{group.outlineId ? `📖 大纲 ${group.outlineOrder}` : '📝 未分类'}
</Tag>
<span style={{ fontWeight: 600, fontSize: 16 }}>
{group.outlineTitle}
</span>
<Badge
count={`${group.chapters.length}`}
style={{ backgroundColor: '#52c41a' }}
/>
<Badge
count={`${group.chapters.reduce((sum, ch) => sum + (ch.word_count || 0), 0)}`}
style={{ backgroundColor: '#1890ff' }}
/>
</div>
}
style={{
padding: '16px 0',
marginBottom: 16,
background: '#fff',
borderRadius: 8,
transition: 'background 0.3s ease',
flexDirection: isMobile ? 'column' : 'row',
alignItems: isMobile ? 'flex-start' : 'center'
border: '1px solid #f0f0f0',
}}
actions={isMobile ? undefined : [
>
<List
dataSource={group.chapters}
renderItem={(item) => (
<List.Item
style={{
padding: '16px 0',
borderRadius: 8,
transition: 'background 0.3s ease',
flexDirection: isMobile ? 'column' : 'row',
alignItems: isMobile ? 'flex-start' : 'center',
}}
actions={isMobile ? undefined : [
<Button
icon={<EditOutlined />}
onClick={() => handleOpenEditor(item.id)}
@@ -806,85 +957,107 @@ export default function Chapters() {
>
</Button>,
]}
>
<div style={{ width: '100%' }}>
<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>{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)}
{!canGenerateChapter(item) && (
<Tooltip title={getGenerateDisabledReason(item)}>
<Tag icon={<LockOutlined />} color="warning">
</Tag>
</Tooltip>
)}
</div>
}
description={
item.content ? (
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
{item.content.substring(0, isMobile ? 80 : 150)}
{item.content.length > (isMobile ? 80 : 150) && '...'}
</div>
) : (
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}></span>
)
}
/>
{isMobile && (
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleOpenEditor(item.id)}
size="small"
title="编辑内容"
/>
{(() => {
const task = analysisTasksMap[item.id];
const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
const hasContent = item.content && item.content.trim() !== '';
]}
>
<div style={{ width: '100%' }}>
<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>
{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>
)}
</div>
}
description={
item.content ? (
<div style={{ marginTop: 8, color: 'rgba(0,0,0,0.65)', lineHeight: 1.6, fontSize: isMobile ? 12 : 14 }}>
{item.content.substring(0, isMobile ? 80 : 150)}
{item.content.length > (isMobile ? 80 : 150) && '...'}
</div>
) : (
<span style={{ color: 'rgba(0,0,0,0.45)', fontSize: isMobile ? 12 : 14 }}></span>
)
}
/>
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
>
{isMobile && (
<Space style={{ marginTop: 12, width: '100%', justifyContent: 'flex-end' }} wrap>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
icon={<EditOutlined />}
onClick={() => handleOpenEditor(item.id)}
size="small"
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
title="编辑内容"
/>
</Tooltip>
);
})()}
<Button
type="text"
icon={<SettingOutlined />}
onClick={() => handleOpenModal(item.id)}
size="small"
title="修改信息"
/>
</Space>
{(() => {
const task = analysisTasksMap[item.id];
const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
const hasContent = item.content && item.content.trim() !== '';
return (
<Tooltip
title={
!hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' :
'查看分析'
}
>
<Button
type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
onClick={() => handleShowAnalysis(item.id)}
size="small"
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
/>
</Tooltip>
);
})()}
<Button
type="text"
icon={<SettingOutlined />}
onClick={() => handleOpenModal(item.id)}
size="small"
title="修改信息"
/>
</Space>
)}
</div>
</List.Item>
)}
</div>
</List.Item>
)}
/>
</Card>
/>
</Collapse.Panel>
))}
</Collapse>
)}
</div>