feat(order): 订单列表页UI重构与多平台(快手/91卡券、抖音、拼多多)适配

- 顶部新增平台快捷分流器(Segmented),支持「全部 / 快手·91卡券 / 抖音码速达 / 拼多多 / 本地模拟」一键筛选
- 重构订单表格列布局为平台与订单号、买家信息、商品规格、交付与履约状态、金额与支付时间、快捷操作等现代化字段
- 优化快手与 91卡券业务归集逻辑,统一数据查询与徽标显示口径
- 增强订单行快捷操作,支持一键复制 H5 领取链接与查看详情

验证方式:后端 674 项单测全通过,前端 132 项单测全通过,前端打包构建通过
This commit is contained in:
yml2213
2026-10-10 18:54:40 +08:00
parent 3f2bb3f09a
commit 03020e85c7
4 changed files with 393 additions and 124 deletions
+19 -2
View File
@@ -402,8 +402,25 @@ function appendProviderFilter(filters: string[], params: unknown[], provider: st
return
}
if (normalized === 'kuaishou' || normalized === 'kuaishou-industry') {
filters.push(`(o.provider LIKE 'kuaishou%' OR o.platform LIKE 'kuaishou%')`)
if (
normalized === 'kuaishou' ||
normalized === 'kuaishou_all' ||
normalized === 'kuaishou_91' ||
normalized === 'kuaishou-industry'
) {
filters.push(
`(o.provider LIKE 'kuaishou%' OR o.platform LIKE 'kuaishou%' OR o.provider IN ('91kaquan', 'open_91', '91') OR o.platform IN ('91kaquan', '91'))`,
)
return
}
if (normalized === 'douyin' || normalized === 'douyin_mashida' || normalized === 'doudian') {
filters.push(`(o.provider LIKE 'douyin%' OR o.platform IN ('doudian', 'douyin'))`)
return
}
if (normalized === 'mock' || normalized === 'local_dev') {
filters.push(`(o.provider IN ('mock', 'local_dev') OR o.platform IN ('mock', 'local_dev'))`)
return
}
+304 -117
View File
@@ -1,5 +1,26 @@
import { SearchOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography, message } from 'antd'
import {
CopyOutlined,
EyeOutlined,
LinkOutlined,
ReloadOutlined,
SearchOutlined,
ShopOutlined,
UserOutlined,
} from '@ant-design/icons'
import {
App,
Button,
Card,
Form,
Input,
Segmented,
Select,
Space,
Table,
Tag,
Tooltip,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useEffect, useMemo } from 'react'
@@ -11,8 +32,13 @@ import StatusTag from '@/components/admin/StatusTag'
import dayjs from '@/lib/dayjs'
import { fetchAdminOrders } from '@/services/admin'
import type { AdminOrderListItem } from '@/types/admin'
import {
resolveOrderFulfillmentMode,
resolveOrderPlatformBadge,
} from '@/utils/admin-display'
import {
adminOrderExecutorOptions,
adminOrderPlatformSegmentedOptions,
adminOrderProviderOptions,
adminPayStatusOptions,
adminTaskStatusOptions,
@@ -44,6 +70,7 @@ function resolveDefaultOrderDateRange() {
export default function AdminOrdersPage() {
const navigate = useNavigate()
const { message } = App.useApp()
const [form] = Form.useForm<OrderFilterForm>()
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1) || 1
@@ -125,38 +152,127 @@ export default function AdminOrdersPage() {
form,
])
const handleCopyText = (text: string, label: string) => {
if (!text) return
navigator.clipboard
.writeText(text)
.then(() => message.success(`已复制 ${label}`))
.catch(() => message.error('复制失败,请手动复制'))
}
const handleSegmentedChange = (val: string) => {
form.setFieldValue('provider', val || undefined)
setSearchParams(
cleanParams({
...filters,
provider: val,
page: 1,
pageSize,
}),
)
}
const columns: TableColumnsType<AdminOrderListItem> = [
{
title: '订单信息',
dataIndex: 'platformOrderId',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Space size={4}>
<Typography.Link onClick={() => navigate(`/admin/orders/${row.orderId}`)}>
{row.platformOrderId}
</Typography.Link>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
title="复制 H5 领取链接"
style={{ padding: 0, height: 'auto', color: '#64748b' }}
onClick={(e) => {
e.stopPropagation()
const url = `${window.location.origin}/h5/${encodeURIComponent(row.platformOrderId)}`
navigator.clipboard.writeText(url).then(() => {
message.success('已复制 H5 链接')
})
}}
/>
</Space>
<span className="muted">{row.shopName || row.shopId || row.provider}</span>
</div>
),
title: '平台 / 订单号',
minWidth: 230,
render: (_, row) => {
const platformBadge = resolveOrderPlatformBadge(row)
const orderNo = row.platformOrderId || String(row.orderId)
return (
<div className="cell-stack" style={{ gap: 5 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
<Tag
style={{
margin: 0,
fontWeight: 600,
fontSize: 12,
borderColor: platformBadge.color,
color: platformBadge.color,
}}
>
{platformBadge.label}
</Tag>
{row.shopName ? (
<span
className="muted"
style={{
fontSize: 12,
maxWidth: 130,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={row.shopName}
>
<ShopOutlined style={{ marginRight: 3 }} />
{row.shopName}
</span>
) : null}
</div>
<Space size={4} align="center">
<Typography.Link
strong
style={{ fontSize: 13 }}
onClick={() => navigate(`/admin/orders/${row.orderId}`)}
title={`查看订单 #${row.orderId}`}
>
{orderNo}
</Typography.Link>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
title="复制订单号"
style={{ padding: 0, height: 'auto', color: '#94a3b8' }}
onClick={(e) => {
e.stopPropagation()
handleCopyText(orderNo, '订单号')
}}
/>
</Space>
</div>
)
},
},
{
title: '商品 / 任务',
title: '买家信息',
minWidth: 140,
render: (_, row) => {
const buyerName = row.buyerName || ''
const buyerId = row.buyerId || ''
const contact = row.receiverContact || ''
if (!buyerName && !buyerId && !contact) {
return <span className="muted">-</span>
}
return (
<div className="cell-stack" style={{ gap: 2 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<UserOutlined style={{ color: '#94a3b8', fontSize: 12 }} />
<Typography.Text strong style={{ fontSize: 13 }} ellipsis={{ tooltip: buyerName }}>
{buyerName || '匿名买家'}
</Typography.Text>
</div>
{buyerId ? (
<span className="muted" style={{ fontSize: 11 }} title={`买家ID: ${buyerId}`}>
ID: {buyerId}
</span>
) : null}
{contact && contact !== buyerName ? (
<span className="muted" style={{ fontSize: 11 }}>
{contact}
</span>
) : null}
</div>
)
},
},
{
title: '商品 / 规格',
minWidth: 260,
render: (_, row) => {
const itemTitle = row.itemTitle || row.itemSummary || '-'
@@ -172,7 +288,7 @@ export default function AdminOrdersPage() {
<Typography.Text
strong
ellipsis={{ tooltip: item.itemTitle }}
style={{ maxWidth: 360 }}
style={{ maxWidth: 360, fontSize: 13 }}
>
{item.itemTitle || '-'}
</Typography.Text>
@@ -186,6 +302,7 @@ export default function AdminOrdersPage() {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: 12,
}}
title={`规格:${item.skuNick}`}
>
@@ -199,7 +316,11 @@ export default function AdminOrdersPage() {
</div>
) : (
<>
<Typography.Text strong ellipsis={{ tooltip: itemTitle }} style={{ maxWidth: 360 }}>
<Typography.Text
strong
ellipsis={{ tooltip: itemTitle }}
style={{ maxWidth: 360, fontSize: 13 }}
>
{itemTitle}
</Typography.Text>
{skuNick ? (
@@ -212,6 +333,7 @@ export default function AdminOrdersPage() {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontSize: 12,
}}
title={`规格:${skuNick}`}
>
@@ -221,41 +343,89 @@ export default function AdminOrdersPage() {
) : null}
</>
)}
<span className="muted">
商品 {row.itemCount} · 数量 {row.totalQuantity} · 任务 {row.taskCount}
<span className="muted" style={{ fontSize: 12 }}>
数量 {row.totalQuantity || 1} · 关联任务 {row.taskCount || 0}
</span>
</div>
)
},
},
{
title: '状态',
minWidth: 240,
render: (_, row) => (
<Space size={[0, 6]} wrap>
<StatusTag value={row.payStatus} kind="pay" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
{row.claimStatus === 'paused' ? (
<Tag color="warning">H5已暂停</Tag>
) : row.claimStatus === 'closed' ? (
<Tag color="default">H5已关闭</Tag>
) : null}
</Space>
),
title: '交付 / 状态',
minWidth: 230,
render: (_, row) => {
const fulfillmentMode = resolveOrderFulfillmentMode(row)
return (
<div className="cell-stack" style={{ gap: 4 }}>
<Space size={[0, 4]} wrap>
<Tag color={fulfillmentMode.color} style={{ margin: 0, fontSize: 11 }}>
{fulfillmentMode.label}
</Tag>
<StatusTag value={row.payStatus} kind="pay" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
{row.claimStatus === 'paused' ? (
<Tag color="warning" style={{ margin: 0, width: 'fit-content' }}>
H5已暂停
</Tag>
) : row.claimStatus === 'closed' ? (
<Tag color="default" style={{ margin: 0, width: 'fit-content' }}>
H5已关闭
</Tag>
) : null}
</div>
)
},
},
{
title: '金额 / 支付时间',
minWidth: 180,
minWidth: 160,
render: (_, row) => (
<div className="cell-stack">
<strong>
{row.totalAmount || '0.00'} {row.currency}
<div className="cell-stack" style={{ gap: 3 }}>
<strong style={{ fontSize: 14, color: '#0f172a' }}>
¥ {row.totalAmount || '0.00'} <span style={{ fontSize: 11, color: '#64748b' }}>{row.currency}</span>
</strong>
<span className="muted">{formatAdminDateTime(row.paidAt || row.createdAt)}</span>
<span className="muted" style={{ fontSize: 12 }}>
{formatAdminDateTime(row.paidAt || row.createdAt)}
</span>
</div>
),
},
{
title: '操作',
minWidth: 140,
fixed: 'right',
render: (_, row) => {
const orderNo = row.platformOrderId || String(row.orderId)
const h5Url = `${window.location.origin}/h5/${encodeURIComponent(orderNo)}`
return (
<Space size={6} wrap>
<Button
type="primary"
size="small"
ghost
icon={<EyeOutlined />}
onClick={() => navigate(`/admin/orders/${row.orderId}`)}
>
详情
</Button>
<Tooltip title="复制 H5 领取链接">
<Button
size="small"
icon={<LinkOutlined />}
onClick={() => handleCopyText(h5Url, 'H5 领取链接')}
>
H5
</Button>
</Tooltip>
</Space>
)
},
},
]
function applyFilters(values: OrderFilterForm) {
@@ -298,75 +468,92 @@ export default function AdminOrdersPage() {
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }))
}
const currentSegmentedProvider = filters.provider || ''
return (
<section className="page-stack">
<PageHeader
title="订单"
description="默认近 7 天(按支付/成团时间)。搜单号或商品时不限日期。"
title="订单管理"
description="多平台订单聚合中台(快手/91卡券、抖音码速达、拼多多等),支持 H5 兑换与自动化履约管理。"
/>
<Card>
<Form<OrderFilterForm>
form={form}
layout="inline"
onFinish={applyFilters}
className="filter-form"
>
<Form.Item name="keyword">
<Input
allowClear
placeholder="订单号 / 商品标题 / SKU / 买家 / 店铺"
style={{ width: 300 }}
<Card style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
<Segmented
size="middle"
options={adminOrderPlatformSegmentedOptions}
value={currentSegmentedProvider}
onChange={(val) => handleSegmentedChange(String(val))}
/>
</Form.Item>
<Form.Item name="provider">
<Select
allowClear
placeholder="来源渠道"
options={adminOrderProviderOptions}
style={{ minWidth: 130 }}
/>
</Form.Item>
<Form.Item name="executorKey">
<Select
allowClear
placeholder="履约渠道"
options={adminOrderExecutorOptions}
style={{ minWidth: 140 }}
/>
</Form.Item>
<Form.Item name="payStatus">
<Select
allowClear
placeholder="支付状态"
options={adminPayStatusOptions.filter((item) => item.value)}
style={{ minWidth: 130 }}
/>
</Form.Item>
<Form.Item name="taskStatus">
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder="任务状态"
options={adminTaskStatusOptions.filter((item) => item.value)}
style={{ minWidth: 150 }}
/>
</Form.Item>
<Form.Item name="dateRange">
<AdminRangePicker allowClear placeholder={['支付开始', '支付结束']} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
查询
</Button>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
重置
</Button>
</Space>
</Form.Item>
</Form>
</div>
<Form<OrderFilterForm>
form={form}
layout="inline"
onFinish={applyFilters}
className="filter-form"
style={{ rowGap: 12 }}
>
<Form.Item name="keyword">
<Input
allowClear
placeholder="订单号 / 商品标题 / SKU / 买家 / 店铺"
style={{ width: 280 }}
/>
</Form.Item>
<Form.Item name="provider">
<Select
allowClear
placeholder="平台/渠道"
options={adminOrderProviderOptions}
style={{ minWidth: 140 }}
onChange={(val) => {
form.setFieldValue('provider', val)
}}
/>
</Form.Item>
<Form.Item name="executorKey">
<Select
allowClear
placeholder="履约模式"
options={adminOrderExecutorOptions}
style={{ minWidth: 130 }}
/>
</Form.Item>
<Form.Item name="payStatus">
<Select
allowClear
placeholder="支付状态"
options={adminPayStatusOptions.filter((item) => item.value)}
style={{ minWidth: 120 }}
/>
</Form.Item>
<Form.Item name="taskStatus">
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder="任务状态"
options={adminTaskStatusOptions.filter((item) => item.value)}
style={{ minWidth: 140 }}
/>
</Form.Item>
<Form.Item name="dateRange">
<AdminRangePicker allowClear placeholder={['支付开始', '支付结束']} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
查询
</Button>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
重置
</Button>
</Space>
</Form.Item>
</Form>
</div>
</Card>
<Card>
@@ -375,7 +562,7 @@ export default function AdminOrdersPage() {
loading={query.isLoading || query.isFetching || !hasBootstrappedFilters}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 900 }}
scroll={{ x: 1100 }}
pagination={buildAdminTablePagination({
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
+57
View File
@@ -182,3 +182,60 @@ export function formatTaskEventPayload(payload: Record<string, unknown>) {
return JSON.stringify(payload || {})
}
export function resolveOrderPlatformBadge(row: {
platform?: string
provider?: string
}): { label: string; color: string } {
const provider = String(row.provider || '').toLowerCase()
const platform = String(row.platform || '').toLowerCase()
if (provider === 'douyin_mashida' || platform === 'doudian' || platform === 'douyin') {
return { label: '抖音码速达', color: '#6366f1' }
}
if (
provider.startsWith('kuaishou') ||
platform.startsWith('kuaishou') ||
provider === '91kaquan' ||
provider === 'open_91' ||
platform === '91kaquan'
) {
if (provider === '91kaquan' || provider === 'open_91' || platform === '91kaquan') {
return { label: '快手 / 91卡券', color: '#ff5000' }
}
return { label: '快手电商', color: '#ff5000' }
}
if (provider === 'pdd' || platform === 'pdd') {
return { label: '拼多多', color: '#e02e24' }
}
if (provider === 'mock' || platform === 'mock' || provider === 'local_dev') {
return { label: '本地模拟', color: '#64748b' }
}
return { label: row.provider || row.platform || '外部订单', color: '#1677ff' }
}
export function resolveOrderFulfillmentMode(row: {
provider?: string
platform?: string
}): { label: string; color: string } {
const provider = String(row.provider || '').toLowerCase()
if (provider === 'douyin_mashida') {
return { label: 'H5直绑', color: 'blue' }
}
if (provider === 'kuaishou_industry' || provider === '91kaquan' || provider === 'open_91') {
return { label: '电子凭证', color: 'gold' }
}
if (provider === 'pdd') {
return { label: '拼多多发货', color: 'red' }
}
return { label: '自动交付', color: 'cyan' }
}
+13 -5
View File
@@ -6,12 +6,20 @@ export const adminPayStatusOptions = [
{ label: '已退款', value: 'refunded' },
]
export const adminOrderProviderOptions = [
{ label: '拼多多', value: 'pdd' },
{ label: '91卡券', value: '91kaquan' },
{ label: '快手', value: 'kuaishou' },
// 抖音码速达:订单落在 orders.provider='douyin_mashida'(platform='doudian')
export const adminOrderPlatformSegmentedOptions = [
{ label: '全部平台', value: '' },
{ label: '快手 / 91卡券', value: 'kuaishou' },
{ label: '抖音码速达', value: 'douyin_mashida' },
{ label: '拼多多', value: 'pdd' },
{ label: '本地模拟', value: 'mock' },
]
export const adminOrderProviderOptions = [
{ label: '全部', value: '' },
{ label: '快手 / 91卡券', value: 'kuaishou' },
{ label: '抖音码速达', value: 'douyin_mashida' },
{ label: '拼多多', value: 'pdd' },
{ label: '本地模拟', value: 'mock' },
]
export const adminOrderExecutorOptions = [