import { ArrowLeftOutlined, CheckCircleOutlined, CloseCircleOutlined, CopyOutlined, ReloadOutlined, RollbackOutlined, SendOutlined, SwapOutlined, ToolOutlined, UserSwitchOutlined, } from '@ant-design/icons' import { Alert, App, Button, Card, Descriptions, Empty, Image, Input, Skeleton, Space, Table, Tabs, Tag, Typography, } from 'antd' import type { TableColumnsType } from 'antd' import { useQuery } from '@tanstack/react-query' import { useEffect, useMemo, useState } from 'react' 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 { closeAdminTask, completeAdminTaskManualDispatch, dispatchAdminTaskKuaishouCloudFulfillment, fetchAdminTaskDetail, fetchAdminTaskScreenshot, markAdminTaskManualReview, prepareAdminTaskKuaishouCloudFulfillment, rebindAdminTaskKuaishouCloudRole, refreshAdminTaskKuaishouCloudRoleInfo, retryAdminTask, returnNumberAdminTaskKuaishouCloudFulfillment, } from '@/services/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' import { formatKuaishouRoleInfoLabel, formatRedeemOutcomeLabel, formatRedeemResolutionStatus, formatTaskEventPayload, formatTaskEventType, } from '@/utils/admin-display' import { formatAdminDateTime } from '@/utils/admin-time' type TaskEvent = AdminTaskDetail['events'][number] type ManualDispatchForm = { deliveryReference: string deliveredCredential: string resultMessage: string } export default function AdminTaskDetailPage() { const navigate = useNavigate() const { taskId = '' } = useParams() const { message, modal } = App.useApp() const [actionLoadingKey, setActionLoadingKey] = useState('') const [lastClaimUrl, setLastClaimUrl] = useState('') const [screenshotPreviewUrl, setScreenshotPreviewUrl] = useState('') const [manualDispatchForm, setManualDispatchForm] = useState({ deliveryReference: '', deliveredCredential: '', resultMessage: '', }) const query = useQuery({ queryKey: ['admin-task-detail', taskId], queryFn: () => fetchAdminTaskDetail(taskId), enabled: Boolean(taskId), }) const detail = query.data?.data const canManageTaskLifecycle = hasAdminRole('operator') const canCloseTasks = hasAdminRole('support') const claimUrl = useMemo(() => { const tokenStatus = String(detail?.claimToken?.status || '').trim() const taskStatus = String(detail?.task.status || '').trim() if (tokenStatus !== 'active' || ['closed', 'expired'].includes(taskStatus)) { return '' } return lastClaimUrl || detail?.claimToken?.claimUrl || '' }, [detail?.claimToken?.claimUrl, detail?.claimToken?.status, detail?.task.status, lastClaimUrl]) const claimLinkInvalid = Boolean( detail?.claimToken?.claimUrl && String(detail.claimToken.status || '').trim() && String(detail.claimToken.status || '').trim() !== 'active', ) useEffect(() => { if (!detail) return setManualDispatchForm({ deliveryReference: detail.manualDispatch?.deliveryReference || '', deliveredCredential: detail.manualDispatch?.deliveredCredential || '', resultMessage: detail.manualDispatch?.resultMessage || detail.task.resultMessage || '', }) }, [detail]) useEffect(() => { let objectUrl = '' setScreenshotPreviewUrl('') if (!detail?.screenshotUrl) { return undefined } fetchAdminTaskScreenshot(detail.task.taskId) .then((blob) => { objectUrl = URL.createObjectURL(blob) setScreenshotPreviewUrl(objectUrl) }) .catch(() => { setScreenshotPreviewUrl('') }) return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl) } } }, [detail?.screenshotUrl, detail?.task.taskId]) if (query.isLoading) { return (
} />
) } if (query.error || !detail) { return (
} />
) } const resolvedDetail = detail const flow = resolvedDetail.kuaishouCloudFulfillment const eventColumns: TableColumnsType = [ { 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() { if (!claimUrl) return await navigator.clipboard.writeText(claimUrl) message.success('领取链接已复制') } function runTaskAction( actionKey: string, action: () => Promise<{ data: AdminTaskActionResponse }>, successMessage: string, confirmText: string, ) { modal.confirm({ title: '确认操作', content: confirmText, okText: '继续执行', cancelText: '取消', centered: true, onOk: async () => { setActionLoadingKey(actionKey) try { const response = await action() if (response.data.claimUrl) { setLastClaimUrl(response.data.claimUrl) } message.success(successMessage) await query.refetch() } catch (error) { message.error(error instanceof Error ? error.message : '操作失败') } finally { setActionLoadingKey('') } }, }) } function submitManualDispatch(outcome: 'delivered' | 'failed') { const actionLabel = outcome === 'failed' ? '标记履约失败' : '标记已完成履约' const confirmText = outcome === 'failed' ? `确认把任务 ${resolvedDetail.task.taskNo} 回写为人工履约失败吗?这会把任务直接收口并保留失败原因。` : `确认把任务 ${resolvedDetail.task.taskNo} 回写为人工履约完成吗?这会把任务直接标记为已发放。` runTaskAction( `manual-${outcome}`, () => completeAdminTaskManualDispatch(resolvedDetail.task.taskId, { outcome, resultMessage: manualDispatchForm.resultMessage, deliveryReference: manualDispatchForm.deliveryReference, deliveredCredential: manualDispatchForm.deliveredCredential, }), actionLabel, confirmText, ) } return (
} /> runTaskAction( 'retry', () => retryAdminTask(resolvedDetail.task.taskId), '任务已重试', `确认重试任务 ${resolvedDetail.task.taskNo} 吗?`, ) } onRebindRole={() => runTaskAction( 'rebind-role', () => rebindAdminTaskKuaishouCloudRole(resolvedDetail.task.taskId), '角色换绑资源已准备完成', `确认为任务 ${resolvedDetail.task.taskNo} 换绑角色吗?当前虚拟号会退还,并生成新的绑定二维码。`, ) } onPrepareKuaishouCloud={() => runTaskAction( 'prepare-kuaishou-cloud', () => prepareAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId), '绑定资源已准备完成', `确认开始为任务 ${resolvedDetail.task.taskNo} 准备绑定资源吗?系统会自动检查背包、余额并申请虚拟号。`, ) } onDispatchKuaishouCloud={() => runTaskAction( 'dispatch-kuaishou-cloud', () => dispatchAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId), '已完成绑定确认并发货', `确认客户已经完成绑定,并立即为任务 ${resolvedDetail.task.taskNo} 执行发货吗?这个动作会把"确认绑定完成"和"发货"合并为一步。`, ) } onReturnKuaishouCloud={() => runTaskAction( 'return-kuaishou-cloud', () => returnNumberAdminTaskKuaishouCloudFulfillment(resolvedDetail.task.taskId), '号码已退还', `确认退还任务 ${resolvedDetail.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`, ) } onMarkManualReview={() => runTaskAction( 'manual-review', () => markAdminTaskManualReview(resolvedDetail.task.taskId), '任务已转人工处理', `确认将任务 ${resolvedDetail.task.taskNo} 转为人工处理吗?`, ) } onCloseTask={() => runTaskAction( 'close', () => closeAdminTask(resolvedDetail.task.taskId), '任务已关闭', `确认关闭任务 ${resolvedDetail.task.taskNo} 吗?关闭后不会自动继续推进。`, ) } /> {resolvedDetail.task.taskId} {resolvedDetail.task.taskNo} {resolvedDetail.task.platformOrderId || resolvedDetail.order?.platformOrderId || '-'} {resolvedDetail.orderItem?.skuName || resolvedDetail.task.skuName || '-'} {resolvedDetail.task.roleName || '-'} {resolvedDetail.task.roleId ? `(${resolvedDetail.task.roleId})` : ''} {formatAdminDateTime(resolvedDetail.task.updatedAt)} {resolvedDetail.task.lastError || '-'} {resolvedDetail.claimToken ? ( {claimUrl ? ( {claimUrl} ) : claimLinkInvalid ? ( 领取链接已失效 ) : ( 暂无可用领取链接 )} 到期:{formatAdminDateTime(resolvedDetail.claimToken.expiredAt)} ) : null} {resolvedDetail.task.executorKey === 'manual_dispatch' || resolvedDetail.operations.canCompleteManualDispatch || resolvedDetail.manualDispatch ? ( ) : null} runTaskAction( 'refresh-role', () => refreshAdminTaskKuaishouCloudRoleInfo(resolvedDetail.task.taskId), '角色信息已刷新', `确认刷新任务 ${resolvedDetail.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 cloudtentacles 的绑定结果。`, ) } /> ) : ( ), }, { key: 'resolution', label: '兑换重试', children: resolvedDetail.redeemResolution ? ( ) : ( ), }, { key: 'screenshot', label: '截图', children: resolvedDetail.screenshotUrl ? ( ) : ( ), }, { key: 'events', label: '事件流水', children: ( rowKey="eventId" pagination={false} columns={eventColumns} dataSource={resolvedDetail.events} scroll={{ x: 760 }} /> ), }, { key: 'raw', label: '原始数据', children: , }, ]} />
) } function TaskActionPanel({ detail, canManageTaskLifecycle, canCloseTasks, actionLoadingKey, onRetry, onRebindRole, onPrepareKuaishouCloud, onDispatchKuaishouCloud, onReturnKuaishouCloud, onMarkManualReview, onCloseTask, }: { detail: AdminTaskDetail canManageTaskLifecycle: boolean canCloseTasks: boolean actionLoadingKey: string onRetry: () => void onRebindRole: () => void onPrepareKuaishouCloud: () => void onDispatchKuaishouCloud: () => void onReturnKuaishouCloud: () => void onMarkManualReview: () => void onCloseTask: () => void }) { const operations = detail.operations const loading = Boolean(actionLoadingKey) return ( {canManageTaskLifecycle ? ( ) : null} {operations.canRebindKuaishouCloudRole ? ( ) : null} {canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? ( ) : null} {canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? ( ) : null} {canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? ( ) : null} {canManageTaskLifecycle ? ( ) : null} {canCloseTasks ? ( ) : null} {!canManageTaskLifecycle && !canCloseTasks && !operations.canRebindKuaishouCloudRole ? ( 当前账号没有可执行的任务操作 ) : null} {loading ? ( 操作执行中,请稍候。 ) : null} ) } function ManualDispatchPanel({ detail, form, canSubmit, actionLoadingKey, onChange, onSubmit, }: { detail: AdminTaskDetail form: ManualDispatchForm canSubmit: boolean actionLoadingKey: string onChange: (form: ManualDispatchForm) => void onSubmit: (outcome: 'delivered' | 'failed') => void }) { return ( ) : null } >
履约单号 / 回执 onChange({ ...form, deliveryReference: event.target.value })} />
发放凭据 onChange({ ...form, deliveredCredential: event.target.value })} />
处理备注 onChange({ ...form, resultMessage: event.target.value })} />
{detail.manualDispatch ? ( {formatAdminDateTime(detail.manualDispatch.completedAt)} {detail.manualDispatch.completedBy?.username || '-'} /{' '} {detail.manualDispatch.completedBy?.role || '-'} {detail.manualDispatch.deliveryReference || '-'} {detail.manualDispatch.deliveredCredential || '-'} {detail.manualDispatch.resultMessage || '-'} ) : null}
) } function KuaishouCloudPanel({ flow, canRefresh, actionLoadingKey, onRefreshRole, }: { flow: NonNullable canRefresh: boolean actionLoadingKey: string onRefreshRole: () => void }) { const roleInfoEntries = flow.role.rawInfo ? Object.entries(flow.role.rawInfo).filter( ([, value]) => value !== null && value !== undefined && String(value).trim() !== '', ) : [] const checklist = [ { key: 'ticket', label: '核销码校验', status: flow.ticket.status || 'pending', detail: flow.ticket.verifiedAt ? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 校验` : '等待客户提交并校验核销码', }, { key: 'bind', label: '绑定资源', status: flow.binding.prepareStatus || 'pending', detail: flow.binding.bindPreparedAt ? `绑定资源已准备,VN ${flow.binding.vnId || '-'}` : '等待准备 Cloud 绑定资源', }, { key: 'role', label: '角色识别', status: flow.role.status || 'pending', detail: flow.role.name || flow.role.rid ? `${flow.role.name || '-'} / ${flow.role.rid || '-'}` : '客户绑定后刷新角色信息', }, { key: 'dispatch', label: '发货执行', status: flow.dispatch.status || 'pending', detail: flow.dispatch.dispatchAt ? `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货` : '等待客服确认绑定并发货', }, { key: 'return', label: '退还号码', status: flow.returnNumber.status || 'pending', detail: flow.returnNumber.returnedAt ? `已于 ${formatAdminDateTime(flow.returnNumber.returnedAt)} 退号` : '发货后执行退还号码', }, { key: 'consume', label: '快手核销', status: flow.consume.status || 'pending', detail: flow.consume.consumedAt ? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销` : flow.consume.errorMessage || '等待退号后核销收口', }, ] return (
} disabled={Boolean(actionLoadingKey) || !canRefresh} loading={actionLoadingKey === 'refresh-role'} onClick={onRefreshRole} > 刷新角色 } > {flow.binding.resolvedSourceLabel || flow.binding.resolvedSourceKey || '-'} {flow.binding.vnPhone || flow.binding.vnId || '-'} {flow.role.name || '-'} {flow.role.rid ? `(${flow.role.rid})` : ''} {flow.binding.bindUrl ? ( {flow.binding.bindUrl} ) : ( '-' )}
{checklist.map((item) => (
{item.label} {item.detail}
))}
{roleInfoEntries.length > 0 ? ( {roleInfoEntries.map(([key, value]) => ( {typeof value === 'object' ? JSON.stringify(value) : String(value)} ))} ) : null} {flow.rebind.history.length > 0 ? ( `${row.attempt}-${row.requestedAt || ''}`} pagination={false} dataSource={flow.rebind.history} scroll={{ x: 860 }} columns={[ { title: '次数', dataIndex: 'attempt', width: 80 }, { title: '时间', dataIndex: 'requestedAt', width: 180, render: (value) => formatAdminDateTime(value), }, { title: '状态', dataIndex: 'status', width: 120, render: (value) => ( {String(value || '-')} ), }, { title: '旧角色', render: (_, row) => `${row.oldBinding.roleName || '-'} / ${row.oldBinding.roleId || '-'}`, }, { title: '旧虚拟号', render: (_, row) => row.oldBinding.vnPhone || row.oldBinding.vnId || row.oldBinding.vnKey || '-', }, { title: '异常', dataIndex: 'errorMessage', render: (value) => value || '-', }, ]} /> ) : null} ) } function RedeemResolutionPanel({ resolution, }: { resolution: NonNullable }) { return ( {formatRedeemResolutionStatus(resolution.status)} {resolution.taskStatus || '-'} {resolution.replacementCount} {formatAdminDateTime(resolution.finishedAt)}
`${row.attempt}-${row.codeMasked}`} pagination={false} dataSource={resolution.attempts} scroll={{ x: 760 }} columns={[ { title: '次数', dataIndex: 'attempt', width: 80 }, { title: '凭据', dataIndex: 'codeMasked', minWidth: 160 }, { title: '类型', dataIndex: 'credentialType', minWidth: 120 }, { title: '结果', dataIndex: 'outcome', minWidth: 160, render: (value) => formatRedeemOutcomeLabel(value), }, { title: '代码', dataIndex: 'resultCode', minWidth: 120 }, { title: '说明', dataIndex: 'resultMessage', minWidth: 220 }, ]} /> ) } function ScreenshotPanel({ screenshotPreviewUrl }: { screenshotPreviewUrl: string }) { return ( {screenshotPreviewUrl ? (
) : ( )}
) } function BackButton() { const navigate = useNavigate() return ( ) }