优化商品匹配配置
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Checkbox,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
@@ -43,6 +44,7 @@ type MappingFormValues = {
|
|||||||
mappingId?: number
|
mappingId?: number
|
||||||
ruleId: number
|
ruleId: number
|
||||||
sellerIds: string[]
|
sellerIds: string[]
|
||||||
|
productScope: 'all_products' | 'selected_products'
|
||||||
relItemId?: string
|
relItemId?: string
|
||||||
itemTitle?: string
|
itemTitle?: string
|
||||||
relSkuId?: string
|
relSkuId?: string
|
||||||
@@ -51,6 +53,11 @@ type MappingFormValues = {
|
|||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SellerOption = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProductMatchPanel() {
|
export default function ProductMatchPanel() {
|
||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -99,7 +106,11 @@ export default function ProductMatchPanel() {
|
|||||||
|
|
||||||
function resetMappingForm() {
|
function resetMappingForm() {
|
||||||
mappingForm.resetFields()
|
mappingForm.resetFields()
|
||||||
mappingForm.setFieldsValue({ mappingType: 'product_default', enabled: true })
|
mappingForm.setFieldsValue({
|
||||||
|
mappingType: 'product_default',
|
||||||
|
productScope: 'selected_products',
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillMappingFromSource(
|
function fillMappingFromSource(
|
||||||
@@ -109,6 +120,7 @@ export default function ProductMatchPanel() {
|
|||||||
mappingForm.setFieldsValue({
|
mappingForm.setFieldsValue({
|
||||||
mappingId: undefined,
|
mappingId: undefined,
|
||||||
sellerIds: [source.sellerId],
|
sellerIds: [source.sellerId],
|
||||||
|
productScope: mappingType === 'product_default' ? 'selected_products' : 'all_products',
|
||||||
relItemId: mappingType === 'product_default' ? source.relItemId : '',
|
relItemId: mappingType === 'product_default' ? source.relItemId : '',
|
||||||
itemTitle: mappingType === 'product_default' ? source.itemTitle : '',
|
itemTitle: mappingType === 'product_default' ? source.itemTitle : '',
|
||||||
relSkuId: mappingType === 'sku_exact' ? source.relSkuId : '',
|
relSkuId: mappingType === 'sku_exact' ? source.relSkuId : '',
|
||||||
@@ -128,6 +140,7 @@ export default function ProductMatchPanel() {
|
|||||||
mappingId: mapping.mappingId,
|
mappingId: mapping.mappingId,
|
||||||
ruleId: mapping.ruleId,
|
ruleId: mapping.ruleId,
|
||||||
sellerIds: mapping.sellerIds,
|
sellerIds: mapping.sellerIds,
|
||||||
|
productScope: mapping.relItemId || mapping.itemTitle ? 'selected_products' : 'all_products',
|
||||||
relItemId: mapping.relItemId,
|
relItemId: mapping.relItemId,
|
||||||
itemTitle: mapping.itemTitle,
|
itemTitle: mapping.itemTitle,
|
||||||
relSkuId: mapping.relSkuId,
|
relSkuId: mapping.relSkuId,
|
||||||
@@ -139,7 +152,8 @@ export default function ProductMatchPanel() {
|
|||||||
|
|
||||||
async function saveMapping(values: MappingFormValues) {
|
async function saveMapping(values: MappingFormValues) {
|
||||||
try {
|
try {
|
||||||
await saveAdminWorkProductRuleMapping(values)
|
const { productScope: _productScope, ...payload } = values
|
||||||
|
await saveAdminWorkProductRuleMapping(payload)
|
||||||
message.success(values.mappingId ? '商品映射已更新' : '商品映射已创建')
|
message.success(values.mappingId ? '商品映射已更新' : '商品映射已创建')
|
||||||
resetMappingForm()
|
resetMappingForm()
|
||||||
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-mappings'] })
|
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-mappings'] })
|
||||||
@@ -149,6 +163,16 @@ export default function ProductMatchPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMappingValuesChange(changedValues: Partial<MappingFormValues>) {
|
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 (
|
if (
|
||||||
changedValues.sellerIds &&
|
changedValues.sellerIds &&
|
||||||
changedValues.sellerIds.length > 1 &&
|
changedValues.sellerIds.length > 1 &&
|
||||||
@@ -187,15 +211,15 @@ export default function ProductMatchPanel() {
|
|||||||
key: 'product',
|
key: 'product',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text strong ellipsis={{ tooltip: row.itemTitle }}>
|
<Typography.Text strong ellipsis={{ tooltip: mappingProductScopeTooltip(row) }}>
|
||||||
{row.itemTitle || row.relItemId}
|
{mappingProductScopeLabel(row)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
店铺{' '}
|
店铺{' '}
|
||||||
{row.sellerIds
|
{row.sellerIds
|
||||||
.map((sellerId) => formatShopLabel(sellerId, shopNameBySellerId))
|
.map((sellerId) => formatShopLabel(sellerId, shopNameBySellerId))
|
||||||
.join('、')}{' '}
|
.join('、')}{' '}
|
||||||
{row.relItemId ? `· 商品 ${row.relItemId}` : ''}
|
· {mappingProductScopeLabel(row)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{row.mappingType !== 'product_default' ? (
|
{row.mappingType !== 'product_default' ? (
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
@@ -378,7 +402,11 @@ export default function ProductMatchPanel() {
|
|||||||
<Form
|
<Form
|
||||||
form={mappingForm}
|
form={mappingForm}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{ mappingType: 'product_default', enabled: true }}
|
initialValues={{
|
||||||
|
mappingType: 'product_default',
|
||||||
|
productScope: 'selected_products',
|
||||||
|
enabled: true,
|
||||||
|
}}
|
||||||
onValuesChange={handleMappingValuesChange}
|
onValuesChange={handleMappingValuesChange}
|
||||||
onFinish={saveMapping}
|
onFinish={saveMapping}
|
||||||
>
|
>
|
||||||
@@ -414,39 +442,63 @@ export default function ProductMatchPanel() {
|
|||||||
name="sellerIds"
|
name="sellerIds"
|
||||||
rules={[{ required: true, message: '至少选择一个店铺' }]}
|
rules={[{ required: true, message: '至少选择一个店铺' }]}
|
||||||
>
|
>
|
||||||
<Select
|
<ShopSelector options={sellerOptions} />
|
||||||
mode="tags"
|
|
||||||
tokenSeparators={[',', ',']}
|
|
||||||
placeholder="选择快手店铺,可多选"
|
|
||||||
options={sellerOptions}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="关联商品 ID" name="relItemId">
|
|
||||||
<Input.TextArea
|
|
||||||
rows={2}
|
|
||||||
placeholder="可填写多个;用逗号、中文逗号或换行分隔"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="快手大标题" name="itemTitle">
|
|
||||||
<Input placeholder="可选;商品默认必填,同名 SKU 分流时填写" />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item noStyle shouldUpdate>
|
<Form.Item noStyle shouldUpdate>
|
||||||
{({ getFieldValue }) =>
|
{({ getFieldValue }) => {
|
||||||
getFieldValue('mappingType') === 'sku_exact' ? (
|
const mappingType = getFieldValue('mappingType')
|
||||||
|
const isProductDefault = mappingType === 'product_default'
|
||||||
|
const productScope = getFieldValue('productScope')
|
||||||
|
const showProductScope =
|
||||||
|
isProductDefault || productScope === 'selected_products'
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item label="关联 SKU ID" name="relSkuId">
|
{!isProductDefault ? (
|
||||||
<Input placeholder="ext.relSkuId,0 自动忽略" />
|
<Form.Item label="商品范围" name="productScope">
|
||||||
</Form.Item>
|
<Select
|
||||||
<Form.Item label="具体 SKU 名称" name="skuNick">
|
options={[
|
||||||
<Input placeholder="ext.skuNick,完整精确匹配" />
|
{ value: 'all_products', label: '全部商品' },
|
||||||
</Form.Item>
|
{ value: 'selected_products', label: '指定商品' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
) : null}
|
||||||
|
{showProductScope ? (
|
||||||
|
<>
|
||||||
|
<Form.Item label="关联商品 ID" name="relItemId">
|
||||||
|
<Input.TextArea
|
||||||
|
rows={2}
|
||||||
|
placeholder="可填写多个;用逗号、中文逗号或换行分隔"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="快手大标题" name="itemTitle">
|
||||||
|
<Input
|
||||||
|
placeholder={
|
||||||
|
isProductDefault
|
||||||
|
? '商品默认规则需填写大标题或关联商品 ID'
|
||||||
|
: '可选;同名 SKU 需要按父商品分流时填写'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{mappingType === 'sku_exact' ? (
|
||||||
|
<>
|
||||||
|
<Form.Item label="关联 SKU ID" name="relSkuId">
|
||||||
|
<Input placeholder="ext.relSkuId,0 自动忽略" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="具体 SKU 名称" name="skuNick">
|
||||||
|
<Input placeholder="ext.skuNick,完整精确匹配" />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
) : mappingType === 'sku_series' ? (
|
||||||
|
<Form.Item label="SKU 系列名称" name="skuNick">
|
||||||
|
<Input placeholder="例如:指挥官密钥;自动覆盖 1个、10个等数量规格" />
|
||||||
|
</Form.Item>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : getFieldValue('mappingType') === 'sku_series' ? (
|
)
|
||||||
<Form.Item label="SKU 系列名称" name="skuNick">
|
}}
|
||||||
<Input placeholder="例如:指挥官密钥;自动覆盖 1个、10个等数量规格" />
|
|
||||||
</Form.Item>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
@@ -597,6 +649,54 @@ function MatchStatusTag({ status }: { status: string }) {
|
|||||||
return <Tag color={color}>{label}</Tag>
|
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) {
|
function normalizeSkuSeriesName(value: string) {
|
||||||
return value
|
return value
|
||||||
.normalize('NFKC')
|
.normalize('NFKC')
|
||||||
@@ -604,6 +704,18 @@ function normalizeSkuSeriesName(value: string) {
|
|||||||
.replace(/(?:\d+|[零一二三四五六七八九十百千两]+)(?:个|份|张|枚)/g, '')
|
.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[]) {
|
function createShopNameBySellerId(shops: AdminKuaishouIndustryShopOption[]) {
|
||||||
const names = new Map<string, string>()
|
const names = new Map<string, string>()
|
||||||
for (const shop of shops) {
|
for (const shop of shops) {
|
||||||
|
|||||||
Reference in New Issue
Block a user