前端: 浅色模式 + 系统自动主题

- 引入语义化 CSS 变量令牌(bg/surface/edge/ink/muted/accent 等), data-theme 三态切换
- theme store(localStorage + 系统偏好跟随), 首屏内联脚本防主题闪烁
- ThemeToggle 组件(浅色/深色/跟随系统) 置于导航与控制台顶栏
- 全量替换硬编码 zinc/emerald/red 类为令牌, 图表基线/状态色随主题适配
- 浅色对比度校准(accent/err/warn 深浅各一套)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-15 16:20:43 +08:00
co-authored by Claude
parent 4846db9293
commit a422034b2b
24 changed files with 380 additions and 249 deletions
+17 -17
View File
@@ -51,30 +51,30 @@ onMounted(load)
<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>
<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-zinc-500">余额</p>
<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-emerald-300">{{ fmtMoney(balance) }}</p>
<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-zinc-500">今日请求</p>
<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-zinc-100">{{ fmtNum(today.requests) }}</p>
<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-zinc-500">今日 Token</p>
<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-zinc-100">{{ fmtNum(today.tokens) }}</p>
<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-zinc-500">近 30 日消耗</p>
<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-zinc-100">{{ fmtCost(monthCost) }}</p>
<p v-else class="mono-num mt-1 text-xl text-ink">{{ fmtCost(monthCost) }}</p>
</div>
</div>
@@ -83,7 +83,7 @@ onMounted(load)
<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>
<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)" />
@@ -95,25 +95,25 @@ onMounted(load)
<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">
<ul v-if="loading" class="divide-y divide-edge/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">
<ul v-else class="divide-y divide-edge/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>
<p class="truncate font-mono text-xs text-ink">{{ l.model }}</p>
<p class="text-[11px] text-muted">{{ 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>
<span class="mono-num text-xs text-muted">{{ l.input_tokens }}/{{ l.output_tokens }}</span>
<span class="mono-num w-16 text-right text-xs text-muted">{{ 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">
<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>
创建密钥开始调用
+10 -10
View File
@@ -75,7 +75,7 @@ onMounted(load)
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-lg font-semibold">API Keys</h1>
<p class="text-sm text-zinc-500">密钥明文仅在创建时展示一次,请立即保存</p>
<p class="text-sm text-muted">密钥明文仅在创建时展示一次,请立即保存</p>
</div>
<Button @click="createOpen = true">新建密钥</Button>
</div>
@@ -84,7 +84,7 @@ onMounted(load)
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-zinc-800 text-left text-xs text-zinc-500">
<tr class="border-b border-edge text-left text-xs text-muted">
<th scope="col" class="px-4 py-2.5 font-medium">名称</th>
<th scope="col" class="px-4 py-2.5 font-medium">前缀</th>
<th scope="col" class="px-4 py-2.5 font-medium">状态</th>
@@ -95,17 +95,17 @@ onMounted(load)
</thead>
<tbody>
<tr v-for="k in keys" :key="k.id" class="table-row">
<td class="px-4 py-2.5 text-zinc-200">{{ k.name }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-400">{{ k.key_prefix }}…</td>
<td class="px-4 py-2.5 text-ink">{{ k.name }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ k.key_prefix }}…</td>
<td class="px-4 py-2.5">
<Badge :variant="k.status === 'active' ? 'ok' : 'neutral'">{{ k.status }}</Badge>
</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ fmtTime(k.last_used_at) }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ fmtTime(k.created_at) }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(k.last_used_at) }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(k.created_at) }}</td>
<td class="px-4 py-2.5 text-right">
<button
v-if="k.status === 'active'"
class="text-xs text-zinc-500 hover:text-red-400"
class="text-xs text-muted hover:text-err"
@click="revoke(k)"
>
吊销
@@ -113,7 +113,7 @@ onMounted(load)
</td>
</tr>
<tr v-if="keys.length === 0">
<td colspan="6" class="px-4 py-10 text-center text-sm text-zinc-600">
<td colspan="6" class="px-4 py-10 text-center text-sm text-muted">
还没有密钥,点击右上角「新建密钥」
</td>
</tr>
@@ -134,9 +134,9 @@ onMounted(load)
<!-- 一次性展示密钥 -->
<Modal :open="!!created" title="密钥已创建" @close="created = null">
<div class="space-y-4">
<p class="text-xs text-zinc-500">请复制并妥善保存,关闭后不再显示。</p>
<p class="text-xs text-muted">请复制并妥善保存,关闭后不再显示。</p>
<div class="flex items-center gap-2">
<code class="mono-num flex-1 truncate rounded-md border border-accent/40 bg-zinc-900 px-3 py-2 text-xs text-emerald-300">
<code class="mono-num flex-1 truncate rounded-md border border-accent/40 bg-surface px-3 py-2 text-xs text-accent">
{{ created?.key }}
</code>
<Button size="sm" @click="copyKey">
+24 -24
View File
@@ -51,42 +51,42 @@ onMounted(load)
<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>
<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-zinc-500">今日请求</p>
<p class="mono-num mt-1 text-xl text-zinc-100">{{ fmtNum(summary.today.requests) }}</p>
<p class="text-xs text-muted">今日请求</p>
<p class="mono-num mt-1 text-xl text-ink">{{ fmtNum(summary.today.requests) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-zinc-500">今日 Token</p>
<p class="mono-num mt-1 text-xl text-zinc-100">{{ fmtNum(summary.today.tokens) }}</p>
<p class="text-xs text-muted">今日 Token</p>
<p class="mono-num mt-1 text-xl text-ink">{{ fmtNum(summary.today.tokens) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-zinc-500">本月请求</p>
<p class="mono-num mt-1 text-xl text-zinc-100">{{ fmtNum(summary.month.requests) }}</p>
<p class="text-xs text-muted">本月请求</p>
<p class="mono-num mt-1 text-xl text-ink">{{ fmtNum(summary.month.requests) }}</p>
</div>
<div class="card p-4">
<p class="text-xs text-zinc-500">本月成本</p>
<p class="mono-num mt-1 text-xl text-zinc-100">{{ fmtCost(summary.month.cost) }}</p>
<p class="text-xs text-muted">本月成本</p>
<p class="mono-num mt-1 text-xl text-ink">{{ fmtCost(summary.month.cost) }}</p>
</div>
</div>
<div class="card p-5">
<div class="mb-4 flex items-center justify-between">
<h2 class="text-sm font-semibold">成本分布</h2>
<div class="flex gap-1 rounded-md border border-zinc-800 p-0.5">
<div class="flex gap-1 rounded-md border border-edge p-0.5">
<button
class="rounded px-2.5 py-1 text-xs transition"
:class="group === 'day' ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-500'"
:class="group === 'day' ? 'bg-surface2 text-ink' : 'text-muted'"
@click="switchGroup('day')"
>
按天
</button>
<button
class="rounded px-2.5 py-1 text-xs transition"
:class="group === 'model' ? 'bg-zinc-800 text-zinc-100' : 'text-zinc-500'"
:class="group === 'model' ? 'bg-surface2 text-ink' : 'text-muted'"
@click="switchGroup('model')"
>
按模型
@@ -97,19 +97,19 @@ onMounted(load)
</div>
<div class="card">
<div class="flex items-center justify-between border-b border-zinc-800 px-4 py-3">
<div class="flex items-center justify-between border-b border-edge px-4 py-3">
<h2 class="text-sm font-semibold">请求明细</h2>
<input
v-model="modelFilter"
placeholder="按模型过滤"
class="h-8 w-48 rounded-md border border-zinc-700 bg-zinc-900 px-2.5 font-mono text-xs outline-none focus:border-accent"
class="h-8 w-48 rounded-md border border-edge2 bg-surface px-2.5 font-mono text-xs outline-none focus:border-accent"
@keyup.enter="page = 1; load()"
/>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-zinc-800 text-left text-xs text-zinc-500">
<tr class="border-b border-edge text-left text-xs text-muted">
<th scope="col" class="px-4 py-2.5 font-medium">模型</th>
<th scope="col" class="px-4 py-2.5 font-medium">协议</th>
<th scope="col" class="px-4 py-2.5 font-medium">Token 入/出</th>
@@ -121,22 +121,22 @@ onMounted(load)
</thead>
<tbody>
<tr v-for="l in logs" :key="l.id" class="table-row">
<td class="px-4 py-2.5 font-mono text-xs text-zinc-200">{{ l.model }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ l.protocol }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-zinc-400">{{ l.input_tokens }}/{{ l.output_tokens }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-zinc-300">{{ fmtCost(l.cost) }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-zinc-500">{{ l.latency_ms }}ms</td>
<td class="px-4 py-2.5 font-mono text-xs text-ink">{{ l.model }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ l.protocol }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.input_tokens }}/{{ l.output_tokens }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-ink">{{ fmtCost(l.cost) }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.latency_ms }}ms</td>
<td class="px-4 py-2.5"><Badge :variant="l.status === 'success' ? 'ok' : 'err'">{{ l.status }}</Badge></td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ fmtTime(l.created_at) }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(l.created_at) }}</td>
</tr>
<tr v-if="logs.length === 0">
<td colspan="7" class="px-4 py-10 text-center text-sm text-zinc-600">暂无请求记录</td>
<td colspan="7" class="px-4 py-10 text-center text-sm text-muted">暂无请求记录</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center justify-between border-t border-zinc-800 px-4 py-3">
<span class="font-mono text-xs text-zinc-600">共 {{ total }} 条</span>
<div class="flex items-center justify-between border-t border-edge px-4 py-3">
<span class="font-mono text-xs text-muted">共 {{ total }} 条</span>
<div class="flex gap-2">
<Button size="sm" variant="ghost" :disabled="page <= 1" @click="goPage(page - 1)">上一页</Button>
<Button size="sm" variant="ghost" :disabled="page * pageSize >= total" @click="goPage(page + 1)">下一页</Button>