前端: 修复浅色模式白字 + 主题优化
- 修 nav 激活态残留 text-zinc-50 导致浅色下白字白底 - @theme 令牌改实色字面量, 浅色用 [data-theme=light] 覆盖同一批 --color-* - 新增 soft 令牌(accent/ok/warn/err, color-mix 运行时解析)替代烘焙的透明度修饰符, 修复 badge 实色底、focus ring 实线等问题 - ThemeToggle 改三选下拉(浅色/深色/跟随系统), 系统自动为显式选项 - 浅色强调色调深保证 WCAG 对比度, 全量复查无残留硬编码颜色类 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -38,10 +38,10 @@ async function logout() {
|
|||||||
|
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<aside
|
<aside
|
||||||
class="fixed inset-y-0 left-0 z-40 flex w-56 transform flex-col border-r border-edge/80 bg-bg transition-transform duration-200 md:translate-x-0"
|
class="fixed inset-y-0 left-0 z-40 flex w-56 transform flex-col border-r border-edge bg-bg transition-transform duration-200 md:translate-x-0"
|
||||||
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
|
||||||
>
|
>
|
||||||
<div class="flex h-14 items-center gap-2 border-b border-edge/80 px-4">
|
<div class="flex h-14 items-center gap-2 border-b border-edge px-4">
|
||||||
<img src="/favicon.svg" alt="" class="size-5" />
|
<img src="/favicon.svg" alt="" class="size-5" />
|
||||||
<span class="text-sm font-semibold tracking-tight">openteam</span>
|
<span class="text-sm font-semibold tracking-tight">openteam</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,8 +55,8 @@ async function logout() {
|
|||||||
v-for="n in sec.items"
|
v-for="n in sec.items"
|
||||||
:key="n.to"
|
:key="n.to"
|
||||||
:to="n.to"
|
:to="n.to"
|
||||||
class="mb-0.5 flex items-center rounded-md px-2 py-1.5 text-sm text-muted transition hover:bg-surface2/50 hover:text-ink"
|
class="mb-0.5 flex items-center rounded-md px-2 py-1.5 text-sm text-muted transition hover:bg-surface2 hover:text-ink"
|
||||||
active-class="bg-surface2/70 text-zinc-50"
|
active-class="bg-surface2 text-ink"
|
||||||
@click="navTo"
|
@click="navTo"
|
||||||
>
|
>
|
||||||
{{ n.label }}
|
{{ n.label }}
|
||||||
@@ -66,7 +66,7 @@ async function logout() {
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="flex min-h-[100dvh] flex-1 flex-col md:ml-56">
|
<div class="flex min-h-[100dvh] flex-1 flex-col md:ml-56">
|
||||||
<header class="flex h-14 items-center justify-between border-b border-edge/80 px-4 md:px-6">
|
<header class="flex h-14 items-center justify-between border-b border-edge px-4 md:px-6">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
class="rounded-md p-1.5 text-muted hover:bg-surface2 hover:text-ink md:hidden"
|
class="rounded-md p-1.5 text-muted hover:bg-surface2 hover:text-ink md:hidden"
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ withDefaults(defineProps<{ variant?: 'neutral' | 'ok' | 'warn' | 'err' | 'accent
|
|||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-mono text-[11px] leading-5"
|
class="inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 font-mono text-[11px] leading-5"
|
||||||
:class="{
|
:class="{
|
||||||
neutral: 'bg-surface2/80 text-ink',
|
neutral: 'bg-surface2 text-ink',
|
||||||
ok: 'bg-ok/15 text-accent',
|
ok: 'bg-ok-soft text-accent',
|
||||||
warn: 'bg-warn/15 text-warn',
|
warn: 'bg-warn-soft text-warn',
|
||||||
err: 'bg-err/15 text-err',
|
err: 'bg-err-soft text-err',
|
||||||
accent: 'bg-accent/15 text-accent',
|
accent: 'bg-accent-soft text-accent',
|
||||||
}[variant]"
|
}[variant]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ withDefaults(
|
|||||||
:class="[
|
:class="[
|
||||||
size === 'sm' ? 'h-8 px-3 text-xs' : 'h-10 px-4 text-sm',
|
size === 'sm' ? 'h-8 px-3 text-xs' : 'h-10 px-4 text-sm',
|
||||||
variant === 'primary' && 'bg-accent text-accent-ink hover:bg-accent-strong',
|
variant === 'primary' && 'bg-accent text-accent-ink hover:bg-accent-strong',
|
||||||
variant === 'ghost' && 'border border-edge2 text-ink hover:border-edge2 hover:bg-surface2/60',
|
variant === 'ghost' && 'border border-edge2 text-ink hover:border-edge2 hover:bg-surface2',
|
||||||
variant === 'danger' && 'border border-err/50 text-err hover:border-err hover:bg-err/10',
|
variant === 'danger' && 'border border-err text-err hover:border-err hover:bg-err-soft',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<span v-if="loading" class="size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
<span v-if="loading" class="size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ const emit = defineEmits<{ 'update:modelValue': [string | number] }>()
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink placeholder-muted outline-none transition focus:border-accent focus:ring-2 focus:ring-accent/30 disabled:cursor-not-allowed disabled:opacity-50"
|
class="h-10 w-full rounded-md border border-edge2 bg-surface px-3 text-sm text-ink placeholder-muted outline-none transition focus:border-accent focus:ring-2 focus:ring-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
:class="error && 'border-err focus:border-err focus:ring-err/30'"
|
:class="error && 'border-err focus:border-err focus:ring-err'"
|
||||||
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value as string | number)"
|
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value as string | number)"
|
||||||
/>
|
/>
|
||||||
<span v-if="hint && !error" class="mt-1.5 block text-xs text-muted">{{ hint }}</span>
|
<span v-if="hint && !error" class="mt-1.5 block text-xs text-muted">{{ hint }}</span>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ withDefaults(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="animate-pulse bg-surface2/60"
|
class="animate-pulse bg-surface2"
|
||||||
:class="[rounded, cls]"
|
:class="[rounded, cls]"
|
||||||
:style="{ height, width }"
|
:style="{ height, width }"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,32 +1,79 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { PhSun, PhMoon, PhMonitor } from '@phosphor-icons/vue'
|
import { PhSun, PhMoon, PhMonitor, PhCheck } from '@phosphor-icons/vue'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore, type ThemeMode } from '@/stores/theme'
|
||||||
|
|
||||||
const theme = useThemeStore()
|
const theme = useThemeStore()
|
||||||
const icon = computed(() => {
|
const open = ref(false)
|
||||||
switch (theme.resolved) {
|
const menu = ref<HTMLElement | null>(null)
|
||||||
case 'light':
|
|
||||||
return PhSun
|
const icon = computed(() => (theme.mode === 'light' ? PhSun : theme.mode === 'dark' ? PhMoon : PhMonitor))
|
||||||
case 'dark':
|
const label = computed(() => ({ light: '浅色', dark: '深色', system: '跟随系统' })[theme.mode])
|
||||||
return PhMoon
|
|
||||||
default:
|
const options: { mode: ThemeMode; label: string; icon: unknown }[] = [
|
||||||
return PhMonitor
|
{ mode: 'light', label: '浅色', icon: PhSun },
|
||||||
}
|
{ mode: 'dark', label: '深色', icon: PhMoon },
|
||||||
|
{ mode: 'system', label: '跟随系统', icon: PhMonitor },
|
||||||
|
]
|
||||||
|
|
||||||
|
function onDocClick(e: MouseEvent) {
|
||||||
|
if (menu.value && !menu.value.contains(e.target as Node)) open.value = false
|
||||||
|
}
|
||||||
|
function onKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') open.value = false
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('mousedown', onDocClick)
|
||||||
|
document.addEventListener('keydown', onKey)
|
||||||
})
|
})
|
||||||
const label = computed(() => {
|
onUnmounted(() => {
|
||||||
const names = { light: '浅色', dark: '深色', system: '跟随系统' }
|
document.removeEventListener('mousedown', onDocClick)
|
||||||
return names[theme.mode]
|
document.removeEventListener('keydown', onKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function pick(mode: ThemeMode) {
|
||||||
|
theme.set(mode)
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div ref="menu" class="relative">
|
||||||
<button
|
<button
|
||||||
class="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-muted transition hover:bg-surface2 hover:text-ink"
|
class="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-muted transition hover:bg-surface2 hover:text-ink"
|
||||||
:title="`主题:${label},点击切换`"
|
:title="`主题:${label}(点击选择)`"
|
||||||
:aria-label="`主题:${label},点击切换`"
|
:aria-label="`主题:${label}`"
|
||||||
@click="theme.cycle()"
|
aria-haspopup="true"
|
||||||
|
:aria-expanded="open"
|
||||||
|
@click="open = !open"
|
||||||
>
|
>
|
||||||
<component :is="icon" :size="15" weight="bold" />
|
<component :is="icon" :size="15" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition duration-100"
|
||||||
|
enter-from-class="scale-95 opacity-0"
|
||||||
|
leave-active-class="transition duration-100"
|
||||||
|
leave-to-class="scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="card absolute right-0 top-full z-50 mt-1 w-36 origin-top-right p-1 shadow-lg"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="opt in options"
|
||||||
|
:key="opt.mode"
|
||||||
|
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition hover:bg-surface2"
|
||||||
|
:class="theme.mode === opt.mode ? 'text-ink' : 'text-muted'"
|
||||||
|
role="menuitemradio"
|
||||||
|
:aria-checked="theme.mode === opt.mode"
|
||||||
|
@click="pick(opt.mode)"
|
||||||
|
>
|
||||||
|
<component :is="opt.icon" :size="14" />
|
||||||
|
<span class="flex-1 text-left">{{ opt.label }}</span>
|
||||||
|
<PhCheck v-if="theme.mode === opt.mode" :size="13" class="text-accent" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const toast = useToastStore()
|
|||||||
v-for="t in toast.items"
|
v-for="t in toast.items"
|
||||||
:key="t.id"
|
:key="t.id"
|
||||||
class="card flex items-start gap-2.5 px-4 py-3 shadow-lg"
|
class="card flex items-start gap-2.5 px-4 py-3 shadow-lg"
|
||||||
:class="t.type === 'err' && 'border-err/40'"
|
:class="t.type === 'err' && 'border-err'"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="mt-0.5 size-1.5 shrink-0 rounded-full"
|
class="mt-0.5 size-1.5 shrink-0 rounded-full"
|
||||||
|
|||||||
+44
-50
@@ -4,77 +4,71 @@
|
|||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* 设计 tokens(taste-skill 产出) */
|
/* 设计 tokens(taste-skill 产出) */
|
||||||
/* 三主题:dark / light / system(data-theme 切换,CSS 变量语义令牌) */
|
/* 三主题 dark / light / system(data-theme 切换) */
|
||||||
/* 单一强调色 emerald · 圆角:卡片 8 / 控件 6 / 徽章 pill */
|
/* 令牌值在 @theme 内写实色字面量 → 透明度修饰符(如 bg-accent-soft)可用; */
|
||||||
|
/* [data-theme=light] 覆盖同一批 --color-* 变量实现主题切换。 */
|
||||||
|
/* 圆角:卡片 8 / 控件 6 / 徽章 pill */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
--font-sans: 'Geist Variable', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||||
--font-mono: 'Geist Mono Variable', ui-monospace, SFMono-Regular, Menlo, monospace;
|
--font-mono: 'Geist Mono Variable', ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
|
||||||
/* 语义令牌 → CSS 变量(随 data-theme 切换) */
|
/* 深色(默认)语义令牌 */
|
||||||
--color-bg: var(--bg);
|
--color-bg: #09090b;
|
||||||
--color-surface: var(--surface);
|
--color-surface: #18181b;
|
||||||
--color-surface2: var(--surface2);
|
--color-surface2: #27272a;
|
||||||
--color-edge: var(--edge);
|
--color-edge: #27272a;
|
||||||
--color-edge2: var(--edge2);
|
--color-edge2: #3f3f46;
|
||||||
--color-ink: var(--ink);
|
--color-ink: #f4f4f5;
|
||||||
--color-muted: var(--muted);
|
--color-muted: #a1a1aa;
|
||||||
--color-accent: var(--accent);
|
--color-accent: oklch(0.72 0.17 152);
|
||||||
--color-accent-strong: var(--accent-strong);
|
--color-accent-strong: oklch(0.64 0.19 152);
|
||||||
--color-accent-ink: var(--accent-ink);
|
--color-accent-ink: #09090b;
|
||||||
--color-ok: var(--ok);
|
--color-ok: oklch(0.72 0.17 152);
|
||||||
--color-warn: var(--warn);
|
--color-warn: #fbbf24;
|
||||||
--color-err: var(--err);
|
--color-err: #f87171;
|
||||||
|
|
||||||
|
/* 软色(color-mix 运行时解析,随主题自适应) */
|
||||||
|
--color-accent-soft: color-mix(in oklab, var(--color-accent) 15%, transparent);
|
||||||
|
--color-ok-soft: color-mix(in oklab, var(--color-ok) 15%, transparent);
|
||||||
|
--color-warn-soft: color-mix(in oklab, var(--color-warn) 15%, transparent);
|
||||||
|
--color-err-soft: color-mix(in oklab, var(--color-err) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 深色(默认) */
|
/* 默认深色 */
|
||||||
:root,
|
:root {
|
||||||
[data-theme='dark'] {
|
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #09090b;
|
|
||||||
--surface: #18181b;
|
|
||||||
--surface2: #27272a;
|
|
||||||
--edge: #27272a;
|
|
||||||
--edge2: #3f3f46;
|
|
||||||
--ink: #f4f4f5;
|
|
||||||
--muted: #a1a1aa;
|
|
||||||
--accent: oklch(0.72 0.17 152);
|
|
||||||
--accent-strong: oklch(0.64 0.19 152);
|
|
||||||
--accent-ink: #09090b;
|
|
||||||
--ok: oklch(0.72 0.17 152);
|
|
||||||
--warn: #fbbf24;
|
|
||||||
--err: #f87171;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 浅色 */
|
/* 浅色:覆盖同一批语义令牌 */
|
||||||
[data-theme='light'] {
|
[data-theme='light'] {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--bg: #fafafa;
|
--color-bg: #fafafa;
|
||||||
--surface: #ffffff;
|
--color-surface: #ffffff;
|
||||||
--surface2: #f4f4f5;
|
--color-surface2: #f4f4f5;
|
||||||
--edge: #e4e4e7;
|
--color-edge: #e4e4e7;
|
||||||
--edge2: #d4d4d8;
|
--color-edge2: #d4d4d8;
|
||||||
--ink: #18181b;
|
--color-ink: #18181b;
|
||||||
--muted: #52525b;
|
--color-muted: #52525b;
|
||||||
--accent: oklch(0.58 0.16 152);
|
--color-accent: oklch(0.52 0.14 152);
|
||||||
--accent-strong: oklch(0.52 0.16 152);
|
--color-accent-strong: oklch(0.47 0.14 152);
|
||||||
--accent-ink: #ffffff;
|
--color-accent-ink: #ffffff;
|
||||||
--ok: oklch(0.58 0.16 152);
|
--color-ok: oklch(0.52 0.14 152);
|
||||||
--warn: #d97706;
|
--color-warn: #d97706;
|
||||||
--err: #dc2626;
|
--color-err: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--bg);
|
background-color: var(--color-bg);
|
||||||
color: var(--ink);
|
color: var(--color-ink);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全站统一键盘焦点可见性 */
|
/* 全站统一键盘焦点可见性 */
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--color-accent);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -90,7 +84,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
@apply border-b border-edge last:border-0 hover:bg-surface2/40;
|
@apply border-b border-edge last:border-0 hover:bg-surface2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const auth = useAuthStore()
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-[100dvh] bg-bg text-ink">
|
<div class="min-h-[100dvh] bg-bg text-ink">
|
||||||
<!-- 导航 -->
|
<!-- 导航 -->
|
||||||
<header class="sticky top-0 z-40 border-b border-edge/60 bg-bg/80 backdrop-blur">
|
<header class="sticky top-0 z-40 border-b border-edge bg-bg backdrop-blur">
|
||||||
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
|
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<img src="/favicon.svg" alt="" class="size-5" />
|
<img src="/favicon.svg" alt="" class="size-5" />
|
||||||
@@ -28,7 +28,7 @@ const auth = useAuthStore()
|
|||||||
<router-link
|
<router-link
|
||||||
v-if="!auth.isAuthed"
|
v-if="!auth.isAuthed"
|
||||||
to="/register"
|
to="/register"
|
||||||
class="rounded-md bg-accent px-3.5 py-1.5 text-sm font-medium text-ink transition hover:bg-accent-strong"
|
class="rounded-md bg-accent px-3.5 py-1.5 text-sm font-medium text-accent-ink transition hover:bg-accent-strong"
|
||||||
>
|
>
|
||||||
免费注册
|
免费注册
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -51,7 +51,7 @@ const auth = useAuthStore()
|
|||||||
<div class="mt-8 flex items-center gap-3">
|
<div class="mt-8 flex items-center gap-3">
|
||||||
<router-link
|
<router-link
|
||||||
to="/register"
|
to="/register"
|
||||||
class="inline-flex h-10 items-center rounded-md bg-accent px-5 text-sm font-medium text-ink transition hover:bg-accent-strong active:scale-[0.98]"
|
class="inline-flex h-10 items-center rounded-md bg-accent px-5 text-sm font-medium text-accent-ink transition hover:bg-accent-strong active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
免费注册
|
免费注册
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -91,13 +91,13 @@ const auth = useAuthStore()
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 协议入口 -->
|
<!-- 协议入口 -->
|
||||||
<section class="border-t border-edge/60">
|
<section class="border-t border-edge">
|
||||||
<div class="mx-auto max-w-6xl px-4 py-16">
|
<div class="mx-auto max-w-6xl px-4 py-16">
|
||||||
<h2 class="text-2xl font-semibold tracking-tight">三套协议,一个入口</h2>
|
<h2 class="text-2xl font-semibold tracking-tight">三套协议,一个入口</h2>
|
||||||
<p class="mt-2 max-w-xl text-sm text-muted">
|
<p class="mt-2 max-w-xl text-sm text-muted">
|
||||||
对客户端暴露统一的 OpenAI 兼容入口;客户端协议与上游渠道不匹配时自动转换,无需关心背后接的是哪家。
|
对客户端暴露统一的 OpenAI 兼容入口;客户端协议与上游渠道不匹配时自动转换,无需关心背后接的是哪家。
|
||||||
</p>
|
</p>
|
||||||
<div class="card mt-8 divide-y divide-edge/70">
|
<div class="card mt-8 divide-y divide-edge">
|
||||||
<div v-for="p in [
|
<div v-for="p in [
|
||||||
{ path: 'POST /v1/chat/completions', desc: 'OpenAI Chat,兼容面最广,SDK 与工具链最全', tag: 'OpenAI' },
|
{ path: 'POST /v1/chat/completions', desc: 'OpenAI Chat,兼容面最广,SDK 与工具链最全', tag: 'OpenAI' },
|
||||||
{ path: 'POST /v1/responses', desc: 'OpenAI Responses,新一代 SDK 与 Agents 首选', tag: 'OpenAI' },
|
{ path: 'POST /v1/responses', desc: 'OpenAI Responses,新一代 SDK 与 Agents 首选', tag: 'OpenAI' },
|
||||||
@@ -113,7 +113,7 @@ const auth = useAuthStore()
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 网关能力 -->
|
<!-- 网关能力 -->
|
||||||
<section class="border-t border-edge/60">
|
<section class="border-t border-edge">
|
||||||
<div class="mx-auto max-w-6xl px-4 py-16">
|
<div class="mx-auto max-w-6xl px-4 py-16">
|
||||||
<h2 class="text-2xl font-semibold tracking-tight">网关替你处理的事</h2>
|
<h2 class="text-2xl font-semibold tracking-tight">网关替你处理的事</h2>
|
||||||
<div class="mt-8 grid gap-4 lg:grid-cols-3">
|
<div class="mt-8 grid gap-4 lg:grid-cols-3">
|
||||||
@@ -150,20 +150,20 @@ const auth = useAuthStore()
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
<section class="border-t border-edge/60">
|
<section class="border-t border-edge">
|
||||||
<div class="mx-auto max-w-6xl px-4 py-20 text-center">
|
<div class="mx-auto max-w-6xl px-4 py-20 text-center">
|
||||||
<h2 class="text-2xl font-semibold tracking-tight">自托管,密钥在自己手里</h2>
|
<h2 class="text-2xl font-semibold tracking-tight">自托管,密钥在自己手里</h2>
|
||||||
<p class="mx-auto mt-3 max-w-md text-sm text-muted">Docker Compose 一键部署,PostgreSQL 存账,渠道密钥加密存储。</p>
|
<p class="mx-auto mt-3 max-w-md text-sm text-muted">Docker Compose 一键部署,PostgreSQL 存账,渠道密钥加密存储。</p>
|
||||||
<router-link
|
<router-link
|
||||||
to="/register"
|
to="/register"
|
||||||
class="mt-8 inline-flex h-10 items-center rounded-md bg-accent px-6 text-sm font-medium text-ink transition hover:bg-accent-strong active:scale-[0.98]"
|
class="mt-8 inline-flex h-10 items-center rounded-md bg-accent px-6 text-sm font-medium text-accent-ink transition hover:bg-accent-strong active:scale-[0.98]"
|
||||||
>
|
>
|
||||||
开始使用
|
开始使用
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer class="border-t border-edge/60">
|
<footer class="border-t border-edge">
|
||||||
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-6 text-xs text-muted">
|
<div class="mx-auto flex max-w-6xl items-center justify-between px-4 py-6 text-xs text-muted">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<img src="/favicon.svg" alt="" class="size-4" />
|
<img src="/favicon.svg" alt="" class="size-4" />
|
||||||
|
|||||||
@@ -53,14 +53,14 @@ onMounted(load)
|
|||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
|
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
|
||||||
:class="config.registration_mode === 'open' ? 'border-accent bg-accent/10 text-accent' : 'border-edge2 text-muted'"
|
:class="config.registration_mode === 'open' ? 'border-accent bg-accent-soft text-accent' : 'border-edge2 text-muted'"
|
||||||
@click="config.registration_mode = 'open'"
|
@click="config.registration_mode = 'open'"
|
||||||
>
|
>
|
||||||
开放注册
|
开放注册
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
|
class="flex-1 rounded-md border px-3 py-2 text-sm transition"
|
||||||
:class="config.registration_mode === 'invite' ? 'border-accent bg-accent/10 text-accent' : 'border-edge2 text-muted'"
|
:class="config.registration_mode === 'invite' ? 'border-accent bg-accent-soft text-accent' : 'border-edge2 text-muted'"
|
||||||
@click="config.registration_mode = 'invite'"
|
@click="config.registration_mode = 'invite'"
|
||||||
>
|
>
|
||||||
邀请码
|
邀请码
|
||||||
@@ -78,7 +78,7 @@ onMounted(load)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between rounded-md border border-edge bg-surface/60 px-4 py-3">
|
<div class="flex items-center justify-between rounded-md border border-edge bg-surface px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm text-ink">其他配置项</p>
|
<p class="text-sm text-ink">其他配置项</p>
|
||||||
<p class="text-xs text-muted">汇率、限流阈值、维护开关在后续里程碑开放</p>
|
<p class="text-xs text-muted">汇率、限流阈值、维护开关在后续里程碑开放</p>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="m.channels.length" class="border-t border-edge/70 px-4 py-2">
|
<div v-if="m.channels.length" class="border-t border-edge px-4 py-2">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span
|
<span
|
||||||
v-for="b in m.channels"
|
v-for="b in m.channels"
|
||||||
@@ -166,7 +166,7 @@ onMounted(load)
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="border-t border-edge/70 px-4 py-2 text-xs text-muted">
|
<p v-else class="border-t border-edge px-4 py-2 text-xs text-muted">
|
||||||
未绑定渠道,客户端无法调用该模型
|
未绑定渠道,客户端无法调用该模型
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ onMounted(load)
|
|||||||
<h2 class="text-sm font-semibold">全局最近请求</h2>
|
<h2 class="text-sm font-semibold">全局最近请求</h2>
|
||||||
<span class="font-mono text-[11px] text-muted">共 {{ data.month.requests }} / 月</span>
|
<span class="font-mono text-[11px] text-muted">共 {{ data.month.requests }} / 月</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="divide-y divide-edge/70">
|
<ul class="divide-y divide-edge">
|
||||||
<li v-for="l in logs" :key="l.id" class="flex items-center justify-between py-2">
|
<li v-for="l in logs" :key="l.id" class="flex items-center justify-between py-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-xs text-ink">
|
<p class="truncate text-xs text-ink">
|
||||||
|
|||||||
@@ -95,13 +95,13 @@ onMounted(load)
|
|||||||
<h2 class="text-sm font-semibold">最近请求</h2>
|
<h2 class="text-sm font-semibold">最近请求</h2>
|
||||||
<router-link to="/console/usage" class="text-xs text-accent hover:text-accent-strong">查看全部</router-link>
|
<router-link to="/console/usage" class="text-xs text-accent hover:text-accent-strong">查看全部</router-link>
|
||||||
</div>
|
</div>
|
||||||
<ul v-if="loading" class="divide-y divide-edge/70">
|
<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">
|
<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="40%" />
|
||||||
<Skeleton height="0.75rem" width="30%" />
|
<Skeleton height="0.75rem" width="30%" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul v-else class="divide-y divide-edge/70">
|
<ul v-else class="divide-y divide-edge">
|
||||||
<li v-for="l in logs" :key="l.id" class="flex items-center justify-between py-2">
|
<li v-for="l in logs" :key="l.id" class="flex items-center justify-between py-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate font-mono text-xs text-ink">{{ l.model }}</p>
|
<p class="truncate font-mono text-xs text-ink">{{ l.model }}</p>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ onMounted(load)
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<p class="text-xs text-muted">请复制并妥善保存,关闭后不再显示。</p>
|
<p class="text-xs text-muted">请复制并妥善保存,关闭后不再显示。</p>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<code class="mono-num flex-1 truncate rounded-md border border-accent/40 bg-surface px-3 py-2 text-xs text-accent">
|
<code class="mono-num flex-1 truncate rounded-md border border-accent bg-surface px-3 py-2 text-xs text-accent">
|
||||||
{{ created?.key }}
|
{{ created?.key }}
|
||||||
</code>
|
</code>
|
||||||
<Button size="sm" @click="copyKey">
|
<Button size="sm" @click="copyKey">
|
||||||
|
|||||||
Reference in New Issue
Block a user