This commit is contained in:
Your Name
2026-04-16 21:35:47 +08:00
parent ff32aa50bf
commit ebf632e651
86 changed files with 14097 additions and 585 deletions

View File

@@ -1,15 +1,17 @@
<template>
<PageCard title="检测控制" description="当前已接入真实状态读取和本地 Worker 启停,后续 Linux 版会沿用同一页接 systemd。">
<PageCard title="检测控制" description="已升级为任务中心:支持启停控制、阶段查看、日志常驻和会话回看,并沉淀线程/代理/进度快照。">
<div class="actions">
<el-button type="primary" :loading="actionLoading === 'start'" @click="invoke('start')">启动检测</el-button>
<el-button :loading="actionLoading === 'stop'" @click="invoke('stop')">停止检测</el-button>
<el-button plain :loading="loading" @click="loadStatus">刷新状态</el-button>
<el-button plain @click="goRuntime">前往运行中心</el-button>
<el-switch v-model="autoRefresh" inline-prompt active-text="自动刷新" inactive-text="手动" />
<span class="updated-at">最近同步{{ lastUpdatedAt || "暂无" }}</span>
</div>
<el-alert
v-if="lastAction.message"
:title="`${lastAction.label}${lastAction.message}`"
:title="`${lastAction.label}${lastAction.message}${lastAction.at ? `${lastAction.at}` : ''}`"
:type="lastAction.type"
:closable="false"
show-icon
@@ -23,14 +25,34 @@
<el-descriptions-item label="API 服务">{{ status.api_service_name || "-" }}</el-descriptions-item>
<el-descriptions-item label="检测进程数">{{ status.worker_process_count }}</el-descriptions-item>
<el-descriptions-item label="线程数">{{ status.thread_count }}</el-descriptions-item>
<el-descriptions-item label="线程来源">{{ threadCountSummary }}</el-descriptions-item>
<el-descriptions-item label="代理启用">{{ status.proxy_enable ? "是" : "否" }}</el-descriptions-item>
<el-descriptions-item label="允许直连">{{ status.allow_direct ? "是" : "否" }}</el-descriptions-item>
<el-descriptions-item label="代理池数量">{{ status.proxy_pool_count }}</el-descriptions-item>
<el-descriptions-item label="可用代理数">{{ status.available_proxy_count }}</el-descriptions-item>
<el-descriptions-item label="代理状态">{{ status.proxy_runtime_label || "-" }}</el-descriptions-item>
<el-descriptions-item label="代理诊断">{{ proxyDiagnosisText }}</el-descriptions-item>
<el-descriptions-item label="最近代理刷新">{{ status.proxy_last_refresh_time || "暂无" }}</el-descriptions-item>
<el-descriptions-item label="最近 Worker 日志时间">{{ status.last_worker_log_time || "暂无" }}</el-descriptions-item>
<el-descriptions-item label="最近启动时间">{{ status.worker_latest_start_time || "暂无" }}</el-descriptions-item>
</el-descriptions>
<el-alert
v-if="status.proxy_runtime_detail"
:type="proxyAlertType"
:closable="false"
show-icon
:title="status.proxy_runtime_detail"
style="margin-top: 16px"
/>
<el-alert
v-if="status.proxy_supplier_empty"
type="warning"
:closable="false"
show-icon
title="代理供应侧最近返回 200但原始代理数为 0当前判断是供应池为空不是程序拉取失败。"
style="margin-top: 12px"
/>
<el-alert
v-if="status.recent_warning"
type="warning"
@@ -39,6 +61,15 @@
:title="status.recent_warning"
style="margin-top: 16px"
/>
<el-alert
v-for="alert in status.dependency_alerts || []"
:key="`${alert.kind}-${alert.detail}`"
:type="alert.level === 'error' ? 'error' : 'warning'"
:closable="false"
show-icon
:title="`${alert.title}${alert.detail}`"
style="margin-top: 12px"
/>
<el-alert
v-if="status.worker_runtime_message"
type="info"
@@ -47,23 +78,216 @@
:title="status.worker_runtime_message"
style="margin-top: 12px"
/>
<el-alert
v-if="status.recent_event"
type="success"
:closable="false"
show-icon
:title="status.recent_event"
style="margin-top: 12px"
/>
<el-alert
:closable="false"
show-icon
type="info"
:title="`当前检测状态:${currentPhaseLabel}Worker ${status.worker_online ? '在线' : '离线'}。`"
style="margin-top: 12px"
/>
<el-row :gutter="12" style="margin-top: 16px">
<el-row :gutter="12" style="margin-top: 16px; margin-bottom: 16px">
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="待检测" :value="status.progress.pending || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="检测中" :value="status.progress.running || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="检测完成" :value="status.progress.completed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="检测通过" :value="status.progress.completed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="黑名单" :value="status.progress.blacklisted || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="检测失败" :value="status.progress.failed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="检测会话" :value="runs.length" /></el-col>
</el-row>
<div class="summary-grid">
<div class="summary-card">
<div class="summary-title">当前任务</div>
<div class="summary-value task-code">{{ activeJob?.job_code || "-" }}</div>
<div class="summary-note">{{ activeJobSummaryText }}</div>
</div>
<div class="summary-card">
<div class="summary-title">检测进度</div>
<div class="summary-value">{{ status.progress_percent || 0 }}%</div>
<el-progress :percentage="Number(status.progress_percent || 0)" :stroke-width="10" />
</div>
<div class="summary-card">
<div class="summary-title">当前阶段</div>
<div class="summary-value phase-value">{{ currentPhaseLabel }}</div>
<div class="summary-note">{{ currentPhaseDetail }}</div>
</div>
<div class="summary-card">
<div class="summary-title">线程快照</div>
<div class="summary-value">{{ status.active_thread_count || 0 }} / {{ status.max_thread_count || status.thread_count || 0 }}</div>
<div class="summary-note">当前活跃线程 / 配置线程上限</div>
</div>
<div class="summary-card">
<div class="summary-title">代理快照</div>
<div class="summary-value">{{ status.available_proxy_count || 0 }} / {{ status.proxy_pool_count || 0 }}</div>
<div class="summary-note">{{ proxyDiagnosisText }}</div>
</div>
</div>
<el-row v-if="activeJob" :gutter="12" style="margin-bottom: 16px">
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务待领" :value="activeJob.items_pending || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务已领" :value="activeJob.items_claimed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务执行中" :value="activeJob.items_running || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务完成" :value="activeJob.items_completed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务黑名单" :value="activeJob.items_blacklisted || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务失败" :value="activeJob.items_failed || 0" /></el-col>
</el-row>
<div v-if="activeJob" class="summary-grid summary-grid-tight">
<div class="summary-card">
<div class="summary-title">任务进度</div>
<div class="summary-value">{{ activeJob.progress_percent || 0 }}%</div>
<el-progress :percentage="Number(activeJob.progress_percent || 0)" :stroke-width="10" />
</div>
<div class="summary-card">
<div class="summary-title">最近任务事件</div>
<div class="summary-value event-value">{{ activeJob.latest_event?.event_type || "-" }}</div>
<div class="summary-note">{{ activeJob.latest_event?.message || "当前还没有任务事件" }}</div>
</div>
</div>
<div class="history-panel">
<div class="history-header">
<h3>队列健康度</h3>
<span class="history-note">聚合近 {{ queueSummary.window_minutes || 15 }} 分钟吞吐租约和积压便于后续多机扩容判断</span>
</div>
<div class="summary-grid summary-grid-tight">
<div class="summary-card">
<div class="summary-title">近窗吞吐</div>
<div class="summary-value">{{ queueSummary.throughput?.processed_recent || 0 }}</div>
<div class="summary-note"> {{ queueSummary.window_minutes || 15 }} 分钟处理 {{ queueSummary.throughput?.processed_per_minute || 0 }} /分钟</div>
</div>
<div class="summary-card">
<div class="summary-title">租约风险</div>
<div class="summary-value">{{ queueSummary.queue?.overdue_leases || 0 }}</div>
<div class="summary-note">过期 {{ queueSummary.queue?.overdue_leases || 0 }} / 5 分钟内到期 {{ queueSummary.queue?.expiring_soon_leases || 0 }}</div>
</div>
<div class="summary-card">
<div class="summary-title">最老待领</div>
<div class="summary-value">{{ queueSummary.queue?.oldest_pending_age_minutes || 0 }} 分钟</div>
<div class="summary-note">{{ queueSummary.queue?.oldest_pending_at || "当前没有待领取任务项" }}</div>
</div>
<div class="summary-card">
<div class="summary-title">任务收敛</div>
<div class="summary-value">{{ queueSummary.queue?.terminal_percent || 0 }}%</div>
<div class="summary-note">已收敛 {{ queueSummary.queue?.terminal || 0 }} / {{ queueSummary.queue?.items_total || 0 }}</div>
</div>
</div>
<el-alert
:closable="false"
show-icon
:type="queueAlertType"
style="margin-bottom: 12px"
:title="queueAlertText"
/>
<el-table :data="queueSummary.nodes || []" border style="margin-bottom: 16px" empty-text="当前没有可展示的节点吞吐摘要">
<el-table-column prop="node_code" label="节点" min-width="180" />
<el-table-column prop="items_running" label="执行中" min-width="90" />
<el-table-column prop="items_claimed" label="已领未跑" min-width="100" />
<el-table-column prop="processed_recent" label="近窗处理" min-width="100" />
<el-table-column prop="processed_per_minute" label="项/分钟" min-width="100" />
<el-table-column prop="completed_recent" label="完成" min-width="90" />
<el-table-column prop="blacklisted_recent" label="黑名单" min-width="90" />
<el-table-column prop="failed_recent" label="失败" min-width="90" />
</el-table>
</div>
<el-table v-if="activeJob?.node_stats?.length" :data="activeJob.node_stats" border style="margin-bottom: 16px">
<el-table-column prop="node_code" label="节点" min-width="180" />
<el-table-column prop="items_total" label="任务总数" min-width="100" />
<el-table-column prop="items_pending" label="待领" min-width="90" />
<el-table-column prop="items_claimed" label="已领" min-width="90" />
<el-table-column prop="items_running" label="执行中" min-width="90" />
<el-table-column prop="items_completed" label="完成" min-width="90" />
<el-table-column prop="items_failed" label="失败" min-width="90" />
</el-table>
<el-table v-if="(activeJob?.current_cycle_events || activeJob?.recent_events)?.length" :data="activeJob.current_cycle_events || activeJob.recent_events" border style="margin-bottom: 16px">
<el-table-column prop="created_at" label="事件时间" min-width="160" />
<el-table-column prop="node_code" label="节点" min-width="160" />
<el-table-column prop="event_type" label="事件类型" min-width="160" />
<el-table-column prop="message" label="事件说明" min-width="320" show-overflow-tooltip />
</el-table>
<el-table :data="runs" border highlight-current-row @current-change="handleCurrentRunChange">
<el-table-column label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" effect="dark">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="阶段" min-width="110">
<template #default="{ row }">
<el-tag :type="phaseTagType(row.phase_label, row.status)" plain>{{ row.phase_label || "-" }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="message" label="会话信息" min-width="280" show-overflow-tooltip />
<el-table-column prop="started_at" label="开始时间" min-width="160" />
<el-table-column prop="completed_at" label="结束时间" min-width="160" />
<el-table-column label="线程/代理" min-width="160">
<template #default="{ row }">
<span>线程 {{ row.settings_summary?.thread_count || 0 }} / 代理 {{ row.settings_summary?.proxy_pool_count || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="进度快照" min-width="220">
<template #default="{ row }">
<span>
{{ row.progress?.pending || 0 }}
/ {{ row.progress?.running || 0 }}
/ {{ row.progress?.completed || 0 }}
/ {{ row.progress?.blacklisted || 0 }}
/ 失败 {{ row.progress?.failed || 0 }}
</span>
</template>
</el-table-column>
</el-table>
<div class="task-detail">
<div class="task-detail-header">
<div>
<div class="task-detail-title">任务日志控制台</div>
<div class="task-detail-subtitle">
{{ selectedRun ? `当前查看会话:${selectedRun.run_id}` : "当前暂无检测会话,日志窗口会显示最近 Worker 输出" }}
</div>
</div>
<div class="task-detail-meta">
<el-switch v-model="logAutoFollow" inline-prompt active-text="跟随日志" inactive-text="暂停跟随" size="small" />
<el-button text size="small" @click="scrollLogToBottom(true)">回到底部</el-button>
<span>Worker{{ status.worker_online ? "在线" : "离线" }}</span>
<span v-if="selectedRun?.phase_label">阶段{{ selectedRun.phase_label }}</span>
<span>运行中{{ status.progress.running || 0 }}</span>
<span>线程{{ threadCountSummary }}</span>
</div>
</div>
<div v-if="selectedRun?.phase_detail" class="phase-detail">{{ selectedRun.phase_detail }}</div>
<pre ref="logConsoleRef" class="log-console" @scroll="handleLogScroll">{{ selectedLogText }}</pre>
</div>
<el-table v-if="phaseHistory.length" :data="phaseHistory" border style="margin-top: 16px">
<el-table-column prop="at" label="切换时间" min-width="180" />
<el-table-column prop="label" label="阶段" min-width="120" />
<el-table-column prop="detail" label="阶段说明" min-width="360" show-overflow-tooltip />
</el-table>
</PageCard>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import PageCard from "@/components/PageCard.vue";
import { detectApi } from "@/api/modules";
const DETECT_LAST_ACTION_KEY = "domaincheck:detect:last-action";
const router = useRouter();
const status = ref({
worker_online: false,
worker_mode: "windows-local",
@@ -73,12 +297,32 @@ const status = ref({
worker_latest_start_time: "",
worker_runtime_message: "",
thread_count: 0,
thread_count_default: 0,
thread_count_source: "default",
thread_count_override: null as number | null,
thread_count_node_code: "",
active_thread_count: 0,
max_thread_count: 0,
proxy_enable: false,
allow_direct: false,
proxy_pool_count: 0,
available_proxy_count: 0,
last_worker_log_time: "",
recent_event: "",
recent_warning: "",
proxy_runtime_state: "",
proxy_runtime_label: "",
proxy_runtime_detail: "",
proxy_runtime_reason: "",
proxy_supplier_empty: false,
proxy_direct_fallback_active: false,
proxy_last_refresh_status: "",
proxy_last_refresh_time: "",
dependency_alerts: [] as any[],
log_lines: [] as string[],
runs: [] as any[],
progress_percent: 0,
active_job: null as any,
progress: {
pending: 0,
running: 0,
@@ -88,15 +332,209 @@ const status = ref({
}
});
const threadCountSummary = computed(() => {
const effective = Number(status.value.thread_count || 0);
const defaultValue = Number(status.value.thread_count_default || 0);
const nodeCode = String(status.value.thread_count_node_code || "").trim();
if (status.value.thread_count_source === "node_override" && status.value.thread_count_override) {
return `${effective}(节点 ${nodeCode || "-"} 单独覆盖,默认 ${defaultValue}`;
}
return `${effective}(默认)`;
});
const queueSummary = ref({
window_minutes: 15,
has_active_job: false,
queue: {
items_total: 0,
terminal: 0,
terminal_percent: 0,
overdue_leases: 0,
expiring_soon_leases: 0,
oldest_pending_at: "",
oldest_pending_age_minutes: 0
},
throughput: {
processed_recent: 0,
processed_per_minute: 0
},
nodes: [] as any[]
});
const autoRefresh = ref(true);
const logAutoFollow = ref(true);
const loading = ref(false);
const actionLoading = ref<"" | "start" | "stop">("");
const lastUpdatedAt = ref("");
const selectedRunId = ref("");
const lastAction = ref({
label: "",
message: "",
at: "",
type: "success" as "success" | "warning" | "info" | "error"
});
let timer: number | null = null;
const logConsoleRef = ref<HTMLElement | null>(null);
const persistLastAction = () => {
if (!lastAction.value.message) {
window.localStorage.removeItem(DETECT_LAST_ACTION_KEY);
return;
}
window.localStorage.setItem(DETECT_LAST_ACTION_KEY, JSON.stringify(lastAction.value));
};
const restoreLastAction = () => {
try {
const raw = window.localStorage.getItem(DETECT_LAST_ACTION_KEY);
if (!raw) return;
const parsed = JSON.parse(raw);
lastAction.value = {
label: parsed.label || "",
message: parsed.message || "",
at: parsed.at || "",
type: parsed.type || "info"
};
} catch {
window.localStorage.removeItem(DETECT_LAST_ACTION_KEY);
}
};
const runs = computed(() => status.value.runs || []);
const activeJob = computed(() => status.value.active_job || null);
const activeJobSummaryText = computed(() => {
if (!activeJob.value) {
return "当前暂无活跃检测任务";
}
const job = activeJob.value;
return `状态:${jobStatusText(job.status)} / 总数 ${job.items_total || 0} / 待领 ${job.items_pending || 0} / 执行中 ${job.items_running || 0}`;
});
const selectedRun = computed(() => runs.value.find((item) => item.run_id === selectedRunId.value) || runs.value[0] || null);
const currentPhaseLabel = computed(() => selectedRun.value?.phase_label || (status.value.worker_online ? "运行中" : "未启动"));
const currentPhaseDetail = computed(() => {
if (selectedRun.value?.phase_detail) {
return selectedRun.value.phase_detail;
}
if (status.value.recent_event) {
return status.value.recent_event;
}
return status.value.worker_runtime_message || "当前暂无阶段明细";
});
const phaseHistory = computed(() => {
const items = selectedRun.value?.phase_history;
if (!Array.isArray(items)) return [];
return [...items].reverse();
});
const selectedLogText = computed(() => {
const runLogs = selectedRun.value?.logs;
if (Array.isArray(runLogs) && runLogs.length) {
return runLogs.join("\n");
}
const liveLogs = status.value.log_lines || [];
return liveLogs.length ? liveLogs.join("\n") : "暂无检测日志";
});
const proxyAlertType = computed<"success" | "warning" | "info" | "error">(() => {
if (status.value.proxy_runtime_state === "healthy") return "success";
if (status.value.proxy_runtime_state === "degraded_direct") return "warning";
if (status.value.proxy_runtime_state === "blocked_no_proxy") return "error";
return "info";
});
const proxyDiagnosisText = computed(() => {
if (status.value.proxy_supplier_empty) {
return "供应池为空,系统已按配置继续运行";
}
return status.value.proxy_runtime_label || "当前可用代理 / 配置代理池链接数";
});
const queueAlertType = computed<"success" | "warning" | "info" | "error">(() => {
if (!queueSummary.value.has_active_job) return "info";
if ((queueSummary.value.queue?.overdue_leases || 0) > 0) return "error";
if ((queueSummary.value.queue?.expiring_soon_leases || 0) > 0 || (queueSummary.value.queue?.oldest_pending_age_minutes || 0) >= 30) return "warning";
return "success";
});
const queueAlertText = computed(() => {
if (!queueSummary.value.has_active_job) {
return "当前没有活跃检测任务,队列健康度会在下一轮任务创建后开始统计。";
}
const queue = queueSummary.value.queue || {};
const throughput = queueSummary.value.throughput || {};
const parts = [
`当前队列待领 ${queue.pending || 0} / 已领 ${queue.claimed || 0} / 执行中 ${queue.running || 0}`,
`${queueSummary.value.window_minutes || 15} 分钟处理 ${throughput.processed_recent || 0} 项,约 ${throughput.processed_per_minute || 0} 项/分钟`
];
if ((queue.overdue_leases || 0) > 0) {
parts.push(`存在 ${queue.overdue_leases} 个过期租约,建议立即检查 Worker 心跳和数据库续租`);
} else if ((queue.expiring_soon_leases || 0) > 0) {
parts.push(`${queue.expiring_soon_leases} 个任务租约将在 5 分钟内到期`);
}
if ((queue.oldest_pending_age_minutes || 0) > 0) {
parts.push(`最老待领任务已等待 ${queue.oldest_pending_age_minutes} 分钟`);
}
return parts.join("");
});
const scrollLogToBottom = async (force = false) => {
if (!force && !logAutoFollow.value) {
return;
}
await nextTick();
const element = logConsoleRef.value;
if (!element) {
return;
}
element.scrollTop = element.scrollHeight;
if (force) {
logAutoFollow.value = true;
}
};
const handleLogScroll = () => {
const element = logConsoleRef.value;
if (!element) {
return;
}
const distanceToBottom = element.scrollHeight - element.scrollTop - element.clientHeight;
logAutoFollow.value = distanceToBottom < 24;
};
const statusText = (value: string) => {
if (value === "starting") return "启动中";
if (value === "running") return "运行中";
if (value === "stopping") return "停止中";
if (value === "completed") return "已完成";
if (value === "stopped") return "已停止";
if (value === "failed") return "失败";
if (value === "partial_failed") return "部分失败";
return value || "-";
};
const jobStatusText = (value: string) => {
if (value === "pending") return "待执行";
if (value === "running") return "执行中";
if (value === "completed") return "已完成";
if (value === "partial_failed") return "部分失败";
if (value === "failed") return "失败";
return value || "-";
};
const statusTagType = (value: string) => {
if (value === "running") return "success";
if (value === "starting" || value === "stopping") return "warning";
if (value === "completed") return "success";
if (value === "partial_failed") return "warning";
if (value === "failed") return "danger";
return "info";
};
const phaseTagType = (phaseLabel: string, statusValue: string) => {
if (statusValue === "failed") return "danger";
if (statusValue === "partial_failed") return "warning";
if (statusValue === "starting" || statusValue === "stopping") return "warning";
if (["检测中", "完成归档", "批次完成", "已完成"].includes(phaseLabel)) return "success";
if (["刷新代理池", "取任务中", "建线程中", "准备检测", "外部站点异常", "网络波动", "部分失败"].includes(phaseLabel)) return "warning";
return "info";
};
const clearRefreshTimer = () => {
if (timer) {
@@ -110,15 +548,25 @@ const ensureRefreshTimer = () => {
if (autoRefresh.value) {
timer = window.setInterval(() => {
loadStatus(false);
}, 10000);
}, 5000);
}
};
const loadStatus = async (showError = true) => {
loading.value = true;
try {
const response = await detectApi.status();
status.value = response.data;
const [statusResponse, queueResponse] = await Promise.all([
detectApi.status(),
detectApi.queueSummary({ window_minutes: 15 })
]);
status.value = statusResponse.data;
queueSummary.value = queueResponse.data;
if (!selectedRunId.value && runs.value.length) {
selectedRunId.value = runs.value[0].run_id;
} else if (selectedRunId.value && !runs.value.some((item) => item.run_id === selectedRunId.value) && runs.value.length) {
selectedRunId.value = runs.value[0].run_id;
}
lastUpdatedAt.value = new Date().toLocaleString("zh-CN", { hour12: false });
} catch {
if (showError) {
ElMessage.error("读取检测状态失败");
@@ -136,34 +584,58 @@ const invoke = async (action: "start" | "stop") => {
lastAction.value = {
label: action === "start" ? "检测启动" : "检测停止",
message: response.message,
at: new Date().toLocaleString("zh-CN", { hour12: false }),
type: "success"
};
persistLastAction();
const pollDelaySeconds = Number(response.data?.poll_after_seconds || 2);
if (response.data?.refresh_status) {
window.setTimeout(() => {
loadStatus(false);
}, pollDelaySeconds * 1000);
return;
}
await loadStatus(false);
window.setTimeout(() => {
loadStatus(false);
}, pollDelaySeconds * 1000);
} catch (error: any) {
const message = error?.message || `${action === "start" ? "启动" : "停止"}操作失败`;
lastAction.value = {
label: action === "start" ? "检测启动" : "检测停止",
message,
at: new Date().toLocaleString("zh-CN", { hour12: false }),
type: "error"
};
persistLastAction();
ElMessage.error(message);
} finally {
actionLoading.value = "";
}
};
const handleCurrentRunChange = (row: any | null) => {
selectedRunId.value = row?.run_id || "";
};
const goRuntime = () => {
router.push("/runtime");
};
watch(autoRefresh, ensureRefreshTimer);
watch(selectedLogText, async (current, previous) => {
if (current === previous) {
return;
}
await scrollLogToBottom();
});
watch(selectedRunId, async () => {
await scrollLogToBottom(true);
});
watch(logAutoFollow, async (enabled) => {
if (enabled) {
await scrollLogToBottom(true);
}
});
onMounted(async () => {
restoreLastAction();
await loadStatus();
ensureRefreshTimer();
await scrollLogToBottom(true);
});
onBeforeUnmount(clearRefreshTimer);
@@ -175,9 +647,140 @@ onBeforeUnmount(clearRefreshTimer);
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
align-items: center;
}
.action-alert {
margin-bottom: 16px;
}
.updated-at {
color: #64748b;
font-size: 13px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.summary-card {
border: 1px solid #dbe4ee;
border-radius: 14px;
padding: 14px 16px;
background: #f8fbff;
}
.summary-title {
color: #64748b;
font-size: 13px;
}
.summary-value {
margin: 8px 0 10px;
color: #0f172a;
font-size: 24px;
font-weight: 700;
}
.phase-value {
font-size: 20px;
}
.summary-note {
margin-top: 8px;
color: #64748b;
font-size: 12px;
}
.summary-grid-tight {
margin-top: -4px;
}
.history-panel {
margin: 16px 0;
}
.history-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.history-header h3 {
margin: 0;
}
.history-note {
color: #64748b;
font-size: 12px;
}
.task-code,
.event-value {
font-size: 18px;
word-break: break-all;
}
.task-detail {
margin: 16px 0;
border-radius: 10px;
overflow: hidden;
border: 1px solid #1f2937;
background: #020617;
}
.task-detail-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
padding: 12px 16px;
background: #111827;
color: #e5e7eb;
}
.task-detail-title {
font-size: 15px;
font-weight: 600;
}
.task-detail-subtitle,
.task-detail-meta {
color: #94a3b8;
font-size: 12px;
}
.task-detail-meta {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.log-console {
margin: 0;
min-height: 320px;
max-height: 520px;
overflow: auto;
padding: 16px;
color: #e0e7ff;
background: #020617;
font-size: 12px;
line-height: 1.7;
font-family: Consolas, Monaco, monospace;
white-space: pre-wrap;
word-break: break-word;
}
.phase-detail {
padding: 10px 16px 0;
color: #93c5fd;
font-size: 12px;
background: #020617;
}
</style>