feat(edit): 编辑页样式修改
This commit is contained in:
@@ -35,6 +35,25 @@ import './ProductEditPage.css';
|
|||||||
type EditorFormValues = SaveProductEditorDraftRequest;
|
type EditorFormValues = SaveProductEditorDraftRequest;
|
||||||
type AttributeDraft = NonNullable<EditorFormValues['attributes']>[number];
|
type AttributeDraft = NonNullable<EditorFormValues['attributes']>[number];
|
||||||
|
|
||||||
|
/** 规格字段有值时才可据此恢复到对应的 SKU 编辑模式。 */
|
||||||
|
function hasPackageWeight(spec: {
|
||||||
|
length_mm: string | null;
|
||||||
|
width_mm: string | null;
|
||||||
|
height_mm: string | null;
|
||||||
|
shipping_weight_g: string | null;
|
||||||
|
}) {
|
||||||
|
return Boolean(spec.shipping_weight_g);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPackageDimensions(spec: {
|
||||||
|
length_mm: string | null;
|
||||||
|
width_mm: string | null;
|
||||||
|
height_mm: string | null;
|
||||||
|
shipping_weight_g: string | null;
|
||||||
|
}) {
|
||||||
|
return Boolean(spec.length_mm && spec.width_mm && spec.height_mm);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 schema 补齐表单中缺失的属性草稿:aspect(变体维度)属性逐个 SKU 一条,
|
* 按 schema 补齐表单中缺失的属性草稿:aspect(变体维度)属性逐个 SKU 一条,
|
||||||
* 其余类目级一条,确保 Form.List 字段与 schema 一一对应、可按定义渲染。
|
* 其余类目级一条,确保 Form.List 字段与 schema 一一对应、可按定义渲染。
|
||||||
@@ -167,11 +186,11 @@ function categoryTree(options: ProductEditorCategoryOption[]) {
|
|||||||
for (const category of options) {
|
for (const category of options) {
|
||||||
const node = nodes.get(category.id)!;
|
const node = nodes.get(category.id)!;
|
||||||
node.isLeaf = !node.children?.length;
|
node.isLeaf = !node.children?.length;
|
||||||
node.disabled = node.isLeaf && (
|
node.disabled =
|
||||||
!category.is_active ||
|
node.isLeaf &&
|
||||||
!category.external_type_id ||
|
(!category.is_active ||
|
||||||
category.external_type_id === '0'
|
!category.external_type_id ||
|
||||||
);
|
category.external_type_id === '0');
|
||||||
}
|
}
|
||||||
for (const category of options) {
|
for (const category of options) {
|
||||||
const path: string[] = [];
|
const path: string[] = [];
|
||||||
@@ -404,6 +423,8 @@ const ProductEditPage = () => {
|
|||||||
const [materializingTransferId, setMaterializingTransferId] = useState<string | null>(null);
|
const [materializingTransferId, setMaterializingTransferId] = useState<string | null>(null);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [moreFeaturesExpanded, setMoreFeaturesExpanded] = useState(false);
|
const [moreFeaturesExpanded, setMoreFeaturesExpanded] = useState(false);
|
||||||
|
const [usesVariantPackageWeight, setUsesVariantPackageWeight] = useState(false);
|
||||||
|
const [usesVariantPackageDimensions, setUsesVariantPackageDimensions] = useState(false);
|
||||||
const blocker = useBlocker(dirty);
|
const blocker = useBlocker(dirty);
|
||||||
const sourceSpecsApplied = useRef(false);
|
const sourceSpecsApplied = useRef(false);
|
||||||
const categories = useMemo(() => categoryTree(categoryOptions), [categoryOptions]);
|
const categories = useMemo(() => categoryTree(categoryOptions), [categoryOptions]);
|
||||||
@@ -440,6 +461,14 @@ const ProductEditPage = () => {
|
|||||||
setSourceOverriddenFields(next.source_material_state.overridden_fields);
|
setSourceOverriddenFields(next.source_material_state.overridden_fields);
|
||||||
sourceSpecsApplied.current = false;
|
sourceSpecsApplied.current = false;
|
||||||
form.setFieldsValue(formValues(next));
|
form.setFieldsValue(formValues(next));
|
||||||
|
setUsesVariantPackageWeight(
|
||||||
|
next.variants.length > 0 &&
|
||||||
|
next.variants.every((variant) => hasPackageWeight(variant.package_spec))
|
||||||
|
);
|
||||||
|
setUsesVariantPackageDimensions(
|
||||||
|
next.variants.length > 0 &&
|
||||||
|
next.variants.every((variant) => hasPackageDimensions(variant.package_spec))
|
||||||
|
);
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(apiErrorMessage(error));
|
message.error(apiErrorMessage(error));
|
||||||
@@ -601,7 +630,9 @@ const ProductEditPage = () => {
|
|||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
results.map(([id, page]) => [
|
results.map(([id, page]) => [
|
||||||
id,
|
id,
|
||||||
dictionaryQueries.current[id] ? (current[id] ?? null) : page.next_cursor,
|
dictionaryQueries.current[id]
|
||||||
|
? (current[id] ?? null)
|
||||||
|
: page.next_cursor,
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -615,13 +646,7 @@ const ProductEditPage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [
|
}, [catalogRefresh, currentPlatform, editableAttributeSchema, selectedCategoryId, snapshot]);
|
||||||
catalogRefresh,
|
|
||||||
currentPlatform,
|
|
||||||
editableAttributeSchema,
|
|
||||||
selectedCategoryId,
|
|
||||||
snapshot,
|
|
||||||
]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!snapshot || !currentPlatform) return;
|
if (!snapshot || !currentPlatform) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
@@ -703,6 +728,53 @@ const ProductEditPage = () => {
|
|||||||
}, [blocker, modal]);
|
}, [blocker, modal]);
|
||||||
|
|
||||||
const leave = () => navigate('/collection');
|
const leave = () => navigate('/collection');
|
||||||
|
/** 将 SPU 共用包装重量复制到每个 SKU,保留默认值作为未填写 SKU 的安全回退。 */
|
||||||
|
const useVariantPackageWeight = () => {
|
||||||
|
const defaultPackageSpec = form.getFieldValue([
|
||||||
|
'common',
|
||||||
|
'default_package_spec',
|
||||||
|
]) as EditorFormValues['common']['default_package_spec'];
|
||||||
|
const variants = form.getFieldValue('variants') as EditorFormValues['variants'];
|
||||||
|
form.setFieldsValue({
|
||||||
|
variants: variants.map((variant) => {
|
||||||
|
const packageSpec = variant.package_spec;
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
package_spec: {
|
||||||
|
...packageSpec,
|
||||||
|
shipping_weight_g:
|
||||||
|
packageSpec.shipping_weight_g ?? defaultPackageSpec.shipping_weight_g,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setUsesVariantPackageWeight(true);
|
||||||
|
setDirty(true);
|
||||||
|
};
|
||||||
|
/** 将 SPU 共用包装尺寸复制到每个 SKU,保留默认值作为未填写 SKU 的安全回退。 */
|
||||||
|
const useVariantPackageDimensions = () => {
|
||||||
|
const defaultPackageSpec = form.getFieldValue([
|
||||||
|
'common',
|
||||||
|
'default_package_spec',
|
||||||
|
]) as EditorFormValues['common']['default_package_spec'];
|
||||||
|
const variants = form.getFieldValue('variants') as EditorFormValues['variants'];
|
||||||
|
form.setFieldsValue({
|
||||||
|
variants: variants.map((variant) => {
|
||||||
|
const packageSpec = variant.package_spec;
|
||||||
|
return {
|
||||||
|
...variant,
|
||||||
|
package_spec: {
|
||||||
|
...packageSpec,
|
||||||
|
length_mm: packageSpec.length_mm ?? defaultPackageSpec.length_mm,
|
||||||
|
width_mm: packageSpec.width_mm ?? defaultPackageSpec.width_mm,
|
||||||
|
height_mm: packageSpec.height_mm ?? defaultPackageSpec.height_mm,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setUsesVariantPackageDimensions(true);
|
||||||
|
setDirty(true);
|
||||||
|
};
|
||||||
/** 保存草稿:携带快照版本号做乐观锁;来源更新/版本冲突时重新加载合并结果。 */
|
/** 保存草稿:携带快照版本号做乐观锁;来源更新/版本冲突时重新加载合并结果。 */
|
||||||
const save = async (values: EditorFormValues) => {
|
const save = async (values: EditorFormValues) => {
|
||||||
if (!productId || !currentPlatform || !snapshot) return;
|
if (!productId || !currentPlatform || !snapshot) return;
|
||||||
@@ -836,7 +908,8 @@ const ProductEditPage = () => {
|
|||||||
if (
|
if (
|
||||||
dictionaryContextRef.current !== requestContext ||
|
dictionaryContextRef.current !== requestContext ||
|
||||||
dictionaryQueries.current[attributeId] !== query
|
dictionaryQueries.current[attributeId] !== query
|
||||||
) return;
|
)
|
||||||
|
return;
|
||||||
setDictionaryValues((current) => ({
|
setDictionaryValues((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[attributeId]: cursor
|
[attributeId]: cursor
|
||||||
@@ -976,6 +1049,10 @@ const ProductEditPage = () => {
|
|||||||
categoryPaths={categories.paths}
|
categoryPaths={categories.paths}
|
||||||
categoryNotFoundContent={categoryNotFoundContent}
|
categoryNotFoundContent={categoryNotFoundContent}
|
||||||
onCategorySearch={setCategorySearch}
|
onCategorySearch={setCategorySearch}
|
||||||
|
usesVariantPackageWeight={usesVariantPackageWeight}
|
||||||
|
usesVariantPackageDimensions={usesVariantPackageDimensions}
|
||||||
|
onUseVariantPackageWeight={useVariantPackageWeight}
|
||||||
|
onUseVariantPackageDimensions={useVariantPackageDimensions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProductAttributeSection
|
<ProductAttributeSection
|
||||||
@@ -1014,6 +1091,8 @@ const ProductEditPage = () => {
|
|||||||
onOpenDictionaryValues={openDictionaryValues}
|
onOpenDictionaryValues={openDictionaryValues}
|
||||||
onLoadMoreDictionaryValues={loadMoreDictionaryValues}
|
onLoadMoreDictionaryValues={loadMoreDictionaryValues}
|
||||||
onDirty={() => setDirty(true)}
|
onDirty={() => setDirty(true)}
|
||||||
|
usesVariantPackageWeight={usesVariantPackageWeight}
|
||||||
|
usesVariantPackageDimensions={usesVariantPackageDimensions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProductOtherSection
|
<ProductOtherSection
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Button, Form, Input, Select, Typography } from 'antd';
|
import { Button, Form, Input, InputNumber, Select, Typography } from 'antd';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
import type {
|
import type {
|
||||||
ProductAttributeDictionaryValue,
|
ProductAttributeDictionaryValue,
|
||||||
@@ -159,18 +159,18 @@ function DynamicAttributeField({
|
|||||||
placeholder={`输入后回车添加,最多 ${definition.max_value_count} 项`}
|
placeholder={`输入后回车添加,最多 ${definition.max_value_count} 项`}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : definition.value_type === 'integer' || definition.value_type === 'decimal' ? (
|
||||||
<Form.Item name={scalarValueName} noStyle>
|
<Form.Item name={scalarValueName} noStyle>
|
||||||
<Input
|
<InputNumber<string>
|
||||||
type={
|
step={definition.value_type === 'integer' ? '1' : '0.001'}
|
||||||
definition.value_type === 'integer' || definition.value_type === 'decimal'
|
stringMode
|
||||||
? 'number'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
step={definition.value_type === 'integer' ? '1' : 'any'}
|
|
||||||
placeholder={`填写${definition.name}`}
|
placeholder={`填写${definition.name}`}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<Form.Item name={scalarValueName} noStyle>
|
||||||
|
<Input placeholder={`填写${definition.name}`} />
|
||||||
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
}
|
}
|
||||||
.dynamic-attribute-control,
|
.dynamic-attribute-control,
|
||||||
.dynamic-attribute-control .ant-select,
|
.dynamic-attribute-control .ant-select,
|
||||||
.dynamic-attribute-control .ant-input {
|
.dynamic-attribute-control .ant-input,
|
||||||
|
.dynamic-attribute-control .ant-input-number {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,9 @@
|
|||||||
/* 长×宽×高输入组:随容器宽度收缩(压缩到下限后再由窄屏断点改为纵向堆叠) */
|
/* 长×宽×高输入组:随容器宽度收缩(压缩到下限后再由窄屏断点改为纵向堆叠) */
|
||||||
.package-dimensions-controls {
|
.package-dimensions-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(64px, 130px) 22px minmax(64px, 130px) 22px minmax(
|
grid-template-columns:
|
||||||
64px,
|
minmax(64px, 130px) 22px minmax(64px, 130px) 22px minmax(64px, 130px)
|
||||||
130px
|
auto auto;
|
||||||
) auto;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -30,6 +29,14 @@
|
|||||||
color: #667085;
|
color: #667085;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
.package-dimensions-controls .ant-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.package-weight-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 类目下拉弹层(Cascader popup,经 classNames.root 挂载) */
|
/* 类目下拉弹层(Cascader popup,经 classNames.root 挂载) */
|
||||||
.editor-category-popup .ant-cascader-menu {
|
.editor-category-popup .ant-cascader-menu {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Card, Cascader, Form, Input, Select } from 'antd';
|
import { Button, Card, Cascader, Form, Input, InputNumber, Select, Tooltip } from 'antd';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import type { ProductEditorSnapshot } from '@atlas/contracts';
|
import type { ProductEditorSnapshot } from '@atlas/contracts';
|
||||||
import { SectionTitle } from './SectionTitle';
|
import { SectionTitle } from './SectionTitle';
|
||||||
import './ProductMainInfoSection.css';
|
import './ProductMainInfoSection.css';
|
||||||
@@ -13,28 +14,68 @@ type CategoryOption = {
|
|||||||
children?: CategoryOption[];
|
children?: CategoryOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function PackageFields({ name }: { name: NamePath }) {
|
function PackageFields({
|
||||||
|
name,
|
||||||
|
usesVariantPackageWeight,
|
||||||
|
usesVariantPackageDimensions,
|
||||||
|
onUseVariantPackageWeight,
|
||||||
|
onUseVariantPackageDimensions,
|
||||||
|
}: {
|
||||||
|
name: NamePath;
|
||||||
|
usesVariantPackageWeight: boolean;
|
||||||
|
usesVariantPackageDimensions: boolean;
|
||||||
|
onUseVariantPackageWeight: () => void;
|
||||||
|
onUseVariantPackageDimensions: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="package-fields">
|
<div className="package-fields">
|
||||||
<Form.Item name={[...name, 'shipping_weight_g']} label="包装重量" required>
|
{!usesVariantPackageWeight && (
|
||||||
<Input type="number" min="0" step="any" addonAfter="g" style={{ width: 150 }} />
|
<Form.Item name={[...name, 'shipping_weight_g']} label="包装重量" required>
|
||||||
</Form.Item>
|
<div className="package-weight-control">
|
||||||
<Form.Item label="包装尺寸" required className="package-dimensions">
|
<InputNumber<string>
|
||||||
<div className="package-dimensions-controls">
|
min="0"
|
||||||
<Form.Item name={[...name, 'length_mm']} noStyle>
|
step="0.001"
|
||||||
<Input type="number" min="0" step="any" placeholder="长" />
|
stringMode
|
||||||
</Form.Item>
|
addonAfter="g"
|
||||||
<span className="package-dimension-separator">×</span>
|
style={{ width: 150 }}
|
||||||
<Form.Item name={[...name, 'width_mm']} noStyle>
|
/>
|
||||||
<Input type="number" min="0" step="any" placeholder="宽" />
|
<Tooltip title="按 SKU 分别填写包装重量">
|
||||||
</Form.Item>
|
<Button
|
||||||
<span className="package-dimension-separator">×</span>
|
type="text"
|
||||||
<Form.Item name={[...name, 'height_mm']} noStyle>
|
icon={<PlusOutlined />}
|
||||||
<Input type="number" min="0" step="any" placeholder="高" />
|
aria-label="按 SKU 分别填写包装重量"
|
||||||
</Form.Item>
|
onClick={onUseVariantPackageWeight}
|
||||||
<span className="package-dimension-unit">mm</span>
|
/>
|
||||||
</div>
|
</Tooltip>
|
||||||
</Form.Item>
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
{!usesVariantPackageDimensions && (
|
||||||
|
<Form.Item label="包装尺寸" required className="package-dimensions">
|
||||||
|
<div className="package-dimensions-controls">
|
||||||
|
<Form.Item name={[...name, 'length_mm']} noStyle>
|
||||||
|
<InputNumber<string> min="0" step="0.001" stringMode placeholder="长" />
|
||||||
|
</Form.Item>
|
||||||
|
<span className="package-dimension-separator">×</span>
|
||||||
|
<Form.Item name={[...name, 'width_mm']} noStyle>
|
||||||
|
<InputNumber<string> min="0" step="0.001" stringMode placeholder="宽" />
|
||||||
|
</Form.Item>
|
||||||
|
<span className="package-dimension-separator">×</span>
|
||||||
|
<Form.Item name={[...name, 'height_mm']} noStyle>
|
||||||
|
<InputNumber<string> min="0" step="0.001" stringMode placeholder="高" />
|
||||||
|
</Form.Item>
|
||||||
|
<span className="package-dimension-unit">mm</span>
|
||||||
|
<Tooltip title="按 SKU 分别填写包装尺寸">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
aria-label="按 SKU 分别填写包装尺寸"
|
||||||
|
onClick={onUseVariantPackageDimensions}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -47,6 +88,10 @@ type Props = {
|
|||||||
categoryPaths: Map<string, string[]>;
|
categoryPaths: Map<string, string[]>;
|
||||||
categoryNotFoundContent: string;
|
categoryNotFoundContent: string;
|
||||||
onCategorySearch: (query: string) => void;
|
onCategorySearch: (query: string) => void;
|
||||||
|
usesVariantPackageWeight: boolean;
|
||||||
|
usesVariantPackageDimensions: boolean;
|
||||||
|
onUseVariantPackageWeight: () => void;
|
||||||
|
onUseVariantPackageDimensions: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +108,10 @@ export function ProductMainInfoSection({
|
|||||||
categoryPaths,
|
categoryPaths,
|
||||||
categoryNotFoundContent,
|
categoryNotFoundContent,
|
||||||
onCategorySearch,
|
onCategorySearch,
|
||||||
|
usesVariantPackageWeight,
|
||||||
|
usesVariantPackageDimensions,
|
||||||
|
onUseVariantPackageWeight,
|
||||||
|
onUseVariantPackageDimensions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<Card className="editor-section editor-horizontal-section" bordered={false}>
|
<Card className="editor-section editor-horizontal-section" bordered={false}>
|
||||||
@@ -139,7 +188,15 @@ export function ProductMainInfoSection({
|
|||||||
<Input className="editor-short-input" placeholder="无品牌请填写“无品牌”" />
|
<Input className="editor-short-input" placeholder="无品牌请填写“无品牌”" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<PackageFields name={['common', 'default_package_spec']} />
|
{(!usesVariantPackageWeight || !usesVariantPackageDimensions) && (
|
||||||
|
<PackageFields
|
||||||
|
name={['common', 'default_package_spec']}
|
||||||
|
usesVariantPackageWeight={usesVariantPackageWeight}
|
||||||
|
usesVariantPackageDimensions={usesVariantPackageDimensions}
|
||||||
|
onUseVariantPackageWeight={onUseVariantPackageWeight}
|
||||||
|
onUseVariantPackageDimensions={onUseVariantPackageDimensions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
.sku-section .ant-input {
|
.sku-section .ant-input {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
.sku-section .ant-input-number {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
.sku-section textarea.ant-input {
|
.sku-section textarea.ant-input {
|
||||||
resize: none;
|
resize: none;
|
||||||
}
|
}
|
||||||
@@ -13,6 +17,9 @@
|
|||||||
.sku-section .ant-input-group .ant-input {
|
.sku-section .ant-input-group .ant-input {
|
||||||
border-radius: 8px 0 0 8px;
|
border-radius: 8px 0 0 8px;
|
||||||
}
|
}
|
||||||
|
.sku-section .ant-input-number-group .ant-input-number {
|
||||||
|
border-radius: 8px 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 卡片列表与卡片骨架 */
|
/* 卡片列表与卡片骨架 */
|
||||||
.sku-card-list {
|
.sku-card-list {
|
||||||
@@ -83,7 +90,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.sku-card-summary .ant-input,
|
.sku-card-summary .ant-input,
|
||||||
.sku-card-summary textarea.ant-input {
|
.sku-card-summary textarea.ant-input,
|
||||||
|
.sku-card-summary .ant-input-number {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.sku-card-field,
|
.sku-card-field,
|
||||||
@@ -310,7 +318,9 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
/* 独立的重量输入(非"长×宽×高"行内组)限制宽度 */
|
/* 独立的重量输入(非"长×宽×高"行内组)限制宽度 */
|
||||||
.sku-spec-panel > .ant-form-item:not(:has(.sku-inline-dimensions)) .ant-input-group-wrapper {
|
.sku-spec-panel
|
||||||
|
> .ant-form-item:not(:has(.sku-inline-dimensions))
|
||||||
|
:is(.ant-input-group-wrapper, .ant-input-number-group-wrapper) {
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
}
|
}
|
||||||
.sku-inline-dimensions {
|
.sku-inline-dimensions {
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Card, Form, Image, Input, Popconfirm, Space, Tag, Typography } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Image,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Popconfirm,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import type { FormInstance } from 'antd';
|
import type { FormInstance } from 'antd';
|
||||||
import { CopyOutlined, DeleteOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons';
|
import { CopyOutlined, DeleteOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import type {
|
import type {
|
||||||
@@ -65,15 +76,15 @@ function InlineDimensions({ name }: { name: NamePath }) {
|
|||||||
return (
|
return (
|
||||||
<div className="sku-inline-dimensions">
|
<div className="sku-inline-dimensions">
|
||||||
<Form.Item name={[...name, 'length_mm']} noStyle>
|
<Form.Item name={[...name, 'length_mm']} noStyle>
|
||||||
<Input type="number" min="0" step="any" placeholder="长" />
|
<InputNumber<string> min="0" step="0.001" stringMode placeholder="长" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<span className="sku-dimension-separator">×</span>
|
<span className="sku-dimension-separator">×</span>
|
||||||
<Form.Item name={[...name, 'width_mm']} noStyle>
|
<Form.Item name={[...name, 'width_mm']} noStyle>
|
||||||
<Input type="number" min="0" step="any" placeholder="宽" />
|
<InputNumber<string> min="0" step="0.001" stringMode placeholder="宽" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<span className="sku-dimension-separator">×</span>
|
<span className="sku-dimension-separator">×</span>
|
||||||
<Form.Item name={[...name, 'height_mm']} noStyle>
|
<Form.Item name={[...name, 'height_mm']} noStyle>
|
||||||
<Input type="number" min="0" step="any" placeholder="高" />
|
<InputNumber<string> min="0" step="0.001" stringMode placeholder="高" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<span className="sku-dimension-unit">mm</span>
|
<span className="sku-dimension-unit">mm</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +105,8 @@ type Props = {
|
|||||||
onOpenDictionaryValues: (dictionaryId: string) => void;
|
onOpenDictionaryValues: (dictionaryId: string) => void;
|
||||||
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
|
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
|
||||||
onDirty: () => void;
|
onDirty: () => void;
|
||||||
|
usesVariantPackageWeight: boolean;
|
||||||
|
usesVariantPackageDimensions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,6 +130,8 @@ export function ProductVariantSection({
|
|||||||
onOpenDictionaryValues,
|
onOpenDictionaryValues,
|
||||||
onLoadMoreDictionaryValues,
|
onLoadMoreDictionaryValues,
|
||||||
onDirty,
|
onDirty,
|
||||||
|
usesVariantPackageWeight,
|
||||||
|
usesVariantPackageDimensions,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// 实时监听变体草稿:驱动主图回显与候选图片的选中态。
|
// 实时监听变体草稿:驱动主图回显与候选图片的选中态。
|
||||||
const draftVariants = (Form.useWatch('variants', form) ?? []) as VariantValues[];
|
const draftVariants = (Form.useWatch('variants', form) ?? []) as VariantValues[];
|
||||||
@@ -140,7 +155,11 @@ export function ProductVariantSection({
|
|||||||
{(fields, { add, remove }) => {
|
{(fields, { add, remove }) => {
|
||||||
// 新建变体默认值:id 为 null 表示服务端尚不存在,
|
// 新建变体默认值:id 为 null 表示服务端尚不存在,
|
||||||
// client_key 作为保存前的临时标识。
|
// client_key 作为保存前的临时标识。
|
||||||
const addVariant = () =>
|
const addVariant = () => {
|
||||||
|
const defaultPackageSpec = form.getFieldValue([
|
||||||
|
'common',
|
||||||
|
'default_package_spec',
|
||||||
|
]) as EditorFormValues['common']['default_package_spec'];
|
||||||
add({
|
add({
|
||||||
id: null,
|
id: null,
|
||||||
client_key: crypto.randomUUID(),
|
client_key: crypto.randomUUID(),
|
||||||
@@ -157,15 +176,24 @@ export function ProductVariantSection({
|
|||||||
status: null,
|
status: null,
|
||||||
},
|
},
|
||||||
package_spec: {
|
package_spec: {
|
||||||
length_mm: null,
|
length_mm: usesVariantPackageDimensions
|
||||||
width_mm: null,
|
? defaultPackageSpec.length_mm
|
||||||
height_mm: null,
|
: null,
|
||||||
shipping_weight_g: null,
|
width_mm: usesVariantPackageDimensions
|
||||||
|
? defaultPackageSpec.width_mm
|
||||||
|
: null,
|
||||||
|
height_mm: usesVariantPackageDimensions
|
||||||
|
? defaultPackageSpec.height_mm
|
||||||
|
: null,
|
||||||
|
shipping_weight_g: usesVariantPackageWeight
|
||||||
|
? defaultPackageSpec.shipping_weight_g
|
||||||
|
: null,
|
||||||
status: null,
|
status: null,
|
||||||
},
|
},
|
||||||
offer_id: null,
|
offer_id: null,
|
||||||
media_asset_ids: [],
|
media_asset_ids: [],
|
||||||
} satisfies VariantValues);
|
} satisfies VariantValues);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="sku-card-list">
|
<div className="sku-card-list">
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
@@ -331,7 +359,12 @@ export function ProductVariantSection({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="—" />
|
<InputNumber<string>
|
||||||
|
min="0"
|
||||||
|
step="0.0001"
|
||||||
|
stringMode
|
||||||
|
placeholder="—"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="sku-card-field sku-price-field">
|
<div className="sku-card-field sku-price-field">
|
||||||
@@ -340,13 +373,15 @@ export function ProductVariantSection({
|
|||||||
{original?.listing.currency_code ?? '₽'}
|
{original?.listing.currency_code ?? '₽'}
|
||||||
)
|
)
|
||||||
</span>
|
</span>
|
||||||
<Input
|
<InputNumber<string>
|
||||||
value={
|
value={
|
||||||
original?.listing.selling_price ??
|
original?.listing.selling_price ??
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
placeholder="—"
|
placeholder="—"
|
||||||
readOnly
|
readOnly
|
||||||
|
controls={false}
|
||||||
|
stringMode
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sku-card-value">
|
<div className="sku-card-value">
|
||||||
@@ -542,26 +577,30 @@ export function ProductVariantSection({
|
|||||||
<Typography.Title level={5}>
|
<Typography.Title level={5}>
|
||||||
SKU 规格
|
SKU 规格
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Form.Item
|
{usesVariantPackageWeight && (
|
||||||
name={[
|
<Form.Item
|
||||||
field.name,
|
name={[
|
||||||
'package_spec',
|
field.name,
|
||||||
'shipping_weight_g',
|
'package_spec',
|
||||||
]}
|
'shipping_weight_g',
|
||||||
label="包装重量"
|
]}
|
||||||
>
|
label="包装重量"
|
||||||
<Input
|
>
|
||||||
type="number"
|
<InputNumber<string>
|
||||||
min="0"
|
min="0"
|
||||||
step="any"
|
step="0.001"
|
||||||
addonAfter="g"
|
stringMode
|
||||||
/>
|
addonAfter="g"
|
||||||
</Form.Item>
|
/>
|
||||||
<Form.Item label="包装尺寸">
|
</Form.Item>
|
||||||
<InlineDimensions
|
)}
|
||||||
name={[field.name, 'package_spec']}
|
{usesVariantPackageDimensions && (
|
||||||
/>
|
<Form.Item label="包装尺寸">
|
||||||
</Form.Item>
|
<InlineDimensions
|
||||||
|
name={[field.name, 'package_spec']}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={[
|
name={[
|
||||||
field.name,
|
field.name,
|
||||||
@@ -570,10 +609,10 @@ export function ProductVariantSection({
|
|||||||
]}
|
]}
|
||||||
label="商品重量"
|
label="商品重量"
|
||||||
>
|
>
|
||||||
<Input
|
<InputNumber<string>
|
||||||
type="number"
|
|
||||||
min="0"
|
min="0"
|
||||||
step="any"
|
step="0.001"
|
||||||
|
stringMode
|
||||||
addonAfter="g"
|
addonAfter="g"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user