From dc2c86275eba5c9600745e2b8c5dc7765485f860 Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Thu, 15 Oct 2026 11:35:34 +0800 Subject: [PATCH] feat(diy): add shared assets, uploaded styles and AI rendering --- .../migration.sql | 18 +++ .../migration.sql | 10 ++ .../migration.sql | 10 ++ furniture-3d-system/prisma/schema.prisma | 21 +++ .../RenderInspectorPromptTagPicker.tsx | 6 +- .../app/ai-render-workbench/promptBuilders.ts | 4 +- .../diy/ai-renders/optimize-prompt/route.ts | 19 +++ .../src/app/api/diy/ai-renders/route.ts | 19 +++ .../src/app/api/diy/catalog/assets/route.ts | 13 ++ .../src/app/api/diy/catalog/manage/route.ts | 31 +++++ .../diy/catalog/materials/[itemCode]/route.ts | 14 ++ .../app/api/diy/catalog/recycle-bin/route.ts | 7 + .../src/app/api/diy/catalog/route.ts | 2 +- .../src/app/api/diy/catalog/upload/route.ts | 12 ++ .../src/app/diy/DiyWorkbench.tsx | 67 ++++++---- .../diy/components/DiyAdvancedSettings.tsx | 12 ++ .../src/app/diy/components/DiyAiPanel.tsx | 39 ++++++ .../app/diy/components/DiyAiPromptForm.tsx | 54 ++++++++ .../src/app/diy/components/DiyAiResults.tsx | 46 +++++++ .../src/app/diy/components/DiyAssetUpload.tsx | 46 +++++++ .../src/app/diy/components/DiyBomDialog.tsx | 4 +- .../app/diy/components/DiyCabinetLibrary.tsx | 54 ++++++-- .../src/app/diy/components/DiyCabinetMesh.tsx | 57 +++++--- .../DiyCabinetThumbnailRenderer.tsx | 38 ++++++ .../src/app/diy/components/DiyCameraRig.tsx | 36 +++++ .../app/diy/components/DiyCaptureBridge.tsx | 53 ++++++++ .../diy/components/DiyCellConfiguration.tsx | 36 +++++ .../src/app/diy/components/DiyCellEditor.tsx | 15 +++ .../src/app/diy/components/DiyCellGuide.tsx | 21 +++ .../src/app/diy/components/DiyCellTarget.tsx | 31 +++++ .../src/app/diy/components/DiyColorForm.tsx | 30 +++++ .../app/diy/components/DiyColorOptions.tsx | 27 ++++ .../src/app/diy/components/DiyInspector.tsx | 31 +++-- .../app/diy/components/DiyInspectorTabs.tsx | 10 ++ .../diy/components/DiyManagementDialog.tsx | 84 ++++++++++++ .../diy/components/DiyManagementFields.tsx | 8 ++ .../app/diy/components/DiyMaterialSwatch.tsx | 15 +++ .../diy/components/DiyModelSetupPreview.tsx | 16 +++ .../src/app/diy/components/DiyPatternForm.tsx | 25 ++++ .../app/diy/components/DiyPatternLibrary.tsx | 47 +++++++ .../app/diy/components/DiyPreviewStage.tsx | 31 +++++ .../src/app/diy/components/DiyRecycleBin.tsx | 48 +++++++ .../src/app/diy/components/DiyRenderReady.tsx | 17 +++ .../src/app/diy/components/DiySeriesForm.tsx | 14 ++ .../diy/components/DiySharedColorLibrary.tsx | 56 ++++++++ .../components/DiyStyleAdvancedSettings.tsx | 35 +++++ .../app/diy/components/DiyStyleCellEditor.tsx | 37 ++++++ .../src/app/diy/components/DiyStyleForm.tsx | 70 ++++++++++ .../src/app/diy/components/DiyToolbar.tsx | 48 +++++++ .../app/diy/components/DiyUploadedModel.tsx | 75 +++++++++++ .../src/app/diy/components/DiyVariantForm.tsx | 57 ++++++++ .../app/diy/components/DiyVariantPreview.tsx | 22 ++++ .../src/app/diy/components/DiyViewport.tsx | 78 +++++------ .../src/app/diy/hooks/useDiyAiRender.ts | 124 ++++++++++++++++++ .../app/diy/hooks/useDiyCabinetThumbnails.ts | 22 ++++ .../src/app/diy/hooks/useDiyCellDetection.ts | 34 +++++ .../src/app/diy/hooks/useDiyWorkbench.ts | 82 +++++++++--- furniture-3d-system/src/app/diy/lib/ai-api.ts | 21 +++ .../src/app/diy/lib/ai-capture.ts | 6 + .../src/app/diy/lib/ai-prompts.ts | 40 ++++++ furniture-3d-system/src/app/diy/lib/api.ts | 22 +++- .../src/app/diy/lib/cabinet-previews.ts | 30 +++++ .../src/app/diy/lib/capture.ts | 6 + .../src/app/diy/lib/cell-detection-grid.ts | 60 +++++++++ .../src/app/diy/lib/cell-detection-section.ts | 63 +++++++++ .../src/app/diy/lib/cell-detection.ts | 43 ++++++ .../src/app/diy/lib/download.ts | 2 +- .../src/app/diy/lib/material-appearance.ts | 10 ++ .../src/app/diy/lib/model-buffers.ts | 31 +++++ furniture-3d-system/src/app/diy/lib/model.ts | 93 +++++++++++++ .../src/app/diy/lib/texture-coordinates.ts | 37 ++++++ furniture-3d-system/src/app/diy/page.tsx | 7 +- .../src/lib/diy/__tests__/ai-render.test.ts | 15 +++ .../lib/diy/__tests__/cell-detection.test.ts | 49 +++++++ .../lib/diy/__tests__/material-codes.test.ts | 19 +++ .../lib/diy/__tests__/multi-series.test.ts | 64 +++++++++ .../lib/diy/__tests__/shared-colors.test.ts | 34 +++++ .../diy/__tests__/texture-coordinates.test.ts | 29 ++++ .../diy/__tests__/workbench-state.test.tsx | 91 +++++++++++++ furniture-3d-system/src/lib/diy/ai-render.ts | 45 +++++++ .../src/lib/diy/asset-availability.ts | 7 + furniture-3d-system/src/lib/diy/bom.ts | 10 +- .../src/lib/diy/catalog-schema.ts | 36 +++-- furniture-3d-system/src/lib/diy/catalog.ts | 21 ++- .../src/lib/diy/cell-configuration.ts | 25 ++++ furniture-3d-system/src/lib/diy/layout.ts | 2 +- .../src/lib/diy/management-schema.ts | 47 +++++++ .../src/lib/diy/material-codes.ts | 15 +++ furniture-3d-system/src/lib/diy/materials.ts | 17 +++ furniture-3d-system/src/lib/diy/schema.ts | 15 ++- furniture-3d-system/src/lib/diy/types.ts | 24 +++- .../repositories/diy-ai-render.repository.ts | 24 ++++ .../diy-asset-lifecycle.repository.ts | 43 ++++++ .../diy-catalog-management.repository.ts | 89 +++++++++++++ .../repositories/diy-catalog.repository.ts | 13 +- .../repositories/diy-material.repository.ts | 22 ++++ .../diy-style-geometry.repository.ts | 32 +++++ .../__tests__/diy-ai-render.service.test.ts | 88 +++++++++++++ .../diy-asset-lifecycle.service.test.ts | 51 +++++++ .../diy-catalog-management.service.test.ts | 44 +++++++ .../diy-catalog-upload.service.test.ts | 29 ++++ .../__tests__/diy-catalog.service.test.ts | 26 +++- .../__tests__/diy-material.service.test.ts | 36 +++++ .../__tests__/diy-scheme.service.test.ts | 6 +- .../diy-style-geometry.service.test.ts | 75 +++++++++++ .../server/services/diy-ai-render.access.ts | 20 +++ .../server/services/diy-ai-render.create.ts | 74 +++++++++++ .../server/services/diy-ai-render.service.ts | 50 +++++++ .../server/services/diy-ai-render.storage.ts | 33 +++++ .../services/diy-asset-lifecycle.service.ts | 26 ++++ .../diy-catalog-management.service.ts | 36 +++++ .../services/diy-catalog-upload.service.ts | 25 ++++ .../server/services/diy-catalog.service.ts | 43 ++++-- .../server/services/diy-material.service.ts | 17 +++ .../src/server/services/diy-scheme.service.ts | 4 +- .../services/diy-style-geometry.service.ts | 30 +++++ 116 files changed, 3713 insertions(+), 197 deletions(-) create mode 100644 furniture-3d-system/prisma/migrations/20261014103000_diy_multi_series_management/migration.sql create mode 100644 furniture-3d-system/prisma/migrations/20261014120000_diy_uploaded_model_geometry/migration.sql create mode 100644 furniture-3d-system/prisma/migrations/20261015100000_diy_asset_soft_delete/migration.sql create mode 100644 furniture-3d-system/src/app/api/diy/ai-renders/optimize-prompt/route.ts create mode 100644 furniture-3d-system/src/app/api/diy/ai-renders/route.ts create mode 100644 furniture-3d-system/src/app/api/diy/catalog/assets/route.ts create mode 100644 furniture-3d-system/src/app/api/diy/catalog/manage/route.ts create mode 100644 furniture-3d-system/src/app/api/diy/catalog/materials/[itemCode]/route.ts create mode 100644 furniture-3d-system/src/app/api/diy/catalog/recycle-bin/route.ts create mode 100644 furniture-3d-system/src/app/api/diy/catalog/upload/route.ts create mode 100644 furniture-3d-system/src/app/diy/components/DiyAdvancedSettings.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyAiPanel.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyAiPromptForm.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyAiResults.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyAssetUpload.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCabinetThumbnailRenderer.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCameraRig.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCaptureBridge.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCellConfiguration.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCellEditor.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCellGuide.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyCellTarget.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyColorForm.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyColorOptions.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyInspectorTabs.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyManagementDialog.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyManagementFields.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyMaterialSwatch.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyModelSetupPreview.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyPatternForm.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyPatternLibrary.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyPreviewStage.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyRecycleBin.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyRenderReady.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiySeriesForm.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiySharedColorLibrary.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyStyleAdvancedSettings.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyStyleCellEditor.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyStyleForm.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyToolbar.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyUploadedModel.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyVariantForm.tsx create mode 100644 furniture-3d-system/src/app/diy/components/DiyVariantPreview.tsx create mode 100644 furniture-3d-system/src/app/diy/hooks/useDiyAiRender.ts create mode 100644 furniture-3d-system/src/app/diy/hooks/useDiyCabinetThumbnails.ts create mode 100644 furniture-3d-system/src/app/diy/hooks/useDiyCellDetection.ts create mode 100644 furniture-3d-system/src/app/diy/lib/ai-api.ts create mode 100644 furniture-3d-system/src/app/diy/lib/ai-capture.ts create mode 100644 furniture-3d-system/src/app/diy/lib/ai-prompts.ts create mode 100644 furniture-3d-system/src/app/diy/lib/cabinet-previews.ts create mode 100644 furniture-3d-system/src/app/diy/lib/capture.ts create mode 100644 furniture-3d-system/src/app/diy/lib/cell-detection-grid.ts create mode 100644 furniture-3d-system/src/app/diy/lib/cell-detection-section.ts create mode 100644 furniture-3d-system/src/app/diy/lib/cell-detection.ts create mode 100644 furniture-3d-system/src/app/diy/lib/material-appearance.ts create mode 100644 furniture-3d-system/src/app/diy/lib/model-buffers.ts create mode 100644 furniture-3d-system/src/app/diy/lib/model.ts create mode 100644 furniture-3d-system/src/app/diy/lib/texture-coordinates.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/ai-render.test.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/cell-detection.test.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/material-codes.test.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/multi-series.test.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/shared-colors.test.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/texture-coordinates.test.ts create mode 100644 furniture-3d-system/src/lib/diy/__tests__/workbench-state.test.tsx create mode 100644 furniture-3d-system/src/lib/diy/ai-render.ts create mode 100644 furniture-3d-system/src/lib/diy/asset-availability.ts create mode 100644 furniture-3d-system/src/lib/diy/cell-configuration.ts create mode 100644 furniture-3d-system/src/lib/diy/management-schema.ts create mode 100644 furniture-3d-system/src/lib/diy/material-codes.ts create mode 100644 furniture-3d-system/src/lib/diy/materials.ts create mode 100644 furniture-3d-system/src/server/repositories/diy-ai-render.repository.ts create mode 100644 furniture-3d-system/src/server/repositories/diy-asset-lifecycle.repository.ts create mode 100644 furniture-3d-system/src/server/repositories/diy-catalog-management.repository.ts create mode 100644 furniture-3d-system/src/server/repositories/diy-material.repository.ts create mode 100644 furniture-3d-system/src/server/repositories/diy-style-geometry.repository.ts create mode 100644 furniture-3d-system/src/server/services/__tests__/diy-ai-render.service.test.ts create mode 100644 furniture-3d-system/src/server/services/__tests__/diy-asset-lifecycle.service.test.ts create mode 100644 furniture-3d-system/src/server/services/__tests__/diy-catalog-management.service.test.ts create mode 100644 furniture-3d-system/src/server/services/__tests__/diy-catalog-upload.service.test.ts create mode 100644 furniture-3d-system/src/server/services/__tests__/diy-material.service.test.ts create mode 100644 furniture-3d-system/src/server/services/__tests__/diy-style-geometry.service.test.ts create mode 100644 furniture-3d-system/src/server/services/diy-ai-render.access.ts create mode 100644 furniture-3d-system/src/server/services/diy-ai-render.create.ts create mode 100644 furniture-3d-system/src/server/services/diy-ai-render.service.ts create mode 100644 furniture-3d-system/src/server/services/diy-ai-render.storage.ts create mode 100644 furniture-3d-system/src/server/services/diy-asset-lifecycle.service.ts create mode 100644 furniture-3d-system/src/server/services/diy-catalog-management.service.ts create mode 100644 furniture-3d-system/src/server/services/diy-catalog-upload.service.ts create mode 100644 furniture-3d-system/src/server/services/diy-material.service.ts create mode 100644 furniture-3d-system/src/server/services/diy-style-geometry.service.ts diff --git a/furniture-3d-system/prisma/migrations/20261014103000_diy_multi_series_management/migration.sql b/furniture-3d-system/prisma/migrations/20261014103000_diy_multi_series_management/migration.sql new file mode 100644 index 00000000..8fa5b874 --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20261014103000_diy_multi_series_management/migration.sql @@ -0,0 +1,18 @@ +ALTER TABLE "DiyCabinetStyle" + ADD COLUMN "modelFileId" TEXT, + ADD COLUMN "mainNodeNames" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + ADD COLUMN "shelfNodeNames" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], + ADD COLUMN "modelRotationY" DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE "DiyCabinetStyle" ADD CONSTRAINT "DiyCabinetStyle_modelFileId_fkey" + FOREIGN KEY ("modelFileId") REFERENCES "DiyAssetFile"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "DiyCabinetVariant" + ADD COLUMN "mainMaterialCode" TEXT, + ADD COLUMN "shelfMaterialCode" TEXT; +ALTER TABLE "DiyCabinetVariant" ADD CONSTRAINT "DiyCabinetVariant_mainMaterialCode_fkey" + FOREIGN KEY ("mainMaterialCode") REFERENCES "DiyMaterial"("itemCode") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "DiyCabinetVariant" ADD CONSTRAINT "DiyCabinetVariant_shelfMaterialCode_fkey" + FOREIGN KEY ("shelfMaterialCode") REFERENCES "DiyMaterial"("itemCode") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "DiyDoorPattern" + ADD COLUMN "width" DOUBLE PRECISION, + ADD COLUMN "height" DOUBLE PRECISION, + ADD COLUMN "thickness" DOUBLE PRECISION; diff --git a/furniture-3d-system/prisma/migrations/20261014120000_diy_uploaded_model_geometry/migration.sql b/furniture-3d-system/prisma/migrations/20261014120000_diy_uploaded_model_geometry/migration.sql new file mode 100644 index 00000000..99c0046f --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20261014120000_diy_uploaded_model_geometry/migration.sql @@ -0,0 +1,10 @@ +-- Uploaded models have a file reference rather than generated panel geometry. +-- A cabinet without door openings is a valid model-only cabinet. +ALTER TABLE "DiyCabinetStyle" + DROP CONSTRAINT "DiyCabinetStyle_cellCount_check", + ADD CONSTRAINT "DiyCabinetStyle_cellCount_check" + CHECK ("cellCount" >= 0 AND jsonb_typeof(cells) = 'array' AND jsonb_array_length(cells) = "cellCount"), + DROP CONSTRAINT "DiyCabinetStyle_panelCount_check", + ADD CONSTRAINT "DiyCabinetStyle_panelCount_check" + CHECK ("panelCount" >= 0 AND jsonb_typeof(panels) = 'array' AND jsonb_array_length(panels) = "panelCount" + AND ("panelCount" > 0 OR "modelFileId" IS NOT NULL)); diff --git a/furniture-3d-system/prisma/migrations/20261015100000_diy_asset_soft_delete/migration.sql b/furniture-3d-system/prisma/migrations/20261015100000_diy_asset_soft_delete/migration.sql new file mode 100644 index 00000000..c26c8cb8 --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20261015100000_diy_asset_soft_delete/migration.sql @@ -0,0 +1,10 @@ +ALTER TABLE "DiyMaterial" + ADD COLUMN "deletedAt" TIMESTAMP(3), + ADD COLUMN "deletedBy" TEXT; + +ALTER TABLE "DiyDoorPattern" + ADD COLUMN "deletedAt" TIMESTAMP(3), + ADD COLUMN "deletedBy" TEXT; + +CREATE INDEX "DiyMaterial_deletedAt_idx" ON "DiyMaterial"("deletedAt"); +CREATE INDEX "DiyDoorPattern_deletedAt_idx" ON "DiyDoorPattern"("deletedAt"); diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma index 095baa4e..d18b0569 100644 --- a/furniture-3d-system/prisma/schema.prisma +++ b/furniture-3d-system/prisma/schema.prisma @@ -2782,6 +2782,11 @@ model DiyCabinetStyle { cells Json sortOrder Int variants DiyCabinetVariant[] + modelFileId String? + model DiyAssetFile? @relation("DiyStyleModel", fields: [modelFileId], references: [id], onDelete: Restrict) + mainNodeNames String[] @default([]) + shelfNodeNames String[] @default([]) + modelRotationY Float @default(0) @@index([spu, sortOrder]) } @@ -2797,6 +2802,10 @@ model DiyCabinetVariant { sortOrder Int thumbnailFileId String thumbnail DiyAssetFile @relation(fields: [thumbnailFileId], references: [id], onDelete: Restrict) + mainMaterialCode String? + mainMaterial DiyMaterial? @relation("DiyVariantMainMaterial", fields: [mainMaterialCode], references: [itemCode], onDelete: Restrict) + shelfMaterialCode String? + shelfMaterial DiyMaterial? @relation("DiyVariantShelfMaterial", fields: [shelfMaterialCode], references: [itemCode], onDelete: Restrict) @@unique([styleCode, name]) } @@ -2812,7 +2821,13 @@ model DiyDoorPattern { sortOrder Int imageFileId String image DiyAssetFile @relation(fields: [imageFileId], references: [id], onDelete: Restrict) + width Float? + height Float? + thickness Float? + deletedAt DateTime? + deletedBy String? @@index([spu, sortOrder]) + @@index([deletedAt]) } model DiyMaterial { @@ -2828,7 +2843,12 @@ model DiyMaterial { sortOrder Int textureFileId String? texture DiyAssetFile? @relation(fields: [textureFileId], references: [id], onDelete: Restrict) + mainVariants DiyCabinetVariant[] @relation("DiyVariantMainMaterial") + shelfVariants DiyCabinetVariant[] @relation("DiyVariantShelfMaterial") + deletedAt DateTime? + deletedBy String? @@unique([spu, name]) + @@index([deletedAt]) } /// 文件索引;url 为平台 /api/storage/object,校验和用于幂等导入。 @@ -2847,6 +2867,7 @@ model DiyAssetFile { variants DiyCabinetVariant[] patterns DiyDoorPattern[] materials DiyMaterial[] + models DiyCabinetStyle[] @relation("DiyStyleModel") @@index([spu]) } diff --git a/furniture-3d-system/src/app/ai-render-workbench/RenderInspectorPromptTagPicker.tsx b/furniture-3d-system/src/app/ai-render-workbench/RenderInspectorPromptTagPicker.tsx index 6b27f333..44f33ab8 100644 --- a/furniture-3d-system/src/app/ai-render-workbench/RenderInspectorPromptTagPicker.tsx +++ b/furniture-3d-system/src/app/ai-render-workbench/RenderInspectorPromptTagPicker.tsx @@ -13,6 +13,7 @@ interface RenderInspectorPromptTagPickerProps { onImageCountChange: (value: number) => void onAspectRatioChange: (value: string) => void onRefreshRecords: () => void + showOutputControls?: boolean } interface AiTagOption { @@ -96,6 +97,7 @@ export function RenderInspectorPromptTagPicker(props: RenderInspectorPromptTagPi onImageCountChange, onAspectRatioChange, onRefreshRecords, + showOutputControls = true, } = props const [selectedTags, setSelectedTags] = useState>({}) @@ -151,7 +153,7 @@ export function RenderInspectorPromptTagPicker(props: RenderInspectorPromptTagPi return ( <>
- onImageCountChange(Number(event.target.value))} className="h-7 rounded-[5px] bg-white/70 px-2 text-[10px] font-bold text-[#2d3335] outline-none ring-1 ring-black/[0.04]" @@ -173,7 +175,7 @@ export function RenderInspectorPromptTagPicker(props: RenderInspectorPromptTagPi ))} -
+
} {AI_OPTIM_TAG_GROUPS.map((group) => { const selectedOption = selectedTags[group.group] const isHovered = hoveredTagGroup === group.group diff --git a/furniture-3d-system/src/app/ai-render-workbench/promptBuilders.ts b/furniture-3d-system/src/app/ai-render-workbench/promptBuilders.ts index 3df9849b..1fb29f0c 100644 --- a/furniture-3d-system/src/app/ai-render-workbench/promptBuilders.ts +++ b/furniture-3d-system/src/app/ai-render-workbench/promptBuilders.ts @@ -198,7 +198,7 @@ function isMaterialPartLocatorReferenceLabel(label: string): boolean { export function buildWhiteBackgroundPrompt(params: { spu: AiRenderSpu | null - sku: AiRenderSku + sku: Pick userPrompt: string aspectRatio: string cameraView?: AiRenderCameraViewSnapshot | null @@ -366,7 +366,7 @@ export function buildEightViewPrompt(params: { export function buildLockedScenePrompt(params: { spu: AiRenderSpu | null - sku: AiRenderSku + sku: Pick userPrompt: string aspectRatio: string }): string { diff --git a/furniture-3d-system/src/app/api/diy/ai-renders/optimize-prompt/route.ts b/furniture-3d-system/src/app/api/diy/ai-renders/optimize-prompt/route.ts new file mode 100644 index 00000000..dec5c3f4 --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/ai-renders/optimize-prompt/route.ts @@ -0,0 +1,19 @@ +import { NextRequest } from 'next/server' +import { z } from 'zod' +import { handleRequest, jsonSuccess, jsonError } from '@/server/core/response' +import { ErrorCode } from '@/server/core/errors' +import { diyAiRenderService } from '@/server/services/diy-ai-render.service' + +const schema = z.object({ prompt: z.string().trim().min(1).max(2000), mode: z.enum(['white-background','scene']), + referenceImageDataUrl: z.string().max(17 * 1024 * 1024) }) +export async function POST(request: NextRequest) { + return handleRequest(request, async ctx => { + const text = await request.text() + if (Buffer.byteLength(text) > 18 * 1024 * 1024) return jsonError(ErrorCode.FILE_TOO_LARGE, '参考图过大', 413, ctx.requestId) + let input: unknown + try { input = JSON.parse(text) } catch { return jsonError(ErrorCode.VALIDATION_FAILED, '优化参数格式无效', 400, ctx.requestId) } + const parsed = schema.safeParse(input) + if (!parsed.success) return jsonError(ErrorCode.VALIDATION_FAILED, parsed.error.issues[0]?.message || '优化参数无效', 400, ctx.requestId) + return jsonSuccess(await diyAiRenderService.optimize(parsed.data, ctx.user), 200, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/diy/ai-renders/route.ts b/furniture-3d-system/src/app/api/diy/ai-renders/route.ts new file mode 100644 index 00000000..34ce9b2c --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/ai-renders/route.ts @@ -0,0 +1,19 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess, jsonError } from '@/server/core/response' +import { ErrorCode } from '@/server/core/errors' +import { diyAiRenderService } from '@/server/services/diy-ai-render.service' + +export const maxDuration = 300 +export async function GET(request: NextRequest) { + return handleRequest(request, async ctx => jsonSuccess(await diyAiRenderService.list(ctx.user, + request.nextUrl.searchParams.get('compositionKey') || undefined, request.nextUrl.searchParams.get('schemeCode') || undefined), 200, ctx.requestId), { requireAuth: true }) +} +export async function POST(request: NextRequest) { + return handleRequest(request, async ctx => { + const text = await request.text() + if (Buffer.byteLength(text) > 35 * 1024 * 1024) return jsonError(ErrorCode.FILE_TOO_LARGE, '出图参考不能超过35MB', 413, ctx.requestId) + let input: unknown + try { input = JSON.parse(text) } catch { return jsonError(ErrorCode.VALIDATION_FAILED, '出图参数格式无效', 400, ctx.requestId) } + return jsonSuccess(await diyAiRenderService.create(input, ctx.user), 201, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/diy/catalog/assets/route.ts b/furniture-3d-system/src/app/api/diy/catalog/assets/route.ts new file mode 100644 index 00000000..c38507e6 --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/catalog/assets/route.ts @@ -0,0 +1,13 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess, jsonError } from '@/server/core/response' +import { ErrorCode } from '@/server/core/errors' +import { diyAssetLifecycleSchema } from '@/lib/diy/management-schema' +import { diyAssetLifecycleService } from '@/server/services/diy-asset-lifecycle.service' + +export async function POST(request: NextRequest) { + return handleRequest(request, async ctx => { + const input = diyAssetLifecycleSchema.safeParse(await request.json().catch(() => null)) + if (!input.success) return jsonError(ErrorCode.VALIDATION_FAILED, '素材信息不完整,请刷新后重试', 422, ctx.requestId) + return jsonSuccess(await diyAssetLifecycleService.change(input.data, ctx.user), 200, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/diy/catalog/manage/route.ts b/furniture-3d-system/src/app/api/diy/catalog/manage/route.ts new file mode 100644 index 00000000..abbc3118 --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/catalog/manage/route.ts @@ -0,0 +1,31 @@ +import { NextRequest } from 'next/server' +import { handleRequest,jsonSuccess,jsonError } from '@/server/core/response' +import { ErrorCode } from '@/server/core/errors' +import { diyManagementSchema,diyStyleCellsSchema,diyMaterialDeleteSchema } from '@/lib/diy/management-schema' +import { diyCatalogManagementService } from '@/server/services/diy-catalog-management.service' +import { diyStyleGeometryService } from '@/server/services/diy-style-geometry.service' +import { diyMaterialService } from '@/server/services/diy-material.service' + +export async function POST(request:NextRequest) { + return handleRequest(request,async ctx=>{ + const input=diyManagementSchema.safeParse(await request.json().catch(()=>null)) + if(!input.success)return jsonError(ErrorCode.VALIDATION_FAILED,input.error.issues[0]?.message||'素材信息不完整',422,ctx.requestId) + return jsonSuccess(await diyCatalogManagementService.create(input.data,ctx.user),201,ctx.requestId) + },{requireAuth:true}) +} + +export async function PATCH(request:NextRequest) { + return handleRequest(request,async ctx=>{ + const input=diyStyleCellsSchema.safeParse(await request.json().catch(()=>null)) + if(!input.success)return jsonError(ErrorCode.VALIDATION_FAILED,input.error.issues[0]?.message||'格口信息不完整',422,ctx.requestId) + return jsonSuccess(await diyStyleGeometryService.updateCells(input.data,ctx.user),200,ctx.requestId) + },{requireAuth:true}) +} + +export async function DELETE(request:NextRequest) { + return handleRequest(request,async ctx=>{ + const input=diyMaterialDeleteSchema.safeParse(await request.json().catch(()=>null)) + if(!input.success)return jsonError(ErrorCode.VALIDATION_FAILED,'颜色信息不完整,请刷新后重试',422,ctx.requestId) + return jsonSuccess(await diyMaterialService.remove(input.data,ctx.user),200,ctx.requestId) + },{requireAuth:true}) +} diff --git a/furniture-3d-system/src/app/api/diy/catalog/materials/[itemCode]/route.ts b/furniture-3d-system/src/app/api/diy/catalog/materials/[itemCode]/route.ts new file mode 100644 index 00000000..bd75f90e --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/catalog/materials/[itemCode]/route.ts @@ -0,0 +1,14 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess, jsonError } from '@/server/core/response' +import { ErrorCode } from '@/server/core/errors' +import { diyMaterialDeleteSchema } from '@/lib/diy/management-schema' +import { diyMaterialService } from '@/server/services/diy-material.service' + +export async function GET(request: NextRequest, { params }: { params: Promise<{ itemCode: string }> }) { + return handleRequest(request, async ctx => { + const { itemCode } = await params + const code = diyMaterialDeleteSchema.shape.itemCode.safeParse(itemCode) + if (!code.success) return jsonError(ErrorCode.VALIDATION_FAILED, '颜色编码无效', 422, ctx.requestId) + return jsonSuccess(await diyMaterialService.usage(code.data, ctx.user), 200, ctx.requestId) + }, { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/diy/catalog/recycle-bin/route.ts b/furniture-3d-system/src/app/api/diy/catalog/recycle-bin/route.ts new file mode 100644 index 00000000..e38e0351 --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/catalog/recycle-bin/route.ts @@ -0,0 +1,7 @@ +import { NextRequest } from 'next/server' +import { handleRequest, jsonSuccess } from '@/server/core/response' +import { diyAssetLifecycleService } from '@/server/services/diy-asset-lifecycle.service' + +export async function GET(request: NextRequest) { + return handleRequest(request, async ctx => jsonSuccess(await diyAssetLifecycleService.recycled(ctx.user), 200, ctx.requestId), { requireAuth: true }) +} diff --git a/furniture-3d-system/src/app/api/diy/catalog/route.ts b/furniture-3d-system/src/app/api/diy/catalog/route.ts index 97699b6f..c638825f 100644 --- a/furniture-3d-system/src/app/api/diy/catalog/route.ts +++ b/furniture-3d-system/src/app/api/diy/catalog/route.ts @@ -6,6 +6,6 @@ import { diySchemeService } from '@/server/services/diy-scheme.service' export async function GET(request: NextRequest) { return handleRequest(request, async ctx => { await diySchemeService.assertView(ctx.user) - return jsonSuccess(await diyCatalogService.getCatalog(), 200, ctx.requestId) + return jsonSuccess(await diyCatalogService.getWorkbenchCatalog(), 200, ctx.requestId) }, { requireAuth: true }) } diff --git a/furniture-3d-system/src/app/api/diy/catalog/upload/route.ts b/furniture-3d-system/src/app/api/diy/catalog/upload/route.ts new file mode 100644 index 00000000..39515ac1 --- /dev/null +++ b/furniture-3d-system/src/app/api/diy/catalog/upload/route.ts @@ -0,0 +1,12 @@ +import { NextRequest } from 'next/server' +import { handleRequest,jsonSuccess,jsonError } from '@/server/core/response' +import { ErrorCode } from '@/server/core/errors' +import { diyCatalogUploadService } from '@/server/services/diy-catalog-upload.service' + +export async function POST(request:NextRequest) { + return handleRequest(request,async ctx=>{ + const form=await request.formData(),file=form.get('file'),kind=form.get('kind') + if(!(file instanceof File)||(kind!=='image'&&kind!=='model'))return jsonError(ErrorCode.VALIDATION_FAILED,'请选择文件和素材类型',422,ctx.requestId) + return jsonSuccess(await diyCatalogUploadService.upload(file,String(form.get('spu')||''),kind,ctx.user),201,ctx.requestId) + },{requireAuth:true}) +} diff --git a/furniture-3d-system/src/app/diy/DiyWorkbench.tsx b/furniture-3d-system/src/app/diy/DiyWorkbench.tsx index f992a359..e06d4e46 100644 --- a/furniture-3d-system/src/app/diy/DiyWorkbench.tsx +++ b/furniture-3d-system/src/app/diy/DiyWorkbench.tsx @@ -2,10 +2,10 @@ import { useRef, useState } from 'react' import dynamic from 'next/dynamic' -import { History, Save, FileSpreadsheet, Camera, Plus, Move, PanelTop } from 'lucide-react' +import { Move, PanelTop } from 'lucide-react' import Button from '@/components/ui/Button' import Modal from '@/components/ui/Modal' -import type { DiyAccess, DiyCatalogDTO } from '@/lib/diy/types' +import type { DiyAccess, DiyCatalogDTO, CabinetCell } from '@/lib/diy/types' import { useDiyWorkbench } from './hooks/useDiyWorkbench' import { saveDiyScheme } from './lib/api' import { downloadDiyFile } from './lib/download' @@ -13,15 +13,34 @@ import DiyCabinetLibrary from './components/DiyCabinetLibrary' import DiyInspector from './components/DiyInspector' import DiyHistoryDialog from './components/DiyHistoryDialog' import DiyBomDialog from './components/DiyBomDialog' +import DiyManagementDialog from './components/DiyManagementDialog' +import DiyAiPanel from './components/DiyAiPanel' +import DiyInspectorTabs from './components/DiyInspectorTabs' +import DiyToolbar from './components/DiyToolbar' +import { useDiyAiRender } from './hooks/useDiyAiRender' +import type { DiyCaptureHandle } from './lib/ai-capture' +import type { DiyAiRenderAccess } from '@/lib/diy/ai-render' const DiyViewport=dynamic(()=>import('./components/DiyViewport'),{ssr:false,loading:()=>
正在加载 3D 工作台…
}) -export default function DiyWorkbench({ access, catalog }: {access:DiyAccess;catalog:DiyCatalogDTO}) { +export default function DiyWorkbench({ access, catalog,canDeleteMaterials=false, aiAccess }: {access:DiyAccess;catalog:DiyCatalogDTO;canDeleteMaterials?:boolean;aiAccess:DiyAiRenderAccess}) { const controller=useDiyWorkbench(catalog),stage=useRef(null) + const captureRef=useRef(null),[capturing,setCapturing]=useState(false) + const ai=useDiyAiRender(controller,captureRef,aiAccess) const [historyOpen,setHistoryOpen]=useState(false),[bomOpen,setBomOpen]=useState(false),[saveOpen,setSaveOpen]=useState(false) const [confirmAction,setConfirmAction]=useState<'new'|'history'|null>(null) - const [name,setName]=useState('SJ0380 组合方案'),[saving,setSaving]=useState(false) - const [mobilePanel,setMobilePanel]=useState<'library'|'inspector'|null>(null) - const disabled=!access.canConfigure||saving + const [name,setName]=useState('组合方案'),[saving,setSaving]=useState(false),[managementOpen,setManagementOpen]=useState(false) + const [mobilePanel,setMobilePanel]=useState<'library'|'inspector'|'ai'|null>(null) + const [editingStyleCode,setEditingStyleCode]=useState('') + const [newDoor,setNewDoor]=useState<({spu:string}&CabinetCell['door'])|undefined>() + const [newVariant,setNewVariant]=useState<{styleCode:string;materialCode:string}|undefined>() + const editCells=(code:string)=>{setEditingStyleCode(code);setNewDoor(undefined);setNewVariant(undefined);setMobilePanel(null);setManagementOpen(true)} + const addPattern=(door:CabinetCell['door'])=>{setEditingStyleCode('');setNewVariant(undefined);setNewDoor({...door,spu:controller.selected?.spu||controller.seriesSpu});setMobilePanel(null);setManagementOpen(true)} + const addColor=(styleCode:string,materialCode:string)=>{setEditingStyleCode('');setNewDoor(undefined);setNewVariant({styleCode,materialCode});setMobilePanel(null);setManagementOpen(true)} + const disabled=!access.canConfigure||saving||capturing + const switchRightPanel=(showAi:boolean)=>{ + if(showAi){if(!ai.open)void ai.openPanel();if(window.innerWidth<1024)setMobilePanel('ai')} + else ai.setOpen(false) + } const save=async()=>{ if(saving)return;setSaving(true) try{const record=await saveDiyScheme(controller.saveSnapshot(),name,controller.scheme?.code);controller.setSaved(record);setSaveOpen(false)} @@ -36,25 +55,19 @@ export default function DiyWorkbench({ access, catalog }: {access:DiyAccess;cata try{const canvas=stage.current?.querySelector('canvas');if(!canvas)throw new Error('3D画面还未就绪') controller.setFeedback({tone:'success',text:'正在生成预览截图…'}) canvas.toBlob(blob=>{if(!blob){controller.setFeedback({tone:'error',text:'截图生成失败'});return} - downloadDiyFile(blob,'DIY-SJ0380-preview.png');controller.setFeedback({tone:'success',text:'预览截图已开始下载'})},'image/png') + downloadDiyFile(blob,'DIY-preview.png');controller.setFeedback({tone:'success',text:'预览截图已开始下载'})},'image/png') }catch(error){controller.setFeedback({tone:'error',text:error instanceof Error?error.message:'截图失败'})} } return
-
-

DIY

- {controller.scheme?.name||'SJ0380 组合柜'}{controller.dirty?' · 未保存':''} -
- - - - - -
-
+ requestAction('history')} onBom={()=>setBomOpen(true)} onCapture={capture} onNew={()=>requestAction('new')} + onManage={()=>{setEditingStyleCode('');setNewDoor(undefined);setNewVariant(undefined);setManagementOpen(true)}} + onSave={()=>{setName(controller.scheme?.name||'组合方案');setSaveOpen(true)}}/>
- -
- + +
+
{(['layout','doors'] as const).map(mode=>
{controller.feedback&&
{controller.feedback.text}
}
- +
+ {aiAccess.canView&&}
setHistoryOpen(false)} controller={controller}/> setBomOpen(false)} controller={controller} canDownload={access.canDownload}/> + {access.canConfigure&&managementOpen&&setManagementOpen(false)} controller={controller} initialStyleCode={editingStyleCode} initialDoor={newDoor} initialVariant={newVariant} canDeleteMaterials={canDeleteMaterials}/>} {if(!saving)setSaveOpen(false)}} title="保存 DIY 方案" size="small" footer={}> @@ -87,8 +106,8 @@ export default function DiyWorkbench({ access, catalog }: {access:DiyAccess;cata }>

继续操作会放弃当前未保存的配置。

- setMobilePanel(null)} title={mobilePanel==='library'?'柜体款式':'配色与柜门'} size="medium"> -
{mobilePanel==='library'?:}
+ {if(mobilePanel==='ai')ai.setOpen(false);setMobilePanel(null)}} title={mobilePanel==='library'?'柜体款式':mobilePanel==='ai'?'AI 出图':'配色与柜门'} size="medium"> +
{mobilePanel==='library'?:mobilePanel==='ai'?:}
} diff --git a/furniture-3d-system/src/app/diy/components/DiyAdvancedSettings.tsx b/furniture-3d-system/src/app/diy/components/DiyAdvancedSettings.tsx new file mode 100644 index 00000000..85552e8e --- /dev/null +++ b/furniture-3d-system/src/app/diy/components/DiyAdvancedSettings.tsx @@ -0,0 +1,12 @@ +'use client' + +import type { ReactNode } from 'react' + +export default function DiyAdvancedSettings({children,label='高级设置',onOpenChange}: { + children:ReactNode;label?:string;onOpenChange?:(open:boolean)=>void +}) { + return
onOpenChange?.(event.currentTarget.open)}> + {label} +
{children}
+
+} diff --git a/furniture-3d-system/src/app/diy/components/DiyAiPanel.tsx b/furniture-3d-system/src/app/diy/components/DiyAiPanel.tsx new file mode 100644 index 00000000..f2138be9 --- /dev/null +++ b/furniture-3d-system/src/app/diy/components/DiyAiPanel.tsx @@ -0,0 +1,39 @@ +'use client' + +import { ImagePlus, Loader2, RefreshCw, Wand2 } from 'lucide-react' +import Button from '@/components/ui/Button' +import type { DiyAiRenderController } from '../hooks/useDiyAiRender' +import DiyAiPromptForm from './DiyAiPromptForm' +import DiyAiResults from './DiyAiResults' + +export default function DiyAiPanel({ ai, name, itemCount, doorCount }: { ai:DiyAiRenderController; name:string; itemCount:number; doorCount:number }) { + const busy = ai.generating || ai.optimizing || Boolean(ai.stage) + const records = ai.records.filter(record => record.mode === ai.mode) + return
+
+
+ {name}{itemCount} 柜体 · {doorCount} 柜门 +
+ 0}/> + {ai.error &&

{ai.error}

} + {ai.notice &&

{ai.notice}

} + {ai.stage &&
+ {ai.stage}
} +
+

出图记录

+ + +
+ +
+
+
+ + +
+
+} diff --git a/furniture-3d-system/src/app/diy/components/DiyAiPromptForm.tsx b/furniture-3d-system/src/app/diy/components/DiyAiPromptForm.tsx new file mode 100644 index 00000000..6a1198ce --- /dev/null +++ b/furniture-3d-system/src/app/diy/components/DiyAiPromptForm.tsx @@ -0,0 +1,54 @@ +'use client' + +import { RefreshCw } from 'lucide-react' +import Button from '@/components/ui/Button' +import { RenderInspectorPromptTagPicker } from '@/app/ai-render-workbench/RenderInspectorPromptTagPicker' +import type { DiyAiRenderInput } from '@/lib/diy/ai-render' +import type { DiyAiRenderController } from '../hooks/useDiyAiRender' + +const field = 'w-full rounded-[4px] border border-black/[0.04] bg-white px-2.5 py-1.5 text-[11px] leading-[18px] text-[#2d3335] outline-none focus:border-[#47646b]/30 disabled:opacity-50' +const box = 'rounded-[6px] border border-black/[0.04] bg-white' +export default function DiyAiPromptForm({ ai, hasItems }: { ai:DiyAiRenderController; hasItems:boolean }) { + const busy = ai.generating || ai.optimizing || Boolean(ai.stage) + return
+
+ {(['white-background','scene'] as const).map(mode => )} +
+
+ 当前视角参考图 +
+ {ai.preview ?
+ {/* eslint-disable-next-line @next/next/no-img-element */} + 当前方案生图参考 +
{ai.preview.snapshot.items.length} 柜体 · {Object.keys(ai.preview.snapshot.doors).length} 柜门 · 生成时更新
+
:

{hasItems?'正在准备参考图…':'请先添加柜体'}

} + +
+
+
+ 生图提示词 +
+