1
0
Эх сурвалжийг харах

feat: 草稿章节支持编辑,原始内容备份到rawContent

Mochocyang 2 сар өмнө
parent
commit
6d9d33e1b1

+ 94 - 1
src/components/novel/story-simulation/story-draft-view.tsx

@@ -1,8 +1,10 @@
 import { useState, useEffect } from "react"
 import { useTranslation } from "react-i18next"
-import { ArrowLeft, Check, Copy, Download, FileText, BookOpen } from "lucide-react"
+import { ArrowLeft, Check, Copy, Download, FileText, BookOpen, Pencil, Save } from "lucide-react"
 import { Button } from "@/components/ui/button"
 import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import type { StoryDraft } from "@/lib/novel/story-simulation/types"
 import {
   Dialog,
   DialogContent,
@@ -27,6 +29,7 @@ export function StoryDraftView({ onBack }: StoryDraftViewProps) {
   const projectPath = useWikiStore((s) => s.project?.path)
   const currentFramework = useStorySimulationStore((s) => s.currentFramework)
   const draft = useStorySimulationStore((s) => s.currentDraft)
+  const setCurrentDraft = useStorySimulationStore((s) => s.setCurrentDraft)
   const setError = useStorySimulationStore((s) => s.setError)
   const setActiveView = useWikiStore((s) => s.setActiveView)
   const setSelectedFile = useWikiStore((s) => s.setSelectedFile)
@@ -45,6 +48,9 @@ export function StoryDraftView({ onBack }: StoryDraftViewProps) {
   const [autoStartChapter, setAutoStartChapter] = useState(true)
   const [selectedIndices, setSelectedIndices] = useState<number[]>([])
   const [importProgress, setImportProgress] = useState<{ current: number; total: number; title: string } | null>(null)
+  const [editingChapterIdx, setEditingChapterIdx] = useState<number | null>(null)
+  const [editContent, setEditContent] = useState("")
+  const [editTitle, setEditTitle] = useState("")
 
   // 打开对话框时自动计算下一个章节号,并默认全选
   useEffect(() => {
@@ -168,6 +174,32 @@ export function StoryDraftView({ onBack }: StoryDraftViewProps) {
     }
   }
 
+  const openEditDialog = (idx: number) => {
+    if (!draft) return
+    const chapter = draft.chapters[idx]
+    setEditTitle(chapter.title)
+    setEditContent(chapter.content)
+    setEditingChapterIdx(idx)
+  }
+
+  const saveEdit = () => {
+    if (editingChapterIdx === null || !draft) return
+    const updatedDraft: StoryDraft = {
+      ...draft,
+      chapters: draft.chapters.map((ch, i) =>
+        i === editingChapterIdx
+          ? { ...ch, title: editTitle.trim() || ch.title, content: editContent }
+          : ch,
+      ),
+    }
+    setCurrentDraft(updatedDraft)
+    setEditingChapterIdx(null)
+  }
+
+  const cancelEdit = () => {
+    setEditingChapterIdx(null)
+  }
+
   const allSelected = draft && selectedIndices.length === draft.chapters.length
 
   return (
@@ -220,10 +252,24 @@ export function StoryDraftView({ onBack }: StoryDraftViewProps) {
               <h3 className="mb-2 flex items-center gap-2 font-medium">
                 <FileText className="h-4 w-4 text-muted-foreground" />
                 {chapter.title}
+                <Button
+                  variant="ghost"
+                  size="sm"
+                  className="ml-auto h-7 w-7 p-0 opacity-50 hover:opacity-100"
+                  onClick={() => openEditDialog(idx)}
+                  title="编辑章节"
+                >
+                  <Pencil className="h-3.5 w-3.5" />
+                </Button>
               </h3>
               <p className="whitespace-pre-wrap text-sm leading-relaxed">
                 {chapter.content}
               </p>
+              {chapter.rawContent && chapter.rawContent !== chapter.content && (
+                <div className="mt-2 rounded bg-amber-50 px-2 py-1 text-xs text-amber-600 dark:bg-amber-950/30 dark:text-amber-400">
+                  ✓ 已编辑(原始内容已备份)
+                </div>
+              )}
             </div>
           ))}
         </div>
@@ -411,6 +457,53 @@ export function StoryDraftView({ onBack }: StoryDraftViewProps) {
           </DialogFooter>
         </DialogContent>
       </Dialog>
+
+      {/* 编辑章节对话框 */}
+      <Dialog open={editingChapterIdx !== null} onOpenChange={(open) => {
+        if (!open) cancelEdit()
+      }}>
+        <DialogContent className="max-h-[90vh] max-w-3xl">
+          <DialogHeader>
+            <DialogTitle>编辑章节</DialogTitle>
+            <DialogDescription>
+              编辑后的内容将用于导入到章节库。
+            </DialogDescription>
+          </DialogHeader>
+          <div className="space-y-3">
+            <div>
+              <label className="mb-1 block text-xs font-medium text-muted-foreground">章节标题</label>
+              <Input
+                value={editTitle}
+                onChange={(e) => setEditTitle(e.target.value)}
+                className="text-sm"
+              />
+            </div>
+            <div>
+              <div className="mb-1 flex items-center justify-between">
+                <label className="text-xs font-medium text-muted-foreground">章节内容</label>
+                <span className="text-xs text-muted-foreground">
+                  {editContent.length} 字
+                </span>
+              </div>
+              <Textarea
+                value={editContent}
+                onChange={(e) => setEditContent(e.target.value)}
+                className="min-h-[50vh] text-sm leading-relaxed"
+                autoFocus
+              />
+            </div>
+          </div>
+          <DialogFooter>
+            <Button variant="outline" onClick={cancelEdit}>
+              放弃
+            </Button>
+            <Button onClick={saveEdit}>
+              <Save className="mr-1 h-3.5 w-3.5" />
+              保存修改
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
     </div>
   )
 }

+ 1 - 0
src/lib/novel/story-simulation/story-draft-generator.ts

@@ -161,6 +161,7 @@ export async function generateStoryDraft(options: DraftGenerationOptions): Promi
       title: node.title,
       content: trimmed,
       correspondingNode: node.index,
+      rawContent: trimmed,
     }
 
     chapters.push(chapter)