feat: 编辑页 SKU 图标默认值优化
This commit is contained in:
@@ -429,7 +429,6 @@ const ProductEditPage = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [savingSourceDescription, setSavingSourceDescription] = useState(false);
|
const [savingSourceDescription, setSavingSourceDescription] = useState(false);
|
||||||
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 [usesVariantPackageWeight, setUsesVariantPackageWeight] = useState(false);
|
||||||
@@ -1268,31 +1267,6 @@ const ProductEditPage = () => {
|
|||||||
const cursor = dictionaryCursors[attributeId];
|
const cursor = dictionaryCursors[attributeId];
|
||||||
if (cursor) void loadDictionaryValues(attributeId, query, cursor);
|
if (cursor) void loadDictionaryValues(attributeId, query, cursor);
|
||||||
};
|
};
|
||||||
/** 把采集素材转存为可引用资产;完成后刷新采集证据,让对应 SKU 的图片立即可选。 */
|
|
||||||
const materializeCandidate = async (transferId: string) => {
|
|
||||||
if (!snapshot) return;
|
|
||||||
setMaterializingTransferId(transferId);
|
|
||||||
try {
|
|
||||||
const queued = await collectionApi.materializeAssets(snapshot.collection_item_id, {
|
|
||||||
transfer_ids: [transferId],
|
|
||||||
});
|
|
||||||
const completed = await waitForCatalogOperation(queued.operation.id);
|
|
||||||
const refreshed = await collectionApi.inspection(snapshot.collection_item_id);
|
|
||||||
setCollectionRecord(refreshed);
|
|
||||||
if (completed?.status === 'succeeded') {
|
|
||||||
message.success('素材已转存,可选择并随草稿保存');
|
|
||||||
} else if (completed?.status === 'failed') {
|
|
||||||
message.error(completed.error?.message ?? '素材转存失败,请稍后重试');
|
|
||||||
} else {
|
|
||||||
message.info('素材正在转存,稍后重新展开此 SKU 即可选择');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(apiErrorMessage(error));
|
|
||||||
} finally {
|
|
||||||
setMaterializingTransferId(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!currentPlatform) return <Alert type="warning" message="正在读取平台上下文" />;
|
if (!currentPlatform) return <Alert type="warning" message="正在读取平台上下文" />;
|
||||||
if (loading && !snapshot)
|
if (loading && !snapshot)
|
||||||
return (
|
return (
|
||||||
@@ -1449,18 +1423,15 @@ const ProductEditPage = () => {
|
|||||||
form={form}
|
form={form}
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
collectionRecord={collectionRecord}
|
collectionRecord={collectionRecord}
|
||||||
materializingTransferId={materializingTransferId}
|
|
||||||
activeAttributeSchema={activeAttributeSchema}
|
activeAttributeSchema={activeAttributeSchema}
|
||||||
catalogSyncing={null}
|
catalogSyncing={null}
|
||||||
dictionaryValues={dictionaryValues}
|
dictionaryValues={dictionaryValues}
|
||||||
onMaterialize={materializeCandidate}
|
|
||||||
onLoadDictionaryValues={(dictionaryId, query) =>
|
onLoadDictionaryValues={(dictionaryId, query) =>
|
||||||
void loadDictionaryValues(dictionaryId, query)
|
void loadDictionaryValues(dictionaryId, query)
|
||||||
}
|
}
|
||||||
onSyncDictionaryValues={syncDictionaryValues}
|
onSyncDictionaryValues={syncDictionaryValues}
|
||||||
onOpenDictionaryValues={openDictionaryValues}
|
onOpenDictionaryValues={openDictionaryValues}
|
||||||
onLoadMoreDictionaryValues={loadMoreDictionaryValues}
|
onLoadMoreDictionaryValues={loadMoreDictionaryValues}
|
||||||
onDirty={() => setDirty(true)}
|
|
||||||
usesVariantPackageWeight={usesVariantPackageWeight}
|
usesVariantPackageWeight={usesVariantPackageWeight}
|
||||||
usesVariantPackageDimensions={usesVariantPackageDimensions}
|
usesVariantPackageDimensions={usesVariantPackageDimensions}
|
||||||
usesVariantTags={usesVariantTags}
|
usesVariantTags={usesVariantTags}
|
||||||
|
|||||||
@@ -240,15 +240,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
.sku-media-item .ant-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 4px;
|
|
||||||
bottom: 4px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.sku-media-item.selected {
|
|
||||||
border: 2px solid #7c3aed;
|
|
||||||
}
|
|
||||||
.sku-media-item .ant-image,
|
.sku-media-item .ant-image,
|
||||||
.sku-media-item img {
|
.sku-media-item img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -117,16 +117,13 @@ type Props = {
|
|||||||
form: FormInstance<EditorFormValues>;
|
form: FormInstance<EditorFormValues>;
|
||||||
snapshot: ProductEditorSnapshot;
|
snapshot: ProductEditorSnapshot;
|
||||||
collectionRecord: CollectionItemInspection | null;
|
collectionRecord: CollectionItemInspection | null;
|
||||||
materializingTransferId: string | null;
|
|
||||||
activeAttributeSchema: ProductAttributeSchema[];
|
activeAttributeSchema: ProductAttributeSchema[];
|
||||||
catalogSyncing: string | null;
|
catalogSyncing: string | null;
|
||||||
dictionaryValues: Record<string, ProductAttributeDictionaryValue[]>;
|
dictionaryValues: Record<string, ProductAttributeDictionaryValue[]>;
|
||||||
onMaterialize: (transferId: string) => void;
|
|
||||||
onLoadDictionaryValues: (dictionaryId: string, query: string) => void;
|
onLoadDictionaryValues: (dictionaryId: string, query: string) => void;
|
||||||
onSyncDictionaryValues: (dictionaryId: string, query: string) => void;
|
onSyncDictionaryValues: (dictionaryId: string, query: string) => void;
|
||||||
onOpenDictionaryValues: (dictionaryId: string) => void;
|
onOpenDictionaryValues: (dictionaryId: string) => void;
|
||||||
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
|
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
|
||||||
onDirty: () => void;
|
|
||||||
usesVariantPackageWeight: boolean;
|
usesVariantPackageWeight: boolean;
|
||||||
usesVariantPackageDimensions: boolean;
|
usesVariantPackageDimensions: boolean;
|
||||||
usesVariantTags: boolean;
|
usesVariantTags: boolean;
|
||||||
@@ -138,25 +135,21 @@ type Props = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 「03 变体设置」区块:SKU 卡片(名称/主图/货号/价格摘要)与展开后的
|
* 「03 变体设置」区块:SKU 卡片(名称/主图/货号/价格摘要)与展开后的
|
||||||
* 采集图片选择、SKU 包装覆盖与类目变体属性编辑。
|
* 已选图片、SKU 包装覆盖与类目变体属性编辑。
|
||||||
*
|
*
|
||||||
* 采集证据(collectionRecord)与素材转存由页面处理;本组件维护卡片
|
* 采集证据仅用于补齐已选图片的预览地址;采集候选图片留待选择弹窗展示。
|
||||||
* 展开状态与图片选择交互。
|
|
||||||
*/
|
*/
|
||||||
export function ProductVariantSection({
|
export function ProductVariantSection({
|
||||||
form,
|
form,
|
||||||
snapshot,
|
snapshot,
|
||||||
collectionRecord,
|
collectionRecord,
|
||||||
materializingTransferId,
|
|
||||||
activeAttributeSchema,
|
activeAttributeSchema,
|
||||||
catalogSyncing,
|
catalogSyncing,
|
||||||
dictionaryValues,
|
dictionaryValues,
|
||||||
onMaterialize,
|
|
||||||
onLoadDictionaryValues,
|
onLoadDictionaryValues,
|
||||||
onSyncDictionaryValues,
|
onSyncDictionaryValues,
|
||||||
onOpenDictionaryValues,
|
onOpenDictionaryValues,
|
||||||
onLoadMoreDictionaryValues,
|
onLoadMoreDictionaryValues,
|
||||||
onDirty,
|
|
||||||
usesVariantPackageWeight,
|
usesVariantPackageWeight,
|
||||||
usesVariantPackageDimensions,
|
usesVariantPackageDimensions,
|
||||||
usesVariantTags,
|
usesVariantTags,
|
||||||
@@ -165,7 +158,7 @@ export function ProductVariantSection({
|
|||||||
onAiCopy,
|
onAiCopy,
|
||||||
onCompareCopy,
|
onCompareCopy,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// 实时监听变体草稿:驱动主图回显与候选图片的选中态。
|
// 实时监听变体草稿,驱动已选图片和主图回显。
|
||||||
const draftVariants = (Form.useWatch('variants', form) ?? []) as VariantValues[];
|
const draftVariants = (Form.useWatch('variants', form) ?? []) as VariantValues[];
|
||||||
// 展开状态按 Form.List 的 field.key(而非下标)记录,增删/复制 SKU 后不会错位。
|
// 展开状态按 Form.List 的 field.key(而非下标)记录,增删/复制 SKU 后不会错位。
|
||||||
const [expandedVariantKeys, setExpandedVariantKeys] = useState<Set<number>>(() => new Set());
|
const [expandedVariantKeys, setExpandedVariantKeys] = useState<Set<number>>(() => new Set());
|
||||||
@@ -245,28 +238,19 @@ export function ProductVariantSection({
|
|||||||
const selectedAssetIds =
|
const selectedAssetIds =
|
||||||
draftVariants[field.name]?.media_asset_ids ?? [];
|
draftVariants[field.name]?.media_asset_ids ?? [];
|
||||||
const selectedImages = selectedAssetIds
|
const selectedImages = selectedAssetIds
|
||||||
.map(
|
.map((assetId) => ({
|
||||||
(assetId) =>
|
assetId,
|
||||||
|
url:
|
||||||
imageCandidates.find(
|
imageCandidates.find(
|
||||||
(candidate) => candidate.asset_id === assetId
|
(candidate) => candidate.asset_id === assetId
|
||||||
)?.source_url ??
|
)?.source_url ??
|
||||||
original?.media.find(
|
original?.media.find(
|
||||||
(media) => media.asset_id === assetId
|
(media) => media.asset_id === assetId
|
||||||
)?.source_url
|
)?.source_url,
|
||||||
)
|
}))
|
||||||
.filter((url): url is string => !!url);
|
.filter((image): image is { assetId: string; url: string } =>
|
||||||
// 选中/取消候选图片;setFieldValue 不触发 Form 的
|
Boolean(image.url)
|
||||||
// onValuesChange,因此需通过 onDirty 手动标记未保存。
|
|
||||||
const toggleImage = (assetId: string) => {
|
|
||||||
const next = selectedAssetIds.includes(assetId)
|
|
||||||
? selectedAssetIds.filter((id) => id !== assetId)
|
|
||||||
: [...selectedAssetIds, assetId];
|
|
||||||
form.setFieldValue(
|
|
||||||
['variants', field.name, 'media_asset_ids'],
|
|
||||||
next
|
|
||||||
);
|
);
|
||||||
onDirty();
|
|
||||||
};
|
|
||||||
const copyVariant = () =>
|
const copyVariant = () =>
|
||||||
add({
|
add({
|
||||||
...variantValue,
|
...variantValue,
|
||||||
@@ -311,7 +295,7 @@ export function ProductVariantSection({
|
|||||||
{selectedImages[0] ? (
|
{selectedImages[0] ? (
|
||||||
<Image
|
<Image
|
||||||
preview={false}
|
preview={false}
|
||||||
src={selectedImages[0]}
|
src={selectedImages[0].url}
|
||||||
alt="SKU 主图"
|
alt="SKU 主图"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -441,106 +425,28 @@ export function ProductVariantSection({
|
|||||||
<Typography.Title level={5}>
|
<Typography.Title level={5}>
|
||||||
SKU 图片
|
SKU 图片
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
{imageCandidates.length ? (
|
<div className="sku-media-grid">
|
||||||
<div className="sku-media-grid">
|
{selectedImages.map((image, imageIndex) => (
|
||||||
{imageCandidates.map(
|
<div
|
||||||
(candidate, imageIndex) => {
|
className={`sku-media-item ${imageIndex === 0 ? 'sku-media-item--main' : ''}`}
|
||||||
// 仅"转存成功"的候选可直接勾选;待转存图片仅保留预览,
|
key={image.assetId}
|
||||||
// 避免在此处重复提供后续素材选择流程的入口。
|
|
||||||
const selectable =
|
|
||||||
candidate.status ===
|
|
||||||
'succeeded' &&
|
|
||||||
!!candidate.asset_id;
|
|
||||||
const selected =
|
|
||||||
selectable &&
|
|
||||||
selectedAssetIds.includes(
|
|
||||||
candidate.asset_id!
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`sku-media-item ${imageIndex === 0 ? 'sku-media-item--main' : ''} ${selected ? 'selected' : ''}`}
|
|
||||||
key={candidate.id}
|
|
||||||
>
|
|
||||||
{imageIndex === 0 && (
|
|
||||||
<span className="sku-media-main-badge">
|
|
||||||
主图
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
candidate.source_url
|
|
||||||
}
|
|
||||||
alt="采集候选图片"
|
|
||||||
/>
|
|
||||||
{selectable ? (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type={
|
|
||||||
selected
|
|
||||||
? 'primary'
|
|
||||||
: 'default'
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
toggleImage(
|
|
||||||
candidate.asset_id!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{selected
|
|
||||||
? '已选择'
|
|
||||||
: '选择'}
|
|
||||||
</Button>
|
|
||||||
) : candidate.status ===
|
|
||||||
'pending_selection' ? null : (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
loading={
|
|
||||||
materializingTransferId ===
|
|
||||||
candidate.id
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
candidate.status ===
|
|
||||||
'queued' ||
|
|
||||||
candidate.status ===
|
|
||||||
'running' ||
|
|
||||||
candidate.status ===
|
|
||||||
'failed'
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
void onMaterialize(
|
|
||||||
candidate.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{candidate.status ===
|
|
||||||
'failed'
|
|
||||||
? '转存失败'
|
|
||||||
: '转存中'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{/* 仅提供素材选择入口的视觉占位,实际选择流程后续接入。 */}
|
|
||||||
<Button
|
|
||||||
className="sku-media-picker"
|
|
||||||
type="dashed"
|
|
||||||
>
|
>
|
||||||
选择图片
|
{imageIndex === 0 && (
|
||||||
</Button>
|
<span className="sku-media-main-badge">
|
||||||
</div>
|
主图
|
||||||
) : (
|
</span>
|
||||||
<div className="sku-media-grid">
|
)}
|
||||||
{/* 没有采集图片时仍保留选择素材的入口。 */}
|
<Image src={image.url} alt="已选 SKU 图片" />
|
||||||
<Button
|
</div>
|
||||||
className="sku-media-picker sku-media-picker--empty"
|
))}
|
||||||
type="dashed"
|
{/* 图片选择交互将在后续接入。 */}
|
||||||
>
|
<Button
|
||||||
选择图片
|
className={`sku-media-picker ${selectedImages.length ? '' : 'sku-media-picker--empty'}`}
|
||||||
</Button>
|
type="dashed"
|
||||||
</div>
|
>
|
||||||
)}
|
选择图片
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(usesVariantPackageWeight ||
|
{(usesVariantPackageWeight ||
|
||||||
usesVariantPackageDimensions ||
|
usesVariantPackageDimensions ||
|
||||||
|
|||||||
Reference in New Issue
Block a user