From de25ef515ed00b9977554afd863f8227439d8fdf Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Tue, 26 May 2026 13:21:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(assets):=20=E4=BD=BF=E7=94=A8=20file-saver?= =?UTF-8?q?=20=E5=BA=93=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 file-saver 库替代手动创建下载链接的方式 - 在 asset-transfer.ts 中使用 saveAs 替代 URL.createObjectURL 和手动点击链接 - 在 canvas-export.ts 中使用 saveAs 替代原有的文件下载逻辑 - 更新 package.json 添加 file-saver 依赖 - 更新 bun.lock 文件同步依赖变更 --- web/bun.lock | 6 ++++++ web/package.json | 2 ++ web/src/app/(user)/assets/asset-transfer.ts | 9 +++------ web/src/app/(user)/assets/page.tsx | 6 ++---- web/src/app/(user)/canvas/[id]/canvas-client-page.tsx | 6 ++---- web/src/app/(user)/canvas/utils/canvas-export.ts | 9 +++------ web/src/app/(user)/image/page.tsx | 6 ++---- web/src/app/(user)/video/page.tsx | 6 ++---- 8 files changed, 22 insertions(+), 28 deletions(-) diff --git a/web/bun.lock b/web/bun.lock index 2f470d9..fed5506 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -18,6 +18,7 @@ "copy-to-clipboard": "^4.0.2", "dayjs": "^1.11.20", "fflate": "^0.8.3", + "file-saver": "^2.0.5", "localforage": "^1.10.0", "lucide-react": "^1.16.0", "motion": "^12.38.0", @@ -34,6 +35,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/file-saver": "^2.0.7", "@types/node": "^20", "@types/react": "19.1.12", "@types/react-dom": "19.1.9", @@ -555,6 +557,8 @@ "@types/debug": ["@types/debug@4.1.13", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/file-saver": ["@types/file-saver@2.0.7", "", {}, "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A=="], + "@types/hast": ["@types/hast@2.3.10", "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], "@types/mdast": ["@types/mdast@3.0.15", "https://registry.npmmirror.com/@types/mdast/-/mdast-3.0.15.tgz", { "dependencies": { "@types/unist": "^2" } }, "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ=="], @@ -807,6 +811,8 @@ "figures": ["figures@6.1.0", "https://registry.npmmirror.com/figures/-/figures-6.1.0.tgz", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "file-saver": ["file-saver@2.0.5", "", {}, "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="], + "fill-range": ["fill-range@7.1.1", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], diff --git a/web/package.json b/web/package.json index 20b1015..48f0ae5 100644 --- a/web/package.json +++ b/web/package.json @@ -24,6 +24,7 @@ "copy-to-clipboard": "^4.0.2", "dayjs": "^1.11.20", "fflate": "^0.8.3", + "file-saver": "^2.0.5", "localforage": "^1.10.0", "lucide-react": "^1.16.0", "motion": "^12.38.0", @@ -40,6 +41,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/file-saver": "^2.0.7", "@types/node": "^20", "@types/react": "19.1.12", "@types/react-dom": "19.1.9", diff --git a/web/src/app/(user)/assets/asset-transfer.ts b/web/src/app/(user)/assets/asset-transfer.ts index 7c39b03..dd50cd3 100644 --- a/web/src/app/(user)/assets/asset-transfer.ts +++ b/web/src/app/(user)/assets/asset-transfer.ts @@ -1,3 +1,5 @@ +import { saveAs } from "file-saver"; + import { createZip, readZip } from "@/lib/zip"; import { getMediaBlob, setMediaBlob } from "@/services/file-storage"; import { getImageBlob, setImageBlob } from "@/services/image-storage"; @@ -36,12 +38,7 @@ export async function exportAssets(assets: Asset[]) { const data: AssetExportFile = { app: "infinite-canvas", version: 1, exportedAt: new Date().toISOString(), assets, files }; const zip = await createZip([{ name: "assets.json", data: JSON.stringify(data, null, 2) }, ...zipFiles]); - const url = URL.createObjectURL(zip); - const link = document.createElement("a"); - link.href = url; - link.download = "我的素材.zip"; - link.click(); - URL.revokeObjectURL(url); + saveAs(zip, "我的素材.zip"); } export async function readAssetPackage(file: File) { diff --git a/web/src/app/(user)/assets/page.tsx b/web/src/app/(user)/assets/page.tsx index 32ddbd7..38c4f49 100644 --- a/web/src/app/(user)/assets/page.tsx +++ b/web/src/app/(user)/assets/page.tsx @@ -3,6 +3,7 @@ import { Copy, Download, PencilLine, Search, Trash2, Upload } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { App, Button, Card, Drawer, Empty, Form, Image, Input, Modal, Pagination, Select, Space, Tag, Typography } from "antd"; +import { saveAs } from "file-saver"; import { useCopyText } from "@/hooks/use-copy-text"; import { formatBytes, readFileAsDataUrl } from "@/lib/image-utils"; @@ -149,10 +150,7 @@ export default function AssetsPage() { const downloadImage = (asset: Asset) => { if (asset.kind !== "image" && asset.kind !== "video") return; - const link = document.createElement("a"); - link.href = asset.kind === "video" ? asset.data.url : asset.data.dataUrl; - link.download = `${asset.title || "asset"}.${asset.data.mimeType.split("/")[1] || "png"}`; - link.click(); + saveAs(asset.kind === "video" ? asset.data.url : asset.data.dataUrl, `${asset.title || "asset"}.${asset.data.mimeType.split("/")[1] || "png"}`); }; const exportAllAssets = async () => { diff --git a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx index a58a7f3..5248539 100644 --- a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx +++ b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr import type { ChangeEvent as ReactChangeEvent, DragEvent as ReactDragEvent, MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent } from "react"; import { useParams, useRouter } from "next/navigation"; import { Home, ImageIcon, Images, List, Menu, MessageSquare, Plus, Redo2, Settings2, Trash2, Undo2, Upload, Video } from "lucide-react"; +import { saveAs } from "file-saver"; import { requestEdit, requestGeneration, requestImageQuestion } from "@/services/api/image"; import { requestVideoGeneration } from "@/services/api/video"; @@ -1317,10 +1318,7 @@ function InfiniteCanvasPage() { const downloadNodeImage = useCallback((node: CanvasNodeData) => { if ((node.type !== CanvasNodeType.Image && node.type !== CanvasNodeType.Video) || !node.metadata?.content) return; - const link = document.createElement("a"); - link.href = node.metadata.content; - link.download = `canvas-${node.type}-${node.id}.${node.type === CanvasNodeType.Video ? "mp4" : imageExtension(node.metadata.content)}`; - link.click(); + saveAs(node.metadata.content, `canvas-${node.type}-${node.id}.${node.type === CanvasNodeType.Video ? "mp4" : imageExtension(node.metadata.content)}`); }, []); const saveNodeAsset = useCallback( diff --git a/web/src/app/(user)/canvas/utils/canvas-export.ts b/web/src/app/(user)/canvas/utils/canvas-export.ts index 80e57e3..5e91aba 100644 --- a/web/src/app/(user)/canvas/utils/canvas-export.ts +++ b/web/src/app/(user)/canvas/utils/canvas-export.ts @@ -1,3 +1,5 @@ +import { saveAs } from "file-saver"; + import { createZip } from "@/lib/zip"; import { getMediaBlob } from "@/services/file-storage"; import { getImageBlob } from "@/services/image-storage"; @@ -24,12 +26,7 @@ export async function exportCanvasProjects(projects: CanvasProject[], fileName = const data: CanvasExportFile = { app: "infinite-canvas", version: 3, exportedAt: new Date().toISOString(), projects: exportedProjects }; const zip = await createZip([{ name: "projects.json", data: JSON.stringify(data, null, 2) }, ...zipFiles]); - const url = URL.createObjectURL(zip); - const link = document.createElement("a"); - link.href = url; - link.download = `${safeFileName(fileName)}.zip`; - link.click(); - URL.revokeObjectURL(url); + saveAs(zip, `${safeFileName(fileName)}.zip`); } function collectStorageKeys(value: unknown, keys = new Set()) { diff --git a/web/src/app/(user)/image/page.tsx b/web/src/app/(user)/image/page.tsx index ba3efcd..b5a3118 100644 --- a/web/src/app/(user)/image/page.tsx +++ b/web/src/app/(user)/image/page.tsx @@ -4,6 +4,7 @@ import { BookOpen, CheckSquare, ClipboardPaste, Download, FolderPlus, History, I import { useEffect, useRef, useState } from "react"; import { App, Button, Checkbox, Drawer, Empty, Image, Input, Modal, Tag, Typography } from "antd"; import localforage from "localforage"; +import { saveAs } from "file-saver"; import { ImageSettingsPanel } from "@/components/image-settings-panel"; import { ModelPicker } from "@/components/model-picker"; @@ -177,10 +178,7 @@ export default function ImagePage() { }; const downloadImage = (image: GeneratedImage, index: number) => { - const link = document.createElement("a"); - link.href = image.dataUrl; - link.download = `image-${index + 1}.png`; - link.click(); + saveAs(image.dataUrl, `image-${index + 1}.png`); }; const addResultToReferences = async (image: GeneratedImage, index: number) => { diff --git a/web/src/app/(user)/video/page.tsx b/web/src/app/(user)/video/page.tsx index 5894108..5252e4b 100644 --- a/web/src/app/(user)/video/page.tsx +++ b/web/src/app/(user)/video/page.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; import { App, Button, Checkbox, Drawer, Empty, Input, Modal, Tag, Typography } from "antd"; import localforage from "localforage"; import { nanoid } from "nanoid"; +import { saveAs } from "file-saver"; import { AssetPickerModal, type InsertAssetPayload } from "@/app/(user)/canvas/components/asset-picker-modal"; import { ModelPicker } from "@/components/model-picker"; @@ -181,10 +182,7 @@ export default function VideoPage() { }; const downloadVideo = (video: GeneratedVideo) => { - const link = document.createElement("a"); - link.href = video.url; - link.download = "video.mp4"; - link.click(); + saveAs(video.url, "video.mp4"); }; const saveResultToAssets = (video: GeneratedVideo) => {