diff --git a/docs/前端UI改动清单.md b/docs/前端UI改动清单.md
index dedfaf9d..2910046f 100644
--- a/docs/前端UI改动清单.md
+++ b/docs/前端UI改动清单.md
@@ -12,9 +12,3 @@
---
## 当前未完成项
-
-### 待前端修正 (前端 UI Agent 负责)
-
-- [ ] **包材资料库页面视觉联调**
- 归属:`前端负责`
- 说明:确认该页面视觉是否已统一到主线风格。
diff --git a/furniture-3d-system/src/app/api/draft-groups/route.ts b/furniture-3d-system/src/app/api/draft-groups/route.ts
index 1f339027..33249ba0 100644
--- a/furniture-3d-system/src/app/api/draft-groups/route.ts
+++ b/furniture-3d-system/src/app/api/draft-groups/route.ts
@@ -2,6 +2,7 @@ import { NextRequest } from 'next/server'
import { AppError, ErrorCode } from '@/server/core/errors'
import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response'
import { draftGroupService } from '@/server/services'
+import type { CreateDraftGroupFromAutoCombinationInput } from '@/server/services'
import { createDraftGroupSchema } from '../platform/schemas'
export async function GET(request: NextRequest) {
@@ -38,7 +39,10 @@ export async function POST(request: NextRequest) {
}
try {
- const result = await draftGroupService.createFromAutoCombination(parsed.data, ctx.user)
+ const result = await draftGroupService.createFromAutoCombination(
+ parsed.data as unknown as CreateDraftGroupFromAutoCombinationInput,
+ ctx.user,
+ )
return jsonSuccess({ result }, 201, ctx.requestId)
} catch (error) {
if (error instanceof AppError) {
diff --git a/furniture-3d-system/src/app/api/platform/schemas.ts b/furniture-3d-system/src/app/api/platform/schemas.ts
index ef9f521f..f593114f 100644
--- a/furniture-3d-system/src/app/api/platform/schemas.ts
+++ b/furniture-3d-system/src/app/api/platform/schemas.ts
@@ -237,6 +237,8 @@ export const createDraftGroupSchema = z.object({
z.object({
key: z.string().trim().min(1),
defaultName: z.string().trim().min(1),
+ candidateSnapshot: z.record(z.string(), z.unknown()).optional().nullable(),
+ assemblyWorkspace: z.record(z.string(), z.unknown()).optional().nullable(),
familySelection: z.object({
desktopFamilyId: z.string().trim().min(1),
legGroupFamilyId: z.string().trim().min(1).optional().nullable(),
diff --git a/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx b/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx
index e18fb7e0..07053c47 100644
--- a/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx
+++ b/furniture-3d-system/src/app/asset-center/material-library/MaterialLibraryClient.tsx
@@ -177,6 +177,10 @@ function dictionaryLabel(type: SubstrateDictionaryType) {
}
}
+function isLegacySeedOptionId(id: string) {
+ return id.startsWith('seed_substrate_')
+}
+
export default function MaterialLibraryClient({
initialSubstrates,
initialFinishes,
@@ -255,6 +259,33 @@ export default function MaterialLibraryClient({
})
}, [substrateDictionaries, substrateForm.environmentGradeId, substrateForm.systemNameOptionId, substrateForm.thicknessSpecId])
+ const usedDictionaryIds = useMemo(() => {
+ return {
+ CATEGORY: new Set(substrates.map((item) => item.categoryId).filter(Boolean)),
+ ENVIRONMENT_GRADE: new Set(substrates.map((item) => item.environmentGradeId).filter(Boolean)),
+ THICKNESS_SPEC: new Set(substrates.map((item) => item.thicknessSpecId).filter(Boolean)),
+ SURFACE_OPTION: new Set(substrates.map((item) => item.surfaceOptionId).filter(Boolean)),
+ SYSTEM_NAME_OPTION: new Set(substrates.map((item) => item.systemNameOptionId).filter(Boolean)),
+ } as Record>
+ }, [substrates])
+
+ const visibleDictionaryOptions = useMemo(() => {
+ const filterOptions = (type: SubstrateDictionaryType, items: SubstrateDictionaryOptionDTO[]) =>
+ items.filter((item) => !isLegacySeedOptionId(item.id) || usedDictionaryIds[type].has(item.id))
+ return {
+ categories: filterOptions('CATEGORY', substrateDictionaries.categories),
+ environmentGrades: filterOptions('ENVIRONMENT_GRADE', substrateDictionaries.environmentGrades),
+ thicknessSpecs: filterOptions('THICKNESS_SPEC', substrateDictionaries.thicknessSpecs),
+ surfaceOptions: filterOptions('SURFACE_OPTION', substrateDictionaries.surfaceOptions),
+ systemNameOptions: filterOptions('SYSTEM_NAME_OPTION', substrateDictionaries.systemNameOptions),
+ }
+ }, [substrateDictionaries, usedDictionaryIds])
+
+ const activeDictionaryOptions = useMemo(
+ () => dictionaryOptionsByType(substrateDictionaries, activeDictionaryType),
+ [activeDictionaryType, substrateDictionaries],
+ )
+
const filteredFinishes = useMemo(() => {
const keyword = searchText.trim().toLowerCase()
return finishes.filter((item) => {
@@ -270,7 +301,16 @@ export default function MaterialLibraryClient({
function openCreateSubstrateModal() {
setSubstrateModalMode('create')
setEditingSubstrateId(null)
- setSubstrateForm(emptySubstrateForm(substrateDictionaries))
+ setSubstrateForm({
+ code: '',
+ description: '',
+ categoryId: visibleDictionaryOptions.categories[0]?.id ?? '',
+ environmentGradeId: visibleDictionaryOptions.environmentGrades[0]?.id ?? '',
+ thicknessSpecId: visibleDictionaryOptions.thicknessSpecs[0]?.id ?? '',
+ surfaceOptionId: visibleDictionaryOptions.surfaceOptions[0]?.id ?? '',
+ systemNameOptionId: visibleDictionaryOptions.systemNameOptions[0]?.id ?? '',
+ suggestedPrice: '',
+ })
setSubstrateModalOpen(true)
}
@@ -366,6 +406,12 @@ export default function MaterialLibraryClient({
setDictionaryModalOpen(true)
}
+ function beginCreateDictionaryOption() {
+ setDictionaryModalMode('create')
+ setEditingDictionaryId(null)
+ setDictionaryForm(emptyDictionaryForm())
+ }
+
async function handleSaveDictionary() {
if (!dictionaryForm.name.trim()) {
toast.error(`请填写${dictionaryLabel(activeDictionaryType)}名称`)
@@ -704,9 +750,9 @@ export default function MaterialLibraryClient({
{activeTab === 'substrate' ? (
-
+
{(['CATEGORY', 'ENVIRONMENT_GRADE', 'THICKNESS_SPEC', 'SURFACE_OPTION', 'SYSTEM_NAME_OPTION'] as SubstrateDictionaryType[]).map((type) => (
-
-
setSubstrateForm((current) => ({ ...current, description: event.target.value }))} placeholder="可选" />
-
setDictionaryModalOpen(false)} title={`${dictionaryModalMode === 'create' ? '新增' : '编辑'}${dictionaryLabel(activeDictionaryType)}`} size="small" footer={ setDictionaryModalOpen(false)}>取消{dictionaryModalMode === 'create' ? '新增' : '保存'}
}>
+ setDictionaryModalOpen(false)} title={`管理${dictionaryLabel(activeDictionaryType)}`} size="medium" footer={ setDictionaryModalOpen(false)}>取消{dictionaryModalMode === 'create' ? '新增' : '保存修改'}
}>
+
+
在这里可以查看已有选项,并直接新增/编辑/删除。
+
+
+ 新增
+
+
+
+ {activeDictionaryOptions.length === 0 ? (
+
暂无选项
+ ) : activeDictionaryOptions.map((item) => (
+
+
+
+ {item.name}
+ {isLegacySeedOptionId(item.id) ? '(历史默认)' : ''}
+
+
+ 排序 {item.sortOrder}
+ {item.type === 'THICKNESS_SPEC' && item.thicknessMm != null ? ` · ${item.thicknessMm}mm` : ''}
+
+
+
openDictionaryModal(activeDictionaryType, item)} className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary" title="编辑">
+
+
+
setDeleteTarget({ type: 'dictionary', id: item.id, name: item.name, dictionaryType: activeDictionaryType })} className="rounded-full p-1.5 text-apple-label-secondary hover:bg-apple-bg-secondary hover:text-apple-red" title="删除">
+
+
+
+ ))}
+
setDictionaryForm((current) => ({ ...current, name: event.target.value }))} />
setDictionaryForm((current) => ({ ...current, sortOrder: event.target.value }))} />
{activeDictionaryType === 'THICKNESS_SPEC' ?
setDictionaryForm((current) => ({ ...current, thicknessMm: event.target.value }))} /> : null}
diff --git a/furniture-3d-system/src/app/asset-center/packaging-materials/PackagingMaterialsClient.tsx b/furniture-3d-system/src/app/asset-center/packaging-materials/PackagingMaterialsClient.tsx
index 96f4e73f..f98428df 100644
--- a/furniture-3d-system/src/app/asset-center/packaging-materials/PackagingMaterialsClient.tsx
+++ b/furniture-3d-system/src/app/asset-center/packaging-materials/PackagingMaterialsClient.tsx
@@ -1748,24 +1748,21 @@ export default function PackagingMaterialsClient({
startEditVariant(variant)}
+ className="rounded-apple-md p-1.5 text-apple-label-primary/60 transition hover:bg-apple-bg-secondary hover:text-apple-label-primary bg-apple-bg-secondary/40 shadow-apple-sm border border-apple-separator/10" onClick={() => startEditVariant(variant)}
>
openCreateDrawer(variant)}
+ className="rounded-apple-md p-1.5 text-apple-label-primary/60 transition hover:bg-apple-bg-secondary hover:text-apple-label-primary bg-apple-bg-secondary/40 shadow-apple-sm border border-apple-separator/10" onClick={() => openCreateDrawer(variant)}
>
+ className="rounded-apple-md p-1.5 text-apple-label-primary/60 transition hover:bg-apple-red/10 hover:text-apple-red bg-apple-bg-secondary/40 shadow-apple-sm border border-apple-separator/10" onClick={() =>
setDeleteTarget({
type: 'variant',
id: variant.id,
diff --git a/furniture-3d-system/src/app/auto-combinations/AutoCombinationsClient.tsx b/furniture-3d-system/src/app/auto-combinations/AutoCombinationsClient.tsx
index 3b704eb5..f9c6caff 100644
--- a/furniture-3d-system/src/app/auto-combinations/AutoCombinationsClient.tsx
+++ b/furniture-3d-system/src/app/auto-combinations/AutoCombinationsClient.tsx
@@ -16,6 +16,7 @@ import Button from '@/components/ui/Button'
import Checkbox from '@/components/ui/Checkbox'
import Input, { Textarea } from '@/components/ui/Input'
import { AccordionSection } from '@/components/platform/AccordionSection'
+import { AssemblyWorkbench } from '@/components/platform/AssemblyWorkbench'
import { ModelPreviewPanel } from '@/components/platform/ModelPreviewPanel'
import { PlatformSplitLayout } from '@/components/platform/PlatformSplitLayout'
import type { PlatformMetricItem } from '@/components/platform/types'
@@ -24,6 +25,7 @@ import {
generateAutoCombinations,
} from '@/lib/open-product/platform-api'
import { cn } from '@/lib/utils'
+import type { AssemblyWorkspaceSnapshot } from '@/lib/open-product/assembly-workspace'
import type { AssetFamilyDTO, SeriesTemplateDTO } from '@/server/services'
import type { CombinationCandidateDTO } from '@/server/services'
@@ -82,6 +84,9 @@ export default function AutoCombinationsClient({
} | null>(null)
const [candidatesExpanded, setCandidatesExpanded] = useState(false)
const [previewCandidateKey, setPreviewCandidateKey] = useState(null)
+ const [candidateWorkspaces, setCandidateWorkspaces] = useState<
+ Record
+ >({})
const selectedTemplate = templates.find((t) => t.id === templateId)
const selectedTemplateCode = selectedTemplate?.code ?? null
@@ -137,8 +142,17 @@ export default function AutoCombinationsClient({
startTransition(() => {
setCandidates(result.candidates)
+ setCandidateWorkspaces(
+ Object.fromEntries(
+ result.candidates.map((candidate) => [
+ candidate.key,
+ candidate.assemblyWorkspace,
+ ]),
+ ),
+ )
setSelectedCandidateKeys([])
setCandidatesExpanded(true)
+ setPreviewCandidateKey(result.candidates[0]?.key ?? null)
if (!draftGroupName) {
setDraftGroupName(
`${templates.find((template) => template.id === templateId)?.seriesName || '新系列'}-草稿组`,
@@ -170,6 +184,9 @@ export default function AutoCombinationsClient({
candidates: selectedCandidates.map((candidate) => ({
key: candidate.key,
defaultName: candidate.defaultName,
+ candidateSnapshot: candidate.candidateSnapshot,
+ assemblyWorkspace:
+ candidateWorkspaces[candidate.key] ?? candidate.assemblyWorkspace,
familySelection: candidate.familySelection,
})),
})
@@ -191,54 +208,37 @@ export default function AutoCombinationsClient({
...connectorFamilies.filter((f) => connectorIds.includes(f.id)),
]
const previewCandidate = candidates.find((c) => c.key === previewCandidateKey) ?? null
+ const previewWorkspace =
+ (previewCandidateKey ? candidateWorkspaces[previewCandidateKey] : null) ??
+ previewCandidate?.assemblyWorkspace ??
+ null
const hasAnySelection = templateId || allSelectedFamilies.length > 0 || candidates.length > 0
const leftContent = (
{previewCandidate ? (
- <>
- {/* 选中候选的 3D 预览 */}
+ previewWorkspace ? (
+
+ setCandidateWorkspaces((current) => ({
+ ...current,
+ [previewCandidate.key]: next,
+ }))
+ }
+ />
+ ) : (
-
- {/* 组合关系展示 */}
- {previewCandidate.combinationItems.length > 0 && (
-
-
组合关系
-
- {previewCandidate.combinationItems.map((item, i) => (
-
- {item.label}:{item.value}{item.quantity > 1 ? ` ×${item.quantity}` : ''}
-
- ))}
-
-
- )}
-
- {/* 候选状态 */}
-
-
- {previewCandidate.statusTag}
-
- {previewCandidate.assemblyWarnings.length > 0 && (
-
- {previewCandidate.assemblyWarnings.length} 条警告
-
- )}
-
- >
+ )
) : candidates.length > 0 ? (
/* 有候选但没选中的引导 */
@@ -246,10 +246,10 @@ export default function AutoCombinationsClient({
- 点击右侧候选卡片预览 3D 组合
+ 点击右侧候选卡片进入装配工作台
- 已生成 {candidates.length} 条候选,点击任意一条即可在此处查看 3D 拼装效果和组合关系。
+ 已生成 {candidates.length} 条候选,点击任意一条即可在此处完成待装区、分子装配区和主装配区的装配确认。
) : !hasAnySelection ? (
@@ -260,7 +260,7 @@ export default function AutoCombinationsClient({
从右侧面板开始配置
- 选择系列模板并勾选资产族后,生成候选并点击即可预览 3D 组合。
+ 选择系列模板并勾选资产族后,生成候选并点击即可进入辅助式手动装配。
) : (
@@ -273,7 +273,7 @@ export default function AutoCombinationsClient({
配置就绪,点击右侧「生成候选列表」
- 已选 {allSelectedFamilies.length} 个资产族,生成后可逐条预览 3D 组合效果。
+ 已选 {allSelectedFamilies.length} 个资产族,生成后可逐条进入装配工作台确认最终结构。
)}
@@ -479,7 +479,7 @@ export default function AutoCombinationsClient({
{/* 候选结果 */}
}
aside={
candidates.length > 0 ? (
@@ -494,6 +494,7 @@ export default function AutoCombinationsClient({
{candidates.length === 0 ? (
还没有候选结果。先选好模板和资产族,生成后点击候选卡片即可预览 3D 拼装效果。
+
) : (
@@ -550,6 +551,29 @@ export default function AutoCombinationsClient({
))}
)}
+
+
+ 工作台
+ {(candidateWorkspaces[candidate.key] ?? candidate.assemblyWorkspace).validation.level ===
+ 'PASSED'
+ ? '已成立'
+ : (candidateWorkspaces[candidate.key] ?? candidate.assemblyWorkspace).validation.level ===
+ 'WARNING'
+ ? '有警告'
+ : '未成立'}
+
+
))}
diff --git a/furniture-3d-system/src/app/home/components/HomeModelsSection.tsx b/furniture-3d-system/src/app/home/components/HomeModelsSection.tsx
index 83d2cd5d..c1dfc78d 100644
--- a/furniture-3d-system/src/app/home/components/HomeModelsSection.tsx
+++ b/furniture-3d-system/src/app/home/components/HomeModelsSection.tsx
@@ -91,7 +91,7 @@ export function HomeModelsSection({