feat: Admin GEO readiness dashboard tab

- New 'GEO 就绪度' tab button in filter bar
- Overview stat cards: total checks, AI citations, citation rate, avg readiness
- AI engine breakdown display (DeepSeek/ChatGPT/Perplexity)
- Readiness scores table with 6-dimension color-coded tags
- Manual GEO tracking trigger button

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Yuzhiran Dev
2026-06-16 08:27:11 +08:00
parent 7c9a8b88b4
commit f15e0f720a
+91
View File
@@ -33,6 +33,7 @@
<el-button size="default" :type="activeTab === 'assistant' ? 'primary' : ''" @click="switchTab('assistant')">AI 助手</el-button>
<el-button size="default" :type="activeTab === 'searchproviders' ? 'primary' : ''" @click="switchTab('searchproviders')">搜索API</el-button>
<el-button size="default" :type="activeTab === 'searchrankings' ? 'primary' : ''" @click="switchTab('searchrankings')">搜索排名</el-button>
<el-button size="default" :type="activeTab === 'geo' ? 'primary' : ''" @click="switchTab('geo')">GEO 就绪度</el-button>
<el-button size="default" :type="activeTab === 'configitems' ? 'primary' : ''" @click="switchTab('configitems')">配置管理</el-button>
</div>
</div>
@@ -655,6 +656,70 @@
</template>
</div>
<div v-if="activeTab === 'geo'">
<div class="toolbar">
<span style="font-size:13px;color:#909399;">GEO 就绪度 — AI 搜索引用追踪 & 结构优化评分,每日 07:30 自动检查</span>
<el-button size="small" type="primary" @click="triggerGeoTrack" :loading="geoTracking">手动追踪</el-button>
</div>
<div v-if="geoLoading" class="card-loading">加载中...</div>
<template v-else>
<div style="display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap;">
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#f0f9ff;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#409eff;">{{ geoOverview.total_geo_checks }}</div>
<div style="font-size:12px;color:#909399;margin-top:4px;">GEO 检查次数</div>
</div>
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#f0fdf4;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#67c23a;">{{ geoOverview.total_cited }}</div>
<div style="font-size:12px;color:#909399;margin-top:4px;">AI 引用次数</div>
</div>
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#fef3f2;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#f56c6c;">{{ geoOverview.citation_rate }}%</div>
<div style="font-size:12px;color:#909399;margin-top:4px;">AI 引用率</div>
</div>
<div class="stat-card" style="flex:1;min-width:140px;padding:16px;background:#fff7e6;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#e6a23c;">{{ geoOverview.avg_geo_readiness }}</div>
<div style="font-size:12px;color:#909399;margin-top:4px;">平均就绪度评分</div>
</div>
</div>
<div v-if="Object.keys(geoOverview.engine_breakdown || {}).length" style="margin-bottom:16px;">
<h4 style="margin:0 0 8px;font-size:14px;color:#606266;">AI 引擎分布</h4>
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<div v-for="(val, engine) in geoOverview.engine_breakdown" :key="engine" style="padding:10px 16px;background:#fafafa;border-radius:6px;border:1px solid #ebeef5;min-width:120px;">
<div style="font-weight:600;font-size:14px;">{{ engine }}</div>
<div style="font-size:12px;color:#909399;margin-top:4px;">检查 {{ val.total }} 次,引用 {{ val.cited }} 次</div>
</div>
</div>
</div>
<h4 style="margin:16px 0 8px;font-size:14px;color:#606266;">就绪度评分</h4>
<el-table :data="geoReadinessScores" style="width:100%" size="small" max-height="500">
<el-table-column prop="article_title" label="文章" min-width="160" show-overflow-tooltip />
<el-table-column prop="total_score" label="总分" width="80">
<template #default="{ row }">
<el-tag :type="row.total_score >= 70 ? 'success' : row.total_score >= 40 ? 'warning' : 'danger'" size="small">{{ row.total_score }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="has_schema" label="结构化数据" width="100">
<template #default="{ row }"><el-tag :type="row.has_schema ? 'success' : 'info'" size="small">{{ row.has_schema ? '✓' : '✗' }}</el-tag></template>
</el-table-column>
<el-table-column prop="has_faq_format" label="FAQ" width="60">
<template #default="{ row }"><el-tag :type="row.has_faq_format ? 'success' : 'info'" size="small">{{ row.has_faq_format ? '✓' : '✗' }}</el-tag></template>
</el-table-column>
<el-table-column prop="has_howto_format" label="HowTo" width="70">
<template #default="{ row }"><el-tag :type="row.has_howto_format ? 'success' : 'info'" size="small">{{ row.has_howto_format ? '✓' : '✗' }}</el-tag></template>
</el-table-column>
<el-table-column prop="has_citations" label="引用" width="60">
<template #default="{ row }"><el-tag :type="row.has_citations ? 'success' : 'info'" size="small">{{ row.has_citations ? '✓' : '✗' }}</el-tag></template>
</el-table-column>
<el-table-column prop="word_count" label="字数" width="70" />
<el-table-column prop="readability_score" label="可读性" width="80" />
<el-table-column prop="heading_structure_score" label="标题结构" width="80" />
<el-table-column prop="checked_at" label="检查时间" width="150" />
</el-table>
</template>
</div>
<div v-if="activeTab === 'configitems'">
<div class="filter-bar">
<el-button size="default" :type="configSubTab === 'sensitive' ? 'primary' : ''" @click="switchConfigSubTab('sensitive')">🔒 敏感词</el-button>
@@ -1332,6 +1397,30 @@ const llmConfigs = ref([]);
finally { rankingsLoading.value = false; }
};
const geoOverview = ref({ total_geo_checks: 0, total_cited: 0, citation_rate: 0, avg_geo_readiness: 0, engine_breakdown: {} });
const geoReadinessScores = ref([]);
const geoLoading = ref(false);
const geoTracking = ref(false);
const loadGeoData = async () => {
geoLoading.value = true;
try {
const overview = await api.get('/api/seo/geo/overview');
geoOverview.value = overview.data || { total_geo_checks: 0, total_cited: 0, citation_rate: 0, avg_geo_readiness: 0, engine_breakdown: {} };
const scores = await api.get('/api/seo/geo/readiness?limit=50');
geoReadinessScores.value = scores.data || [];
} catch (e) { console.error('加载 GEO 数据失败:', e); }
finally { geoLoading.value = false; }
};
const triggerGeoTrack = async () => {
geoTracking.value = true;
try {
const r = await api.post('/api/seo/geo/track');
ElMessage.success('GEO 追踪完成');
await loadGeoData();
} catch (e) { ElMessage.error('GEO 追踪失败: ' + e.message); }
finally { geoTracking.value = false; }
};
const contentCleanRules = ref([]);
const ccrLoading = ref(false);
const ccrDialogVisible = ref(false);
@@ -1382,6 +1471,7 @@ const llmConfigs = ref([]);
logs: loadLogTypes, orgs: loadOrgs, roles: loadRoles, menus: loadMenus, assistant: loadAssistantConfig,
searchproviders: loadSearchProviders,
searchrankings: loadRankings,
geo: loadGeoData,
configitems: () => { loadSensitiveWords(); loadContentCleanRules(); },
};
const loadedTabs = new Set([]);
@@ -1442,6 +1532,7 @@ const llmConfigs = ref([]);
contentCleanRules, ccrLoading, ccrDialogVisible, ccrDialogTitle, ccrSaving, ccrForm, editingCcrId,
loadContentCleanRules, showAddCleanRule, editCleanRule, saveCleanRule, deleteCleanRule,
rankings, rankingStats, rankingsLoading, loadRankings,
geoOverview, geoReadinessScores, geoLoading, geoTracking, loadGeoData, triggerGeoTrack,
};
}
});