Files
SEONexusAdmin/src/views/site/domainExternalSeoSnapshotValidateRunDialog.vue

371 lines
14 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="1080px" draggable>
<div class="snapshot-validate-run-dialog">
<div class="snapshot-validate-run-dialog__toolbar">
<el-button type="primary" plain @click="loadRuns">刷新</el-button>
</div>
<div v-if="summary" class="snapshot-validate-run-dialog__summary">
<el-alert
:title="buildSummaryTitle(summary)"
:type="summary.alert_buckets?.high ? 'error' : summary.alert_buckets?.medium ? 'warning' : 'success'"
:closable="false"
show-icon
/>
</div>
<div v-if="summary" class="snapshot-validate-run-dialog__hero">
<div class="snapshot-validate-run-dialog__hero-main">
<div class="snapshot-validate-run-dialog__hero-label">最近 dry-run 总览</div>
<div class="snapshot-validate-run-dialog__hero-value">{{ buildValidateHealth(summary) }}</div>
<div class="snapshot-validate-run-dialog__hero-note">
先看高优先级 dry-run 数量再看最新一笔字段校验是否已经恢复通过
</div>
</div>
<div class="snapshot-validate-run-dialog__hero-metrics">
<div v-for="item in buildHeroMetrics(summary)" :key="item.label" class="snapshot-validate-run-dialog__metric">
<div class="snapshot-validate-run-dialog__metric-label">{{ item.label }}</div>
<div class="snapshot-validate-run-dialog__metric-value">{{ item.value }}</div>
</div>
</div>
</div>
<div v-if="summary" class="snapshot-validate-run-dialog__attention">
<div class="snapshot-validate-run-dialog__attention-box">
<div class="snapshot-validate-run-dialog__attention-title">优先关注的 dry-run</div>
<div v-if="!(summary.top_attention_runs || []).length" class="snapshot-validate-run-dialog__attention-empty">当前没有高优先级 dry-run 异常</div>
<div v-else class="snapshot-validate-run-dialog__attention-list">
<div v-for="item in (summary.top_attention_runs || []).slice(0, 3)" :key="item.run_id" class="snapshot-validate-run-dialog__attention-item">
<div class="snapshot-validate-run-dialog__attention-run">{{ item.run_id }}</div>
<div class="snapshot-validate-run-dialog__attention-reason">{{ item.alert_reason || "-" }}</div>
<div class="snapshot-validate-run-dialog__attention-meta">
通过率 {{ Number(item.success_rate || 0).toFixed(0) }}% / {{ Number(item.error_count || 0) }}
</div>
</div>
</div>
</div>
<div class="snapshot-validate-run-dialog__attention-box">
<div class="snapshot-validate-run-dialog__attention-title">最新一笔字段校验</div>
<div class="snapshot-validate-run-dialog__latest-grid">
<div class="snapshot-validate-run-dialog__mini">
<div class="snapshot-validate-run-dialog__mini-label">Run</div>
<div class="snapshot-validate-run-dialog__mini-value snapshot-validate-run-dialog__mini-value--break">
{{ summary.latest_run?.run_id || "-" }}
</div>
</div>
<div class="snapshot-validate-run-dialog__mini">
<div class="snapshot-validate-run-dialog__mini-label">状态</div>
<div class="snapshot-validate-run-dialog__mini-value">{{ summary.latest_run?.passed ? "passed" : "failed" }}</div>
</div>
<div class="snapshot-validate-run-dialog__mini">
<div class="snapshot-validate-run-dialog__mini-label">通过率</div>
<div class="snapshot-validate-run-dialog__mini-value">{{ Number(summary.latest_run?.success_rate || 0).toFixed(0) }}%</div>
</div>
<div class="snapshot-validate-run-dialog__mini">
<div class="snapshot-validate-run-dialog__mini-label">主要原因</div>
<div class="snapshot-validate-run-dialog__mini-value">{{ summary.latest_run?.alert_reason || "-" }}</div>
</div>
</div>
</div>
</div>
<el-table v-loading="loading" :data="rows" border>
<el-table-column prop="run_id" label="Run ID" min-width="180" />
<el-table-column prop="received_count" label="接收" width="90" />
<el-table-column prop="valid_count" label="通过" width="90" />
<el-table-column prop="error_count" label="错误" width="90" />
<el-table-column prop="warning_count" label="警告" width="90" />
<el-table-column label="通过率" width="100">
<template #default="{ row }">
<span>{{ Number(row.success_rate || 0).toFixed(0) }}%</span>
</template>
</el-table-column>
<el-table-column label="告警" width="120">
<template #default="{ row }">
<el-tag v-if="row.alert_level && row.alert_level !== 'none'" :type="row.alert_level === 'high' ? 'danger' : row.alert_level === 'medium' ? 'warning' : 'info'" size="small">
{{ row.alert_level }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.passed ? 'success' : 'danger'" size="small">
{{ row.passed ? "passed" : "failed" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="错误聚合" min-width="240">
<template #default="{ row }">
<span v-if="!(row.error_buckets || []).length">-</span>
<span v-else>
{{
(row.error_buckets || [])
.slice(0, 3)
.map((item: any) => `${item.message} x${item.count}`)
.join(" ; ")
}}
</span>
</template>
</el-table-column>
<el-table-column label="警告聚合" min-width="220">
<template #default="{ row }">
<span v-if="!(row.warning_buckets || []).length">-</span>
<span v-else>
{{
(row.warning_buckets || [])
.slice(0, 2)
.map((item: any) => `${item.message} x${item.count}`)
.join(" ; ")
}}
</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.payload_json_path" link type="warning" @click="openPublicPath(row.payload_json_path)">原始快照</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoSnapshotValidateRuns } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const visible = ref(false);
const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoSnapshotValidateRunItem[]>([]);
const summary = ref<SiteList.DomainExternalSeoSnapshotValidateRunData | null>(null);
const openPublicPath = (path?: string) => {
void openAdminArtifact(path);
};
const loadRuns = async () => {
loading.value = true;
try {
const res = await getDomainExternalSeoSnapshotValidateRuns({ limit: 20 });
summary.value = res.data ?? null;
rows.value = res.data.items ?? [];
} catch (_error) {
ElMessage.error("读取站外快照校验记录失败");
} finally {
loading.value = false;
}
};
const buildSummaryTitle = (data?: SiteList.DomainExternalSeoSnapshotValidateRunData | null) => {
const high = Number(data?.alert_buckets?.high || 0);
const medium = Number(data?.alert_buckets?.medium || 0);
if (high > 0) {
const item = (data?.top_attention_runs || [])[0];
return `最近 dry-run 里有 ${high} 笔高优先级校验失败,先看 ${item?.run_id || "最新异常 run"}`;
}
if (medium > 0) {
const item = (data?.top_attention_runs || [])[0];
return `最近 dry-run 里有 ${medium} 笔需要关注的字段问题,建议先看 ${item?.run_id || "异常 run"}`;
}
return "最近 dry-run 整体稳定,当前没有高优先级字段校验问题。";
};
const buildValidateHealth = (data?: SiteList.DomainExternalSeoSnapshotValidateRunData | null) => {
if (Number(data?.alert_buckets?.high || 0) > 0) return "字段校验高风险";
if (Number(data?.alert_buckets?.medium || 0) > 0) return "字段校验需关注";
if (data?.latest_run?.passed) return "最近 dry-run 已恢复通过";
return "等待新的 dry-run 数据";
};
const buildHeroMetrics = (data?: SiteList.DomainExternalSeoSnapshotValidateRunData | null) => {
return [
{
label: "总校验笔数",
value: `${Number(data?.total || 0)}`,
},
{
label: "高优先级 Run",
value: `${Number(data?.alert_buckets?.high || 0)}`,
},
{
label: "最新通过率",
value: `${Number(data?.latest_run?.success_rate || 0).toFixed(0)}%`,
},
{
label: "最新错误数",
value: `${Number(data?.latest_run?.error_count || 0)}`,
},
];
};
const open = async () => {
visible.value = true;
await loadRuns();
};
defineExpose({ open });
</script>
<style scoped lang="scss">
.snapshot-validate-run-dialog__toolbar {
margin-bottom: 12px;
}
.snapshot-validate-run-dialog__summary {
margin-bottom: 12px;
}
.snapshot-validate-run-dialog__hero {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.snapshot-validate-run-dialog__hero-main {
border-radius: 12px;
padding: 16px;
color: #fff;
background: linear-gradient(135deg, #9a3412 0%, #b91c1c 100%);
}
.snapshot-validate-run-dialog__hero-label {
font-size: 13px;
opacity: 0.86;
}
.snapshot-validate-run-dialog__hero-value {
margin-top: 6px;
font-size: 28px;
font-weight: 700;
}
.snapshot-validate-run-dialog__hero-note {
margin-top: 8px;
font-size: 13px;
line-height: 1.6;
opacity: 0.92;
}
.snapshot-validate-run-dialog__hero-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.snapshot-validate-run-dialog__metric {
border: 1px solid #ebeef5;
border-radius: 12px;
padding: 14px;
background: #fafafa;
}
.snapshot-validate-run-dialog__metric-label {
color: #909399;
font-size: 12px;
}
.snapshot-validate-run-dialog__metric-value {
margin-top: 8px;
font-size: 24px;
font-weight: 700;
color: #303133;
}
.snapshot-validate-run-dialog__attention {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-bottom: 12px;
}
.snapshot-validate-run-dialog__attention-box {
border: 1px solid #ebeef5;
border-radius: 12px;
padding: 14px;
background: #fff;
}
.snapshot-validate-run-dialog__attention-title {
font-weight: 600;
margin-bottom: 10px;
}
.snapshot-validate-run-dialog__attention-empty {
color: #909399;
font-size: 13px;
}
.snapshot-validate-run-dialog__attention-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.snapshot-validate-run-dialog__attention-item {
padding: 10px;
border-radius: 10px;
background: #fafafa;
}
.snapshot-validate-run-dialog__attention-run {
font-size: 12px;
color: #606266;
word-break: break-all;
}
.snapshot-validate-run-dialog__attention-reason {
margin-top: 6px;
color: #303133;
line-height: 1.5;
}
.snapshot-validate-run-dialog__attention-meta {
margin-top: 6px;
color: #909399;
font-size: 12px;
}
.snapshot-validate-run-dialog__latest-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.snapshot-validate-run-dialog__mini {
padding: 10px;
border-radius: 10px;
background: #fafafa;
}
.snapshot-validate-run-dialog__mini-label {
color: #909399;
font-size: 12px;
}
.snapshot-validate-run-dialog__mini-value {
margin-top: 6px;
color: #303133;
font-weight: 600;
line-height: 1.5;
}
.snapshot-validate-run-dialog__mini-value--break {
word-break: break-all;
}
@media (max-width: 980px) {
.snapshot-validate-run-dialog__hero,
.snapshot-validate-run-dialog__attention,
.snapshot-validate-run-dialog__hero-metrics,
.snapshot-validate-run-dialog__latest-grid {
grid-template-columns: 1fr;
}
}
</style>