import { useState, useEffect, useMemo } from 'react'; import { Button, List, Modal, Form, Input, message, Empty, Space, Popconfirm, Card, Select, Radio, Tag, InputNumber, Tabs, Pagination, theme } 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); } interface OutlineStructureData { key_events?: string[]; key_points?: string[]; characters_involved?: string[]; characters?: unknown[]; scenes?: string[] | Array<{ location: string; characters: string[]; purpose: string; }>; emotion?: string; goal?: string; title?: string; summary?: string; content?: string; } function parseOutlineStructure(structure?: string): OutlineStructureData { if (!structure) return {}; try { return JSON.parse(structure) as OutlineStructureData; } catch (e) { console.error('解析structure失败:', e); return {}; } } 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 { token } = theme.useToken(); const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`; // ✅ 新增:记录场景区域的展开/折叠状态 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); }, []); // 大纲查询与分页状态 const [outlineSearchKeyword, setOutlineSearchKeyword] = useState(''); const [outlinePage, setOutlinePage] = useState(1); const [outlinePageSize, setOutlinePageSize] = useState(20); // 使用同步 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); } }; // 从后端返回字段直接构建展开状态,避免前端 N+1 请求 const outlineExpandStatus = useMemo(() => { const statusMap: Record = {}; outlines.forEach((outline) => { statusMap[outline.id] = Boolean(outline.has_chapters); }); return statusMap; }, [outlines]); // 统一预解析 structure,避免 render 阶段重复 JSON.parse const outlineStructureMap = useMemo(() => { const parsedMap: Record = {}; outlines.forEach((outline) => { parsedMap[outline.id] = parseOutlineStructure(outline.structure); }); return parsedMap; }, [outlines]); // 当角色确认数据变化时,初始化选中状态(默认全选) // 当组织确认数据变化时,初始化选中状态(默认全选) // 移除事件监听,避免无限循环 // Hook 内部已经更新了 store,不需要再次刷新 // 确保大纲按 order_index 排序 const sortedOutlines = [...outlines].sort((a, b) => a.order_index - b.order_index); // 前端查询过滤 const filteredOutlines = useMemo(() => { const keyword = outlineSearchKeyword.trim().toLowerCase(); if (!keyword) return sortedOutlines; return sortedOutlines.filter((outline) => { return ( String(outline.order_index).includes(keyword) || outline.title.toLowerCase().includes(keyword) || outline.content.toLowerCase().includes(keyword) ); }); }, [sortedOutlines, outlineSearchKeyword]); // 当前分页数据 const pagedOutlines = useMemo(() => { const start = (outlinePage - 1) * outlinePageSize; return filteredOutlines.slice(start, start + outlinePageSize); }, [filteredOutlines, outlinePage, outlinePageSize]); // 搜索词或页大小变化时,回到第一页 useEffect(() => { setOutlinePage(1); }, [outlineSearchKeyword, outlinePageSize]); // 数据变化导致页码越界时自动纠正 useEffect(() => { const maxPage = Math.max(1, Math.ceil(filteredOutlines.length / outlinePageSize)); if (outlinePage > maxPage) { setOutlinePage(maxPage); } }, [filteredOutlines.length, outlinePage, outlinePageSize]); if (!currentProject) return null; const handleOpenEditModal = (id: string) => { const outline = outlines.find(o => o.id === id); if (outline) { const structureData = outlineStructureMap[outline.id] || {}; // 解析角色/组织条目(兼容新旧格式) 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: (