fix(edit): 类目问题修改
This commit is contained in:
@@ -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%;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user