新增图片信息弹窗组件,承接 metadata 详情渲染和 UnifiedModal 接入 修复未登录进入编辑器时项目和素材接口抢跑 401 修复重置画布视图点击事件误传导致适合视图报错 补充图片信息弹窗、鉴权门禁和重置按钮回归测试 更新前端拆分文档和 TRACKING 浏览器回归记录
252 lines
7.7 KiB
TypeScript
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,
|
|
};
|
|
}
|