新增 React Ant Design 前端骨架
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user