fix(canvas): 画布保存 - 修复重生成媒体绑定死锁并新增手动保存与强制覆盖保存
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Clapperboard, CloudDownload, Coins, CopyPlus, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Search, Share2, Trash2, Undo2, Upload } from "lucide-react";
|
||||
import { Clapperboard, CloudDownload, CloudUpload, Coins, CopyPlus, Focus, FolderKanban, Gauge, Home, LayoutGrid, LoaderCircle, Menu, Pencil, Plus, Redo2, Save, Search, Share2, Trash2, Undo2, Upload } from "lucide-react";
|
||||
import { Button, Dropdown, Tooltip } from "antd";
|
||||
|
||||
import { useWalletBalance } from "@/hooks/use-wallet-balance";
|
||||
@@ -25,6 +25,8 @@ type CanvasTopBarProps = {
|
||||
canRedo: boolean;
|
||||
onCreateProject: () => void;
|
||||
onDeleteProject: () => void;
|
||||
onSave: () => void | Promise<void>;
|
||||
onForceSave: () => void;
|
||||
onImportImage: () => void;
|
||||
onImportLibTV: () => void;
|
||||
onImportTapNow: () => void;
|
||||
@@ -52,6 +54,8 @@ export function CanvasTopBar({
|
||||
canRedo,
|
||||
onCreateProject,
|
||||
onDeleteProject,
|
||||
onSave,
|
||||
onForceSave,
|
||||
onImportImage,
|
||||
onImportLibTV,
|
||||
onImportTapNow,
|
||||
@@ -105,6 +109,8 @@ export function CanvasTopBar({
|
||||
{ type: "divider" },
|
||||
{ key: "new", icon: <Plus className="size-4" />, label: "新建画布", onClick: onCreateProject },
|
||||
{ key: "delete", danger: true, icon: <Trash2 className="size-4" />, label: "删除当前画布", onClick: onDeleteProject },
|
||||
{ key: "save", icon: <Save className="size-4" />, label: <MenuLabel text="保存" shortcut="⌘ S" />, onClick: () => void onSave() },
|
||||
{ key: "force-save", icon: <CloudUpload className="size-4" />, label: "强制覆盖保存", onClick: onForceSave },
|
||||
{ type: "divider" },
|
||||
{ key: "import", icon: <Upload className="size-4" />, label: "导入素材", onClick: onImportImage },
|
||||
{ key: "search", icon: <Search className="size-4" />, label: <MenuLabel text="搜索节点" shortcut="⌘ F" />, onClick: onOpenSearch },
|
||||
|
||||
@@ -415,7 +415,7 @@ function InfiniteCanvasPage() {
|
||||
[cleanupAssetImages, getHistoryCleanupContext],
|
||||
);
|
||||
|
||||
const { loadError, retryLoad, addedSkills, agentCreatedNodes, clearCanvasFiles, createAndOpenProject, currentProject, deleteCurrentProject, renameCurrentProject, saveCanvasProject, updateProject } = useCanvasProjectLifecycle({
|
||||
const { loadError, retryLoad, addedSkills, agentCreatedNodes, clearCanvasFiles, createAndOpenProject, currentProject, deleteCurrentProject, renameCurrentProject, saveCanvasProject, forceSaveCanvasProject, updateProject } = useCanvasProjectLifecycle({
|
||||
projectId,
|
||||
projectLoaded,
|
||||
nodes,
|
||||
@@ -444,6 +444,18 @@ function InfiniteCanvasPage() {
|
||||
cleanupCanvasFiles,
|
||||
});
|
||||
|
||||
// 强制覆盖会改写云端版本并重绑媒体素材关联,必须让用户显式确认 destructive 语义。
|
||||
const confirmForceSaveCanvas = useCallback(() => {
|
||||
modal.confirm({
|
||||
title: "用本地内容强制覆盖云端?",
|
||||
content: "将把当前本地画布保存并覆盖云端版本,同时自动修复画布媒体与素材库的绑定(缺少素材记录时会按节点新建)。云端尚未同步到本地的改动会被覆盖。",
|
||||
okText: "强制覆盖保存",
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: "取消",
|
||||
onOk: () => forceSaveCanvasProject(),
|
||||
});
|
||||
}, [forceSaveCanvasProject, modal]);
|
||||
|
||||
const applyLibTVImport = useCallback(
|
||||
async (importedNodes: CanvasNodeData[], importedConnections: CanvasConnection[]) => {
|
||||
const previousNodes = nodesRef.current;
|
||||
@@ -2327,6 +2339,8 @@ function InfiniteCanvasPage() {
|
||||
canRedo={historyState.canRedo}
|
||||
onCreateProject={createAndOpenProject}
|
||||
onDeleteProject={deleteCurrentProject}
|
||||
onSave={() => void saveCanvasProject()}
|
||||
onForceSave={confirmForceSaveCanvas}
|
||||
onImportImage={() => handleUploadRequest()}
|
||||
onImportLibTV={() => setLibTVImportOpen(true)}
|
||||
onImportTapNow={() => setTapNowImportOpen(true)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { removeCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
|
||||
import { normalizeCanvasNodeTimestamps } from "@/lib/canvas/canvas-node-timestamps";
|
||||
import { hydrateAssistantImages, resetInterruptedGeneration } from "@/lib/canvas/canvas-project-generation";
|
||||
import { listAddedSkills, type Skill } from "@/services/api/skills";
|
||||
import { createCanvasProjectWithRemoteSync, deleteCanvasProjectsWithRemoteSync, loadCanvasProjectForEditing, localSavedRemotePendingMessage, saveRemoteUserDataNow, subscribeAgentCanvasRefresh } from "@/services/user-data-sync";
|
||||
import { createCanvasProjectWithRemoteSync, deleteCanvasProjectsWithRemoteSync, forceOverwriteRemoteCanvasSync, loadCanvasProjectForEditing, localSavedRemotePendingMessage, saveRemoteUserDataNow, subscribeAgentCanvasRefresh } from "@/services/user-data-sync";
|
||||
import { flushCanvasStorePersistence, useCanvasStore, type CanvasProject } from "@/stores/canvas/use-canvas-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -242,7 +242,7 @@ export function useCanvasProjectLifecycle({
|
||||
renameProject(projectId, title);
|
||||
}, [projectId, renameProject]);
|
||||
|
||||
const saveCanvasProject = useCallback(async (): Promise<boolean> => {
|
||||
const persistCanvasSnapshot = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
updateProject(projectId, {
|
||||
nodes: nodesRef.current,
|
||||
@@ -256,10 +256,15 @@ export function useCanvasProjectLifecycle({
|
||||
directorScenes: currentProject?.directorScenes || [],
|
||||
});
|
||||
await flushCanvasStorePersistence();
|
||||
return true;
|
||||
} catch {
|
||||
message.error("画布保存失败,请稍后重试");
|
||||
return false;
|
||||
}
|
||||
}, [activeChatId, backgroundMode, canvasAppearance, chatSessions, connectionsRef, currentProject?.directorScenes, message, nodesRef, projectId, showImageInfo, updateProject, viewportRef]);
|
||||
|
||||
const saveCanvasProject = useCallback(async (): Promise<boolean> => {
|
||||
if (!(await persistCanvasSnapshot())) return false;
|
||||
try {
|
||||
await saveRemoteUserDataNow();
|
||||
message.success("画布布局和位置已保存");
|
||||
@@ -268,7 +273,18 @@ export function useCanvasProjectLifecycle({
|
||||
message.warning(localSavedRemotePendingMessage("本地画布布局已保存", error));
|
||||
}
|
||||
return true;
|
||||
}, [activeChatId, backgroundMode, canvasAppearance, chatSessions, connectionsRef, currentProject?.directorScenes, message, nodesRef, projectId, showImageInfo, updateProject, viewportRef]);
|
||||
}, [message, persistCanvasSnapshot]);
|
||||
|
||||
const forceSaveCanvasProject = useCallback(async (): Promise<boolean> => {
|
||||
if (!(await persistCanvasSnapshot())) return false;
|
||||
try {
|
||||
const result = await forceOverwriteRemoteCanvasSync();
|
||||
message.success(result.reboundNodes > 0 ? `已用本地内容覆盖云端,并修复 ${result.reboundNodes} 处媒体与素材的绑定` : "已用本地内容覆盖云端画布");
|
||||
} catch (error) {
|
||||
message.error(`强制覆盖保存失败:${error instanceof Error ? error.message : "未知错误"}`);
|
||||
}
|
||||
return true;
|
||||
}, [message, persistCanvasSnapshot]);
|
||||
|
||||
const clearCanvasFiles = useCallback(() => {
|
||||
cleanupCanvasFiles({ projectId, nodes: [], chatSessions: [] });
|
||||
@@ -285,6 +301,7 @@ export function useCanvasProjectLifecycle({
|
||||
deleteCurrentProject,
|
||||
renameCurrentProject,
|
||||
saveCanvasProject,
|
||||
forceSaveCanvasProject,
|
||||
updateProject,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user