新增 React Ant Design 前端骨架

This commit is contained in:
yml2213
2026-07-07 18:15:02 +08:00
parent c2c168f18e
commit 45d4df74fc
61 changed files with 6869 additions and 0 deletions
@@ -0,0 +1,20 @@
import { Button, Empty } from 'antd'
import { useNavigate } from 'react-router'
type NotFoundPageProps = {
title?: string
}
export default function NotFoundPage({ title = '页面不存在' }: NotFoundPageProps) {
const navigate = useNavigate()
return (
<main className="empty-page">
<Empty description={title}>
<Button type="primary" onClick={() => navigate('/admin/dashboard')}>
返回概览
</Button>
</Empty>
</main>
)
}
@@ -0,0 +1,51 @@
import { Alert, Card, Col, Row, Skeleton, Statistic } from 'antd'
import { useQuery } from '@tanstack/react-query'
import PageHeader from '@/components/admin/PageHeader'
import { fetchAdminDashboardSummary } from '@/services/admin'
const cards = [
{ key: 'todayOrders', label: '今日订单' },
{ key: 'paidPendingClaim', label: '待领取' },
{ key: 'claimingTasks', label: '领取中' },
{ key: 'redeemedToday', label: '今日成功' },
{ key: 'abnormalTasks', label: '异常任务' },
] as const
export default function AdminDashboardPage() {
const query = useQuery({
queryKey: ['admin-dashboard-summary'],
queryFn: () => fetchAdminDashboardSummary(),
})
const summary = query.data?.data
return (
<section className="page-stack">
<PageHeader title="系统概览" description="快速查看订单和交付链路的运行状态。" />
{query.error ? (
<Alert
type="error"
showIcon
message={query.error instanceof Error ? query.error.message : '读取概览失败'}
/>
) : null}
{query.isLoading ? (
<Card>
<Skeleton active paragraph={{ rows: 8 }} />
</Card>
) : (
<Row gutter={[16, 16]}>
{cards.map((card) => (
<Col xs={24} md={12} xl={8} key={card.key}>
<Card>
<Statistic title={card.label} value={summary?.[card.key] ?? 0} />
</Card>
</Col>
))}
</Row>
)}
</section>
)
}
@@ -0,0 +1,74 @@
import { LockOutlined, UserOutlined } from '@ant-design/icons'
import { App, Button, Card, Form, Input, Typography } from 'antd'
import { useState } from 'react'
import { useNavigate } from 'react-router'
import { loginAdmin } from '@/services/admin'
import { setAdminSession } from '@/utils/admin-auth'
type LoginForm = {
username: string
password: string
}
export default function AdminLoginPage() {
const navigate = useNavigate()
const { message } = App.useApp()
const [loading, setLoading] = useState(false)
async function submitLogin(values: LoginForm) {
setLoading(true)
try {
const response = await loginAdmin({
username: values.username.trim(),
password: values.password.trim(),
})
setAdminSession(response.data.token, response.data.expiresAt, response.data.user)
message.success('登录成功')
navigate('/admin/dashboard', { replace: true })
} catch (error) {
message.error(error instanceof Error ? error.message : '后台登录失败')
} finally {
setLoading(false)
}
}
return (
<main className="login-page">
<Card className="login-panel" variant="borderless">
<div className="login-heading">
<Typography.Text className="eyebrow">Order Site Admin</Typography.Text>
<Typography.Title level={1}>运营后台登录</Typography.Title>
<Typography.Paragraph type="secondary">
使用账号密码进入后台,并按角色开放不同操作权限。
</Typography.Paragraph>
</div>
<Form<LoginForm> layout="vertical" requiredMark={false} onFinish={submitLogin}>
<Form.Item
name="username"
label="账号"
rules={[{ required: true, message: '请输入账号' }]}
>
<Input prefix={<UserOutlined />} placeholder="输入账号" autoComplete="username" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="输入密码"
autoComplete="current-password"
/>
</Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
登录后台
</Button>
</Form>
</Card>
</main>
)
}
@@ -0,0 +1,161 @@
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Alert, Button, Card, Descriptions, Skeleton, Space, Table, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminOrderDetail } from '@/services/admin'
import type { AdminOrderDetail, AdminTaskListItem } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
export default function AdminOrderDetailPage() {
const navigate = useNavigate()
const { orderId = '' } = useParams()
const query = useQuery({
queryKey: ['admin-order-detail', orderId],
queryFn: () => fetchAdminOrderDetail(orderId),
enabled: Boolean(orderId),
})
const detail = query.data?.data
if (query.isLoading) {
return (
<section className="page-stack">
<PageHeader title="订单详情" extra={<BackButton />} />
<Card>
<Skeleton active paragraph={{ rows: 10 }} />
</Card>
</section>
)
}
if (query.error || !detail) {
return (
<section className="page-stack">
<PageHeader title="订单详情" extra={<BackButton />} />
<Alert
type="error"
showIcon
message={query.error instanceof Error ? query.error.message : '订单不存在或读取失败'}
/>
</section>
)
}
const taskColumns: TableColumnsType<AdminTaskListItem> = [
{
title: '任务号',
dataIndex: 'taskNo',
minWidth: 180,
render: (_, row) => (
<Typography.Link onClick={() => navigate(`/admin/tasks/${row.taskId}`)}>
{row.taskNo}
</Typography.Link>
),
},
{
title: '状态',
minWidth: 220,
render: (_, row) => (
<Space size={[0, 6]} wrap>
<StatusTag value={row.status} kind="task" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
),
},
{
title: '角色',
minWidth: 160,
render: (_, row) => (
<div className="cell-stack">
<span>{row.roleName || '-'}</span>
<span className="muted">{row.roleId || '-'}</span>
</div>
),
},
{
title: '错误',
dataIndex: 'lastError',
minWidth: 220,
render: (value) => <span className="muted">{value || '-'}</span>,
},
]
return (
<section className="page-stack">
<PageHeader
title="订单详情"
description={detail.order.platformOrderId}
extra={<BackButton />}
/>
<Card title="基础信息">
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="订单 ID">{detail.order.orderId}</Descriptions.Item>
<Descriptions.Item label="店铺">
{detail.order.shopName || detail.order.shopId || '-'}
</Descriptions.Item>
<Descriptions.Item label="平台订单号">{detail.order.platformOrderId}</Descriptions.Item>
<Descriptions.Item label="订单状态">
<StatusTag value={detail.order.orderStatus} />
</Descriptions.Item>
<Descriptions.Item label="支付状态">
<StatusTag value={detail.order.payStatus} kind="pay" />
</Descriptions.Item>
<Descriptions.Item label="金额">
{detail.order.totalAmount} {detail.order.currency}
</Descriptions.Item>
<Descriptions.Item label="创建时间">
{formatAdminDateTime(detail.order.createdAt)}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{formatAdminDateTime(detail.order.updatedAt)}
</Descriptions.Item>
<Descriptions.Item label="商品摘要">{detail.order.itemSummary || '-'}</Descriptions.Item>
</Descriptions>
</Card>
<Card title="商品明细">
<Table<AdminOrderDetail['items'][number]>
rowKey="orderItemId"
pagination={false}
dataSource={detail.items}
scroll={{ x: 760 }}
columns={[
{ title: '商品', dataIndex: 'itemTitle', minWidth: 240 },
{ title: 'SKU', dataIndex: 'skuCode', minWidth: 180 },
{ title: '数量', dataIndex: 'quantity', width: 90 },
{ title: '履约模式', dataIndex: 'deliveryMode', minWidth: 140 },
]}
/>
</Card>
<Card title="任务">
<Table<AdminTaskListItem>
rowKey="taskId"
pagination={false}
dataSource={detail.tasks}
columns={taskColumns}
scroll={{ x: 900 }}
/>
</Card>
<Card title="原始载荷">
<JsonPreview value={detail.order.rawPayload} />
</Card>
</section>
)
}
function BackButton() {
const navigate = useNavigate()
return (
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/orders')}>
返回订单
</Button>
)
}
@@ -0,0 +1,169 @@
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminOrders } from '@/services/admin'
import type { AdminOrderListItem } from '@/types/admin'
import { adminPayStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
type OrderFilterForm = {
platformOrderId?: string
payStatus?: string
skuCode?: string
}
export default function AdminOrdersPage() {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = Number(searchParams.get('pageSize') || 20) || 20
const filters = {
platformOrderId: searchParams.get('platformOrderId') || '',
payStatus: searchParams.get('payStatus') || '',
skuCode: searchParams.get('skuCode') || '',
}
const queryParams = useMemo(
() => ({ page, pageSize, ...filters }),
[filters.platformOrderId, filters.payStatus, filters.skuCode, page, pageSize],
)
const query = useQuery({
queryKey: ['admin-orders', queryParams],
queryFn: () => fetchAdminOrders(queryParams),
})
const data = query.data?.data
const columns: TableColumnsType<AdminOrderListItem> = [
{
title: '订单信息',
dataIndex: 'platformOrderId',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Link onClick={() => navigate(`/admin/orders/${row.orderId}`)}>
{row.platformOrderId}
</Typography.Link>
<span className="muted">{row.shopName || row.shopId || row.provider}</span>
</div>
),
},
{
title: '商品 / 任务',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<span>{row.itemSummary || '-'}</span>
<span className="muted">
商品 {row.itemCount} · 数量 {row.totalQuantity} · 任务 {row.taskCount}
</span>
</div>
),
},
{
title: '状态',
minWidth: 220,
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" />
</Space>
),
},
{
title: '金额 / 时间',
minWidth: 180,
render: (_, row) => (
<div className="cell-stack">
<strong>{row.totalAmount || '0.00'} {row.currency}</strong>
<span className="muted">{formatAdminDateTime(row.createdAt)}</span>
</div>
),
},
]
function applyFilters(values: OrderFilterForm) {
setSearchParams(cleanParams({ ...values, page: 1, pageSize }))
}
function resetFilters() {
setSearchParams(cleanParams({ page: 1, pageSize }))
}
return (
<section className="page-stack">
<PageHeader title="订单" description="查看订单入库、商品匹配和任务拆分情况。" />
<Card>
<Form<OrderFilterForm>
layout="inline"
initialValues={filters}
onFinish={applyFilters}
className="filter-form"
>
<Form.Item name="platformOrderId">
<Input allowClear placeholder="平台订单号" />
</Form.Item>
<Form.Item name="payStatus">
<Select
allowClear
placeholder="支付状态"
options={adminPayStatusOptions}
style={{ minWidth: 140 }}
/>
</Form.Item>
<Form.Item name="skuCode">
<Input allowClear placeholder="SKU 标识" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
查询
</Button>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
重置
</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card>
<Table<AdminOrderListItem>
rowKey="orderId"
loading={query.isLoading || query.isFetching}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 900 }}
pagination={{
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
total: data?.pagination.total || 0,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
onChange: (nextPage, nextPageSize) => {
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
},
}}
/>
</Card>
</section>
)
}
function cleanParams(params: Record<string, unknown>) {
const next = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
const normalized = String(value ?? '').trim()
if (normalized) {
next.set(key, normalized)
}
})
return next
}
@@ -0,0 +1,237 @@
import { ArrowLeftOutlined, CopyOutlined, ReloadOutlined } from '@ant-design/icons'
import {
Alert,
App,
Button,
Card,
Descriptions,
Empty,
Skeleton,
Space,
Table,
Tabs,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useNavigate, useParams } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminTaskDetail } from '@/services/admin'
import type { AdminTaskDetail } from '@/types/admin'
import { formatTaskEventPayload, formatTaskEventType } from '@/utils/admin-display'
import { formatAdminDateTime } from '@/utils/admin-time'
type TaskEvent = AdminTaskDetail['events'][number]
export default function AdminTaskDetailPage() {
const navigate = useNavigate()
const { taskId = '' } = useParams()
const { message } = App.useApp()
const query = useQuery({
queryKey: ['admin-task-detail', taskId],
queryFn: () => fetchAdminTaskDetail(taskId),
enabled: Boolean(taskId),
})
const detail = query.data?.data
if (query.isLoading) {
return (
<section className="page-stack">
<PageHeader title="任务详情" extra={<BackButton />} />
<Card>
<Skeleton active paragraph={{ rows: 10 }} />
</Card>
</section>
)
}
if (query.error || !detail) {
return (
<section className="page-stack">
<PageHeader title="任务详情" extra={<BackButton />} />
<Alert
type="error"
showIcon
message={query.error instanceof Error ? query.error.message : '任务不存在或读取失败'}
/>
</section>
)
}
const resolvedDetail = detail
const flow = resolvedDetail.kuaishouCloudFulfillment
const eventColumns: TableColumnsType<TaskEvent> = [
{
title: '时间',
dataIndex: 'createdAt',
width: 180,
render: (value) => formatAdminDateTime(value),
},
{
title: '事件',
dataIndex: 'eventType',
width: 180,
render: (value) => formatTaskEventType(value),
},
{
title: '摘要',
render: (_, row) => formatTaskEventPayload(row.payload),
},
]
async function copyClaimUrl() {
const claimUrl = resolvedDetail.claimToken?.claimUrl || ''
if (!claimUrl) return
await navigator.clipboard.writeText(claimUrl)
message.success('领取链接已复制')
}
return (
<section className="page-stack">
<PageHeader
title="任务详情"
description={resolvedDetail.task.taskNo}
extra={
<Space wrap>
<Button icon={<ReloadOutlined />} onClick={() => query.refetch()}>
刷新
</Button>
<BackButton />
</Space>
}
/>
<Card title="基础信息">
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="任务 ID">{resolvedDetail.task.taskId}</Descriptions.Item>
<Descriptions.Item label="任务号">{resolvedDetail.task.taskNo}</Descriptions.Item>
<Descriptions.Item label="平台订单号">
{resolvedDetail.task.platformOrderId || resolvedDetail.order?.platformOrderId || '-'}
</Descriptions.Item>
<Descriptions.Item label="任务状态">
<StatusTag value={resolvedDetail.task.status} kind="task" />
</Descriptions.Item>
<Descriptions.Item label="资源状态">
<StatusTag value={resolvedDetail.task.resourceStatus} kind="resource" />
</Descriptions.Item>
<Descriptions.Item label="客户步骤">
<StatusTag value={resolvedDetail.task.customerStatus} kind="customer" />
</Descriptions.Item>
<Descriptions.Item label="商品">
{resolvedDetail.orderItem?.skuName || resolvedDetail.task.skuName || '-'}
</Descriptions.Item>
<Descriptions.Item label="角色">
{resolvedDetail.task.roleName || '-'} {resolvedDetail.task.roleId ? `(${resolvedDetail.task.roleId})` : ''}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{formatAdminDateTime(resolvedDetail.task.updatedAt)}
</Descriptions.Item>
<Descriptions.Item label="最近错误" span={3}>
{resolvedDetail.task.lastError || '-'}
</Descriptions.Item>
</Descriptions>
</Card>
{resolvedDetail.claimToken ? (
<Card title="领取链接">
<Space direction="vertical" size={8} className="full-width">
<Typography.Text copyable={{ text: resolvedDetail.claimToken.claimUrl }}>
{resolvedDetail.claimToken.claimUrl}
</Typography.Text>
<Space wrap>
<StatusTag value={resolvedDetail.claimToken.status} />
<Typography.Text type="secondary">
到期:{formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}
</Typography.Text>
<Button icon={<CopyOutlined />} onClick={copyClaimUrl}>
复制链接
</Button>
</Space>
</Space>
</Card>
) : null}
<Tabs
items={[
{
key: 'cloud',
label: '快手 Cloud',
children: flow ? <KuaishouCloudPanel flow={flow} /> : <Empty description="无快手 Cloud 履约上下文" />,
},
{
key: 'events',
label: '事件流水',
children: (
<Table<TaskEvent>
rowKey="eventId"
pagination={false}
columns={eventColumns}
dataSource={resolvedDetail.events}
scroll={{ x: 760 }}
/>
),
},
{
key: 'raw',
label: '原始数据',
children: <JsonPreview value={resolvedDetail} />,
},
]}
/>
</section>
)
}
function KuaishouCloudPanel({ flow }: { flow: NonNullable<AdminTaskDetail['kuaishouCloudFulfillment']> }) {
return (
<Card>
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
<Descriptions.Item label="来源账号">
{flow.binding.resolvedSourceLabel || flow.binding.resolvedSourceKey || '-'}
</Descriptions.Item>
<Descriptions.Item label="虚拟号">
{flow.binding.vnPhone || flow.binding.vnId || '-'}
</Descriptions.Item>
<Descriptions.Item label="绑定状态">
<StatusTag value={flow.binding.prepareStatus} />
</Descriptions.Item>
<Descriptions.Item label="角色">
{flow.role.name || '-'} {flow.role.rid ? `(${flow.role.rid})` : ''}
</Descriptions.Item>
<Descriptions.Item label="卡券">
<StatusTag value={flow.ticket.status} />
</Descriptions.Item>
<Descriptions.Item label="发货">
<StatusTag value={flow.dispatch.status} kind="delivery" />
</Descriptions.Item>
<Descriptions.Item label="核销">
<StatusTag value={flow.consume.status} />
</Descriptions.Item>
<Descriptions.Item label="退号">
<StatusTag value={flow.returnNumber.status} />
</Descriptions.Item>
<Descriptions.Item label="绑定链接" span={3}>
{flow.binding.bindUrl ? (
<Typography.Text copyable={{ text: flow.binding.bindUrl }}>
{flow.binding.bindUrl}
</Typography.Text>
) : (
'-'
)}
</Descriptions.Item>
</Descriptions>
</Card>
)
}
function BackButton() {
const navigate = useNavigate()
return (
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/admin/tasks')}>
返回任务
</Button>
)
}
@@ -0,0 +1,191 @@
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Typography } from 'antd'
import type { TableColumnsType } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useNavigate, useSearchParams } from 'react-router'
import PageHeader from '@/components/admin/PageHeader'
import StatusTag from '@/components/admin/StatusTag'
import { fetchAdminTasks } from '@/services/admin'
import type { AdminTaskListItem } from '@/types/admin'
import { adminTaskStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
type TaskFilterForm = {
status?: string
platformOrderId?: string
taskNo?: string
skuCode?: string
roleId?: string
}
export default function AdminTasksPage() {
const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = Number(searchParams.get('pageSize') || 20) || 20
const filters = {
status: searchParams.get('status') || '',
platformOrderId: searchParams.get('platformOrderId') || '',
taskNo: searchParams.get('taskNo') || '',
skuCode: searchParams.get('skuCode') || '',
roleId: searchParams.get('roleId') || '',
}
const queryParams = useMemo(
() => ({ page, pageSize, ...filters }),
[
filters.platformOrderId,
filters.roleId,
filters.skuCode,
filters.status,
filters.taskNo,
page,
pageSize,
],
)
const query = useQuery({
queryKey: ['admin-tasks', queryParams],
queryFn: () => fetchAdminTasks(queryParams),
})
const data = query.data?.data
const columns: TableColumnsType<AdminTaskListItem> = [
{
title: '任务 / 订单',
minWidth: 230,
render: (_, row) => (
<div className="cell-stack">
<Typography.Link onClick={() => navigate(`/admin/tasks/${row.taskId}`)}>
{row.taskNo}
</Typography.Link>
<span className="muted">{row.platformOrderId}</span>
</div>
),
},
{
title: '商品',
minWidth: 210,
render: (_, row) => (
<div className="cell-stack">
<span>{row.skuName || '-'}</span>
<span className="muted">{row.skuCode || row.executorKey}</span>
</div>
),
},
{
title: '状态',
minWidth: 250,
render: (_, row) => (
<Space size={[0, 6]} wrap>
<StatusTag value={row.status} kind="task" />
<StatusTag value={row.resourceStatus} kind="resource" />
<StatusTag value={row.customerStatus} kind="customer" />
</Space>
),
},
{
title: '角色',
minWidth: 180,
render: (_, row) => (
<div className="cell-stack">
<span>{row.roleName || '-'}</span>
<span className="muted">{row.roleId || row.loginType || '-'}</span>
</div>
),
},
{
title: '时间 / 异常',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<span>{formatAdminDateTime(row.updatedAt)}</span>
<span className="muted">{row.lastError || '-'}</span>
</div>
),
},
]
function applyFilters(values: TaskFilterForm) {
setSearchParams(cleanParams({ ...values, page: 1, pageSize }))
}
function resetFilters() {
setSearchParams(cleanParams({ page: 1, pageSize }))
}
return (
<section className="page-stack">
<PageHeader title="任务" description="跟踪履约任务状态、客户步骤和资源准备进度。" />
<Card>
<Form<TaskFilterForm>
layout="inline"
initialValues={filters}
onFinish={applyFilters}
className="filter-form"
>
<Form.Item name="status">
<Select
allowClear
placeholder="任务状态"
options={adminTaskStatusOptions}
style={{ minWidth: 170 }}
/>
</Form.Item>
<Form.Item name="platformOrderId">
<Input allowClear placeholder="平台订单号" />
</Form.Item>
<Form.Item name="taskNo">
<Input allowClear placeholder="任务号" />
</Form.Item>
<Form.Item name="roleId">
<Input allowClear placeholder="角色 ID" />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
查询
</Button>
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
重置
</Button>
</Space>
</Form.Item>
</Form>
</Card>
<Card>
<Table<AdminTaskListItem>
rowKey="taskId"
loading={query.isLoading || query.isFetching}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 1080 }}
pagination={{
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
total: data?.pagination.total || 0,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
onChange: (nextPage, nextPageSize) => {
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
},
}}
/>
</Card>
</section>
)
}
function cleanParams(params: Record<string, unknown>) {
const next = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
const normalized = String(value ?? '').trim()
if (normalized) {
next.set(key, normalized)
}
})
return next
}