From d146fa198902a75fcac692466d5bbc599fc932a2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 22 Feb 2026 15:27:51 +0800 Subject: [PATCH] chore(ui): sync manual updates for image editor and canvas image node --- components/canvas/nodes/ImageNode.tsx | 4 + components/image-annotator.tsx | 1222 +++++++++++++------------ 2 files changed, 636 insertions(+), 590 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 4e507fe4e..94056d75b 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2022,6 +2022,8 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { imageUrl={data.src} onConfirm={handleAnnotateComplete} onCancel={() => setAnnotatorOpen(false)} + open={annotatorOpen} + onOpenChange={setAnnotatorOpen} /> )} @@ -2032,6 +2034,8 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { confirmButtonText="发送到对话" onConfirm={handleResizeEditorComplete} onCancel={() => setResizeAnnotatorOpen(false)} + open={resizeAnnotatorOpen} + onOpenChange={setResizeAnnotatorOpen} /> )} diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index 15a146264..1f56181b5 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -4,12 +4,27 @@ import React, { useRef, useState, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; import Link from "next/link"; import { useTheme } from "next-themes"; -import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2, ZoomIn, ZoomOut, Loader2, Move3D, Layers, Upload as UploadIcon, Image as ImageIcon, Droplet } from "lucide-react"; +import { X, Pencil, Circle, ArrowRight, Type, Check, Undo, Trash2, Scissors, Sparkles, Download, Expand, Move, Maximize2, ZoomIn, ZoomOut, Loader2, Move3D, Layers, Upload as UploadIcon, Image as ImageIcon, Droplet, Redo, MousePointer2 } from "lucide-react"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; +import { Slider } from "./ui/slider"; import { toast } from "sonner"; import { AngleEditDialog } from "./angle-edit-dialog"; import { SceneFusionView } from "./scene-fusion-view"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; type Tool = "pen" | "arrow" | "circle" | "text"; @@ -34,6 +49,8 @@ interface ImageAnnotatorProps { onHomeClick?: () => void; initialResizeMode?: boolean; // 是否直接进入尺寸调整模式 confirmButtonText?: string; + open?: boolean; // 兼容 Dialog 模式 + onOpenChange?: (open: boolean) => void; } const COLORS = [ @@ -57,6 +74,8 @@ export function ImageAnnotator({ onHomeClick, initialResizeMode = false, confirmButtonText = "发送到新对话", + open = true, + onOpenChange, }: ImageAnnotatorProps) { const canvasRef = useRef(null); const containerRef = useRef(null); @@ -87,6 +106,14 @@ export function ImageAnnotator({ const fileInputRef = useRef(null); + // Handle open change + const handleOpenChange = (newOpen: boolean) => { + onOpenChange?.(newOpen); + if (!newOpen) { + onCancel(); + } + }; + // 上传图片到服务器获取 URL 的辅助函数 const uploadImageFile = async (file: File): Promise => { const formData = new FormData(); @@ -1226,11 +1253,6 @@ export function ImageAnnotator({ // 更完善的缩略图URL转换逻辑 if (downloadUrl.includes("-thumb") || downloadUrl.includes("/thumb/") || downloadUrl.includes("thumbnail")) { // 如果URL包含thumb标识,尝试转换为原图URL - // 处理各种可能的格式: - // - rmbg-thumb-xxx.png -> rmbg-orig-xxx.png - // - enhance-thumb-xxx.jpg -> enhance-orig-xxx.jpg - // - extend-thumb-xxx.jpg -> extend-orig-xxx.jpg - // - xxx/thumb/xxx -> xxx/orig/xxx downloadUrl = downloadUrl .replace(/-thumb-/g, "-orig-") .replace(/-thumb\./g, "-orig.") @@ -1377,592 +1399,612 @@ export function ImageAnnotator({ } }; - // 弹窗内容 - const modalContent = ( -
- {/* 弹窗容器 - 全屏显示 */} -
e.stopPropagation()} - > - {/* 顶部标题栏 - 始终深色模式,高度与首页导航栏一致 */} -
- { - e.stopPropagation(); - if (onHomeClick) { - e.preventDefault(); - onHomeClick(); - } - }} - > - {siteLogoDark || siteLogo ? ( - <> - {siteName} - {siteName} - - ) : ( - {siteName} - )} - -
- - - - - - - - {!showSceneFusion && ( - <> - - - - )} - - {!showSceneFusion && ( - - )} -
-
- - {showSceneFusion ? ( -
- setShowSceneFusion(false)} - // 直接复用当前编辑器中的图片 URL,进入「详情替换」时自动带入产品图 - initialProductImage={processedImageUrl ?? originalImageUrl ?? null} - /> -
- ) : ( - <> - {/* 工具选择栏 / 尺寸调整工具栏 */} - {isResizeMode ? ( - /* 尺寸调整模式下显示尺寸调整工具栏 */ -
- {/* 比例选择和尺寸输入在同一行 */} -
- {/* 比例选择 */} -
- -
- {aspectRatios.map((ratio) => ( - - ))} -
-
- - {/* 尺寸输入 */} -
-
- - handleWidthChange(Number.parseInt(e.target.value) || 1)} - className="w-24 h-8" - min={1} - max={10000} - /> - px -
-
- - handleHeightChange(Number.parseInt(e.target.value) || 1)} - className="w-24 h-8" - min={1} - max={10000} - /> - px -
-
- - setResizeBackgroundColor(e.target.value)} - disabled={resizeBackgroundTransparent} - className="w-12 h-8 rounded border cursor-pointer disabled:opacity-50" - /> -
-
- -
-
- - {/* 操作按钮 */} -
- - - - -
-
-
- ) : ( - /* 非尺寸调整模式下显示图片标注工具栏 */ -
- {/* 工具按钮 */} -
- - - - -
- - {/* 颜色选择 */} -
- {COLORS.map((c) => ( -
- - {/* 线宽选择 */} -
- {LINE_WIDTHS.map((w) => ( - - ))} -
-
- )} - - {/* 画布区域 - 并排显示原图和处理后的图片 */} -
- {isLoading ? ( - /* 加载动画 */ -
-
- - - -
- 正在加载图片... -
- ) : !image ? ( - /* 没有图片时显示上传区域 */ -
-
fileInputRef.current?.click()} - onDrop={handleDrop} - onDragOver={handleDragOver} - > -
- -
-

上传图片以开始编辑

-

拖拽图片到此处、点击选择或 Ctrl+V 粘贴

-

支持 JPG, PNG, WebP 格式,最大 50MB

- - -
-
- ) : isResizeMode ? ( - /* 尺寸调整模式 - 左右对比布局 */ - <> - {/* 左侧:原图 */} -
-
原图
-
- {image && ( - 原图 - )} -
-
- - {/* 右侧:尺寸调整预览画布 */} -
-
- 调整尺寸 - {(processedImageUrl || processedImageBase64) && processType === "resize" && ( - - )} -
-
-
- -
- 画布: {resizeCanvasWidth} x {resizeCanvasHeight} | 缩放: {(resizeImageScale * 100).toFixed(0)}% | 拖拽移动图片,滚轮缩放 -
-
-
-
- - ) : ( - <> - {/* 原图画布 */} -
-
原图
- -
- - {/* 处理后的图片展示区域 */} - {processedImageBase64 && ( -
-
- - {processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : processType === "watermark" ? "去水印结果" : "拓展结果"} - - -
-
- {processedImageUrl ? ( - {processType - ) : processedImageBase64 ? ( - {processType - ) : null} -
-
- )} - - )} -
- - )} -
- - {/* 文字输入弹窗 */} - {textPosition && ( -
setTextPosition(null)}> -
e.stopPropagation()}> -
输入标注文字
- setTextInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleAddText(); - }} - className="mb-3 w-64 rounded-md border bg-muted px-3 py-2 text-foreground outline-none focus:border-primary" - placeholder="输入文字..." - autoFocus - /> -
- - -
-
-
- )} - - {/* 角度调整对话框 - 包裹在阻止事件冒泡的 div 中 */} - {showAngleEditDialog && originalImageUrl && ( -
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}> - -
- )} -
- ); - - // 使用 Portal 渲染到 document.body,避免受父元素定位影响 if (!mounted) { return null; } - return createPortal(modalContent, document.body); + return ( + + + + 图片编辑器 + 编辑、标注或处理图片。 + + + {/* Top Header - Minimal */} +
+
+ Image Editor +
+ + {isResizeMode ? "调整尺寸" : "图片标注与处理"} + +
+
+ + {/* Floating Toolbar - Top (Tools & Actions) */} + {!showSceneFusion && ( +
+
+ + {!isResizeMode && ( + <> + {/* Drawing Tools */} + + + + + + 画笔 + + + + + + + 箭头 + + + + + + + 圆圈 + + + + + + + 文字 + + + +
+ + {/* Colors */} +
+ {COLORS.slice(0, 5).map((c) => ( +
+ +
+ + {/* Line Width */} +
+ {LINE_WIDTHS.map((w) => ( + + ))} +
+ +
+ + )} + + {/* AI Actions */} + + {!isResizeMode && ( + <> + + + + + 移除背景 + + + + + + + 变清晰 + + + + + + + 图像拓展 + + + + + + + 去水印 + + +
+ + + + + + 尺寸调整 + + + + + + + 角度调整 + + + + + + + 详情替换 + + +
+ + )} + + {/* History */} + {!isResizeMode && ( + <> + + + + + 撤销 + + + + + + + 清空 + + + )} + + {/* Resize Mode specific tools */} + {isResizeMode && ( + <> +
+ {aspectRatios.slice(0, 4).map((ratio) => ( + + ))} +
+
+
+ handleWidthChange(Number.parseInt(e.target.value) || 1)} + className="h-8 w-20 text-xs" + /> + x + handleHeightChange(Number.parseInt(e.target.value) || 1)} + className="h-8 w-20 text-xs" + /> +
+
+ + + + + 缩小 + + + + + + 放大 + + + )} + +
+
+ )} + + {/* Main Content Area */} +
+ {showSceneFusion ? ( +
+ setShowSceneFusion(false)} + initialProductImage={processedImageUrl ?? originalImageUrl ?? null} + /> +
+ ) : ( +
+
+ + {/* Main Canvas / Image Area */} +
+ {isLoading ? ( +
+
+ + + +
+ 正在加载图片... +
+ ) : !image ? ( +
+
fileInputRef.current?.click()} + onDrop={handleDrop} + onDragOver={handleDragOver} + > +
+ +
+

上传图片以开始编辑

+

拖拽图片到此处、点击选择或 Ctrl+V 粘贴

+

支持 JPG, PNG, WebP 格式,最大 50MB

+ + +
+
+ ) : isResizeMode ? ( + /* Resize Mode View */ +
+ {/* Source Image */} +
+
原图
+
+ {image && ( + 原图 + )} +
+
+ + {/* Preview Canvas */} +
+
+ 调整预览 + {(processedImageUrl || processedImageBase64) && processType === "resize" && ( + + )} +
+
+
+ +
+
+
+
+ ) : ( + /* Normal Mode View */ +
+
+ + + {/* Processed Result Overlay/Side-by-side could go here if we want splitscreen, but ImageAnnotator usually replaces or shows result in place? + Currently it shows result in a separate box if processed. + Let's keep the logic: if processedImage exists, show it side-by-side or replace? + Original code showed side-by-side. + */} + +
+ + {/* Processed Result (Side by Side) */} + {(processedImageUrl || processedImageBase64) && ( +
+
+ {processType === "rmbg" ? "抠图结果" : processType === "enhance" ? "清晰结果" : processType === "resize" ? "尺寸调整结果" : processType === "watermark" ? "去水印结果" : "拓展结果"} +
+
+ {processedImageUrl ? ( + Result + ) : processedImageBase64 ? ( + Result + ) : null} + + +
+
+ )} +
+ )} +
+
+ )} +
+ + {/* Floating Toolbar - Bottom (Confirm/Cancel) */} + {!showSceneFusion && ( +
+
+ +
+ +
+
+ )} + + {/* Text Input Modal */} + {textPosition && ( +
setTextPosition(null)}> +
e.stopPropagation()}> +
输入标注文字
+ setTextInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleAddText(); + }} + className="mb-3 w-64 rounded-md border bg-muted px-3 py-2 text-foreground outline-none focus:border-primary" + placeholder="输入文字..." + autoFocus + /> +
+ + +
+
+
+ )} + + {/* Angle Edit Dialog */} + {showAngleEditDialog && originalImageUrl && ( +
e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}> + +
+ )} + +
+ ); }