补齐任务操作能力

This commit is contained in:
yml2213
2026-07-07 19:43:45 +08:00
parent b384b127e0
commit b12e624294
3 changed files with 1027 additions and 55 deletions
@@ -1,14 +1,28 @@
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { Button, Card, Form, Input, Select, Space, Table, Typography } from 'antd'
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 { useMemo } from 'react'
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 { fetchAdminTasks } from '@/services/admin'
import type { AdminTaskListItem } from '@/types/admin'
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'
@@ -18,24 +32,36 @@ type TaskFilterForm = {
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,
@@ -96,19 +122,172 @@ export default function AdminTasksPage() {
),
},
{
title: '时间 / 异常',
title: '时间',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<span>{formatAdminDateTime(row.updatedAt)}</span>
<span className="muted">{row.lastError || '-'}</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) {
setSearchParams(cleanParams({ ...values, page: 1, pageSize }))
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() {
@@ -122,7 +301,13 @@ export default function AdminTasksPage() {
<Card>
<Form<TaskFilterForm>
layout="inline"
initialValues={filters}
initialValues={{
...filters,
dateRange:
filters.dateFrom && filters.dateTo
? [dayjs(filters.dateFrom), dayjs(filters.dateTo)]
: undefined,
}}
onFinish={applyFilters}
className="filter-form"
>
@@ -143,6 +328,12 @@ export default function AdminTasksPage() {
<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">
@@ -156,13 +347,24 @@ export default function AdminTasksPage() {
</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: 1080 }}
scroll={{ x: 1240 }}
pagination={{
current: data?.pagination.page || page,
pageSize: data?.pagination.pageSize || pageSize,
@@ -179,6 +381,14 @@ export default function AdminTasksPage() {
)
}
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]) => {