From 4e0c0d9cd0fbaf988f896fa173a233267a665828 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 8 Feb 2026 12:05:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E7=89=87=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=A0=8F=E9=81=BF=E8=AE=A9=E5=B9=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=A4=87=E6=B3=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/canvas/nodes/ImageNode.tsx | 164 +++++++++++++++++++++++++- types/canvas/index.ts | 1 + 2 files changed, 160 insertions(+), 5 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index d64ad93e2..bbd365f2b 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; -import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type } from "lucide-react"; +import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type, StickyNote } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -218,6 +218,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const [annotatorOpen, setAnnotatorOpen] = useState(false); const [fullscreenPreviewOpen, setFullscreenPreviewOpen] = useState(false); const [textEditPanelOpen, setTextEditPanelOpen] = useState(false); + const [notePanelOpen, setNotePanelOpen] = useState(false); const [textEditItems, setTextEditItems] = useState( () => normalizeTextEditItems(data.textEditItems), ); @@ -227,6 +228,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const [deletedTextItems, setDeletedTextItems] = useState>([]); const [isEditingTitle, setIsEditingTitle] = useState(false); const [draftTitle, setDraftTitle] = useState(""); + const [draftNote, setDraftNote] = useState(""); // 文件输入的 ref const fileInputRef = useRef(null); @@ -271,6 +273,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { setTextEditPanelOpen(false); setIsEditingTitle(false); setPromptDialogOpen(false); + setNotePanelOpen(false); } }, [isSelected]); @@ -285,6 +288,12 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { setRegeneratePrompt(data.prompt || ""); } }, [data.prompt, promptDialogOpen]); + + useEffect(() => { + if (notePanelOpen) { + setDraftNote(data.note || ""); + } + }, [data.note, notePanelOpen]); // 🆕 处理图片点击标注 const handlePickerClick = useCallback(async (e: React.MouseEvent) => { @@ -359,11 +368,22 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { // 获取工具栏位置 - 在渲染时直接计算 const getToolbarPosition = () => { - if (containerRef.current) { + if (containerRef.current && typeof window !== "undefined") { const rect = containerRef.current.getBoundingClientRect(); + const toolbarHeight = 40; + const titleHeight = 28; + const gap = 10; + let top = rect.top - toolbarHeight - titleHeight - gap; + if (top < 12) { + top = rect.top + 8; + } + + const minLeft = 80; + const maxLeft = window.innerWidth - 80; + const left = Math.min(Math.max(rect.left + rect.width / 2, minLeft), maxLeft); return { - top: rect.top - 50, - left: rect.left + rect.width / 2, + top, + left, }; } return { top: 0, left: 0 }; @@ -388,6 +408,25 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { return { top, left }; }; + const getNotePanelPosition = () => { + if (!containerRef.current || typeof window === "undefined") { + return { top: 0, left: 0 }; + } + + const rect = containerRef.current.getBoundingClientRect(); + const panelWidth = 300; + const spacing = 12; + const maxTop = Math.max(12, window.innerHeight - 360); + + let left = rect.right + spacing; + if (left + panelWidth > window.innerWidth - 12) { + left = Math.max(12, rect.left - panelWidth - spacing); + } + + const top = Math.min(Math.max(12, rect.top + 8), maxTop); + return { top, left }; + }; + const displayTitle = getImageNodeTitle(data, node.id); const handleSaveTitle = useCallback(() => { @@ -403,6 +442,24 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { toast.success("图片名称已更新"); }, [data, displayTitle, draftTitle, node.id, updateNode]); + const handleOpenNotePanel = useCallback(() => { + setDraftNote(data.note || ""); + setNotePanelOpen(true); + }, [data.note]); + + const handleSaveNote = useCallback(() => { + const nextNote = draftNote.trim(); + updateNode(node.id, { + data: { + ...data, + note: nextNote || undefined, + }, + }); + setDraftNote(nextNote); + setNotePanelOpen(false); + toast.success(nextNote ? "备注已保存" : "备注已清除"); + }, [data, draftNote, node.id, updateNode]); + const detectTextItems = useCallback( async (forceRefresh: boolean = false) => { if (!data.src) { @@ -710,6 +767,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 'scene-fusion': '详情替换', 'annotate': '图片标注', 'text-edit': '编辑文字', + 'note': '图片备注', 'prompt-regenerate': '提示词重绘', }; return names[action] || action; @@ -747,6 +805,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { await handleOpenTextEdit(); return; } + if (action === "note") { + handleOpenNotePanel(); + return; + } if (action === "prompt-regenerate") { setRegeneratePrompt(data.prompt || ""); setPromptDialogOpen(true); @@ -1330,6 +1392,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 编辑文字 + handleAction('note')}> + + 图片备注 + { setDraftTitle(displayTitle); @@ -1362,6 +1428,16 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {

{data.prompt}

)} + + {data.note && !isSelected && ( +
+ + 备注 +
+ )} {/* 角度编辑对话框 */} @@ -1465,7 +1541,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} - {isSelected && data.src && typeof document !== 'undefined' && createPortal( + {isSelected && data.src && !isEditingTitle && typeof document !== 'undefined' && createPortal(
编辑文字 + {/* 图片备注 */} + + + + + 图片备注 + + {/* 提示词重绘 */} @@ -1718,6 +1813,65 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { document.body )} + {/* 图片备注面板 */} + {isSelected && notePanelOpen && typeof document !== "undefined" && createPortal( +
e.stopPropagation()} + > +
+
+ + 图片备注 +
+ +
+ +
+