From bff8ffa100540ae86ceb66177dd26eec839f3b99 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 4 Mar 2026 01:59:23 +0800 Subject: [PATCH] feat: sync 3d canvas updates and block trackpad history swipe --- app/canvas/[id]/model3d/page.tsx | 1261 ++++++++++--------- app/canvas/[id]/page.tsx | 25 + components/canvas/model3d/Model3DViewer.tsx | 132 +- 3 files changed, 759 insertions(+), 659 deletions(-) diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx index 17c85464f..6d84a4c2e 100644 --- a/app/canvas/[id]/model3d/page.tsx +++ b/app/canvas/[id]/model3d/page.tsx @@ -4,19 +4,25 @@ import { formatDistanceToNow } from "date-fns"; import { zhCN } from "date-fns/locale"; import { ArrowLeft, + Bell, Bone, Box, Boxes, ChevronDown, ChevronUp, + CircleDollarSign, + Crown, Download, Eye, EyeOff, + Globe, Grid3X3, History, + Home, ImagePlus, Images, Layers3, + LayoutGrid, Loader2, Maximize, Package, @@ -30,12 +36,16 @@ import { Settings2, Sparkles, Sun, + User, WandSparkles, X, + Zap, } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; +import { useSession } from "next-auth/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import useSWR from "swr"; import { type Model3DLayer, type Model3DStats, @@ -77,7 +87,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; +import { cn, fetcher } from "@/lib/utils"; import { canvasService, type Model3DTaskStatus, @@ -210,9 +220,12 @@ function formatBalance(data: Record | undefined): string | null { data.amount, ]; for (const value of candidates) { - if (typeof value === "number" && Number.isFinite(value)) + if (typeof value === "number" && Number.isFinite(value)) { return String(value); - if (typeof value === "string" && value.trim()) return value.trim(); + } + if (typeof value === "string" && value.trim()) { + return value.trim(); + } } return null; } @@ -311,6 +324,8 @@ export default function CanvasModel3DPage() { const routeParams = useParams(); const id = String(routeParams?.id || ""); const historyStorageKey = useMemo(() => makeStorageKey(id), [id]); + const { data: session } = useSession(); + const user = session?.user; const viewport = useCanvasStore(canvasSelectors.viewport); const addNode = useCanvasStore((state) => state.addNode); @@ -409,13 +424,29 @@ export default function CanvasModel3DPage() { const [lowpolyQuad, setLowpolyQuad] = useState(false); const [lowpolyBake, setLowpolyBake] = useState(true); + // UI States + const [activeTab, setActiveTab] = useState<"create" | "edit" | "scene">( + "create" + ); + const pollAttemptsRef = useRef(0); const multiViewSlotsRef = useRef([]); const batchImageSlotsRef = useRef([]); + // Fetch site settings for Logo + const { data: siteSettingsData } = useSWR("/api/site-settings", fetcher); + + const siteLogo = siteSettingsData?.siteLogo; + const siteLogoDark = siteSettingsData?.siteLogoDark; + const siteName = siteSettingsData?.siteName || "SparkAI"; + + // Display logo logic (prefer dark mode logo since we are in dark theme) + const displayLogo = + siteLogoDark && siteLogoDark.trim() !== "" ? siteLogoDark : siteLogo; + // Effect hooks for data loading and cleanup useEffect(() => { - console.log("CanvasModel3DPage mounted - v2.0"); + console.log("CanvasModel3DPage mounted - v3.0 Dark Mode"); const loadBalance = async () => { const result = await model3DGenerationService.getBalance(); if (!result.success) return; @@ -425,6 +456,31 @@ export default function CanvasModel3DPage() { void loadBalance(); }, []); + useEffect(() => { + const html = document.documentElement; + const body = document.body; + const prevHtmlOverscrollX = html.style.overscrollBehaviorX; + const prevBodyOverscrollX = body.style.overscrollBehaviorX; + + html.style.overscrollBehaviorX = "none"; + body.style.overscrollBehaviorX = "none"; + + const preventHistorySwipe = (event: WheelEvent) => { + if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return; + if (event.cancelable) { + event.preventDefault(); + } + }; + + window.addEventListener("wheel", preventHistorySwipe, { passive: false }); + + return () => { + window.removeEventListener("wheel", preventHistorySwipe); + html.style.overscrollBehaviorX = prevHtmlOverscrollX; + body.style.overscrollBehaviorX = prevBodyOverscrollX; + }; + }, []); + useEffect(() => { if (typeof window === "undefined") return; const raw = window.localStorage.getItem(historyStorageKey); @@ -1196,334 +1252,316 @@ export default function CanvasModel3DPage() { const activePromptSummary = currentTask?.prompt?.trim() || "尚未创建任务"; return ( -
- {/* 1. Background Viewer */} -
- - - {/* Viewer Toolbar (Bottom Center) */} -
- - - - - - 网格 - - - - - - - 自动旋转 - - - - - - - 线框模式 - - - - -
- - - setViewerSettings((prev) => ({ - ...prev, - lightIntensity: values[0] ?? 1, - })) - } - step={0.1} - value={[viewerSettings.lightIntensity]} +
+ {/* Top Navigation Bar */} +
+
+ {/* Logo & Brand */} +
router.push("/")} + > + {displayLogo ? ( + {siteName} -
- -
-
+ ) : ( +
+ +
+ )} + + {siteName} + +
- {/* 2. Header (Top) */} -
-
+ {/* Divider */} +
+ + {/* Nav Links */} + +
+ +
+ {/* DCC Bridge (Mock) */} + + {/* Credits */} +
+ + {balanceLabel || "0"} +
+ + {/* Upgrade Button */} + + + {/* Divider */} +
+ + {/* Icons */} + -
- - 3D 工作台{" "} - - v2.0 - - - - Tripo3D 驱动 - -
-
-
- - 额度: {balanceLabel || "--"} - + + + {/* User Profile */} +
+ {user?.image ? ( + {user.name + ) : ( +
+ +
+ )} +
- {/* 3. Left Panel (History) */} -
-
-
-
- -

任务历史

+
+ {/* 1. Left Nav Sidebar (Icon only) */} + + + {/* 2. Left Tool Panel (Expandable Content) */} +
+
+

+ {activeTab === "create" && "生成模型"} + {activeTab === "edit" && "模型编辑"} + {activeTab === "scene" && "场景管理"} +

-
- {taskHistory.length === 0 ? ( -
- 暂无任务记录 -
- ) : ( - taskHistory.map((task) => ( -
-

- {task.prompt || "-"} -

-
- {new Date(task.createdAt).toLocaleString()} -
- - )) - )} -
- -
-
- - {/* 4. Right Panel (Controls) */} -
-
- -
- - 生成 - 编辑 - 场景 - -
- - - -
- {/* Mode Selector */} -
- - - - + + {m.label} + + ))}
- {/* Inputs */}
- +