import { Card, Badge, Button, Modal, Table, Tag } from "antd"; import type { ColumnsType } from "antd/es/table"; import { SearchControls } from "@/components/SearchControls"; import { queryExecutionLogUsingPost } from "../collection.apis"; import useFetchData from "@/hooks/useFetchData"; import { useEffect, useState } from "react"; import { TaskExecution } from "@/pages/DataCollection/collection.model.ts"; import { mapTaskExecution } from "@/pages/DataCollection/collection.const.ts"; import { queryExecutionLogFileByIdUsingGet } from "../collection.apis"; import { FileTextOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { appendTokenToUrl } from "@/utils/request"; export default function Execution({ taskId }: { taskId?: string }) { const { t } = useTranslation(); const [dateRange, setDateRange] = useState<[any, any] | null>(null); const [logOpen, setLogOpen] = useState(false); const [logLoading, setLogLoading] = useState(false); const [logTitle, setLogTitle] = useState(""); const [logContent, setLogContent] = useState(""); const [logFilename, setLogFilename] = useState(""); const [logBlobUrl, setLogBlobUrl] = useState(""); const filterOptions = [ { key: "status", label: t("dataCollection.execution.filters.statusFilter"), options: [ { value: "all", label: t("dataCollection.execution.filters.allStatus") }, { value: "RUNNING", label: t("dataCollection.execution.filters.running") }, { value: "COMPLETED", label: t("dataCollection.execution.filters.success") }, { value: "FAILED", label: t("dataCollection.execution.filters.failed") }, ], }, ]; const formatDuration = (seconds?: number) => { if (seconds === undefined || seconds === null) return t("common.placeholders.empty"); const total = Math.max(0, Math.floor(seconds)); if (total < 60) return `${total}${t("dataCollection.execution.duration.secondsSuffix")}`; const min = Math.floor(total / 60); const sec = total % 60; return `${min}${t("dataCollection.execution.duration.minutesSuffix")}${sec}${t("dataCollection.execution.duration.secondsSuffix")}`; }; const handleReset = () => { setDateRange(null); setSearchParams({ keyword: "", filter: { type: [], status: [], tags: [], }, current: 1, pageSize: 10, start_time: undefined, end_time: undefined, }); }; const { loading, tableData, pagination, searchParams, setSearchParams, handleFiltersChange, handleKeywordChange, } = useFetchData( (params) => { const { keyword, start_time, end_time, status, ...rest } = params || {}; return queryExecutionLogUsingPost({ ...rest, task_id: taskId || undefined, task_name: keyword || undefined, status: status || undefined, start_time, end_time, }); }, (execution) => mapTaskExecution(execution, t), 30000, false, [], 0 ); useEffect(() => { setSearchParams((prev) => ({ ...prev, current: 1, })); }, [taskId, setSearchParams, t]); const handleViewLog = async (record: TaskExecution) => { setLogOpen(true); setLogLoading(true); setLogTitle(`${record.taskName} / ${record.id}`); setLogContent(""); setLogFilename(""); if (logBlobUrl) { URL.revokeObjectURL(logBlobUrl); setLogBlobUrl(""); } try { const { blob, filename } = await queryExecutionLogFileByIdUsingGet(record.id); setLogFilename(filename); const url = URL.createObjectURL(blob); setLogBlobUrl(url); const text = await blob.text(); setLogContent(text); } catch (e: any) { setLogContent(e?.data?.detail || e?.message || t("dataCollection.execution.messages.loadLogFailed")); } finally { setLogLoading(false); } }; const columns: ColumnsType = [ { title: t("dataCollection.execution.columns.taskName"), dataIndex: "taskName", key: "taskName", fixed: "left", render: (text: string) => ( {text} ), }, { title: t("dataCollection.execution.columns.status"), dataIndex: "status", key: "status", render: (status: any) => (( {status?.label || status} ) ), }, { title: t("dataCollection.execution.columns.startTime"), dataIndex: "startedAt", key: "startedAt", }, { title: t("dataCollection.execution.columns.endTime"), dataIndex: "completedAt", key: "completedAt", }, { title: t("dataCollection.execution.columns.duration"), dataIndex: "durationSeconds", key: "durationSeconds", render: (v?: number) => formatDuration(v), }, { title: t("dataCollection.execution.columns.errorMessage"), dataIndex: "errorMessage", key: "errorMessage", render: (msg?: string) => msg ? ( {msg} ) : ( {t("common.placeholders.empty")} ), }, { title: t("dataCollection.execution.columns.actions"), key: "action", fixed: "right", width: 120, render: (_: any, record: TaskExecution) => ( ), }, ]; return (
{/* Filter Controls */}
{ setDateRange(null); setSearchParams((prev) => ({ ...prev, filter: { ...prev.filter, status: [] }, current: 1, keyword: "", start_time: undefined, end_time: undefined, })); }} showDatePicker dateRange={dateRange as any} onDateChange={(date) => { // 自动修正日期顺序:确保开始时间 <= 结束时间 if (date && date[0] && date[1] && date[0].isAfter(date[1])) { // 如果开始时间晚于结束时间,交换它们 date = [date[1], date[0]]; } setDateRange(date as any); // 转换为不带时区的 ISO 格式字符串(YYYY-MM-DDTHH:mm:ss) const toLocalISOString = (d: any) => { if (!d) return undefined; const pad = (n: number) => String(n).padStart(2, '0'); return `${d.year()}-${pad(d.month() + 1)}-${pad(d.date())}T${pad(d.hour())}:${pad(d.minute())}:${pad(d.second())}`; }; setSearchParams((prev) => ({ ...prev, current: 1, start_time: toLocalISOString(date?.[0]), end_time: toLocalISOString(date?.[1]), })); }} onReload={handleReset} searchPlaceholder={t("dataCollection.execution.filters.searchPlaceholder")} className="flex-1" />
{ setLogOpen(false); if (logBlobUrl) { URL.revokeObjectURL(logBlobUrl); setLogBlobUrl(""); } }} footer={
{logFilename || ""}
{logBlobUrl ? ( ) : null}
} width={900} >
{logLoading ? t("dataCollection.execution.modal.loading") : (logContent || t("dataCollection.execution.modal.empty"))}
); }