feat: 编辑页 SKU 图标默认值优化

This commit is contained in:
R524809
2026-09-23 17:39:05 +08:00
parent b3958166dd
commit d24ce8f4e0
3 changed files with 32 additions and 164 deletions
@@ -429,7 +429,6 @@ const ProductEditPage = () => {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [savingSourceDescription, setSavingSourceDescription] = useState(false);
const [materializingTransferId, setMaterializingTransferId] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
const [moreFeaturesExpanded, setMoreFeaturesExpanded] = useState(false);
const [usesVariantPackageWeight, setUsesVariantPackageWeight] = useState(false);
@@ -1268,31 +1267,6 @@ const ProductEditPage = () => {
const cursor = dictionaryCursors[attributeId];
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 (loading && !snapshot)
return (
@@ -1449,18 +1423,15 @@ const ProductEditPage = () => {
form={form}
snapshot={snapshot}
collectionRecord={collectionRecord}
materializingTransferId={materializingTransferId}
activeAttributeSchema={activeAttributeSchema}
catalogSyncing={null}
dictionaryValues={dictionaryValues}
onMaterialize={materializeCandidate}
onLoadDictionaryValues={(dictionaryId, query) =>
void loadDictionaryValues(dictionaryId, query)
}
onSyncDictionaryValues={syncDictionaryValues}
onOpenDictionaryValues={openDictionaryValues}
onLoadMoreDictionaryValues={loadMoreDictionaryValues}
onDirty={() => setDirty(true)}
usesVariantPackageWeight={usesVariantPackageWeight}
usesVariantPackageDimensions={usesVariantPackageDimensions}
usesVariantTags={usesVariantTags}
@@ -240,15 +240,6 @@
font-size: 11px;
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 img {
width: 100%;
@@ -117,16 +117,13 @@ type Props = {
form: FormInstance<EditorFormValues>;
snapshot: ProductEditorSnapshot;
collectionRecord: CollectionItemInspection | null;
materializingTransferId: string | null;
activeAttributeSchema: ProductAttributeSchema[];
catalogSyncing: string | null;
dictionaryValues: Record<string, ProductAttributeDictionaryValue[]>;
onMaterialize: (transferId: string) => void;
onLoadDictionaryValues: (dictionaryId: string, query: string) => void;
onSyncDictionaryValues: (dictionaryId: string, query: string) => void;
onOpenDictionaryValues: (dictionaryId: string) => void;
onLoadMoreDictionaryValues: (dictionaryId: string, query: string) => void;
onDirty: () => void;
usesVariantPackageWeight: boolean;
usesVariantPackageDimensions: boolean;
usesVariantTags: boolean;
@@ -138,25 +135,21 @@ type Props = {
/**
* 「03 变体设置」区块:SKU 卡片(名称/主图/货号/价格摘要)与展开后的
* 采集图片选择、SKU 包装覆盖与类目变体属性编辑。
* 已选图片、SKU 包装覆盖与类目变体属性编辑。
*
* 采集证据(collectionRecord)与素材转存由页面处理;本组件维护卡片
* 展开状态与图片选择交互。
* 采集证据仅用于补齐已选图片的预览地址;采集候选图片留待选择弹窗展示。
*/
export function ProductVariantSection({
form,
snapshot,
collectionRecord,
materializingTransferId,
activeAttributeSchema,
catalogSyncing,
dictionaryValues,
onMaterialize,
onLoadDictionaryValues,
onSyncDictionaryValues,
onOpenDictionaryValues,
onLoadMoreDictionaryValues,
onDirty,
usesVariantPackageWeight,
usesVariantPackageDimensions,
usesVariantTags,
@@ -165,7 +158,7 @@ export function ProductVariantSection({
onAiCopy,
onCompareCopy,
}: Props) {
// 实时监听变体草稿:驱动主图回显与候选图片的选中态。
// 实时监听变体草稿,驱动已选图片和主图回显。
const draftVariants = (Form.useWatch('variants', form) ?? []) as VariantValues[];
// 展开状态按 Form.List 的 field.key(而非下标)记录,增删/复制 SKU 后不会错位。
const [expandedVariantKeys, setExpandedVariantKeys] = useState<Set<number>>(() => new Set());
@@ -245,28 +238,19 @@ export function ProductVariantSection({
const selectedAssetIds =
draftVariants[field.name]?.media_asset_ids ?? [];
const selectedImages = selectedAssetIds
.map(
(assetId) =>
.map((assetId) => ({
assetId,
url:
imageCandidates.find(
(candidate) => candidate.asset_id === assetId
)?.source_url ??
original?.media.find(
(media) => media.asset_id === assetId
)?.source_url
)
.filter((url): url is string => !!url);
// 选中/取消候选图片;setFieldValue 不触发 Form 的
// 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
)?.source_url,
}))
.filter((image): image is { assetId: string; url: string } =>
Boolean(image.url)
);
onDirty();
};
const copyVariant = () =>
add({
...variantValue,
@@ -311,7 +295,7 @@ export function ProductVariantSection({
{selectedImages[0] ? (
<Image
preview={false}
src={selectedImages[0]}
src={selectedImages[0].url}
alt="SKU 主图"
/>
) : (
@@ -441,106 +425,28 @@ export function ProductVariantSection({
<Typography.Title level={5}>
SKU 图片
</Typography.Title>
{imageCandidates.length ? (
<div className="sku-media-grid">
{imageCandidates.map(
(candidate, imageIndex) => {
// 仅"转存成功"的候选可直接勾选;待转存图片仅保留预览,
// 避免在此处重复提供后续素材选择流程的入口。
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"
<div className="sku-media-grid">
{selectedImages.map((image, imageIndex) => (
<div
className={`sku-media-item ${imageIndex === 0 ? 'sku-media-item--main' : ''}`}
key={image.assetId}
>
选择图片
</Button>
</div>
) : (
<div className="sku-media-grid">
{/* 没有采集图片时仍保留选择素材的入口。 */}
<Button
className="sku-media-picker sku-media-picker--empty"
type="dashed"
>
选择图片
</Button>
</div>
)}
{imageIndex === 0 && (
<span className="sku-media-main-badge">
主图
</span>
)}
<Image src={image.url} alt="已选 SKU 图片" />
</div>
))}
{/* 图片选择交互将在后续接入。 */}
<Button
className={`sku-media-picker ${selectedImages.length ? '' : 'sku-media-picker--empty'}`}
type="dashed"
>
选择图片
</Button>
</div>
</div>
{(usesVariantPackageWeight ||
usesVariantPackageDimensions ||