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

262 lines
11 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="failed-queue-dialog">
<div class="failed-queue-dialog__toolbar">
<el-button type="primary" plain @click="loadQueue">刷新</el-button>
<el-button type="success" plain :loading="selfHealingLoading" @click="runSelfHealing">后台执行自动修复</el-button>
<el-button type="danger" plain :loading="rerunLoading" @click="runQueueRerun">后台执行整批重跑</el-button>
<el-button type="warning" plain :loading="remediationLoading" @click="runQueueRemediation">后台执行补料候选</el-button>
<el-button v-if="rerunAllCommand" type="warning" plain @click="showRerunAllCommand">整批重跑命令</el-button>
<el-button type="info" plain @click="openSelfHealingDialog">最近自动修复记录</el-button>
<el-button type="info" plain @click="openRerunDialog">最近重跑记录</el-button>
<el-button type="info" plain @click="openRemediationDialog">最近补料记录</el-button>
</div>
<el-alert
title="这里是导入成功后,自动页面探测仍未通过的 host"
description="它不代表域名没有入库,而是代表首页、搜索、详情、播放这条页面链路还没有探通。"
type="warning"
show-icon
:closable="false"
/>
<div class="failed-queue-dialog__summary">
<span>当前探测失败{{ queueSummary.active_count || healthTrend.active_count || healthTrend.queue_count || 0 }}</span>
<span>已自动消退{{ queueSummary.resolved_count || healthTrend.resolved_count || 0 }}</span>
<span>重跑记录{{ healthTrend.rerun_runs_count || 0 }}</span>
<span>补料记录{{ healthTrend.remediation_runs_count || 0 }}</span>
<span>{{ trendLabel() }}</span>
</div>
<div class="failed-queue-dialog__summary">
<span>重跑消退{{ queueSummary.resolved_by_rerun_count || healthTrend.resolved_by_rerun_count || 0 }}</span>
<span>补料消退{{ queueSummary.resolved_by_remediation_count || healthTrend.resolved_by_remediation_count || 0 }}</span>
</div>
<div v-if="resolvedPreview().length" class="failed-queue-dialog__resolved">
<span class="failed-queue-dialog__resolved-label">最近自动消退</span>
<span>{{ resolvedPreview().join(" / ") }}</span>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column prop="host" label="Host" min-width="180" />
<el-table-column prop="run_id" label="Run ID" min-width="160" />
<el-table-column prop="failed_stage" label="失败阶段" width="100" />
<el-table-column label="失败明细" min-width="260">
<template #default="{ row }">
<span>
{{ formatFailedChecks(row) }}
</span>
</template>
</el-table-column>
<el-table-column prop="search_keyword" label="搜索词样本" min-width="160" />
<el-table-column label="补料候选" min-width="220">
<template #default="{ row }">
<span>
{{ row.remediation_plan?.title || "-" }} / {{ row.remediation_plan?.summary || "-" }}
</span>
</template>
</el-table-column>
<el-table-column prop="updated_at" label="更新时间" min-width="180" />
<el-table-column label="操作" width="320" fixed="right">
<template #default="{ row }">
<el-button v-if="row.summary_html_path" link type="primary" @click="openPublicPath(row.summary_html_path)">HTML摘要</el-button>
<el-button v-if="row.summary_json_path" link type="info" @click="openPublicPath(row.summary_json_path)">JSON摘要</el-button>
<el-button v-if="row.rerun_command" link type="warning" @click="showCommand('单条重跑命令', row.rerun_command)">重跑命令</el-button>
<el-button v-if="row.sample_command" link type="success" @click="showCommand('自动样本命令', row.sample_command)">样本命令</el-button>
<el-button v-if="row.remediation_command" link type="danger" @click="showCommand('补料候选命令', row.remediation_command)">补料命令</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<DomainImportSelfHealingDialog ref="selfHealingDialogRef" />
<DomainImportRerunDialog ref="rerunDialogRef" />
<DomainImportRemediationDialog ref="remediationDialogRef" />
<DomainImportTaskConsoleDialog ref="taskConsoleDialogRef" />
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { SiteList } from "@/api/interface/site/list";
import { getDomainImportFailedQueue, getDomainImportHealthTrend, runDomainImportFailedQueue, runDomainImportFailureRemediation, runDomainImportSelfHealing } from "@/api/modules/site/list";
import DomainImportSelfHealingDialog from "./domainImportSelfHealingDialog.vue";
import DomainImportRerunDialog from "./domainImportRerunDialog.vue";
import DomainImportRemediationDialog from "./domainImportRemediationDialog.vue";
import DomainImportTaskConsoleDialog from "./domainImportTaskConsoleDialog.vue";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false);
const loading = ref(false);
const selfHealingLoading = ref(false);
const rerunLoading = ref(false);
const remediationLoading = ref(false);
const rows = ref<SiteList.DomainImportFailedQueueItem[]>([]);
const queueSummary = ref<SiteList.DomainImportFailedQueueData>({});
const rerunAllCommand = ref("");
const selfHealingDialogRef = ref();
const rerunDialogRef = ref();
const remediationDialogRef = ref();
const taskConsoleDialogRef = ref();
const healthTrend = ref<SiteList.DomainImportHealthTrendData>({});
const formatFailedChecks = (row: SiteList.DomainImportFailedQueueItem) => {
return (row.failed_checks || [])
.slice(0, 2)
.map(item => `${item?.stage || "-"}:${item?.detail || "-"}`)
.join(" | ") || "-";
};
const openPublicPath = (path?: string) => {
if (!path) {
return;
}
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
};
const showCommand = async (title: string, command?: string) => {
if (!command) {
return;
}
await ElMessageBox.alert(`<code>${command}</code>`, title, {
dangerouslyUseHTMLString: true,
confirmButtonText: "知道了"
});
};
const showRerunAllCommand = async () => {
await showCommand("整批重跑命令", rerunAllCommand.value);
};
const openRerunDialog = () => {
rerunDialogRef.value?.open();
};
const openRemediationDialog = () => {
remediationDialogRef.value?.open();
};
const openSelfHealingDialog = () => {
selfHealingDialogRef.value?.open();
};
const trendLabel = () => {
const label = healthTrend.value.failure_trend?.label || "flat";
if (label === "improving") return "失败趋势改善中";
if (label === "worsening") return "失败趋势上升";
return "失败趋势持平";
};
const resolvedPreview = () => {
return queueSummary.value.resolved_hosts_preview || healthTrend.value.resolved_hosts_preview || [];
};
const runQueueRerun = async () => {
rerunLoading.value = true;
try {
const res = await runDomainImportFailedQueue({ limit: 50 });
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 loadQueue();
openRerunDialog();
});
} catch (_error) {
ElMessage.error("执行失败队列重跑失败");
} finally {
rerunLoading.value = false;
}
};
const runSelfHealing = 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 loadQueue();
openSelfHealingDialog();
});
} catch (_error) {
ElMessage.error("执行自动修复失败");
} finally {
selfHealingLoading.value = false;
}
};
const runQueueRemediation = async () => {
remediationLoading.value = true;
try {
const res = await runDomainImportFailureRemediation({ limit: 50 });
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 loadQueue();
openRemediationDialog();
});
} catch (_error) {
ElMessage.error("执行失败队列补料候选失败");
} finally {
remediationLoading.value = false;
}
};
const loadQueue = async () => {
loading.value = true;
try {
const [queueRes, trendRes] = await Promise.all([
getDomainImportFailedQueue({ limit: 50 }),
getDomainImportHealthTrend({ limit: 10 })
]);
queueSummary.value = queueRes.data ?? {};
rows.value = queueSummary.value.items ?? [];
rerunAllCommand.value = queueSummary.value.rerun_all_command ?? "";
healthTrend.value = trendRes.data ?? {};
} catch (_error) {
ElMessage.error("读取导入后探测失败队列失败");
} finally {
loading.value = false;
}
};
const open = async () => {
visible.value = true;
await loadQueue();
};
defineExpose({
open
});
</script>
<style scoped lang="scss">
.failed-queue-dialog__toolbar {
margin-bottom: 12px;
}
.failed-queue-dialog__summary {
display: flex;
gap: 16px;
margin-bottom: 12px;
color: #606266;
font-size: 13px;
}
.failed-queue-dialog__resolved {
margin-bottom: 12px;
font-size: 13px;
color: #909399;
}
.failed-queue-dialog__resolved-label {
margin-right: 8px;
color: #606266;
}
</style>