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,371 @@
<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";
const BASE_URL = import.meta.env.VITE_API_URL as string;
const visible = ref(false);
const loading = ref(false);
const rows = ref<SiteList.DomainExternalSeoSnapshotValidateRunItem[]>([]);
const summary = ref<SiteList.DomainExternalSeoSnapshotValidateRunData | null>(null);
const openPublicPath = (path?: string) => {
if (!path) return;
window.open(`${BASE_URL}/${String(path).replace(/^\/+/, "")}`, "_blank", "noopener");
};
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>