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

196 lines
6.7 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="920px" draggable>
<div class="external-real-dialog">
<div class="external-real-dialog__toolbar">
<el-button type="primary" plain @click="loadSummary">刷新</el-button>
<el-button v-if="summary.summary_html_path" type="success" plain @click="openSummaryFile(summary.summary_html_path)">打开 HTML 摘要</el-button>
<el-button v-if="summary.summary_json_path" type="info" plain @click="openSummaryFile(summary.summary_json_path)">打开 JSON 摘要</el-button>
</div>
<el-alert
:title="alertTitle"
:description="summary.note || '当前尚未拉取真实外部平台数据。'"
:type="summary.real_metrics_available ? 'success' : 'warning'"
show-icon
:closable="false"
/>
<div class="external-real-dialog__summary">
<span>数据来源{{ summary.provider_label || "-" }}</span>
<span>来源状态{{ summary.provider_status || "-" }}</span>
<span>来源模式{{ sourceModeLabel(summary.source_mode) }}</span>
<span>当前状态{{ statusLabel(summary.status) }}</span>
</div>
<div class="external-real-dialog__metrics">
<div v-for="metric in summary.headline_metrics || []" :key="metric.key || metric.label" class="metric-card">
<div class="metric-card__label">{{ metric.label || "-" }}</div>
<div class="metric-card__value">{{ renderMetric(metric) }}</div>
<div class="metric-card__note">{{ metric.note || "-" }}</div>
</div>
</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="统计周期">{{ summary.days || 0 }} </el-descriptions-item>
<el-descriptions-item label="来源说明">{{ sourceHint }}</el-descriptions-item>
<el-descriptions-item label="结果更新时间">{{ formatDateTime(summary.generated_at) }}</el-descriptions-item>
<el-descriptions-item label="最新快照日期">{{ formatDateTime(summary.snapshot_metrics?.latest_metric_date) }}</el-descriptions-item>
</el-descriptions>
<div class="external-real-dialog__steps">
<div class="external-real-dialog__steps-title">下一步</div>
<ul>
<li v-for="(step, index) in summary.next_steps || []" :key="`${index}-${step}`">{{ step }}</li>
</ul>
</div>
</div>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { ElMessage } from "element-plus";
import { SiteList } from "@/api/interface/site/list";
import { getDomainExternalSeoRealSummary } from "@/api/modules/site/list";
import { openAdminArtifact } from "@/utils/adminArtifact";
const visible = ref(false);
const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoRealSummaryData>({});
const alertTitle = computed(() => {
if (summary.value.source_mode === "automation_snapshot") {
return "当前展示的是 Node 自动快照结果";
}
if (summary.value.real_metrics_available) {
return "当前已接入真实站外指标";
}
return "当前还在真实结果接入阶段";
});
const sourceHint = computed(() => {
if (summary.value.source_mode === "automation_snapshot") {
return "自动快照优先,主要看收录、起词和流量就绪,不是展现/点击报表。";
}
if (summary.value.source_mode === "manual_csv") {
return "当前来自人工 CSV / 报表导入,适合临时补录和历史对账。";
}
return "当前仍在平台级真实结果接入阶段。";
});
const loadSummary = async () => {
loading.value = true;
try {
const res = await getDomainExternalSeoRealSummary({ days: 7 });
summary.value = res.data ?? {};
} catch (_error) {
ElMessage.error("读取真实站外 summary 骨架失败");
} finally {
loading.value = false;
}
};
const openSummaryFile = (path?: string) => {
void openAdminArtifact(path);
};
const statusLabel = (status?: string) => {
if (status === "ready") return "已就绪";
if (status === "not_configured") return "未配置";
if (status === "provider_ready_but_property_map_missing") return "缺少属性映射";
if (status === "provider_ready_but_client_missing") return "缺少客户端依赖";
if (status === "ready_for_api_implementation") return "可接 API";
return status || "-";
};
const sourceModeLabel = (mode?: string) => {
if (mode === "automation_snapshot") return "自动快照";
if (mode === "manual_csv") return "人工报表";
if (mode === "provider_skeleton") return "接入骨架";
return mode || "-";
};
const renderMetric = (metric?: any) => {
if (!metric) return "-";
const value = metric.value;
if (value === null || value === undefined || value === "") return "-";
if (metric.format === "percent" && typeof value === "number") {
return `${(value * 100).toFixed(2)}%`;
}
return String(value);
};
const formatDateTime = (value?: string) => {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
const pad = (num: number) => String(num).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const open = async () => {
visible.value = true;
await loadSummary();
};
defineExpose({
open
});
</script>
<style scoped lang="scss">
.external-real-dialog__toolbar {
margin-bottom: 12px;
}
.external-real-dialog__summary {
display: flex;
gap: 16px;
margin: 12px 0;
color: #606266;
font-size: 13px;
}
.external-real-dialog__steps {
margin-top: 16px;
}
.external-real-dialog__metrics {
display: grid;
grid-template-columns: repeat(4, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.metric-card {
border: 1px solid #ebeef5;
border-radius: 10px;
padding: 14px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
.metric-card__label {
color: #909399;
font-size: 12px;
margin-bottom: 8px;
}
.metric-card__value {
color: #303133;
font-size: 26px;
font-weight: 700;
line-height: 1.2;
}
.metric-card__note {
margin-top: 8px;
color: #606266;
font-size: 12px;
}
.external-real-dialog__steps-title {
font-weight: 600;
margin-bottom: 8px;
}
</style>