优化图片工具栏避让并新增图片备注功能
This commit is contained in:
parent
6b87adc622
commit
4e0c0d9cd0
@ -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<ImageTextEditItem[]>(
|
||||
() => normalizeTextEditItems(data.textEditItems),
|
||||
);
|
||||
@ -227,6 +228,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
const [deletedTextItems, setDeletedTextItems] = useState<Array<{ id: string; originalText: string }>>([]);
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [draftTitle, setDraftTitle] = useState("");
|
||||
const [draftNote, setDraftNote] = useState("");
|
||||
|
||||
// 文件输入的 ref
|
||||
const fileInputRef = useRef<HTMLInputElement>(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<HTMLImageElement>) => {
|
||||
@ -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) {
|
||||
<Type className="h-4 w-4 mr-2" />
|
||||
编辑文字
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleAction('note')}>
|
||||
<StickyNote className="h-4 w-4 mr-2" />
|
||||
图片备注
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setDraftTitle(displayTitle);
|
||||
@ -1362,6 +1428,16 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
<p className="text-xs text-white line-clamp-2">{data.prompt}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.note && !isSelected && (
|
||||
<div
|
||||
className="pointer-events-none absolute top-2 left-2 inline-flex items-center gap-1 rounded-full bg-amber-100/95 px-2 py-1 text-[11px] text-amber-700 shadow-sm"
|
||||
title="该图片含有备注"
|
||||
>
|
||||
<StickyNote className="h-3 w-3" />
|
||||
备注
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 角度编辑对话框 */}
|
||||
@ -1465,7 +1541,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
</Dialog>
|
||||
|
||||
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
|
||||
{isSelected && data.src && typeof document !== 'undefined' && createPortal(
|
||||
{isSelected && data.src && !isEditingTitle && typeof document !== 'undefined' && createPortal(
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div
|
||||
className="fixed flex items-center gap-0.5 rounded-full shadow-xl px-2 py-1 z-[9999] bg-white/95 border border-gray-200 text-gray-800 dark:bg-gray-900/95 dark:border-gray-700 dark:text-gray-100"
|
||||
@ -1602,6 +1678,25 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
<TooltipContent side="bottom" className="text-xs">编辑文字</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* 图片备注 */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={isProcessing}
|
||||
className={cn(
|
||||
"h-8 w-8 rounded-full hover:bg-gray-100 dark:hover:bg-white/10 disabled:opacity-50 disabled:pointer-events-none",
|
||||
data.note && "text-amber-600 dark:text-amber-300"
|
||||
)}
|
||||
onClick={() => handleAction('note')}
|
||||
>
|
||||
<StickyNote className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs">图片备注</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* 提示词重绘 */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@ -1718,6 +1813,65 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* 图片备注面板 */}
|
||||
{isSelected && notePanelOpen && typeof document !== "undefined" && createPortal(
|
||||
<div
|
||||
className="fixed z-[9999] w-[300px] rounded-xl border border-amber-200 bg-amber-50/95 shadow-2xl backdrop-blur"
|
||||
style={{
|
||||
top: getNotePanelPosition().top,
|
||||
left: getNotePanelPosition().left,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-amber-200/80">
|
||||
<div className="flex items-center gap-2 text-amber-800">
|
||||
<StickyNote className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">图片备注</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-amber-700 hover:text-amber-900 hover:bg-amber-100"
|
||||
onClick={() => setNotePanelOpen(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3">
|
||||
<Textarea
|
||||
value={draftNote}
|
||||
onChange={(e) => setDraftNote(e.target.value)}
|
||||
placeholder="记录这张图的想法、待办或交付要求..."
|
||||
rows={6}
|
||||
className="resize-y bg-white/80 border-amber-200 focus-visible:ring-amber-300"
|
||||
/>
|
||||
<p className="mt-2 text-[11px] text-amber-700/80">
|
||||
备注仅在你点开时显示,不会直接覆盖在画面上。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-3 border-t border-amber-200/80">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-amber-700 hover:text-amber-900 hover:bg-amber-100"
|
||||
onClick={() => setNotePanelOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 text-white hover:bg-amber-700"
|
||||
onClick={handleSaveNote}
|
||||
>
|
||||
保存备注
|
||||
</Button>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{/* 文字编辑面板(显示在图片节点旁边) */}
|
||||
{isSelected && textEditPanelOpen && data.src && typeof document !== "undefined" && createPortal(
|
||||
<div
|
||||
|
||||
@ -42,6 +42,7 @@ export interface ImageTextEditItem {
|
||||
export interface ImageNodeData {
|
||||
src: string;
|
||||
title?: string;
|
||||
note?: string;
|
||||
prompt?: string;
|
||||
model?: string;
|
||||
status: NodeStatus;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user