diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index 7a18500de..14d633723 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -192,6 +192,11 @@ import { sanitizeAgentWorkflowSelections, } from "@/lib/agents/workflow-ui-config"; import { cn, fetcher } from "@/lib/utils"; +import { + loadCanvasChatPanelModule, + preloadCanvasChatPanel, +} from "@/components/canvas/canvas-chat-panel-loader"; +import { preloadCanvasChatData } from "@/lib/client/canvas-chat-preload"; import { buildVideoWorkflowCanvasNode, getVideoWorkflowNodeDefinition, @@ -311,7 +316,7 @@ const CanvasWorkspace = dynamic( const CanvasChatPanel = dynamic( () => - import("@/components/canvas/CanvasChatPanel").then( + loadCanvasChatPanelModule().then( (module) => module.CanvasChatPanel ), { @@ -3278,16 +3283,14 @@ export default function CanvasEditorLegacyPage({ }; }, [checkCanvasAuthSession]); - useEffect( - () => - scheduleCanvasIdleTask(() => { - void Promise.all([ - import("@/components/canvas/CanvasChatPanel"), - import("@/components/canvas/CanvasFileListPanel"), - ]); - }, 700), - [] - ); + useEffect(() => { + preloadCanvasChatPanel(); + preloadCanvasChatData(id); + + return scheduleCanvasIdleTask(() => { + void import("@/components/canvas/CanvasFileListPanel"); + }, 700); + }, [id]); const hydrateCanvasProject = useCallback( (project: CanvasInitialProject) => { diff --git a/app/canvas/[id]/CanvasPageClientRouter.tsx b/app/canvas/[id]/CanvasPageClientRouter.tsx index be8c40b15..6522faf25 100644 --- a/app/canvas/[id]/CanvasPageClientRouter.tsx +++ b/app/canvas/[id]/CanvasPageClientRouter.tsx @@ -5,8 +5,9 @@ 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"; +import { loadCanvasEditorModule } from "./canvas-editor-loader"; -const CanvasEditorLegacyPage = dynamic(() => import("./CanvasEditorLegacyPageClient"), { +const CanvasEditorLegacyPage = dynamic(loadCanvasEditorModule, { loading: () => , ssr: false, }); diff --git a/app/canvas/[id]/canvas-editor-loader.ts b/app/canvas/[id]/canvas-editor-loader.ts new file mode 100644 index 000000000..90058804a --- /dev/null +++ b/app/canvas/[id]/canvas-editor-loader.ts @@ -0,0 +1,12 @@ +type CanvasEditorModule = typeof import("./CanvasEditorLegacyPageClient"); + +let canvasEditorModulePromise: Promise | null = null; + +export function loadCanvasEditorModule() { + canvasEditorModulePromise ??= import("./CanvasEditorLegacyPageClient"); + return canvasEditorModulePromise; +} + +export function preloadCanvasEditor() { + void loadCanvasEditorModule(); +} diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index a851455b9..9b77bde48 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -13152,6 +13152,7 @@ export function CanvasChatPanel({ return (
| null = null; + +export function loadCanvasChatPanelModule() { + canvasChatPanelModulePromise ??= import("./CanvasChatPanel"); + return canvasChatPanelModulePromise; +} + +export function preloadCanvasChatPanel() { + void loadCanvasChatPanelModule(); +} diff --git a/components/home/lovart-home.tsx b/components/home/lovart-home.tsx index f87304eb3..e2b74662e 100644 --- a/components/home/lovart-home.tsx +++ b/components/home/lovart-home.tsx @@ -58,6 +58,10 @@ import { type CanvasLaunchAttachment, getCanvasLaunchStorageKey, } from "@/lib/canvas-launch"; +import { + preloadCanvasWorkspace, + prepareCanvasNavigation, +} from "@/lib/client/canvas-workspace-preload"; import type { MembershipConfig } from "@/lib/membership/config"; import { normalizeLovartHomeSidebarConfig, @@ -918,6 +922,28 @@ export function LovartHome({ setLoginDialogOpen(false); }, [isAuthenticated]); + useEffect(() => { + if (!isAuthenticated || typeof window === "undefined") return; + + const idleWindow = window as typeof window & { + cancelIdleCallback?: (handle: number) => void; + requestIdleCallback?: ( + callback: IdleRequestCallback, + options?: IdleRequestOptions + ) => number; + }; + + if (typeof idleWindow.requestIdleCallback === "function") { + const idleId = idleWindow.requestIdleCallback(preloadCanvasWorkspace, { + timeout: 1200, + }); + return () => idleWindow.cancelIdleCallback?.(idleId); + } + + const timer = window.setTimeout(preloadCanvasWorkspace, 450); + return () => window.clearTimeout(timer); + }, [isAuthenticated]); + useEffect(() => { if (!loginDialogOpen) { setIsLoggingIn(false); @@ -1365,6 +1391,7 @@ export function LovartHome({ if (!prompt.trim() && files.length === 0) return; + preloadCanvasWorkspace(); setIsSubmitting(true); try { const uploadedAttachments = await uploadSelectedFiles(); @@ -1384,6 +1411,7 @@ export function LovartHome({ } const canvasId: string = canvasResult.data.id; + prepareCanvasNavigation(canvasId); if (typeof window !== "undefined") { sessionStorage.setItem( @@ -1408,7 +1436,16 @@ export function LovartHome({ files.forEach((item) => URL.revokeObjectURL(item.previewUrl)); setFiles([]); setPrompt(""); - await mutateCanvas(); + void mutateCanvas((current) => { + if (!current) return current; + return { + ...current, + data: [ + canvasResult.data as CanvasProject, + ...(current.data || []).filter((item) => item.id !== canvasId), + ], + }; + }, false); router.push(`/canvas/${canvasId}`); } catch (error) { toast.error(error instanceof Error ? error.message : "创建项目失败"); @@ -1417,14 +1454,63 @@ export function LovartHome({ } }; - const openNewCanvas = () => { + const openNewCanvas = async () => { if (!isAuthenticated) { openLoginDialog(); return; } - router.push("/canvas?new=1"); + if (isSubmitting) return; + + preloadCanvasWorkspace(); + setIsSubmitting(true); + try { + const response = await fetch("/api/canvas", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title: "未命名画布" }), + }); + const result = await response.json(); + + if (!response.ok || !result?.data?.id) { + throw new Error(result?.error || "创建项目失败"); + } + + const canvasId = String(result.data.id); + prepareCanvasNavigation(canvasId); + void mutateCanvas((current) => { + if (!current) return current; + return { + ...current, + data: [ + result.data as CanvasProject, + ...(current.data || []).filter((item) => item.id !== canvasId), + ], + }; + }, false); + router.push(`/canvas/${canvasId}`); + } catch (error) { + toast.error(error instanceof Error ? error.message : "创建项目失败"); + } finally { + setIsSubmitting(false); + } }; + const prepareCanvasProject = useCallback( + (projectId: string) => { + prepareCanvasNavigation(projectId); + router.prefetch(`/canvas/${projectId}`); + }, + [router] + ); + + const openCanvasProject = useCallback( + (projectId: string) => { + prepareCanvasProject(projectId); + router.push(`/canvas/${projectId}`); + }, + [prepareCanvasProject, router] + ); + const handleDeleteProject = async (projectId: string, projectTitle?: string) => { if (deletingProjectId === projectId) return; @@ -1863,7 +1949,9 @@ export function LovartHome({ isProjectsOnlyView={isProjectsOnlyView} onCreateProject={openNewCanvas} onDeleteProject={handleDeleteProject} - onOpenProject={(projectId) => router.push(`/canvas/${projectId}`)} + onOpenProject={openCanvasProject} + onPrepareCreate={preloadCanvasWorkspace} + onPrepareProject={prepareCanvasProject} onViewAllProjects={openAllProjectsInPage} projects={recentProjects} projectSectionRef={projectSectionRef} diff --git a/components/home/lovart/lovart-projects-section.tsx b/components/home/lovart/lovart-projects-section.tsx index c691564d7..b49b1cc5a 100644 --- a/components/home/lovart/lovart-projects-section.tsx +++ b/components/home/lovart/lovart-projects-section.tsx @@ -10,6 +10,8 @@ type LovartProjectsSectionProps = { onCreateProject: () => void; onDeleteProject: (projectId: string, projectTitle?: string) => void; onOpenProject: (projectId: string) => void; + onPrepareCreate: () => void; + onPrepareProject: (projectId: string) => void; onViewAllProjects: () => void; projects: CanvasProject[]; projectSectionRef: Ref; @@ -22,6 +24,8 @@ export function LovartProjectsSection({ onCreateProject, onDeleteProject, onOpenProject, + onPrepareCreate, + onPrepareProject, onViewAllProjects, projects, projectSectionRef, @@ -51,6 +55,8 @@ export function LovartProjectsSection({