Files
SEONexusAdmin/src/views/site/domainExternalSeoCnOverviewDialog.vue
Your Name 845d68110b debug
2026-04-15 12:00:09 +08:00

257 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="大陆 SEO 总览" width="1220px" draggable>
<div class="cn-overview-dialog">
<div class="cn-overview-dialog__toolbar">
<el-button type="primary" plain @click="loadSummary">刷新</el-button>
</div>
<el-alert
:title="summary.health_label ? `当前大盘状态:${buildHealthLabel(summary.health_label)}` : '当前还没有大陆 SEO 总览数据'"
:description="summary.note || '这里会把快照摘要、收录变化、关键词升降和自动建议聚合到一个视角。'"
:type="buildHealthType(summary.health_label)"
show-icon
:closable="false"
/>
<div class="cn-overview-dialog__hero">
<div class="cn-overview-dialog__hero-main">
<div class="cn-overview-dialog__hero-label">大陆 SEO 大盘</div>
<div class="cn-overview-dialog__hero-value">{{ buildHealthLabel(summary.health_label) }}</div>
<div class="cn-overview-dialog__hero-note">
先看收录和关键词是否同时在变好再结合优先建议判断下一步要补哪里
</div>
</div>
<div class="cn-overview-dialog__hero-metrics">
<div v-for="item in buildHeroMetrics(summary)" :key="item.label" class="metric-card metric-card--hero">
<div class="metric-card__label">{{ item.label }}</div>
<div class="metric-card__value">{{ item.value }}</div>
</div>
</div>
</div>
<div class="cn-overview-dialog__metrics">
<div class="metric-card"><div class="metric-card__label">Host</div><div class="metric-card__value">{{ summary.snapshot_summary?.host_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">indexed_like</div><div class="metric-card__value">{{ summary.snapshot_summary?.indexed_like_host_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">keyword_ready</div><div class="metric-card__value">{{ summary.snapshot_summary?.keyword_ready_host_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">traffic_ready</div><div class="metric-card__value">{{ summary.snapshot_summary?.traffic_ready_host_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">newly_indexed</div><div class="metric-card__value">{{ summary.index_movement?.newly_indexed_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">lost_indexed</div><div class="metric-card__value">{{ summary.index_movement?.lost_indexed_count || 0 }}</div></div>
</div>
<div class="cn-overview-dialog__metrics cn-overview-dialog__metrics--priority">
<div class="metric-card metric-card--priority-high"><div class="metric-card__label">high</div><div class="metric-card__value">{{ summary.priority_buckets?.high || 0 }}</div></div>
<div class="metric-card metric-card--priority-medium"><div class="metric-card__label">medium</div><div class="metric-card__value">{{ summary.priority_buckets?.medium || 0 }}</div></div>
<div class="metric-card metric-card--priority-low"><div class="metric-card__label">low</div><div class="metric-card__value">{{ summary.priority_buckets?.low || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">keyword_up</div><div class="metric-card__value">{{ summary.keyword_movement?.up_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">keyword_down</div><div class="metric-card__value">{{ summary.keyword_movement?.down_count || 0 }}</div></div>
<div class="metric-card"><div class="metric-card__label">keyword_dropped</div><div class="metric-card__value">{{ summary.keyword_movement?.dropped_count || 0 }}</div></div>
</div>
<div class="cn-overview-dialog__descriptions">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="latest_snapshot_date">{{ summary.snapshot_summary?.latest_metric_date || "-" }}</el-descriptions-item>
<el-descriptions-item label="latest_index_date">{{ summary.index_movement?.latest_metric_date || "-" }}</el-descriptions-item>
<el-descriptions-item label="latest_keyword_date">{{ summary.keyword_movement?.latest_metric_date || "-" }}</el-descriptions-item>
<el-descriptions-item label="health_label">{{ summary.health_label || "-" }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="cn-overview-dialog__attention">
<div class="pane-card">
<div class="pane-card__title">大盘重点 Host</div>
<div v-if="!(summary.top_hosts || []).length" class="pane-card__empty">当前还没有重点 Host 数据</div>
<div v-else class="cn-overview-dialog__focus-list">
<div v-for="(item, index) in (summary.top_hosts || []).slice(0, 3)" :key="`${item.host || 'host'}-${index}`" class="cn-overview-dialog__focus-item">
<div class="cn-overview-dialog__focus-main">
<div class="cn-overview-dialog__focus-host">{{ item.host || "-" }}</div>
<div class="cn-overview-dialog__focus-meta">
{{ item.provider || "-" }} / 收录 {{ item.indexed_status || "-" }}
</div>
</div>
<div class="cn-overview-dialog__focus-side">
<div>PC词数 {{ item.pc_keyword_count ?? "-" }}</div>
<div>移动词数 {{ item.mobile_keyword_count ?? "-" }}</div>
</div>
</div>
</div>
</div>
<div class="pane-card">
<div class="pane-card__title">当前优先动作</div>
<div v-if="!(summary.top_suggestions || []).length" class="pane-card__empty">当前还没有自动建议</div>
<div v-else class="cn-overview-dialog__focus-list">
<div
v-for="(item, index) in (summary.top_suggestions || []).slice(0, 3)"
:key="`${item.title || 'suggestion'}-${index}`"
class="cn-overview-dialog__focus-item"
>
<div class="cn-overview-dialog__focus-main">
<div class="cn-overview-dialog__focus-host">{{ item.title || "-" }}</div>
<div class="cn-overview-dialog__focus-meta">{{ item.summary || "-" }}</div>
</div>
<div class="cn-overview-dialog__focus-side cn-overview-dialog__focus-side--action">
{{ item.action || "-" }}
</div>
</div>
</div>
</div>
</div>
<el-row :gutter="16" class="cn-overview-dialog__panes">
<el-col :span="12">
<div class="pane-card">
<div class="pane-card__title">重点 Host</div>
<el-table v-loading="loading" :data="summary.top_hosts || []" border max-height="360">
<el-table-column prop="host" label="Host" min-width="160" />
<el-table-column prop="provider" label="Provider" width="100" />
<el-table-column prop="indexed_status" label="收录状态" width="110" />
<el-table-column prop="pc_keyword_count" label="PC词数" width="90" />
<el-table-column prop="mobile_keyword_count" label="移动词数" width="90" />
</el-table>
</div>
</el-col>
<el-col :span="12">
<div class="pane-card">
<div class="pane-card__title">优先建议</div>
<el-table v-loading="loading" :data="summary.top_suggestions || []" border max-height="360">
<el-table-column prop="priority" label="优先级" width="90" />
<el-table-column prop="title" label="建议标题" min-width="160" />
<el-table-column prop="action" label="建议动作" min-width="260" />
</el-table>
</div>
</el-col>
</el-row>
</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 { getDomainExternalSeoCnOverview } from "@/api/modules/site/list";
const visible = ref(false);
const loading = ref(false);
const summary = ref<SiteList.DomainExternalSeoCnOverviewData>({});
const loadSummary = async () => {
loading.value = true;
try {
const res = await getDomainExternalSeoCnOverview({ days: 14, limit: 20 });
summary.value = res.data ?? {};
} catch (_error) {
ElMessage.error("读取大陆 SEO 总览失败");
} finally {
loading.value = false;
}
};
const buildHealthType = (healthLabel?: string) => {
if (healthLabel === "growing") return "success";
if (healthLabel === "attention" || healthLabel === "captcha") return "warning";
return "info";
};
const buildHealthLabel = (healthLabel?: string) => {
if (healthLabel === "growing") return "增长中";
if (healthLabel === "attention") return "需要关注";
if (healthLabel === "captcha") return "抓取风控偏多";
return healthLabel || "等待大盘数据";
};
const buildHeroMetrics = (data: SiteList.DomainExternalSeoCnOverviewData) => {
return [
{
label: "收录新增",
value: `${Number(data.index_movement?.newly_indexed_count || 0)}`,
},
{
label: "收录丢失",
value: `${Number(data.index_movement?.lost_indexed_count || 0)}`,
},
{
label: "关键词上升",
value: `${Number(data.keyword_movement?.up_count || 0)}`,
},
{
label: "关键词下降",
value: `${Number(data.keyword_movement?.down_count || 0)}`,
},
];
};
const open = async () => {
visible.value = true;
await loadSummary();
};
defineExpose({ open });
</script>
<style scoped lang="scss">
.cn-overview-dialog__toolbar { margin-bottom: 12px; }
.cn-overview-dialog__hero {
display:grid;
grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);
gap:12px;
margin:16px 0;
}
.cn-overview-dialog__hero-main {
border-radius:12px;
padding:16px;
color:#fff;
background:linear-gradient(135deg, #14532d 0%, #1d4ed8 100%);
}
.cn-overview-dialog__hero-label { font-size:13px; opacity:.86; }
.cn-overview-dialog__hero-value { margin-top:6px; font-size:28px; font-weight:700; }
.cn-overview-dialog__hero-note { margin-top:8px; font-size:13px; line-height:1.6; opacity:.92; }
.cn-overview-dialog__hero-metrics {
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:12px;
}
.cn-overview-dialog__metrics { display:grid; grid-template-columns:repeat(6,minmax(120px,1fr)); gap:12px; margin:16px 0; }
.cn-overview-dialog__metrics--priority { grid-template-columns:repeat(6,minmax(120px,1fr)); }
.metric-card { border:1px solid #ebeef5; border-radius:12px; padding:12px; background:#fafafa; }
.metric-card--hero { background:#fafafa; }
.metric-card--priority-high { background:#fff1f0; border-color:#fecaca; }
.metric-card--priority-medium { background:#fffbeb; border-color:#fde68a; }
.metric-card--priority-low { background:#f0f9ff; border-color:#bae6fd; }
.metric-card__label { color:#909399; font-size:12px; margin-bottom:6px; }
.metric-card__value { color:#303133; font-size:22px; font-weight:600; }
.cn-overview-dialog__descriptions { margin: 8px 0 16px; }
.cn-overview-dialog__attention {
display:grid;
grid-template-columns:repeat(2,minmax(0,1fr));
gap:16px;
margin-bottom:16px;
}
.cn-overview-dialog__panes { margin-top: 16px; }
.pane-card { border:1px solid #ebeef5; border-radius:12px; padding:12px; background:#fff; }
.pane-card__title { font-weight:600; margin-bottom:12px; }
.pane-card__empty { color:#909399; font-size:13px; }
.cn-overview-dialog__focus-list { display:flex; flex-direction:column; gap:10px; }
.cn-overview-dialog__focus-item {
display:flex;
justify-content:space-between;
gap:12px;
padding:10px;
border-radius:10px;
background:#fafafa;
}
.cn-overview-dialog__focus-main { min-width:0; }
.cn-overview-dialog__focus-host { color:#303133; font-weight:600; word-break:break-all; }
.cn-overview-dialog__focus-meta { margin-top:6px; color:#606266; font-size:12px; line-height:1.5; }
.cn-overview-dialog__focus-side { color:#606266; font-size:12px; line-height:1.8; text-align:right; }
.cn-overview-dialog__focus-side--action { max-width:220px; text-align:left; }
@media (max-width: 1100px) {
.cn-overview-dialog__hero,
.cn-overview-dialog__attention,
.cn-overview-dialog__hero-metrics,
.cn-overview-dialog__metrics,
.cn-overview-dialog__metrics--priority {
grid-template-columns:1fr;
}
}
</style>