import { useState, useEffect } from 'react'; import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs } from 'antd'; import { EditOutlined, DeleteOutlined, ThunderboltOutlined, BranchesOutlined, AppstoreAddOutlined, CheckCircleOutlined, ExclamationCircleOutlined, PlusOutlined, FileTextOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { useOutlineSync } from '../store/hooks'; import { SSEPostClient } from '../utils/sseClient'; import { SSEProgressModal } from '../components/SSEProgressModal'; import { outlineApi, chapterApi, projectApi, characterApi } from '../services/api'; import type { OutlineExpansionResponse, BatchOutlineExpansionResponse, ChapterPlanItem, ApiError, Character } from '../types'; // 大纲生成请求数据类型 interface OutlineGenerateRequestData { project_id: string; genre: string; theme: string; chapter_count: number; narrative_perspective: string; target_words: number; requirements?: string; mode: 'auto' | 'new' | 'continue'; story_direction?: string; plot_stage: 'development' | 'climax' | 'ending'; model?: string; provider?: string; } // 跳过的大纲信息类型 interface SkippedOutlineInfo { outline_id: string; outline_title: string; reason: string; } // 场景类型 interface SceneInfo { location: string; characters: string[]; purpose: string; } // 角色/组织条目类型(新格式) interface CharacterEntry { name: string; type: 'character' | 'organization'; } /** * 解析 characters 字段,兼容新旧格式 * 旧格式: string[] -> 全部当作 character * 新格式: {name: string, type: "character"|"organization"}[] */ function parseCharacterEntries(characters: unknown): CharacterEntry[] { if (!Array.isArray(characters) || characters.length === 0) return []; return characters.map((entry) => { if (typeof entry === 'string') { // 旧格式:纯字符串,默认为 character return { name: entry, type: 'character' as const }; } if (typeof entry === 'object' && entry !== null && 'name' in entry) { // 新格式:带类型标识的对象 return { name: (entry as { name: string }).name, type: ((entry as { type?: string }).type === 'organization' ? 'organization' : 'character') as 'character' | 'organization' }; } return null; }).filter((e): e is CharacterEntry => e !== null); } /** 从 entries 中提取角色名称列表 */ function getCharacterNames(entries: CharacterEntry[]): string[] { return entries.filter(e => e.type === 'character').map(e => e.name); } /** 从 entries 中提取组织名称列表 */ function getOrganizationNames(entries: CharacterEntry[]): string[] { return entries.filter(e => e.type === 'organization').map(e => e.name); } const { TextArea } = Input; export default function Outline() { const { currentProject, outlines, setCurrentProject } = useStore(); const [isGenerating, setIsGenerating] = useState(false); const [editForm] = Form.useForm(); const [generateForm] = Form.useForm(); const [expansionForm] = Form.useForm(); const [modalApi, contextHolder] = Modal.useModal(); const [batchExpansionForm] = Form.useForm(); 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>({}); // 缓存批量展开的规划数据,避免重复AI调用 const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState(null); // 批量展开预览的状态 const [batchPreviewVisible, setBatchPreviewVisible] = useState(false); const [batchPreviewData, setBatchPreviewData] = useState(null); const [selectedOutlineIdx, setSelectedOutlineIdx] = useState(0); const [selectedChapterIdx, setSelectedChapterIdx] = useState(0); // SSE进度状态 const [sseProgress, setSSEProgress] = useState(0); const [sseMessage, setSSEMessage] = useState(''); const [sseModalVisible, setSSEModalVisible] = useState(false); useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth <= 768); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // 使用同步 hooks const { refreshOutlines, updateOutline, 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 () => { if (outlines.length === 0) return; const statusMap: Record = {}; for (const outline of outlines) { try { const chapters = await outlineApi.getOutlineChapters(outline.id); statusMap[outline.id] = chapters.has_chapters; } catch (error) { console.error(`加载大纲 ${outline.id} 状态失败:`, error); statusMap[outline.id] = false; } } setOutlineExpandStatus(statusMap); }; loadExpandStatus(); }, [outlines]); // 当角色确认数据变化时,初始化选中状态(默认全选) // 当组织确认数据变化时,初始化选中状态(默认全选) // 移除事件监听,避免无限循环 // Hook 内部已经更新了 store,不需要再次刷新 if (!currentProject) return null; // 确保大纲按 order_index 排序 const sortedOutlines = [...outlines].sort((a, b) => a.order_index - b.order_index); const handleOpenEditModal = (id: string) => { const outline = outlines.find(o => o.id === id); if (outline) { // 解析structure数据 let structureData: { characters?: unknown[]; // 兼容新旧格式 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); } } // 解析角色/组织条目(兼容新旧格式) const editEntries = parseCharacterEntries(structureData.characters); const editCharNames = getCharacterNames(editEntries); const editOrgNames = getOrganizationNames(editEntries); // 处理场景数据 - 可能是字符串数组或对象数组 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: editCharNames, organizations: editOrgNames, scenes: scenesText, key_points: keyPointsText, emotion: structureData.emotion || '', goal: structureData.goal || '' }); modalApi.confirm({ title: '编辑大纲', width: 800, centered: true, styles: { body: { maxHeight: 'calc(100vh - 200px)', overflowY: 'auto' } }, content: (