371 lines
14 KiB
Vue
371 lines
14 KiB
Vue
<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>
|