/** * AI 调度状态页面。 * * - 顶部筛选器:event_type / status / site_id / 日期范围 * - 统计行:今日去重跳过数 * - 主体:分页表格(事件类型、会员、状态、执行链、耗时、操作列) * - 操作列:查看详情 Modal、手动重跑 Popconfirm */ import React, { useEffect, useState, useCallback } from "react"; import { Card, Table, Tag, Select, Button, DatePicker, Row, Col, Space, Statistic, Modal, Popconfirm, Descriptions, message, Typography, } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { getTriggerJobs, getTriggerJobDetail, retryTriggerJob, type TriggerJobItem, type TriggerJobDetailResponse, type TriggerJobQuery, } from "../api/adminAI"; const { RangePicker } = DatePicker; const { Title } = Typography; const STATUS_COLOR: Record = { pending: "default", running: "processing", success: "success", failed: "error", skipped_duplicate: "warning", timeout: "orange", }; function fmtTime(raw: string | null): string { if (!raw) return "—"; const d = new Date(raw); return Number.isNaN(d.getTime()) ? raw : d.toLocaleString("zh-CN"); } function calcDuration(start: string | null, end: string | null): string { if (!start || !end) return "—"; const ms = new Date(end).getTime() - new Date(start).getTime(); if (Number.isNaN(ms) || ms < 0) return "—"; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; } const AITriggerJobs: React.FC = () => { const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [skipped, setSkipped] = useState(0); const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [pageSize] = useState(20); // 筛选状态 const [eventType, setEventType] = useState(); const [status, setStatus] = useState(); const [siteId, setSiteId] = useState(); const [dateRange, setDateRange] = useState<[string, string] | null>(null); // 详情 Modal const [detailVisible, setDetailVisible] = useState(false); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const load = useCallback(async () => { setLoading(true); try { const params: TriggerJobQuery = { page, page_size: pageSize, event_type: eventType, status, site_id: siteId, date_from: dateRange?.[0], date_to: dateRange?.[1], }; const res = await getTriggerJobs(params); setItems(res.items); setTotal(res.total); setSkipped(res.today_skipped_duplicates); } catch { message.error("加载调度任务失败"); } finally { setLoading(false); } }, [page, pageSize, eventType, status, siteId, dateRange]); useEffect(() => { load(); }, [load]); const handleViewDetail = async (id: number) => { setDetailLoading(true); setDetailVisible(true); try { const res = await getTriggerJobDetail(id); setDetail(res); } catch { message.error("加载详情失败"); } finally { setDetailLoading(false); } }; const handleRetry = async (id: number) => { try { const res = await retryTriggerJob(id); message.success(`已创建重跑任务 #${res.trigger_job_id}`); load(); } catch { message.error("重跑失败"); } }; const columns: ColumnsType = [ { title: "ID", dataIndex: "id", key: "id", width: 70 }, { title: "事件类型", dataIndex: "event_type", key: "event_type", width: 140 }, { title: "会员 ID", dataIndex: "member_id", key: "member_id", width: 100, render: (v) => v ?? "—" }, { title: "状态", dataIndex: "status", key: "status", width: 120, render: (v: string) => {v}, }, { title: "执行链", dataIndex: "app_chain", key: "app_chain", ellipsis: true, render: (v) => v ?? "—" }, { title: "强制执行", dataIndex: "is_forced", key: "is_forced", width: 80, render: (v: boolean) => v ? : "否", }, { title: "耗时", key: "duration", width: 90, render: (_: unknown, r: TriggerJobItem) => calcDuration(r.started_at, r.finished_at), }, { title: "创建时间", dataIndex: "created_at", key: "created_at", width: 170, render: fmtTime }, { title: "操作", key: "action", width: 160, fixed: "right" as const, render: (_: unknown, r: TriggerJobItem) => ( handleRetry(r.id)}> ), }, ]; return (
AI 调度状态 {/* 筛选器行 */} { setStatus(v); setPage(1); }} options={[ { label: "pending", value: "pending" }, { label: "running", value: "running" }, { label: "success", value: "success" }, { label: "failed", value: "failed" }, { label: "skipped_duplicate", value: "skipped_duplicate" }, ]} />