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; // 附件类型