diff --git a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts new file mode 100644 index 00000000..f5dcca7b --- /dev/null +++ b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/contracts/route.ts @@ -0,0 +1,121 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AppError, ErrorCode } from "@/server/core/errors"; +import { handleRequest, jsonError } from "@/server/core/response"; +import { intakeNonIntegratedProductService } from "@/server/services/intake-non-integrated-product.service"; +import { + createIntakeProductFactoryContractDocx, + normalizeProductFactoryContractBatch, + normalizeProductFactoryContractType, +} from "@/server/services/intake-contract-docx.service"; +import { isIntakeNonIntegratedSupplyChainUser } from "@/lib/non-integrated-permissions"; +import type { AuthUser } from "@/server/core/types"; + +export const runtime = "nodejs"; + +type RouteContext = { + params: Promise<{ id: string }>; +}; + +function canDownloadProductFactoryContract(user: AuthUser) { + if (user.role === "ADMIN" || user.role === "SUPER_ADMIN") return true; + + return isIntakeNonIntegratedSupplyChainUser({ + id: user.id, + name: user.name, + role: user.role, + businessRoles: user.businessRoles, + department: user.department, + departmentPaths: user.departmentPaths, + }); +} + +function buildContentDisposition(fileName: string) { + const fallbackName = fileName.replace(/[^\x20-\x7E]+/g, "_"); + return `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodeURIComponent( + fileName, + )}`; +} + +export async function GET(request: NextRequest, context: RouteContext) { + return handleRequest( + request, + async (ctx) => { + if (!canDownloadProductFactoryContract(ctx.user)) { + return jsonError( + ErrorCode.AUTH_PERMISSION_DENIED, + "当前账号没有下载产品定厂合同的权限", + 403, + ctx.requestId, + ); + } + + const { id } = await context.params; + const productId = decodeURIComponent(id); + const searchParams = request.nextUrl.searchParams; + const contractType = normalizeProductFactoryContractType( + searchParams.get("type"), + ); + const batch = normalizeProductFactoryContractBatch( + searchParams.get("batch"), + ); + + if (!contractType) { + return jsonError( + ErrorCode.VALIDATION_INVALID_FORMAT, + "合同类型不正确", + 400, + ctx.requestId, + ); + } + + try { + const product = await intakeNonIntegratedProductService.getById( + productId, + ctx.user, + ); + if (!product) { + return jsonError( + ErrorCode.PRODUCT_NOT_FOUND, + "非集成产品不存在", + 404, + ctx.requestId, + ); + } + + const contract = await createIntakeProductFactoryContractDocx({ + product, + contractType, + batch, + }); + + const contractArrayBuffer = new ArrayBuffer(contract.bytes.byteLength); + new Uint8Array(contractArrayBuffer).set(contract.bytes); + const contractBlob = new Blob([contractArrayBuffer], { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + }); + + return new NextResponse(contractBlob, { + status: 200, + headers: { + "Content-Type": + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Content-Disposition": buildContentDisposition(contract.fileName), + "Cache-Control": "no-store", + "X-Request-Id": ctx.requestId, + }, + }); + } catch (error) { + if (error instanceof AppError) { + return jsonError( + error.code, + error.message, + error.statusCode, + ctx.requestId, + ); + } + throw error; + } + }, + { requireAuth: true }, + ); +} diff --git a/furniture-3d-system/src/app/globals.css b/furniture-3d-system/src/app/globals.css index 2cea3d15..0e8078ec 100644 --- a/furniture-3d-system/src/app/globals.css +++ b/furniture-3d-system/src/app/globals.css @@ -1161,6 +1161,34 @@ background: rgba(45, 51, 56, 0.25); } + .non-integrated-table-scroll { + overflow-x: scroll !important; + overflow-y: hidden; + padding-bottom: 12px; + scrollbar-color: rgba(45, 51, 56, 0.34) rgba(45, 51, 56, 0.08); + scrollbar-width: auto; + } + + .non-integrated-table-scroll::-webkit-scrollbar { + display: block; + height: 12px; + } + + .non-integrated-table-scroll::-webkit-scrollbar-track { + background: rgba(45, 51, 56, 0.1); + border-radius: 9999px; + } + + .non-integrated-table-scroll::-webkit-scrollbar-thumb { + background: rgba(45, 51, 56, 0.32); + border: 3px solid rgba(45, 51, 56, 0.1); + border-radius: 9999px; + } + + .non-integrated-table-scroll::-webkit-scrollbar-thumb:hover { + background: rgba(45, 51, 56, 0.48); + } + [data-theme='dark'] ::-webkit-scrollbar-thumb { background: rgba(220, 227, 233, 0.15); } @@ -1168,6 +1196,25 @@ [data-theme='dark'] ::-webkit-scrollbar-thumb:hover { background: rgba(220, 227, 233, 0.25); } + + [data-theme='dark'] .non-integrated-table-scroll { + scrollbar-color: rgba(220, 227, 233, 0.36) + rgba(220, 227, 233, 0.1); + } + + [data-theme='dark'] .non-integrated-table-scroll::-webkit-scrollbar-track { + background: rgba(220, 227, 233, 0.1); + } + + [data-theme='dark'] .non-integrated-table-scroll::-webkit-scrollbar-thumb { + background: rgba(220, 227, 233, 0.36); + border: 3px solid rgba(220, 227, 233, 0.1); + } + + [data-theme='dark'] + .non-integrated-table-scroll::-webkit-scrollbar-thumb:hover { + background: rgba(220, 227, 233, 0.5); + } } /* 3D 相关样式 */ diff --git a/furniture-3d-system/src/app/intake/IntakeClient.tsx b/furniture-3d-system/src/app/intake/IntakeClient.tsx index db4deca5..34483777 100644 --- a/furniture-3d-system/src/app/intake/IntakeClient.tsx +++ b/furniture-3d-system/src/app/intake/IntakeClient.tsx @@ -9573,6 +9573,7 @@ export default function IntakeClient(props: IntakeClientProps) { seriesNameOptions={seriesNameOptions} existingSeriesNames={existingSeriesNames} seriesDraftSkuOptions={seriesDraftSkuOptions} + materialLibraryFinishes={props.materialLibraryFinishes} initialDocumentWorkspaceView={activeWorkspaceTab.detailState?.view} initialDocumentWorkspaceCategory={activeWorkspaceTab.detailState?.category} currentUserRole={props.user.role} @@ -9649,6 +9650,7 @@ export default function IntakeClient(props: IntakeClientProps) { seriesNameOptions={seriesNameOptions} existingSeriesNames={existingSeriesNames} seriesDraftSkuOptions={seriesDraftSkuOptions} + materialLibraryFinishes={props.materialLibraryFinishes} currentUserRole={props.user.role} currentUserId={props.user.id} currentUserName={props.user.name} diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx index 750fff7c..e1e44eba 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx @@ -35,6 +35,7 @@ import { PRODUCT_PATENT_STORAGE_KEY, type ProductPatentRecord, } from "../productPatentStorage"; +import type { MaterialDTO } from "@/server/services"; import { SPU_RULE_LIBRARY_STORAGE_KEY } from "../spuRuleLibraryLogic"; import { parseZipFile } from "@/lib/zip-parser"; import { extractPreviewModelFromZip } from "../../../lib/model-preview-zip"; @@ -62,8 +63,15 @@ jest.mock("@/components/platform/ModelPreviewPanel", () => ({ emptyTitle?: string; emptyDescription?: string; overlayTopRight?: ReactNode; + overlayRight?: ReactNode; overlayBottomCenter?: ReactNode; onModelNodesChange?: (nodeNames: string[]) => void; + onNodeContextMenu?: (payload: { + nodeName?: string | null; + rawNodeName?: string | null; + clientX: number; + clientY: number; + }) => void; onNodePositionOffsetChange?: (payload: { nodeName: string; offset: [number, number, number]; @@ -83,7 +91,14 @@ jest.mock("@/components/platform/ModelPreviewPanel", () => ({ nodeName: string; rotation: [number, number, number, number]; }>; + materialAssignments?: Array<{ + nodeName: string; + material: { id?: string; textureRepeatU?: number }; + }>; selectedNodeName?: string | null; + selectedNodeNames?: string[]; + enableTranslateGizmo?: boolean; + gizmoSize?: number; }) => { modelPreviewPanelMock(props); return ( @@ -93,6 +108,7 @@ jest.mock("@/components/platform/ModelPreviewPanel", () => ({
{title}
@@ -9159,47 +9628,6 @@ function BomSteelMaterialField({
);
}
-function collectSkuBatchKeys(
- skuItems: NonIntegratedSkuItem[],
- skuBatchCount: number,
-) {
- const normalizedBatchCount = Math.max(
- 1,
- Number.isFinite(skuBatchCount) ? Math.floor(skuBatchCount) : 1,
- );
- const batchKeys = new Set