From d7ac4417d19326c4615bd57751cd269cb208f2e8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 6 Mar 2026 19:29:35 +0800 Subject: [PATCH] feat: sync latest canvas dialog and selection updates --- components/canvas/CanvasWorkspace.tsx | 8 +- components/canvas/nodes/ImageNode.tsx | 9 +- components/home/lovart-home.tsx | 1 + components/image-annotator.tsx | 149 ++++++++++++++++++++------ components/image-preview.tsx | 1 + components/preview-attachment.tsx | 1 + components/top-navbar.tsx | 1 + 7 files changed, 130 insertions(+), 40 deletions(-) diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index 7cdd3ba5a..66ac7ad33 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -837,7 +837,7 @@ export function CanvasWorkspace({ occlusionRight = 0 }: CanvasWorkspaceProps) { height: IMAGE_HANDLE_SIZE, right: -IMAGE_HANDLE_OFFSET, top: -IMAGE_HANDLE_OFFSET, - borderRadius: 999, + borderRadius: 0, border: `${SELECTION_STROKE_WIDTH}px solid ${SELECTION_COLOR}`, backgroundColor: "#ffffff", }, @@ -846,7 +846,7 @@ export function CanvasWorkspace({ occlusionRight = 0 }: CanvasWorkspaceProps) { height: IMAGE_HANDLE_SIZE, right: -IMAGE_HANDLE_OFFSET, bottom: -IMAGE_HANDLE_OFFSET, - borderRadius: 999, + borderRadius: 0, border: `${SELECTION_STROKE_WIDTH}px solid ${SELECTION_COLOR}`, backgroundColor: "#ffffff", }, @@ -855,7 +855,7 @@ export function CanvasWorkspace({ occlusionRight = 0 }: CanvasWorkspaceProps) { height: IMAGE_HANDLE_SIZE, left: -IMAGE_HANDLE_OFFSET, bottom: -IMAGE_HANDLE_OFFSET, - borderRadius: 999, + borderRadius: 0, border: `${SELECTION_STROKE_WIDTH}px solid ${SELECTION_COLOR}`, backgroundColor: "#ffffff", }, @@ -864,7 +864,7 @@ export function CanvasWorkspace({ occlusionRight = 0 }: CanvasWorkspaceProps) { height: IMAGE_HANDLE_SIZE, left: -IMAGE_HANDLE_OFFSET, top: -IMAGE_HANDLE_OFFSET, - borderRadius: 999, + borderRadius: 0, border: `${SELECTION_STROKE_WIDTH}px solid ${SELECTION_COLOR}`, backgroundColor: "#ffffff", }, diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index dd82d946f..66c3e8b24 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2157,7 +2157,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { {/* 提示词重绘对话框 */} - + @@ -2176,7 +2179,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { onChange={(e) => setRegeneratePrompt(e.target.value)} placeholder="例如:保持主体不变,把背景改成黄昏海边,氛围光影更柔和,细节更丰富" rows={6} - className="resize-y" + className="resize-y border-slate-300 bg-white opacity-100 dark:border-slate-700 dark:bg-slate-950" /> @@ -2191,7 +2194,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} - {isSelected && !isMultiSelected && data.src && !isEditingTitle && !sceneFusionDialogOpen && typeof document !== 'undefined' && createPortal( + {isSelected && !isMultiSelected && data.src && !isEditingTitle && !sceneFusionDialogOpen && !promptDialogOpen && !notePanelOpen && typeof document !== 'undefined' && createPortal(
diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index d06036e13..64da95cf6 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -3,6 +3,7 @@ import React, { useRef, useState, useEffect, useCallback } from "react"; import { createPortal } from "react-dom"; import Link from "next/link"; +import { useRouter } from "next/navigation"; import { useTheme } from "next-themes"; import { X, 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"; @@ -27,6 +28,7 @@ import { } from "@/components/ui/tooltip"; import { requestLongTaskResult } from "@/lib/client/long-task"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; +import { getCanvasLaunchStorageKey, type CanvasLaunchAttachment } from "@/lib/canvas-launch"; import { buildImageProxyUrl } from "@/lib/image-delivery"; type Tool = "pen" | "arrow" | "circle" | "text"; @@ -52,6 +54,7 @@ interface ImageAnnotatorProps { onHomeClick?: () => void; initialResizeMode?: boolean; // 是否直接进入尺寸调整模式 confirmButtonText?: string; + submitMode?: "confirm" | "publish-new-canvas"; open?: boolean; // 兼容 Dialog 模式 onOpenChange?: (open: boolean) => void; } @@ -62,10 +65,12 @@ export function ImageAnnotator({ onCancel, onHomeClick, initialResizeMode = false, - confirmButtonText = "发送到新对话", + confirmButtonText, + submitMode = "confirm", open = true, onOpenChange, }: ImageAnnotatorProps) { + const router = useRouter(); const canvasRef = useRef(null); const containerRef = useRef(null); const [ctx, setCtx] = useState(null); @@ -95,6 +100,9 @@ export function ImageAnnotator({ const fileInputRef = useRef(null); const objectUrlRef = useRef(null); + const resolvedConfirmButtonText = + confirmButtonText || + (submitMode === "publish-new-canvas" ? "发布到新的画布" : "发送到新对话"); const toCanvasLoadUrl = useCallback((url: string) => { return buildImageProxyUrl(url); }, []); @@ -1366,61 +1374,136 @@ export function ImageAnnotator({ } }; - // 确认 - const handleConfirm = async () => { - // 优先使用处理后的图片(如果有) + const inferImageContentType = useCallback((url: string) => { + const normalized = String(url || "").split("?")[0]?.toLowerCase() || ""; + if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg"; + if (normalized.endsWith(".webp")) return "image/webp"; + if (normalized.endsWith(".gif")) return "image/gif"; + return "image/png"; + }, []); + + const resolveConfirmedImageUrl = useCallback(async (): Promise => { if (processedImageUrl) { - // 如果有处理后的图片URL,直接使用它 - onConfirm(processedImageUrl); - return; + return processedImageUrl; } - + if (processedImageBase64) { - // 如果有处理后的图片Base64,先上传到服务器获取URL const mimeType = processType === "rmbg" || processType === "resize" ? "image/png" : "image/jpeg"; const dataUrl = `data:${mimeType};base64,${processedImageBase64}`; const ext = processType === "rmbg" || processType === "resize" ? "png" : "jpg"; const filename = `processed-${Date.now()}.${ext}`; - - try { - toast.info("正在上传图片..."); - const uploadedUrl = await uploadImageToServer(dataUrl, filename); - onConfirm(uploadedUrl); - } catch { - toast.error("上传失败,请重试"); - } - return; + toast.info("正在上传图片..."); + return await uploadImageToServer(dataUrl, filename); } - // 否则,导出标注后的图片 const canvas = canvasRef.current; - if (!canvas || !image) return; + if (!canvas || !image) return null; - // 创建一个新的 canvas 用于导出原始尺寸 const exportCanvas = document.createElement("canvas"); exportCanvas.width = image.width; exportCanvas.height = image.height; const exportCtx = exportCanvas.getContext("2d"); - if (!exportCtx) return; + if (!exportCtx) return null; - // 绘制原始图片 exportCtx.drawImage(image, 0, 0); - // 绘制所有动作(使用原始尺寸) for (const action of actions) { drawAction(exportCtx, action, 1); } - // 导出为 data URL,然后上传到服务器 const dataUrl = exportCanvas.toDataURL("image/png"); const filename = `annotated-${Date.now()}.png`; - + toast.info("正在上传图片..."); + return await uploadImageToServer(dataUrl, filename); + }, [actions, image, processType, processedImageBase64, processedImageUrl]); + + const publishToNewCanvas = useCallback(async (resultUrl: string) => { + const normalizedOriginalUrl = String(originalImageUrl || "").trim(); + const normalizedResultUrl = String(resultUrl || "").trim(); + const resultLabelMap: Record | "annotate", string> = { + annotate: "编辑结果", + rmbg: "抠图结果", + enhance: "变清晰结果", + extend: "拓展结果", + resize: "尺寸调整结果", + watermark: "去水印结果", + }; + const resultLabel = + resultLabelMap[processType || "annotate"] || "编辑结果"; + + const attachments: CanvasLaunchAttachment[] = []; + if (normalizedOriginalUrl) { + attachments.push({ + url: normalizedOriginalUrl, + name: "原图", + contentType: inferImageContentType(normalizedOriginalUrl), + }); + } + if (normalizedResultUrl) { + const hasSameAsOriginal = + normalizedOriginalUrl && normalizedOriginalUrl === normalizedResultUrl; + if (!hasSameAsOriginal || attachments.length === 0) { + attachments.push({ + url: normalizedResultUrl, + name: resultLabel, + contentType: inferImageContentType(normalizedResultUrl), + }); + } + } + + if (attachments.length === 0) { + throw new Error("没有可发布到画布的图片"); + } + + const titleLabel = processType ? resultLabel.replace("结果", "") : "图片编辑"; + const createResponse = await fetch("/api/canvas", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: `${titleLabel}-${new Date().toLocaleDateString("zh-CN")}`, + }), + }); + + const createResult = await createResponse.json(); + if (!createResponse.ok || !createResult?.data?.id) { + throw new Error(createResult?.error || "创建新画布失败"); + } + + const canvasId = String(createResult.data.id); + sessionStorage.setItem( + getCanvasLaunchStorageKey(canvasId), + JSON.stringify({ + prompt: "", + attachments, + autoSend: false, + createdAt: Date.now(), + }), + ); + + toast.success("已发布到新的画布"); + router.push(`/canvas/${canvasId}`); + }, [inferImageContentType, originalImageUrl, processType, router]); + + // 确认 + const handleConfirm = async () => { try { - toast.info("正在上传图片..."); - const uploadedUrl = await uploadImageToServer(dataUrl, filename); - onConfirm(uploadedUrl); - } catch { - toast.error("上传失败,请重试"); + const finalUrl = await resolveConfirmedImageUrl(); + if (!finalUrl) { + toast.error("没有可发布的图片"); + return; + } + + if (submitMode === "publish-new-canvas") { + await publishToNewCanvas(finalUrl); + return; + } + + await onConfirm(finalUrl); + } catch (error) { + const message = error instanceof Error ? error.message : "处理失败,请重试"; + toast.error(message); } }; @@ -1712,7 +1795,7 @@ export function ImageAnnotator({ ) : ( <> - {confirmButtonText} + {resolvedConfirmButtonText} )} @@ -1877,7 +1960,7 @@ export function ImageAnnotator({ className="rounded-[18px] px-6 bg-primary text-primary-foreground hover:bg-primary/90" > - {confirmButtonText} + {resolvedConfirmButtonText}
diff --git a/components/image-preview.tsx b/components/image-preview.tsx index e5afaa5b6..073a83e1e 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -589,6 +589,7 @@ export function ImagePreview({ setShowAnnotator(false); toast.success("编辑图片已添加到聊天"); }} + submitMode="publish-new-canvas" onCancel={() => setShowAnnotator(false)} /> )} diff --git a/components/preview-attachment.tsx b/components/preview-attachment.tsx index cf824981a..482748ef6 100644 --- a/components/preview-attachment.tsx +++ b/components/preview-attachment.tsx @@ -372,6 +372,7 @@ export const PreviewAttachment = ({ setShowAnnotator(false); toast.success("编辑图片已添加到聊天"); }} + submitMode="publish-new-canvas" onCancel={() => setShowAnnotator(false)} /> )} diff --git a/components/top-navbar.tsx b/components/top-navbar.tsx index 059222a54..80a1e72e9 100644 --- a/components/top-navbar.tsx +++ b/components/top-navbar.tsx @@ -495,6 +495,7 @@ export function TopNavbar({ )}