Files
openteam/web/src/views/console/DashboardView.vue
T
SakurasanandClaude 4846db9293 M3 收尾: 限流/配额 + 前端骨架屏 + 单端口托管前端
- ratelimit(内存计数): 密钥级每日请求数/Token 配额、用户级每秒速率
  (OT_RATELIMIT_USER_RPS), 超限返回 429
- 网关 Auth 前置配额/限流检查, finishUsage 累计密钥 token 用量
- 前端 Skeleton 组件 + Dashboard/管理总览加载态
- Go 服务托管 web/dist 静态资源(SPA 回退), 单端口即可访问前后端

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 16:13:09 +08:00

126 lines
4.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { http, errMsg } from '@/api/client'
import { useToastStore } from '@/stores/toast'
import { fmtMoney, fmtNum, fmtCost, fmtTime } from '@/lib/format'
import Badge from '@/components/ui/Badge.vue'
import Skeleton from '@/components/ui/Skeleton.vue'
import TrendChart from '@/components/ui/TrendChart.vue'
import type { UsageLog } from '@/types'
const toast = useToastStore()
const loading = ref(true)
const balance = ref(0)
const today = ref({ requests: 0, tokens: 0, cost: 0 })
const monthCost = ref(0)
const models = ref(0)
const trend = ref<{ label: string; value: number }[]>([])
const logs = ref<UsageLog[]>([])
async function load() {
try {
const [b, s, l] = await Promise.all([
http.get('/user/balance'),
http.get('/usage/stats?group=day&from=' + daysAgo(13)),
http.get('/usage/logs?page_size=8'),
])
const d = b.data.data
balance.value = d.balance
today.value = d.today
monthCost.value = d.spent_last_30d
models.value = d.models_available
trend.value = s.data.data.items.map((x: { date: string; cost: number }) => ({ label: x.date.slice(5), value: x.cost }))
logs.value = l.data.data.items
} catch (e) {
toast.err(errMsg(e))
} finally {
loading.value = false
}
}
function daysAgo(n: number): string {
const d = new Date(Date.now() - n * 864e5)
return d.toISOString().slice(0, 10)
}
onMounted(load)
</script>
<template>
<div class="mx-auto max-w-5xl space-y-6">
<div class="mb-2">
<h1 class="text-lg font-semibold">仪表盘</h1>
<p class="text-sm text-zinc-500">余额、用量与最近请求</p>
</div>
<!-- 指标条 -->
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
<div class="card p-4">
<p class="text-xs text-zinc-500">余额</p>
<Skeleton v-if="loading" class="mt-2" height="1.5rem" width="55%" />
<p v-else class="mono-num mt-1 text-xl text-emerald-300">{{ fmtMoney(balance) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-zinc-500">今日请求</p>
<Skeleton v-if="loading" class="mt-2" height="1.5rem" width="40%" />
<p v-else class="mono-num mt-1 text-xl text-zinc-100">{{ fmtNum(today.requests) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-zinc-500">今日 Token</p>
<Skeleton v-if="loading" class="mt-2" height="1.5rem" width="40%" />
<p v-else class="mono-num mt-1 text-xl text-zinc-100">{{ fmtNum(today.tokens) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-zinc-500">近 30 日消耗</p>
<Skeleton v-if="loading" class="mt-2" height="1.5rem" width="45%" />
<p v-else class="mono-num mt-1 text-xl text-zinc-100">{{ fmtCost(monthCost) }}</p>
</div>
</div>
<div class="grid gap-6 lg:grid-cols-5">
<!-- 用量趋势 -->
<div class="card p-5 lg:col-span-3">
<div class="mb-4 flex items-baseline justify-between">
<h2 class="text-sm font-semibold">近 14 天成本</h2>
<span class="font-mono text-[11px] text-zinc-600">{{ models }} 个可用模型</span>
</div>
<Skeleton v-if="loading" height="160px" />
<TrendChart v-else :points="trend" :format="(v) => '$' + v.toExponential(2)" />
</div>
<!-- 最近请求 -->
<div class="card p-5 lg:col-span-2">
<div class="mb-3 flex items-baseline justify-between">
<h2 class="text-sm font-semibold">最近请求</h2>
<router-link to="/console/usage" class="text-xs text-accent hover:text-accent-strong">查看全部</router-link>
</div>
<ul v-if="loading" class="divide-y divide-zinc-800/70">
<li v-for="i in 4" :key="i" class="flex items-center justify-between py-2">
<Skeleton height="0.75rem" width="40%" />
<Skeleton height="0.75rem" width="30%" />
</li>
</ul>
<ul v-else class="divide-y divide-zinc-800/70">
<li v-for="l in logs" :key="l.id" class="flex items-center justify-between py-2">
<div class="min-w-0">
<p class="truncate font-mono text-xs text-zinc-300">{{ l.model }}</p>
<p class="text-[11px] text-zinc-600">{{ fmtTime(l.created_at) }}</p>
</div>
<div class="flex items-center gap-2">
<span class="mono-num text-xs text-zinc-400">{{ l.input_tokens }}/{{ l.output_tokens }}</span>
<span class="mono-num w-16 text-right text-xs text-zinc-500">{{ fmtCost(l.cost) }}</span>
<Badge :variant="l.status === 'success' ? 'ok' : 'err'">{{ l.status }}</Badge>
</div>
</li>
<li v-if="logs.length === 0" class="py-6 text-center text-xs text-zinc-600">
还没有请求记录,去
<router-link to="/console/keys" class="text-accent">API Keys</router-link>
创建密钥开始调用
</li>
</ul>
</div>
</div>
</div>
</template>