import { useState, useEffect, useCallback } from 'react' import { Save, BookOpen, RefreshCw, Plus, Trash2, Sparkles, PenLine } from 'lucide-react' import { useTranslation } from 'react-i18next' import { useProjectStore } from '../../stores/project-store' import { useWorkflowStore } from '../../stores/workflow-store' import { useLayoutStore } from '../../stores/layout-store' import { ipc } from '../../services/ipc-client' import { loadDirectoryBlueprints, saveChapterBlueprint, saveAllBlueprints, createDirectoryWorkflow, type ChapterBlueprint, type DirectoryWorkflowParams, } from '../../services/workflows/directory-workflow' import { guardDirectoryGeneration } from '../../services/workflow-guards' import DirectoryConfigDialog from '../dialogs/DirectoryConfigDialog' import { Button } from '../ui/Button' import { Input } from '../ui/Input' import { Textarea } from '../ui/Textarea' import { Label } from '../ui/Label' import { NativeSelect } from '../ui/NativeSelect' import { cn } from '../../lib/utils' import { toast } from '../ui/Toast' import { confirm } from '../ui/Confirm' import { globalEventBus } from '../../shared/event-bus' const ROLES = ['建置', '铺垫', '发展', '冲突', '高潮', '转折', '收尾'] const ROLE_COLORS: Record = { 高潮: 'bg-red-500/20 text-red-400', 冲突: 'bg-orange-500/20 text-orange-400', 转折: 'bg-purple-500/20 text-purple-400', 建置: 'bg-blue-500/20 text-blue-400', 收尾: 'bg-green-500/20 text-green-400', } /** 章节蓝图编辑器 — 读写 directory.json */ export default function ChapterCardEditor() { const { t } = useTranslation('editors') const currentProject = useProjectStore(s => s.currentProject) // ✅ action 用 getState() 获取,不订阅 workflow store 高频更新 const startWorkflow = useWorkflowStore.getState().startWorkflow const addLog = useWorkflowStore.getState().addLog const [blueprints, setBlueprints] = useState([]) const [selectedIdx, setSelectedIdx] = useState(0) const [saving, setSaving] = useState(false) const [loading, setLoading] = useState(true) const [dirty, setDirty] = useState(false) // 下一个可写的章节号 const [nextWriteChapter, setNextWriteChapter] = useState(null) // 蓝图生成弹窗(替代原 inline 批量面板) const [showBlueprintDialog, setShowBlueprintDialog] = useState(false) const loadBlueprints = useCallback(async () => { if (!currentProject) return setLoading(true) try { const data = await loadDirectoryBlueprints() setBlueprints(data) if (data.length > 0) setSelectedIdx(0) // 获取下一个待写章节号 const maxFinalized = await ipc.invoke('db:draft-get-max-finalized-chapter') setNextWriteChapter(maxFinalized !== null ? maxFinalized + 1 : 1) } catch { addLog('error', t('chapterCard.readBlueprintFailed')) } setLoading(false) setDirty(false) }, [currentProject, addLog]) useEffect(() => { let mounted = true Promise.resolve().then(() => { if (mounted) loadBlueprints() }) return () => { mounted = false } }, [loadBlueprints]) // 监听工作流完成事件,如果蓝图生成完毕则自动刷新 useEffect(() => { return globalEventBus.on('WORKFLOW_COMPLETE', (payload) => { if (payload.type === 'directory') { loadBlueprints() } }) }, [loadBlueprints]) const selected = blueprints[selectedIdx] ?? null /** 更新选中章节蓝图的字段 */ const updateField = (key: K, value: ChapterBlueprint[K]) => { setBlueprints(prev => prev.map((b, i) => (i === selectedIdx ? { ...b, [key]: value } : b)) ) setDirty(true) } /** 保存当前章节蓝图 */ const handleSaveOne = async () => { if (!currentProject || !selected) return setSaving(true) await saveChapterBlueprint(selected) setSaving(false) setDirty(false) addLog('info', t('chapterCard.blueprintSaved', { chapter: selected.chapterNumber })) } /** 全量保存(每章写入独立 JSON 文件) */ const handleSaveAll = async () => { if (!currentProject) return setSaving(true) await saveAllBlueprints(blueprints) setSaving(false) setDirty(false) addLog('info', t('chapterCard.allBlueprintsSaved', { count: blueprints.length })) } /** 新建空章节 */ const handleAddChapter = () => { const maxNum = blueprints.reduce((m, b) => Math.max(m, b.chapterNumber), 0) const newBlueprint: ChapterBlueprint = { chapterNumber: maxNum + 1, title: '', role: '发展', purpose: '', keyEvents: '', characters: [], suspenseHook: '', userGuidance: '', notes: '', notesUpdatedAt: '', } setBlueprints(prev => [...prev, newBlueprint]) setSelectedIdx(blueprints.length) setDirty(true) } /** 删除选中章节 */ const handleDeleteChapter = async () => { if (!selected) return const ok = await confirm(t('chapterCard.deleteConfirmText', { chapter: selected.chapterNumber }), { title: t('chapterCard.deleteConfirmTitle'), confirmText: t('chapterCard.deleteConfirm'), danger: true, }) if (!ok) return const newList = blueprints.filter((_, i) => i !== selectedIdx) setBlueprints(newList) setSelectedIdx(Math.max(0, selectedIdx - 1)) setDirty(true) } /** 触发蓝图批量生成(来自 DirectoryConfigDialog 的确认回调) */ const handleBatchGenerate = async (params: DirectoryWorkflowParams) => { if (!currentProject) return // 前置校验:故事架构是否就绪 const guard = await guardDirectoryGeneration() if (!guard.ok) { // 校验失败:阻断并提示 addLog('error', t('chapterCard.guardFailed', { message: guard.message })) toast.warning(t('chapterCard.cannotStart', { message: guard.message })) return } if (guard.message) { // 有警告但允许继续:弹出确认 const yes = await confirm(t('chapterCard.guardWarning', { message: guard.message }), { title: t('chapterCard.guardWarningTitle'), confirmText: t('chapterCard.continueGeneration'), }) if (!yes) return } startWorkflow(createDirectoryWorkflow(params)) addLog('info', t('chapterCard.workflowStarted')) } /** * 写作此章 — 将当前蓝图信息注入创作弹窗 * 支持指定章节(默认为当前选中章) */ const handleWriteChapter = (bp: ChapterBlueprint) => { // 通过 layout-store openChapterCreation 传递预填参数,替代 window.dispatchEvent useLayoutStore.getState().openChapterCreation({ chapterNumber: bp.chapterNumber, title: bp.title, role: bp.role, purpose: bp.purpose, keyEvents: bp.keyEvents, characters: bp.characters.join('、'), userGuidance: bp.userGuidance || '', }) } if (loading) { return (
{t('chapterCard.loadingBlueprints')}
) } if (!currentProject) { return (
{t('chapterCard.openProjectFirst')}
) } return (
{/* 顶部工具栏 */}
{t('chapterCard.title')} {blueprints.length > 0 && ( {t('chapterCard.chapterCount', { count: blueprints.length })} )} {dirty && {t('chapterCard.unsaved')}}
{/* 写作入口 — 仅下一章可写时显示 */} {nextWriteChapter !== null && ( )} {/* AI 生成蓝图 → 弹出 DirectoryConfigDialog */} {dirty && ( )}
{/* 蓝图生成配置弹窗 */} setShowBlueprintDialog(false)} existingCount={blueprints.length} onConfirm={handleBatchGenerate} /> {/* 主区域:左侧列表 + 右侧编辑 */}
{/* 左侧章节列表 */}
{blueprints.length === 0 ? (
{t('chapterCard.noBlueprints')}
) : (
{blueprints.map((bp, idx) => (
setSelectedIdx(idx)} >
{bp.chapterNumber} {bp.title || t('chapterCard.unnamed')}
{bp.role} {bp.userGuidance && ( {t('chapterCard.hasGuidance')} )} {bp.notes && ( {t('chapterCard.hasNotes')} )}
))}
)}
{/* 右侧编辑区 */}
{selected ? (
{/* 编辑区头部 */}

{t('chapterCard.chapterTitle', { chapter: selected.chapterNumber, title: selected.title || t('chapterCard.unnamed') })}

{/* 仅下一章允许写作 */} {nextWriteChapter !== null && selected.chapterNumber === nextWriteChapter && ( )}
{/* 基本信息 */}
updateField('chapterNumber', (e.target.value === '' ? '' : parseInt(e.target.value)) as number)} onBlur={() => { const v = Number(selected.chapterNumber); if (!v || v < 1) updateField('chapterNumber', 1) }} />
updateField('title', e.target.value)} placeholder={t('chapterCard.chapterTitlePlaceholder')} />
updateField('role', e.target.value)}> {ROLES.map(r => )}
updateField('characters', e.target.value.split(/[,,、\s]+/).filter(Boolean))} placeholder={t('chapterCard.charactersPlaceholder')} />