feat: sync latest canvas dialog and selection updates
This commit is contained in:
parent
c23c395010
commit
d7ac4417d1
@ -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",
|
||||
},
|
||||
|
||||
@ -2157,7 +2157,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
|
||||
{/* 提示词重绘对话框 */}
|
||||
<Dialog open={promptDialogOpen} onOpenChange={setPromptDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[560px]">
|
||||
<DialogContent
|
||||
overlayClassName="bg-black"
|
||||
className="sm:max-w-[560px] border-slate-200 bg-white opacity-100 shadow-2xl backdrop-blur-none dark:border-slate-700 dark:bg-slate-950"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<RefreshCw className="h-5 w-5" />
|
||||
@ -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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -2191,7 +2194,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
</Dialog>
|
||||
|
||||
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
|
||||
{isSelected && !isMultiSelected && data.src && !isEditingTitle && !sceneFusionDialogOpen && typeof document !== 'undefined' && createPortal(
|
||||
{isSelected && !isMultiSelected && data.src && !isEditingTitle && !sceneFusionDialogOpen && !promptDialogOpen && !notePanelOpen && typeof document !== 'undefined' && createPortal(
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div
|
||||
ref={toolbarRef}
|
||||
|
||||
@ -1915,6 +1915,7 @@ export function LovartHome({
|
||||
<ImageAnnotator
|
||||
imageUrl={editorImageUrl}
|
||||
onConfirm={handleImageEditorConfirm}
|
||||
submitMode="publish-new-canvas"
|
||||
onCancel={handleImageEditorCancel}
|
||||
onHomeClick={handleImageEditorCancel}
|
||||
/>
|
||||
|
||||
@ -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<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ctx, setCtx] = useState<CanvasRenderingContext2D | null>(null);
|
||||
@ -95,6 +100,9 @@ export function ImageAnnotator({
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const objectUrlRef = useRef<string | null>(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<string | null> => {
|
||||
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<NonNullable<typeof processType> | "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({
|
||||
) : (
|
||||
<>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
{confirmButtonText}
|
||||
{resolvedConfirmButtonText}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@ -1877,7 +1960,7 @@ export function ImageAnnotator({
|
||||
className="rounded-[18px] px-6 bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
{confirmButtonText}
|
||||
{resolvedConfirmButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -589,6 +589,7 @@ export function ImagePreview({
|
||||
setShowAnnotator(false);
|
||||
toast.success("编辑图片已添加到聊天");
|
||||
}}
|
||||
submitMode="publish-new-canvas"
|
||||
onCancel={() => setShowAnnotator(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -372,6 +372,7 @@ export const PreviewAttachment = ({
|
||||
setShowAnnotator(false);
|
||||
toast.success("编辑图片已添加到聊天");
|
||||
}}
|
||||
submitMode="publish-new-canvas"
|
||||
onCancel={() => setShowAnnotator(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -495,6 +495,7 @@ export function TopNavbar({
|
||||
<ImageAnnotator
|
||||
imageUrl={editorImageUrl}
|
||||
onConfirm={handleImageEditorConfirm}
|
||||
submitMode="publish-new-canvas"
|
||||
onCancel={handleImageEditorCancel}
|
||||
/>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user