'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { format } from 'date-fns'; import { Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw, } from 'lucide-react'; import services from '@/lib/services'; import type { TaskExecution, TaskExecutionStatus, TaskMeta, } from '@/lib/services/admin'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Spinner } from '@/components/ui/spinner'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; const STATUS_LABELS: Record = { pending: '等待中', running: '执行中', succeeded: '成功', failed: '失败', }; const TRIGGER_LABELS: Record = { system: '系统', manual: '手动', retry: '重试', schedule: '定时', }; const PAGE_SIZE = 10; function formatDateTime(value?: string | null) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return format(date, 'yyyy-MM-dd HH:mm:ss'); } function formatDuration(duration: number) { if (!duration) return '-'; if (duration < 1000) return `${duration}ms`; return `${(duration / 1000).toFixed(2)}s`; } function statusVariant(status: TaskExecutionStatus) { if (status === 'failed') return 'destructive'; if (status === 'succeeded') return 'secondary'; return 'outline'; } export function TaskExecutionsManager() { const queryClient = useQueryClient(); const [executionsPage, setExecutionsPage] = useState(1); const [executionStatus, setExecutionStatus] = useState< TaskExecutionStatus | 'all' >('all'); const [executionTaskType, setExecutionTaskType] = useState('all'); const [selectedExecutionId, setSelectedExecutionId] = useState( null, ); const [executionPreview, setExecutionPreview] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const taskTypesQuery = useQuery({ queryKey: ['admin', 'task-types'], queryFn: () => services.adminTask.getTaskTypes(), }); const executionsQuery = useQuery({ queryKey: [ 'admin', 'task-executions', executionsPage, executionStatus, executionTaskType, ], queryFn: () => services.adminTask.listTaskExecutions({ page: executionsPage, page_size: PAGE_SIZE, status: executionStatus === 'all' ? undefined : executionStatus, task_type: executionTaskType === 'all' ? undefined : executionTaskType, }), }); const detailQuery = useQuery({ queryKey: ['admin', 'task-execution', selectedExecutionId], queryFn: () => services.adminTask.getTaskExecution(selectedExecutionId!), enabled: detailOpen && !!selectedExecutionId, }); const retryMutation = useMutation({ mutationFn: (id: string) => services.adminTask.retryTaskExecution(id), onSuccess: (taskID) => { toast.success('任务已重新下发', { description: `新任务 ID:${taskID}`, }); void queryClient.invalidateQueries({ queryKey: ['admin', 'task-executions'], }); }, onError: (err: Error) => { toast.error('任务重试失败', { description: err.message || '未知错误', }); }, }); const taskTypes: TaskMeta[] = taskTypesQuery.data ?? []; const executions = executionsQuery.data?.items ?? []; const executionsTotal = executionsQuery.data?.total ?? 0; const executionsLoading = executionsQuery.isPending || executionsQuery.isFetching; const executionsError = executionsQuery.isError ? executionsQuery.error : null; const selectedExecution = detailQuery.data ?? executionPreview; const detailLoading = detailQuery.isFetching; const totalPages = useMemo( () => Math.max(1, Math.ceil(executionsTotal / PAGE_SIZE)), [executionsTotal], ); const openExecutionDetail = (execution: TaskExecution) => { setExecutionPreview(execution); setSelectedExecutionId(execution.id); setDetailOpen(true); }; const handleStatusChange = (value: TaskExecutionStatus | 'all') => { setExecutionStatus(value); setExecutionsPage(1); }; const handleTaskTypeChange = (value: string) => { setExecutionTaskType(value); setExecutionsPage(1); }; return (

{executionsError ? (
void executionsQuery.refetch()} className='justify-center' />
) : executionsLoading && executions.length === 0 ? ( ) : executions.length === 0 ? ( ) : (
任务 状态 触发 重试 耗时 结果/错误 创建时间 {executions.map((execution) => ( openExecutionDetail(execution)} >
{execution.task_name || execution.task_type} {execution.task_id}
{STATUS_LABELS[execution.status] || execution.status} {TRIGGER_LABELS[execution.triggered_by] || execution.triggered_by} {execution.retry_count}/{execution.max_retry} {formatDuration(execution.duration)} {execution.error_message || execution.result || '-'} {formatDateTime(execution.created_at)}
))}
)}
共 {executionsTotal} 条,当前第 {executionsPage}/{totalPages} 页
任务执行详情 {selectedExecution?.task_name || selectedExecution?.task_type || '任务记录'}
{detailLoading && !selectedExecution ? ( ) : selectedExecution ? (
状态
{STATUS_LABELS[selectedExecution.status] || selectedExecution.status}
触发来源
{TRIGGER_LABELS[selectedExecution.triggered_by] || selectedExecution.triggered_by}
重试次数
{selectedExecution.retry_count}/ {selectedExecution.max_retry}
耗时
{formatDuration(selectedExecution.duration)}
{selectedExecution.task_id}
{formatDateTime(selectedExecution.created_at)}
{formatDateTime(selectedExecution.started_at)}
{formatDateTime(selectedExecution.finished_at)}
{formatDateTime(selectedExecution.updated_at)}
{selectedExecution.result || '-'}
{selectedExecution.error_message && (
{selectedExecution.error_message}
)}
                    {selectedExecution.payload || '{}'}
                  
                    {selectedExecution.log || '暂无日志'}
                  
) : ( )}
{selectedExecution && selectedExecution.status === 'failed' && selectedExecution.retryable && ( )}
); }