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