C端SKU数量小于等于1不展示chip

后端修复规格轴数据错乱显示问题
This commit is contained in:
2026-08-26 07:43:26 +08:00
parent 1f4506f60b
commit c7cd0f6cf2
6 changed files with 58 additions and 40 deletions
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { import {
Button, Image, Input, InputNumber, Modal, Radio, Select, Space, Switch, Table, Typography, message, Button, Image, Input, InputNumber, Modal, Radio, Select, Space, Switch, Table, Typography, message,
} from 'antd'; } from 'antd';
@@ -50,6 +50,38 @@ function keyOf(ids: string[]) {
return [...ids].sort().join('_'); 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 = { type Props = {
productId: string; productId: string;
initialAttrs: SpecAttr[]; initialAttrs: SpecAttr[];
@@ -58,44 +90,19 @@ type Props = {
}; };
export default function ProductSpecsEditor({ productId, initialAttrs, initialSkus, onSaved }: Props) { export default function ProductSpecsEditor({ productId, initialAttrs, initialSkus, onSaved }: Props) {
const [attrs, setAttrs] = useState<SpecAttr[]>( const [attrs, setAttrs] = useState<SpecAttr[]>(() => (initialAttrs.length ? initialAttrs : []));
initialAttrs.length const [skus, setSkus] = useState<SkuRow[]>(() => normalizeSkuRows(initialSkus));
? initialAttrs
: [],
);
const [skus, setSkus] = useState<SkuRow[]>(
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 [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [editIndex, setEditIndex] = useState<number | null>(null); const [editIndex, setEditIndex] = useState<number | null>(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]); const combos = useMemo(() => cartesian(attrs.filter((a) => a.name.trim() && a.values.some((v) => v.name.trim()))), [attrs]);
function regenerateMatrix() { function regenerateMatrix() {
@@ -610,6 +610,7 @@ export default function ProductsPage() {
label: '规格与 SKU', label: '规格与 SKU',
children: ( children: (
<ProductSpecsEditor <ProductSpecsEditor
key={String(detail.id)}
productId={String(detail.id)} productId={String(detail.id)}
initialAttrs={(detail.specAttrs as never) ?? []} initialAttrs={(detail.specAttrs as never) ?? []}
initialSkus={(detail.skus as never) ?? []} initialSkus={(detail.skus as never) ?? []}
@@ -140,7 +140,8 @@ export default function ProductDetailPage() {
const attrs = product?.specAttrs ?? []; const attrs = product?.specAttrs ?? [];
const skus = product?.skus ?? []; const skus = product?.skus ?? [];
const specEnabled = !!(product?.specEnabled && attrs.length > 0); const onSaleSkuCount = skus.filter((s) => s.status === 'ON_SALE').length;
const specEnabled = onSaleSkuCount > 1 && attrs.length > 0;
const activeSku = useMemo(() => { const activeSku = useMemo(() => {
if (!product) return undefined; if (!product) return undefined;
if (!specEnabled) { if (!specEnabled) {
+1 -1
View File
@@ -65,7 +65,7 @@ export interface ProductDto {
allowOnlinePurchase?: boolean; allowOnlinePurchase?: boolean;
/** 是否允许跨城配送 */ /** 是否允许跨城配送 */
allowCrossCityDelivery?: boolean; allowCrossCityDelivery?: boolean;
/** v3.5.4:是否配置了销售规格(多 SKU */ /** C 端是否展示规格选择(可售 SKU > 1 且已配规格轴 */
specEnabled?: boolean; specEnabled?: boolean;
/** 列表轻量:展示 SKU 销售单位 */ /** 列表轻量:展示 SKU 销售单位 */
saleUnit?: ProductSaleUnit; saleUnit?: ProductSaleUnit;
@@ -7,6 +7,7 @@ import {
} from '../../common/test-whitelist/test-whitelist.service'; } from '../../common/test-whitelist/test-whitelist.service';
import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper'; import { groupResourcesByProductId, mapProductMedia } from './catalog.mapper';
import { import {
catalogSpecPickerEnabled,
flattenSkuOntoProduct, flattenSkuOntoProduct,
listMinOnSalePrice, listMinOnSalePrice,
mapSkuDto, mapSkuDto,
@@ -133,7 +134,7 @@ export class CatalogService {
allowOnSitePickup: flat.allowOnSitePickup, allowOnSitePickup: flat.allowOnSitePickup,
allowOnlinePurchase: flat.allowOnlinePurchase, allowOnlinePurchase: flat.allowOnlinePurchase,
allowCrossCityDelivery: flat.allowCrossCityDelivery, allowCrossCityDelivery: flat.allowCrossCityDelivery,
specEnabled: specAttrs.length > 0 || skus.length > 1, specEnabled: catalogSpecPickerEnabled(skus, specAttrs.length),
saleUnit: flat.saleUnit, saleUnit: flat.saleUnit,
...media, ...media,
}; };
@@ -196,7 +197,7 @@ export class CatalogService {
allowOnlinePurchase: flat.allowOnlinePurchase, allowOnlinePurchase: flat.allowOnlinePurchase,
allowCrossCityDelivery: flat.allowCrossCityDelivery, allowCrossCityDelivery: flat.allowCrossCityDelivery,
saleUnit: flat.saleUnit, saleUnit: flat.saleUnit,
specEnabled: specAttrs.length > 0 || skus.length > 1, specEnabled: catalogSpecPickerEnabled(skus, specAttrs.length),
specAttrs: mapSpecAttrsDto(specAttrs), specAttrs: mapSpecAttrsDto(specAttrs),
skus: cSkus, skus: cSkus,
defaultSkuId: defaultSku?.id.toString(), defaultSkuId: defaultSku?.id.toString(),
@@ -18,6 +18,14 @@ export function isSkuOnSale(sku: { status: string }): boolean {
return sku.status === 'ON_SALE'; 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 { export function buildSpecKey(valueIds: bigint[]): string {
if (!valueIds.length) return ''; if (!valueIds.length) return '';
return [...valueIds] return [...valueIds]