fix(edit): 类目问题修改

This commit is contained in:
R524809
2026-09-11 10:36:25 +08:00
parent 05a00d46b9
commit 2c4e94ab3d
11 changed files with 869 additions and 79 deletions
@@ -147,7 +147,7 @@
}
.package-dimensions-controls {
display: grid;
grid-template-columns: 120px 22px 120px 22px 120px auto;
grid-template-columns: 130px 22px 130px 22px 130px auto;
align-items: center;
gap: 7px;
width: max-content;
@@ -163,6 +163,29 @@
font-size: 16px;
line-height: 40px;
}
.editor-standard-cascader .ant-select-selector {
min-height: 40px !important;
font-size: 16px !important;
}
.editor-standard-cascader .ant-select-selection-placeholder,
.editor-standard-cascader .ant-select-selection-item {
line-height: 38px !important;
font-size: 16px !important;
}
.editor-category-popup .ant-cascader-menu {
max-height: 320px;
}
.editor-category-popup .ant-cascader-menu-item {
display: flex;
min-height: 40px;
align-items: center;
font-size: 16px;
line-height: 24px;
}
.supply-link-control .ant-btn {
height: 40px;
min-width: 40px;
}
.category-row {
display: flex;
align-items: center;
@@ -411,6 +434,7 @@
gap: 10px;
}
.sku-media-item {
position: relative;
min-width: 0;
aspect-ratio: 3 / 4;
overflow: hidden;
@@ -418,6 +442,15 @@
border-radius: 8px;
background: #f5f3f7;
}
.sku-media-item .ant-btn {
position: absolute;
right: 6px;
bottom: 6px;
z-index: 1;
}
.sku-media-item.selected {
border: 2px solid #7c3aed;
}
.sku-media-item .ant-image,
.sku-media-item img {
width: 100%;
+140 -39
View File
@@ -95,6 +95,7 @@ function formValues(snapshot: ProductEditorSnapshot): EditorFormValues {
physical_spec: variant.physical_spec,
package_spec: variant.package_spec,
offer_id: variant.listing.offer_id,
media_asset_ids: variant.media.map((media) => media.asset_id),
})),
};
}
@@ -110,7 +111,7 @@ const wait = (milliseconds: number) =>
type CategoryTreeOption = {
value: string;
label: string;
disabled?: boolean;
isLeaf?: boolean;
children?: CategoryTreeOption[];
};
@@ -121,7 +122,6 @@ function categoryTree(options: ProductEditorCategoryOption[]) {
nodes.set(category.id, {
value: category.id,
label: category.name,
...(category.is_active ? {} : { disabled: true }),
children: [],
});
}
@@ -134,6 +134,13 @@ function categoryTree(options: ProductEditorCategoryOption[]) {
if (parent) parent.children!.push(node);
else roots.push(node);
}
// Cascader navigation nodes must never be disabled: disabled folders cannot
// be opened. `changeOnSelect` remains false (the default), so the component
// only commits a value for a leaf node.
for (const category of options) {
const node = nodes.get(category.id)!;
node.isLeaf = !node.children?.length;
}
for (const category of options) {
const path: string[] = [];
const visited = new Set<string>();
@@ -320,7 +327,10 @@ function detectedSpecs(attributes: CollectedAttribute[]) {
return result;
}
function variantImageUrls(recordValue: CollectionItemInspection | null, variant: SnapshotVariant) {
function variantImageCandidates(
recordValue: CollectionItemInspection | null,
variant: SnapshotVariant,
) {
if (!recordValue) return [];
const sourceVariants = new Map(recordValue.variants.map((item) => [item.id, item]));
return recordValue.asset_transfers
@@ -332,8 +342,10 @@ function variantImageUrls(recordValue: CollectionItemInspection | null, variant:
variant.source_variant_key
);
})
.map((transfer) => transfer.source_url)
.filter((url, index, urls) => !!url && urls.indexOf(url) === index);
.filter(
(transfer, index, transfers) =>
transfers.findIndex((item) => item.id === transfer.id) === index,
);
}
function PackageFields({ name }: { name: NamePath }) {
@@ -433,6 +445,7 @@ const ProductEditPage = () => {
const selectedCategoryId = Form.useWatch(['profile', 'store_product_category_id'], form);
const selectedStoreId = Form.useWatch(['profile', 'target_store_id'], form);
const draftAttributes = Form.useWatch('attributes', form) ?? [];
const draftVariants = Form.useWatch('variants', form) ?? [];
const [snapshot, setSnapshot] = useState<ProductEditorSnapshot | null>(null);
const [collectionRecord, setCollectionRecord] = useState<CollectionItemInspection | null>(null);
const [sourceDraft, setSourceDraft] = useState<ProductAiSourceMaterial | null>(null);
@@ -450,6 +463,7 @@ const ProductEditPage = () => {
const [catalogSyncing, setCatalogSyncing] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [materializingTransferId, setMaterializingTransferId] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
const [expandedVariantKeys, setExpandedVariantKeys] = useState<Set<number>>(() => new Set());
const blocker = useBlocker(dirty);
@@ -811,6 +825,29 @@ const ProductEditPage = () => {
'字典值已同步',
).then(() => void loadDictionaryValues(dictionaryId, query));
};
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)
@@ -914,6 +951,9 @@ const ProductEditPage = () => {
>
<Cascader
className="editor-standard-cascader"
classNames={{
popup: { root: 'editor-category-popup' },
}}
showSearch
placeholder="选择当前店铺可发布的产品类目"
options={categories.options}
@@ -1298,6 +1338,7 @@ const ProductEditPage = () => {
status: null,
},
offer_id: null,
media_asset_ids: [],
} satisfies VariantValues);
return (
<div className="sku-card-list">
@@ -1315,9 +1356,33 @@ const ProductEditPage = () => {
field.name,
]) as VariantValues;
const expanded = expandedVariantKeys.has(field.key);
const imageUrls = original
? variantImageUrls(collectionRecord, original)
const imageCandidates = original
? variantImageCandidates(collectionRecord, original)
: [];
const selectedAssetIds =
draftVariants[field.name]?.media_asset_ids ?? [];
const selectedImages = selectedAssetIds
.map(
(assetId) =>
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);
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
);
setDirty(true);
};
const copyVariant = () =>
add({
...variantValue,
@@ -1328,6 +1393,7 @@ const ProductEditPage = () => {
? `${variantValue.display_name} 副本`
: 'SKU 副本',
offer_id: null,
media_asset_ids: [],
});
const toggle = () =>
setExpandedVariantKeys((previous) => {
@@ -1357,6 +1423,12 @@ const ProductEditPage = () => {
>
<Input />
</Form.Item>
<Form.Item
name={[field.name, 'media_asset_ids']}
hidden
>
<Input />
</Form.Item>
<div className="sku-card-top">
<span className="sku-card-index">
{index + 1}
@@ -1420,10 +1492,10 @@ const ProductEditPage = () => {
<div className="sku-card-field sku-image-field">
<span>主图</span>
<div className="sku-main-thumbnail">
{imageUrls[0] ? (
{selectedImages[0] ? (
<Image
preview={false}
src={imageUrls[0]}
src={selectedImages[0]}
alt="SKU 主图"
/>
) : (
@@ -1500,38 +1572,67 @@ const ProductEditPage = () => {
<Typography.Title level={5}>
SKU 图片
</Typography.Title>
{imageUrls.length ? (
{imageCandidates.length ? (
<div className="sku-media-grid">
<div className="sku-media-item main">
<Image
src={imageUrls[0]}
alt="SKU 主图"
/>
</div>
<div className="sku-media-secondary">
{imageUrls
.slice(1, 5)
.map(
(
url,
imageIndex
) => (
<div
className="sku-media-item"
key={
url
{imageCandidates.map((candidate) => {
const selectable =
candidate.status === 'succeeded' &&
!!candidate.asset_id;
const selected =
selectable &&
selectedAssetIds.includes(
candidate.asset_id!
);
return (
<div
className={`sku-media-item ${selected ? 'selected' : ''}`}
key={candidate.id}
>
<Image
src={candidate.source_url}
alt="采集候选图片"
/>
{selectable ? (
<Button
size="small"
type={selected ? 'primary' : 'default'}
onClick={() =>
toggleImage(
candidate.asset_id!
)
}
>
<Image
src={
url
}
alt={`SKU 图片 ${imageIndex + 2}`}
/>
</div>
)
)}
</div>
{selected ? '已选择' : '选择'}
</Button>
) : (
<Button
size="small"
loading={
materializingTransferId ===
candidate.id
}
disabled={
candidate.status === 'queued' ||
candidate.status === 'running' ||
candidate.status === 'failed'
}
onClick={() =>
void materializeCandidate(
candidate.id
)
}
>
{candidate.status ===
'pending_selection'
? '转存后选择'
: candidate.status === 'failed'
? '转存失败'
: '转存中'}
</Button>
)}
</div>
);
})}
</div>
) : (
<div className="sku-media-empty">
@@ -1679,7 +1780,7 @@ const ProductEditPage = () => {
/>
<div className="editor-form-grid">
<Form.Item label="货源链接" className="span-two">
<Space.Compact block>
<Space.Compact block className="supply-link-control">
<Form.Item
name={['common', 'purchase_url']}
noStyle