diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 7c78d1a63..1fd7ec252 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -481,6 +481,25 @@ function buildDownloadFilename(url: string): string { return fallback; } +function triggerBrowserDownload( + url: string, + filename?: string, + options?: { openInNewTab?: boolean } +) { + const link = document.createElement("a"); + link.href = url; + if (filename) { + link.download = filename; + } + if (options?.openInNewTab) { + link.target = "_blank"; + } + link.rel = "noopener noreferrer"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + function getAssistantImageNodeIdentityKey(node: any): string { if (!node || node.type !== "image") return ""; const data = ((node.data as any) || {}) as Record; @@ -2482,22 +2501,20 @@ export function CanvasChatPanel({ } const blob = await response.blob(); const blobUrl = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = blobUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + triggerBrowserDownload(blobUrl, filename); window.setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000); return; - } catch { - const link = document.createElement("a"); - link.href = originalUrl; - link.target = "_blank"; - link.rel = "noopener noreferrer"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + } catch (error) { + console.error("下载原图失败:", error); + try { + triggerBrowserDownload(originalUrl, filename); + toast.message("代理下载失败,已切换为原图直链下载"); + return; + } catch (fallbackError) { + console.error("原图直链下载失败:", fallbackError); + } + + triggerBrowserDownload(originalUrl, undefined, { openInNewTab: true }); toast.message("已打开原图,可右键另存为"); } }, []); diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index 18190cb8f..2447cbcff 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -48,6 +48,7 @@ import { normalizeCanvasToolbarConfig, type CanvasToolbarConfig, } from "@/lib/canvas-toolbar-config"; +import { inferOriginalImageUrl } from "@/lib/image-url"; const VIDEO_CANVAS_SYMBOL_ICON_MAP: Record = { idea: Lightbulb, @@ -1170,7 +1171,7 @@ export function CanvasWorkspace({ const nodeData = node.data as any; const rawImageUrl = String(nodeData?.src || "").trim(); if (!rawImageUrl) return null; - let imageUrl = rawImageUrl; + let imageUrl = inferOriginalImageUrl(rawImageUrl) || rawImageUrl; if (browserOrigin && rawImageUrl.startsWith("/")) { imageUrl = `${browserOrigin}${rawImageUrl}`; } else if (browserOrigin && rawImageUrl.startsWith("//")) { diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 1a21a6ea9..c2b3ec5a2 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -1158,6 +1158,25 @@ const sanitizeFileName = (name: string) => { return safe || "image"; }; +const triggerBrowserDownload = ( + url: string, + filename?: string, + options?: { openInNewTab?: boolean } +) => { + const link = document.createElement("a"); + link.href = url; + if (filename) { + link.download = filename; + } + if (options?.openInNewTab) { + link.target = "_blank"; + } + link.rel = "noopener noreferrer"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +}; + const GENERIC_CANVAS_PROMPTS = new Set([ "对话生成", "图片生成", @@ -6083,7 +6102,15 @@ export function ImageNode({ // 下载图片 - 通过 fetch 获取图片数据实现真正的下载(避免跨域问题导致跳转) const handleDownload = async () => { if (!data.src) return; - const originalUrl = inferOriginalImageUrl(data.src) || data.src; + const originalUrl = String(inferOriginalImageUrl(data.src) || data.src || "").trim(); + if (!originalUrl) { + toast.error("下载失败,请重试"); + return; + } + + const urlParts = originalUrl.split("."); + const ext = urlParts[urlParts.length - 1]?.split("?")[0] || "png"; + const filename = `${sanitizeFileName(displayTitle)}.${ext}`; try { toast.info("正在准备下载..."); @@ -6102,11 +6129,7 @@ export function ImageNode({ // 创建下载链接 const link = document.createElement("a"); link.href = blobUrl; - - // 从 URL 中提取文件扩展名,默认使用 png - const urlParts = originalUrl.split("."); - const ext = urlParts[urlParts.length - 1]?.split("?")[0] || "png"; - link.download = `${sanitizeFileName(displayTitle)}.${ext}`; + link.download = filename; // 触发下载 document.body.appendChild(link); @@ -6119,10 +6142,21 @@ export function ImageNode({ toast.success("下载成功"); } catch (error) { console.error("下载失败:", error); - toast.error("下载失败,请重试"); + try { + triggerBrowserDownload(originalUrl, filename); + toast.message("代理下载失败,已切换为原图直链下载"); + return; + } catch (fallbackError) { + console.error("原图直链下载失败:", fallbackError); + } - // 降级方案:直接打开图片 - window.open(originalUrl, "_blank", "noopener,noreferrer"); + try { + triggerBrowserDownload(originalUrl, undefined, { openInNewTab: true }); + toast.message("已打开原图,可直接另存为"); + } catch (openError) { + console.error("打开原图失败:", openError); + toast.error("下载失败,请重试"); + } } }; diff --git a/components/image-preview.tsx b/components/image-preview.tsx index 073a83e1e..b6344b57c 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -7,6 +7,25 @@ import { ImageAnnotator } from "./image-annotator"; import { AngleEditDialog } from "./angle-edit-dialog"; import { buildImageProxyUrl } from "@/lib/image-delivery"; +function triggerBrowserDownload( + url: string, + filename?: string, + options?: { openInNewTab?: boolean } +) { + const link = document.createElement("a"); + link.href = url; + if (filename) { + link.download = filename; + } + if (options?.openInNewTab) { + link.target = "_blank"; + } + link.rel = "noopener noreferrer"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + export function ImagePreview({ src, alt, @@ -184,19 +203,39 @@ export function ImagePreview({ // 下载图片函数(下载原图) const handleDownload = async () => { + const safeOriginalUrl = String(original || "").trim(); + if (!safeOriginalUrl) { + toast.error("下载失败,请重试"); + return; + } + const filename = `generated-image-${Date.now()}.jpg`; try { - const response = await fetch(original); + const response = await fetch( + buildImageProxyUrl(safeOriginalUrl, { delivery: "proxy" }) + ); + if (!response.ok) { + throw new Error(`下载失败(${response.status})`); + } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `gemini-image-${Date.now()}.jpg`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + triggerBrowserDownload(url, filename); window.URL.revokeObjectURL(url); + toast.success("下载成功"); } catch (error) { console.error("下载失败:", error); + try { + triggerBrowserDownload(safeOriginalUrl, filename); + toast.message("代理下载失败,已切换为原图直链下载"); + } catch (fallbackError) { + console.error("原图直链下载失败:", fallbackError); + try { + triggerBrowserDownload(safeOriginalUrl, undefined, { openInNewTab: true }); + toast.message("已打开原图,可直接另存为"); + } catch (openError) { + console.error("打开原图失败:", openError); + toast.error("下载失败,请重试"); + } + } } };