262 lines
11 KiB
Vue
262 lines
11 KiB
Vue
<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>
|