From 6fa9bd6823a7f392d971341bfd8f03ee28e3a9c3 Mon Sep 17 00:00:00 2001 From: Danborad Date: Fri, 11 Sep 2026 17:15:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20=E4=BF=AE=E5=A4=8D=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E5=9B=BE=E7=89=87=E5=9B=A0=E5=BC=82=E6=AD=A5=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E5=A4=B1=E7=84=A6=E5=AF=BC=E8=87=B4=E7=9A=84=20Clipbo?= =?UTF-8?q?ard=20=E5=86=99=E5=85=A5=E9=94=99=E8=AF=AF=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/canvas/project.tsx | 60 +++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index 1ac74cab4..d6ef927bf 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -13,6 +13,7 @@ import { resourceFileUrl, resourceIdFromStorageKey, syncResourceToArkPrivateAsse import { uploadImage } from "@/services/image-storage"; import { imageMetadata } from "@/lib/canvas/canvas-generation-task-sync"; import { isCanvasImageSourceNode } from "@/lib/canvas/canvas-image-source"; +import { getCachedResourceBlob } from "@/services/resource-blob-cache"; import copyToClipboard from "copy-to-clipboard"; import { nanoid } from "nanoid"; import { canvasAppearanceBaseTheme, canvasAppearanceForTheme, DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, resolveCanvasAppearance, writeCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance"; @@ -162,12 +163,35 @@ const CanvasDrawingEditorModal = lazy(() => import("@/components/canvas/canvas-d const NODE_STATUS_SUCCESS = "success" as const; const EMPTY_RESOURCE_REFERENCES: CanvasResourceReference[] = []; -async function copyImageToSystemClipboard(source: string) { +async function copyImageToSystemClipboard(source: string, storageKey?: string) { if (typeof ClipboardItem === "undefined" || !navigator.clipboard?.write) throw new Error("当前浏览器不支持复制图片"); - const response = await fetch(source); - if (!response.ok) throw new Error(`图片读取失败(HTTP ${response.status})`); - const sourceBlob = await response.blob(); - const blob = sourceBlob.type === "image/png" ? sourceBlob : await convertClipboardImageToPNG(sourceBlob); + if (typeof window !== "undefined") window.focus(); + + const fetchPNG = async (): Promise => { + let sourceBlob: Blob | null = null; + if (storageKey) { + sourceBlob = await getCachedResourceBlob(storageKey).catch(() => null); + } + if (!sourceBlob) { + const response = await fetch(source); + if (!response.ok) throw new Error(`图片读取失败(HTTP ${response.status})`); + sourceBlob = await response.blob(); + } + return sourceBlob.type === "image/png" ? sourceBlob : await convertClipboardImageToPNG(sourceBlob); + }; + + // 优先尝试将 Promise 直接传给 ClipboardItem(现代浏览器标准),在用户激活手势内立即声明写入, + // 避免因 fetch / 格式转换耗时导致手势过期或窗口失焦抛出 "Document is not focused" 错误。 + try { + const item = new ClipboardItem({ "image/png": fetchPNG() }); + await navigator.clipboard.write([item]); + return; + } catch { + // 部分浏览器环境不支持延迟 Promise,回退到先取 Blob 再写入 + } + + const blob = await fetchPNG(); + if (typeof window !== "undefined") window.focus(); await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]); } @@ -1630,22 +1654,40 @@ function InfiniteCanvasPage() { [message, pasteCopiedNodes, pasteSystemClipboard, shouldPreferCopiedNodes], ); + const copyingNodeContentRef = useRef(false); const copyNodeContentToClipboard = useCallback( async (node: CanvasNodeData | null) => { + if (copyingNodeContentRef.current) return; + copyingNodeContentRef.current = true; releaseCopiedNodesPastePriority(); const content = node?.metadata?.content?.trim(); const resourceId = resourceIdFromStorageKey(node?.metadata?.storageKey); const copySource = content || (node?.type === CanvasNodeType.Image && resourceId ? resourceFileUrl(resourceId) : ""); if (!node || !copySource) { + copyingNodeContentRef.current = false; message.warning("没有可复制的内容"); return; } try { if (node.type === CanvasNodeType.Image) { - await copyImageToSystemClipboard(copySource); - message.success("图片已复制"); - return; + try { + await copyImageToSystemClipboard(copySource, node.metadata?.storageKey); + message.success("图片已复制到剪贴板"); + return; + } catch (imageErr) { + const fallbackUrl = new URL(copySource, window.location.href).toString(); + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(fallbackUrl).catch(() => undefined); + message.info("由于浏览器未获得焦点,已为您复制图片地址"); + return; + } + if (await copyToClipboard(fallbackUrl)) { + message.info("由于浏览器未获得焦点,已为您复制图片地址"); + return; + } + throw imageErr; + } } if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(copySource); @@ -1653,6 +1695,8 @@ function InfiniteCanvasPage() { message.success(node.type === CanvasNodeType.Text ? "文本已复制" : "内容链接已复制"); } catch (error) { message.error(error instanceof Error ? error.message : "复制失败,请检查浏览器剪贴板权限"); + } finally { + copyingNodeContentRef.current = false; } }, [message, releaseCopiedNodesPastePriority],