Files
SEONexusAdmin/src/views/site/domainImportHealthWorkbenchDialog.vue
Your Name 845d68110b debug
2026-04-15 12:00:09 +08:00

678 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>