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

- 引入语义化 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
+14 -14
View File
@@ -131,7 +131,7 @@ onMounted(load)
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-lg font-semibold">渠道</h1>
<p class="text-sm text-zinc-500">接入上游服务,API Key 加密存储</p>
<p class="text-sm text-muted">接入上游服务,API Key 加密存储</p>
</div>
<Button @click="openCreate">添加渠道</Button>
</div>
@@ -140,7 +140,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">Base URL</th>
@@ -152,29 +152,29 @@ onMounted(load)
</thead>
<tbody>
<tr v-for="ch in channels" :key="ch.id" class="table-row">
<td class="px-4 py-2.5 text-zinc-200">{{ ch.name }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-400">{{ providerMap[ch.provider] }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ ch.base_url }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-600">{{ ch.api_key_masked || '****' }}</td>
<td class="px-4 py-2.5 text-ink">{{ ch.name }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ providerMap[ch.provider] }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.base_url }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ ch.api_key_masked || '****' }}</td>
<td class="px-4 py-2.5">
<Badge :variant="ch.health_status === 'healthy' ? 'ok' : ch.health_status === 'cooldown' ? 'err' : 'warn'">
{{ ch.health_status }}
</Badge>
</td>
<td class="px-4 py-2.5 text-xs text-zinc-400">{{ ch.enabled ? '是' : '否' }}</td>
<td class="px-4 py-2.5 text-xs text-muted">{{ ch.enabled ? '是' : '否' }}</td>
<td class="px-4 py-2.5 text-right">
<div class="flex justify-end gap-2">
<button class="text-xs text-zinc-500 hover:text-accent" :disabled="busyId === ch.id" @click="testChannel(ch)">
<button class="text-xs text-muted hover:text-accent" :disabled="busyId === ch.id" @click="testChannel(ch)">
{{ busyId === ch.id ? '测试中…' : '测试' }}
</button>
<button class="text-xs text-zinc-500 hover:text-accent" @click="importModels(ch)">导入模型</button>
<button class="text-xs text-zinc-500 hover:text-zinc-200" @click="openEdit(ch)">编辑</button>
<button class="text-xs text-zinc-500 hover:text-red-400" @click="remove(ch)">删除</button>
<button class="text-xs text-muted hover:text-accent" @click="importModels(ch)">导入模型</button>
<button class="text-xs text-muted hover:text-ink" @click="openEdit(ch)">编辑</button>
<button class="text-xs text-muted hover:text-err" @click="remove(ch)">删除</button>
</div>
</td>
</tr>
<tr v-if="channels.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>
@@ -186,8 +186,8 @@ onMounted(load)
<div class="grid grid-cols-2 gap-4">
<Input v-model="form.name" label="名称" placeholder="openai" />
<label class="block">
<span class="mb-1.5 block text-xs font-medium text-zinc-400">API 类型</span>
<select v-model="form.provider" class="h-10 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 text-sm text-zinc-100 outline-none focus:border-accent">
<span class="mb-1.5 block text-xs font-medium text-muted">API 类型</span>
<select v-model="form.provider" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="compatible">兼容</option>
+12 -12
View File
@@ -41,49 +41,49 @@ onMounted(load)
<div class="mx-auto max-w-2xl">
<div class="mb-6">
<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="card space-y-5 p-6">
<div v-if="loading" class="py-8 text-center text-sm text-zinc-600">加载中…</div>
<div v-if="loading" class="py-8 text-center text-sm text-muted">加载中…</div>
<template v-else>
<div class="space-y-4">
<div>
<p class="mb-1.5 text-xs font-medium text-zinc-400">注册模式</p>
<p class="mb-1.5 text-xs font-medium text-muted">注册模式</p>
<div class="flex gap-2">
<button
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
:class="config.registration_mode === 'open' ? 'border-accent bg-accent/10 text-emerald-300' : 'border-zinc-700 text-zinc-400'"
:class="config.registration_mode === 'open' ? 'border-accent bg-accent/10 text-accent' : 'border-edge2 text-muted'"
@click="config.registration_mode = 'open'"
>
开放注册
</button>
<button
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
:class="config.registration_mode === 'invite' ? 'border-accent bg-accent/10 text-emerald-300' : 'border-zinc-700 text-zinc-400'"
:class="config.registration_mode === 'invite' ? 'border-accent bg-accent/10 text-accent' : 'border-edge2 text-muted'"
@click="config.registration_mode = 'invite'"
>
邀请码
</button>
</div>
<p class="mt-1.5 text-xs text-zinc-600">邀请模式下注册需填写有效邀请码(invite_codes 配置)</p>
<p class="mt-1.5 text-xs text-muted">邀请模式下注册需填写有效邀请码(invite_codes 配置)</p>
</div>
<div>
<p class="mb-1.5 text-xs font-medium text-zinc-400">邀请码(逗号分隔)</p>
<p class="mb-1.5 text-xs font-medium text-muted">邀请码(逗号分隔)</p>
<input
v-model="config.invite_codes"
placeholder="code1,code2"
class="h-10 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 font-mono text-xs outline-none focus:border-accent"
class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 font-mono text-xs outline-none focus:border-accent"
/>
</div>
<div class="flex items-center justify-between rounded-md border border-zinc-800 bg-zinc-900/60 px-4 py-3">
<div class="flex items-center justify-between rounded-md border border-edge bg-surface/60 px-4 py-3">
<div>
<p class="text-sm text-zinc-300">其他配置项</p>
<p class="text-xs text-zinc-600">汇率、限流阈值、维护开关在后续里程碑开放</p>
<p class="text-sm text-ink">其他配置项</p>
<p class="text-xs text-muted">汇率、限流阈值、维护开关在后续里程碑开放</p>
</div>
<span class="font-mono text-xs text-zinc-600">M3+</span>
<span class="font-mono text-xs text-muted">M3+</span>
</div>
</div>
+16 -16
View File
@@ -130,7 +130,7 @@ onMounted(load)
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-lg font-semibold">模型与定价</h1>
<p class="text-sm text-zinc-500">价格按每百万 token (USD),历史用量按当时价格入账</p>
<p class="text-sm text-muted">价格按每百万 token (USD),历史用量按当时价格入账</p>
</div>
<Button @click="openCreate">添加模型</Button>
</div>
@@ -139,39 +139,39 @@ onMounted(load)
<div v-for="m in models" :key="m.id" class="card">
<div class="flex items-center justify-between px-4 py-3">
<div class="flex items-center gap-3">
<span class="font-mono text-sm text-zinc-100">{{ m.name }}</span>
<span class="font-mono text-sm text-ink">{{ m.name }}</span>
<Badge :variant="m.enabled ? 'ok' : 'neutral'">{{ m.enabled ? '启用' : '停用' }}</Badge>
</div>
<div class="flex items-center gap-3">
<span class="mono-num text-xs text-zinc-400">入 {{ m.input_price }}</span>
<span class="mono-num text-xs text-zinc-400">出 {{ m.output_price }}</span>
<span class="mono-num text-xs text-zinc-500">缓存读 {{ m.cache_read_price }}</span>
<span class="mono-num text-xs text-muted">入 {{ m.input_price }}</span>
<span class="mono-num text-xs text-muted">出 {{ m.output_price }}</span>
<span class="mono-num text-xs text-muted">缓存读 {{ m.cache_read_price }}</span>
</div>
<div class="flex gap-2">
<button class="text-xs text-zinc-500 hover:text-accent" @click="openBind(m)">绑定渠道</button>
<button class="text-xs text-zinc-500 hover:text-zinc-200" @click="openEdit(m)">编辑</button>
<button class="text-xs text-zinc-500 hover:text-red-400" @click="removeModel(m)">删除</button>
<button class="text-xs text-muted hover:text-accent" @click="openBind(m)">绑定渠道</button>
<button class="text-xs text-muted hover:text-ink" @click="openEdit(m)">编辑</button>
<button class="text-xs text-muted hover:text-err" @click="removeModel(m)">删除</button>
</div>
</div>
<div v-if="m.channels.length" class="border-t border-zinc-800/70 px-4 py-2">
<div v-if="m.channels.length" class="border-t border-edge/70 px-4 py-2">
<div class="flex flex-wrap gap-2">
<span
v-for="b in m.channels"
:key="b.id"
class="inline-flex items-center gap-1.5 rounded-md border border-zinc-800 bg-zinc-900 px-2 py-0.5 font-mono text-[11px] text-zinc-400"
class="inline-flex items-center gap-1.5 rounded-md border border-edge bg-surface px-2 py-0.5 font-mono text-[11px] text-muted"
>
{{ b.channel_name }} → {{ b.upstream_model }}
<button class="text-zinc-400 hover:text-red-400" aria-label="移除绑定" @click="removeBinding(m, b.id)">×</button>
<button class="text-muted hover:text-err" aria-label="移除绑定" @click="removeBinding(m, b.id)">×</button>
</span>
</div>
</div>
<p v-else class="border-t border-zinc-800/70 px-4 py-2 text-xs text-zinc-600">
<p v-else class="border-t border-edge/70 px-4 py-2 text-xs text-muted">
未绑定渠道,客户端无法调用该模型
</p>
</div>
<p v-if="models.length === 0" class="card px-4 py-10 text-center text-sm text-zinc-600">
<p v-if="models.length === 0" class="card px-4 py-10 text-center text-sm text-muted">
还没有模型,点击「添加模型」或到渠道页「导入模型」
</p>
</div>
@@ -196,10 +196,10 @@ onMounted(load)
<!-- 绑定渠道 -->
<Modal :open="bindOpen" title="绑定渠道" @close="bindOpen = false">
<div class="space-y-4">
<p class="text-xs text-zinc-500">模型 <span class="font-mono text-emerald-300">{{ bindModel?.name }}</span> 通过以下渠道提供</p>
<p class="text-xs text-muted">模型 <span class="font-mono text-accent">{{ bindModel?.name }}</span> 通过以下渠道提供</p>
<label class="block">
<span class="mb-1.5 block text-xs font-medium text-zinc-400">渠道</span>
<select v-model="binding.channel_id" class="h-10 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 text-sm text-zinc-100 outline-none focus:border-accent">
<span class="mb-1.5 block text-xs font-medium text-muted">渠道</span>
<select v-model="binding.channel_id" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink outline-none focus:border-accent">
<option v-for="ch in channels" :key="ch.id" :value="ch.id">{{ ch.name }}</option>
</select>
</label>
+17 -17
View File
@@ -37,29 +37,29 @@ onMounted(load)
<div class="mx-auto max-w-6xl 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-zinc-100">{{ fmtNum(data.total_users) }} / {{ fmtNum(data.total_keys) }}</p>
<p v-else class="mono-num mt-1 text-xl text-ink">{{ fmtNum(data.total_users) }} / {{ fmtNum(data.total_keys) }}</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="45%" />
<p v-else class="mono-num mt-1 text-xl text-zinc-100">{{ fmtNum(data.total_channels) }} / {{ fmtNum(data.total_models) }}</p>
<p v-else class="mono-num mt-1 text-xl text-ink">{{ fmtNum(data.total_channels) }} / {{ fmtNum(data.total_models) }}</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(data.today.requests) }}</p>
<p v-else class="mono-num mt-1 text-xl text-ink">{{ fmtNum(data.today.requests) }}</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="45%" />
<p v-else class="mono-num mt-1 text-xl text-emerald-300">{{ fmtCost(data.month.cost) }}</p>
<p v-else class="mono-num mt-1 text-xl text-accent">{{ fmtCost(data.month.cost) }}</p>
</div>
</div>
@@ -67,7 +67,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">{{ fmtCost(data.month.cost) }} / 本月</span>
<span class="font-mono text-[11px] text-muted">{{ fmtCost(data.month.cost) }} / 本月</span>
</div>
<Skeleton v-if="loading" height="160px" />
<TrendChart
@@ -80,22 +80,22 @@ onMounted(load)
<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>
<span class="font-mono text-[11px] text-zinc-600">共 {{ data.month.requests }} / 月</span>
<span class="font-mono text-[11px] text-muted">共 {{ data.month.requests }} / 月</span>
</div>
<ul class="divide-y divide-zinc-800/70">
<ul 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 text-xs text-zinc-300">
<span class="font-mono text-emerald-400/80">{{ l.user }}</span> · {{ l.model }}
<p class="truncate text-xs text-ink">
<span class="font-mono text-accent">{{ l.user }}</span> · {{ l.model }}
</p>
<p class="font-mono text-[11px] text-zinc-600">{{ fmtTime(l.created_at) }}</p>
<p class="font-mono 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-500">{{ fmtCost(l.cost) }}</span>
<span class="mono-num 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>
<li v-if="logs.length === 0" class="py-6 text-center text-xs text-muted">暂无请求</li>
</ul>
</div>
</div>
+19 -19
View File
@@ -93,13 +93,13 @@ onMounted(load)
<div class="mb-6 flex items-center justify-between">
<div>
<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="flex gap-2">
<input
v-model="q"
placeholder="搜索用户名 / 邮箱"
class="h-10 w-56 rounded-md border border-zinc-700 bg-zinc-900 px-3 text-sm outline-none focus:border-accent"
class="h-10 w-56 rounded-md border border-edge2 bg-surface px-3 text-sm outline-none focus:border-accent"
@keyup.enter="search"
/>
<Button variant="ghost" @click="search">搜索</Button>
@@ -110,7 +110,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">ID</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>
@@ -123,35 +123,35 @@ onMounted(load)
</thead>
<tbody>
<tr v-for="u in users" :key="u.id" class="table-row">
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ u.id }}</td>
<td class="px-4 py-2.5 text-zinc-200">
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ u.id }}</td>
<td class="px-4 py-2.5 text-ink">
{{ u.username }}
<span v-if="u.id === auth.user?.id" class="text-[11px] text-zinc-600">(我)</span>
<span v-if="u.id === auth.user?.id" class="text-[11px] text-muted">(我)</span>
</td>
<td class="px-4 py-2.5 text-xs text-zinc-400">{{ u.email }}</td>
<td class="px-4 py-2.5 text-xs text-muted">{{ u.email }}</td>
<td class="px-4 py-2.5">
<Badge :variant="u.role === 'admin' ? 'accent' : 'neutral'">{{ u.role }}</Badge>
</td>
<td class="px-4 py-2.5 mono-num text-xs text-emerald-300/90">{{ fmtMoney(u.balance) }}</td>
<td class="px-4 py-2.5 mono-num text-xs text-accent">{{ fmtMoney(u.balance) }}</td>
<td class="px-4 py-2.5">
<Badge :variant="u.status === 'active' ? 'ok' : 'warn'">{{ u.status }}</Badge>
</td>
<td class="px-4 py-2.5 font-mono text-xs text-zinc-500">{{ fmtTime(u.created_at) }}</td>
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(u.created_at) }}</td>
<td class="px-4 py-2.5 text-right">
<div class="flex justify-end gap-2">
<button class="text-xs text-zinc-500 hover:text-zinc-200" @click="openEdit(u)">编辑</button>
<button class="text-xs text-zinc-500 hover:text-accent" @click="openBalance(u)">调余额</button>
<button class="text-xs text-muted hover:text-ink" @click="openEdit(u)">编辑</button>
<button class="text-xs text-muted hover:text-accent" @click="openBalance(u)">调余额</button>
</div>
</td>
</tr>
<tr v-if="users.length === 0">
<td colspan="8" class="px-4 py-10 text-center text-sm text-zinc-600">无用户</td>
<td colspan="8" 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>
@@ -163,15 +163,15 @@ onMounted(load)
<Modal :open="editOpen" title="编辑用户" @close="editOpen = false">
<div class="space-y-4">
<label class="block">
<span class="mb-1.5 block text-xs font-medium text-zinc-400">角色</span>
<select v-model="editForm.role" class="h-10 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 text-sm outline-none focus:border-accent">
<span class="mb-1.5 block text-xs font-medium text-muted">角色</span>
<select v-model="editForm.role" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm outline-none focus:border-accent">
<option value="user">user</option>
<option value="admin">admin</option>
</select>
</label>
<label class="block">
<span class="mb-1.5 block text-xs font-medium text-zinc-400">状态</span>
<select v-model="editForm.status" class="h-10 w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 text-sm outline-none focus:border-accent">
<span class="mb-1.5 block text-xs font-medium text-muted">状态</span>
<select v-model="editForm.status" class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm outline-none focus:border-accent">
<option value="active">active</option>
<option value="disabled">disabled</option>
</select>
@@ -186,7 +186,7 @@ onMounted(load)
<!-- 调整余额 -->
<Modal :open="balanceOpen" :title="`调整余额 · ${balanceUser?.username}`" @close="balanceOpen = false">
<div class="space-y-4">
<p class="text-xs text-zinc-500">当前余额 {{ fmtMoney(balanceUser?.balance ?? 0) }}</p>
<p class="text-xs text-muted">当前余额 {{ fmtMoney(balanceUser?.balance ?? 0) }}</p>
<Input v-model="balanceForm.amount" label="调整金额" type="number" hint="正数增加,负数扣减" />
<Input v-model="balanceForm.remark" label="备注" placeholder="可选" />
</div>