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

@@ -24,18 +24,107 @@
</PageCard>
</div>
<PageCard title="第一期目标" description="先把运营最需要的控制面搬到 Web再逐步把检测执行和运维能力完全迁到 Linux。">
<el-timeline>
<el-timeline-item timestamp="当前阶段" placement="top">已创建 Web 后台API运行中心和导入/导出/筛选主链路</el-timeline-item>
<el-timeline-item timestamp="下一步" placement="top">继续补 Linux Worker 控制闭环和更完整的运行诊断</el-timeline-item>
<el-timeline-item timestamp="后续阶段" placement="top">把检测端逐步迁为 Linux Worker前后端彻底分离</el-timeline-item>
</el-timeline>
<PageCard
title="运营总览"
description="先给运营一个能判断有没有跑起来、卡在哪一步、还要跑多久的最小面板。"
>
<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">
{{
activeJob.job_code
? `状态 ${activeJob.status || "-"} / 当前任务总数 ${activeJob.items_total || 0} / 当前累计活跃任务 ${opsSummary.active_jobs_total || 0}`
: "当前没有活跃检测任务"
}}
</div>
</div>
<div class="summary-card">
<div class="summary-title">当前速度</div>
<div class="summary-value">{{ processedPerMinuteText }}</div>
<div class="summary-note">近窗完成 {{ opsSummary.completed_recent || 0 }} / 失败 {{ opsSummary.failed_recent || 0 }} / 黑名单 {{ opsSummary.blacklisted_recent || 0 }}</div>
</div>
<div class="summary-card">
<div class="summary-title">预计剩余</div>
<div class="summary-value">{{ etaText }}</div>
<div class="summary-note">按当前吞吐推算剩余 {{ opsSummary.remaining_items || 0 }} </div>
</div>
<div class="summary-card">
<div class="summary-title">瓶颈步骤</div>
<div class="summary-value">{{ bottleneckStep.step_name || "-" }}</div>
<div class="summary-note">待处理 {{ bottleneckStep.items_pending || 0 }} / 执行中 {{ bottleneckStep.items_running || 0 }}</div>
</div>
<div class="summary-card">
<div class="summary-title">真实积压</div>
<div class="summary-value">{{ backlogPendingText }}</div>
<div class="summary-note">注册待处理 {{ backlogRegisterPendingText }} / 后续步骤 {{ backlogDownstreamPendingText }}</div>
</div>
<div class="summary-card">
<div class="summary-title">有效执行节点</div>
<div class="summary-value">{{ opsSummary.active_execution_nodes || 0 }}</div>
<div class="summary-note">在线 Worker {{ opsSummary.online_worker_nodes || 0 }} / 独立 Worker {{ opsSummary.dedicated_online_worker_nodes || 0 }} / 当前线程负载 {{ queueDisplayRunningText }}</div>
</div>
</div>
<el-row v-if="activeJob.job_code" :gutter="12" class="stat-row">
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前任务待处理" :value="activeJob.items_pending || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前线程负载" :value="activeJobDisplayRunning" /></el-col>
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前任务执行中" :value="activeJob.items_running || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前任务通过" :value="activeJob.items_completed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前任务黑名单" :value="activeJob.items_blacklisted || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前任务失败" :value="activeJob.items_failed || 0" /></el-col>
<el-col :xs="12" :sm="8" :md="6"><el-statistic title="当前进度" :value="activeJob.progress_percent || 0" suffix="%" /></el-col>
</el-row>
<el-alert
:closable="false"
show-icon
:type="activeJob.job_code ? 'info' : 'warning'"
:title="activeJob.job_code ? opsHeadline : '当前没有活跃检测任务,运营面板暂无可分析数据。'"
/>
</PageCard>
<PageCard title="步骤队列" description="看每一步堆积、吞吐和失败快速判断到底卡在注册、百度、360、爱站还是站长之家。">
<el-table :data="stepQueue" border empty-text="当前没有活跃步骤队列">
<el-table-column prop="step_name" label="步骤" min-width="150" />
<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_blacklisted" label="黑名单" min-width="90" />
<el-table-column prop="items_failed" label="失败" min-width="90" />
<el-table-column prop="started_recent" label="近窗启动" min-width="100" />
<el-table-column prop="processed_recent" label="近窗处理" min-width="100" />
<el-table-column prop="completed_recent" label="近窗完成" min-width="100" />
<el-table-column prop="failed_recent" label="近窗失败" min-width="100" />
<el-table-column prop="processed_per_minute" label="项/分钟" min-width="100" />
</el-table>
</PageCard>
<PageCard title="节点吞吐" description="看 controller 和 worker 是不是都真在跑,谁在空转,谁在出结果。">
<el-table :data="nodeThroughput" border empty-text="当前没有节点吞吐数据">
<el-table-column prop="node_code" label="节点" min-width="180" />
<el-table-column prop="items_running" label="任务执行中" min-width="110" />
<el-table-column prop="display_running" label="线程负载" min-width="100" />
<el-table-column label="活跃线程" min-width="120">
<template #default="{ row }">
{{ formatThreadState(row) }}
</template>
</el-table-column>
<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="failed_recent" label="失败" min-width="90" />
<el-table-column prop="blacklisted_recent" label="黑名单" min-width="90" />
</el-table>
</PageCard>
</div>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import PageCard from "@/components/PageCard.vue";
import { dashboardApi } from "@/api/modules";
@@ -47,14 +136,31 @@ let timer: number | null = null;
const stats = ref([
{ label: "总域名", value: "--", note: "数据库域名总量" },
{ label: "待检测", value: "--", note: "当前待处理域名" },
{ label: "黑名单", value: "--", note: "命中风险域名" },
{ label: "库存待检测", value: "--", note: "全库尚未跑完的域名" },
{ label: "库存检测中", value: "--", note: "全库当前处理中域名" },
{ label: "库存检测通过", value: "--", note: "全库已检测通过域名" },
{ label: "库存黑名单", value: "--", note: "全库已命中黑名单域名" },
{ label: "库存失败", value: "--", note: "全库检测失败域名" },
{ label: "可注册", value: "--", note: "检测通过且注册状态为可注册" },
{ label: "可购买", value: "--", note: "检测通过 + 非黑名单 + 可注册 + 未使用" },
{ label: "当前队列待处理", value: "--", note: "当前活跃任务队列 pending" },
{ label: "当前队列已领", value: "--", note: "当前活跃任务已领未跑" },
{ label: "当前队列执行中", value: "--", note: "当前活跃任务 running" },
{ label: "当前队列已完成", value: "--", note: "当前活跃任务 completed" },
{ label: "当前速度", value: "--", note: "近窗处理项/分钟" },
{ label: "预计剩余", value: "--", note: "按当前吞吐估算" },
{ label: "API", value: "离线", note: "当前 API 运行状态" },
{ label: "本机 Worker", value: "离线", note: "当前节点本机 Worker 运行状态" },
{ label: "有效执行节点", value: "离线", note: "当前集群可承担检测任务的在线节点" },
{ label: "模式", value: "--", note: "当前 Worker 运行模式" }
]);
const activeJob = ref<Record<string, any>>({});
const opsSummary = ref<Record<string, any>>({});
const bottleneckStep = ref<Record<string, any>>({});
const stepQueue = ref<Record<string, any>[]>([]);
const nodeThroughput = ref<Record<string, any>[]>([]);
const clearRefreshTimer = () => {
if (timer) {
window.clearInterval(timer);
@@ -76,6 +182,43 @@ const formatNow = () =>
hour12: false
});
const processedPerMinuteText = computed(() => {
const value = Number(opsSummary.value.processed_per_minute || 0);
return value > 0 ? value.toFixed(value >= 10 ? 0 : 2) : "0";
});
const activeJobDisplayRunning = computed(() => Number(activeJob.value.items_display_running || activeJob.value.items_running || 0));
const queueDisplayRunningText = computed(() => String(Number(opsSummary.value.queue_display_running_total || 0)));
const backlogPendingText = computed(() => String(Number(opsSummary.value.backlog_pending_total || 0)));
const backlogRegisterPendingText = computed(() => String(Number(opsSummary.value.backlog_register_pending_total || 0)));
const backlogDownstreamPendingText = computed(() => String(Number(opsSummary.value.backlog_downstream_pending_total || 0)));
const etaText = computed(() => {
const hours = Number(opsSummary.value.estimated_hours_remaining || 0);
if (hours <= 0) return "待计算";
if (hours < 1) return `${Math.max(1, Math.round(hours * 60))} 分钟`;
return `${hours.toFixed(hours >= 10 ? 0 : 1)} 小时`;
});
const opsHeadline = computed(() => {
const jobCode = String(activeJob.value.job_code || "-");
const ppm = processedPerMinuteText.value;
const eta = etaText.value;
const stepName = String(bottleneckStep.value.step_name || "-");
return `当前任务 ${jobCode},真实积压 ${backlogPendingText.value},近窗速度 ${ppm} 项/分钟,预计剩余 ${eta},当前最重步骤 ${stepName}`;
});
const formatThreadState = (row: Record<string, any>) => {
const active = Number(row?.active_threads || 0);
const max = Number(row?.max_threads || 0);
if (max > 0) return `${active}/${max}`;
return String(active);
};
const loadOverview = async (showError = true) => {
loading.value = true;
try {
@@ -88,10 +231,38 @@ const loadOverview = async (showError = true) => {
data.local_worker_status === "not-applicable"
? `当前节点 ${data.node_region || "-"} / ${data.node_role || "-"},默认不承载本机 Worker`
: "当前节点本机 Worker 运行状态";
activeJob.value = data.active_job || {};
opsSummary.value = {
...(data.ops_summary || {}),
retry_total: data.retry_total || 0,
backlog_pending_total: data.backlog_pending_total || 0,
backlog_claimed_total: data.backlog_claimed_total || 0,
backlog_running_total: data.backlog_running_total || 0,
backlog_register_pending_total: data.backlog_register_pending_total || 0,
backlog_downstream_pending_total: data.backlog_downstream_pending_total || 0,
queue_display_running_total: data.queue_display_running_total || 0
};
bottleneckStep.value = data.bottleneck_step || {};
stepQueue.value = Array.isArray(data.step_queue) ? data.step_queue : [];
nodeThroughput.value = Array.isArray(data.node_throughput) ? data.node_throughput : [];
stats.value = [
{ label: "总域名", value: String(data.domains_total), note: "数据库域名总量" },
{ label: "待检测", value: String(data.pending_total), note: "当前待处理域名" },
{ label: "黑名单", value: String(data.blacklist_total), note: "命中风险域名" },
{ label: "库存待检测", value: String(data.pending_total), note: "全库尚未跑完的域名" },
{ label: "库存检测通过", value: String(data.completed_total), note: "全库已检测通过域名" },
{ label: "库存黑名单", value: String(data.blacklist_total), note: "海外主库最终账本中的黑名单域名" },
{ label: "库存失败", value: String(data.failed_total || 0), note: "全库检测失败域名" },
{ label: "可注册", value: String(data.registerable_total || 0), note: "以海外主库最终账本为准,检测通过且注册状态为可注册" },
{ label: "可购买", value: String(data.purchasable_total || 0), note: "可注册 + 非黑名单 + 未使用,以海外主库最终账本为准" },
{ label: "真实积压待处理", value: String(data.backlog_pending_total || 0), note: "大陆执行面真实 backlog pending" },
{ label: "注册待处理", value: String(data.backlog_register_pending_total || 0), note: "第一步 register 积压" },
{ label: "后续步骤待处理", value: String(data.backlog_downstream_pending_total || 0), note: "百度/360/站长/爱站等后续步骤积压" },
{ label: "当前任务待处理", value: String(data.queue_pending_total || 0), note: "当前展示任务的 pending" },
{ label: "当前队列已领", value: String(data.queue_claimed_total || 0), note: "当前活跃任务已领未跑" },
{ label: "当前任务执行中", value: String(data.queue_running_total || 0), note: "当前展示任务真实 running 项" },
{ label: "当前线程负载", value: String(data.queue_display_running_total || 0), note: "当前活跃线程/负载,更接近机器真实忙碌度" },
{ label: "当前队列已完成", value: String(data.queue_completed_total || 0), note: "当前活跃任务 completed" },
{ label: "当前速度", value: processedPerMinuteText.value, note: `近窗处理 ${opsSummary.value.processed_recent || 0}` },
{ label: "预计剩余", value: etaText.value, note: `剩余 ${opsSummary.value.remaining_items || 0}` },
{ label: "API", value: data.api_status, note: "当前 API 运行状态" },
{ label: "本机 Worker", value: localWorkerValue, note: localWorkerNote },
{
@@ -143,7 +314,7 @@ onBeforeUnmount(clearRefreshTimer);
.stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
@@ -158,7 +329,7 @@ onBeforeUnmount(clearRefreshTimer);
}
.stat-item strong {
font-size: 32px;
font-size: 28px;
color: #0f172a;
}
@@ -166,9 +337,65 @@ onBeforeUnmount(clearRefreshTimer);
color: #94a3b8;
}
@media (max-width: 960px) {
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 16px;
}
.summary-card {
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 16px;
background: linear-gradient(180deg, #fff 0%, #f8fafc 100%);
}
.summary-title {
color: #64748b;
font-size: 13px;
margin-bottom: 8px;
}
.summary-value {
font-size: 28px;
font-weight: 700;
color: #0f172a;
}
.summary-note {
margin-top: 8px;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
.task-code {
font-size: 22px;
word-break: break-all;
}
.stat-row {
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 960px) {
.stats-grid,
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.stats-grid,
.summary-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
</style>