| 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({