This commit is contained in:
Your Name
2026-04-15 12:00:09 +08:00
parent 2962da0907
commit 845d68110b
426 changed files with 43961 additions and 34179 deletions

View File

@@ -0,0 +1,677 @@
<template>
<el-dialog v-model="visible" title="导入健康台" width="1100px" draggable>
<div class="import-health-workbench">
<div class="import-health-workbench__toolbar">
<el-button type="primary" plain @click="loadSummary">刷新</el-button>
<el-button type="primary" plain :loading="workbenchRunLoading" @click="runWorkbenchSummaryAction">刷新并落盘健康台</el-button>
<el-button type="success" plain :loading="selfHealingLoading" @click="runSelfHealingAction">执行自动修复</el-button>
<el-button type="success" plain @click="openClosureOverview">闭环总览</el-button>
<el-button type="info" plain @click="openImportRuns">最近导入记录</el-button>
<el-button type="warning" plain @click="openFailedQueue">探测失败队列</el-button>
<el-button type="info" plain @click="openRerunRuns">最近重跑记录</el-button>
<el-button type="warning" plain @click="openRemediationRuns">最近补料记录</el-button>
<el-button type="info" plain @click="openSelfHealingRuns">最近自动修复记录</el-button>
<el-button type="danger" plain @click="openManualAttention">人工关注队列</el-button>
<el-button type="primary" plain @click="openManualHandleRuns">最近人工处理记录</el-button>
<el-button type="info" plain @click="openWorkbenchRuns">最近健康台记录</el-button>
<el-button type="info" plain @click="openWorkbenchTrend">健康台趋势</el-button>
</div>
<div class="import-health-workbench__hero">
<div class="import-health-workbench__hero-title">导入健康台</div>
<div class="import-health-workbench__hero-desc">{{ healthLabelText() }}</div>
<div v-if="summary.hero_summary" class="import-health-workbench__hero-note">
{{ summary.hero_summary }}
</div>
</div>
<div class="import-health-workbench__stats">
<span>导入记录{{ summary.import_runs_count || 0 }}</span>
<span>当前失败{{ summary.active_count || summary.queue_count || 0 }}</span>
<span>自动修复记录{{ summary.self_healing_runs_count || 0 }}</span>
<span>人工关注{{ summary.manual_attention_count || 0 }}</span>
<span>重跑恢复{{ summary.resolved_by_rerun_count || 0 }}</span>
<span>补料恢复{{ summary.resolved_by_remediation_count || 0 }}</span>
</div>
<div class="import-health-workbench__blocks">
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">最新导入</div>
<div>{{ summary.latest_import_run?.run_id || "-" }}</div>
<div>失败 {{ summary.latest_import_run?.failed_count || 0 }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">最新失败 Host</div>
<div>{{ summary.latest_failed_item?.host || "-" }}</div>
<div>{{ summary.latest_failed_item?.failed_stage || "-" }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">最新自动修复</div>
<div>{{ summary.latest_self_healing_run?.run_id || "-" }}</div>
<div>{{ summary.latest_self_healing_run?.status || "-" }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">最新重跑</div>
<div>{{ summary.latest_rerun_run?.run_id || "-" }}</div>
<div>失败 {{ summary.latest_rerun_run?.failed_count || 0 }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">最新补料</div>
<div>{{ summary.latest_remediation_run?.run_id || "-" }}</div>
<div>恢复 {{ summary.latest_remediation_run?.recovered_count || 0 }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">失败趋势</div>
<div>{{ failureTrendText() }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">人工关注</div>
<div>{{ summary.manual_attention_count || 0 }}</div>
<div>{{ summary.latest_manual_attention_item?.host || "-" }}</div>
</div>
<div class="import-health-workbench__block">
<div class="import-health-workbench__block-title">最新人工处理</div>
<div>{{ summary.latest_manual_handle_run?.run_id || "-" }}</div>
<div>{{ summary.latest_manual_handle_run?.status || "-" }}</div>
</div>
</div>
<div v-if="(summary.resolved_hosts_preview || []).length" class="import-health-workbench__resolved">
<span class="import-health-workbench__resolved-label">最近恢复</span>
<span>{{ (summary.resolved_hosts_preview || []).join(" / ") }}</span>
</div>
<div class="import-health-workbench__insights">
<div class="import-health-workbench__insight-card">
<div class="import-health-workbench__panel-title">当前失败阶段分布</div>
<div
v-for="item in (summary.top_failed_stages || []).slice(0, 5)"
:key="item.stage"
class="import-health-workbench__panel-row"
>
<span>{{ item.stage || "-" }}</span>
<span>{{ item.count || 0 }}</span>
</div>
<div v-if="!(summary.top_failed_stages || []).length" class="import-health-workbench__empty">当前没有活跃失败阶段</div>
</div>
<div class="import-health-workbench__insight-card">
<div class="import-health-workbench__panel-title">当前恢复主路径</div>
<div class="import-health-workbench__mix-main">{{ recoveryMixText() }}</div>
<div class="import-health-workbench__mix-sub">
重跑恢复 {{ summary.recovery_mix?.resolved_by_rerun_count || 0 }} /
补料恢复 {{ summary.recovery_mix?.resolved_by_remediation_count || 0 }}
</div>
<div class="import-health-workbench__mix-sub">
自动修复记录 {{ summary.recovery_mix?.self_healing_runs_count || 0 }}
</div>
</div>
</div>
<div class="import-health-workbench__insights">
<div class="import-health-workbench__insight-card">
<div class="import-health-workbench__panel-title">人工关注分层</div>
<div class="import-health-workbench__mix-main">{{ manualAttentionText() }}</div>
<div class="import-health-workbench__mix-sub">
高优先 {{ summary.manual_attention_buckets?.high || 0 }} /
中优先 {{ summary.manual_attention_buckets?.medium || 0 }} /
低优先 {{ summary.manual_attention_buckets?.low || 0 }}
</div>
<div
v-for="item in (summary.recent_manual_attention_items || []).slice(0, 3)"
:key="item.host"
class="import-health-workbench__panel-row"
>
<span>{{ item.host || "-" }}</span>
<span>{{ item.attention_reason || "-" }}</span>
</div>
<div v-if="!(summary.recent_manual_attention_items || []).length" class="import-health-workbench__empty">当前没有需要人工接管的 host</div>
</div>
<div class="import-health-workbench__insight-card">
<div class="import-health-workbench__panel-title">最近人工处理</div>
<div class="import-health-workbench__mix-main">{{ manualHandleText() }}</div>
<div class="import-health-workbench__mix-sub">
处理记录 {{ summary.manual_handle_runs_count || 0 }} / 最新复核 {{ summary.latest_manual_handle_run?.reprobe_status || "-" }}
</div>
<div
v-for="item in (summary.recent_manual_handle_runs || []).slice(0, 3)"
:key="item.run_id"
class="import-health-workbench__panel-row"
>
<span>{{ item.host || "-" }}</span>
<span>{{ item.action || "-" }} / {{ item.status || "-" }}</span>
</div>
<div v-if="!(summary.recent_manual_handle_runs || []).length" class="import-health-workbench__empty">当前还没有人工处理记录</div>
</div>
</div>
<div class="import-health-workbench__insights">
<div class="import-health-workbench__insight-card">
<div class="import-health-workbench__panel-title">健康台记录告警</div>
<div class="import-health-workbench__mix-main">{{ workbenchAlertText() }}</div>
<div class="import-health-workbench__mix-sub">
高优先 {{ workbenchRunsSummary.alert_buckets?.high || 0 }} /
中优先 {{ workbenchRunsSummary.alert_buckets?.medium || 0 }} /
低优先 {{ workbenchRunsSummary.alert_buckets?.low || 0 }}
</div>
<div
v-for="item in (workbenchRunsSummary.top_attention_runs || []).slice(0, 3)"
:key="item.run_id"
class="import-health-workbench__panel-row"
>
<span>{{ item.run_id || "-" }}</span>
<span>{{ item.alert_reason || "-" }}</span>
</div>
<div v-if="!(workbenchRunsSummary.top_attention_runs || []).length" class="import-health-workbench__empty">当前没有需要优先回看的健康台 run</div>
</div>
<div class="import-health-workbench__insight-card">
<div class="import-health-workbench__panel-title">健康台趋势告警</div>
<div class="import-health-workbench__mix-main">{{ trendAlertText() }}</div>
<div class="import-health-workbench__mix-sub">
当前趋势 {{ trendText() }}
</div>
<div class="import-health-workbench__mix-sub">
{{ workbenchTrend.alert_reason || "当前健康台趋势平稳" }}
</div>
<div
v-for="item in (workbenchTrend.stage_buckets || []).slice(0, 3)"
:key="item.stage"
class="import-health-workbench__panel-row"
>
<span>{{ item.stage || "-" }}</span>
<span>{{ item.count || 0 }}</span>
</div>
</div>
</div>
<div v-if="(summary.priority_actions || []).length" class="import-health-workbench__actions">
<div
v-for="item in summary.priority_actions || []"
:key="item.key"
class="import-health-workbench__action-card"
>
<div class="import-health-workbench__action-head">
<div class="import-health-workbench__action-title">{{ item.label || "-" }}</div>
<el-tag size="small" :type="item.tag_type || 'info'">{{ actionTagText(item.tag_type) }}</el-tag>
</div>
<div class="import-health-workbench__action-summary">{{ item.summary || "-" }}</div>
<div class="import-health-workbench__action-foot">
<el-button size="small" :type="buttonType(item.tag_type)" plain @click="handlePriorityAction(item.action_key)">
{{ item.action_label || "执行" }}
</el-button>
</div>
</div>
</div>
<div class="import-health-workbench__panels">
<div class="import-health-workbench__panel">
<div class="import-health-workbench__panel-title">最近导入</div>
<div
v-for="item in (summary.recent_import_runs || []).slice(0, 5)"
:key="item.run_id"
class="import-health-workbench__panel-row"
>
<span>{{ item.run_id || "-" }}</span>
<span>失败 {{ item.failed_count || 0 }}</span>
</div>
</div>
<div class="import-health-workbench__panel">
<div class="import-health-workbench__panel-title">最近自动修复</div>
<div
v-for="item in (summary.recent_self_healing_runs || []).slice(0, 5)"
:key="item.run_id"
class="import-health-workbench__panel-row"
>
<span>{{ item.run_id || "-" }}</span>
<span>{{ item.status || "-" }}</span>
</div>
</div>
<div class="import-health-workbench__panel">
<div class="import-health-workbench__panel-title">最近重跑</div>
<div
v-for="item in (summary.recent_rerun_runs || []).slice(0, 5)"
:key="item.run_id"
class="import-health-workbench__panel-row"
>
<span>{{ item.run_id || "-" }}</span>
<span>失败 {{ item.failed_count || 0 }}</span>
</div>
</div>
<div class="import-health-workbench__panel">
<div class="import-health-workbench__panel-title">最近补料</div>
<div
v-for="item in (summary.recent_remediation_runs || []).slice(0, 5)"
:key="item.run_id"
class="import-health-workbench__panel-row"
>
<span>{{ item.run_id || "-" }}</span>
<span>恢复 {{ item.recovered_count || 0 }}</span>
</div>
</div>
</div>
</div>
</el-dialog>
<DomainImportRunDialog ref="importRunDialogRef" />
<DomainImportFailedQueueDialog ref="failedQueueDialogRef" />
<DomainImportRerunDialog ref="rerunDialogRef" />
<DomainImportRemediationDialog ref="remediationDialogRef" />
<DomainImportSelfHealingDialog ref="selfHealingDialogRef" />
<DomainImportManualAttentionDialog ref="manualAttentionDialogRef" />
<DomainImportManualAttentionHandleDialog ref="manualHandleDialogRef" />
<DomainImportHealthWorkbenchSummaryDialog ref="workbenchSummaryDialogRef" />
<DomainImportHealthWorkbenchTrendDialog ref="workbenchTrendDialogRef" />
<DomainImportTaskConsoleDialog ref="taskConsoleDialogRef" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list";
import {
getDomainImportHealthWorkbench,
getDomainImportHealthWorkbenchSummary,
getDomainImportHealthWorkbenchTrend,
runDomainImportHealthWorkbenchSummary,
runDomainImportSelfHealing
} from "@/api/modules/site/list";
import DomainImportRunDialog from "./domainImportRunDialog.vue";
import DomainImportFailedQueueDialog from "./domainImportFailedQueueDialog.vue";
import DomainImportRerunDialog from "./domainImportRerunDialog.vue";
import DomainImportRemediationDialog from "./domainImportRemediationDialog.vue";
import DomainImportSelfHealingDialog from "./domainImportSelfHealingDialog.vue";
import DomainImportManualAttentionDialog from "./domainImportManualAttentionDialog.vue";
import DomainImportManualAttentionHandleDialog from "./domainImportManualAttentionHandleDialog.vue";
import DomainImportHealthWorkbenchSummaryDialog from "./domainImportHealthWorkbenchSummaryDialog.vue";
import DomainImportHealthWorkbenchTrendDialog from "./domainImportHealthWorkbenchTrendDialog.vue";
import DomainImportTaskConsoleDialog from "./domainImportTaskConsoleDialog.vue";
const visible = ref(false);
const loading = ref(false);
const workbenchRunLoading = ref(false);
const selfHealingLoading = ref(false);
const summary = ref<SiteList.DomainImportHealthWorkbenchData>({});
const workbenchRunsSummary = ref<SiteList.DomainImportHealthWorkbenchSummaryData>({});
const workbenchTrend = ref<SiteList.DomainImportHealthWorkbenchTrendData>({});
const importRunDialogRef = ref();
const failedQueueDialogRef = ref();
const rerunDialogRef = ref();
const remediationDialogRef = ref();
const selfHealingDialogRef = ref();
const manualAttentionDialogRef = ref();
const manualHandleDialogRef = ref();
const workbenchSummaryDialogRef = ref();
const workbenchTrendDialogRef = ref();
const taskConsoleDialogRef = ref();
const requestGlobalDialogOpen = (key: string) => {
window.dispatchEvent(new CustomEvent("site-dialog-open", { detail: { key } }));
};
const healthLabelText = () => {
const label = summary.value.health_label || "steady";
if (label === "worsening") return "探测失败趋势上升,优先处理探测失败队列";
if (label === "attention") return "当前仍有失败 host建议关注自愈结果";
if (label === "waiting_self_healing") return "存在失败 host但还缺自动修复进展";
return "导入链整体平稳";
};
const actionTagText = (tagType?: string) => {
if (tagType === "danger") return "高优先";
if (tagType === "warning") return "先处理";
if (tagType === "success") return "已接入";
return "建议";
};
const buttonType = (tagType?: string) => {
if (tagType === "danger") return "danger";
if (tagType === "warning") return "warning";
if (tagType === "success") return "success";
return "primary";
};
const failureTrendText = () => {
const label = summary.value.failure_trend?.label || "flat";
if (label === "improving") return "改善中";
if (label === "worsening") return "上升中";
return "持平";
};
const recoveryMixText = () => {
const path = summary.value.recovery_mix?.primary_path || "none";
if (path === "rerun") return "当前恢复主要靠重跑";
if (path === "remediation") return "当前恢复主要靠补料";
if (path === "balanced") return "当前恢复由重跑和补料共同承担";
return "当前还没有明显恢复主路径";
};
const manualAttentionText = () => {
const count = summary.value.manual_attention_count || 0;
if (count > 0) {
return `当前有 ${count} 个 host 需要人工接管`;
}
return "当前没有需要人工接管的 host";
};
const manualHandleText = () => {
const latestRun = summary.value.latest_manual_handle_run || {};
if (latestRun.run_id) {
return `最新人工处理 ${latestRun.run_id}Host ${latestRun.host || "-"},状态 ${latestRun.status || "-"}`;
}
return "当前还没有人工处理记录";
};
const workbenchAlertText = () => {
const level = workbenchRunsSummary.value.latest_run?.alert_level || "none";
if (level === "high") return "最近健康台 run 有高优先级告警";
if (level === "medium") return "最近健康台 run 需要继续重点关注";
if (level === "low") return "当前处于恢复观察态";
return "最近健康台 run 整体平稳";
};
const trendText = () => {
const label = workbenchTrend.value.label || "flat";
if (label === "improving") return "改善中";
if (label === "worsening") return "上升中";
return "持平";
};
const trendAlertText = () => {
const level = workbenchTrend.value.alert_level || "none";
if (level === "high") return "趋势高优先级告警";
if (level === "medium") return "趋势需要继续重点关注";
if (level === "low") return "趋势进入低优先级观察态";
return "趋势平稳";
};
const openImportRuns = () => {
importRunDialogRef.value?.open();
};
const openFailedQueue = () => {
failedQueueDialogRef.value?.open();
};
const openSelfHealingRuns = () => {
selfHealingDialogRef.value?.open();
};
const openManualAttention = () => {
manualAttentionDialogRef.value?.open();
};
const openManualHandleRuns = () => {
manualHandleDialogRef.value?.open();
};
const openRerunRuns = () => {
rerunDialogRef.value?.open();
};
const openRemediationRuns = () => {
remediationDialogRef.value?.open();
};
const openWorkbenchRuns = () => {
workbenchSummaryDialogRef.value?.open();
};
const openWorkbenchTrend = () => {
workbenchTrendDialogRef.value?.open();
};
const openClosureOverview = () => {
requestGlobalDialogOpen("domainImportExternalSeoClosureDialog");
};
const handlePriorityAction = async (actionKey?: string) => {
if (actionKey === "open_import_runs") {
openImportRuns();
return;
}
if (actionKey === "open_failed_queue") {
openFailedQueue();
return;
}
if (actionKey === "open_self_healing_runs") {
openSelfHealingRuns();
return;
}
if (actionKey === "open_manual_attention") {
openManualAttention();
return;
}
if (actionKey === "open_manual_handle_runs") {
openManualHandleRuns();
return;
}
if (actionKey === "open_rerun_runs") {
openRerunRuns();
return;
}
if (actionKey === "open_remediation_runs") {
openRemediationRuns();
return;
}
if (actionKey === "run_self_healing") {
await runSelfHealingAction();
}
};
const runSelfHealingAction = async () => {
selfHealingLoading.value = true;
try {
const res = await runDomainImportSelfHealing();
const job = res.data.job;
if (!job?.job_id) {
throw new Error("job_id missing");
}
ElMessage.success("自动修复任务已创建");
taskConsoleDialogRef.value?.open("自动修复任务", job.job_id, async () => {
await loadSummary();
openSelfHealingRuns();
});
} catch (_error) {
ElMessage.error("执行自动修复失败");
} finally {
selfHealingLoading.value = false;
}
};
const runWorkbenchSummaryAction = async () => {
workbenchRunLoading.value = true;
try {
const res = await runDomainImportHealthWorkbenchSummary({ limit: 10 });
const job = res.data.job;
if (!job?.job_id) {
throw new Error("job_id missing");
}
ElMessage.success("健康台刷新任务已创建");
taskConsoleDialogRef.value?.open("健康台落盘任务", job.job_id, async () => {
await loadSummary();
openWorkbenchRuns();
});
} catch (_error) {
ElMessage.error("刷新并落盘健康台失败");
} finally {
workbenchRunLoading.value = false;
}
};
const loadSummary = async () => {
loading.value = true;
try {
const [workbenchRes, runsRes, trendRes] = await Promise.all([
getDomainImportHealthWorkbench({ limit: 10 }),
getDomainImportHealthWorkbenchSummary({ limit: 10 }),
getDomainImportHealthWorkbenchTrend({ limit: 10 })
]);
summary.value = workbenchRes.data ?? {};
workbenchRunsSummary.value = runsRes.data ?? {};
workbenchTrend.value = trendRes.data ?? {};
} catch (_error) {
ElMessage.error("读取导入健康台失败");
} finally {
loading.value = false;
}
};
const open = async () => {
visible.value = true;
await loadSummary();
};
defineExpose({
open
});
</script>
<style scoped lang="scss">
.import-health-workbench__toolbar {
margin-bottom: 12px;
}
.import-health-workbench__hero {
padding: 14px 16px;
border-radius: 12px;
background: linear-gradient(135deg, #f3f8ff 0%, #eefaf4 100%);
margin-bottom: 12px;
}
.import-health-workbench__hero-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.import-health-workbench__hero-desc,
.import-health-workbench__hero-note,
.import-health-workbench__stats,
.import-health-workbench__resolved {
font-size: 13px;
color: #606266;
}
.import-health-workbench__hero-note {
margin-top: 8px;
}
.import-health-workbench__stats {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.import-health-workbench__blocks {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.import-health-workbench__block {
border: 1px solid #ebeef5;
border-radius: 12px;
padding: 12px;
background: #fff;
}
.import-health-workbench__block-title {
font-size: 13px;
color: #909399;
margin-bottom: 6px;
}
.import-health-workbench__resolved-label {
margin-right: 8px;
color: #303133;
}
.import-health-workbench__insights {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 12px;
}
.import-health-workbench__insight-card {
border: 1px solid #ebeef5;
border-radius: 12px;
padding: 12px;
background: #fff;
}
.import-health-workbench__mix-main {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.import-health-workbench__mix-sub,
.import-health-workbench__empty {
font-size: 13px;
color: #606266;
}
.import-health-workbench__actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 12px;
}
.import-health-workbench__action-card {
border: 1px solid #ebeef5;
border-radius: 12px;
padding: 12px;
background: #fff;
}
.import-health-workbench__action-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.import-health-workbench__action-title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.import-health-workbench__action-summary {
font-size: 13px;
color: #606266;
line-height: 1.6;
min-height: 42px;
}
.import-health-workbench__action-foot {
margin-top: 10px;
}
.import-health-workbench__panels {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 12px;
}
.import-health-workbench__panel {
border: 1px solid #ebeef5;
border-radius: 12px;
padding: 12px;
background: #fff;
}
.import-health-workbench__panel-title {
font-size: 13px;
color: #909399;
margin-bottom: 8px;
}
.import-health-workbench__panel-row {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 6px 0;
font-size: 13px;
color: #606266;
border-top: 1px dashed #f0f2f5;
}
.import-health-workbench__panel-row:first-of-type {
border-top: 0;
}
</style>