feat(edit): 商品编辑页优化
This commit is contained in:
@@ -215,8 +215,8 @@
|
||||
max-width: 100% !important;
|
||||
min-width: 0;
|
||||
}
|
||||
/* SKU 规格面板的固定标签宽在纵向布局下放开 */
|
||||
.sku-spec-panel .ant-form-item-label {
|
||||
/* SKU 信息区的标签在纵向布局下保持自适应宽度。 */
|
||||
.sku-info-panel .ant-form-item-label {
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,8 @@ function formValues(snapshot: ProductEditorSnapshot): EditorFormValues {
|
||||
source_sku: variant.source_sku,
|
||||
specification_values: variant.specification_values,
|
||||
purchase_cost_cny: variant.purchase_cost_cny,
|
||||
tags: variant.tags,
|
||||
description: variant.description,
|
||||
physical_spec: variant.physical_spec,
|
||||
package_spec: variant.package_spec,
|
||||
offer_id: variant.listing.offer_id,
|
||||
@@ -346,13 +348,12 @@ function millimetres(value: string): [string, string, string] | null {
|
||||
: null;
|
||||
}
|
||||
|
||||
/** 从采集属性中识别商品/包装各自的尺寸与重量字段,用于首次自动填充默认规格。 */
|
||||
/** 从采集属性中识别包装尺寸与重量,用于首次自动填充 SPU 默认包装规格。 */
|
||||
function detectedSpecs(attributes: CollectedAttribute[]) {
|
||||
const result: Record<string, string> = {};
|
||||
for (const attribute of attributes) {
|
||||
const label = `${attribute.key} ${attribute.name}`.toLowerCase();
|
||||
const isPackage = /package|packaging|parcel|shipping|包装|包裹|外箱/.test(label);
|
||||
const isPhysical = /product|item|physical|商品|产品|实物/.test(label);
|
||||
const isDimension = /dimension|size|length|width|height|尺寸|长宽高|规格/.test(label);
|
||||
const isWeight = /weight|重量|质量/.test(label);
|
||||
const dimensions = isDimension ? millimetres(attribute.value) : null;
|
||||
@@ -361,12 +362,7 @@ function detectedSpecs(attributes: CollectedAttribute[]) {
|
||||
[result.package_length_mm, result.package_width_mm, result.package_height_mm] =
|
||||
dimensions;
|
||||
}
|
||||
if (isPhysical && dimensions) {
|
||||
[result.physical_length_mm, result.physical_width_mm, result.physical_height_mm] =
|
||||
dimensions;
|
||||
}
|
||||
if (isPackage && weight) result.shipping_weight_g = weight;
|
||||
if (isPhysical && weight) result.net_weight_g = weight;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -425,6 +421,8 @@ const ProductEditPage = () => {
|
||||
const [moreFeaturesExpanded, setMoreFeaturesExpanded] = useState(false);
|
||||
const [usesVariantPackageWeight, setUsesVariantPackageWeight] = useState(false);
|
||||
const [usesVariantPackageDimensions, setUsesVariantPackageDimensions] = useState(false);
|
||||
const [usesVariantTags, setUsesVariantTags] = useState(false);
|
||||
const [usesVariantDescription, setUsesVariantDescription] = useState(false);
|
||||
const blocker = useBlocker(dirty);
|
||||
const sourceSpecsApplied = useRef(false);
|
||||
const categories = useMemo(() => categoryTree(categoryOptions), [categoryOptions]);
|
||||
@@ -469,6 +467,8 @@ const ProductEditPage = () => {
|
||||
next.variants.length > 0 &&
|
||||
next.variants.every((variant) => hasPackageDimensions(variant.package_spec))
|
||||
);
|
||||
setUsesVariantTags(next.variants.some((variant) => variant.tags.length > 0));
|
||||
setUsesVariantDescription(next.variants.some((variant) => !!variant.description));
|
||||
setDirty(false);
|
||||
} catch (error) {
|
||||
message.error(apiErrorMessage(error));
|
||||
@@ -663,18 +663,14 @@ const ProductEditPage = () => {
|
||||
};
|
||||
}, [currentPlatform, snapshot]);
|
||||
const collected = sourceDraft ?? snapshot?.source_material ?? null;
|
||||
// 首次拿到采集数据时自动填充默认规格:只补空字段,绝不覆盖已有值;
|
||||
// 首次拿到采集数据时只自动填充 SPU 默认包装规格:只补空字段,绝不覆盖已有值;
|
||||
// 商品净重、尺寸属于 Ozon 类目属性,必须等 schema 决定 SPU/SKU 归属后由动态字段填写。
|
||||
// sourceSpecsApplied 保证整个页面生命周期只执行一次。
|
||||
useEffect(() => {
|
||||
if (sourceSpecsApplied.current || !collected) return;
|
||||
const specs = detectedSpecs(collected.attributes);
|
||||
const physical = form.getFieldValue(['common', 'default_physical_spec']);
|
||||
const packageSpec = form.getFieldValue(['common', 'default_package_spec']);
|
||||
const hasAppliedValue = [
|
||||
[physical?.length_mm, specs.physical_length_mm],
|
||||
[physical?.width_mm, specs.physical_width_mm],
|
||||
[physical?.height_mm, specs.physical_height_mm],
|
||||
[physical?.net_weight_g, specs.net_weight_g],
|
||||
[packageSpec?.length_mm, specs.package_length_mm],
|
||||
[packageSpec?.width_mm, specs.package_width_mm],
|
||||
[packageSpec?.height_mm, specs.package_height_mm],
|
||||
@@ -683,13 +679,6 @@ const ProductEditPage = () => {
|
||||
if (hasAppliedValue) {
|
||||
form.setFieldsValue({
|
||||
common: {
|
||||
default_physical_spec: {
|
||||
...physical,
|
||||
length_mm: physical?.length_mm ?? specs.physical_length_mm ?? null,
|
||||
width_mm: physical?.width_mm ?? specs.physical_width_mm ?? null,
|
||||
height_mm: physical?.height_mm ?? specs.physical_height_mm ?? null,
|
||||
net_weight_g: physical?.net_weight_g ?? specs.net_weight_g ?? null,
|
||||
},
|
||||
default_package_spec: {
|
||||
...packageSpec,
|
||||
length_mm: packageSpec?.length_mm ?? specs.package_length_mm ?? null,
|
||||
@@ -775,6 +764,32 @@ const ProductEditPage = () => {
|
||||
setUsesVariantPackageDimensions(true);
|
||||
setDirty(true);
|
||||
};
|
||||
/** 将 SPU 主题标签复制到各 SKU;后续可在每个 SKU 内独立维护。 */
|
||||
const useVariantTags = () => {
|
||||
const tags = (form.getFieldValue(['profile', 'tags']) ?? []) as string[];
|
||||
const variants = form.getFieldValue('variants') as EditorFormValues['variants'];
|
||||
form.setFieldsValue({
|
||||
variants: variants.map((variant) => ({
|
||||
...variant,
|
||||
tags: variant.tags.length ? variant.tags : tags,
|
||||
})),
|
||||
});
|
||||
setUsesVariantTags(true);
|
||||
setDirty(true);
|
||||
};
|
||||
/** 将 SPU 产品描述复制到各 SKU;后续可在每个 SKU 内独立维护。 */
|
||||
const useVariantDescription = () => {
|
||||
const description = form.getFieldValue(['profile', 'description']) as string | null;
|
||||
const variants = form.getFieldValue('variants') as EditorFormValues['variants'];
|
||||
form.setFieldsValue({
|
||||
variants: variants.map((variant) => ({
|
||||
...variant,
|
||||
description: variant.description ?? description,
|
||||
})),
|
||||
});
|
||||
setUsesVariantDescription(true);
|
||||
setDirty(true);
|
||||
};
|
||||
/** 保存草稿:携带快照版本号做乐观锁;来源更新/版本冲突时重新加载合并结果。 */
|
||||
const save = async (values: EditorFormValues) => {
|
||||
if (!productId || !currentPlatform || !snapshot) return;
|
||||
@@ -1073,6 +1088,10 @@ const ProductEditPage = () => {
|
||||
onOpenDictionaryValues={openDictionaryValues}
|
||||
onLoadMoreDictionaryValues={loadMoreDictionaryValues}
|
||||
onSyncAttributeSchema={syncAttributeSchema}
|
||||
usesVariantTags={usesVariantTags}
|
||||
usesVariantDescription={usesVariantDescription}
|
||||
onUseVariantTags={useVariantTags}
|
||||
onUseVariantDescription={useVariantDescription}
|
||||
/>
|
||||
|
||||
<ProductVariantSection
|
||||
@@ -1093,6 +1112,8 @@ const ProductEditPage = () => {
|
||||
onDirty={() => setDirty(true)}
|
||||
usesVariantPackageWeight={usesVariantPackageWeight}
|
||||
usesVariantPackageDimensions={usesVariantPackageDimensions}
|
||||
usesVariantTags={usesVariantTags}
|
||||
usesVariantDescription={usesVariantDescription}
|
||||
/>
|
||||
|
||||
<ProductOtherSection
|
||||
|
||||
@@ -61,6 +61,11 @@
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.collected-description-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.collected-field > strong {
|
||||
display: block;
|
||||
color: #352f43;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Alert, Button, Card, Collapse, Image, Input, Space, Tag, Typography } from 'antd';
|
||||
import type {
|
||||
CollectionItemInspection,
|
||||
@@ -50,6 +51,9 @@ export function CollectedProductInfoPanel({
|
||||
onShowDifferences,
|
||||
onAcceptLatest,
|
||||
}: Props) {
|
||||
const sourceDescription = collected?.description ?? '';
|
||||
const [descriptionDraft, setDescriptionDraft] = useState(sourceDescription);
|
||||
useEffect(() => setDescriptionDraft(sourceDescription), [sourceDescription]);
|
||||
// 采集图片按角色分三组展示;同组内按 URL 去重(同一张图可能同时出现在
|
||||
// 画廊与详情富文本里,转存记录会以不同角色各存一条)。
|
||||
const imageGroups = (
|
||||
@@ -131,11 +135,20 @@ export function CollectedProductInfoPanel({
|
||||
)}
|
||||
</div>
|
||||
<Input.TextArea
|
||||
value={collected?.description ?? ''}
|
||||
value={descriptionDraft}
|
||||
rows={12}
|
||||
placeholder="本次采集未取得商品描述。可补充后用于 AI 生成。"
|
||||
onChange={(event) => onUpdate('description', event.target.value)}
|
||||
onChange={(event) => setDescriptionDraft(event.target.value)}
|
||||
/>
|
||||
<div className="collected-description-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={descriptionDraft === sourceDescription}
|
||||
onClick={() => onUpdate('description', descriptionDraft)}
|
||||
>
|
||||
保存描述
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="collected-field">
|
||||
<span>采集基础数据</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Form, Input, InputNumber, Select, Typography } from 'antd';
|
||||
import { Button, Form, Input, InputNumber, Select, Tooltip, Typography } from 'antd';
|
||||
import type { FormInstance } from 'antd';
|
||||
import { InfoCircleTwoTone } from '@ant-design/icons';
|
||||
import type {
|
||||
ProductAttributeDictionaryValue,
|
||||
ProductAttributeSchema,
|
||||
@@ -10,6 +11,97 @@ import type {
|
||||
type EditorFormValues = SaveProductEditorDraftRequest;
|
||||
type AttributeDraft = NonNullable<EditorFormValues['attributes']>[number];
|
||||
type NamePath = Array<string | number>;
|
||||
const productColorValueLimit = 100;
|
||||
|
||||
/**
|
||||
* Ozon 商品颜色字典(attribute_id=10096)的 value_id → SVG 色值与俄文颜色名。
|
||||
* 数据来自 Ozon 卖家后台颜色下拉 DOM;键必须是 Ozon 字典值 ID,不能依赖会本地化或改名的显示文案。
|
||||
*/
|
||||
const ozonProductColorByValueId: Record<string, { color: string | null; russianName: string }> = {
|
||||
'258411654': { color: '#806b2a', russianName: 'хаки' },
|
||||
'970671251': { color: '#171717', russianName: 'матовый чёрный' },
|
||||
'369939085': { color: null, russianName: 'мультиколор' },
|
||||
'61584': { color: '#c3e6fe', russianName: 'небесно-голубой' },
|
||||
'258411648': { color: '#fffdd0', russianName: 'кремовый' },
|
||||
'61606': { color: '#422c16', russianName: 'шоколадный' },
|
||||
'61603': { color: '#7e1e00', russianName: 'красно-коричневый' },
|
||||
'61575': { color: '#924900', russianName: 'коричневый' },
|
||||
'61605': { color: '#808000', russianName: 'оливковый' },
|
||||
'61585': { color: '#ff9900', russianName: 'оранжевый' },
|
||||
'61591': { color: '#cd853f', russianName: 'светло-коричневый' },
|
||||
'61594': { color: '#cccccc', russianName: 'светло-серый' },
|
||||
'61593': { color: '#e2d2c3', russianName: 'светло-бежевый' },
|
||||
'61596': { color: '#ffcbde', russianName: 'светло-розовый' },
|
||||
'971039569': { color: '#db7093', russianName: 'светло-фиолетовый' },
|
||||
'61589': { color: '#89eb89', russianName: 'светло-зелёный' },
|
||||
'971001201': { color: '#4040ff', russianName: 'светло-синий' },
|
||||
'970673967': { color: '#f2f288', russianName: 'светло-жёлтый' },
|
||||
'61588': { color: '#9c85cd', russianName: 'бледно-фиолетовый' },
|
||||
'258411659': { color: '#7fff00', russianName: 'бледно-зелёный' },
|
||||
'61598': { color: '#573120', russianName: 'тёмно-коричневый' },
|
||||
'61600': { color: '#474a51', russianName: 'тёмно-серый' },
|
||||
'61604': { color: '#9a7a65', russianName: 'тёмно-бежевый' },
|
||||
'61611': { color: '#e75480', russianName: 'тёмно-розовый' },
|
||||
'61602': { color: '#00541f', russianName: 'тёмно-зелёный' },
|
||||
'61592': { color: '#00007e', russianName: 'тёмно-синий' },
|
||||
'970832145': { color: '#4a1517', russianName: 'тёмно-бордовый' },
|
||||
'61576': { color: '#666666', russianName: 'серый' },
|
||||
'61601': { color: '#ef774b', russianName: 'коралловый' },
|
||||
'971039568': { color: '#9c9c9c', russianName: 'перламутровый' },
|
||||
'61571': { color: '#ffffff', russianName: 'белый' },
|
||||
'61573': { color: '#c0987e', russianName: 'бежевый' },
|
||||
'61580': { color: '#fd85b2', russianName: 'розовый' },
|
||||
'61599': { color: '#f152db', russianName: 'пурпурный' },
|
||||
'61586': { color: '#6600cc', russianName: 'фиолетовый' },
|
||||
'970628491': { color: '#c400ab', russianName: 'красно-фиолетовый' },
|
||||
'61579': { color: '#ff0000', russianName: 'красный' },
|
||||
'970671252': { color: '#dc143c', russianName: 'ягодно-красный' },
|
||||
'61583': { color: '#287a41', russianName: 'зелёный' },
|
||||
'258411664': { color: '#ffdb58', russianName: 'горчичный' },
|
||||
'61581': { color: '#0000ff', russianName: 'синий' },
|
||||
'258411643': { color: '#007fff', russianName: 'лазурный' },
|
||||
'61597': { color: '#fbf3e1', russianName: 'слоновая кость' },
|
||||
'61572': { color: '#fffdfa', russianName: 'прозрачный' },
|
||||
'61590': { color: '#9b2d30', russianName: 'бордовый' },
|
||||
'61577': { color: '#b8b8b8', russianName: 'металлический серый' },
|
||||
'61582': { color: '#ffd700', russianName: 'золотой' },
|
||||
'61609': { color: '#b87333', russianName: 'медный' },
|
||||
'970726613': { color: '#dbe4eb', russianName: 'хромовый' },
|
||||
'61610': { color: '#c0c0c0', russianName: 'серебристый' },
|
||||
'970693003': { color: '#fffdfa', russianName: 'зеркальный' },
|
||||
'61595': { color: '#30d5c8', russianName: 'бирюзовый' },
|
||||
'61587': { color: '#e6d690', russianName: 'бронзовый' },
|
||||
'61578': { color: '#ffff00', russianName: 'жёлтый' },
|
||||
'61607': { color: '#454545', russianName: 'чёрно-серый' },
|
||||
'61574': { color: '#000000', russianName: 'чёрный' },
|
||||
};
|
||||
|
||||
/** 商品颜色允许一个 SKU 同时选择多个颜色,作为类目单值配置的业务例外。 */
|
||||
function isProductColor(definition: ProductAttributeSchema) {
|
||||
return definition.name.trim() === '商品颜色';
|
||||
}
|
||||
|
||||
/** 颜色展示严格按 Ozon 字典值 ID 取色,未知 ID 使用中性色以避免根据名称臆测。 */
|
||||
function colorSwatch(valueId: string) {
|
||||
const definition = ozonProductColorByValueId[valueId];
|
||||
if (definition?.color === null) return { color: null, isMulticolor: true };
|
||||
return { color: definition?.color ?? '#D0D5DD', isMulticolor: false };
|
||||
}
|
||||
|
||||
/** 颜色字典的下拉选项与已选标签共用该展示,提交值始终保持字典 ID。 */
|
||||
function ProductColorOption({ valueId, value }: { valueId: string; value: string }) {
|
||||
const swatch = colorSwatch(valueId);
|
||||
return (
|
||||
<span className="product-color-option">
|
||||
<span
|
||||
className={`product-color-swatch${swatch.isMulticolor ? ' product-color-swatch--multicolor' : ''}`}
|
||||
style={swatch.color ? { backgroundColor: swatch.color } : undefined}
|
||||
aria-hidden
|
||||
/>
|
||||
<span>{value}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
form: FormInstance<EditorFormValues>;
|
||||
@@ -23,6 +115,9 @@ type Props = {
|
||||
onOpenDictionaryValues: (dictionaryId: string) => void;
|
||||
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
|
||||
emptyText?: string;
|
||||
itemLayout?: 'horizontal' | 'vertical';
|
||||
forceRequiredNames?: string[];
|
||||
colorNameDefinition?: ProductAttributeSchema;
|
||||
};
|
||||
|
||||
/** 根据 schema 与归属找到顶层属性草稿的稳定数组位置。 */
|
||||
@@ -51,6 +146,12 @@ function DynamicAttributeField({
|
||||
onSyncDictionaryValues,
|
||||
onOpenDictionaryValues,
|
||||
onLoadMoreDictionaryValues,
|
||||
itemLayout,
|
||||
forceRequiredNames,
|
||||
form,
|
||||
scope,
|
||||
variantId,
|
||||
colorNameDefinition,
|
||||
}: {
|
||||
definition: ProductAttributeSchema;
|
||||
index: number;
|
||||
@@ -60,6 +161,12 @@ function DynamicAttributeField({
|
||||
onSyncDictionaryValues: Props['onSyncDictionaryValues'];
|
||||
onOpenDictionaryValues: Props['onOpenDictionaryValues'];
|
||||
onLoadMoreDictionaryValues: Props['onLoadMoreDictionaryValues'];
|
||||
itemLayout: Props['itemLayout'];
|
||||
forceRequiredNames: Props['forceRequiredNames'];
|
||||
form: Props['form'];
|
||||
scope: Props['scope'];
|
||||
variantId: Props['variantId'];
|
||||
colorNameDefinition: Props['colorNameDefinition'];
|
||||
}) {
|
||||
const [dictionaryQuery, setDictionaryQuery] = useState('');
|
||||
const onSyncRef = useRef(onSyncDictionaryValues);
|
||||
@@ -76,22 +183,87 @@ function DynamicAttributeField({
|
||||
|
||||
const valueName = ['attributes', index, 'values'] as NamePath;
|
||||
const scalarValueName = [...valueName, 0] as NamePath;
|
||||
const selectedProductColorValue = Form.useWatch(
|
||||
['attributes', index, 'values'],
|
||||
form
|
||||
) as unknown;
|
||||
const lastColorSelection = useRef<string | null>(null);
|
||||
const allowsMultipleValues = definition.is_collection || isProductColor(definition);
|
||||
const isRequired =
|
||||
definition.is_required || forceRequiredNames?.includes(definition.name.trim()) === true;
|
||||
const maxValueCount = isProductColor(definition)
|
||||
? productColorValueLimit
|
||||
: definition.max_value_count;
|
||||
const dictionaryOptions = definition.dictionary_id
|
||||
? (dictionaryValues[definition.attribute_id] ?? []).map((value) => ({
|
||||
value: value.value_id,
|
||||
label: value.value,
|
||||
label: isProductColor(definition) ? (
|
||||
<ProductColorOption valueId={value.value_id} value={value.value} />
|
||||
) : (
|
||||
value.value
|
||||
),
|
||||
}))
|
||||
: [];
|
||||
const selectedColorValueIds = Array.isArray(selectedProductColorValue)
|
||||
? selectedProductColorValue.filter((value): value is string => typeof value === 'string')
|
||||
: typeof selectedProductColorValue === 'string'
|
||||
? [selectedProductColorValue]
|
||||
: [];
|
||||
const colorSelectionKey = selectedColorValueIds.join('\u0000');
|
||||
/** 商品颜色变更时,将俄文表达立即写入同一 SKU 的颜色文字字段。 */
|
||||
const syncColorName = (selectedValue: unknown) => {
|
||||
if (!isProductColor(definition) || !colorNameDefinition) return;
|
||||
const valueIds = Array.isArray(selectedValue)
|
||||
? selectedValue.filter((value): value is string => typeof value === 'string')
|
||||
: typeof selectedValue === 'string'
|
||||
? [selectedValue]
|
||||
: [];
|
||||
const russianNames = valueIds.flatMap((valueId) => {
|
||||
const russianName = ozonProductColorByValueId[valueId]?.russianName;
|
||||
return russianName ? [russianName] : [];
|
||||
});
|
||||
const attributes = (form.getFieldValue('attributes') ?? []) as AttributeDraft[];
|
||||
const colorNameIndex = attributeIndex(attributes, colorNameDefinition, scope, variantId);
|
||||
if (colorNameIndex < 0) return;
|
||||
form.setFieldValue(
|
||||
['attributes', colorNameIndex, 'values'],
|
||||
russianNames.length ? [russianNames.join(',')] : []
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isProductColor(definition) || !colorNameDefinition) return;
|
||||
if (lastColorSelection.current === null) {
|
||||
lastColorSelection.current = colorSelectionKey;
|
||||
return;
|
||||
}
|
||||
if (lastColorSelection.current === colorSelectionKey) return;
|
||||
lastColorSelection.current = colorSelectionKey;
|
||||
|
||||
// 监听表单值作为兜底,覆盖远程字典选项更新或以编程方式修改商品颜色的场景。
|
||||
syncColorName(selectedColorValueIds);
|
||||
}, [
|
||||
colorNameDefinition,
|
||||
colorSelectionKey,
|
||||
definition,
|
||||
form,
|
||||
scope,
|
||||
selectedColorValueIds,
|
||||
syncColorName,
|
||||
variantId,
|
||||
]);
|
||||
|
||||
const input =
|
||||
definition.value_type === 'dictionary' ? (
|
||||
<Form.Item name={definition.is_collection ? valueName : scalarValueName} noStyle>
|
||||
<Form.Item name={allowsMultipleValues ? valueName : scalarValueName} noStyle>
|
||||
<Select
|
||||
mode={definition.is_collection ? 'multiple' : undefined}
|
||||
maxCount={definition.is_collection ? definition.max_value_count : undefined}
|
||||
className={isProductColor(definition) ? 'product-color-select' : undefined}
|
||||
mode={allowsMultipleValues ? 'multiple' : undefined}
|
||||
maxCount={allowsMultipleValues ? maxValueCount : undefined}
|
||||
options={dictionaryOptions}
|
||||
showSearch
|
||||
allowClear
|
||||
filterOption={false}
|
||||
onChange={(value) => syncColorName(value)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open || !definition.dictionary_id) return;
|
||||
setDictionaryQuery('');
|
||||
@@ -150,13 +322,13 @@ function DynamicAttributeField({
|
||||
placeholder={`选择${definition.name}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : definition.is_collection ? (
|
||||
) : allowsMultipleValues ? (
|
||||
<Form.Item name={valueName} noStyle>
|
||||
<Select
|
||||
mode="tags"
|
||||
maxCount={definition.max_value_count}
|
||||
maxCount={maxValueCount}
|
||||
tokenSeparators={[',']}
|
||||
placeholder={`输入后回车添加,最多 ${definition.max_value_count} 项`}
|
||||
placeholder={`输入后回车添加,最多 ${maxValueCount} 项`}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : definition.value_type === 'integer' || definition.value_type === 'decimal' ? (
|
||||
@@ -175,12 +347,21 @@ function DynamicAttributeField({
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
label={definition.name}
|
||||
required={definition.is_required}
|
||||
className="dynamic-attribute-item"
|
||||
extra={
|
||||
definition.is_collection ? `最多填写 ${definition.max_value_count} 项` : undefined
|
||||
label={
|
||||
allowsMultipleValues ? (
|
||||
<span className="dynamic-attribute-label">
|
||||
<span>{definition.name}</span>
|
||||
<Tooltip title={`最多填写 ${maxValueCount} 项`}>
|
||||
<InfoCircleTwoTone aria-label={`最多填写 ${maxValueCount} 项`} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : (
|
||||
definition.name
|
||||
)
|
||||
}
|
||||
required={isRequired}
|
||||
layout={itemLayout}
|
||||
className="dynamic-attribute-item"
|
||||
>
|
||||
<div className="dynamic-attribute-control">{input}</div>
|
||||
</Form.Item>
|
||||
@@ -200,6 +381,9 @@ export function DynamicAttributeFields({
|
||||
onOpenDictionaryValues,
|
||||
onLoadMoreDictionaryValues,
|
||||
emptyText,
|
||||
itemLayout,
|
||||
forceRequiredNames,
|
||||
colorNameDefinition,
|
||||
}: Props) {
|
||||
const attributes = (form.getFieldValue('attributes') ?? []) as AttributeDraft[];
|
||||
const fields = definitions.flatMap((definition) => {
|
||||
@@ -223,6 +407,12 @@ export function DynamicAttributeFields({
|
||||
onSyncDictionaryValues={onSyncDictionaryValues}
|
||||
onOpenDictionaryValues={onOpenDictionaryValues}
|
||||
onLoadMoreDictionaryValues={onLoadMoreDictionaryValues}
|
||||
itemLayout={itemLayout}
|
||||
forceRequiredNames={forceRequiredNames}
|
||||
form={form}
|
||||
scope={scope}
|
||||
variantId={variantId}
|
||||
colorNameDefinition={colorNameDefinition}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -54,9 +54,76 @@
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* 多值字段把上限说明收进标题提示,避免额外说明行打乱 SKU 三列表单的节奏。 */
|
||||
.dynamic-attribute-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.dynamic-attribute-label .anticon {
|
||||
flex: 0 0 auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
.dynamic-attribute-control,
|
||||
.dynamic-attribute-control .ant-select,
|
||||
.dynamic-attribute-control .ant-input,
|
||||
.dynamic-attribute-control .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* SPU 字段启用 SKU 覆盖前显示圆形加号,控件与按钮保持紧邻。 */
|
||||
.profile-override-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.profile-override-control > .ant-form-item {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.profile-override-control--description {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* 商品颜色字典只改变显示,表单仍保存 Ozon 字典值 ID。 */
|
||||
.product-color-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.product-color-swatch {
|
||||
flex: 0 0 auto;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgb(71 84 103 / 45%);
|
||||
border-radius: 50%;
|
||||
}
|
||||
/* “多色”没有单一颜色,使用空心圆表达而不伪造填充色。 */
|
||||
.product-color-swatch--multicolor {
|
||||
border-color: #667085;
|
||||
background: transparent;
|
||||
}
|
||||
/* 多选标签保持单行控件的 32px 基准高度;多项换行时仍可按内容自然增高。 */
|
||||
.product-color-select.ant-select-multiple .ant-select-selector {
|
||||
box-sizing: border-box;
|
||||
min-height: 32px;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.product-color-select .ant-select-selection-overflow {
|
||||
align-items: center;
|
||||
}
|
||||
.product-color-select .ant-select-selection-item {
|
||||
display: inline-flex;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.product-color-select .product-color-option {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Alert, Button, Card, Empty, Form, Input, Select, Space, Spin, Typography } from 'antd';
|
||||
import type { FormInstance } from 'antd';
|
||||
import { EditOutlined } from '@ant-design/icons';
|
||||
import { EditOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||
import type {
|
||||
ProductAttributeDictionaryValue,
|
||||
ProductAttributeSchema,
|
||||
@@ -37,6 +37,10 @@ type Props = {
|
||||
onOpenDictionaryValues: (dictionaryId: string) => void;
|
||||
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
|
||||
onSyncAttributeSchema: () => void;
|
||||
usesVariantTags: boolean;
|
||||
usesVariantDescription: boolean;
|
||||
onUseVariantTags: () => void;
|
||||
onUseVariantDescription: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -62,6 +66,10 @@ export function ProductAttributeSection({
|
||||
onOpenDictionaryValues,
|
||||
onLoadMoreDictionaryValues,
|
||||
onSyncAttributeSchema,
|
||||
usesVariantTags,
|
||||
usesVariantDescription,
|
||||
onUseVariantTags,
|
||||
onUseVariantDescription,
|
||||
}: Props) {
|
||||
// 富内容本地草稿:富内容能力接入前不写入表单、不随草稿保存。
|
||||
const [richContent, setRichContent] = useState('');
|
||||
@@ -87,17 +95,41 @@ export function ProductAttributeSection({
|
||||
title="产品属性"
|
||||
description="用于当前平台发布与 AI 文案生成。"
|
||||
/>
|
||||
<Form.Item name={['profile', 'tags']} label="主题标签">
|
||||
<Select
|
||||
className="editor-standard-select"
|
||||
mode="tags"
|
||||
tokenSeparators={[',']}
|
||||
placeholder="输入后回车添加标签"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['profile', 'description']} label="产品描述" rules={[{ max: 100000 }]}>
|
||||
<Input.TextArea rows={6} placeholder="商品卖点、规格与使用场景" />
|
||||
</Form.Item>
|
||||
{!usesVariantTags && (
|
||||
<Form.Item label="主题标签">
|
||||
<div className="profile-override-control">
|
||||
<Form.Item name={['profile', 'tags']} noStyle>
|
||||
<Select
|
||||
className="editor-standard-select"
|
||||
mode="tags"
|
||||
tokenSeparators={[',']}
|
||||
placeholder="输入后回车添加标签"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusCircleOutlined />}
|
||||
aria-label="按 SKU 分别填写主题标签"
|
||||
onClick={onUseVariantTags}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!usesVariantDescription && (
|
||||
<Form.Item label="产品描述" rules={[{ max: 100000 }]}>
|
||||
<div className="profile-override-control profile-override-control--description">
|
||||
<Form.Item name={['profile', 'description']} noStyle>
|
||||
<Input.TextArea rows={6} placeholder="商品卖点、规格与使用场景" />
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusCircleOutlined />}
|
||||
aria-label="按 SKU 分别填写产品描述"
|
||||
onClick={onUseVariantDescription}
|
||||
/>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name={['common', 'barcode']} label="条形码">
|
||||
<Input className="editor-short-input" placeholder="EAN / UPC / GTIN(选填)" />
|
||||
</Form.Item>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(64px, 130px) 22px minmax(64px, 130px) 22px minmax(64px, 130px)
|
||||
auto auto;
|
||||
22px 32px;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
max-width: 100%;
|
||||
@@ -26,8 +26,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
.package-dimension-unit {
|
||||
width: 22px;
|
||||
color: #667085;
|
||||
line-height: 32px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.package-dimensions-controls .ant-input-number {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button, Card, Cascader, Form, Input, InputNumber, Select, Tooltip } from 'antd';
|
||||
import type { FormInstance } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { PlusCircleOutlined } from '@ant-design/icons';
|
||||
import type { ProductEditorSnapshot } from '@atlas/contracts';
|
||||
import { SectionTitle } from './SectionTitle';
|
||||
import './ProductMainInfoSection.css';
|
||||
@@ -42,7 +42,7 @@ function PackageFields({
|
||||
<Tooltip title="按 SKU 分别填写包装重量">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusCircleOutlined />}
|
||||
aria-label="按 SKU 分别填写包装重量"
|
||||
onClick={onUseVariantPackageWeight}
|
||||
/>
|
||||
@@ -68,7 +68,7 @@ function PackageFields({
|
||||
<Tooltip title="按 SKU 分别填写包装尺寸">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusCircleOutlined />}
|
||||
aria-label="按 SKU 分别填写包装尺寸"
|
||||
onClick={onUseVariantPackageDimensions}
|
||||
/>
|
||||
@@ -118,7 +118,7 @@ export function ProductMainInfoSection({
|
||||
<SectionTitle
|
||||
number="01"
|
||||
title="主要信息"
|
||||
description="上架所需的核心资料。包装信息用于定价、物流与发布。"
|
||||
description="上架所需的核心资料。默认包装规格用于定价、物流与发布,也可分别按 SKU 覆盖。"
|
||||
/>
|
||||
<div className="editor-form-grid">
|
||||
<Form.Item
|
||||
|
||||
@@ -177,11 +177,6 @@
|
||||
.sku-attribute-panel .ant-form-item-label > label {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.sku-card-expanded .sku-attribute-panel--optional {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.sku-card-value strong {
|
||||
min-height: 32px;
|
||||
padding-top: 5px;
|
||||
@@ -193,7 +188,7 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 展开区:先展示 SKU 规格,再展示该 SKU 的图片选择区。 */
|
||||
/* 展开区按图片、SKU 信息的顺序展示。 */
|
||||
.sku-card-expanded {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
@@ -203,7 +198,7 @@
|
||||
border-top: 1px solid #f0edf3;
|
||||
}
|
||||
.sku-media-panel,
|
||||
.sku-spec-panel {
|
||||
.sku-info-panel {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
@@ -213,7 +208,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
.sku-media-panel h5.ant-typography,
|
||||
.sku-spec-panel h5.ant-typography {
|
||||
.sku-info-panel h5.ant-typography {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -224,8 +219,8 @@
|
||||
}
|
||||
.sku-media-panel h5.ant-typography::before,
|
||||
.sku-media-panel h5.ant-typography::after,
|
||||
.sku-spec-panel h5.ant-typography::before,
|
||||
.sku-spec-panel h5.ant-typography::after {
|
||||
.sku-info-panel h5.ant-typography::before,
|
||||
.sku-info-panel h5.ant-typography::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-top: 1px dashed #d9d4df;
|
||||
@@ -300,32 +295,71 @@
|
||||
grid-column: span 2;
|
||||
grid-row: span 2;
|
||||
}
|
||||
.sku-spec-panel {
|
||||
order: -1;
|
||||
.sku-info-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.sku-spec-panel .ant-form-item {
|
||||
margin-bottom: 10px;
|
||||
.sku-info-panel h5.ant-typography,
|
||||
.sku-info-panel > .ant-typography {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.sku-spec-panel .ant-form-item-label {
|
||||
flex: 0 0 78px !important;
|
||||
.sku-info-panel .ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* SKU 信息保持标签在上、控件在下,独立于页面其他横向表单布局。 */
|
||||
.sku-info-panel .ant-form-item-row {
|
||||
display: block;
|
||||
}
|
||||
.sku-info-panel .ant-form-item-label {
|
||||
display: block;
|
||||
min-height: 26px;
|
||||
padding: 0 0 4px;
|
||||
text-align: left;
|
||||
}
|
||||
.sku-spec-panel .ant-form-item-label > label {
|
||||
.sku-info-panel .ant-form-item-label > label {
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sku-spec-panel .ant-form-item-control {
|
||||
.sku-info-panel .ant-form-item-control {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
/* 独立的重量输入(非"长×宽×高"行内组)限制宽度 */
|
||||
.sku-spec-panel
|
||||
> .ant-form-item:not(:has(.sku-inline-dimensions))
|
||||
:is(.ant-input-group-wrapper, .ant-input-number-group-wrapper) {
|
||||
max-width: 220px;
|
||||
.sku-info-package-row {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.sku-info-package-row .sku-info-package-dimensions {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.sku-info-variant-description {
|
||||
grid-column: span 2;
|
||||
}
|
||||
/* 动态 SKU 属性也属于同一信息网格,不能再形成独立的横向属性区。 */
|
||||
.sku-info-panel .dynamic-attribute-list {
|
||||
display: contents;
|
||||
}
|
||||
.sku-info-panel .dynamic-attribute-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.product-editor .sku-info-panel .dynamic-attribute-item .ant-form-item-label {
|
||||
display: block;
|
||||
padding: 0 0 4px;
|
||||
text-align: left;
|
||||
}
|
||||
.product-editor .sku-info-panel .dynamic-attribute-item .ant-form-item-label > label {
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
.sku-inline-dimensions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(54px, 1fr) 16px minmax(54px, 1fr) 16px minmax(54px, 1fr) 28px;
|
||||
grid-template-columns:
|
||||
minmax(81px, 1.5fr) 16px minmax(81px, 1.5fr) 16px minmax(81px, 1.5fr)
|
||||
28px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -359,4 +393,16 @@
|
||||
.sku-inline-dimensions > span {
|
||||
display: none;
|
||||
}
|
||||
.sku-info-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sku-info-package-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.sku-info-package-row .sku-info-package-dimensions {
|
||||
grid-column: span 1;
|
||||
}
|
||||
.sku-info-variant-description {
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -91,6 +92,20 @@ function InlineDimensions({ name }: { name: NamePath }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Ozon 不同类目会使用“颜色名称”或“印刷名称”承载颜色的文字表达。 */
|
||||
function isColorNameExpression(definition: ProductAttributeSchema) {
|
||||
const label = definition.name.trim();
|
||||
return label === '颜色名称' || label === '印刷名称';
|
||||
}
|
||||
|
||||
/** 商品颜色和其文字表达是卖家识别 SKU 的首要字段,优先展示且保留其余 schema 顺序。 */
|
||||
function skuInfoAttributeOrder(definition: ProductAttributeSchema) {
|
||||
const label = definition.name.trim();
|
||||
if (label === '商品颜色') return 0;
|
||||
if (isColorNameExpression(definition)) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
form: FormInstance<EditorFormValues>;
|
||||
snapshot: ProductEditorSnapshot;
|
||||
@@ -107,11 +122,13 @@ type Props = {
|
||||
onDirty: () => void;
|
||||
usesVariantPackageWeight: boolean;
|
||||
usesVariantPackageDimensions: boolean;
|
||||
usesVariantTags: boolean;
|
||||
usesVariantDescription: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 「03 变体设置」区块:SKU 卡片(名称/主图/货号/价格摘要)与展开后的
|
||||
* 采集图片选择、包装与商品规格、规格轴编辑。
|
||||
* 采集图片选择、SKU 包装覆盖与类目变体属性编辑。
|
||||
*
|
||||
* 采集证据(collectionRecord)与素材转存由页面处理;本组件维护卡片
|
||||
* 展开状态与图片选择交互。
|
||||
@@ -132,34 +149,45 @@ export function ProductVariantSection({
|
||||
onDirty,
|
||||
usesVariantPackageWeight,
|
||||
usesVariantPackageDimensions,
|
||||
usesVariantTags,
|
||||
usesVariantDescription,
|
||||
}: Props) {
|
||||
// 实时监听变体草稿:驱动主图回显与候选图片的选中态。
|
||||
const draftVariants = (Form.useWatch('variants', form) ?? []) as VariantValues[];
|
||||
// 展开状态按 Form.List 的 field.key(而非下标)记录,增删/复制 SKU 后不会错位。
|
||||
const [expandedVariantKeys, setExpandedVariantKeys] = useState<Set<number>>(() => new Set());
|
||||
const requiredAspectDefinitions = activeAttributeSchema.filter(
|
||||
(definition) => definition.is_aspect && definition.is_required && !definition.editor_field
|
||||
(definition) =>
|
||||
definition.is_aspect &&
|
||||
definition.is_required &&
|
||||
definition.name.trim() !== '商品颜色' &&
|
||||
!definition.editor_field
|
||||
);
|
||||
const optionalAspectDefinitions = activeAttributeSchema.filter(
|
||||
(definition) => definition.is_aspect && !definition.is_required && !definition.editor_field
|
||||
const skuInfoAspectDefinitions = activeAttributeSchema
|
||||
.filter(
|
||||
(definition) =>
|
||||
definition.is_aspect &&
|
||||
(!definition.is_required || definition.name.trim() === '商品颜色') &&
|
||||
!definition.editor_field
|
||||
)
|
||||
.sort((left, right) => skuInfoAttributeOrder(left) - skuInfoAttributeOrder(right));
|
||||
const colorNameDefinition = activeAttributeSchema.find(
|
||||
(definition) =>
|
||||
definition.is_aspect && !definition.editor_field && isColorNameExpression(definition)
|
||||
);
|
||||
return (
|
||||
<Card className="editor-section sku-section" bordered={false}>
|
||||
<SectionTitle
|
||||
number="03"
|
||||
title="变体设置"
|
||||
description="卡片用于快速核对与填写;展开后可维护图片、规格轴和该 SKU 的覆盖规格。"
|
||||
description="卡片用于快速核对与填写;展开后可维护图片和该 SKU 的包装覆盖。"
|
||||
extra={<Tag>{snapshot.variants.length} 个 SKU</Tag>}
|
||||
/>
|
||||
<Form.List name="variants">
|
||||
{(fields, { add, remove }) => {
|
||||
// 新建变体默认值:id 为 null 表示服务端尚不存在,
|
||||
// client_key 作为保存前的临时标识。
|
||||
// 包装覆盖字段保持为空,发布时继承 SPU 默认值,避免持久化重复数据。
|
||||
const addVariant = () => {
|
||||
const defaultPackageSpec = form.getFieldValue([
|
||||
'common',
|
||||
'default_package_spec',
|
||||
]) as EditorFormValues['common']['default_package_spec'];
|
||||
add({
|
||||
id: null,
|
||||
client_key: crypto.randomUUID(),
|
||||
@@ -168,6 +196,8 @@ export function ProductVariantSection({
|
||||
source_sku: null,
|
||||
specification_values: [],
|
||||
purchase_cost_cny: null,
|
||||
tags: [],
|
||||
description: null,
|
||||
physical_spec: {
|
||||
length_mm: null,
|
||||
width_mm: null,
|
||||
@@ -176,18 +206,10 @@ export function ProductVariantSection({
|
||||
status: null,
|
||||
},
|
||||
package_spec: {
|
||||
length_mm: usesVariantPackageDimensions
|
||||
? defaultPackageSpec.length_mm
|
||||
: null,
|
||||
width_mm: usesVariantPackageDimensions
|
||||
? defaultPackageSpec.width_mm
|
||||
: null,
|
||||
height_mm: usesVariantPackageDimensions
|
||||
? defaultPackageSpec.height_mm
|
||||
: null,
|
||||
shipping_weight_g: usesVariantPackageWeight
|
||||
? defaultPackageSpec.shipping_weight_g
|
||||
: null,
|
||||
length_mm: null,
|
||||
width_mm: null,
|
||||
height_mm: null,
|
||||
shipping_weight_g: null,
|
||||
status: null,
|
||||
},
|
||||
offer_id: null,
|
||||
@@ -438,36 +460,6 @@ export function ProductVariantSection({
|
||||
)}
|
||||
{expanded && (
|
||||
<div className="sku-card-expanded">
|
||||
{optionalAspectDefinitions.length > 0 &&
|
||||
sourceId && (
|
||||
<div className="sku-attribute-panel sku-attribute-panel--optional">
|
||||
<Typography.Title level={5}>
|
||||
Ozon 选填变体属性
|
||||
</Typography.Title>
|
||||
<DynamicAttributeFields
|
||||
form={form}
|
||||
definitions={
|
||||
optionalAspectDefinitions
|
||||
}
|
||||
scope="variant"
|
||||
variantId={sourceId}
|
||||
dictionaryValues={dictionaryValues}
|
||||
catalogSyncing={catalogSyncing}
|
||||
onLoadDictionaryValues={
|
||||
onLoadDictionaryValues
|
||||
}
|
||||
onSyncDictionaryValues={
|
||||
onSyncDictionaryValues
|
||||
}
|
||||
onOpenDictionaryValues={
|
||||
onOpenDictionaryValues
|
||||
}
|
||||
onLoadMoreDictionaryValues={
|
||||
onLoadMoreDictionaryValues
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="sku-media-panel">
|
||||
<Typography.Title level={5}>
|
||||
SKU 图片
|
||||
@@ -573,103 +565,125 @@ export function ProductVariantSection({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="sku-spec-panel">
|
||||
<Typography.Title level={5}>
|
||||
SKU 规格
|
||||
</Typography.Title>
|
||||
{usesVariantPackageWeight && (
|
||||
<Form.Item
|
||||
name={[
|
||||
field.name,
|
||||
'package_spec',
|
||||
'shipping_weight_g',
|
||||
]}
|
||||
label="包装重量"
|
||||
>
|
||||
<InputNumber<string>
|
||||
min="0"
|
||||
step="0.001"
|
||||
stringMode
|
||||
addonAfter="g"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{usesVariantPackageDimensions && (
|
||||
<Form.Item label="包装尺寸">
|
||||
<InlineDimensions
|
||||
name={[field.name, 'package_spec']}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
name={[
|
||||
field.name,
|
||||
'physical_spec',
|
||||
'net_weight_g',
|
||||
]}
|
||||
label="商品重量"
|
||||
>
|
||||
<InputNumber<string>
|
||||
min="0"
|
||||
step="0.001"
|
||||
stringMode
|
||||
addonAfter="g"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="商品尺寸">
|
||||
<InlineDimensions
|
||||
name={[field.name, 'physical_spec']}
|
||||
/>
|
||||
</Form.Item>
|
||||
{/* 规格轴来自采集结果:颜色轴展示为"商品颜色" */}
|
||||
<Form.List
|
||||
name={[field.name, 'specification_values']}
|
||||
>
|
||||
{(specFields) =>
|
||||
specFields.length ? (
|
||||
<>
|
||||
{specFields.map((specField) => {
|
||||
const axisKey =
|
||||
form.getFieldValue([
|
||||
'variants',
|
||||
{(usesVariantPackageWeight ||
|
||||
usesVariantPackageDimensions ||
|
||||
usesVariantTags ||
|
||||
usesVariantDescription ||
|
||||
(skuInfoAspectDefinitions.length > 0 &&
|
||||
sourceId)) && (
|
||||
<div className="sku-info-panel">
|
||||
<Typography.Title level={5}>
|
||||
SKU 信息
|
||||
</Typography.Title>
|
||||
{(usesVariantPackageWeight ||
|
||||
usesVariantPackageDimensions) && (
|
||||
<div className="sku-info-package-row">
|
||||
{usesVariantPackageWeight && (
|
||||
<Form.Item
|
||||
name={[
|
||||
field.name,
|
||||
'package_spec',
|
||||
'shipping_weight_g',
|
||||
]}
|
||||
label="包装重量"
|
||||
required
|
||||
layout="vertical"
|
||||
className="sku-info-package-weight"
|
||||
>
|
||||
<InputNumber<string>
|
||||
min="0"
|
||||
step="0.001"
|
||||
stringMode
|
||||
addonAfter="g"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{usesVariantPackageDimensions && (
|
||||
<Form.Item
|
||||
label="包装尺寸"
|
||||
required
|
||||
layout="vertical"
|
||||
className="sku-info-package-dimensions"
|
||||
>
|
||||
<InlineDimensions
|
||||
name={[
|
||||
field.name,
|
||||
'specification_values',
|
||||
specField.name,
|
||||
'axis_key',
|
||||
]) as
|
||||
string | undefined;
|
||||
const label =
|
||||
/color|颜色/i.test(
|
||||
axisKey ?? ''
|
||||
)
|
||||
? '商品颜色'
|
||||
: axisKey ||
|
||||
'商品规格';
|
||||
return (
|
||||
<Form.Item
|
||||
key={specField.key}
|
||||
name={[
|
||||
specField.name,
|
||||
'value_name',
|
||||
]}
|
||||
label={label}
|
||||
>
|
||||
<Input placeholder="未填写" />
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<Form.Item label="商品颜色">
|
||||
<Input
|
||||
placeholder="未填写"
|
||||
disabled
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.List>
|
||||
</div>
|
||||
'package_spec',
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{usesVariantTags && (
|
||||
<Form.Item
|
||||
name={[field.name, 'tags']}
|
||||
label="主题标签"
|
||||
layout="vertical"
|
||||
className="sku-info-variant-tags"
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
tokenSeparators={[',']}
|
||||
placeholder="输入后回车添加标签"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{usesVariantDescription && (
|
||||
<Form.Item
|
||||
name={[field.name, 'description']}
|
||||
label="产品描述"
|
||||
layout="vertical"
|
||||
className="sku-info-variant-description"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="填写该 SKU 的产品描述"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{skuInfoAspectDefinitions.length > 0 &&
|
||||
sourceId && (
|
||||
<DynamicAttributeFields
|
||||
form={form}
|
||||
definitions={
|
||||
skuInfoAspectDefinitions
|
||||
}
|
||||
scope="variant"
|
||||
variantId={sourceId}
|
||||
dictionaryValues={
|
||||
dictionaryValues
|
||||
}
|
||||
catalogSyncing={catalogSyncing}
|
||||
onLoadDictionaryValues={
|
||||
onLoadDictionaryValues
|
||||
}
|
||||
onSyncDictionaryValues={
|
||||
onSyncDictionaryValues
|
||||
}
|
||||
onOpenDictionaryValues={
|
||||
onOpenDictionaryValues
|
||||
}
|
||||
onLoadMoreDictionaryValues={
|
||||
onLoadMoreDictionaryValues
|
||||
}
|
||||
itemLayout="vertical"
|
||||
forceRequiredNames={[
|
||||
'商品颜色',
|
||||
]}
|
||||
colorNameDefinition={
|
||||
colorNameDefinition
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(usesVariantPackageWeight ||
|
||||
usesVariantPackageDimensions) && (
|
||||
<Typography.Text type="secondary">
|
||||
留空时使用 SPU 默认包装规格
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user