"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 {AdminService} from "@/lib/services/admin" 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("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: () => AdminService.getTaskTypes(), }) const executionsQuery = useQuery({ queryKey: ["admin", "task-executions", executionsPage, executionStatus, executionTaskType], queryFn: () => AdminService.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: () => AdminService.getTaskExecution(selectedExecutionId!), enabled: detailOpen && !!selectedExecutionId, }) const retryMutation = useMutation({ mutationFn: (id: string) => AdminService.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 && ( )}
) }