fix(admin): 订单列表默认近7天并按支付时间筛选
避免无日期过滤时展示全库,关键词搜索不受日期限制;SSE 插入裁剪并尊重筛选,新单入库优先使用平台支付时间。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,8 +15,8 @@ test('订单列表综合筛选下推到总数和分页查询', async () => {
|
|||||||
executorKey: 'affiliate_dash',
|
executorKey: 'affiliate_dash',
|
||||||
payStatus: 'paid',
|
payStatus: 'paid',
|
||||||
taskStatus: 'manual_review',
|
taskStatus: 'manual_review',
|
||||||
dateFrom: '2026-09-01T00:00:00.000Z',
|
dateFrom: '2026-09-01',
|
||||||
dateTo: '2026-09-02T23:59:59.999Z',
|
dateTo: '2026-09-02',
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.equal(result.total, 3)
|
assert.equal(result.total, 3)
|
||||||
@@ -24,25 +24,37 @@ test('订单列表综合筛选下推到总数和分页查询', async () => {
|
|||||||
assert.match(calls[0]?.sql || '', /o\.platform_order_id.*ILIKE \$1/s)
|
assert.match(calls[0]?.sql || '', /o\.platform_order_id.*ILIKE \$1/s)
|
||||||
assert.match(calls[0]?.sql || '', /oi\.sku_code.*oi\.sku_name.*ILIKE \$1/s)
|
assert.match(calls[0]?.sql || '', /oi\.sku_code.*oi\.sku_name.*ILIKE \$1/s)
|
||||||
assert.match(calls[0]?.sql || '', /kuaishouSendCode'.*itemTitle.*kuaishouSendCode'.*skuNick/s)
|
assert.match(calls[0]?.sql || '', /kuaishouSendCode'.*itemTitle.*kuaishouSendCode'.*skuNick/s)
|
||||||
assert.match(calls[0]?.sql || '', /o\.provider = \$2/)
|
assert.match(calls[0]?.sql || '', /o\.provider IN \('91kaquan', 'open_91', '91'\)/)
|
||||||
assert.match(calls[0]?.sql || '', /o\.pay_status = \$3/)
|
assert.match(calls[0]?.sql || '', /o\.pay_status = \$2/)
|
||||||
assert.match(calls[0]?.sql || '', /ft\.task_status = \$4/)
|
assert.match(calls[0]?.sql || '', /ft\.task_status = \$3/)
|
||||||
assert.match(calls[0]?.sql || '', /ft\.executor_key = \$5/)
|
assert.match(calls[0]?.sql || '', /ft\.executor_key = \$4/)
|
||||||
assert.match(calls[0]?.sql || '', /o\.created_at >= \$6/)
|
// 有关键词时不应用日期过滤
|
||||||
assert.match(calls[0]?.sql || '', /o\.created_at <= \$7/)
|
assert.doesNotMatch(calls[0]?.sql || '', /paid_at/i)
|
||||||
assert.deepEqual(calls[0]?.params, [
|
assert.deepEqual(calls[0]?.params, ['%皮肤店%', 'paid', 'manual_review', 'affiliate_dash'])
|
||||||
'%皮肤店%',
|
assert.match(calls[1]?.sql || '', /LIMIT \$5 OFFSET \$6/)
|
||||||
'91kaquan',
|
|
||||||
'paid',
|
|
||||||
'manual_review',
|
|
||||||
'affiliate_dash',
|
|
||||||
'2026-09-01T00:00:00.000Z',
|
|
||||||
'2026-09-02T23:59:59.999Z',
|
|
||||||
])
|
|
||||||
assert.match(calls[1]?.sql || '', /LIMIT \$8 OFFSET \$9/)
|
|
||||||
assert.deepEqual(calls[1]?.params, [...(calls[0]?.params || []), 10, 10])
|
assert.deepEqual(calls[1]?.params, [...(calls[0]?.params || []), 10, 10])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('订单列表无关键词时按支付时间与中国时区日期筛选', async () => {
|
||||||
|
const calls: Array<{ sql: string; params: unknown[] }> = []
|
||||||
|
|
||||||
|
await listOrdersWithExecutor(createExecutor(calls, 0), {
|
||||||
|
dateFrom: '2026-09-01',
|
||||||
|
dateTo: '2026-09-07',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.match(
|
||||||
|
calls[0]?.sql || '',
|
||||||
|
/COALESCE\(o\.paid_at, o\.created_at\) >= \(\$1::date\)::timestamp AT TIME ZONE 'Asia\/Shanghai'/,
|
||||||
|
)
|
||||||
|
assert.match(
|
||||||
|
calls[0]?.sql || '',
|
||||||
|
/COALESCE\(o\.paid_at, o\.created_at\) < \(\(\$2::date \+ 1\)::timestamp AT TIME ZONE 'Asia\/Shanghai'\)/,
|
||||||
|
)
|
||||||
|
assert.deepEqual(calls[0]?.params, ['2026-09-01', '2026-09-07'])
|
||||||
|
assert.match(calls[1]?.sql || '', /ORDER BY COALESCE\(o\.paid_at, o\.created_at\) DESC/)
|
||||||
|
})
|
||||||
|
|
||||||
test('订单列表保留订单号和精确 SKU 旧筛选参数兼容', async () => {
|
test('订单列表保留订单号和精确 SKU 旧筛选参数兼容', async () => {
|
||||||
const calls: Array<{ sql: string; params: unknown[] }> = []
|
const calls: Array<{ sql: string; params: unknown[] }> = []
|
||||||
|
|
||||||
|
|||||||
@@ -295,8 +295,7 @@ export async function listOrdersWithExecutor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (provider) {
|
if (provider) {
|
||||||
params.push(provider)
|
appendProviderFilter(filters, params, provider)
|
||||||
filters.push(`o.provider = $${params.length}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platformOrderId) {
|
if (platformOrderId) {
|
||||||
@@ -339,14 +338,20 @@ export async function listOrdersWithExecutor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateFrom) {
|
// 有关键词时不限日期,避免默认近 N 天挡住搜单号/商品。
|
||||||
params.push(dateFrom)
|
const applyDateFilter = Boolean(dateFrom || dateTo) && !String(keyword || '').trim()
|
||||||
filters.push(`o.created_at >= $${params.length}`)
|
if (applyDateFilter && dateFrom) {
|
||||||
|
params.push(String(dateFrom).trim().slice(0, 10))
|
||||||
|
filters.push(
|
||||||
|
`COALESCE(o.paid_at, o.created_at) >= ($${params.length}::date)::timestamp AT TIME ZONE 'Asia/Shanghai'`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateTo) {
|
if (applyDateFilter && dateTo) {
|
||||||
params.push(dateTo)
|
params.push(String(dateTo).trim().slice(0, 10))
|
||||||
filters.push(`o.created_at <= $${params.length}`)
|
filters.push(
|
||||||
|
`COALESCE(o.paid_at, o.created_at) < (($${params.length}::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai')`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
|
||||||
@@ -362,7 +367,7 @@ export async function listOrdersWithExecutor(
|
|||||||
SELECT o.*
|
SELECT o.*
|
||||||
FROM orders o
|
FROM orders o
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY o.id DESC
|
ORDER BY COALESCE(o.paid_at, o.created_at) DESC NULLS LAST, o.id DESC
|
||||||
LIMIT $${params.length - 1} OFFSET $${params.length}
|
LIMIT $${params.length - 1} OFFSET $${params.length}
|
||||||
`,
|
`,
|
||||||
params,
|
params,
|
||||||
@@ -373,3 +378,31 @@ export async function listOrdersWithExecutor(
|
|||||||
total: Number(totalResult.rows[0]?.total || 0),
|
total: Number(totalResult.rows[0]?.total || 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 来源渠道筛选:兼容历史 provider / platform 多种写法。 */
|
||||||
|
function appendProviderFilter(filters: string[], params: unknown[], provider: string) {
|
||||||
|
const normalized = String(provider || '').trim().toLowerCase()
|
||||||
|
if (!normalized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'pdd') {
|
||||||
|
filters.push(`(o.provider = 'pdd' OR o.platform = 'pdd')`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === '91' || normalized === '91kaquan' || normalized === 'open_91') {
|
||||||
|
filters.push(
|
||||||
|
`(o.provider IN ('91kaquan', 'open_91', '91') OR o.platform IN ('91kaquan', '91'))`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'kuaishou' || normalized === 'kuaishou-industry') {
|
||||||
|
filters.push(`(o.provider LIKE 'kuaishou%' OR o.platform LIKE 'kuaishou%')`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params.push(provider)
|
||||||
|
filters.push(`(o.provider = $${params.length} OR o.platform = $${params.length})`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { createHttpError } from '../../utils/http.js'
|
|||||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||||
import {
|
import {
|
||||||
normalizeDateQuery,
|
normalizeDateQuery,
|
||||||
|
normalizeDateString,
|
||||||
normalizePage,
|
normalizePage,
|
||||||
normalizePageSize,
|
normalizePageSize,
|
||||||
safeParseJson,
|
safeParseJson,
|
||||||
@@ -94,8 +95,8 @@ export async function getAdminOrders(
|
|||||||
payStatus: String(query.payStatus || '').trim(),
|
payStatus: String(query.payStatus || '').trim(),
|
||||||
taskStatus: String(query.taskStatus || '').trim(),
|
taskStatus: String(query.taskStatus || '').trim(),
|
||||||
skuCode: String(query.skuCode || '').trim(),
|
skuCode: String(query.skuCode || '').trim(),
|
||||||
dateFrom: normalizeDateQuery(query.dateFrom),
|
dateFrom: normalizeDateString(query.dateFrom),
|
||||||
dateTo: normalizeDateQuery(query.dateTo, true),
|
dateTo: normalizeDateString(query.dateTo),
|
||||||
})
|
})
|
||||||
|
|
||||||
const orderIds = items.map((item) => Number(item.id)).filter((id) => id > 0)
|
const orderIds = items.map((item) => Number(item.id)).filter((id) => id > 0)
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ export async function upsertOrderFromSource(
|
|||||||
: await createOrderWithExecutor(executor, {
|
: await createOrderWithExecutor(executor, {
|
||||||
...basePayload,
|
...basePayload,
|
||||||
...mergedPayload,
|
...mergedPayload,
|
||||||
createdAt: now,
|
// 优先用平台支付/成团时间,避免积压同步把历史单排成「刚刚入库」。
|
||||||
|
createdAt: String(event.paidAt || now).trim() || now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
} from '@/utils/admin-auth'
|
} from '@/utils/admin-auth'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||||
|
import dayjs from '@/lib/dayjs'
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSED_KEY = 'react-admin-sidebar-collapsed'
|
const SIDEBAR_COLLAPSED_KEY = 'react-admin-sidebar-collapsed'
|
||||||
const NOTIFICATION_SOUND_KEY = 'admin-notification-sound-enabled'
|
const NOTIFICATION_SOUND_KEY = 'admin-notification-sound-enabled'
|
||||||
@@ -493,35 +494,82 @@ function patchAdminOrderCaches(
|
|||||||
queryClient: ReturnType<typeof useQueryClient>,
|
queryClient: ReturnType<typeof useQueryClient>,
|
||||||
event: RealtimeEvent,
|
event: RealtimeEvent,
|
||||||
) {
|
) {
|
||||||
const item = event.data as { orderId?: unknown; platformOrderId?: unknown; payStatus?: unknown }
|
const item = event.data as {
|
||||||
|
orderId?: unknown
|
||||||
|
platformOrderId?: unknown
|
||||||
|
payStatus?: unknown
|
||||||
|
paidAt?: unknown
|
||||||
|
createdAt?: unknown
|
||||||
|
provider?: unknown
|
||||||
|
shopName?: unknown
|
||||||
|
itemTitle?: unknown
|
||||||
|
itemSummary?: unknown
|
||||||
|
}
|
||||||
for (const [queryKey, current] of queryClient.getQueriesData({ queryKey: ['admin-orders'] })) {
|
for (const [queryKey, current] of queryClient.getQueriesData({ queryKey: ['admin-orders'] })) {
|
||||||
const params = isRecord(queryKey[1]) ? queryKey[1] : {}
|
const params = isRecord(queryKey[1]) ? queryKey[1] : {}
|
||||||
const page = Number(params.page || 1)
|
const page = Number(params.page || 1)
|
||||||
const matches =
|
const pageSize = Math.max(1, Number(params.pageSize || 20) || 20)
|
||||||
(!String(params.platformOrderId || '').trim() ||
|
const keyword = String(params.keyword || '').trim()
|
||||||
String(item.platformOrderId || '') === String(params.platformOrderId || '').trim()) &&
|
const matchesFilters =
|
||||||
|
matchesAdminOrderKeyword(item, keyword) &&
|
||||||
|
matchesAdminOrderProvider(item, String(params.provider || '').trim()) &&
|
||||||
(!String(params.payStatus || '').trim() ||
|
(!String(params.payStatus || '').trim() ||
|
||||||
String(item.payStatus || '') === String(params.payStatus || '').trim())
|
String(item.payStatus || '') === String(params.payStatus || '').trim()) &&
|
||||||
const shouldInsert = event.operation === 'upsert' && page === 1 && matches && !params.skuCode
|
// 有关键词时不限日期;否则实时插入也要落在当前日期筛选内。
|
||||||
const operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert'
|
(keyword ? true : matchesAdminOrderDateRange(item, params))
|
||||||
|
const shouldInsert =
|
||||||
|
event.operation === 'upsert' && page === 1 && matchesFilters && !params.skuCode
|
||||||
|
const operation = event.operation === 'remove' || !matchesFilters ? 'remove' : 'upsert'
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
queryKey,
|
queryKey,
|
||||||
patchAdminOrderListEnvelope(current, { ...event, operation }, shouldInsert),
|
patchAdminOrderListEnvelope(current, { ...event, operation }, shouldInsert, pageSize),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchAdminOrderListEnvelope(current: unknown, event: RealtimeEvent, insert = false) {
|
function patchAdminOrderListEnvelope(
|
||||||
|
current: unknown,
|
||||||
|
event: RealtimeEvent,
|
||||||
|
insert = false,
|
||||||
|
pageSize = 20,
|
||||||
|
) {
|
||||||
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
|
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
const id = Number((event.data as { orderId?: unknown }).orderId || event.entityId)
|
const id = Number((event.data as { orderId?: unknown }).orderId || event.entityId)
|
||||||
const items = current.data.items as unknown[]
|
const items = current.data.items as unknown[]
|
||||||
const nextItems =
|
const existed = items.some((entry) => Number((entry as { orderId?: unknown })?.orderId) === id)
|
||||||
|
let nextItems =
|
||||||
event.operation === 'remove'
|
event.operation === 'remove'
|
||||||
? items.filter((entry) => Number((entry as { orderId?: unknown })?.orderId) !== id)
|
? items.filter((entry) => Number((entry as { orderId?: unknown })?.orderId) !== id)
|
||||||
: upsertById(items, event.data, 'orderId', insert)
|
: upsertById(items, event.data, 'orderId', insert)
|
||||||
return { ...current, data: { ...current.data, items: nextItems } }
|
|
||||||
|
if (insert && nextItems.length > pageSize) {
|
||||||
|
nextItems = nextItems.slice(0, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pagination = isRecord(current.data.pagination) ? current.data.pagination : null
|
||||||
|
let nextPagination = pagination
|
||||||
|
if (pagination && insert && !existed) {
|
||||||
|
nextPagination = {
|
||||||
|
...pagination,
|
||||||
|
total: Number(pagination.total || 0) + 1,
|
||||||
|
}
|
||||||
|
} else if (pagination && event.operation === 'remove' && existed) {
|
||||||
|
nextPagination = {
|
||||||
|
...pagination,
|
||||||
|
total: Math.max(0, Number(pagination.total || 0) - 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
data: {
|
||||||
|
...current.data,
|
||||||
|
items: nextItems,
|
||||||
|
...(nextPagination ? { pagination: nextPagination } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchWorkOrderListEnvelope(current: unknown, event: RealtimeEvent, insert = false) {
|
function patchWorkOrderListEnvelope(current: unknown, event: RealtimeEvent, insert = false) {
|
||||||
@@ -560,6 +608,51 @@ function upsertById(items: unknown[], nextItem: unknown, idKey: string, insert =
|
|||||||
return items.map((item, itemIndex) => (itemIndex === index ? nextItem : item))
|
return items.map((item, itemIndex) => (itemIndex === index ? nextItem : item))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesAdminOrderKeyword(
|
||||||
|
item: {
|
||||||
|
platformOrderId?: unknown
|
||||||
|
shopName?: unknown
|
||||||
|
itemTitle?: unknown
|
||||||
|
itemSummary?: unknown
|
||||||
|
},
|
||||||
|
keyword: string,
|
||||||
|
) {
|
||||||
|
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||||
|
if (!normalizedKeyword) return true
|
||||||
|
return [item.platformOrderId, item.shopName, item.itemTitle, item.itemSummary]
|
||||||
|
.map((value) => String(value || '').toLowerCase())
|
||||||
|
.some((value) => value.includes(normalizedKeyword))
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesAdminOrderProvider(item: { provider?: unknown }, provider: string) {
|
||||||
|
const normalized = provider.trim().toLowerCase()
|
||||||
|
if (!normalized) return true
|
||||||
|
const value = String(item.provider || '').toLowerCase()
|
||||||
|
if (normalized === 'pdd') return value === 'pdd'
|
||||||
|
if (normalized === '91' || normalized === '91kaquan' || normalized === 'open_91') {
|
||||||
|
return ['91kaquan', 'open_91', '91'].includes(value)
|
||||||
|
}
|
||||||
|
if (normalized === 'kuaishou' || normalized === 'kuaishou-industry') {
|
||||||
|
return value.startsWith('kuaishou')
|
||||||
|
}
|
||||||
|
return value === normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesAdminOrderDateRange(
|
||||||
|
item: { paidAt?: unknown; createdAt?: unknown },
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
const dateFrom = String(params.dateFrom || '').trim().slice(0, 10)
|
||||||
|
const dateTo = String(params.dateTo || '').trim().slice(0, 10)
|
||||||
|
if (!dateFrom && !dateTo) return true
|
||||||
|
const raw = String(item.paidAt || item.createdAt || '').trim()
|
||||||
|
if (!raw) return false
|
||||||
|
const day = dayjs(raw).format('YYYY-MM-DD')
|
||||||
|
if (dateFrom && day < dateFrom) return false
|
||||||
|
if (dateTo && day > dateTo) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function matchesWorkOrderKeyword(
|
function matchesWorkOrderKeyword(
|
||||||
item: { productName?: unknown; platformOrderId?: unknown },
|
item: { productName?: unknown; platformOrderId?: unknown },
|
||||||
keyword: string,
|
keyword: string,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { fetchAdminOrders } from '@/services/admin'
|
|||||||
import type { AdminOrderListItem } from '@/types/admin'
|
import type { AdminOrderListItem } from '@/types/admin'
|
||||||
import {
|
import {
|
||||||
adminOrderExecutorOptions,
|
adminOrderExecutorOptions,
|
||||||
|
adminOrderProviderOptions,
|
||||||
adminPayStatusOptions,
|
adminPayStatusOptions,
|
||||||
adminTaskStatusOptions,
|
adminTaskStatusOptions,
|
||||||
} from '@/utils/admin-options'
|
} from '@/utils/admin-options'
|
||||||
@@ -25,20 +26,46 @@ import { formatAdminDateTime } from '@/utils/admin-time'
|
|||||||
|
|
||||||
type OrderFilterForm = {
|
type OrderFilterForm = {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
|
provider?: string
|
||||||
executorKey?: string
|
executorKey?: string
|
||||||
payStatus?: string
|
payStatus?: string
|
||||||
taskStatus?: string
|
taskStatus?: string
|
||||||
dateRange?: unknown
|
dateRange?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 默认展示近 7 天(含今天)。 */
|
||||||
|
function resolveDefaultOrderDateRange() {
|
||||||
|
const today = dayjs()
|
||||||
|
return {
|
||||||
|
dateFrom: today.subtract(6, 'day').format('YYYY-MM-DD'),
|
||||||
|
dateTo: today.format('YYYY-MM-DD'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminOrdersPage() {
|
export default function AdminOrdersPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [form] = Form.useForm<OrderFilterForm>()
|
const [form] = Form.useForm<OrderFilterForm>()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const page = Number(searchParams.get('page') || 1) || 1
|
const page = Number(searchParams.get('page') || 1) || 1
|
||||||
const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE)
|
const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE)
|
||||||
|
|
||||||
|
// 首次进入(仅有分页参数)时写入默认近 7 天,避免一把拉出全库。
|
||||||
|
useEffect(() => {
|
||||||
|
const meaningfulKeys = [...searchParams.keys()].filter(
|
||||||
|
(key) => key !== 'page' && key !== 'pageSize',
|
||||||
|
)
|
||||||
|
if (meaningfulKeys.length > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { dateFrom, dateTo } = resolveDefaultOrderDateRange()
|
||||||
|
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }), { replace: true })
|
||||||
|
// 仅首屏引导默认日期,避免与后续筛选互相打架。
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
keyword: searchParams.get('keyword') || '',
|
keyword: searchParams.get('keyword') || '',
|
||||||
|
provider: searchParams.get('provider') || '',
|
||||||
executorKey: searchParams.get('executorKey') || '',
|
executorKey: searchParams.get('executorKey') || '',
|
||||||
payStatus: searchParams.get('payStatus') || '',
|
payStatus: searchParams.get('payStatus') || '',
|
||||||
taskStatus: searchParams.get('taskStatus') || '',
|
taskStatus: searchParams.get('taskStatus') || '',
|
||||||
@@ -54,20 +81,31 @@ export default function AdminOrdersPage() {
|
|||||||
filters.executorKey,
|
filters.executorKey,
|
||||||
filters.keyword,
|
filters.keyword,
|
||||||
filters.payStatus,
|
filters.payStatus,
|
||||||
|
filters.provider,
|
||||||
filters.taskStatus,
|
filters.taskStatus,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 等待默认日期写入 URL 后再请求,避免首屏无日期打出全库。
|
||||||
|
const hasBootstrappedFilters =
|
||||||
|
Boolean(filters.dateFrom && filters.dateTo) ||
|
||||||
|
Boolean(filters.keyword) ||
|
||||||
|
searchParams.get('allTime') === '1' ||
|
||||||
|
Boolean(filters.provider || filters.executorKey || filters.payStatus || filters.taskStatus)
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['admin-orders', queryParams],
|
queryKey: ['admin-orders', queryParams],
|
||||||
queryFn: () => fetchAdminOrders(queryParams),
|
queryFn: () => fetchAdminOrders(queryParams),
|
||||||
|
enabled: hasBootstrappedFilters,
|
||||||
})
|
})
|
||||||
const data = query.data?.data
|
const data = query.data?.data
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
keyword: filters.keyword || undefined,
|
keyword: filters.keyword || undefined,
|
||||||
|
provider: filters.provider || undefined,
|
||||||
executorKey: filters.executorKey || undefined,
|
executorKey: filters.executorKey || undefined,
|
||||||
payStatus: filters.payStatus || undefined,
|
payStatus: filters.payStatus || undefined,
|
||||||
taskStatus: filters.taskStatus || undefined,
|
taskStatus: filters.taskStatus || undefined,
|
||||||
@@ -82,6 +120,7 @@ export default function AdminOrdersPage() {
|
|||||||
filters.executorKey,
|
filters.executorKey,
|
||||||
filters.keyword,
|
filters.keyword,
|
||||||
filters.payStatus,
|
filters.payStatus,
|
||||||
|
filters.provider,
|
||||||
filters.taskStatus,
|
filters.taskStatus,
|
||||||
form,
|
form,
|
||||||
])
|
])
|
||||||
@@ -185,14 +224,14 @@ export default function AdminOrdersPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '金额 / 时间',
|
title: '金额 / 支付时间',
|
||||||
minWidth: 180,
|
minWidth: 180,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<strong>
|
<strong>
|
||||||
{row.totalAmount || '0.00'} {row.currency}
|
{row.totalAmount || '0.00'} {row.currency}
|
||||||
</strong>
|
</strong>
|
||||||
<span className="muted">{formatAdminDateTime(row.createdAt)}</span>
|
<span className="muted">{formatAdminDateTime(row.paidAt || row.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -200,29 +239,50 @@ export default function AdminOrdersPage() {
|
|||||||
|
|
||||||
function applyFilters(values: OrderFilterForm) {
|
function applyFilters(values: OrderFilterForm) {
|
||||||
const { dateRange, ...restValues } = values
|
const { dateRange, ...restValues } = values
|
||||||
const [dateFrom, dateTo] = Array.isArray(dateRange)
|
const keyword = String(restValues.keyword || '').trim()
|
||||||
|
const [rawFrom, rawTo] = Array.isArray(dateRange)
|
||||||
? dateRange.map((item: { format?: (pattern: string) => string }) =>
|
? dateRange.map((item: { format?: (pattern: string) => string }) =>
|
||||||
item?.format ? item.format('YYYY-MM-DD') : '',
|
item?.format ? item.format('YYYY-MM-DD') : '',
|
||||||
)
|
)
|
||||||
: ['', '']
|
: ['', '']
|
||||||
|
|
||||||
setSearchParams(cleanParams({ ...restValues, dateFrom, dateTo, page: 1, pageSize }))
|
// 有关键词时不强制日期(后端也会忽略日期);清空日期且无关键词则查全部时间。
|
||||||
|
const dateFrom = rawFrom || ''
|
||||||
|
const dateTo = rawTo || ''
|
||||||
|
const allTime = !dateFrom && !dateTo && !keyword ? '1' : ''
|
||||||
|
|
||||||
|
setSearchParams(
|
||||||
|
cleanParams({
|
||||||
|
...restValues,
|
||||||
|
keyword,
|
||||||
|
dateFrom,
|
||||||
|
dateTo,
|
||||||
|
allTime,
|
||||||
|
page: 1,
|
||||||
|
pageSize,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
|
const { dateFrom, dateTo } = resolveDefaultOrderDateRange()
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
keyword: undefined,
|
keyword: undefined,
|
||||||
|
provider: undefined,
|
||||||
executorKey: undefined,
|
executorKey: undefined,
|
||||||
payStatus: undefined,
|
payStatus: undefined,
|
||||||
taskStatus: undefined,
|
taskStatus: undefined,
|
||||||
dateRange: undefined,
|
dateRange: [dayjs(dateFrom), dayjs(dateTo)],
|
||||||
})
|
})
|
||||||
setSearchParams(cleanParams({ page: 1, pageSize }))
|
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-stack">
|
<section className="page-stack">
|
||||||
<PageHeader title="订单" description="查看订单入库、商品匹配和任务拆分情况。" />
|
<PageHeader
|
||||||
|
title="订单"
|
||||||
|
description="默认近 7 天(按支付/成团时间)。搜单号或商品时不限日期。"
|
||||||
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<Form<OrderFilterForm>
|
<Form<OrderFilterForm>
|
||||||
@@ -234,8 +294,16 @@ export default function AdminOrdersPage() {
|
|||||||
<Form.Item name="keyword">
|
<Form.Item name="keyword">
|
||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="订单号 / 商品标题 / SKU 标题 / 买家 / 店铺"
|
placeholder="订单号 / 商品标题 / SKU / 买家 / 店铺"
|
||||||
style={{ width: 320 }}
|
style={{ width: 300 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="provider">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder="来源渠道"
|
||||||
|
options={adminOrderProviderOptions}
|
||||||
|
style={{ minWidth: 130 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="executorKey">
|
<Form.Item name="executorKey">
|
||||||
@@ -243,15 +311,15 @@ export default function AdminOrdersPage() {
|
|||||||
allowClear
|
allowClear
|
||||||
placeholder="履约渠道"
|
placeholder="履约渠道"
|
||||||
options={adminOrderExecutorOptions}
|
options={adminOrderExecutorOptions}
|
||||||
style={{ minWidth: 150 }}
|
style={{ minWidth: 140 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="payStatus">
|
<Form.Item name="payStatus">
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="支付状态"
|
placeholder="支付状态"
|
||||||
options={adminPayStatusOptions}
|
options={adminPayStatusOptions.filter((item) => item.value)}
|
||||||
style={{ minWidth: 140 }}
|
style={{ minWidth: 130 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="taskStatus">
|
<Form.Item name="taskStatus">
|
||||||
@@ -260,12 +328,12 @@ export default function AdminOrdersPage() {
|
|||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
placeholder="任务状态"
|
placeholder="任务状态"
|
||||||
options={adminTaskStatusOptions}
|
options={adminTaskStatusOptions.filter((item) => item.value)}
|
||||||
style={{ minWidth: 170 }}
|
style={{ minWidth: 150 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="dateRange">
|
<Form.Item name="dateRange">
|
||||||
<AdminRangePicker />
|
<AdminRangePicker allowClear placeholder={['支付开始', '支付结束']} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
@@ -283,7 +351,7 @@ export default function AdminOrdersPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<Table<AdminOrderListItem>
|
<Table<AdminOrderListItem>
|
||||||
rowKey="orderId"
|
rowKey="orderId"
|
||||||
loading={query.isLoading || query.isFetching}
|
loading={query.isLoading || query.isFetching || !hasBootstrappedFilters}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items || []}
|
dataSource={data?.items || []}
|
||||||
scroll={{ x: 900 }}
|
scroll={{ x: 900 }}
|
||||||
@@ -292,7 +360,14 @@ export default function AdminOrdersPage() {
|
|||||||
pageSize: data?.pagination.pageSize || pageSize,
|
pageSize: data?.pagination.pageSize || pageSize,
|
||||||
total: data?.pagination.total || 0,
|
total: data?.pagination.total || 0,
|
||||||
onChange: (nextPage, nextPageSize) => {
|
onChange: (nextPage, nextPageSize) => {
|
||||||
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
|
setSearchParams(
|
||||||
|
cleanParams({
|
||||||
|
...filters,
|
||||||
|
allTime: searchParams.get('allTime') || '',
|
||||||
|
page: nextPage,
|
||||||
|
pageSize: nextPageSize,
|
||||||
|
}),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ export const adminPayStatusOptions = [
|
|||||||
{ label: '已退款', value: 'refunded' },
|
{ label: '已退款', value: 'refunded' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const adminOrderProviderOptions = [
|
||||||
|
{ label: '拼多多', value: 'pdd' },
|
||||||
|
{ label: '91卡券', value: '91kaquan' },
|
||||||
|
{ label: '快手', value: 'kuaishou' },
|
||||||
|
]
|
||||||
|
|
||||||
export const adminOrderExecutorOptions = [
|
export const adminOrderExecutorOptions = [
|
||||||
{ label: '联盟发货', value: 'affiliate_dash' },
|
{ label: '联盟发货', value: 'affiliate_dash' },
|
||||||
{ label: '行业凭证', value: 'kuaishou-industry' },
|
{ label: '行业凭证', value: 'kuaishou-industry' },
|
||||||
|
|||||||
Reference in New Issue
Block a user