d
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>当前保持桌面版不变,Web 管理后台进入可持续运维阶段。</p>
|
||||
<p>{{ pageDescription }}</p>
|
||||
</div>
|
||||
|
||||
<div class="topbar-actions">
|
||||
@@ -68,6 +68,7 @@ const menuItems = [
|
||||
{ path: "/runtime", label: "运行中心" },
|
||||
{ path: "/runtime-debug", label: "运行调试" },
|
||||
{ path: "/ops-center", label: "运维中枢" },
|
||||
{ path: "/managed-nodes", label: "托管节点" },
|
||||
{ path: "/settings", label: "系统设置" },
|
||||
{ path: "/imports", label: "域名导入" },
|
||||
{ path: "/sensitive-words", label: "敏感词配置" },
|
||||
@@ -79,6 +80,13 @@ const menuItems = [
|
||||
];
|
||||
|
||||
const title = computed(() => String(route.meta?.title || "概览"));
|
||||
const pageDescription = computed(
|
||||
() =>
|
||||
String(
|
||||
route.meta?.description ||
|
||||
"当前保持桌面版不变,Web 管理后台进入可持续运维阶段。"
|
||||
)
|
||||
);
|
||||
|
||||
const loadRuntimeSummary = async () => {
|
||||
try {
|
||||
@@ -258,7 +266,9 @@ onBeforeUnmount(() => {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 28px 28px;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
|
||||
@@ -13,18 +13,19 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("@/layouts/MainLayout.vue"),
|
||||
children: [
|
||||
{ path: "", redirect: "/dashboard" },
|
||||
{ path: "dashboard", name: "dashboard", component: () => import("@/views/dashboard/DashboardView.vue"), meta: { title: "概览" } },
|
||||
{ path: "runtime", name: "runtime", component: () => import("@/views/runtime/RuntimeView.vue"), meta: { title: "运行中心" } },
|
||||
{ path: "runtime-debug", name: "runtime-debug", component: () => import("@/views/runtime/RuntimeDebugView.vue"), meta: { title: "运行调试" } },
|
||||
{ path: "ops-center", name: "ops-center", component: () => import("@/views/ops/OpsCenterView.vue"), meta: { title: "运维中枢" } },
|
||||
{ path: "settings", name: "settings", component: () => import("@/views/settings/SettingsView.vue"), meta: { title: "系统设置" } },
|
||||
{ path: "imports", name: "imports", component: () => import("@/views/imports/ImportsView.vue"), meta: { title: "域名导入" } },
|
||||
{ path: "sensitive-words", name: "sensitive-words", component: () => import("@/views/sensitive-words/SensitiveWordsView.vue"), meta: { title: "敏感词配置" } },
|
||||
{ path: "juming", name: "juming", component: () => import("@/views/juming/JumingView.vue"), meta: { title: "聚名采集" } },
|
||||
{ path: "detect", name: "detect", component: () => import("@/views/detect/DetectView.vue"), meta: { title: "检测控制" } },
|
||||
{ path: "domains", name: "domains", component: () => import("@/views/domains/DomainsView.vue"), meta: { title: "域名筛选" } },
|
||||
{ path: "exports", name: "exports", component: () => import("@/views/exports/ExportsView.vue"), meta: { title: "导出中心" } },
|
||||
{ path: "logs", name: "logs", component: () => import("@/views/logs/LogsView.vue"), meta: { title: "日志诊断" } }
|
||||
{ path: "dashboard", name: "dashboard", component: () => import("@/views/dashboard/DashboardView.vue"), meta: { title: "概览", description: "统一查看当前任务、吞吐、瓶颈步骤和整体运行态。" } },
|
||||
{ path: "runtime", name: "runtime", component: () => import("@/views/runtime/RuntimeView.vue"), meta: { title: "运行中心", description: "看检测任务有没有真正跑起来,重点盯 running、ppm、步骤队列和节点负载。" } },
|
||||
{ path: "runtime-debug", name: "runtime-debug", component: () => import("@/views/runtime/RuntimeDebugView.vue"), meta: { title: "运行调试", description: "排查步骤卡点、事件流、调试日志和运行时异常。" } },
|
||||
{ path: "ops-center", name: "ops-center", component: () => import("@/views/ops/OpsCenterView.vue"), meta: { title: "运维中枢", description: "这里是总驾驶舱,负责发布、运维任务、巡检、接管进度和整体运维视角。" } },
|
||||
{ path: "managed-nodes", name: "managed-nodes", component: () => import("@/views/ops/ManagedNodesView.vue"), meta: { title: "托管节点", description: "这里专门管理机器本身:新增节点、填 SSH、启用停用、维护接入信息。" } },
|
||||
{ path: "settings", name: "settings", component: () => import("@/views/settings/SettingsView.vue"), meta: { title: "系统设置", description: "维护系统基础配置、运行参数和默认行为。" } },
|
||||
{ path: "imports", name: "imports", component: () => import("@/views/imports/ImportsView.vue"), meta: { title: "域名导入", description: "导入待检测域名数据,建立任务源。" } },
|
||||
{ path: "sensitive-words", name: "sensitive-words", component: () => import("@/views/sensitive-words/SensitiveWordsView.vue"), meta: { title: "敏感词配置", description: "维护过滤词、黑名单词和筛选规则。" } },
|
||||
{ path: "juming", name: "juming", component: () => import("@/views/juming/JumingView.vue"), meta: { title: "聚名采集", description: "抓取聚名删除列表,跟踪采集、去重和入库进度。" } },
|
||||
{ path: "detect", name: "detect", component: () => import("@/views/detect/DetectView.vue"), meta: { title: "检测控制", description: "启动检测、观察步骤推进,并排查并发和补位问题。" } },
|
||||
{ path: "domains", name: "domains", component: () => import("@/views/domains/DomainsView.vue"), meta: { title: "域名筛选", description: "查看域名状态、筛选结果和流程产出。" } },
|
||||
{ path: "exports", name: "exports", component: () => import("@/views/exports/ExportsView.vue"), meta: { title: "导出中心", description: "管理导出任务和结果文件。" } },
|
||||
{ path: "logs", name: "logs", component: () => import("@/views/logs/LogsView.vue"), meta: { title: "日志诊断", description: "集中查看系统日志、错误信息和诊断输出。" } }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -205,7 +205,11 @@
|
||||
<span v-if="selectedTask?.result">抓取:{{ selectedTask.result.domains_found || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre class="log-console">{{ selectedTaskLogs }}</pre>
|
||||
<div class="task-detail-meta task-detail-meta--console">
|
||||
<el-switch v-model="logAutoFollow" inline-prompt active-text="跟随日志" inactive-text="暂停跟随" size="small" />
|
||||
<el-button text size="small" @click="scrollLogToBottom(true)">回到底部</el-button>
|
||||
</div>
|
||||
<pre ref="logConsoleRef" class="log-console" @scroll="handleLogScroll">{{ selectedTaskLogs }}</pre>
|
||||
</div>
|
||||
|
||||
<el-row v-if="selectedTask?.result" :gutter="16" style="margin-bottom: 16px">
|
||||
@@ -229,7 +233,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { UploadFile } from "element-plus";
|
||||
import PageCard from "@/components/PageCard.vue";
|
||||
@@ -242,11 +246,13 @@ const loggingIn = ref(false);
|
||||
const linkingJucha = ref(false);
|
||||
const cookieFile = ref<File | null>(null);
|
||||
const autoRefresh = ref(true);
|
||||
const logAutoFollow = ref(true);
|
||||
const lastUpdatedAt = ref("");
|
||||
const preferencesSavedAt = ref("");
|
||||
const preferencesSaving = ref(false);
|
||||
const tasks = ref<any[]>([]);
|
||||
const selectedTaskId = ref("");
|
||||
const logConsoleRef = ref<HTMLElement | null>(null);
|
||||
let timer: number | null = null;
|
||||
let savePreferencesTimer: number | null = null;
|
||||
let suppressPreferenceSave = false;
|
||||
@@ -600,12 +606,43 @@ const handleCurrentTaskChange = (row: any | null) => {
|
||||
selectedTaskId.value = row?.task_id || "";
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
watch(autoRefresh, ensureRefreshTimer);
|
||||
watch(
|
||||
() => [form.mode, form.page_start, form.page_size, form.page_count, form.crawl_date, form.auto_date],
|
||||
schedulePersistPreferences,
|
||||
{ deep: false }
|
||||
);
|
||||
watch(selectedTaskLogs, () => {
|
||||
void scrollLogToBottom(false);
|
||||
});
|
||||
watch(selectedTaskId, () => {
|
||||
logAutoFollow.value = true;
|
||||
void scrollLogToBottom(true);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([refreshAll(), loadCredentials()]);
|
||||
@@ -728,6 +765,12 @@ onBeforeUnmount(() => {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-detail-meta--console {
|
||||
padding: 8px 16px 0;
|
||||
background: #020617;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.log-console {
|
||||
margin: 0;
|
||||
min-height: 260px;
|
||||
|
||||
471
domain-web/src/views/ops/ManagedNodesView.vue
Normal file
471
domain-web/src/views/ops/ManagedNodesView.vue
Normal file
@@ -0,0 +1,471 @@
|
||||
<template>
|
||||
<PageCard title="托管节点" description="专门用于纳管机器、维护 SSH 信息、启停节点。运维中枢继续负责总驾驶舱、任务、发布和巡检。">
|
||||
<template #header-extra>
|
||||
<div class="header-actions">
|
||||
<el-button plain :loading="loading.syncing" @click="syncNodesFromCluster">同步节点</el-button>
|
||||
<el-button plain :loading="loading.refreshing" @click="loadNodes">刷新</el-button>
|
||||
<el-button plain @click="goOpsCenter">打开运维中枢</el-button>
|
||||
<el-button type="primary" @click="openManagedNodeDialog()">新增节点</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-alert
|
||||
:closable="false"
|
||||
show-icon
|
||||
type="info"
|
||||
style="margin-bottom: 16px"
|
||||
title="这里是独立的节点管理入口。后面你新增机器、填 SSH、启用/停用节点,都从这里做,不用再去运维中枢长页面里找。"
|
||||
/>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">总节点</span>
|
||||
<strong>{{ summary.total }}</strong>
|
||||
<span class="summary-note">已纳管 {{ summary.managedTotal }} / 已启用 {{ summary.enabled }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">Agent 在线</span>
|
||||
<strong>{{ summary.online }}</strong>
|
||||
<span class="summary-note">stale {{ summary.stale }} / 待接入 {{ summary.pendingBootstrap }}</span>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<span class="summary-label">执行节点</span>
|
||||
<strong>{{ summary.participating }}</strong>
|
||||
<span class="summary-note">在线待命 {{ summary.standby }} / 死信 {{ summary.queueDeadLetterNodes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="nodes" stripe size="small" height="100%">
|
||||
<el-table-column label="节点" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="node-cell">
|
||||
<strong>{{ row.node_code || "-" }}</strong>
|
||||
<span v-if="row.title && row.title !== row.node_code">{{ row.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="region" label="地域" width="90" />
|
||||
<el-table-column prop="role" label="角色" width="90" />
|
||||
<el-table-column label="启用" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_enabled ? 'success' : 'info'" size="small">
|
||||
{{ row.is_enabled ? "已启用" : "已停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="接管态" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="agentStateType(row.agent_state)" size="small">
|
||||
{{ row.agent_state_label || "未接管" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="SSH" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<div class="node-cell">
|
||||
<span>{{ sshEndpoint(row) }}</span>
|
||||
<span class="muted">
|
||||
{{ row.auth_mode || "key" }}
|
||||
<template v-if="row.ssh_password_configured"> / 已存密码</template>
|
||||
<template v-else-if="row.ssh_private_key_configured"> / 已存私钥</template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最近心跳" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.last_seen_at || row.cluster_last_heartbeat_at || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="table-actions">
|
||||
<el-button size="small" text @click="openManagedNodeDialog(row)">编辑</el-button>
|
||||
<el-button size="small" text type="primary" @click="toggleNodeEnabled(row)">
|
||||
{{ row.is_enabled ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="managedNodeVisible" :title="managedNodeEditing ? '编辑托管节点' : '新增托管节点'" width="760px">
|
||||
<el-form label-position="top" :model="managedNodeForm" class="dialog-form">
|
||||
<div class="dialog-grid">
|
||||
<el-form-item label="节点编码">
|
||||
<el-input v-model="managedNodeForm.node_code" :disabled="managedNodeEditing" placeholder="例如 mainland-worker-02" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名称">
|
||||
<el-input v-model="managedNodeForm.title" placeholder="例如 上海 worker 02" />
|
||||
</el-form-item>
|
||||
<el-form-item label="地域">
|
||||
<el-select v-model="managedNodeForm.region">
|
||||
<el-option label="mainland" value="mainland" />
|
||||
<el-option label="overseas" value="overseas" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="managedNodeForm.role">
|
||||
<el-option label="worker" value="worker" />
|
||||
<el-option label="control" value="control" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH 一行录入" class="full-width">
|
||||
<el-input
|
||||
v-model="managedNodeForm.ssh_entry"
|
||||
placeholder="例如 121.204.244.188 root <password> 或 121.204.244.248:22 root <password>"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH Host">
|
||||
<el-input v-model="managedNodeForm.ssh_host" placeholder="例如 121.204.244.248" />
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH Port">
|
||||
<el-input-number v-model="managedNodeForm.ssh_port" :min="1" :max="65535" />
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH User">
|
||||
<el-input v-model="managedNodeForm.ssh_user" placeholder="例如 root" />
|
||||
</el-form-item>
|
||||
<el-form-item label="认证方式">
|
||||
<el-select v-model="managedNodeForm.auth_mode">
|
||||
<el-option label="key" value="key" />
|
||||
<el-option label="password" value="password" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="managedNodeForm.is_enabled" active-text="已启用" inactive-text="已停用" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="managedNodeForm.auth_mode === 'password'" label="SSH 密码" class="full-width">
|
||||
<el-input
|
||||
v-model="managedNodeForm.ssh_password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="留空表示保留当前密码"
|
||||
/>
|
||||
<div class="summary-note">当前状态:{{ managedNodeForm.ssh_password_configured && !managedNodeForm.clear_ssh_password ? "已保存密码" : "未保存密码" }}</div>
|
||||
<el-switch v-model="managedNodeForm.clear_ssh_password" active-text="清除已保存密码" inactive-text="保留当前密码" />
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="SSH 私钥" class="full-width">
|
||||
<el-input
|
||||
v-model="managedNodeForm.ssh_private_key"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="可选:粘贴私钥;留空表示保留当前私钥"
|
||||
/>
|
||||
<div class="summary-note">当前状态:{{ managedNodeForm.ssh_private_key_configured && !managedNodeForm.clear_ssh_private_key ? "已保存私钥" : "未保存私钥" }}</div>
|
||||
<el-switch v-model="managedNodeForm.clear_ssh_private_key" active-text="清除已保存私钥" inactive-text="保留当前私钥" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="managedNodeVisible = false">关闭</el-button>
|
||||
<el-button type="primary" :loading="loading.saving" @click="saveManagedNode">保存节点</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</PageCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import PageCard from "@/components/PageCard.vue";
|
||||
import { opsApi } from "@/api/modules";
|
||||
|
||||
type GenericRecord = Record<string, any>;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref({
|
||||
refreshing: false,
|
||||
syncing: false,
|
||||
saving: false,
|
||||
});
|
||||
|
||||
const nodes = ref<GenericRecord[]>([]);
|
||||
const nodesSummary = ref<GenericRecord>({});
|
||||
const managedNodeVisible = ref(false);
|
||||
const managedNodeEditing = ref(false);
|
||||
|
||||
const managedNodeForm = ref({
|
||||
node_code: "",
|
||||
title: "",
|
||||
region: "mainland",
|
||||
role: "worker",
|
||||
ssh_entry: "",
|
||||
ssh_host: "",
|
||||
ssh_port: 22,
|
||||
ssh_user: "root",
|
||||
auth_mode: "key",
|
||||
ssh_password: "",
|
||||
ssh_private_key: "",
|
||||
ssh_password_configured: false,
|
||||
ssh_private_key_configured: false,
|
||||
clear_ssh_password: false,
|
||||
clear_ssh_private_key: false,
|
||||
is_enabled: true,
|
||||
metadata: {} as GenericRecord,
|
||||
});
|
||||
|
||||
const summary = computed(() => ({
|
||||
total: Number(nodesSummary.value.total || nodes.value.length || 0),
|
||||
managedTotal: Number(nodesSummary.value.managed_total || nodes.value.filter((item) => Boolean(item.is_managed)).length || 0),
|
||||
enabled: Number(nodesSummary.value.managed_enabled || nodes.value.filter((item) => Boolean(item.is_enabled)).length || 0),
|
||||
online: Number(nodesSummary.value.online || 0),
|
||||
stale: Number(nodesSummary.value.stale || 0),
|
||||
pendingBootstrap: Number(nodesSummary.value.pending_bootstrap || 0),
|
||||
participating: Number(nodesSummary.value.participating_nodes || nodes.value.filter((item) => Boolean(item.detect_participating)).length || 0),
|
||||
standby: Number(nodesSummary.value.non_participating_nodes || 0),
|
||||
queueDeadLetterNodes: Number(nodesSummary.value.queue_dead_letter_nodes || 0),
|
||||
}));
|
||||
|
||||
const resetManagedNodeForm = () => {
|
||||
managedNodeForm.value = {
|
||||
node_code: "",
|
||||
title: "",
|
||||
region: "mainland",
|
||||
role: "worker",
|
||||
ssh_entry: "",
|
||||
ssh_host: "",
|
||||
ssh_port: 22,
|
||||
ssh_user: "root",
|
||||
auth_mode: "key",
|
||||
ssh_password: "",
|
||||
ssh_private_key: "",
|
||||
ssh_password_configured: false,
|
||||
ssh_private_key_configured: false,
|
||||
clear_ssh_password: false,
|
||||
clear_ssh_private_key: false,
|
||||
is_enabled: true,
|
||||
metadata: {},
|
||||
};
|
||||
};
|
||||
|
||||
const sshEndpoint = (row: GenericRecord) => {
|
||||
const user = String(row?.ssh_user || "-").trim() || "-";
|
||||
const host = String(row?.ssh_host || "-").trim() || "-";
|
||||
const port = Number(row?.ssh_port || 22) || 22;
|
||||
return `${user}@${host}:${port}`;
|
||||
};
|
||||
|
||||
const agentStateType = (state: string) => {
|
||||
const normalized = String(state || "").trim();
|
||||
if (normalized === "online" || normalized === "online_busy") return "success";
|
||||
if (normalized === "stale" || normalized === "pending_bootstrap" || normalized === "runtime_only") return "warning";
|
||||
if (normalized === "token_expired" || normalized === "token_disabled") return "danger";
|
||||
return "info";
|
||||
};
|
||||
|
||||
const loadNodes = async () => {
|
||||
loading.value.refreshing = true;
|
||||
try {
|
||||
const response = await opsApi.nodes();
|
||||
nodes.value = Array.isArray(response.data?.nodes) ? response.data.nodes : [];
|
||||
nodesSummary.value = response.data?.summary || {};
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "读取托管节点失败");
|
||||
} finally {
|
||||
loading.value.refreshing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const syncNodesFromCluster = async () => {
|
||||
loading.value.syncing = true;
|
||||
try {
|
||||
const response = await opsApi.syncNodesFromCluster(false);
|
||||
await loadNodes();
|
||||
ElMessage.success(response.message || "节点已同步");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "同步节点失败");
|
||||
} finally {
|
||||
loading.value.syncing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openManagedNodeDialog = (row?: GenericRecord) => {
|
||||
resetManagedNodeForm();
|
||||
managedNodeEditing.value = Boolean(row?.node_code && row?.is_managed);
|
||||
if (row?.node_code) {
|
||||
managedNodeForm.value = {
|
||||
node_code: String(row.node_code || ""),
|
||||
title: String(row.title || row.node_code || ""),
|
||||
region: String(row.region || "mainland"),
|
||||
role: String(row.role || "worker"),
|
||||
ssh_entry: "",
|
||||
ssh_host: String(row.ssh_host || row.cluster_ip || row.agent_ip || row.metadata?.ip || ""),
|
||||
ssh_port: Number(row.ssh_port || 22) || 22,
|
||||
ssh_user: String(row.ssh_user || "root"),
|
||||
auth_mode: String(row.auth_mode || "key"),
|
||||
ssh_password: "",
|
||||
ssh_private_key: "",
|
||||
ssh_password_configured: Boolean(row.ssh_password_configured),
|
||||
ssh_private_key_configured: Boolean(row.ssh_private_key_configured),
|
||||
clear_ssh_password: false,
|
||||
clear_ssh_private_key: false,
|
||||
is_enabled: Boolean(row.is_managed ? row.is_enabled : true),
|
||||
metadata: { ...(row.metadata || {}) },
|
||||
};
|
||||
}
|
||||
managedNodeVisible.value = true;
|
||||
};
|
||||
|
||||
const saveManagedNode = async () => {
|
||||
if (!managedNodeForm.value.node_code.trim()) {
|
||||
ElMessage.warning("请先填写节点编码");
|
||||
return;
|
||||
}
|
||||
loading.value.saving = true;
|
||||
try {
|
||||
const response = await opsApi.upsertNode({
|
||||
node_code: managedNodeForm.value.node_code.trim(),
|
||||
title: managedNodeForm.value.title.trim() || managedNodeForm.value.node_code.trim(),
|
||||
region: managedNodeForm.value.region,
|
||||
role: managedNodeForm.value.role,
|
||||
ssh_entry: managedNodeForm.value.ssh_entry.trim(),
|
||||
ssh_host: managedNodeForm.value.ssh_host.trim(),
|
||||
ssh_port: managedNodeForm.value.ssh_port,
|
||||
ssh_user: managedNodeForm.value.ssh_user.trim(),
|
||||
auth_mode: managedNodeForm.value.auth_mode,
|
||||
ssh_password: managedNodeForm.value.auth_mode === "password" ? managedNodeForm.value.ssh_password : "",
|
||||
ssh_private_key: managedNodeForm.value.auth_mode === "key" ? managedNodeForm.value.ssh_private_key : "",
|
||||
clear_ssh_password: Boolean(managedNodeForm.value.clear_ssh_password),
|
||||
clear_ssh_private_key: Boolean(managedNodeForm.value.clear_ssh_private_key),
|
||||
is_enabled: managedNodeForm.value.is_enabled,
|
||||
metadata: {
|
||||
...(managedNodeForm.value.metadata || {}),
|
||||
updated_from: "managed-nodes-view",
|
||||
},
|
||||
});
|
||||
managedNodeVisible.value = false;
|
||||
await loadNodes();
|
||||
ElMessage.success(response.message || "托管节点已保存");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "保存托管节点失败");
|
||||
} finally {
|
||||
loading.value.saving = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleNodeEnabled = async (row: GenericRecord) => {
|
||||
const nextEnabled = !Boolean(row?.is_enabled);
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
nextEnabled ? `确认启用节点 ${row.node_code} 吗?` : `确认停用节点 ${row.node_code} 吗?`,
|
||||
nextEnabled ? "启用节点" : "停用节点",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: nextEnabled ? "确认启用" : "确认停用",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await opsApi.upsertNode({
|
||||
node_code: String(row.node_code || "").trim(),
|
||||
is_enabled: nextEnabled,
|
||||
});
|
||||
await loadNodes();
|
||||
ElMessage.success(response.message || (nextEnabled ? "节点已启用" : "节点已停用"));
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || "更新节点状态失败");
|
||||
}
|
||||
};
|
||||
|
||||
const goOpsCenter = () => {
|
||||
router.push("/ops-center");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await loadNodes();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.94));
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.summary-note {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.node-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.node-cell strong {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dialog-form {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dialog-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.summary-grid,
|
||||
.dialog-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6168,6 +6168,12 @@
|
||||
<el-option label="control" value="control" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH 一行录入" class="full-width">
|
||||
<el-input
|
||||
v-model="managedNodeForm.ssh_entry"
|
||||
placeholder="例如 121.204.244.188 root <password> 或 121.204.244.248:22 root <password>"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="SSH Host">
|
||||
<el-input v-model="managedNodeForm.ssh_host" placeholder="例如 121.204.244.248" />
|
||||
</el-form-item>
|
||||
@@ -6183,6 +6189,38 @@
|
||||
<el-option label="password" value="password" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="managedNodeForm.auth_mode === 'password'" label="SSH 密码" class="full-width">
|
||||
<el-input
|
||||
v-model="managedNodeForm.ssh_password"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="留空表示保留当前密码"
|
||||
/>
|
||||
<div class="summary-note" style="margin-top: 6px">
|
||||
当前状态:{{ managedNodeForm.ssh_password_configured && !managedNodeForm.clear_ssh_password ? "已保存密码" : "未保存密码" }}
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="managedNodeForm.clear_ssh_password"
|
||||
active-text="清除已保存密码"
|
||||
inactive-text="保留当前密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-else label="SSH 私钥" class="full-width">
|
||||
<el-input
|
||||
v-model="managedNodeForm.ssh_private_key"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
placeholder="可选:粘贴私钥;留空表示保留当前私钥"
|
||||
/>
|
||||
<div class="summary-note" style="margin-top: 6px">
|
||||
当前状态:{{ managedNodeForm.ssh_private_key_configured && !managedNodeForm.clear_ssh_private_key ? "已保存私钥" : "未保存私钥" }}
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="managedNodeForm.clear_ssh_private_key"
|
||||
active-text="清除已保存私钥"
|
||||
inactive-text="保留当前私钥"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发布渠道">
|
||||
<el-select v-model="managedNodeForm.deploy_channel">
|
||||
<el-option label="stable" value="stable" />
|
||||
@@ -6652,10 +6690,17 @@ const managedNodeForm = ref({
|
||||
title: "",
|
||||
region: "mainland",
|
||||
role: "worker",
|
||||
ssh_entry: "",
|
||||
ssh_host: "",
|
||||
ssh_port: 22,
|
||||
ssh_user: "root",
|
||||
auth_mode: "key",
|
||||
ssh_password: "",
|
||||
ssh_private_key: "",
|
||||
ssh_password_configured: false,
|
||||
ssh_private_key_configured: false,
|
||||
clear_ssh_password: false,
|
||||
clear_ssh_private_key: false,
|
||||
deploy_channel: "stable",
|
||||
is_enabled: true,
|
||||
metadata: {} as GenericRecord
|
||||
@@ -12712,10 +12757,17 @@ const resetManagedNodeForm = () => {
|
||||
title: "",
|
||||
region: "mainland",
|
||||
role: "worker",
|
||||
ssh_entry: "",
|
||||
ssh_host: "",
|
||||
ssh_port: 22,
|
||||
ssh_user: "root",
|
||||
auth_mode: "key",
|
||||
ssh_password: "",
|
||||
ssh_private_key: "",
|
||||
ssh_password_configured: false,
|
||||
ssh_private_key_configured: false,
|
||||
clear_ssh_password: false,
|
||||
clear_ssh_private_key: false,
|
||||
deploy_channel: "stable",
|
||||
is_enabled: true,
|
||||
metadata: {}
|
||||
@@ -13696,10 +13748,17 @@ const openManagedNodeDialog = (
|
||||
title: String(row.title || row.node_code || ""),
|
||||
region: String(row.region || "mainland"),
|
||||
role: String(row.role || "worker"),
|
||||
ssh_entry: "",
|
||||
ssh_host: String(row.ssh_host || row.cluster_ip || row.agent_ip || row.metadata?.ip || ""),
|
||||
ssh_port: Number(row.ssh_port || 22) || 22,
|
||||
ssh_user: String(row.ssh_user || "root"),
|
||||
auth_mode: String(row.auth_mode || "key"),
|
||||
ssh_password: "",
|
||||
ssh_private_key: "",
|
||||
ssh_password_configured: Boolean(row.ssh_password_configured),
|
||||
ssh_private_key_configured: Boolean(row.ssh_private_key_configured),
|
||||
clear_ssh_password: false,
|
||||
clear_ssh_private_key: false,
|
||||
deploy_channel: String(row.deploy_channel || "stable"),
|
||||
is_enabled: Boolean(row.is_managed ? row.is_enabled : true),
|
||||
metadata: {
|
||||
@@ -13866,10 +13925,15 @@ const saveManagedNode = async () => {
|
||||
title: managedNodeForm.value.title.trim() || managedNodeForm.value.node_code.trim(),
|
||||
region: managedNodeForm.value.region,
|
||||
role: managedNodeForm.value.role,
|
||||
ssh_entry: managedNodeForm.value.ssh_entry.trim(),
|
||||
ssh_host: managedNodeForm.value.ssh_host.trim(),
|
||||
ssh_port: managedNodeForm.value.ssh_port,
|
||||
ssh_user: managedNodeForm.value.ssh_user.trim(),
|
||||
auth_mode: managedNodeForm.value.auth_mode,
|
||||
ssh_password: managedNodeForm.value.auth_mode === "password" ? managedNodeForm.value.ssh_password : "",
|
||||
ssh_private_key: managedNodeForm.value.auth_mode === "key" ? managedNodeForm.value.ssh_private_key : "",
|
||||
clear_ssh_password: Boolean(managedNodeForm.value.clear_ssh_password),
|
||||
clear_ssh_private_key: Boolean(managedNodeForm.value.clear_ssh_private_key),
|
||||
deploy_channel: managedNodeForm.value.deploy_channel,
|
||||
is_enabled: managedNodeForm.value.is_enabled,
|
||||
metadata: {
|
||||
@@ -13885,6 +13949,14 @@ const saveManagedNode = async () => {
|
||||
ssh_port: managedNodeForm.value.ssh_port,
|
||||
ssh_user: managedNodeForm.value.ssh_user.trim(),
|
||||
auth_mode: managedNodeForm.value.auth_mode,
|
||||
ssh_password_configured:
|
||||
managedNodeForm.value.auth_mode === "password"
|
||||
? Boolean(managedNodeForm.value.ssh_password || (managedNodeForm.value.ssh_password_configured && !managedNodeForm.value.clear_ssh_password))
|
||||
: false,
|
||||
ssh_private_key_configured:
|
||||
managedNodeForm.value.auth_mode === "key"
|
||||
? Boolean(managedNodeForm.value.ssh_private_key || (managedNodeForm.value.ssh_private_key_configured && !managedNodeForm.value.clear_ssh_private_key))
|
||||
: false,
|
||||
deploy_channel: managedNodeForm.value.deploy_channel,
|
||||
is_enabled: managedNodeForm.value.is_enabled,
|
||||
metadata: { ...(managedNodeForm.value.metadata || {}) },
|
||||
|
||||
@@ -750,7 +750,7 @@
|
||||
<el-input :model-value="formatNodeThreadSummary(nodeThreadDialog.nodeCode)" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="单独覆盖线程数">
|
||||
<el-input-number v-model="nodeThreadDialog.threadCount" :min="1" :max="256" />
|
||||
<el-input-number v-model="nodeThreadDialog.threadCount" :min="1" />
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
:closable="false"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
/>
|
||||
|
||||
<el-form-item label="默认检测线程数">
|
||||
<el-input-number v-model="threadCount" :min="1" :max="256" />
|
||||
<el-input-number v-model="threadCount" :min="1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="节点独立线程覆盖">
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div v-for="(item, index) in nodeThreadOverrides" :key="`${item.node_code}-${index}`" class="node-thread-row">
|
||||
<el-input v-model="item.node_code" placeholder="节点编码,例如 mainland-worker-01" />
|
||||
<el-input-number v-model="item.thread_count" :min="1" :max="256" />
|
||||
<el-input-number v-model="item.thread_count" :min="1" />
|
||||
<el-button type="danger" plain @click="removeNodeThreadOverride(index)">删除</el-button>
|
||||
</div>
|
||||
<div class="node-thread-actions">
|
||||
|
||||
Reference in New Issue
Block a user