"use client" import {useCallback, useEffect, useState} from "react" import {toast} from "sonner" import {format} from "date-fns" import {Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw} from "lucide-react" import {AdminService, TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services" 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: "定时", } 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 [taskTypes, setTaskTypes] = useState([]) const [executionsLoading, setExecutionsLoading] = useState(false) const [executionsError, setExecutionsError] = useState(null) const [executions, setExecutions] = useState([]) const [executionsTotal, setExecutionsTotal] = useState(0) const [executionsPage, setExecutionsPage] = useState(1) const [executionStatus, setExecutionStatus] = useState("all") const [executionTaskType, setExecutionTaskType] = useState("all") const [selectedExecution, setSelectedExecution] = useState(null) const [detailOpen, setDetailOpen] = useState(false) const [detailLoading, setDetailLoading] = useState(false) const [retrying, setRetrying] = useState(false) const fetchTaskTypes = useCallback(async () => { try { const data = await AdminService.getTaskTypes() setTaskTypes(data) } catch { setTaskTypes([]) } }, []) const fetchTaskExecutions = useCallback(async (overrides?: { page?: number status?: TaskExecutionStatus | "all" taskType?: string }) => { try { setExecutionsLoading(true) setExecutionsError(null) const page = overrides?.page ?? executionsPage const status = overrides?.status ?? executionStatus const taskType = overrides?.taskType ?? executionTaskType const data = await AdminService.listTaskExecutions({ page, page_size: 10, status: status === "all" ? undefined : status, task_type: taskType === "all" ? undefined : taskType, }) setExecutions(data.items || []) setExecutionsTotal(data.total || 0) } catch (err) { setExecutionsError(err instanceof Error ? err : new Error("加载任务执行记录失败")) } finally { setExecutionsLoading(false) } }, [executionStatus, executionTaskType, executionsPage]) useEffect(() => { fetchTaskTypes() }, [fetchTaskTypes]) useEffect(() => { fetchTaskExecutions() }, [fetchTaskExecutions]) useEffect(() => { setExecutionsPage(1) }, [executionStatus, executionTaskType]) const openExecutionDetail = async (execution: TaskExecution) => { setSelectedExecution(execution) setDetailOpen(true) try { setDetailLoading(true) const detail = await AdminService.getTaskExecution(execution.id) setSelectedExecution(detail) } catch (err) { toast.error("任务详情加载失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setDetailLoading(false) } } const handleRetryExecution = async () => { if (!selectedExecution) return try { setRetrying(true) const taskID = await AdminService.retryTaskExecution(selectedExecution.id) toast.success("任务已重新下发", { description: `新任务 ID:${ taskID }`, }) await fetchTaskExecutions() } catch (err) { toast.error("任务重试失败", { description: err instanceof Error ? err.message : "未知错误", }) } finally { setRetrying(false) } } const totalPages = Math.max(1, Math.ceil(executionsTotal / 10)) return (

{executionsError ? (
) : 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 !== "pending" && selectedExecution.status !== "running" && selectedExecution.retryable && selectedExecution.retry_count < selectedExecution.max_retry && ( )}
) }