fix(canvas): 画布 - 防止旧内容覆盖并支持版本历史恢复
This commit is contained in:
@@ -2,6 +2,7 @@ import { create } from "zustand";
|
||||
import { persist, type PersistStorage, type StorageValue } from "zustand/middleware";
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
import { sameCanvasContent } from "@/lib/canvas/canvas-content";
|
||||
import { DEFAULT_CANVAS_BACKGROUND_MODE, normalizeCanvasAppearance, readCanvasAppearanceDefault, type CanvasAppearance } from "@/lib/canvas/canvas-appearance";
|
||||
import { parseCanvasStorageDocument, rebaseCanvasProjects, serializeCanvasStorageDocument, type CanvasStorageDocument } from "@/lib/canvas/canvas-storage-revision";
|
||||
import { localForageStorageForScope } from "@/lib/localforage-storage";
|
||||
@@ -14,6 +15,8 @@ import type { TimelineProject } from "@/types/timeline";
|
||||
|
||||
export type CanvasProject = {
|
||||
id: string;
|
||||
revision?: number;
|
||||
remoteContentHash?: string;
|
||||
projectId?: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
@@ -448,6 +451,7 @@ export const useCanvasStore = create<CanvasStore>()(
|
||||
const appearanceDefault = readCanvasAppearanceDefault();
|
||||
const project: CanvasProject = {
|
||||
id,
|
||||
revision: 0,
|
||||
projectId,
|
||||
title,
|
||||
createdAt: now,
|
||||
@@ -469,6 +473,7 @@ export const useCanvasStore = create<CanvasStore>()(
|
||||
const now = new Date().toISOString();
|
||||
const project: CanvasProject = {
|
||||
id: nanoid(),
|
||||
revision: 0,
|
||||
projectId: source.projectId,
|
||||
title: source.title || "导入画布",
|
||||
createdAt: source.createdAt || now,
|
||||
@@ -490,20 +495,27 @@ export const useCanvasStore = create<CanvasStore>()(
|
||||
openProject: (id) => {
|
||||
return get().projects.find((item) => item.id === id) || null;
|
||||
},
|
||||
renameProject: (id, title) =>
|
||||
set((state) => ({
|
||||
projects: state.projects.map((project) => (project.id === id ? { ...project, title: title.trim() || project.title, updatedAt: new Date().toISOString() } : project)),
|
||||
})),
|
||||
renameProject: (id, title) => set((state) => {
|
||||
const current = state.projects.find((project) => project.id === id);
|
||||
const nextTitle = title.trim() || current?.title;
|
||||
if (!current || current.title === nextTitle) return state;
|
||||
return { projects: state.projects.map((project) => project === current ? { ...project, title: nextTitle!, updatedAt: new Date().toISOString() } : project) };
|
||||
}),
|
||||
deleteProjects: (ids) =>
|
||||
set((state) => {
|
||||
const projects = state.projects.filter((project) => !ids.includes(project.id));
|
||||
return { projects };
|
||||
}),
|
||||
replaceProjects: (projects) => set({ projects }),
|
||||
updateProject: (id, patch) =>
|
||||
set((state) => ({
|
||||
projects: state.projects.map((project) => (project.id === id ? { ...project, ...patch, updatedAt: new Date().toISOString() } : project)),
|
||||
})),
|
||||
updateProject: (id, patch) => set((state) => {
|
||||
const current = state.projects.find((project) => project.id === id);
|
||||
if (!current) return state;
|
||||
const next = { ...current, ...patch };
|
||||
const contentChanged = !sameCanvasContent(current, next);
|
||||
if (!contentChanged && samePersistenceValue(current.viewport, next.viewport)) return state;
|
||||
if (contentChanged) next.updatedAt = new Date().toISOString();
|
||||
return { projects: state.projects.map((project) => project === current ? next : project) };
|
||||
}),
|
||||
}),
|
||||
{
|
||||
name: CANVAS_STORE_KEY,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { cleanupUnusedImages, collectImageStorageKeys, resolveImageUrl, uploadIm
|
||||
import { cleanupUnusedMedia, collectMediaStorageKeys, resolveMediaUrl } from "@/services/file-storage";
|
||||
import { flushGenerationAssetStorageLocks, insertOrReturnGenerationAsset, withGenerationArtifactCommitLock, withGenerationAssetStorageLock } from "@/services/generation-asset-repository";
|
||||
import { CANVAS_STORE_KEY, commitPendingCanvasStorePersistenceLocked, pendingCanvasStorePersistence, withCanvasStorePersistenceLock } from "@/stores/canvas/use-canvas-store";
|
||||
import { readAllCanvasSyncDrafts } from "@/services/canvas-sync-drafts";
|
||||
|
||||
export type AssetKind = "text" | "image" | "video" | "audio" | "model" | "entity";
|
||||
export type { AssetCategory } from "@/lib/asset-category";
|
||||
@@ -414,6 +415,7 @@ export const useAssetStore = create<AssetStore>()(
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
window.setTimeout(() =>
|
||||
withGenerationArtifactCommitLock(scope, async () => {
|
||||
const syncDrafts = await readAllCanvasSyncDrafts(scope);
|
||||
// 固定锁序:artifact -> Canvas(释放)-> Asset,避免跨 store 锁重入。
|
||||
const canvasProjects = await withCanvasStorePersistenceLock(scope, async () => {
|
||||
await commitPendingCanvasStorePersistenceLocked(scope);
|
||||
@@ -423,7 +425,7 @@ export const useAssetStore = create<AssetStore>()(
|
||||
await withGenerationAssetStorageLock(scope, async () => {
|
||||
await commitPendingAssetStorePersistenceLocked(scope);
|
||||
const durableAssets = (await readPersistedAssetDocumentForScope(scope)).state.assets;
|
||||
const references = { projects: canvasProjects, assets: durableAssets };
|
||||
const references = { projects: canvasProjects, assets: durableAssets, syncDrafts };
|
||||
const imageKeys = new Set([...frozenExtraImageKeys, ...collectImageStorageKeys(references)]);
|
||||
const mediaKeys = new Set([...frozenExtraMediaKeys, ...collectMediaStorageKeys(references)]);
|
||||
await cleanupUnusedImages(
|
||||
|
||||
@@ -4,8 +4,9 @@ export type SyncProjectProgress = {
|
||||
projectId: string;
|
||||
total: number;
|
||||
completed: number;
|
||||
phase: "uploading" | "saving" | "done" | "error";
|
||||
phase: "pending" | "uploading" | "saving" | "done" | "error" | "conflict";
|
||||
message?: string;
|
||||
draftCount?: number;
|
||||
};
|
||||
|
||||
type SyncProgressStore = {
|
||||
@@ -55,7 +56,7 @@ export const useSyncProgressStore = create<SyncProgressStore>((set, get) => ({
|
||||
clearAll: () => set({ syncingProjects: {} }),
|
||||
isAnySyncing: () => {
|
||||
const list = Object.values(get().syncingProjects);
|
||||
return list.some((item) => item.phase === "uploading" || item.phase === "saving");
|
||||
return list.some((item) => item.phase !== "done");
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user