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) {
{/* 提示词重绘对话框 */}
{/* 工具栏使用 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({
)}