重命名前端目录
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
import {
|
||||
CloseCircleOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
SwapOutlined,
|
||||
UserSwitchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Alert, App, Button, Card, DatePicker, Form, Input, Select, Space, Table, Typography } from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router'
|
||||
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import StatusTag from '@/components/admin/StatusTag'
|
||||
import {
|
||||
closeAdminTask,
|
||||
fetchAdminTasks,
|
||||
markAdminTaskManualReview,
|
||||
rebindAdminTaskKuaishouCloudRole,
|
||||
retryAdminTask,
|
||||
} from '@/services/admin'
|
||||
import type { AdminTaskActionResponse, AdminTaskListItem } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { adminTaskStatusOptions } from '@/utils/admin-options'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type TaskFilterForm = {
|
||||
status?: string
|
||||
platformOrderId?: string
|
||||
taskNo?: string
|
||||
skuCode?: string
|
||||
roleId?: string
|
||||
dateRange?: unknown
|
||||
}
|
||||
|
||||
type TaskListAction = 'retry' | 'rebind_role' | 'manual_review' | 'close'
|
||||
|
||||
export default function AdminTasksPage() {
|
||||
const navigate = useNavigate()
|
||||
const { message, modal } = App.useApp()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [actionLoadingId, setActionLoadingId] = useState<number | null>(null)
|
||||
const [lastClaimUrl, setLastClaimUrl] = useState('')
|
||||
const page = Number(searchParams.get('page') || 1) || 1
|
||||
const pageSize = Number(searchParams.get('pageSize') || 20) || 20
|
||||
const canManageTaskLifecycle = hasAdminRole('operator')
|
||||
const canCloseTasks = hasAdminRole('support')
|
||||
const filters = {
|
||||
status: searchParams.get('status') || '',
|
||||
platformOrderId: searchParams.get('platformOrderId') || '',
|
||||
taskNo: searchParams.get('taskNo') || '',
|
||||
skuCode: searchParams.get('skuCode') || '',
|
||||
roleId: searchParams.get('roleId') || '',
|
||||
dateFrom: searchParams.get('dateFrom') || '',
|
||||
dateTo: searchParams.get('dateTo') || '',
|
||||
}
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => ({ page, pageSize, ...filters }),
|
||||
[
|
||||
filters.dateFrom,
|
||||
filters.dateTo,
|
||||
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">创建 {formatAdminDateTime(row.createdAt)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '异常 / 操作',
|
||||
minWidth: 260,
|
||||
fixed: 'right',
|
||||
render: (_, row) => {
|
||||
const rowLoading = actionLoadingId === row.taskId
|
||||
const actionRunning = actionLoadingId !== null
|
||||
const canRetry =
|
||||
canManageTaskLifecycle &&
|
||||
row.executorKey !== 'manual_dispatch' &&
|
||||
['retry_pending', 'manual_review'].includes(row.status)
|
||||
const canRebind = canRebindListTask(row, canManageTaskLifecycle)
|
||||
const canClose = canCloseTasks && !['redeemed', 'closed'].includes(row.status)
|
||||
const hasActions = canManageTaskLifecycle || canRebind || canClose
|
||||
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text type={row.lastError ? 'danger' : 'secondary'} ellipsis>
|
||||
{row.lastError || '当前无异常'}
|
||||
</Typography.Text>
|
||||
{hasActions ? (
|
||||
<Space size={[6, 6]} wrap>
|
||||
{canManageTaskLifecycle ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
ghost
|
||||
icon={<ReloadOutlined />}
|
||||
disabled={actionRunning || !canRetry}
|
||||
loading={rowLoading}
|
||||
onClick={() => handleActionCommand('retry', row)}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
) : null}
|
||||
{canRebind ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<SwapOutlined />}
|
||||
disabled={actionRunning}
|
||||
loading={rowLoading}
|
||||
onClick={() => handleActionCommand('rebind_role', row)}
|
||||
>
|
||||
换绑
|
||||
</Button>
|
||||
) : null}
|
||||
{canManageTaskLifecycle ? (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<UserSwitchOutlined />}
|
||||
disabled={actionRunning}
|
||||
loading={rowLoading}
|
||||
onClick={() => handleActionCommand('manual_review', row)}
|
||||
>
|
||||
转人工
|
||||
</Button>
|
||||
) : null}
|
||||
{canClose ? (
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<CloseCircleOutlined />}
|
||||
disabled={actionRunning}
|
||||
loading={rowLoading}
|
||||
onClick={() => handleActionCommand('close', row)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function handleActionCommand(actionKey: TaskListAction, item: AdminTaskListItem) {
|
||||
const actionConfig = resolveTaskListAction(actionKey, item)
|
||||
|
||||
modal.confirm({
|
||||
title: '确认操作',
|
||||
content: actionConfig.confirmText,
|
||||
okText: '继续执行',
|
||||
cancelText: '取消',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
setActionLoadingId(item.taskId)
|
||||
try {
|
||||
const response = await actionConfig.action()
|
||||
if (response.data.claimUrl) {
|
||||
setLastClaimUrl(response.data.claimUrl)
|
||||
}
|
||||
message.success(actionConfig.successMessage)
|
||||
await query.refetch()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '操作失败')
|
||||
} finally {
|
||||
setActionLoadingId(null)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function resolveTaskListAction(actionKey: TaskListAction, item: AdminTaskListItem): {
|
||||
action: () => Promise<{ data: AdminTaskActionResponse }>
|
||||
confirmText: string
|
||||
successMessage: string
|
||||
} {
|
||||
if (actionKey === 'retry') {
|
||||
return {
|
||||
action: () => retryAdminTask(item.taskId),
|
||||
confirmText: `确认重试任务 ${item.taskNo} 吗?`,
|
||||
successMessage: '任务已重试',
|
||||
}
|
||||
}
|
||||
|
||||
if (actionKey === 'rebind_role') {
|
||||
return {
|
||||
action: () => rebindAdminTaskKuaishouCloudRole(item.taskId),
|
||||
confirmText: `确认为任务 ${item.taskNo} 换绑角色吗?当前虚拟号会退还,并生成新的绑定二维码。`,
|
||||
successMessage: '角色换绑资源已准备完成',
|
||||
}
|
||||
}
|
||||
|
||||
if (actionKey === 'manual_review') {
|
||||
return {
|
||||
action: () => markAdminTaskManualReview(item.taskId),
|
||||
confirmText: `确认将任务 ${item.taskNo} 转为人工处理吗?`,
|
||||
successMessage: '任务已转人工处理',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
action: () => closeAdminTask(item.taskId),
|
||||
confirmText: `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
|
||||
successMessage: '任务已关闭',
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters(values: TaskFilterForm) {
|
||||
const { dateRange, ...restValues } = values
|
||||
const [dateFrom, dateTo] = Array.isArray(dateRange)
|
||||
? dateRange.map((item: { format?: (pattern: string) => string }) =>
|
||||
item?.format ? item.format('YYYY-MM-DD') : '',
|
||||
)
|
||||
: ['', '']
|
||||
|
||||
setSearchParams(
|
||||
cleanParams({
|
||||
...restValues,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
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,
|
||||
dateRange:
|
||||
filters.dateFrom && filters.dateTo
|
||||
? [dayjs(filters.dateFrom), dayjs(filters.dateTo)]
|
||||
: undefined,
|
||||
}}
|
||||
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 name="skuCode">
|
||||
<Input allowClear placeholder="商品名 / SKU" />
|
||||
</Form.Item>
|
||||
<Form.Item name="dateRange">
|
||||
<DatePicker.RangePicker />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{lastClaimUrl ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="最近可用领取链接"
|
||||
description={
|
||||
<Typography.Text copyable={{ text: lastClaimUrl }}>{lastClaimUrl}</Typography.Text>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Card>
|
||||
<Table<AdminTaskListItem>
|
||||
rowKey="taskId"
|
||||
loading={query.isLoading || query.isFetching}
|
||||
columns={columns}
|
||||
dataSource={data?.items || []}
|
||||
scroll={{ x: 1240 }}
|
||||
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 canRebindListTask(item: AdminTaskListItem, canManageTaskLifecycle: boolean) {
|
||||
return (
|
||||
canManageTaskLifecycle &&
|
||||
item.executorKey === 'kuaishou_ct_assisted' &&
|
||||
['waiting_binding', 'role_confirmed', 'manual_review', 'retry_pending'].includes(item.status)
|
||||
)
|
||||
}
|
||||
|
||||
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