185 lines
5.9 KiB
TypeScript
185 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { useCallback, useEffect } from "react";
|
|
import { toast } from "sonner";
|
|
import { StyleProWorkspace } from "@/components/style-pro/StyleProWorkspace";
|
|
import { canvasService } from "@/services/canvas";
|
|
import { canvasSelectors, useCanvasStore } from "@/store/canvas";
|
|
import type {
|
|
StyleProHistoryItem,
|
|
StyleProPersistedState,
|
|
} from "@/lib/style-pro/config";
|
|
import { normalizeStyleProPersistedState } from "@/lib/style-pro/config";
|
|
|
|
export default function CanvasStyleProPage() {
|
|
const router = useRouter();
|
|
const routeParams = useParams();
|
|
const id = String(routeParams?.id || "");
|
|
|
|
const viewport = useCanvasStore(canvasSelectors.viewport);
|
|
const selectedNodeIds = useCanvasStore(canvasSelectors.selectedNodeIds);
|
|
const nodes = useCanvasStore(canvasSelectors.nodes);
|
|
const styleProState = useCanvasStore((state) => state.styleProState);
|
|
const addNode = useCanvasStore((state) => state.addNode);
|
|
const loadCanvas = useCanvasStore((state) => state.loadCanvas);
|
|
const selectNodes = useCanvasStore((state) => state.selectNodes);
|
|
const setProject = useCanvasStore((state) => state.setProject);
|
|
const setStyleProState = useCanvasStore((state) => state.setStyleProState);
|
|
const getCanvasState = useCanvasStore((state) => state.getCanvasState);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const loadPersistedStyleProState = async () => {
|
|
if (!id) return;
|
|
const currentState = useCanvasStore.getState();
|
|
if (
|
|
currentState.projectId === id &&
|
|
(currentState.nodes.length > 0 ||
|
|
currentState.styleProState.history.length > 0)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const result = await canvasService.getProject(id);
|
|
if (cancelled || !result.success || !result.data?.canvasData) return;
|
|
|
|
const canvasData = result.data.canvasData;
|
|
const latestState = useCanvasStore.getState();
|
|
setProject(result.data.id, result.data.title);
|
|
if (latestState.nodes.length === 0 && latestState.edges.length === 0) {
|
|
loadCanvas(canvasData);
|
|
return;
|
|
}
|
|
|
|
const nextStyleProState = normalizeStyleProPersistedState(
|
|
canvasData.styleProState
|
|
);
|
|
if (nextStyleProState.history.length > 0) {
|
|
setStyleProState(nextStyleProState);
|
|
}
|
|
};
|
|
|
|
void loadPersistedStyleProState();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [id, loadCanvas, setProject, setStyleProState]);
|
|
|
|
const selectedImageNode =
|
|
selectedNodeIds.length === 1
|
|
? nodes.find(
|
|
(node) => node.id === selectedNodeIds[0] && node.type === "image"
|
|
)
|
|
: null;
|
|
const selectedImageUrl = selectedImageNode
|
|
? String((selectedImageNode.data as any)?.src || "").trim()
|
|
: "";
|
|
|
|
const handleBack = useCallback(() => {
|
|
router.push(`/canvas/${id}`);
|
|
}, [id, router]);
|
|
|
|
const handlePersistState = useCallback(
|
|
async (nextState: StyleProPersistedState) => {
|
|
useCanvasStore.getState().setStyleProState(nextState);
|
|
const result = await canvasService.saveCanvasState(
|
|
id,
|
|
useCanvasStore.getState().getCanvasState()
|
|
);
|
|
if (!result.success) {
|
|
console.error(
|
|
"[CanvasStyleProPage] 保存风格化 PRO 状态失败:",
|
|
result.error
|
|
);
|
|
}
|
|
},
|
|
[id]
|
|
);
|
|
|
|
const handleAddToCanvas = useCallback(
|
|
async (item: StyleProHistoryItem) => {
|
|
if (!item.resultImageUrl) {
|
|
toast.error("没有可添加到画布的结果图");
|
|
return;
|
|
}
|
|
|
|
const viewportX = viewport.x || 0;
|
|
const viewportY = viewport.y || 0;
|
|
const viewportZoom = viewport.zoom || 1;
|
|
const stageWidth =
|
|
typeof window !== "undefined" ? window.innerWidth : 1280;
|
|
const stageHeight =
|
|
typeof window !== "undefined" ? window.innerHeight : 720;
|
|
const naturalWidth = Number(item.width || 0);
|
|
const naturalHeight = Number(item.height || 0);
|
|
const hasNaturalSize = naturalWidth > 0 && naturalHeight > 0;
|
|
const maxPreviewEdge = 460;
|
|
const fallbackSize = 360;
|
|
const scale = hasNaturalSize
|
|
? Math.min(1, maxPreviewEdge / Math.max(naturalWidth, naturalHeight))
|
|
: 1;
|
|
const targetWidth = hasNaturalSize
|
|
? Math.max(160, Math.round(naturalWidth * scale))
|
|
: fallbackSize;
|
|
const targetHeight = hasNaturalSize
|
|
? Math.max(160, Math.round(naturalHeight * scale))
|
|
: fallbackSize;
|
|
const x =
|
|
(stageWidth * 0.46 - viewportX) / viewportZoom - targetWidth / 2;
|
|
const y =
|
|
(stageHeight * 0.46 - viewportY) / viewportZoom - targetHeight / 2;
|
|
|
|
const nodeId = addNode({
|
|
type: "image",
|
|
position: { x, y },
|
|
size: { width: targetWidth, height: targetHeight },
|
|
data: {
|
|
src: item.resultImageUrl,
|
|
originalSrc: item.sourceImageUrl,
|
|
status: "completed",
|
|
title: "风格化 PRO 结果",
|
|
prompt: item.optionLabels.join(" / ") || "风格化 PRO",
|
|
model: item.model || "Style Pro",
|
|
metadata: {
|
|
compareSourceUrl: item.sourceImageUrl,
|
|
styleProHistoryId: item.id,
|
|
styleProSelections: item.selections,
|
|
},
|
|
},
|
|
connections: [],
|
|
});
|
|
|
|
selectNodes([nodeId]);
|
|
const result = await canvasService.saveCanvasState(id, getCanvasState());
|
|
if (!result.success) {
|
|
toast.error(result.error || "保存画布失败");
|
|
return;
|
|
}
|
|
toast.success("风格化 PRO 结果已添加到画布");
|
|
},
|
|
[
|
|
addNode,
|
|
getCanvasState,
|
|
id,
|
|
selectNodes,
|
|
viewport.x,
|
|
viewport.y,
|
|
viewport.zoom,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<StyleProWorkspace
|
|
canvasId={id}
|
|
initialSourceImageUrl={selectedImageUrl}
|
|
onAddToCanvas={handleAddToCanvas}
|
|
onBack={handleBack}
|
|
onPersistState={handlePersistState}
|
|
persistedState={styleProState}
|
|
/>
|
|
);
|
|
}
|