Files
Genarrative/src/components/image-editor/useImageCanvasProjectPersistence.ts
kdletters f34556d33d 拆分图片画布图片信息弹窗
新增图片信息弹窗组件,承接 metadata 详情渲染和 UnifiedModal 接入

修复未登录进入编辑器时项目和素材接口抢跑 401

修复重置画布视图点击事件误传导致适合视图报错

补充图片信息弹窗、鉴权门禁和重置按钮回归测试

更新前端拆分文档和 TRACKING 浏览器回归记录
2026-06-17 10:56:51 +08:00

252 lines
7.7 KiB
TypeScript

import { type RefObject, useCallback, useEffect, useRef, useState } from 'react';
import { ApiClientError } from '../../services/apiClient';
import {
createEditorProjectResource,
loadEditorProject,
loadOrCreateRecentEditorProject,
saveEditorProjectLayout,
} from '../../services/image-editor/editorProjectClient';
import { hydrateLayer, serializeLayer } from './ImageCanvasEditorModel';
import type { CanvasLayer, CanvasViewport } from './ImageCanvasEditorTypes';
type ProjectResourceOptions = {
onCreated?: (resourceId: string) => void;
snapshotLayers?: CanvasLayer[];
};
type PendingProjectResourceLayer = {
layer: CanvasLayer;
options: ProjectResourceOptions;
};
type ImageCanvasProjectPersistenceRefs = {
layersRef: RefObject<CanvasLayer[]>;
viewportRef: RefObject<CanvasViewport>;
};
type ImageCanvasProjectPersistenceSetters = {
setProjectTitle: (title: string) => void;
setProjectRenameValue: (title: string) => void;
setViewport: (viewport: CanvasViewport) => void;
setLayers: (layers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
setLayerCounter: (value: number) => void;
};
function isEditorAuthError(error: unknown) {
return (
error instanceof ApiClientError &&
(error.status === 401 || error.status === 403)
);
}
export function useImageCanvasProjectPersistence({
refs,
setters,
layers,
viewport,
canAccessProtectedData,
openEditorLoginModal,
}: {
refs: ImageCanvasProjectPersistenceRefs;
setters: ImageCanvasProjectPersistenceSetters;
layers: CanvasLayer[];
viewport: CanvasViewport;
canAccessProtectedData: boolean;
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
}) {
const projectIdRef = useRef<string | null>(null);
const pendingProjectResourceLayersRef = useRef<PendingProjectResourceLayer[]>(
[],
);
const saveTimerRef = useRef<number | null>(null);
const [projectId, setProjectId] = useState<string | null>(null);
const [isProjectReady, setIsProjectReady] = useState(false);
const createProjectResourceForLayer = useCallback(
(layer: CanvasLayer, options: ProjectResourceOptions = {}) => {
const readyProjectId = projectIdRef.current;
if (!readyProjectId) {
pendingProjectResourceLayersRef.current.push({ layer, options });
return;
}
createEditorProjectResource(readyProjectId, {
imageSrc: layer.src,
objectKey: layer.objectKey,
assetObjectId: layer.assetObjectId,
width: layer.originalWidth,
height: layer.originalHeight,
sourceType: layer.sourceType,
prompt: layer.prompt,
actualPrompt: layer.actualPrompt,
model: layer.model,
provider: layer.provider,
taskId: layer.taskId,
sourceResourceId: layer.sourceResourceId,
})
.then((resource) => {
options.onCreated?.(resource.resourceId);
const layerWithResourceId = {
...layer,
resourceId: resource.resourceId,
};
const currentLayers = refs.layersRef.current;
const nextLayers = currentLayers.some(
(currentLayer) => currentLayer.id === layer.id,
)
? currentLayers.map((currentLayer) =>
currentLayer.id === layer.id
? layerWithResourceId
: currentLayer,
)
: options.snapshotLayers?.some(
(snapshotLayer) => snapshotLayer.id === layer.id,
)
? options.snapshotLayers.map((snapshotLayer) =>
snapshotLayer.id === layer.id
? layerWithResourceId
: snapshotLayer,
)
: currentLayers;
refs.layersRef.current = nextLayers;
setters.setLayers(nextLayers);
if (nextLayers.length) {
void saveEditorProjectLayout(readyProjectId, {
viewport: refs.viewportRef.current,
layers: nextLayers.map(serializeLayer),
}).catch((error: unknown) => {
if (isEditorAuthError(error)) {
openEditorLoginModal();
}
});
}
})
.catch((error: unknown) => {
if (isEditorAuthError(error)) {
openEditorLoginModal();
}
});
},
[openEditorLoginModal, refs, setters],
);
const appendCanvasLayersWithResources = useCallback(
(nextLayers: CanvasLayer[]) => {
if (!nextLayers.length) {
return;
}
const snapshotLayers = [...refs.layersRef.current, ...nextLayers];
refs.layersRef.current = snapshotLayers;
setters.setLayers(snapshotLayers);
nextLayers.forEach((layer) =>
createProjectResourceForLayer(layer, { snapshotLayers }),
);
},
[createProjectResourceForLayer, refs, setters],
);
useEffect(() => {
if (!canAccessProtectedData) {
setIsProjectReady(false);
return undefined;
}
let cancelled = false;
const projectIdFromQuery =
typeof window === 'undefined'
? null
: new URLSearchParams(window.location.search)
.get('projectid')
?.trim() || null;
const loadProject = projectIdFromQuery
? loadEditorProject(projectIdFromQuery)
: loadOrCreateRecentEditorProject();
loadProject
.then((project) => {
if (cancelled) {
return;
}
projectIdRef.current = project.projectId;
setProjectId(project.projectId);
const nextProjectTitle = project.title?.trim() || '未命名画布';
setters.setProjectTitle(nextProjectTitle);
setters.setProjectRenameValue(nextProjectTitle);
const pendingLayers = pendingProjectResourceLayersRef.current.splice(0);
pendingLayers.forEach(({ layer, options }) => {
createProjectResourceForLayer(layer, options);
});
setters.setViewport(project.viewport);
const resourcesById = new Map(
project.resources.map((resource) => [
resource.resourceId,
{ imageSrc: resource.imageSrc },
]),
);
const hydratedLayers = project.layers
.map((layer) => hydrateLayer(layer, resourcesById))
.filter((layer): layer is CanvasLayer => Boolean(layer));
if (hydratedLayers.length > 0) {
setters.setLayerCounter(hydratedLayers.length);
setters.setLayers(hydratedLayers);
setters.selectSingleLayer(hydratedLayers[0]?.id ?? null);
}
setIsProjectReady(true);
})
.catch((error: unknown) => {
if (cancelled) {
return;
}
setIsProjectReady(false);
if (isEditorAuthError(error)) {
openEditorLoginModal(() => {
window.location.reload();
});
}
});
return () => {
cancelled = true;
};
}, [
canAccessProtectedData,
createProjectResourceForLayer,
openEditorLoginModal,
setters,
]);
useEffect(() => {
if (!projectId || !isProjectReady) {
return undefined;
}
if (saveTimerRef.current) {
window.clearTimeout(saveTimerRef.current);
}
saveTimerRef.current = window.setTimeout(() => {
saveEditorProjectLayout(projectId, {
viewport,
layers: layers.map(serializeLayer),
}).catch((error: unknown) => {
if (isEditorAuthError(error)) {
openEditorLoginModal();
}
});
}, 450);
return () => {
if (saveTimerRef.current) {
window.clearTimeout(saveTimerRef.current);
}
};
}, [isProjectReady, layers, openEditorLoginModal, projectId, viewport]);
return {
projectId,
isProjectReady,
projectIdRef,
createProjectResourceForLayer,
appendCanvasLayersWithResources,
};
}