优化商品匹配配置

This commit is contained in:
yml2213
2026-08-18 13:01:12 +08:00
parent f2c95bc3c8
commit 9ebc027b96
@@ -4,6 +4,7 @@ import {
App,
Button,
Card,
Checkbox,
Descriptions,
Form,
Input,
@@ -43,6 +44,7 @@ type MappingFormValues = {
mappingId?: number
ruleId: number
sellerIds: string[]
productScope: 'all_products' | 'selected_products'
relItemId?: string
itemTitle?: string
relSkuId?: string
@@ -51,6 +53,11 @@ type MappingFormValues = {
enabled?: boolean
}
type SellerOption = {
value: string
label: string
}
export default function ProductMatchPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
@@ -99,7 +106,11 @@ export default function ProductMatchPanel() {
function resetMappingForm() {
mappingForm.resetFields()
mappingForm.setFieldsValue({ mappingType: 'product_default', enabled: true })
mappingForm.setFieldsValue({
mappingType: 'product_default',
productScope: 'selected_products',
enabled: true,
})
}
function fillMappingFromSource(
@@ -109,6 +120,7 @@ export default function ProductMatchPanel() {
mappingForm.setFieldsValue({
mappingId: undefined,
sellerIds: [source.sellerId],
productScope: mappingType === 'product_default' ? 'selected_products' : 'all_products',
relItemId: mappingType === 'product_default' ? source.relItemId : '',
itemTitle: mappingType === 'product_default' ? source.itemTitle : '',
relSkuId: mappingType === 'sku_exact' ? source.relSkuId : '',
@@ -128,6 +140,7 @@ export default function ProductMatchPanel() {
mappingId: mapping.mappingId,
ruleId: mapping.ruleId,
sellerIds: mapping.sellerIds,
productScope: mapping.relItemId || mapping.itemTitle ? 'selected_products' : 'all_products',
relItemId: mapping.relItemId,
itemTitle: mapping.itemTitle,
relSkuId: mapping.relSkuId,
@@ -139,7 +152,8 @@ export default function ProductMatchPanel() {
async function saveMapping(values: MappingFormValues) {
try {
await saveAdminWorkProductRuleMapping(values)
const { productScope: _productScope, ...payload } = values
await saveAdminWorkProductRuleMapping(payload)
message.success(values.mappingId ? '商品映射已更新' : '商品映射已创建')
resetMappingForm()
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-mappings'] })
@@ -149,6 +163,16 @@ export default function ProductMatchPanel() {
}
function handleMappingValuesChange(changedValues: Partial<MappingFormValues>) {
if (changedValues.mappingType) {
const isProductDefault = changedValues.mappingType === 'product_default'
mappingForm.setFieldsValue({
productScope: isProductDefault ? 'selected_products' : 'all_products',
...(isProductDefault ? {} : { relItemId: '', itemTitle: '' }),
})
}
if (changedValues.productScope === 'all_products') {
mappingForm.setFieldsValue({ relItemId: '', itemTitle: '' })
}
if (
changedValues.sellerIds &&
changedValues.sellerIds.length > 1 &&
@@ -187,15 +211,15 @@ export default function ProductMatchPanel() {
key: 'product',
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong ellipsis={{ tooltip: row.itemTitle }}>
{row.itemTitle || row.relItemId}
<Typography.Text strong ellipsis={{ tooltip: mappingProductScopeTooltip(row) }}>
{mappingProductScopeLabel(row)}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{' '}
{row.sellerIds
.map((sellerId) => formatShopLabel(sellerId, shopNameBySellerId))
.join('、')}{' '}
{row.relItemId ? `· 商品 ${row.relItemId}` : ''}
· {mappingProductScopeLabel(row)}
</Typography.Text>
{row.mappingType !== 'product_default' ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
@@ -378,7 +402,11 @@ export default function ProductMatchPanel() {
<Form
form={mappingForm}
layout="vertical"
initialValues={{ mappingType: 'product_default', enabled: true }}
initialValues={{
mappingType: 'product_default',
productScope: 'selected_products',
enabled: true,
}}
onValuesChange={handleMappingValuesChange}
onFinish={saveMapping}
>
@@ -414,13 +442,29 @@ export default function ProductMatchPanel() {
name="sellerIds"
rules={[{ required: true, message: '至少选择一个店铺' }]}
>
<ShopSelector options={sellerOptions} />
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) => {
const mappingType = getFieldValue('mappingType')
const isProductDefault = mappingType === 'product_default'
const productScope = getFieldValue('productScope')
const showProductScope =
isProductDefault || productScope === 'selected_products'
return (
<>
{!isProductDefault ? (
<Form.Item label="商品范围" name="productScope">
<Select
mode="tags"
tokenSeparators={[',', '']}
placeholder="选择快手店铺,可多选"
options={sellerOptions}
options={[
{ value: 'all_products', label: '全部商品' },
{ value: 'selected_products', label: '指定商品' },
]}
/>
</Form.Item>
) : null}
{showProductScope ? (
<>
<Form.Item label="关联商品 ID" name="relItemId">
<Input.TextArea
rows={2}
@@ -428,11 +472,17 @@ export default function ProductMatchPanel() {
/>
</Form.Item>
<Form.Item label="快手大标题" name="itemTitle">
<Input placeholder="可选;商品默认必填,同名 SKU 分流时填写" />
<Input
placeholder={
isProductDefault
? '商品默认规则需填写大标题或关联商品 ID'
: '可选;同名 SKU 需要按父商品分流时填写'
}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) =>
getFieldValue('mappingType') === 'sku_exact' ? (
</>
) : null}
{mappingType === 'sku_exact' ? (
<>
<Form.Item label="关联 SKU ID" name="relSkuId">
<Input placeholder="ext.relSkuId0 自动忽略" />
@@ -441,12 +491,14 @@ export default function ProductMatchPanel() {
<Input placeholder="ext.skuNick,完整精确匹配" />
</Form.Item>
</>
) : getFieldValue('mappingType') === 'sku_series' ? (
) : mappingType === 'sku_series' ? (
<Form.Item label="SKU 系列名称" name="skuNick">
<Input placeholder="例如:指挥官密钥;自动覆盖 1个、10个等数量规格" />
</Form.Item>
) : null
}
) : null}
</>
)
}}
</Form.Item>
<Form.Item label="启用" name="enabled" valuePropName="checked">
<Switch />
@@ -597,6 +649,54 @@ function MatchStatusTag({ status }: { status: string }) {
return <Tag color={color}>{label}</Tag>
}
function ShopSelector({
value = [],
onChange,
options,
}: {
value?: string[]
onChange?: (nextValue: string[]) => void
options: SellerOption[]
}) {
const optionValues = options.map((option) => option.value)
const selectedValues = Array.isArray(value) ? value.map(String) : []
const selectedOptionValues = selectedValues.filter((sellerId) => optionValues.includes(sellerId))
const otherValues = selectedValues.filter((sellerId) => !optionValues.includes(sellerId))
const allSelected = optionValues.length > 0 && selectedOptionValues.length === optionValues.length
const partiallySelected = selectedOptionValues.length > 0 && !allSelected
return (
<div className="page-stack" style={{ gap: 8 }}>
<Checkbox
checked={allSelected}
disabled={optionValues.length === 0}
indeterminate={partiallySelected}
onChange={(event) =>
onChange?.(event.target.checked ? [...otherValues, ...optionValues] : otherValues)
}
>
{optionValues.length}
</Checkbox>
<Checkbox.Group
value={selectedValues}
onChange={(nextValues) => onChange?.(nextValues.map(String))}
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
gap: 8,
width: '100%',
}}
>
{options.map((option) => (
<Checkbox key={option.value} value={option.value}>
{option.label}
</Checkbox>
))}
</Checkbox.Group>
</div>
)
}
function normalizeSkuSeriesName(value: string) {
return value
.normalize('NFKC')
@@ -604,6 +704,18 @@ function normalizeSkuSeriesName(value: string) {
.replace(/(?:\d+|[零一二三四五六七八九十百千两]+)(?:个|份|张|枚)/g, '')
}
function mappingProductScopeLabel(mapping: WorkProductRuleMapping) {
if (mapping.itemTitle) return mapping.itemTitle
const productIds = String(mapping.relItemId || '')
.split(/[,;\s\n\r]+/)
.filter(Boolean)
return productIds.length > 0 ? `指定商品(${productIds.length} 个)` : '全部商品'
}
function mappingProductScopeTooltip(mapping: WorkProductRuleMapping) {
return mapping.itemTitle || mapping.relItemId || '覆盖店铺内的全部商品'
}
function createShopNameBySellerId(shops: AdminKuaishouIndustryShopOption[]) {
const names = new Map<string, string>()
for (const shop of shops) {