优化商品匹配配置
This commit is contained in:
@@ -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.relSkuId,0 自动忽略" />
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user