diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx
index 7d517de98..8e501117e 100644
--- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx
+++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx
@@ -68,6 +68,7 @@ import {
useCallback,
useDeferredValue,
useEffect,
+ useLayoutEffect,
useMemo,
useRef,
useState,
@@ -243,6 +244,7 @@ import type {
import type {
AgentSlotNodeData,
CanvasEdge,
+ CanvasInitialProject,
CanvasNode,
CanvasState,
CanvasWorkspaceType,
@@ -523,6 +525,7 @@ const AgentWorkflowMaterialSelectionPanel = dynamic(
interface PageProps {
params: Promise<{ id: string }>;
+ initialProject?: CanvasInitialProject | null;
}
// 图片尺寸预设
@@ -1798,7 +1801,10 @@ function resolveCanvasImageDisplaySize(url: string) {
});
}
-export default function CanvasEditorLegacyPage({ params }: PageProps) {
+export default function CanvasEditorLegacyPage({
+ initialProject = null,
+ params,
+}: PageProps) {
// 使用 useParams 确保客户端导航时能正确获取更新的 id
const routeParams = useParams();
const searchParams = useSearchParams();
@@ -1826,6 +1832,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const createNewCanvasUrl = isVideoMode
? "/video-canvas?new=1"
: "/canvas?new=1";
+ const initialProjectForCurrentRoute =
+ initialProject?.id === id ? initialProject : null;
const shouldOpenBatchProcessing = searchParams.get("batch") === "1";
const goHome = useCallback(() => {
if (typeof window !== "undefined") {
@@ -1874,7 +1882,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
);
// Local state
- const [isLoading, setIsLoading] = useState(true);
+ const [isLoading, setIsLoading] = useState(
+ () => !initialProjectForCurrentRoute
+ );
const [isSaving, setIsSaving] = useState(false);
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [isAddImageDialogOpen, setIsAddImageDialogOpen] = useState(false);
@@ -3276,11 +3286,140 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
return () => window.clearTimeout(timeoutId);
}, []);
+ const hydrateCanvasProject = useCallback(
+ (project: CanvasInitialProject) => {
+ useCanvasStore.getState().setProject(id, project.title);
+ const projectUpdatedAt = String(project.updatedAt || "");
+ useCanvasStore
+ .getState()
+ .setCanvasRevision(projectUpdatedAt, projectUpdatedAt);
+
+ if (project.canvasData) {
+ useCanvasStore.getState().loadCanvas(project.canvasData);
+ const persistedWorkflow = project.canvasData.workflowState;
+ if (!requestedAgentMode && persistedWorkflow) {
+ const nextAppearanceRecognitionEnabled =
+ persistedWorkflow.appearanceRecognitionEnabled === true;
+ const nextWorkflowMaterials = nextAppearanceRecognitionEnabled
+ ? normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials)
+ : clearAppearanceSummaryFromMaterials(
+ normalizeAgentWorkflowMaterialSelections(
+ persistedWorkflow.materials
+ )
+ );
+ setWorkflowInputs({
+ primarySku: String(
+ persistedWorkflow.inputs?.primarySku || ""
+ ).trim(),
+ secondarySku: String(
+ persistedWorkflow.inputs?.secondarySku || ""
+ ).trim(),
+ brand: String(persistedWorkflow.inputs?.brand || "").trim(),
+ store: String(persistedWorkflow.inputs?.store || "").trim(),
+ officialSecondCategory: String(
+ persistedWorkflow.inputs?.officialSecondCategory || ""
+ ).trim(),
+ officialThirdCategory: String(
+ persistedWorkflow.inputs?.officialThirdCategory || ""
+ ).trim(),
+ thirdCategory: String(
+ persistedWorkflow.inputs?.thirdCategory || ""
+ ).trim(),
+ fourthCategory: String(
+ persistedWorkflow.inputs?.fourthCategory || ""
+ ).trim(),
+ fifthCategory: String(
+ persistedWorkflow.inputs?.fifthCategory || ""
+ ).trim(),
+ priceBand: String(
+ persistedWorkflow.inputs?.priceBand || ""
+ ).trim(),
+ });
+ setWorkflowMaterials(nextWorkflowMaterials);
+ setAppearanceRecognitionEnabled(nextAppearanceRecognitionEnabled);
+ setWorkflowSelections(persistedWorkflow.selections || {});
+ setSelectedBrandModel(
+ persistedWorkflow.selectedBrandModel
+ ? {
+ ...persistedWorkflow.selectedBrandModel,
+ brandName: String(
+ persistedWorkflow.selectedBrandModel.brandName || ""
+ ).trim(),
+ id: String(
+ persistedWorkflow.selectedBrandModel.id || ""
+ ).trim(),
+ imageUrl: String(
+ persistedWorkflow.selectedBrandModel.imageUrl || ""
+ ).trim(),
+ assetId:
+ String(
+ persistedWorkflow.selectedBrandModel.assetId || ""
+ ).trim() || null,
+ displayName:
+ String(
+ persistedWorkflow.selectedBrandModel.displayName || ""
+ ).trim() || null,
+ mimeType:
+ String(
+ persistedWorkflow.selectedBrandModel.mimeType || ""
+ ).trim() || null,
+ sortOrder: Number.isFinite(
+ Number(persistedWorkflow.selectedBrandModel.sortOrder)
+ )
+ ? Math.max(
+ 0,
+ Math.min(
+ 999,
+ Math.round(
+ Number(
+ persistedWorkflow.selectedBrandModel.sortOrder
+ )
+ )
+ )
+ )
+ : 0,
+ }
+ : null
+ );
+ setSelectedAgentPresetId(
+ String(persistedWorkflow.selectedAgentPresetId || "").trim() ||
+ null
+ );
+ setCurrentAgentStage(
+ persistedWorkflow.currentStage === "mode" ? "mode" : "workflow"
+ );
+ setAgentModeState(
+ normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState)
+ );
+ setWorkflowHydrationSource("project");
+ return;
+ }
+ }
+
+ setWorkflowHydrationSource("empty");
+ },
+ [id, requestedAgentMode]
+ );
+
+ useLayoutEffect(() => {
+ if (!initialProjectForCurrentRoute) return;
+ if (lastLoadedCanvasIdRef.current === id) return;
+
+ hydrateCanvasProject(initialProjectForCurrentRoute);
+ setIsLoading(false);
+ lastLoadedCanvasIdRef.current = id;
+ }, [hydrateCanvasProject, id, initialProjectForCurrentRoute]);
+
// 加载画布数据
useEffect(() => {
console.log("[Canvas] Loading project:", id);
const loadProject = async () => {
+ if (lastLoadedCanvasIdRef.current === id) {
+ setIsLoading(false);
+ return;
+ }
+
setIsLoading(true);
if (!requestedAgentMode) {
setWorkflowHydrationSource("pending");
diff --git a/app/canvas/[id]/CanvasPageClientRouter.tsx b/app/canvas/[id]/CanvasPageClientRouter.tsx
index 6c4fef3fa..0eb394c91 100644
--- a/app/canvas/[id]/CanvasPageClientRouter.tsx
+++ b/app/canvas/[id]/CanvasPageClientRouter.tsx
@@ -4,6 +4,7 @@ import dynamic from "next/dynamic";
import { useParams, useSearchParams } from "next/navigation";
import { createContext, type ReactNode, useContext, useMemo } from "react";
import { CanvasLogoLoadingView } from "@/components/canvas/canvas-logo-loading";
+import type { CanvasInitialProject } from "@/types/canvas";
const CanvasEditorLegacyPage = dynamic(() => import("./CanvasEditorLegacyPageClient"), {
loading: () => ,
@@ -25,6 +26,7 @@ export function CanvasPageClientRouter({
initialSiteLogoDark = null,
initialSiteName = "AI Chatbot",
initialWorkspaceType = "",
+ initialProject = null,
}: {
initialId: string;
initialMode?: string;
@@ -32,6 +34,7 @@ export function CanvasPageClientRouter({
initialSiteLogoDark?: string | null;
initialSiteName?: string | null;
initialWorkspaceType?: string;
+ initialProject?: CanvasInitialProject | null;
}) {
const routeParams = useParams();
const searchParams = useSearchParams();
@@ -70,7 +73,10 @@ export function CanvasPageClientRouter({
return (
-
+
);
}
diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx
index 9487759c8..c6e104bbb 100644
--- a/app/canvas/[id]/page.tsx
+++ b/app/canvas/[id]/page.tsx
@@ -1,6 +1,11 @@
import { Suspense } from "react";
import { CanvasLogoLoadingView } from "@/components/canvas/canvas-logo-loading";
+import { auth } from "@/app/(auth)/auth";
+import { db } from "@/lib/db";
+import { canvasProject } from "@/lib/db/schema";
import { getSiteSettings } from "@/lib/db/queries";
+import { and, eq } from "drizzle-orm";
+import type { CanvasInitialProject } from "@/types/canvas";
import { CanvasPageClientRouter } from "./CanvasPageClientRouter";
type CanvasEditorPageProps = {
@@ -15,13 +20,40 @@ function getFirstParamValue(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] || "" : value || "";
}
+async function getInitialCanvasProject(id: string): Promise {
+ const session = await auth();
+ if (!session?.user?.id) return null;
+
+ const [project] = await db
+ .select()
+ .from(canvasProject)
+ .where(
+ and(
+ eq(canvasProject.id, id),
+ eq(canvasProject.userId, session.user.id)
+ )
+ )
+ .limit(1);
+
+ if (!project) return null;
+
+ return {
+ ...project,
+ createdAt: project.createdAt.toISOString(),
+ updatedAt: project.updatedAt.toISOString(),
+ };
+}
+
export default async function CanvasEditorPage({
params,
searchParams,
}: CanvasEditorPageProps) {
const { id } = await params;
const resolvedSearchParams = searchParams ? await searchParams : {};
- const siteSettings = await getSiteSettings().catch(() => null);
+ const [siteSettings, initialProject] = await Promise.all([
+ getSiteSettings().catch(() => null),
+ getInitialCanvasProject(id).catch(() => null),
+ ]);
const loadingBrand = {
siteLogo: siteSettings?.siteLogo,
siteLogoDark: siteSettings?.siteLogoDark,
@@ -39,6 +71,7 @@ export default async function CanvasEditorPage({
initialWorkspaceType={getFirstParamValue(
resolvedSearchParams.workspaceType
)}
+ initialProject={initialProject}
/>
);
diff --git a/types/canvas/index.ts b/types/canvas/index.ts
index bb9faabd1..2683959d2 100644
--- a/types/canvas/index.ts
+++ b/types/canvas/index.ts
@@ -271,6 +271,17 @@ export interface CanvasProject {
updatedAt: Date;
}
+export type CanvasInitialProject = Omit<
+ CanvasProject,
+ "canvasData" | "createdAt" | "description" | "thumbnail" | "updatedAt"
+> & {
+ canvasData: any;
+ createdAt: string;
+ description?: string | null;
+ thumbnail?: string | null;
+ updatedAt: string;
+};
+
export type CanvasProjectListItem = Omit;
// 附件类型