fix:1.修复章节规划预览窗口UI显示问题

2.修复部分端点删除章节后字数没有更新的问题
This commit is contained in:
xiamuceer
2025-12-10 15:47:02 +08:00
parent 51d3b1ec1a
commit 9fcc06055c
6 changed files with 331 additions and 218 deletions
+56 -9
View File
@@ -279,9 +279,20 @@ async def delete_outline(
project_id = outline.project_id project_id = outline.project_id
deleted_order = outline.order_index deleted_order = outline.order_index
# 根据项目模式删除对应的章节 # 获取要删除的章节并计算总字数
deleted_word_count = 0
if project.outline_mode == 'one-to-one': if project.outline_mode == 'one-to-one':
# one-to-one模式:通过chapter_number删除对应章节 # one-to-one模式:通过chapter_number获取对应章节
chapters_result = await db.execute(
select(Chapter).where(
Chapter.project_id == project_id,
Chapter.chapter_number == outline.order_index
)
)
chapters_to_delete = chapters_result.scalars().all()
deleted_word_count = sum(ch.word_count or 0 for ch in chapters_to_delete)
# 删除章节
delete_result = await db.execute( delete_result = await db.execute(
delete(Chapter).where( delete(Chapter).where(
Chapter.project_id == project_id, Chapter.project_id == project_id,
@@ -289,14 +300,26 @@ async def delete_outline(
) )
) )
deleted_chapters_count = delete_result.rowcount deleted_chapters_count = delete_result.rowcount
logger.info(f"一对一模式:删除大纲 {outline_id}(序号{outline.order_index}),同时删除了第{outline.order_index}章({deleted_chapters_count}个章节)") logger.info(f"一对一模式:删除大纲 {outline_id}(序号{outline.order_index}),同时删除了第{outline.order_index}章({deleted_chapters_count}个章节{deleted_word_count}")
else: else:
# one-to-many模式:通过outline_id删除关联章节 # one-to-many模式:通过outline_id获取关联章节
chapters_result = await db.execute(
select(Chapter).where(Chapter.outline_id == outline_id)
)
chapters_to_delete = chapters_result.scalars().all()
deleted_word_count = sum(ch.word_count or 0 for ch in chapters_to_delete)
# 删除章节
delete_result = await db.execute( delete_result = await db.execute(
delete(Chapter).where(Chapter.outline_id == outline_id) delete(Chapter).where(Chapter.outline_id == outline_id)
) )
deleted_chapters_count = delete_result.rowcount deleted_chapters_count = delete_result.rowcount
logger.info(f"一对多模式:删除大纲 {outline_id},同时删除了 {deleted_chapters_count} 个关联章节") logger.info(f"一对多模式:删除大纲 {outline_id},同时删除了 {deleted_chapters_count} 个关联章节{deleted_word_count}字)")
# 更新项目字数
if deleted_word_count > 0:
project.current_words = max(0, project.current_words - deleted_word_count)
logger.info(f"更新项目字数:减少 {deleted_word_count}")
# 删除大纲 # 删除大纲
await db.delete(outline) await db.delete(outline)
@@ -530,12 +553,24 @@ async def _generate_new_outline(
from sqlalchemy import delete as sql_delete from sqlalchemy import delete as sql_delete
# 先删除所有旧章节(无论是一对一还是一对多模式) # 先获取所有旧章节并计算总字数
old_chapters_result = await db.execute(
select(Chapter).where(Chapter.project_id == project.id)
)
old_chapters = old_chapters_result.scalars().all()
deleted_word_count = sum(ch.word_count or 0 for ch in old_chapters)
# 删除所有旧章节(无论是一对一还是一对多模式)
delete_result = await db.execute( delete_result = await db.execute(
sql_delete(Chapter).where(Chapter.project_id == project.id) sql_delete(Chapter).where(Chapter.project_id == project.id)
) )
deleted_chapters_count = delete_result.rowcount deleted_chapters_count = delete_result.rowcount
logger.info(f"✅ 全新生成:删除了 {deleted_chapters_count} 个旧章节") logger.info(f"✅ 全新生成:删除了 {deleted_chapters_count} 个旧章节{deleted_word_count}字)")
# 更新项目字数
if deleted_word_count > 0:
project.current_words = max(0, project.current_words - deleted_word_count)
logger.info(f"更新项目字数:减少 {deleted_word_count}")
# 再删除所有旧大纲 # 再删除所有旧大纲
delete_outline_result = await db.execute( delete_outline_result = await db.execute(
@@ -1156,12 +1191,24 @@ async def new_outline_generator(
from sqlalchemy import delete as sql_delete from sqlalchemy import delete as sql_delete
# 先删除所有旧章节 # 先获取所有旧章节并计算总字数
old_chapters_result = await db.execute(
select(Chapter).where(Chapter.project_id == project_id)
)
old_chapters = old_chapters_result.scalars().all()
deleted_word_count = sum(ch.word_count or 0 for ch in old_chapters)
# 删除所有旧章节
delete_chapters_result = await db.execute( delete_chapters_result = await db.execute(
sql_delete(Chapter).where(Chapter.project_id == project_id) sql_delete(Chapter).where(Chapter.project_id == project_id)
) )
deleted_chapters_count = delete_chapters_result.rowcount deleted_chapters_count = delete_chapters_result.rowcount
logger.info(f"✅ 全新生成:删除了 {deleted_chapters_count} 个旧章节") logger.info(f"✅ 全新生成:删除了 {deleted_chapters_count} 个旧章节{deleted_word_count}字)")
# 更新项目字数
if deleted_word_count > 0:
project.current_words = max(0, project.current_words - deleted_word_count)
logger.info(f"更新项目字数:减少 {deleted_word_count}")
# 再删除所有旧大纲 # 再删除所有旧大纲
delete_outlines_result = await db.execute( delete_outlines_result = await db.execute(
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "1.0.11", "version": "1.0.12",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+4
View File
@@ -127,3 +127,7 @@ body {
justify-content: center; justify-content: center;
} }
} }
.ant-tabs-dropdown {
z-index: 2000 !important;
}
+94 -76
View File
@@ -27,7 +27,7 @@ export default function Chapters() {
const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]); const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]);
const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>(); const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>();
const [targetWordCount, setTargetWordCount] = useState<number>(3000); const [targetWordCount, setTargetWordCount] = useState<number>(3000);
const [availableModels, setAvailableModels] = useState<Array<{value: string, label: string}>>([]); const [availableModels, setAvailableModels] = useState<Array<{ value: string, label: string }>>([]);
const [selectedModel, setSelectedModel] = useState<string | undefined>(); const [selectedModel, setSelectedModel] = useState<string | undefined>();
const [batchSelectedModel, setBatchSelectedModel] = useState<string | undefined>(); // 批量生成的模型选择 const [batchSelectedModel, setBatchSelectedModel] = useState<string | undefined>(); // 批量生成的模型选择
const [temporaryNarrativePerspective, setTemporaryNarrativePerspective] = useState<string | undefined>(); // 临时人称选择 const [temporaryNarrativePerspective, setTemporaryNarrativePerspective] = useState<string | undefined>(); // 临时人称选择
@@ -710,6 +710,12 @@ export default function Chapters() {
if (status.completed > 0) { if (status.completed > 0) {
refreshChapters(); refreshChapters();
loadAnalysisTasks(); loadAnalysisTasks();
// 刷新项目信息以实时更新总字数统计
if (currentProject?.id) {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
} }
// 任务完成或失败,停止轮询 // 任务完成或失败,停止轮询
@@ -725,6 +731,12 @@ export default function Chapters() {
await refreshChapters(); await refreshChapters();
await loadAnalysisTasks(); await loadAnalysisTasks();
// 刷新项目信息以更新总字数统计
if (currentProject?.id) {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
if (status.status === 'completed') { if (status.status === 'completed') {
message.success(`批量生成完成!成功生成 ${status.completed}`); message.success(`批量生成完成!成功生成 ${status.completed}`);
} else if (status.status === 'failed') { } else if (status.status === 'failed') {
@@ -770,6 +782,12 @@ export default function Chapters() {
// 取消后立即刷新章节列表和分析任务,显示已生成的章节 // 取消后立即刷新章节列表和分析任务,显示已生成的章节
await refreshChapters(); await refreshChapters();
await loadAnalysisTasks(); await loadAnalysisTasks();
// 刷新项目信息以更新总字数统计
if (currentProject?.id) {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
} catch (error: any) { } catch (error: any) {
message.error('取消失败:' + (error.message || '未知错误')); message.error('取消失败:' + (error.message || '未知错误'));
} }
@@ -1333,21 +1351,21 @@ export default function Chapters() {
} }
}; };
const handleChapterSelect = (chapterId: string) => { const handleChapterSelect = (chapterId: string) => {
const element = document.getElementById(`chapter-item-${chapterId}`); const element = document.getElementById(`chapter-item-${chapterId}`);
if (element) { if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' }); element.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Optional: add a visual highlight effect // Optional: add a visual highlight effect
element.style.transition = 'background-color 0.5s ease'; element.style.transition = 'background-color 0.5s ease';
element.style.backgroundColor = '#e6f7ff'; element.style.backgroundColor = '#e6f7ff';
setTimeout(() => { setTimeout(() => {
element.style.backgroundColor = ''; element.style.backgroundColor = '';
}, 1500); }, 1500);
} }
}; };
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ <div style={{
position: 'sticky', position: 'sticky',
top: 0, top: 0,
@@ -1441,8 +1459,8 @@ export default function Chapters() {
<Tooltip <Tooltip
title={ title={
!hasContent ? '请先生成章节内容' : !hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析进行中,请稍候...' : isAnalyzing ? '分析进行中,请稍候...' :
'' ''
} }
> >
<Button <Button
@@ -1524,8 +1542,8 @@ export default function Chapters() {
<Tooltip <Tooltip
title={ title={
!hasContent ? '请先生成章节内容' : !hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' : isAnalyzing ? '分析中' :
'查看分析' '查看分析'
} }
> >
<Button <Button
@@ -1601,64 +1619,64 @@ export default function Chapters() {
alignItems: isMobile ? 'flex-start' : 'center', alignItems: isMobile ? 'flex-start' : 'center',
}} }}
actions={isMobile ? undefined : [ actions={isMobile ? undefined : [
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleOpenEditor(item.id)}
>
</Button>,
(() => {
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 <Button
type="text" type="text"
icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />} icon={<EditOutlined />}
onClick={() => handleShowAnalysis(item.id)} onClick={() => handleOpenEditor(item.id)}
disabled={!hasContent || isAnalyzing}
loading={isAnalyzing}
> >
{isAnalyzing ? '分析中' : '查看分析'}
</Button> </Button>,
</Tooltip> (() => {
); const task = analysisTasksMap[item.id];
})(), const isAnalyzing = task && (task.status === 'pending' || task.status === 'running');
<Button const hasContent = item.content && item.content.trim() !== '';
type="text"
icon={<SettingOutlined />} return (
onClick={() => handleOpenModal(item.id)} <Tooltip
> title={
!hasContent ? '请先生成章节内容' :
</Button>, isAnalyzing ? '分析进行中,请稍候...' :
// 只在 one-to-many 模式下显示删除按钮 ''
...(currentProject.outline_mode === 'one-to-many' ? [ }
<Popconfirm >
title="确定删除这个章节吗?" <Button
description="删除后将无法恢复,章节内容和分析结果都将被删除。" type="text"
onConfirm={() => handleDeleteChapter(item.id)} icon={isAnalyzing ? <SyncOutlined spin /> : <FundOutlined />}
okText="确定删除" onClick={() => handleShowAnalysis(item.id)}
cancelText="取消" disabled={!hasContent || isAnalyzing}
okButtonProps={{ danger: true }} loading={isAnalyzing}
> >
<Button {isAnalyzing ? '分析中' : '查看分析'}
type="text" </Button>
danger </Tooltip>
icon={<DeleteOutlined />} );
> })(),
<Button
</Button> type="text"
</Popconfirm> icon={<SettingOutlined />}
] : []), onClick={() => handleOpenModal(item.id)}
>
</Button>,
// 只在 one-to-many 模式下显示删除按钮
...(currentProject.outline_mode === 'one-to-many' ? [
<Popconfirm
title="确定删除这个章节吗?"
description="删除后将无法恢复,章节内容和分析结果都将被删除。"
onConfirm={() => handleDeleteChapter(item.id)}
okText="确定删除"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>
] : []),
]} ]}
> >
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
@@ -1741,8 +1759,8 @@ export default function Chapters() {
<Tooltip <Tooltip
title={ title={
!hasContent ? '请先生成章节内容' : !hasContent ? '请先生成章节内容' :
isAnalyzing ? '分析中' : isAnalyzing ? '分析中' :
'查看分析' '查看分析'
} }
> >
<Button <Button
+13 -4
View File
@@ -6,13 +6,13 @@ import { useOutlineSync } from '../store/hooks';
import { cardStyles } from '../components/CardStyles'; import { cardStyles } from '../components/CardStyles';
import { SSEPostClient } from '../utils/sseClient'; import { SSEPostClient } from '../utils/sseClient';
import { SSEProgressModal } from '../components/SSEProgressModal'; import { SSEProgressModal } from '../components/SSEProgressModal';
import { outlineApi, chapterApi } from '../services/api'; import { outlineApi, chapterApi, projectApi } from '../services/api';
import type { OutlineExpansionResponse, BatchOutlineExpansionResponse } from '../types'; import type { OutlineExpansionResponse, BatchOutlineExpansionResponse } from '../types';
const { TextArea } = Input; const { TextArea } = Input;
export default function Outline() { export default function Outline() {
const { currentProject, outlines } = useStore(); const { currentProject, outlines, setCurrentProject } = useStore();
const [isGenerating, setIsGenerating] = useState(false); const [isGenerating, setIsGenerating] = useState(false);
const [editForm] = Form.useForm(); const [editForm] = Form.useForm();
const [generateForm] = Form.useForm(); const [generateForm] = Form.useForm();
@@ -142,8 +142,12 @@ export default function Outline() {
try { try {
await deleteOutline(id); await deleteOutline(id);
message.success('删除成功'); message.success('删除成功');
// 删除后刷新大纲列表,确保显示最新的顺序 // 删除后刷新大纲列表和项目信息,更新字数显示
await refreshOutlines(); await refreshOutlines();
if (currentProject?.id) {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
} catch { } catch {
message.error('删除失败'); message.error('删除失败');
} }
@@ -745,7 +749,12 @@ export default function Outline() {
await Promise.all(deletePromises); await Promise.all(deletePromises);
message.success(`已删除《${outlineTitle}》展开的所有 ${chapters.length} 个章节`); message.success(`已删除《${outlineTitle}》展开的所有 ${chapters.length} 个章节`);
refreshOutlines(); await refreshOutlines();
// 刷新项目信息以更新字数显示
if (currentProject?.id) {
const updatedProject = await projectApi.getProject(currentProject.id);
setCurrentProject(updatedProject);
}
} catch (error: any) { } catch (error: any) {
message.error(error.response?.data?.detail || '删除章节失败'); message.error(error.response?.data?.detail || '删除章节失败');
} }
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
MuMuAINovel 服务启动脚本
用于快速启动后端服务
"""
import sys
import os
from pathlib import Path
# 将backend目录添加到Python路径
backend_dir = Path(__file__).parent / "backend"
sys.path.insert(0, str(backend_dir))
import uvicorn
from app.config import settings
if __name__ == "__main__":
print("=" * 60)
print(f"🚀 启动 {settings.app_name} v{settings.app_version}")
print("=" * 60)
print(f"📍 服务地址: http://{settings.app_host}:{settings.app_port}")
print(f"📚 API文档: http://{settings.app_host}:{settings.app_port}/docs")
print(f"🔧 调试模式: {'启用' if settings.debug else '禁用'}")
print(f"🗄️ 数据库: PostgreSQL")
print("=" * 60)
print()
uvicorn.run(
"app.main:app",
host=settings.app_host,
port=settings.app_port,
reload=settings.debug,
log_level="info"
)