From c7cd0f6cf2f414c6917b4bea1546aeea035ab857 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Wed, 26 Aug 2026 07:43:26 +0800 Subject: [PATCH] =?UTF-8?q?C=E7=AB=AFSKU=E6=95=B0=E9=87=8F=E5=B0=8F?= =?UTF-8?q?=E4=BA=8E=E7=AD=89=E4=BA=8E1=E4=B8=8D=E5=B1=95=E7=A4=BAchip=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BF=AE=E5=A4=8D=E8=A7=84=E6=A0=BC=E8=BD=B4?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=94=99=E4=B9=B1=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ProductSpecsEditor.tsx | 79 ++++++++++--------- apps/admin-web/src/pages/ProductsPage.tsx | 1 + .../src/pages/product-detail/index.tsx | 3 +- packages/shared-types/src/catalog.ts | 2 +- .../src/modules/catalog/catalog.service.ts | 5 +- .../src/modules/catalog/product-sku.util.ts | 8 ++ 6 files changed, 58 insertions(+), 40 deletions(-) diff --git a/apps/admin-web/src/components/ProductSpecsEditor.tsx b/apps/admin-web/src/components/ProductSpecsEditor.tsx index b89901b..16201f1 100644 --- a/apps/admin-web/src/components/ProductSpecsEditor.tsx +++ b/apps/admin-web/src/components/ProductSpecsEditor.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Button, Image, Input, InputNumber, Modal, Radio, Select, Space, Switch, Table, Typography, message, } from 'antd'; @@ -50,6 +50,38 @@ function keyOf(ids: string[]) { return [...ids].sort().join('_'); } +function normalizeSkuRows(initialSkus: SkuRow[]): SkuRow[] { + if (!initialSkus.length) { + return [ + { + specValueIds: [], + barcode69: '', + price: 0, + status: 'ON_SALE', + allowOnlinePurchase: true, + allowCrossCityDelivery: true, + allowOnSitePickup: false, + saleUnit: 'BOTTLE', + bottlesPerUnit: 1, + isDefault: true, + imageUrl: '', + }, + ]; + } + return initialSkus.map((s) => ({ + ...s, + specValueIds: s.specValueIds ?? [], + status: s.status || 'ON_SALE', + allowOnlinePurchase: s.allowOnlinePurchase !== false, + allowCrossCityDelivery: s.allowCrossCityDelivery !== false, + allowOnSitePickup: !!s.allowOnSitePickup, + saleUnit: s.saleUnit === 'BOX' ? 'BOX' : 'BOTTLE', + bottlesPerUnit: s.bottlesPerUnit || (s.saleUnit === 'BOX' ? 6 : 1), + isDefault: !!s.isDefault, + imageUrl: s.imageUrl ?? '', + })); +} + type Props = { productId: string; initialAttrs: SpecAttr[]; @@ -58,44 +90,19 @@ type Props = { }; export default function ProductSpecsEditor({ productId, initialAttrs, initialSkus, onSaved }: Props) { - const [attrs, setAttrs] = useState( - initialAttrs.length - ? initialAttrs - : [], - ); - const [skus, setSkus] = useState( - initialSkus.length - ? initialSkus.map((s) => ({ - ...s, - specValueIds: s.specValueIds ?? [], - status: s.status || 'ON_SALE', - allowOnlinePurchase: s.allowOnlinePurchase !== false, - allowCrossCityDelivery: s.allowCrossCityDelivery !== false, - allowOnSitePickup: !!s.allowOnSitePickup, - saleUnit: s.saleUnit === 'BOX' ? 'BOX' : 'BOTTLE', - bottlesPerUnit: s.bottlesPerUnit || (s.saleUnit === 'BOX' ? 6 : 1), - isDefault: !!s.isDefault, - imageUrl: s.imageUrl ?? '', - })) - : [ - { - specValueIds: [], - barcode69: '', - price: 0, - status: 'ON_SALE', - allowOnlinePurchase: true, - allowCrossCityDelivery: true, - allowOnSitePickup: false, - saleUnit: 'BOTTLE', - bottlesPerUnit: 1, - isDefault: true, - imageUrl: '', - }, - ], - ); + const [attrs, setAttrs] = useState(() => (initialAttrs.length ? initialAttrs : [])); + const [skus, setSkus] = useState(() => normalizeSkuRows(initialSkus)); const [saving, setSaving] = useState(false); const [editIndex, setEditIndex] = useState(null); + useEffect(() => { + setAttrs(initialAttrs.length ? initialAttrs : []); + setSkus(normalizeSkuRows(initialSkus)); + setEditIndex(null); + // 仅切商品时重载,避免父组件重渲染冲掉编辑中内容 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [productId]); + const combos = useMemo(() => cartesian(attrs.filter((a) => a.name.trim() && a.values.some((v) => v.name.trim()))), [attrs]); function regenerateMatrix() { diff --git a/apps/admin-web/src/pages/ProductsPage.tsx b/apps/admin-web/src/pages/ProductsPage.tsx index c9f8c35..88c73e3 100644 --- a/apps/admin-web/src/pages/ProductsPage.tsx +++ b/apps/admin-web/src/pages/ProductsPage.tsx @@ -610,6 +610,7 @@ export default function ProductsPage() { label: '规格与 SKU', children: ( 0); + const onSaleSkuCount = skus.filter((s) => s.status === 'ON_SALE').length; + const specEnabled = onSaleSkuCount > 1 && attrs.length > 0; const activeSku = useMemo(() => { if (!product) return undefined; if (!specEnabled) { diff --git a/packages/shared-types/src/catalog.ts b/packages/shared-types/src/catalog.ts index 1e3a516..d2451b9 100644 --- a/packages/shared-types/src/catalog.ts +++ b/packages/shared-types/src/catalog.ts @@ -65,7 +65,7 @@ export interface ProductDto { allowOnlinePurchase?: boolean; /** 是否允许跨城配送 */ allowCrossCityDelivery?: boolean; - /** v3.5.4:是否配置了销售规格(多 SKU) */ + /** C 端是否展示规格选择(可售 SKU > 1 且已配规格轴) */ specEnabled?: boolean; /** 列表轻量:展示 SKU 销售单位 */ saleUnit?: ProductSaleUnit; diff --git a/server/dukang-api/src/modules/catalog/catalog.service.ts b/server/dukang-api/src/modules/catalog/catalog.service.ts index 5273665..a6b5452 100644 --- a/server/dukang-api/src/modules/catalog/catalog.service.ts +++ b/server/dukang-api/src/modules/catalog/catalog.service.ts @@ -7,6 +7,7 @@ import { } from '../../common/test-whitelist/test-whitelist.service'; import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper'; import { + catalogSpecPickerEnabled, flattenSkuOntoProduct, listMinOnSalePrice, mapSkuDto, @@ -133,7 +134,7 @@ export class CatalogService { allowOnSitePickup: flat.allowOnSitePickup, allowOnlinePurchase: flat.allowOnlinePurchase, allowCrossCityDelivery: flat.allowCrossCityDelivery, - specEnabled: specAttrs.length > 0 || skus.length > 1, + specEnabled: catalogSpecPickerEnabled(skus, specAttrs.length), saleUnit: flat.saleUnit, ...media, }; @@ -196,7 +197,7 @@ export class CatalogService { allowOnlinePurchase: flat.allowOnlinePurchase, allowCrossCityDelivery: flat.allowCrossCityDelivery, saleUnit: flat.saleUnit, - specEnabled: specAttrs.length > 0 || skus.length > 1, + specEnabled: catalogSpecPickerEnabled(skus, specAttrs.length), specAttrs: mapSpecAttrsDto(specAttrs), skus: cSkus, defaultSkuId: defaultSku?.id.toString(), diff --git a/server/dukang-api/src/modules/catalog/product-sku.util.ts b/server/dukang-api/src/modules/catalog/product-sku.util.ts index 505c964..6cca998 100644 --- a/server/dukang-api/src/modules/catalog/product-sku.util.ts +++ b/server/dukang-api/src/modules/catalog/product-sku.util.ts @@ -18,6 +18,14 @@ export function isSkuOnSale(sku: { status: string }): boolean { return sku.status === 'ON_SALE'; } +/** C 端是否展示规格选择:可售 SKU 多于 1 且已配规格轴 */ +export function catalogSpecPickerEnabled( + skus: Array<{ status: string }>, + specAttrCount: number, +): boolean { + return skus.filter(isSkuOnSale).length > 1 && specAttrCount > 0; +} + export function buildSpecKey(valueIds: bigint[]): string { if (!valueIds.length) return ''; return [...valueIds]