d
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user