This commit is contained in:
Your Name
2026-04-22 14:13:21 +08:00
parent e0406b5d0e
commit 7cbde2aa78
145 changed files with 23086 additions and 2243 deletions

View File

@@ -29,8 +29,10 @@
<el-descriptions-item label="Worker 服务">{{ status.worker_service_name || "-" }}</el-descriptions-item>
<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="本机线程配置">{{ status.thread_count }}</el-descriptions-item>
<el-descriptions-item label="线程来源">{{ threadCountSummary }}</el-descriptions-item>
<el-descriptions-item v-if="isAggregateDetectView" label="当前汇总线程">{{ threadSnapshotActive }} / {{ threadSnapshotMax }}</el-descriptions-item>
<el-descriptions-item v-if="isAggregateDetectView" label="当前参与节点">{{ participatingNodeCount }}</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>
@@ -97,7 +99,7 @@
:closable="false"
show-icon
type="info"
:title="`当前检测状态:${currentPhaseLabel};本机 Worker ${status.worker_online ? '在线' : '离线'}。`"
:title="detectScopeSummaryText"
style="margin-top: 12px"
/>
<el-alert
@@ -110,11 +112,11 @@
<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="displayRunningStat" /></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-col :xs="12" :sm="8" :md="4"><el-statistic title="可注册" :value="registerableStat" /></el-col>
</el-row>
<div class="summary-grid">
@@ -134,9 +136,19 @@
<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 class="summary-title">活跃线程</div>
<div class="summary-value">{{ threadSnapshotActive }} / {{ threadSnapshotMax }}</div>
<div class="summary-note">当前参与节点实际活跃线程 / 配置线程上限</div>
</div>
<div class="summary-card">
<div class="summary-title">当前速度</div>
<div class="summary-value">{{ queueProcessedPerMinute }}</div>
<div class="summary-note"> {{ queueProcessedPerHour }} /小时</div>
</div>
<div class="summary-card">
<div class="summary-title">预计剩余</div>
<div class="summary-value">{{ estimatedRemainingText }}</div>
<div class="summary-note">按当前吞吐推算剩余 {{ remainingItemsEstimate }} </div>
</div>
<div class="summary-card">
<div class="summary-title">代理快照</div>
@@ -149,6 +161,7 @@
<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="activeJobDisplayClaimed" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="任务执行中" :value="activeJobDisplayRunning" /></el-col>
<el-col :xs="12" :sm="8" :md="4"><el-statistic title="活跃线程" :value="threadSnapshotActive" /></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>
@@ -205,7 +218,7 @@
<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_running" label="任务执行中" min-width="100" />
<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" />
@@ -220,18 +233,17 @@
<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_running" label="任务执行中" min-width="100" />
<el-table-column label="活跃线程" min-width="130">
<template #default="{ row }">
<span>{{ row.active_threads || 0 }} / {{ row.max_threads || 0 }}</span>
</template>
</el-table-column>
<el-table-column prop="current_load" label="在途负载" min-width="100" />
<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 }">
@@ -275,16 +287,41 @@
<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>{{ isAggregateDetectView ? "参与节点" : "本机 Worker" }}{{ isAggregateDetectView ? participatingNodeCount : (status.worker_online ? "在线" : "离线") }}</span>
<span v-if="selectedRun?.phase_label">阶段{{ selectedRun.phase_label }}</span>
<span>运行中{{ status.progress.running || 0 }}</span>
<span>线程{{ threadCountSummary }}</span>
<span>运行中{{ displayRunningStat }}</span>
<span>线程{{ runtimeThreadSummary }}</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>
<div
v-if="(activeJob?.current_cycle_events || activeJob?.recent_events)?.length"
class="event-stream-panel"
>
<div class="event-stream-header">
<div>
<div class="event-stream-title">检测事件流</div>
<div class="event-stream-subtitle">事件列表单独滚动避免检测进行越久页面越被撑长</div>
</div>
<span class="event-stream-count">
{{ (activeJob.current_cycle_events || activeJob.recent_events || []).length }}
</span>
</div>
<el-table
:data="activeJob.current_cycle_events || activeJob.recent_events"
border
max-height="320"
>
<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>
</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" />
@@ -333,6 +370,7 @@ const status = ref({
proxy_last_refresh_status: "",
proxy_last_refresh_time: "",
dependency_alerts: [] as any[],
aggregate_detect_view: false,
log_lines: [] as string[],
remote_log_lines: [] as string[],
remote_log_line_count: 0,
@@ -423,8 +461,28 @@ const restoreLastAction = () => {
const runs = computed(() => status.value.runs || []);
const activeJob = computed(() => status.value.active_job || null);
const isAggregateDetectView = computed(() => Boolean(status.value.aggregate_detect_view));
const activeJobDisplayClaimed = computed(() => Number(activeJob.value?.display_items_claimed ?? activeJob.value?.items_claimed ?? 0));
const activeJobDisplayRunning = computed(() => Number(activeJob.value?.display_items_running ?? activeJob.value?.items_running ?? 0));
const activeJobDisplayRunning = computed(() => Math.max(
Number(activeJob.value?.items_running ?? 0),
Number(activeJob.value?.display_active_threads ?? 0),
));
const threadSnapshotActive = computed(() => {
const jobValue = Number(activeJob.value?.display_active_threads ?? 0);
if (jobValue > 0) {
return jobValue;
}
return Number(status.value.active_thread_count || 0);
});
const threadSnapshotMax = computed(() => {
const jobValue = Number(activeJob.value?.display_max_threads ?? 0);
if (jobValue > 0) {
return jobValue;
}
return Number(status.value.max_thread_count || status.value.thread_count || 0);
});
const displayRunningStat = computed(() => Math.max(Number(status.value.progress.running || 0), activeJobDisplayRunning.value));
const registerableStat = computed(() => Number(status.value.progress?.registerable || 0));
const activeJobDisplayNodeStats = computed(() => {
const distributed = activeJob.value?.distributed_node_stats;
if (Array.isArray(distributed) && distributed.length) {
@@ -432,14 +490,51 @@ const activeJobDisplayNodeStats = computed(() => {
}
return Array.isArray(activeJob.value?.node_stats) ? activeJob.value.node_stats : [];
});
const participatingNodeCount = computed(() => {
const rows = activeJobDisplayNodeStats.value.filter((item) => String(item?.node_code || "").trim() && String(item?.node_code || "").trim() !== "unassigned");
return rows.length || 0;
});
const queueProcessedPerMinute = computed(() => Number(queueSummary.value.throughput?.processed_per_minute || 0));
const queueProcessedPerHour = computed(() => Number((queueProcessedPerMinute.value * 60).toFixed(2)));
const remainingItemsEstimate = computed(() => {
const queue = queueSummary.value.queue || {};
return Number(queue.pending || 0) + Number(queue.claimed || 0) + Number(queue.running || 0);
});
const estimatedRemainingMinutes = computed(() => {
const perMinute = queueProcessedPerMinute.value;
if (perMinute <= 0) {
return 0;
}
return Number((remainingItemsEstimate.value / perMinute).toFixed(1));
});
const estimatedRemainingText = computed(() => {
if (!queueSummary.value.has_active_job) {
return "-";
}
if (estimatedRemainingMinutes.value <= 0) {
return "即将完成";
}
if (estimatedRemainingMinutes.value >= 60) {
return `${(estimatedRemainingMinutes.value / 60).toFixed(2)}h`;
}
return `${estimatedRemainingMinutes.value}m`;
});
const runtimeThreadSummary = computed(() => {
if (isAggregateDetectView.value) {
return `${threadSnapshotActive.value} / ${threadSnapshotMax.value}(参与节点汇总)`;
}
return threadCountSummary.value;
});
const activeJobSummaryText = computed(() => {
if (!activeJob.value) {
return "当前暂无活跃检测任务";
}
const job = activeJob.value;
const displayClaimed = Number(job.display_items_claimed ?? job.items_claimed ?? 0);
const displayRunning = Number(job.display_items_running ?? job.items_running ?? 0);
return `状态:${jobStatusText(job.status)} / 总数 ${job.items_total || 0} / 待领 ${job.items_pending || 0} / 已领 ${displayClaimed} / 执行中 ${displayRunning}`;
const displayRunning = activeJobDisplayRunning.value;
const displayActiveThreads = Number(job.display_active_threads ?? 0);
const displayMaxThreads = Number(job.display_max_threads ?? 0);
return `状态:${jobStatusText(job.status)} / 总数 ${job.items_total || 0} / 待领 ${job.items_pending || 0} / 已领 ${displayClaimed} / 实时执行 ${displayRunning} / 活跃线程 ${displayActiveThreads}/${displayMaxThreads} / 速度 ${queueProcessedPerMinute.value} 项/分钟 / 预计剩余 ${estimatedRemainingText.value}`;
});
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 ? "运行中" : "未启动"));
@@ -532,6 +627,12 @@ const remoteLogMirrorText = computed(() => {
}
return parts.join("");
});
const detectScopeSummaryText = computed(() => {
if (isAggregateDetectView.value) {
return `当前检测状态:${currentPhaseLabel};当前页展示的是多节点汇总运行态,参与节点 ${participatingNodeCount.value} 台,实时活跃线程 ${threadSnapshotActive.value}/${threadSnapshotMax.value}`;
}
return `当前检测状态:${currentPhaseLabel};本机 Worker ${status.value.worker_online ? "在线" : "离线"}`;
});
const queueAlertType = computed<"success" | "warning" | "info" | "error">(() => {
if (!queueSummary.value.has_active_job) return "info";
@@ -546,10 +647,10 @@ const queueAlertText = computed(() => {
}
const queue = queueSummary.value.queue || {};
const throughput = queueSummary.value.throughput || {};
const displayClaimed = Number(queue.display_claimed ?? queue.claimed ?? 0);
const displayRunning = Number(queue.display_running ?? queue.running ?? 0);
const displayClaimed = activeJobDisplayClaimed.value || Number(queue.display_claimed ?? queue.claimed ?? 0);
const displayRunning = displayRunningStat.value || Number(queue.display_running ?? queue.running ?? 0);
const parts = [
`当前队列待领 ${queue.pending || 0} / 已领 ${displayClaimed} / 执行中 ${displayRunning}`,
`当前队列待领 ${queue.pending || 0} / 已领 ${displayClaimed} / 任务执行中 ${displayRunning}`,
`${queueSummary.value.window_minutes || 15} 分钟处理 ${throughput.processed_recent || 0} 项,约 ${throughput.processed_per_minute || 0} 项/分钟`
];
if ((queue.overdue_leases || 0) > 0) {
@@ -889,8 +990,8 @@ onBeforeUnmount(clearRefreshTimer);
.log-console {
margin: 0;
min-height: 320px;
max-height: 520px;
min-height: 160px;
max-height: 260px;
overflow: auto;
padding: 16px;
color: #e0e7ff;
@@ -908,4 +1009,34 @@ onBeforeUnmount(clearRefreshTimer);
font-size: 12px;
background: #020617;
}
.event-stream-panel {
margin: 16px 0;
}
.event-stream-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.event-stream-title {
color: #0f172a;
font-size: 15px;
font-weight: 600;
}
.event-stream-subtitle {
margin-top: 4px;
color: #64748b;
font-size: 12px;
}
.event-stream-count {
color: #64748b;
font-size: 12px;
white-space: nowrap;
}
</style>