From a9aa01495a26767fda1c255aaafa42bd691e48a6 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Sat, 25 Apr 2026 16:03:45 +0800 Subject: [PATCH 1/3] feat(asset-center): transplant obsidian workbench UI onto xuxiake baseline --- .../asset-center/AssetCenterClient.test.tsx | 2 +- .../app/asset-center/AssetCenterClient.tsx | 641 ++++++++++-------- .../src/components/3d/ModelViewer.tsx | 62 +- .../src/components/layout/AppShell.tsx | 13 +- .../src/components/layout/AppTopNav.tsx | 79 ++- 5 files changed, 507 insertions(+), 290 deletions(-) diff --git a/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx b/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx index a7d7f749..7fa676de 100644 --- a/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx +++ b/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx @@ -790,7 +790,7 @@ describe('AssetCenterClient', () => { await actor.clear(screen.getByLabelText('纹理重复')) await actor.type(screen.getByLabelText('纹理重复'), '2') - await actor.click(screen.getByRole('button', { name: '保存 UV' })) + await actor.click(screen.getByRole('button', { name: '确认并同步修改' })) expect(mockedUpdateAssetVariant).toHaveBeenCalledWith( 'variant-board-uv', diff --git a/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx b/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx index 30d00c37..8c0e8f9d 100644 --- a/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx +++ b/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx @@ -35,7 +35,6 @@ import { import { DeleteConfirmDialog } from '@/components/ui/DeleteConfirmDialog' import { useToast, ToastContainer } from '@/components/ui/Toast' import { ModelPreviewPanel } from '@/components/platform/ModelPreviewPanel' -import { PlatformSplitLayout } from '@/components/platform/PlatformSplitLayout' import type { PlatformMetricItem } from '@/components/platform/types' import { applyAssetVariantUvConfigToMaterial, @@ -621,28 +620,28 @@ function canReviewAssets(role: string): boolean { } const compactRowActionClassName = - 'inline-flex h-7 items-center rounded-full border border-apple-separator/60 bg-apple-bg-secondary/70 px-2 text-[11px] text-apple-label-secondary transition hover:border-apple-separator hover:bg-apple-bg-secondary hover:text-apple-label-primary disabled:cursor-not-allowed disabled:opacity-50' + 'inline-flex h-7 items-center rounded-[4px] border border-black/[0.04] bg-black/[0.03] px-2 text-[11px] text-[#5a6062] transition hover:border-black/10 hover:bg-black/[0.05] hover:text-[#2d3335] disabled:cursor-not-allowed disabled:opacity-50' const compactRowPrimaryActionClassName = - 'inline-flex h-7 items-center rounded-full border border-apple-blue/15 bg-apple-blue/8 px-2 text-[11px] text-apple-blue transition hover:bg-apple-blue/12 disabled:cursor-not-allowed disabled:opacity-50' + 'inline-flex h-7 items-center rounded-[4px] border border-[#2d3335]/10 bg-[#2d3335] px-2 text-[11px] text-white transition hover:bg-[#384042] disabled:cursor-not-allowed disabled:opacity-50' const compactRowDangerActionClassName = - 'inline-flex h-7 items-center rounded-full border border-apple-red/15 bg-apple-red/5 px-2 text-[11px] text-apple-red transition hover:bg-apple-red/10 disabled:cursor-not-allowed disabled:opacity-50' + 'inline-flex h-7 items-center rounded-[4px] border border-red-500/10 bg-red-500/5 px-2 text-[11px] text-red-600 transition hover:bg-red-500/10 disabled:cursor-not-allowed disabled:opacity-50' const compactRowNeutralStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-full border border-apple-separator/60 bg-apple-bg-secondary/70 px-2 text-[11px] text-apple-label-secondary' + 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-black/[0.04] bg-black/[0.03] px-2 text-[11px] text-[#5a6062]' const compactRowSuccessStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-full border border-apple-green/15 bg-apple-green/8 px-2 text-[11px] text-apple-green' + 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-emerald-500/10 bg-emerald-500/5 px-2 text-[11px] text-emerald-700' const compactRowWarningStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-full border border-apple-orange/15 bg-apple-orange/5 px-2 text-[11px] text-apple-orange' + 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-amber-500/10 bg-amber-500/5 px-2 text-[11px] text-amber-700' const compactRowDangerStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-full border border-apple-red/15 bg-apple-red/5 px-2 text-[11px] text-apple-red' + 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-red-500/10 bg-red-500/5 px-2 text-[11px] text-red-600' const compactRowIconActionClassName = - 'inline-flex h-7 w-7 items-center justify-center rounded-full border border-apple-separator/40 bg-apple-bg-secondary/60 text-apple-label-secondary transition hover:border-apple-separator hover:bg-apple-bg-secondary hover:text-apple-label-primary' + 'inline-flex h-7 w-7 items-center justify-center rounded-[4px] border border-black/[0.04] bg-black/[0.03] text-[#5a6062] transition hover:border-black/10 hover:bg-black/[0.05] hover:text-[#2d3335]' type ReviewDialogState = { targetType: 'family' | 'variant' @@ -1355,6 +1354,14 @@ export default function AssetCenterClient({ // ---- 规格款弹窗操作 ---- + function openCreateVariantModal(family: AssetFamilyDTO) { + setVariantModalMode('create') + setEditingVariantId(null) + setVariantForm(emptyVariantForm(family.id)) + setExpandedFamilyIds((prev) => new Set(prev).add(family.id)) + setVariantModalOpen(true) + } + function openDuplicateVariantModal(variant: AssetVariantDTO) { const dims = variant.dimensions ?? {} const mp = variant.mountPoints?.[0] @@ -2271,90 +2278,113 @@ export default function AssetCenterClient({ return ( <> - +
+ { + void handleRegenerateVariantThumbnail() + }} + disabled={submitting} + > + 重生成缩略图 + + ) : null + } + showCameraActionButtons + defaultShowSceneGrid + enableTranslateGizmo + gizmoMode={gizmoMode} + onGizmoModeChange={setGizmoMode} + selectedNodeName={selectedPreviewNodeName} + selectableNodeNames={previewNodeNames} + nodePositionOffsets={previewNodeOffsets} + nodeRotationStates={previewNodeRotations} + materialAssignments={previewMaterialAssignments} + onNodePositionOffsetChange={handlePreviewNodePositionOffsetChange} + onNodeRotationChange={handlePreviewNodeRotationChange} + onGizmoInteractionStart={handlePreviewTransformStart} + onGizmoInteractionEnd={handlePreviewTransformEnd} + onModelNodesChange={(nodeNames) => { + setPreviewNodeNames(nodeNames) + setPreviewNodeOffsetMap((current) => { + const validNodeNameSet = new Set(nodeNames) + const nextEntries = Object.entries(current).filter(([nodeName]) => + validNodeNameSet.has(nodeName), + ) + if (nextEntries.length === Object.keys(current).length) { + return current + } + return Object.fromEntries(nextEntries) + }) + setPreviewNodeRotationMap((current) => { + const validNodeNameSet = new Set(nodeNames) + const nextEntries = Object.entries(current).filter(([nodeName]) => + validNodeNameSet.has(nodeName), + ) + if (nextEntries.length === Object.keys(current).length) { + return current + } + return Object.fromEntries(nextEntries) + }) + setSelectedPreviewNodeName((current) => + current && nodeNames.includes(current) ? current : nodeNames[0] ?? null, + ) + }} + onNodeSelect={(payload) => { + setSelectedPreviewNodeName(payload.nodeName ?? payload.rawNodeName ?? null) + }} + onNodeContextMenu={(payload) => { + setSelectedPreviewNodeName(payload.nodeName ?? payload.rawNodeName ?? selectedPreviewNodeName) + setPreviewContextMenuState({ + visible: true, + x: payload.clientX, + y: payload.clientY, + nodeName: payload.nodeName ?? payload.rawNodeName ?? selectedPreviewNodeName, + }) + setActivePreviewSubmenu(null) + setActiveFinishCategoryId(null) + }} + /> +
+

+ WORKSPACE +

+

+ 左键拖拽旋转 · 滚轮缩放 · 中键平移 · 右键快捷修改 +

+
+
- leftContent={ - { - void handleRegenerateVariantThumbnail() - }} - disabled={submitting} - > - 重生成缩略图 - - ) : null - } - showCameraActionButtons - defaultShowSceneGrid - enableTranslateGizmo - gizmoMode={gizmoMode} - onGizmoModeChange={setGizmoMode} - selectedNodeName={selectedPreviewNodeName} - selectableNodeNames={previewNodeNames} - nodePositionOffsets={previewNodeOffsets} - nodeRotationStates={previewNodeRotations} - materialAssignments={previewMaterialAssignments} - onNodePositionOffsetChange={handlePreviewNodePositionOffsetChange} - onNodeRotationChange={handlePreviewNodeRotationChange} - onGizmoInteractionStart={handlePreviewTransformStart} - onGizmoInteractionEnd={handlePreviewTransformEnd} - onModelNodesChange={(nodeNames) => { - setPreviewNodeNames(nodeNames) - setPreviewNodeOffsetMap((current) => { - const validNodeNameSet = new Set(nodeNames) - const nextEntries = Object.entries(current).filter(([nodeName]) => - validNodeNameSet.has(nodeName), - ) - if (nextEntries.length === Object.keys(current).length) { - return current - } - return Object.fromEntries(nextEntries) - }) - setPreviewNodeRotationMap((current) => { - const validNodeNameSet = new Set(nodeNames) - const nextEntries = Object.entries(current).filter(([nodeName]) => - validNodeNameSet.has(nodeName), - ) - if (nextEntries.length === Object.keys(current).length) { - return current - } - return Object.fromEntries(nextEntries) - }) - setSelectedPreviewNodeName((current) => - current && nodeNames.includes(current) ? current : nodeNames[0] ?? null, - ) - }} - onNodeSelect={(payload) => { - setSelectedPreviewNodeName(payload.nodeName ?? payload.rawNodeName ?? null) - }} - onNodeContextMenu={(payload) => { - setSelectedPreviewNodeName(payload.nodeName ?? payload.rawNodeName ?? selectedPreviewNodeName) - setPreviewContextMenuState({ - visible: true, - x: payload.clientX, - y: payload.clientY, - nodeName: payload.nodeName ?? payload.rawNodeName ?? selectedPreviewNodeName, - }) - setActivePreviewSubmenu(null) - setActiveFinishCategoryId(null) - }} - /> - } - rightContent={ - <> + + {previewContextMenuState.visible && selectedVariant && ( - {/* 规格款弹窗 —— 放在 PlatformSplitLayout 外部,避免被父级 overflow/transform 裁切 */} + {/* 规格款弹窗 —— 放在资产中心工作台外部,避免被父级 overflow/transform 裁切 */} -
-
-

- 加载 3D 模型中... +

+
+
+ + MODEL STREAM + + + SYNC + +
+
+
+
+

+ 3D 模型流正在建立 +

+

+ 正在读取模型文件与场景网格

)} {error && ( -
-
-
- +
+
+
+
+

+ MODEL INTEGRITY +

+

+ 3D 模型加载失败 +

+
+
+ +
-

- 3D 模型加载失败 -

-

+

{error}

-
- {retryCount > 0 && ( - - 已重试 {retryCount} 次 + + RETRY {retryCount} )}
-

- 如果问题持续存在,请检查模型文件或联系技术支持 +

+ 请核对模型文件是否已同步到本地资源目录,或切换到已有模型的规格款。

diff --git a/furniture-3d-system/src/components/layout/AppShell.tsx b/furniture-3d-system/src/components/layout/AppShell.tsx index ac8c212f..80cf46e3 100644 --- a/furniture-3d-system/src/components/layout/AppShell.tsx +++ b/furniture-3d-system/src/components/layout/AppShell.tsx @@ -39,11 +39,14 @@ interface AppShellProps { export function AppShell({ children, user }: AppShellProps) { const pathname = usePathname() + const isAssetCenterRoute = + pathname === '/asset-center' || pathname.startsWith('/asset-center/') const isAssistedAssemblyRoute = pathname === '/assisted-assembly' || pathname.startsWith('/assisted-assembly/') const isIntakeRoute = pathname === '/intake' || pathname.startsWith('/intake/') const isWarehouseRoute = pathname === '/warehouse' || pathname.startsWith('/warehouse/') - const shouldLockMainViewport = isAssistedAssemblyRoute || isIntakeRoute || isWarehouseRoute + const shouldLockMainViewport = + isAssetCenterRoute || isAssistedAssemblyRoute || isIntakeRoute || isWarehouseRoute useEffect(() => { if (!shouldLockMainViewport || typeof document === 'undefined') { @@ -76,11 +79,13 @@ export function AppShell({ children, user }: AppShellProps) { return (
- - + + {!isAssetCenterRoute && }
+ + +
+

{APP_BRAND_NAME}

+

资产中心

+
+ + +
+ +
+ {WORKBENCH_NAV_ITEMS.map((item) => { + const Icon = item.icon + const active = item.href === '/' ? pathname === '/' : pathname.startsWith(item.href) + + return ( + + + {item.label} + + ) + })} +
+ +
+ + + {user && } +
+ + ) + } + return ( ) @@ -147,7 +150,7 @@ export function AppTopNav({ user, variant = 'default' }: AppTopNavProps) { - {user && } +
) diff --git a/furniture-3d-system/src/components/platform/AssetWorkbenchEmptyBackdrop.tsx b/furniture-3d-system/src/components/platform/AssetWorkbenchEmptyBackdrop.tsx new file mode 100644 index 00000000..4dbd9703 --- /dev/null +++ b/furniture-3d-system/src/components/platform/AssetWorkbenchEmptyBackdrop.tsx @@ -0,0 +1,137 @@ +import { cn } from '@/lib/utils' + +interface AssetWorkbenchEmptyBackdropProps { + className?: string +} + +const componentPlates = [ + 'left-[18%] top-[28%] h-[86px] w-[156px] -rotate-6', + 'right-[18%] top-[24%] h-[70px] w-[126px] rotate-3', + 'left-[30%] bottom-[20%] h-[78px] w-[138px] rotate-2', + 'right-[28%] bottom-[27%] h-[56px] w-[104px] -rotate-3', +] + +const hardwareNodes = [ + 'left-[23%] top-[34%]', + 'left-[39%] top-[42%]', + 'right-[24%] top-[31%]', + 'left-[34%] bottom-[27%]', + 'right-[33%] bottom-[36%]', + 'right-[20%] bottom-[25%]', +] + +const relationLines = [ + 'left-[28%] top-[39%] w-[18%] rotate-[13deg]', + 'left-[45%] top-[43%] w-[22%] -rotate-[11deg]', + 'left-[38%] bottom-[34%] w-[25%] rotate-[7deg]', +] + +export function AssetWorkbenchEmptyBackdrop({ className }: AssetWorkbenchEmptyBackdropProps) { + return ( +