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