fix(admin): 修复履约渠道分页并统一列表分页状态
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
assignChannelToOrderItems,
|
||||
deleteChannelRule,
|
||||
deleteChannelRules,
|
||||
listChannelRules,
|
||||
listChannelRulesPage,
|
||||
listPendingChannelGroups,
|
||||
switchChannelForOrderItems,
|
||||
unlockChannel,
|
||||
@@ -179,8 +179,10 @@ router.get(
|
||||
requireAdminPermission('finance.view'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
listChannelRules({
|
||||
listChannelRulesPage({
|
||||
channel: String(req.query.channel || ''),
|
||||
page: Number(req.query.page),
|
||||
pageSize: Number(req.query.pageSize),
|
||||
...(req.query.enabled === undefined
|
||||
? {}
|
||||
: { enabled: String(req.query.enabled) !== 'false' }),
|
||||
|
||||
@@ -63,6 +63,13 @@ export type ChannelRule = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ChannelRulePage = {
|
||||
items: ChannelRule[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type ChannelCandidateItem = {
|
||||
id: number
|
||||
provider: string
|
||||
@@ -1033,12 +1040,12 @@ export async function assignChannelBySku(input: {
|
||||
})
|
||||
}
|
||||
|
||||
export async function listChannelRules(
|
||||
input: {
|
||||
channel?: string
|
||||
enabled?: boolean
|
||||
} = {},
|
||||
): Promise<ChannelRule[]> {
|
||||
type ChannelRuleFilterInput = {
|
||||
channel?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
function buildChannelRuleFilter(input: ChannelRuleFilterInput) {
|
||||
const params: unknown[] = []
|
||||
const clauses: string[] = []
|
||||
if (input.channel) {
|
||||
@@ -1050,25 +1057,70 @@ export async function listChannelRules(
|
||||
clauses.push(`enabled = $${params.length}`)
|
||||
}
|
||||
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : ''
|
||||
const rows = await query<ChannelRule>(
|
||||
`
|
||||
SELECT id, provider, shop_id, match_key_type, match_key, channel, priority, enabled
|
||||
FROM (
|
||||
SELECT id, ''::text AS provider, ''::text AS shop_id,
|
||||
'internal_sku'::text AS match_key_type, internal_sku AS match_key,
|
||||
channel, priority, enabled
|
||||
FROM internal_sku_channels
|
||||
) internal_rules
|
||||
${where}
|
||||
ORDER BY priority, channel, match_key
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return rows.rows.map((row) => ({
|
||||
return { where, params }
|
||||
}
|
||||
|
||||
const CHANNEL_RULE_FROM_SQL = `
|
||||
FROM (
|
||||
SELECT id, ''::text AS provider, ''::text AS shop_id,
|
||||
'internal_sku'::text AS match_key_type, internal_sku AS match_key,
|
||||
channel, priority, enabled
|
||||
FROM internal_sku_channels
|
||||
) internal_rules
|
||||
`
|
||||
|
||||
function mapChannelRule(row: ChannelRule): ChannelRule {
|
||||
return {
|
||||
...row,
|
||||
id: Number(row.id),
|
||||
priority: Number(row.priority),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export async function listChannelRules(input: ChannelRuleFilterInput = {}): Promise<ChannelRule[]> {
|
||||
const { where, params } = buildChannelRuleFilter(input)
|
||||
const rows = await query<ChannelRule>(
|
||||
`
|
||||
SELECT id, provider, shop_id, match_key_type, match_key, channel, priority, enabled
|
||||
${CHANNEL_RULE_FROM_SQL}
|
||||
${where}
|
||||
ORDER BY priority, channel, match_key, id
|
||||
`,
|
||||
params,
|
||||
)
|
||||
return rows.rows.map(mapChannelRule)
|
||||
}
|
||||
|
||||
export async function listChannelRulesPage(
|
||||
input: ChannelRuleFilterInput & { page?: number; pageSize?: number } = {},
|
||||
): Promise<ChannelRulePage> {
|
||||
const page = Math.max(1, Math.trunc(Number(input.page) || 1))
|
||||
const pageSize = Math.min(100, Math.max(1, Math.trunc(Number(input.pageSize) || 20)))
|
||||
const offset = (page - 1) * pageSize
|
||||
const { where, params } = buildChannelRuleFilter(input)
|
||||
const pageParams = [...params, pageSize, offset]
|
||||
const [countResult, rows] = await Promise.all([
|
||||
query<{ total: number }>(
|
||||
`SELECT COUNT(*)::int AS total ${CHANNEL_RULE_FROM_SQL} ${where}`,
|
||||
params,
|
||||
),
|
||||
query<ChannelRule>(
|
||||
`
|
||||
SELECT id, provider, shop_id, match_key_type, match_key, channel, priority, enabled
|
||||
${CHANNEL_RULE_FROM_SQL}
|
||||
${where}
|
||||
ORDER BY priority, channel, match_key, id
|
||||
LIMIT $${pageParams.length - 1} OFFSET $${pageParams.length}
|
||||
`,
|
||||
pageParams,
|
||||
),
|
||||
])
|
||||
return {
|
||||
items: rows.rows.map(mapChannelRule),
|
||||
total: Number(countResult.rows[0]?.total || 0),
|
||||
page,
|
||||
pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
export async function upsertChannelRule(input: {
|
||||
|
||||
@@ -23,6 +23,7 @@ test(
|
||||
deleteChannelRules,
|
||||
ensureVoucherRuleForSku,
|
||||
listChannelRules,
|
||||
listChannelRulesPage,
|
||||
switchChannelForOrderItems,
|
||||
upsertChannelRule,
|
||||
} = await import('./channel-assign-service.js')
|
||||
@@ -171,6 +172,16 @@ test(
|
||||
assert.ok(
|
||||
listedRules.some((rule) => rule.id === savedRule.id && rule.match_key === voucherSku),
|
||||
)
|
||||
const firstRulePage = await listChannelRulesPage({
|
||||
channel: 'third_party_voucher',
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 2,
|
||||
})
|
||||
assert.equal(firstRulePage.page, 1)
|
||||
assert.equal(firstRulePage.pageSize, 2)
|
||||
assert.ok(firstRulePage.items.length <= 2)
|
||||
assert.equal(firstRulePage.total, listedRules.length)
|
||||
const deletedRules = await deleteChannelRules([savedRule.id])
|
||||
assert.equal(deletedRules.deletedCount, 1)
|
||||
assert.deepEqual(deletedRules.deletedIds, [savedRule.id])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Table as AntTable } from 'antd'
|
||||
import type { TableProps } from 'antd'
|
||||
import type { TablePaginationConfig, TableProps } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { normalizePercentColumns } from '@/utils/percent-table-layout'
|
||||
|
||||
@@ -12,7 +13,56 @@ import { normalizePercentColumns } from '@/utils/percent-table-layout'
|
||||
export default function PercentTable<RecordType extends object = Record<string, unknown>>(
|
||||
props: TableProps<RecordType>,
|
||||
) {
|
||||
const { columns, scroll, tableLayout: _tableLayout, ...rest } = props
|
||||
const { columns, scroll, tableLayout: _tableLayout, pagination, dataSource, ...rest } = props
|
||||
const paginationConfig = pagination && typeof pagination === 'object' ? pagination : null
|
||||
const paginationIsExternallyControlled = paginationConfig?.current !== undefined
|
||||
// 本地分页也必须由组件持有 current;只交给 AntD 内部状态时,父级重渲染会丢失翻页。
|
||||
const [localPagination, setLocalPagination] = useState(() => ({
|
||||
current: 1,
|
||||
pageSize: Number(paginationConfig?.defaultPageSize || paginationConfig?.pageSize || 10),
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
if (paginationIsExternallyControlled || !paginationConfig?.pageSize) return
|
||||
const nextPageSize = Number(paginationConfig.pageSize)
|
||||
if (!Number.isFinite(nextPageSize) || nextPageSize <= 0) return
|
||||
setLocalPagination((current) =>
|
||||
current.pageSize === nextPageSize ? current : { current: 1, pageSize: nextPageSize },
|
||||
)
|
||||
}, [paginationConfig?.pageSize, paginationIsExternallyControlled])
|
||||
|
||||
useEffect(() => {
|
||||
if (paginationIsExternallyControlled || !paginationConfig) return
|
||||
const total = Math.max(0, Number(paginationConfig.total ?? dataSource?.length ?? 0))
|
||||
const maxPage = Math.max(1, Math.ceil(total / Math.max(1, localPagination.pageSize)))
|
||||
if (localPagination.current > maxPage) {
|
||||
setLocalPagination((current) => ({ ...current, current: maxPage }))
|
||||
}
|
||||
}, [
|
||||
dataSource?.length,
|
||||
localPagination.current,
|
||||
localPagination.pageSize,
|
||||
paginationConfig,
|
||||
paginationIsExternallyControlled,
|
||||
])
|
||||
|
||||
const resolvedPagination: TablePaginationConfig | false | undefined = paginationConfig
|
||||
? paginationIsExternallyControlled
|
||||
? paginationConfig
|
||||
: {
|
||||
...paginationConfig,
|
||||
current: localPagination.current,
|
||||
pageSize: localPagination.pageSize,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
const normalizedPageSize = Math.max(1, Number(nextPageSize) || localPagination.pageSize)
|
||||
setLocalPagination({
|
||||
current: normalizedPageSize === localPagination.pageSize ? nextPage : 1,
|
||||
pageSize: normalizedPageSize,
|
||||
})
|
||||
paginationConfig.onChange?.(nextPage, nextPageSize)
|
||||
},
|
||||
}
|
||||
: pagination
|
||||
const reservedPercent =
|
||||
(props.rowSelection ? 4 : 0) +
|
||||
(props.expandable && typeof props.expandable === 'object' ? 4 : 0)
|
||||
@@ -24,7 +74,9 @@ export default function PercentTable<RecordType extends object = Record<string,
|
||||
return (
|
||||
<AntTable<RecordType>
|
||||
{...rest}
|
||||
dataSource={dataSource}
|
||||
columns={normalizedColumns}
|
||||
pagination={resolvedPagination}
|
||||
tableLayout="fixed"
|
||||
scroll={{ ...scrollConfig, x: '100%' }}
|
||||
/>
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
message,
|
||||
} from 'antd'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import FinanceSubNav from '@/components/admin/FinanceSubNav'
|
||||
@@ -48,11 +48,7 @@ import {
|
||||
} from '@/services/admin'
|
||||
import { formatRedeemAmount } from '@/utils/redeem'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import {
|
||||
ADMIN_DEFAULT_PAGE_SIZE,
|
||||
buildAdminLocalTablePagination,
|
||||
buildAdminTablePagination,
|
||||
} from '@/utils/admin-pagination'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
|
||||
const CHANNEL_OPTIONS = [
|
||||
{ value: 'work_order', label: '接单平台' },
|
||||
@@ -251,12 +247,20 @@ function RulesPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedRuleIds, setSelectedRuleIds] = useState<number[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['fulfillment-rules'],
|
||||
queryFn: () => fetchChannelRules(),
|
||||
queryKey: ['fulfillment-rules', page, pageSize],
|
||||
queryFn: () => fetchChannelRules({ page, pageSize }),
|
||||
})
|
||||
const ruleCount = query.data?.data?.total || 0
|
||||
|
||||
useEffect(() => {
|
||||
const maxPage = Math.max(1, Math.ceil(ruleCount / pageSize))
|
||||
if (page > maxPage) setPage(maxPage)
|
||||
}, [page, pageSize, ruleCount])
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (values: Record<string, unknown>) => saveChannelRule(values as never),
|
||||
@@ -339,7 +343,7 @@ function RulesPanel() {
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={`统一渠道配置(${query.data?.data?.length ?? 0})`}
|
||||
title={`统一渠道配置(${ruleCount})`}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
@@ -368,13 +372,21 @@ function RulesPanel() {
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={query.isFetching}
|
||||
dataSource={query.data?.data || []}
|
||||
dataSource={query.data?.data?.items || []}
|
||||
columns={columns}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedRuleIds,
|
||||
onChange: (keys) => setSelectedRuleIds(keys.map(Number)),
|
||||
}}
|
||||
pagination={buildAdminLocalTablePagination()}
|
||||
pagination={buildAdminTablePagination({
|
||||
current: query.data?.data?.page || page,
|
||||
pageSize: query.data?.data?.pageSize || pageSize,
|
||||
total: ruleCount,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(nextPageSize !== pageSize ? 1 : nextPage)
|
||||
setPageSize(nextPageSize)
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -31,6 +31,13 @@ export type ChannelRule = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ChannelRuleListResult = {
|
||||
items: ChannelRule[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export type ChannelException = {
|
||||
id: number
|
||||
reason: string
|
||||
@@ -127,8 +134,10 @@ export function switchChannelForItems(payload: {
|
||||
}>('/api/v1/admin/fulfillment/switch', payload)
|
||||
}
|
||||
|
||||
export function fetchChannelRules(params: { channel?: string } = {}) {
|
||||
return apiGet<ChannelRule[]>('/api/v1/admin/fulfillment/rules', params)
|
||||
export function fetchChannelRules(
|
||||
params: { channel?: string; page?: number; pageSize?: number } = {},
|
||||
) {
|
||||
return apiGet<ChannelRuleListResult>('/api/v1/admin/fulfillment/rules', params)
|
||||
}
|
||||
|
||||
export function saveChannelRule(payload: {
|
||||
|
||||
Reference in New Issue
Block a user