Your Name e8a3212b35
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: persist style pro history and model pricing
2026-05-16 21:28:00 +08:00

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}
/>
);
}