Signed-off-by: 吴文峰 <kevin@lmve.net>

This commit is contained in:
2026-04-08 20:11:43 +08:00
commit d3e7ae5724
20 changed files with 2647 additions and 0 deletions
+142
View File
@@ -0,0 +1,142 @@
<script setup>
import { ref, onMounted } from 'vue'
import { fetchStats, flushIndex } from '../api.js'
const stats = ref(null)
const loading = ref(true)
const flushing = ref(false)
const error = ref(null)
onMounted(async () => {
try {
stats.value = await fetchStats()
} catch (e) {
error.value = '无法加载统计数据,可能人服务器未启动或端口不对'
console.error(e)
} finally {
loading.value = false
}
})
async function doFlush() {
flushing.value = true
try {
await flushIndex()
// 重新拉取 statspending 应该归零
stats.value = await fetchStats()
} catch (e) {
error.value = '刷盘失败: ' + e.message
} finally {
flushing.value = false
}
}
function fmt(n) {
if (!n && n !== 0) return '0'
return Number(n).toLocaleString()
}
function topDomains(domains) {
if (!domains) return []
return Object.entries(domains).sort((a, b) => b[1] - a[1]).slice(0, 10)
}
function langColor(lang) {
const map = { zh: '#e53e3e', en: '#3182ce', ja: '#e53e3e', ko: '#3182ce', fr: '#38a169', de: '#d69e2e', es: '#38a169', ru: '#805ad5', other: '#718096' }
return map[lang] || map.other
}
</script>
<template>
<div class="p-8">
<h1 class="text-2xl font-semibold text-white mb-8">概览</h1>
<!-- Loading / Error -->
<div v-if="loading" class="flex items-center justify-center h-48">
<div class="text-gray-400 animate-pulse">加载中...</div>
</div>
<div v-else-if="error" class="bg-red-900/30 border border-red-800 rounded-lg p-4 text-red-300">
{{ error }}
</div>
<template v-else-if="stats">
<!-- Stat Cards -->
<div class="grid grid-cols-4 gap-5 mb-8">
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5">
<div class="text-sm text-gray-500 mb-2">已爬取 URL</div>
<div class="text-3xl font-bold text-white">{{ fmt(stats.total_urls) }}</div>
</div>
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5">
<div class="text-sm text-gray-500 mb-2">总词数</div>
<div class="text-3xl font-bold text-white">{{ fmt(stats.total_words) }}</div>
</div>
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5">
<div class="text-sm text-gray-500 mb-2">域名数量</div>
<div class="text-3xl font-bold text-white">{{ fmt(Object.keys(stats.domains).length) }}</div>
</div>
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5 flex flex-col justify-between">
<div>
<div class="text-sm text-gray-500 mb-2">待刷盘</div>
<div class="text-3xl font-bold" :class="stats.pending > 0 ? 'text-yellow-400' : 'text-green-400'">
{{ fmt(stats.pending) }}
</div>
</div>
<button
class="mt-3 w-full bg-blue-700 hover:bg-blue-600 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium py-1.5 px-3 rounded transition-colors cursor-pointer"
:disabled="flushing || !stats.pending"
@click="doFlush"
>
{{ flushing ? '刷盘中...' : '立即刷盘' }}
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-5">
<!-- Domain Distribution -->
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5">
<h2 class="text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">域名分布 Top 10</h2>
<div class="space-y-2">
<div
v-for="[domain, count] in topDomains(stats.domains)"
:key="domain"
class="flex items-center gap-3"
>
<div class="w-36 text-xs text-gray-400 truncate shrink-0" :title="domain">{{ domain }}</div>
<div class="flex-1 bg-gray-800 rounded-full h-5 overflow-hidden">
<div
class="h-full bg-blue-600 rounded-full transition-all duration-500"
:style="{ width: `${(count / stats.domains[Object.keys(stats.domains)[0]]) * 100}%` }"
></div>
</div>
<div class="w-16 text-xs text-gray-500 text-right shrink-0">{{ fmt(count) }}</div>
</div>
</div>
</div>
<!-- Language Distribution -->
<div class="bg-gray-900 border border-gray-800 rounded-xl p-5">
<h2 class="text-sm font-semibold text-gray-300 mb-4 uppercase tracking-wider">语种分布</h2>
<div class="space-y-2">
<div
v-for="[lang, count] in Object.entries(stats.languages || {}).sort((a,b) => b[1]-a[1])"
:key="lang"
class="flex items-center gap-3"
>
<div class="w-10 text-xs text-gray-400 shrink-0 font-mono">{{ lang }}</div>
<div class="flex-1 bg-gray-800 rounded-full h-5 overflow-hidden">
<div
class="h-full rounded-full transition-all duration-500"
:style="{
width: `${(count / stats.total_urls) * 100}%`,
backgroundColor: langColor(lang)
}"
></div>
</div>
<div class="w-16 text-xs text-gray-500 text-right shrink-0">{{ fmt(count) }}</div>
</div>
</div>
</div>
</div>
</template>
</div>
</template>