From 5060cf3addb676b2e3f667779bbdde2e9a4c240f Mon Sep 17 00:00:00 2001 From: yuwangi Date: Fri, 17 Apr 2026 12:09:56 +0800 Subject: [PATCH 1/2] .3719011831386640:337e2ed897d270588944b4b24a8823a2_69e1a94a62ddd48e15758510.69e1aceb62ddd48e15758514.69e1aceac5004366ac9072b0:Trae CN.T(2026/4/17 11:45:47) --- apps/backend/src/routes/novel.routes.ts | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/routes/novel.routes.ts b/apps/backend/src/routes/novel.routes.ts index 289bb34..fe6d906 100644 --- a/apps/backend/src/routes/novel.routes.ts +++ b/apps/backend/src/routes/novel.routes.ts @@ -466,9 +466,35 @@ router.delete( "/:novelId/characters/:characterId", async (req: AuthRequest, res, next) => { try { + const { novelId, characterId } = req.params; + + const allCharacters = await db.query.characters.findMany({ + where: eq(schema.characters.novelId, novelId), + }); + + for (const char of allCharacters) { + if (char.id === characterId) continue; + + if (char.relationships && char.relationships.length > 0) { + const filteredRelationships = char.relationships.filter( + (r) => r.characterId !== characterId, + ); + + if (filteredRelationships.length !== char.relationships.length) { + await db + .update(schema.characters) + .set({ + relationships: filteredRelationships, + updatedAt: new Date(), + }) + .where(eq(schema.characters.id, char.id)); + } + } + } + await db .delete(schema.characters) - .where(eq(schema.characters.id, req.params.characterId)); + .where(eq(schema.characters.id, characterId)); res.status(204).send(); } catch (error) { From fc656507050f12a6a936a2bce43cbbeaf42b11e0 Mon Sep 17 00:00:00 2001 From: yuwangi Date: Fri, 17 Apr 2026 12:26:44 +0800 Subject: [PATCH 2/2] .3719011831386640:f0acc782365d44418e0d849281a080a9_69e1a94a62ddd48e15758510.69e1b35b62ddd48e157585db.69e1b35bc5004366ac9072b3:Trae CN.T(2026/4/17 12:13:15) --- apps/backend/src/routes/novel.routes.ts | 6 +- .../components/novel/CharacterManager.tsx | 241 ++++++++++++------ 2 files changed, 170 insertions(+), 77 deletions(-) diff --git a/apps/backend/src/routes/novel.routes.ts b/apps/backend/src/routes/novel.routes.ts index fe6d906..36f7187 100644 --- a/apps/backend/src/routes/novel.routes.ts +++ b/apps/backend/src/routes/novel.routes.ts @@ -467,17 +467,19 @@ router.delete( async (req: AuthRequest, res, next) => { try { const { novelId, characterId } = req.params; + const targetCharacterId = String(characterId).toLowerCase(); const allCharacters = await db.query.characters.findMany({ where: eq(schema.characters.novelId, novelId), }); for (const char of allCharacters) { - if (char.id === characterId) continue; + const currentCharId = String(char.id).toLowerCase(); + if (currentCharId === targetCharacterId) continue; if (char.relationships && char.relationships.length > 0) { const filteredRelationships = char.relationships.filter( - (r) => r.characterId !== characterId, + (r) => String(r.characterId).toLowerCase() !== targetCharacterId, ); if (filteredRelationships.length !== char.relationships.length) { diff --git a/apps/frontend/components/novel/CharacterManager.tsx b/apps/frontend/components/novel/CharacterManager.tsx index 17ab9f1..5c80495 100644 --- a/apps/frontend/components/novel/CharacterManager.tsx +++ b/apps/frontend/components/novel/CharacterManager.tsx @@ -1,14 +1,22 @@ -'use client'; +"use client"; -import { useState } from 'react'; -import { Users, Plus, Edit2, Trash2, Network, LayoutGrid, X } from 'lucide-react'; -import RelationshipGraph from './RelationshipGraph'; -import { Button } from '@/components/ui/button'; -import { Modal } from '@/components/ui/modal'; -import { Input } from '@/components/ui/input'; -import { Textarea } from '@/components/ui/textarea'; -import { novelsAPI } from '@/lib/api'; -import { toast } from 'sonner'; +import { useState } from "react"; +import { + Users, + Plus, + Edit2, + Trash2, + Network, + LayoutGrid, + X, +} from "lucide-react"; +import RelationshipGraph from "./RelationshipGraph"; +import { Button } from "@/components/ui/button"; +import { Modal } from "@/components/ui/modal"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { novelsAPI } from "@/lib/api"; +import { toast } from "sonner"; interface Character { id: string; @@ -26,38 +34,52 @@ interface CharacterManagerProps { onUpdate: () => void; } -export default function CharacterManager({ novelId, characters, onUpdate }: CharacterManagerProps) { - const [viewMode, setViewMode] = useState<'card' | 'graph'>('card'); +export default function CharacterManager({ + novelId, + characters, + onUpdate, +}: CharacterManagerProps) { + const [viewMode, setViewMode] = useState<"card" | "graph">("card"); const [isModalOpen, setIsModalOpen] = useState(false); - const [editingCharacter, setEditingCharacter] = useState(null); + const [editingCharacter, setEditingCharacter] = useState( + null, + ); const [formData, setFormData] = useState({ - name: '', - role: '', - personality: '', - abilities: '', - currentState: '', + name: "", + role: "", + personality: "", + abilities: "", + currentState: "", relationships: [] as { characterId: string; relation: string }[], }); const handleOpenModal = (character?: Character) => { + const validCharacterIds = characters.map((c) => c.id); + if (character) { + const validRelationships = (character.relationships || []).filter((r) => + validCharacterIds.includes(r.characterId), + ); + setEditingCharacter(character); setFormData({ name: character.name, role: character.role, - personality: character.personality.join('、'), - abilities: character.abilities.map(a => `${a.name}:${a.level}`).join('、'), - currentState: character.currentState || '', - relationships: character.relationships || [], + personality: character.personality.join("、"), + abilities: character.abilities + .map((a) => `${a.name}:${a.level}`) + .join("、"), + currentState: character.currentState || "", + relationships: validRelationships, }); } else { setEditingCharacter(null); setFormData({ - name: '', - role: '', - personality: '', - abilities: '', - currentState: '', + name: "", + role: "", + personality: "", + abilities: "", + currentState: "", relationships: [], }); } @@ -66,46 +88,55 @@ export default function CharacterManager({ novelId, characters, onUpdate }: Char const handleSubmit = async () => { if (!formData.name) { - toast.error('请输入姓名'); + toast.error("请输入姓名"); return; } + const validCharacterIds = characters.map((c) => c.id); const data = { name: formData.name, role: formData.role, personality: formData.personality.split(/[,,、]/).filter(Boolean), - abilities: formData.abilities.split(/[,,、]/).filter(Boolean).map(a => { - const [name, level] = a.split(/[::]/); - return { name, level: parseInt(level) || 1 }; - }), + abilities: formData.abilities + .split(/[,,、]/) + .filter(Boolean) + .map((a) => { + const [name, level] = a.split(/[::]/); + return { name, level: parseInt(level) || 1 }; + }), currentState: formData.currentState, - relationships: formData.relationships.filter(r => r.characterId && r.relation), + relationships: formData.relationships.filter( + (r) => + r.characterId && + r.relation && + validCharacterIds.includes(r.characterId), + ), }; try { if (editingCharacter) { await novelsAPI.updateCharacter(novelId, editingCharacter.id, data); - toast.success('人物资料已更新'); + toast.success("人物资料已更新"); } else { await novelsAPI.createCharacter(novelId, data); - toast.success('人物添加成功'); + toast.success("人物添加成功"); } setIsModalOpen(false); onUpdate(); } catch (error) { - console.error('Failed to save character:', error); - toast.error('保存失败'); + console.error("Failed to save character:", error); + toast.error("保存失败"); } }; const handleDelete = async (id: string) => { - if (confirm('确定要删除这个人物吗?')) { + if (confirm("确定要删除这个人物吗?")) { try { await novelsAPI.deleteCharacter(novelId, id); - toast.success('人物已删除'); + toast.success("人物已删除"); onUpdate(); } catch (error) { - console.error('Failed to delete character:', error); - toast.error('删除失败'); + console.error("Failed to delete character:", error); + toast.error("删除失败"); } } }; @@ -113,19 +144,21 @@ export default function CharacterManager({ novelId, characters, onUpdate }: Char return (
-

人物卡

+

+ 人物卡 +

- ) : viewMode === 'graph' ? ( + ) : viewMode === "graph" ? ( ) : (
{characters.map((character) => ( -
+

{character.name}

-

{character.role}

+

+ {character.role} +