diff --git a/backend/app/schemas/outline.py b/backend/app/schemas/outline.py index a31cbbe..af4bb23 100644 --- a/backend/app/schemas/outline.py +++ b/backend/app/schemas/outline.py @@ -88,8 +88,8 @@ class OutlineUpdate(BaseModel): """更新大纲的请求模型""" title: Optional[str] = None content: Optional[str] = None + structure: Optional[str] = Field(None, description="结构化大纲数据(JSON)") # order_index 不允许通过普通更新修改,只能通过 reorder_outlines 接口批量调整 - # structure 暂不支持修改 class OutlineResponse(BaseModel): diff --git a/frontend/src/pages/Chapters.tsx b/frontend/src/pages/Chapters.tsx index 60bd371..e9accb8 100644 --- a/frontend/src/pages/Chapters.tsx +++ b/frontend/src/pages/Chapters.tsx @@ -592,12 +592,17 @@ export default function Chapters() { if (!currentProject) return null; - // 获取人称的中文显示文本 + // 获取人称的中文显示文本(同时支持中英文值) const getNarrativePerspectiveText = (perspective?: string): string => { const texts: Record = { + // 英文值映射(向后兼容) 'first_person': '第一人称(我)', 'third_person': '第三人称(他/她)', 'omniscient': '全知视角', + // 中文值映射(项目设置使用) + '第一人称': '第一人称(我)', + '第三人称': '第三人称(他/她)', + '全知视角': '全知视角', }; return texts[perspective || ''] || '第三人称(默认)'; }; @@ -2419,9 +2424,9 @@ export default function Chapters() { allowClear disabled={isGenerating} > - 第一人称(我) - 第三人称(他/她) - 全知视角 + 第一人称(我) + 第三人称(他/她) + 全知视角 {temporaryNarrativePerspective && (
diff --git a/frontend/src/pages/Outline.tsx b/frontend/src/pages/Outline.tsx index eadad87..855f469 100644 --- a/frontend/src/pages/Outline.tsx +++ b/frontend/src/pages/Outline.tsx @@ -3,11 +3,10 @@ import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Ca import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { useOutlineSync } from '../store/hooks'; -import { cardStyles } from '../components/CardStyles'; import { SSEPostClient } from '../utils/sseClient'; import { SSEProgressModal } from '../components/SSEProgressModal'; -import { outlineApi, chapterApi, projectApi } from '../services/api'; -import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError } from '../types'; +import { outlineApi, chapterApi, projectApi, characterApi } from '../services/api'; +import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError, Character } from '../types'; // 角色预测数据类型 interface PredictedCharacter { @@ -113,9 +112,13 @@ export default function Outline() { const [manualCreateForm] = Form.useForm(); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isExpanding, setIsExpanding] = useState(false); + const [projectCharacters, setProjectCharacters] = useState>([]); // ✅ 新增:记录每个大纲的展开状态 const [outlineExpandStatus, setOutlineExpandStatus] = useState>({}); + + // ✅ 新增:记录场景区域的展开/折叠状态 + const [scenesExpandStatus, setScenesExpandStatus] = useState>({}); // 角色确认相关状态 const [characterConfirmData, setCharacterConfirmData] = useState(null); @@ -158,14 +161,32 @@ export default function Outline() { deleteOutline } = useOutlineSync(); - // 初始加载大纲列表 + // 初始加载大纲列表和角色列表 useEffect(() => { if (currentProject?.id) { refreshOutlines(); + // 加载项目角色列表 + loadProjectCharacters(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentProject?.id]); // 只依赖 ID,不依赖函数 + // 加载项目角色列表 + const loadProjectCharacters = async () => { + if (!currentProject?.id) return; + try { + const characters = await characterApi.getCharacters(currentProject.id); + setProjectCharacters( + characters.map((char: Character) => ({ + label: char.name, + value: char.name + })) + ); + } catch (error) { + console.error('加载角色列表失败:', error); + } + }; + // ✅ 新增:加载所有大纲的展开状态 useEffect(() => { const loadExpandStatus = async () => { @@ -216,21 +237,76 @@ export default function Outline() { const handleOpenEditModal = (id: string) => { const outline = outlines.find(o => o.id === id); if (outline) { - editForm.setFieldsValue(outline); + // 解析structure数据 + let structureData: { + characters?: string[]; + scenes?: string[] | Array<{ + location: string; + characters: string[]; + purpose: string; + }>; + key_points?: string[]; + emotion?: string; + goal?: string; + } = {}; + + if (outline.structure) { + try { + structureData = JSON.parse(outline.structure); + } catch (e) { + console.error('解析structure失败:', e); + } + } + + // 处理场景数据 - 可能是字符串数组或对象数组 + let scenesText = ''; + if (structureData.scenes) { + if (typeof structureData.scenes[0] === 'string') { + // 字符串数组格式 + scenesText = (structureData.scenes as string[]).join('\n'); + } else { + // 对象数组格式 + scenesText = (structureData.scenes as Array<{location: string; characters: string[]; purpose: string}>) + .map(s => `${s.location}|${(s.characters || []).join('、')}|${s.purpose}`) + .join('\n'); + } + } + + // 处理情节要点数据 + const keyPointsText = structureData.key_points ? structureData.key_points.join('\n') : ''; + + // 设置表单初始值 + editForm.setFieldsValue({ + title: outline.title, + content: outline.content, + characters: structureData.characters || [], + scenes: scenesText, + key_points: keyPointsText, + emotion: structureData.emotion || '', + goal: structureData.goal || '' + }); + modalApi.confirm({ title: '编辑大纲', - width: 600, + width: 800, centered: true, + styles: { + body: { + maxHeight: 'calc(100vh - 200px)', + overflowY: 'auto' + } + }, content: (
@@ -239,8 +315,67 @@ export default function Outline() { label="内容" name="content" rules={[{ required: true, message: '请输入内容' }]} + style={{ marginBottom: 12 }} > -