Signed-off-by: 吴文峰 <kevin@lmve.net>
This commit is contained in:
@@ -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()
|
||||
// 重新拉取 stats,pending 应该归零
|
||||
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>
|
||||
Reference in New Issue
Block a user