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:
@@ -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,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user