微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
@@ -7,13 +7,15 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { Input, Button, Space, message, Card, Typography, Tag, Badge } from "antd";
|
||||
import { Input, Button, Space, message, Card, Typography, Tag, Badge, Segmented } from "antd";
|
||||
import {
|
||||
LinkOutlined, DisconnectOutlined, HistoryOutlined,
|
||||
FileTextOutlined, SearchOutlined, ClearOutlined,
|
||||
AppstoreOutlined, UnorderedListOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { apiClient } from "../api/client";
|
||||
import LogStream from "../components/LogStream";
|
||||
import TaskLogViewer from "../components/TaskLogViewer";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -36,6 +38,8 @@ const LogViewer: React.FC = () => {
|
||||
const [lines, setLines] = useState<string[]>([]);
|
||||
const [filterKeyword, setFilterKeyword] = useState("");
|
||||
const [connected, setConnected] = useState(false);
|
||||
/** 展示模式:raw = 原始流,grouped = 按任务分组 */
|
||||
const [viewMode, setViewMode] = useState<"raw" | "grouped">("grouped");
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,20 +120,37 @@ const LogViewer: React.FC = () => {
|
||||
<Button icon={<HistoryOutlined />} onClick={handleLoadHistory}>加载历史</Button>
|
||||
<Button icon={<ClearOutlined />} onClick={handleClear} disabled={lines.length === 0}>清空</Button>
|
||||
</Space>
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="过滤关键词..."
|
||||
value={filterKeyword}
|
||||
onChange={(e) => setFilterKeyword(e.target.value)}
|
||||
allowClear
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Space>
|
||||
<Segmented
|
||||
value={viewMode}
|
||||
onChange={(v) => setViewMode(v as "raw" | "grouped")}
|
||||
options={[
|
||||
{ value: "grouped", icon: <AppstoreOutlined />, label: "按任务" },
|
||||
{ value: "raw", icon: <UnorderedListOutlined />, label: "原始" },
|
||||
]}
|
||||
size="small"
|
||||
/>
|
||||
{viewMode === "raw" && (
|
||||
<Input
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder="过滤关键词..."
|
||||
value={filterKeyword}
|
||||
onChange={(e) => setFilterKeyword(e.target.value)}
|
||||
allowClear
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
{/* 日志流 */}
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
<LogStream executionId={executionId} lines={filteredLines} />
|
||||
{/* 日志展示区域 */}
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
||||
{viewMode === "grouped" ? (
|
||||
<TaskLogViewer lines={lines} />
|
||||
) : (
|
||||
<LogStream executionId={executionId} lines={filteredLines} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,12 @@ import {
|
||||
Tooltip,
|
||||
Segmented,
|
||||
Spin,
|
||||
Modal,
|
||||
Form,
|
||||
Select,
|
||||
TimePicker,
|
||||
Descriptions,
|
||||
Tag,
|
||||
} from "antd";
|
||||
import {
|
||||
SendOutlined,
|
||||
@@ -35,16 +41,20 @@ import {
|
||||
SyncOutlined,
|
||||
ShopOutlined,
|
||||
ApiOutlined,
|
||||
ScheduleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import TaskSelector from "../components/TaskSelector";
|
||||
import { validateTaskConfig, fetchFlows } from "../api/tasks";
|
||||
import type { FlowDef, ProcessingModeDef } from "../api/tasks";
|
||||
import { submitToQueue, executeDirectly } from "../api/execution";
|
||||
import { createSchedule } from "../api/schedules";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import BusinessDayHint from "../components/BusinessDayHint";
|
||||
import type { RadioChangeEvent } from "antd";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import type { TaskConfig as TaskConfigType } from "../types";
|
||||
import dayjs from "dayjs";
|
||||
import type { TaskConfig as TaskConfigType, ScheduleConfig } from "../types";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
@@ -224,6 +234,12 @@ const TaskConfig: React.FC = () => {
|
||||
const [forceFull, setForceFull] = useState(false);
|
||||
const [useLocalJson, setUseLocalJson] = useState(false);
|
||||
|
||||
/* ---------- Pipeline 调优 ---------- */
|
||||
const [pipelineWorkers, setPipelineWorkers] = useState<number | null>(null);
|
||||
const [pipelineBatchSize, setPipelineBatchSize] = useState<number | null>(null);
|
||||
const [pipelineRateMin, setPipelineRateMin] = useState<number | null>(null);
|
||||
const [pipelineRateMax, setPipelineRateMax] = useState<number | null>(null);
|
||||
|
||||
/* ---------- CLI 预览 ---------- */
|
||||
const [cliCommand, setCliCommand] = useState<string>("");
|
||||
const [cliEdited, setCliEdited] = useState(false);
|
||||
@@ -232,6 +248,12 @@ const TaskConfig: React.FC = () => {
|
||||
/* ---------- 提交状态 ---------- */
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
/* ---------- 调度任务弹窗 ---------- */
|
||||
const [scheduleModalOpen, setScheduleModalOpen] = useState(false);
|
||||
const [scheduleSubmitting, setScheduleSubmitting] = useState(false);
|
||||
const [scheduleType, setScheduleType] = useState<string>("daily");
|
||||
const [scheduleForm] = Form.useForm();
|
||||
|
||||
/* ---------- 派生状态 ---------- */
|
||||
const layers = flowDefs[flow]?.layers ?? [];
|
||||
const showVerifyOption = processingMode === "verify_only";
|
||||
@@ -262,6 +284,10 @@ const TaskConfig: React.FC = () => {
|
||||
/* CHANGE [2026-02-19] intent: DWD 表正向勾选,选中=装载 */
|
||||
dwd_only_tables: layers.includes("DWD") ? (selectedDwdTables.length > 0 ? selectedDwdTables : null) : null,
|
||||
force_full: forceFull,
|
||||
pipeline_workers: pipelineWorkers,
|
||||
pipeline_batch_size: pipelineBatchSize,
|
||||
pipeline_rate_min: pipelineRateMin,
|
||||
pipeline_rate_max: pipelineRateMax,
|
||||
extra_args: {},
|
||||
};
|
||||
};
|
||||
@@ -288,7 +314,8 @@ const TaskConfig: React.FC = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [flow, processingMode, fetchBeforeVerify, windowMode, lookbackHours, overlapSeconds,
|
||||
windowStart, windowEnd, windowSplitDays, selectedTasks, selectedDwdTables,
|
||||
dryRun, forceFull, useLocalJson, selectedConnectorStores]);
|
||||
dryRun, forceFull, useLocalJson, selectedConnectorStores,
|
||||
pipelineWorkers, pipelineBatchSize, pipelineRateMin, pipelineRateMax]);
|
||||
|
||||
/* ---------- 事件处理 ---------- */
|
||||
const handleFlowChange = (e: RadioChangeEvent) => setFlow(e.target.value);
|
||||
@@ -321,6 +348,67 @@ const TaskConfig: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/* ---------- 调度任务弹窗 ---------- */
|
||||
const handleOpenScheduleModal = () => {
|
||||
scheduleForm.resetFields();
|
||||
scheduleForm.setFieldsValue({
|
||||
schedule_config: {
|
||||
schedule_type: "daily",
|
||||
interval_value: 1,
|
||||
interval_unit: "hours",
|
||||
daily_time: dayjs("04:00", "HH:mm"),
|
||||
weekly_days: [1],
|
||||
weekly_time: dayjs("04:00", "HH:mm"),
|
||||
cron_expression: "0 4 * * *",
|
||||
},
|
||||
});
|
||||
setScheduleType("daily");
|
||||
setScheduleModalOpen(true);
|
||||
};
|
||||
|
||||
const handleScheduleSubmit = async () => {
|
||||
try {
|
||||
const values = await scheduleForm.validateFields();
|
||||
setScheduleSubmitting(true);
|
||||
|
||||
const cfg = { ...values.schedule_config };
|
||||
if (cfg.daily_time && typeof cfg.daily_time !== "string") {
|
||||
cfg.daily_time = cfg.daily_time.format("HH:mm");
|
||||
}
|
||||
if (cfg.weekly_time && typeof cfg.weekly_time !== "string") {
|
||||
cfg.weekly_time = cfg.weekly_time.format("HH:mm");
|
||||
}
|
||||
|
||||
const scheduleConfig: ScheduleConfig = {
|
||||
schedule_type: cfg.schedule_type ?? "daily",
|
||||
interval_value: cfg.interval_value ?? 1,
|
||||
interval_unit: cfg.interval_unit ?? "hours",
|
||||
daily_time: cfg.daily_time ?? "04:00",
|
||||
weekly_days: cfg.weekly_days ?? [1],
|
||||
weekly_time: cfg.weekly_time ?? "04:00",
|
||||
cron_expression: cfg.cron_expression ?? "0 4 * * *",
|
||||
enabled: true,
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
};
|
||||
|
||||
const taskConfig = buildTaskConfig();
|
||||
await createSchedule({
|
||||
name: values.name,
|
||||
task_codes: taskConfig.tasks,
|
||||
task_config: taskConfig,
|
||||
schedule_config: scheduleConfig,
|
||||
run_immediately: !!values.run_immediately,
|
||||
});
|
||||
message.success("调度任务已创建");
|
||||
setScheduleModalOpen(false);
|
||||
} catch {
|
||||
// 表单验证失败
|
||||
} finally {
|
||||
setScheduleSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* ---------- 样式常量 ---------- */
|
||||
const cardStyle = { marginBottom: 12 };
|
||||
const sectionTitleStyle: React.CSSProperties = {
|
||||
@@ -464,6 +552,7 @@ const TaskConfig: React.FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Text style={sectionTitleStyle}>开始日期</Text>
|
||||
@@ -481,6 +570,10 @@ const TaskConfig: React.FC = () => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<BusinessDayHint />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 12 }}>
|
||||
@@ -526,6 +619,65 @@ const TaskConfig: React.FC = () => {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Pipeline 调优参数 */}
|
||||
<div style={{ marginTop: 12, borderTop: "1px solid #f0f0f0", paddingTop: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12, marginBottom: 8, display: "block" }}>
|
||||
Pipeline 调优(留空使用默认值)
|
||||
</Text>
|
||||
<Row gutter={[24, 8]}>
|
||||
<Col span={6}>
|
||||
<Text style={{ fontSize: 12 }}>并发 workers</Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={1}
|
||||
max={32}
|
||||
placeholder="默认 3"
|
||||
value={pipelineWorkers}
|
||||
onChange={(v) => setPipelineWorkers(v)}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Text style={{ fontSize: 12 }}>批量大小</Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={1}
|
||||
max={10000}
|
||||
placeholder="默认 200"
|
||||
value={pipelineBatchSize}
|
||||
onChange={(v) => setPipelineBatchSize(v)}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Text style={{ fontSize: 12 }}>限流下限(秒)</Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={60}
|
||||
step={0.1}
|
||||
placeholder="默认 1.0"
|
||||
value={pipelineRateMin}
|
||||
onChange={(v) => setPipelineRateMin(v)}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Text style={{ fontSize: 12 }}>限流上限(秒)</Text>
|
||||
<InputNumber
|
||||
size="small"
|
||||
min={0}
|
||||
max={60}
|
||||
step={0.1}
|
||||
placeholder="默认 3.0"
|
||||
value={pipelineRateMax}
|
||||
onChange={(v) => setPipelineRateMax(v)}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ---- 任务选择(含 DWD 表过滤) ---- */}
|
||||
@@ -587,26 +739,142 @@ const TaskConfig: React.FC = () => {
|
||||
|
||||
{/* ---- 操作按钮 ---- */}
|
||||
<Card size="small" style={{ marginBottom: 24 }}>
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SendOutlined />}
|
||||
loading={submitting}
|
||||
onClick={handleSubmitToQueue}
|
||||
>
|
||||
提交到队列
|
||||
</Button>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SendOutlined />}
|
||||
loading={submitting}
|
||||
onClick={handleSubmitToQueue}
|
||||
>
|
||||
提交到队列
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ScheduleOutlined />}
|
||||
onClick={handleOpenScheduleModal}
|
||||
>
|
||||
添加到调度任务
|
||||
</Button>
|
||||
</Space>
|
||||
<div style={{ flex: 1 }} />
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={submitting}
|
||||
onClick={handleExecuteDirectly}
|
||||
>
|
||||
直接执行
|
||||
直接执行!
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* ---- 调度任务创建 Modal ---- */}
|
||||
<Modal
|
||||
title="添加到调度任务"
|
||||
open={scheduleModalOpen}
|
||||
onOk={handleScheduleSubmit}
|
||||
onCancel={() => setScheduleModalOpen(false)}
|
||||
confirmLoading={scheduleSubmitting}
|
||||
destroyOnClose
|
||||
width={560}
|
||||
>
|
||||
{/* 当前配置摘要(只读) */}
|
||||
<Descriptions
|
||||
column={1}
|
||||
bordered
|
||||
size="small"
|
||||
style={{ marginBottom: 16 }}
|
||||
title="当前任务配置(只读)"
|
||||
>
|
||||
<Descriptions.Item label="任务">
|
||||
{selectedTasks.length > 0
|
||||
? selectedTasks.map((t) => <Tag key={t}>{t}</Tag>)
|
||||
: <Text type="secondary">未选择</Text>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Flow">
|
||||
<Tag>{flow}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="处理模式">{processingMode}</Descriptions.Item>
|
||||
<Descriptions.Item label="时间窗口">
|
||||
{windowMode === "lookback"
|
||||
? `回溯 ${lookbackHours} 小时`
|
||||
: `${windowStart?.format("YYYY-MM-DD") ?? "?"} ~ ${windowEnd?.format("YYYY-MM-DD") ?? "?"}`}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Form form={scheduleForm} layout="vertical" preserve={false}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="调度任务名称"
|
||||
rules={[{ required: true, message: "请输入调度任务名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:每日全量同步" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name={["schedule_config", "schedule_type"]}
|
||||
label="调度类型"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ label: "一次性", value: "once" },
|
||||
{ label: "固定间隔", value: "interval" },
|
||||
{ label: "每日", value: "daily" },
|
||||
{ label: "每周", value: "weekly" },
|
||||
{ label: "Cron", value: "cron" },
|
||||
]}
|
||||
onChange={(v: string) => setScheduleType(v)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{scheduleType === "interval" && (
|
||||
<Space>
|
||||
<Form.Item name={["schedule_config", "interval_value"]} noStyle rules={[{ required: true }]}>
|
||||
<InputNumber min={1} placeholder="间隔值" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["schedule_config", "interval_unit"]} noStyle rules={[{ required: true }]}>
|
||||
<Select style={{ width: 100 }} options={[
|
||||
{ label: "分钟", value: "minutes" },
|
||||
{ label: "小时", value: "hours" },
|
||||
{ label: "天", value: "days" },
|
||||
]} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
)}
|
||||
{scheduleType === "daily" && (
|
||||
<Form.Item name={["schedule_config", "daily_time"]} label="执行时间" rules={[{ required: true }]}>
|
||||
<TimePicker format="HH:mm" />
|
||||
</Form.Item>
|
||||
)}
|
||||
{scheduleType === "weekly" && (
|
||||
<>
|
||||
<Form.Item name={["schedule_config", "weekly_days"]} label="星期" rules={[{ required: true }]}>
|
||||
<Checkbox.Group options={[
|
||||
{ label: "周一", value: 1 }, { label: "周二", value: 2 },
|
||||
{ label: "周三", value: 3 }, { label: "周四", value: 4 },
|
||||
{ label: "周五", value: 5 }, { label: "周六", value: 6 },
|
||||
{ label: "周日", value: 0 },
|
||||
]} />
|
||||
</Form.Item>
|
||||
<Form.Item name={["schedule_config", "weekly_time"]} label="执行时间" rules={[{ required: true }]}>
|
||||
<TimePicker format="HH:mm" />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{scheduleType === "cron" && (
|
||||
<Form.Item name={["schedule_config", "cron_expression"]} label="Cron 表达式" rules={[{ required: true }]}>
|
||||
<Input placeholder="0 4 * * *" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item name="run_immediately" valuePropName="checked" style={{ marginBottom: 0 }}>
|
||||
<Checkbox>创建后立即执行一次</Checkbox>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -138,6 +138,14 @@ const QueueTab: React.FC = () => {
|
||||
};
|
||||
|
||||
const columns: ColumnsType<QueuedTask> = [
|
||||
{
|
||||
title: '任务 ID', dataIndex: 'id', key: 'id', width: 120,
|
||||
render: (id: string) => (
|
||||
<Text copyable={{ text: id }} style={{ fontSize: 11 }}>
|
||||
{id.slice(0, 8)}…
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务', dataIndex: ['config', 'tasks'], key: 'tasks',
|
||||
render: (tasks: string[]) => (
|
||||
@@ -234,6 +242,26 @@ const HistoryTab: React.FC = () => {
|
||||
const [detail, setDetail] = useState<ExecutionLog | null>(null);
|
||||
const [historyLogLines, setHistoryLogLines] = useState<string[]>([]);
|
||||
const [logLoading, setLogLoading] = useState(false);
|
||||
const [wsConnected, setWsConnected] = useState(false);
|
||||
const historyWsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
/** 关闭 WebSocket 连接并重置状态 */
|
||||
const closeHistoryWs = useCallback(() => {
|
||||
historyWsRef.current?.close();
|
||||
historyWsRef.current = null;
|
||||
setWsConnected(false);
|
||||
}, []);
|
||||
|
||||
/* 组件卸载时清理 WebSocket */
|
||||
useEffect(() => {
|
||||
return () => { historyWsRef.current?.close(); };
|
||||
}, []);
|
||||
|
||||
const handleCancelHistory = useCallback(async (id: string) => {
|
||||
try { await cancelExecution(id); message.success('已发送终止信号'); load(); }
|
||||
catch { message.error('终止失败'); }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -244,29 +272,73 @@ const HistoryTab: React.FC = () => {
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
/** 点击行时加载详情和日志 */
|
||||
/** 点击行时加载详情和日志;running 任务走 WebSocket 实时流 */
|
||||
const handleRowClick = useCallback(async (record: ExecutionLog) => {
|
||||
setDetail(record);
|
||||
setHistoryLogLines([]);
|
||||
setLogLoading(true);
|
||||
try {
|
||||
const { data: logData } = await apiClient.get<{
|
||||
execution_id: string;
|
||||
output_log: string | null;
|
||||
error_log: string | null;
|
||||
}>(`/execution/${record.id}/logs`);
|
||||
const parts: string[] = [];
|
||||
if (logData.output_log) parts.push(logData.output_log);
|
||||
if (logData.error_log) parts.push(logData.error_log);
|
||||
setHistoryLogLines(parts.join('\n').split('\n').filter(Boolean));
|
||||
} catch {
|
||||
/* 日志可能不存在,静默处理 */
|
||||
} finally {
|
||||
setLogLoading(false);
|
||||
closeHistoryWs();
|
||||
|
||||
if (record.status === 'running') {
|
||||
// running 任务:通过 WebSocket 实时推送日志
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
const ws = new WebSocket(`${protocol}//${host}/ws/logs/${record.id}`);
|
||||
historyWsRef.current = ws;
|
||||
|
||||
ws.onopen = () => { setWsConnected(true); setLogLoading(false); };
|
||||
ws.onmessage = (event) => {
|
||||
setHistoryLogLines((prev) => [...prev, event.data]);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
setWsConnected(false);
|
||||
// 任务结束后刷新历史列表以更新状态
|
||||
load();
|
||||
};
|
||||
ws.onerror = () => {
|
||||
message.error('WebSocket 连接失败,回退到静态日志');
|
||||
setWsConnected(false);
|
||||
// 回退:用 REST API 拉取已有日志
|
||||
apiClient.get<{
|
||||
execution_id: string;
|
||||
output_log: string | null;
|
||||
error_log: string | null;
|
||||
}>(`/execution/${record.id}/logs`).then(({ data: logData }) => {
|
||||
const parts: string[] = [];
|
||||
if (logData.output_log) parts.push(logData.output_log);
|
||||
if (logData.error_log) parts.push(logData.error_log);
|
||||
setHistoryLogLines(parts.join('\n').split('\n').filter(Boolean));
|
||||
}).catch(() => {}).finally(() => setLogLoading(false));
|
||||
};
|
||||
} else {
|
||||
// 已完成任务:REST API 一次性拉取
|
||||
try {
|
||||
const { data: logData } = await apiClient.get<{
|
||||
execution_id: string;
|
||||
output_log: string | null;
|
||||
error_log: string | null;
|
||||
}>(`/execution/${record.id}/logs`);
|
||||
const parts: string[] = [];
|
||||
if (logData.output_log) parts.push(logData.output_log);
|
||||
if (logData.error_log) parts.push(logData.error_log);
|
||||
setHistoryLogLines(parts.join('\n').split('\n').filter(Boolean));
|
||||
} catch {
|
||||
/* 日志可能不存在,静默处理 */
|
||||
} finally {
|
||||
setLogLoading(false);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [closeHistoryWs, load]);
|
||||
|
||||
const columns: ColumnsType<ExecutionLog> = [
|
||||
{
|
||||
title: '执行 ID', dataIndex: 'id', key: 'id', width: 120,
|
||||
render: (id: string) => (
|
||||
<Text copyable={{ text: id }} style={{ fontSize: 11 }}>
|
||||
{id.slice(0, 8)}…
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '任务', dataIndex: 'task_codes', key: 'task_codes',
|
||||
render: (codes: string[]) => (
|
||||
@@ -275,6 +347,12 @@ const HistoryTab: React.FC = () => {
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '调度 ID', dataIndex: 'schedule_id', key: 'schedule_id', width: 120,
|
||||
render: (id: string | null) => id
|
||||
? <Text copyable={{ text: id }} style={{ fontSize: 11 }}>{id.slice(0, 8)}…</Text>
|
||||
: '—',
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 90,
|
||||
render: (s: string) => <Tag color={STATUS_COLOR[s] ?? 'default'}>{s}</Tag>,
|
||||
@@ -287,6 +365,21 @@ const HistoryTab: React.FC = () => {
|
||||
<Tag color={v === 0 ? 'success' : 'error'}>{v}</Tag>
|
||||
) : '—',
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 80, align: 'center',
|
||||
render: (_: unknown, record: ExecutionLog) => {
|
||||
if (record.status === 'running') {
|
||||
return (
|
||||
<Popconfirm title="确认终止该任务?" onConfirm={(e) => { e?.stopPropagation(); handleCancelHistory(record.id); }} onCancel={(e) => e?.stopPropagation()}>
|
||||
<Button type="link" danger icon={<StopOutlined />} size="small" onClick={(e) => e.stopPropagation()}>
|
||||
终止
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -303,7 +396,18 @@ const HistoryTab: React.FC = () => {
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
title="执行详情" open={!!detail} onClose={() => setDetail(null)}
|
||||
title={
|
||||
<Space>
|
||||
<span>执行详情</span>
|
||||
{detail?.status === 'running' && (
|
||||
wsConnected
|
||||
? <Tag color="processing">实时连接中</Tag>
|
||||
: <Tag>未连接</Tag>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
open={!!detail}
|
||||
onClose={() => { closeHistoryWs(); setDetail(null); }}
|
||||
width={720}
|
||||
styles={{ body: { padding: 12 } }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user