feat: 管理后台仪表盘统计卡片接入真实数据

新增 /api/admin/stats 聚合接口,填充运行时长、活跃设备、今日流量、在线节点四项卡片:
- VpnService 记录启动时间,提供 StartedAt/TotalLiveTraffic 方法
- 新增 TrafficStat 模型,客户端断开时 upsert 当日 rx/tx 流量(持久化)
- 前端 AdminView 调用 stats 接口并每 30 秒自动刷新
This commit is contained in:
2026-07-07 15:43:29 +08:00
parent 570dc82125
commit c14fe5ec06
7 changed files with 160 additions and 27 deletions
+36 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
@@ -17,6 +17,17 @@ const stats = ref([
])
const userCount = ref<number | null>(null)
let statsTimer: ReturnType<typeof setInterval> | null = null
function formatUptime(seconds: number): string {
if (seconds <= 0) return '0m'
const d = Math.floor(seconds / 86400)
const h = Math.floor((seconds % 86400) / 3600)
const m = Math.floor((seconds % 3600) / 60)
if (d > 0) return `${d}d ${h}h`
if (h > 0) return `${h}h ${m}m`
return `${m}m`
}
async function fetchUserCount() {
try {
@@ -32,9 +43,33 @@ async function fetchUserCount() {
} catch {}
}
async function fetchStats() {
try {
const res = await fetch('/api/admin/stats', {
headers: { Authorization: `Bearer ${authStore.token}` },
})
if (!res.ok) return
const data = await res.json()
const set = (label: string, value: string) => {
const stat = stats.value.find(s => s.label === label)
if (stat) stat.value = value
}
set('admin.uptime', formatUptime(data.uptime_seconds))
set('admin.activeDevices', String(data.active_devices))
set('admin.todayTraffic', (data.today_traffic_bytes / 1e9).toFixed(2))
set('admin.onlineNodes', String(data.online_nodes))
} catch {}
}
onMounted(async () => {
await authStore.fetchUser()
fetchUserCount()
fetchStats()
statsTimer = setInterval(fetchStats, 30000)
})
onUnmounted(() => {
if (statsTimer) clearInterval(statsTimer)
})
function handleStatClick(route: string) {