Files
openteam/web/src/views/console/DashboardView.vue
T
SakurasanandClaude 74424d980c 前端: 仪表盘快速开始卡片 + 移动端适配 + 主页 curl 用浏览器 host
- 仪表盘底部新增「快速开始」:Base URL(浏览器 host)+ 协议徽标 + curl 示例(可复制)
- 最近请求改为响应式两行(模型+状态 / token·成本·时间),窄屏不再挤压
- 主页调用演示改用 window.location.origin,去掉写死的 api.openteam.dev

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 20:30:03 +08:00

187 lines
7.3 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 { copyText } from '@/lib/clipboard'
import Badge from '@/components/ui/Badge.vue'
import Button from '@/components/ui/Button.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)
}
// 快速开始:Base URL 用浏览器当前 host(兼容端点),curl 示例
const baseURL = `${window.location.origin}/v1`
const curlCmd = `curl ${baseURL}/chat/completions \\
-H "Authorization: Bearer sk-xxxx" \\
-H "Content-Type: application/json" \\
-d '{"model":"gpt-test","stream":true,"messages":[{"role":"user","content":"hi"}]}'`
const copied = ref('')
async function copy(text: string, key: string) {
const ok = await copyText(text)
if (ok) {
copied.value = key
setTimeout(() => (copied.value = ''), 2000)
} else {
toast.err('复制失败,请手动复制')
}
}
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-muted">余额、用量与最近请求</p>
</div>
<!-- 指标条 -->
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
<div class="card p-4">
<p class="text-xs text-muted">余额</p>
<Skeleton v-if="loading" class="mt-2" height="1.5rem" width="55%" />
<p v-else class="mono-num mt-1 text-xl text-accent">{{ fmtMoney(balance) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-muted">今日请求</p>
<Skeleton v-if="loading" class="mt-2" height="1.5rem" width="40%" />
<p v-else class="mono-num mt-1 text-xl text-ink">{{ fmtNum(today.requests) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-muted">今日 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-ink">{{ fmtNum(today.tokens) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-muted">近 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-ink">{{ 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-muted">{{ 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-edge">
<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-edge">
<li v-for="l in logs" :key="l.id" class="py-2">
<div class="flex items-center justify-between gap-2">
<p class="min-w-0 truncate font-mono text-xs text-ink">{{ l.model }}</p>
<Badge class="shrink-0" :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
</div>
<p class="mt-0.5 flex flex-wrap items-center gap-x-2 text-[11px] text-muted">
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }}</span>
<span class="opacity-40">·</span>
<span class="mono-num">{{ fmtCost(l.cost) }}</span>
<span class="opacity-40">·</span>
<span>{{ fmtTime(l.created_at) }}</span>
</p>
</li>
<li v-if="logs.length === 0" class="py-6 text-center text-xs text-muted">
还没有请求记录,去
<router-link to="/console/keys" class="text-accent">API Keys</router-link>
创建密钥开始调用
</li>
</ul>
</div>
</div>
<!-- 快速开始 -->
<div class="card p-5">
<div class="mb-4">
<h2 class="text-sm font-semibold">快速开始</h2>
<p class="text-xs text-muted">通过兼容端点接入,OpenAI / Anthropic 协议自动转换</p>
</div>
<div class="space-y-4">
<div>
<p class="mb-1.5 text-[11px] font-medium text-muted">Base URL</p>
<div class="flex items-center gap-2">
<code class="min-w-0 flex-1 overflow-x-auto rounded-md border border-edge bg-surface px-3 py-2 font-mono text-xs whitespace-nowrap text-ink">{{ baseURL }}</code>
<Button size="sm" variant="ghost" class="shrink-0" @click="copy(baseURL, 'base')">
{{ copied === 'base' ? '已复制' : '复制' }}
</Button>
</div>
</div>
<div>
<p class="mb-1.5 text-[11px] font-medium text-muted">兼容</p>
<div class="flex flex-wrap gap-1.5">
<Badge variant="neutral">messages</Badge>
<Badge variant="neutral">chat/completions</Badge>
<Badge variant="neutral">responses</Badge>
</div>
</div>
<div>
<p class="mb-1.5 text-[11px] font-medium text-muted">curl 示例</p>
<div class="relative">
<pre class="overflow-x-auto rounded-md border border-edge bg-surface px-3 py-2.5 pr-20 font-mono text-[11px] leading-relaxed text-ink">{{ curlCmd }}</pre>
<Button size="sm" variant="ghost" class="absolute top-2 right-2" @click="copy(curlCmd, 'curl')">
{{ copied === 'curl' ? '已复制' : '复制' }}
</Button>
</div>
</div>
</div>
</div>
</div>
</template>