feat(fulfillment): 增加统一渠道配置搜索与筛选
This commit is contained in:
@@ -249,6 +249,8 @@ router.get(
|
||||
(req) =>
|
||||
listChannelRulesPage({
|
||||
channel: String(req.query.channel || ''),
|
||||
source: String(req.query.source || ''),
|
||||
keyword: String(req.query.keyword || ''),
|
||||
page: Number(req.query.page),
|
||||
pageSize: Number(req.query.pageSize),
|
||||
...(req.query.enabled === undefined
|
||||
|
||||
@@ -1098,6 +1098,8 @@ export async function assignChannelBySku(input: {
|
||||
|
||||
type ChannelRuleFilterInput = {
|
||||
channel?: string
|
||||
source?: string
|
||||
keyword?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
@@ -1108,6 +1110,20 @@ function buildChannelRuleFilter(input: ChannelRuleFilterInput) {
|
||||
params.push(input.channel)
|
||||
clauses.push(`channel = $${params.length}`)
|
||||
}
|
||||
const source = String(input.source || '').trim()
|
||||
if (source === 'rule') {
|
||||
clauses.push(`source LIKE 'rule:%'`)
|
||||
} else if (source) {
|
||||
params.push(source)
|
||||
clauses.push(`source = $${params.length}`)
|
||||
}
|
||||
const keyword = String(input.keyword || '').trim()
|
||||
if (keyword) {
|
||||
params.push(`%${keyword}%`)
|
||||
clauses.push(
|
||||
`(match_key ILIKE $${params.length} OR config_json::text ILIKE $${params.length} OR COALESCE(note, '') ILIKE $${params.length})`,
|
||||
)
|
||||
}
|
||||
if (typeof input.enabled === 'boolean') {
|
||||
params.push(input.enabled)
|
||||
clauses.push(`enabled = $${params.length}`)
|
||||
|
||||
@@ -401,6 +401,15 @@ test(
|
||||
assert.equal(firstRulePage.pageSize, 2)
|
||||
assert.ok(firstRulePage.items.length <= 2)
|
||||
assert.equal(firstRulePage.total, listedRules.length)
|
||||
const searchedRules = await listChannelRulesPage({
|
||||
keyword: voucherSku,
|
||||
source: 'operator',
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
assert.ok(searchedRules.items.some((rule) => rule.id === savedRule.id))
|
||||
assert.ok(searchedRules.items.every((rule) => rule.source === 'operator'))
|
||||
const deletedRules = await deleteChannelRules([savedRule.id])
|
||||
assert.equal(deletedRules.deletedCount, 1)
|
||||
assert.deepEqual(deletedRules.deletedIds, [savedRule.id])
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DeleteOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
Switch,
|
||||
Tabs,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
@@ -77,6 +79,16 @@ const CHANNEL_SOURCE_LABEL: Record<string, string> = {
|
||||
backfill: '历史回填',
|
||||
}
|
||||
|
||||
const CHANNEL_SOURCE_FILTER_OPTIONS = [
|
||||
...Object.entries(CHANNEL_SOURCE_LABEL).map(([value, label]) => ({ value, label })),
|
||||
{ value: 'rule', label: '规则迁移' },
|
||||
]
|
||||
|
||||
const ENABLED_FILTER_OPTIONS = [
|
||||
{ value: 'true', label: '启用' },
|
||||
{ value: 'false', label: '停用' },
|
||||
]
|
||||
|
||||
const MATCH_KEY_OPTIONS = [{ value: 'internal_sku', label: '内部 SKU' }]
|
||||
|
||||
const INTERNAL_SKU_MAPPING_TYPES: Array<{
|
||||
@@ -509,13 +521,34 @@ function RulesPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedRuleIds, setSelectedRuleIds] = useState<number[]>([])
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [channelFilter, setChannelFilter] = useState('')
|
||||
const [sourceFilter, setSourceFilter] = useState('')
|
||||
const [enabledFilter, setEnabledFilter] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [form] = Form.useForm()
|
||||
const hasActiveFilters = Boolean(keyword || channelFilter || sourceFilter || enabledFilter)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['fulfillment-rules', page, pageSize],
|
||||
queryFn: () => fetchChannelRules({ page, pageSize }),
|
||||
queryKey: [
|
||||
'fulfillment-rules',
|
||||
keyword,
|
||||
channelFilter,
|
||||
sourceFilter,
|
||||
enabledFilter,
|
||||
page,
|
||||
pageSize,
|
||||
],
|
||||
queryFn: () =>
|
||||
fetchChannelRules({
|
||||
keyword,
|
||||
channel: channelFilter,
|
||||
source: sourceFilter,
|
||||
...(enabledFilter ? { enabled: enabledFilter === 'true' } : {}),
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
})
|
||||
const ruleCount = query.data?.data?.total || 0
|
||||
|
||||
@@ -549,6 +582,15 @@ function RulesPanel() {
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '删除规则失败'),
|
||||
})
|
||||
|
||||
function resetFilters() {
|
||||
setKeyword('')
|
||||
setChannelFilter('')
|
||||
setSourceFilter('')
|
||||
setEnabledFilter('')
|
||||
setPage(1)
|
||||
setSelectedRuleIds([])
|
||||
}
|
||||
|
||||
function confirmDeleteRules(ids: number[]) {
|
||||
if (ids.length === 0) return
|
||||
Modal.confirm({
|
||||
@@ -603,17 +645,26 @@ function RulesPanel() {
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
render: (_: unknown, row: ChannelRule) => (
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
loading={deleteMutation.isPending}
|
||||
disabled={!canDeleteChannelRule(row)}
|
||||
onClick={() => confirmDeleteRules([row.id])}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
render: (_: unknown, row: ChannelRule) => {
|
||||
const button = (
|
||||
<Button
|
||||
danger
|
||||
size="small"
|
||||
loading={deleteMutation.isPending}
|
||||
disabled={!canDeleteChannelRule(row)}
|
||||
onClick={() => confirmDeleteRules([row.id])}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)
|
||||
return canDeleteChannelRule(row) ? (
|
||||
button
|
||||
) : (
|
||||
<Tooltip title="系统生成或由来源配置维护,不能在统一渠道页直接删除;配置有误时请新增同 SKU 的正确规则覆盖。">
|
||||
<span>{button}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -643,7 +694,62 @@ function RulesPanel() {
|
||||
<Typography.Paragraph type="secondary">
|
||||
配置按内部 SKU 全局生效,会同时影响所有平台和店铺。每条内部 SKU 只能有一个主渠道;
|
||||
其他渠道请设置 90 以上优先级作为兜底。接单平台的具体模板由统一配置中的 workRuleKey 控制。
|
||||
来源为商品能力迁移、人工配置、卡券或历史回填的系统规则受保护,配置有误时请新增同 SKU
|
||||
的正确规则覆盖。
|
||||
</Typography.Paragraph>
|
||||
<Space wrap style={{ marginBottom: 12 }}>
|
||||
<Input
|
||||
allowClear
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="搜索内部 SKU、载体参数或备注"
|
||||
value={keyword}
|
||||
onChange={(event) => {
|
||||
setKeyword(event.target.value)
|
||||
setPage(1)
|
||||
setSelectedRuleIds([])
|
||||
}}
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="全部渠道"
|
||||
value={channelFilter || undefined}
|
||||
onChange={(value) => {
|
||||
setChannelFilter(value || '')
|
||||
setPage(1)
|
||||
setSelectedRuleIds([])
|
||||
}}
|
||||
options={CHANNEL_OPTIONS}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="全部来源"
|
||||
value={sourceFilter || undefined}
|
||||
onChange={(value) => {
|
||||
setSourceFilter(value || '')
|
||||
setPage(1)
|
||||
setSelectedRuleIds([])
|
||||
}}
|
||||
options={CHANNEL_SOURCE_FILTER_OPTIONS}
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="全部状态"
|
||||
value={enabledFilter || undefined}
|
||||
onChange={(value) => {
|
||||
setEnabledFilter(value || '')
|
||||
setPage(1)
|
||||
setSelectedRuleIds([])
|
||||
}}
|
||||
options={ENABLED_FILTER_OPTIONS}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<Button disabled={!hasActiveFilters} onClick={resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
<PercentTable
|
||||
rowKey="id"
|
||||
size="small"
|
||||
|
||||
@@ -165,7 +165,14 @@ export function switchChannelForItems(payload: {
|
||||
}
|
||||
|
||||
export function fetchChannelRules(
|
||||
params: { channel?: string; page?: number; pageSize?: number } = {},
|
||||
params: {
|
||||
channel?: string
|
||||
source?: string
|
||||
keyword?: string
|
||||
enabled?: boolean
|
||||
page?: number
|
||||
pageSize?: number
|
||||
} = {},
|
||||
) {
|
||||
return apiGet<ChannelRuleListResult>('/api/v1/admin/fulfillment/rules', params)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user