- 仪表盘底部新增「快速开始」:Base URL(浏览器 host)+ 协议徽标 + curl 示例(可复制) - 最近请求改为响应式两行(模型+状态 / token·成本·时间),窄屏不再挤压 - 主页调用演示改用 window.location.origin,去掉写死的 api.openteam.dev Co-Authored-By: Claude <noreply@anthropic.com>
187 lines
7.3 KiB
Vue
187 lines
7.3 KiB
Vue
<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>
|