diff --git a/apps/frontend-react/src/pages/admin/AdminTaskDetailPage.tsx b/apps/frontend-react/src/pages/admin/AdminTaskDetailPage.tsx index 61fe67b1..1e89fa9e 100644 --- a/apps/frontend-react/src/pages/admin/AdminTaskDetailPage.tsx +++ b/apps/frontend-react/src/pages/admin/AdminTaskDetailPage.tsx @@ -1,4 +1,15 @@ -import { ArrowLeftOutlined, CopyOutlined, ReloadOutlined } from '@ant-design/icons' +import { + ArrowLeftOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + CopyOutlined, + ReloadOutlined, + RollbackOutlined, + SendOutlined, + SwapOutlined, + ToolOutlined, + UserSwitchOutlined, +} from '@ant-design/icons' import { Alert, App, @@ -6,36 +17,123 @@ import { 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 { fetchAdminTaskDetail } from '@/services/admin' -import type { AdminTaskDetail } from '@/types/admin' -import { formatTaskEventPayload, formatTaskEventType } from '@/utils/admin-display' +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 } = App.useApp() + 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 ( @@ -83,12 +181,62 @@ export default function AdminTaskDetailPage() { ] async function copyClaimUrl() { - const claimUrl = resolvedDetail.claimToken?.claimUrl || '' 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} @@ -138,15 +349,19 @@ export default function AdminTaskDetailPage() { {resolvedDetail.claimToken ? ( - - {resolvedDetail.claimToken.claimUrl} - + {claimUrl ? ( + {claimUrl} + ) : claimLinkInvalid ? ( + 领取链接已失效 + ) : ( + 暂无可用领取链接 + )} 到期:{formatAdminDateTime(resolvedDetail.claimToken.expiredAt)} - @@ -154,12 +369,59 @@ export default function AdminTaskDetailPage() { ) : null} + {resolvedDetail.task.executorKey === 'manual_dispatch' || + resolvedDetail.operations.canCompleteManualDispatch || + resolvedDetail.manualDispatch ? ( + + ) : null} + : , + children: flow ? ( + + 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', @@ -185,44 +447,485 @@ export default function AdminTaskDetailPage() { ) } -function KuaishouCloudPanel({ flow }: { flow: NonNullable }) { +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 ( - - - - {flow.binding.resolvedSourceLabel || flow.binding.resolvedSourceKey || '-'} + + + {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)} - - {flow.binding.vnPhone || flow.binding.vnId || '-'} - - - - - - {flow.role.name || '-'} {flow.role.rid ? `(${flow.role.rid})` : ''} - - - - - - - - - - - - - - - {flow.binding.bindUrl ? ( - - {flow.binding.bindUrl} - - ) : ( - '-' - )} + {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 ? ( +
+ +
+ ) : ( + + )}
) } diff --git a/apps/frontend-react/src/pages/admin/AdminTasksPage.tsx b/apps/frontend-react/src/pages/admin/AdminTasksPage.tsx index e655b274..417e7c01 100644 --- a/apps/frontend-react/src/pages/admin/AdminTasksPage.tsx +++ b/apps/frontend-react/src/pages/admin/AdminTasksPage.tsx @@ -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(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) => (
{formatAdminDateTime(row.updatedAt)} - {row.lastError || '-'} + 创建 {formatAdminDateTime(row.createdAt)}
), }, + { + 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 ( +
+ + {row.lastError || '当前无异常'} + + {hasActions ? ( + + {canManageTaskLifecycle ? ( + + ) : null} + {canRebind ? ( + + ) : null} + {canManageTaskLifecycle ? ( + + ) : null} + {canClose ? ( + + ) : null} + + ) : null} +
+ ) + }, + }, ] + 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() { 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() { + + + + + +