| <template> |
| <div class="min-h-screen"> |
| <div class="flex min-h-screen flex-col lg:flex-row"> |
| <div |
| v-if="isSidebarOpen" |
| class="fixed inset-0 z-30 bg-black/20 backdrop-blur-sm lg:hidden" |
| @click="isSidebarOpen = false" |
| ></div> |
| <aside |
| class="fixed inset-y-0 left-0 z-40 w-72 -translate-x-full bg-card/90 backdrop-blur-sm lg:backdrop-blur-none border-r border-border |
| transition-[width,transform] duration-200 ease-out will-change-[transform] transform-gpu flex flex-col lg:static lg:translate-x-0 lg:bg-card/80 |
| lg:border-b-0 lg:border-r lg:sticky lg:top-0 lg:h-screen" |
| :class="[ |
| { 'translate-x-0': isSidebarOpen, 'w-20 lg:w-20': isSidebarCollapsed }, |
| ]" |
| > |
| <div |
| class="flex h-16 items-center justify-between px-6 pt-4 lg:h-20 lg:pt-5" |
| :class="isSidebarCollapsed ? 'justify-center px-0' : ''" |
| > |
| <div class="flex items-center gap-2" :class="isSidebarCollapsed ? 'gap-0 justify-center w-full' : ''"> |
| <a |
| href="https://github.com/Dreamy-rain/gemini-business2api" |
| target="_blank" |
| rel="noopener noreferrer" |
| class="text-foreground transition-colors hover:text-primary" |
| aria-label="GitHub" |
| > |
| <svg |
| aria-hidden="true" |
| viewBox="0 0 24 24" |
| class="h-6 w-6" |
| fill="currentColor" |
| > |
| <path d="M12 2C6.477 2 2 6.477 2 12c0 4.419 2.865 8.166 6.839 9.489.5.09.682-.217.682-.483 0-.237-.009-.868-.014-1.703-2.782.604-3.369-1.341-3.369-1.341-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.004.071 1.532 1.031 1.532 1.031.892 1.529 2.341 1.087 2.91.832.091-.647.349-1.087.636-1.337-2.22-.253-4.555-1.11-4.555-4.944 0-1.092.39-1.987 1.029-2.687-.103-.253-.446-1.272.098-2.65 0 0 .84-.269 2.75 1.026A9.564 9.564 0 0 1 12 6.844c.85.004 1.705.115 2.504.337 1.909-1.295 2.748-1.026 2.748-1.026.546 1.378.202 2.397.1 2.65.64.7 1.028 1.595 1.028 2.687 0 3.842-2.338 4.687-4.566 4.936.359.309.678.919.678 1.852 0 1.337-.012 2.418-.012 2.747 0 .268.18.577.688.479A10.002 10.002 0 0 0 22 12c0-5.523-4.477-10-10-10z" /> |
| </svg> |
| </a> |
| <span v-if="!isSidebarCollapsed" class="text-base font-semibold text-foreground">Gemini Business2API</span> |
| </div> |
| </div> |
|
|
| <nav |
| class="pb-4 pt-4 lg:pt-6 flex-1 overflow-y-auto" |
| :class="isSidebarCollapsed ? 'px-2' : 'px-3'" |
| > |
| <p |
| v-if="!isSidebarCollapsed" |
| class="px-3 pb-2 text-xs uppercase tracking-[0.28em] text-muted-foreground" |
| > |
| 导航 |
| </p> |
| <div class="space-y-1"> |
| <RouterLink |
| v-for="item in menuItems" |
| :key="item.path" |
| :to="item.path" |
| class="group flex items-center rounded-2xl py-2 text-sm font-medium transition-colors overflow-hidden" |
| :class="navItemClass(item.path)" |
| :title="isSidebarCollapsed ? item.label : undefined" |
| > |
| <span |
| class="inline-flex h-9 w-9 items-center justify-center rounded-2xl border border-border" |
| :class="navIconClass(item.path)" |
| > |
| <svg aria-hidden="true" viewBox="0 0 24 24" class="h-5 w-5" fill="currentColor"> |
| <path :d="item.icon" /> |
| </svg> |
| </span> |
| <span v-if="!isSidebarCollapsed" class="flex-1 min-w-0 truncate">{{ item.label }}</span> |
| <span v-if="!isSidebarCollapsed" class="ml-auto text-xs opacity-0 transition-opacity group-hover:opacity-100"> |
| 进入 |
| </span> |
| </RouterLink> |
| </div> |
| </nav> |
|
|
| <div class="mt-auto border-t border-border px-6 py-3 lg:py-4"> |
| <div v-if="!isSidebarCollapsed" class="rounded-2xl bg-secondary/60 p-3"> |
| <p class="text-xs tracking-[0.12em] text-muted-foreground"> |
| <a |
| href="https://github.com/Dreamy-rain/gemini-business2api" |
| target="_blank" |
| rel="noopener noreferrer" |
| class="inline-flex items-center gap-1 transition-colors hover:text-foreground" |
| > |
| gemini-business2api |
| </a> |
| <span> · 声明</span> |
| </p> |
| <p class="mt-2 text-xs text-muted-foreground"> |
| 本项目仅限学习与研究用途,禁止用于商业用途。请保留本声明、原作者信息与开源来源。 |
| </p> |
| </div> |
| <div |
| class="mt-4 flex items-center gap-3" |
| :class="isSidebarCollapsed ? 'justify-center' : ''" |
| > |
| <button |
| v-if="!isSidebarCollapsed" |
| @click="handleLogout" |
| class="flex-1 rounded-2xl border border-border bg-background px-4 py-3 text-sm font-medium |
| text-muted-foreground transition-colors hover:border-destructive/40 hover:text-destructive" |
| > |
| 退出登录 |
| </button> |
| <button |
| class="h-10 w-10 shrink-0 items-center justify-center rounded-2xl border border-border text-muted-foreground transition-all |
| hover:border-primary hover:text-primary flex" |
| @click="isSidebarCollapsed = !isSidebarCollapsed" |
| :title="isSidebarCollapsed ? '展开侧边栏' : '收起侧边栏'" |
| > |
| <svg |
| aria-hidden="true" |
| viewBox="0 0 24 24" |
| class="h-4 w-4 shrink-0" |
| fill="currentColor" |
| > |
| <path d="M6 4h2v16H6V4zm4 4h8v2h-8V8zm0 6h8v2h-8v-2z" /> |
| </svg> |
| </button> |
| </div> |
| </div> |
| </aside> |
|
|
| <main class="min-w-0 flex-1 overflow-hidden lg:ml-0"> |
| <header class="min-w-0 flex flex-col gap-4 border-b border-border bg-card/70 px-6 py-5 backdrop-blur lg:flex-row lg:items-center lg:justify-between lg:px-10"> |
| <div class="flex items-center gap-3"> |
| <button |
| class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-border text-foreground transition-colors |
| hover:border-primary hover:text-primary lg:hidden" |
| @click="isSidebarOpen = true" |
| aria-label="打开导航" |
| > |
| <svg aria-hidden="true" viewBox="0 0 24 24" class="h-5 w-5" fill="currentColor"> |
| <path d="M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h16v2H4v-2z" /> |
| </svg> |
| </button> |
| <svg |
| aria-hidden="true" |
| viewBox="0 0 130 150" |
| class="logo-mark h-9 w-9 shrink-0 text-foreground" |
| > |
| <defs> |
| <filter id="head-shadow" x="-50%" y="-50%" width="200%" height="200%"> |
| <feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="rgba(0, 188, 212, 0.2)"/> |
| </filter> |
| </defs> |
| <g class="logo-cat-wrapper" transform="translate(0, 12)"> |
| <g transform="translate(16, 20) rotate(-10, 9, 12)"> |
| <path d="M14 0 L18 24 L0 24 Z" fill="#2c3e50" /> |
| </g> |
| <g transform="translate(96, 20) rotate(10, 9, 12)"> |
| <path d="M4 0 L18 24 L0 24 Z" fill="#2c3e50" /> |
| </g> |
| <g filter="url(#head-shadow)"> |
| <path d="M 32 40 L 98 40 A 12 12 0 0 1 110 52 L 110 90 A 30 30 0 0 1 80 120 L 50 120 A 30 30 0 0 1 20 90 L 20 52 A 12 12 0 0 1 32 40 Z" |
| fill="rgba(255, 255, 255, 0.9)" |
| stroke="#2c3e50" |
| stroke-width="3" |
| /> |
| </g> |
| <rect class="logo-eye" x="35" y="68" width="14" height="4" rx="1" /> |
| <rect class="logo-eye" x="81" y="68" width="14" height="4" rx="1" /> |
| </g> |
| </svg> |
| <h2 class="text-xl font-semibold text-foreground lg:text-2xl"> |
| {{ currentPageTitle }} |
| </h2> |
| </div> |
| <div class="flex flex-wrap items-center gap-3"> |
| <button |
| @click="refreshPage" |
| class="rounded-full border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| title="刷新" |
| > |
| 刷新 |
| </button> |
| <button |
| class="rounded-full border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| @click="openApiInfo" |
| > |
| 接口信息 |
| </button> |
| <RouterLink |
| to="/public/uptime" |
| target="_blank" |
| class="rounded-full border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| > |
| 状态监控 |
| </RouterLink> |
| <RouterLink |
| to="/public/logs" |
| target="_blank" |
| class="rounded-full border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| > |
| 公开日志 |
| </RouterLink> |
| </div> |
| </header> |
|
|
| <div class="h-full overflow-y-auto overflow-x-hidden bg-card/70 px-4 pb-10 pt-6 backdrop-blur lg:px-10 lg:pt-10"> |
| <RouterView /> |
| </div> |
| </main> |
| </div> |
| <ConfirmDialog |
| :open="confirmDialog.open.value" |
| :title="confirmDialog.title.value" |
| :message="confirmDialog.message.value" |
| :confirm-text="confirmDialog.confirmText.value" |
| :cancel-text="confirmDialog.cancelText.value" |
| @confirm="confirmDialog.confirm" |
| @cancel="confirmDialog.cancel" |
| /> |
| <Teleport to="body"> |
| <div v-if="isApiInfoOpen" class="fixed inset-0 z-[100] flex items-center justify-center bg-black/30 px-4"> |
| <div class="w-full max-w-lg rounded-3xl border border-border bg-card p-6 shadow-xl"> |
| <div class="flex items-center justify-between"> |
| <p class="text-sm font-medium text-foreground">API 接口</p> |
| <button |
| class="text-xs text-muted-foreground transition-colors hover:text-foreground" |
| @click="isApiInfoOpen = false" |
| > |
| 关闭 |
| </button> |
| </div> |
| <p class="mt-2 text-xs text-muted-foreground">根据客户端选择对应接口</p> |
|
|
| <div class="mt-4 space-y-3 text-sm"> |
| <div> |
| <p class="text-xs text-muted-foreground">基础端点</p> |
| <div class="mt-1 flex items-start gap-2"> |
| <p class="min-w-0 flex-1 break-all rounded-2xl border border-border bg-background px-3 py-2 font-mono text-xs"> |
| {{ apiBaseUrl }} |
| </p> |
| <button |
| class="shrink-0 rounded-full border border-border px-3 py-1 text-[11px] text-muted-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| @click="copyText(apiBaseUrl)" |
| > |
| 复制 |
| </button> |
| </div> |
| </div> |
| <div> |
| <p class="text-xs text-muted-foreground">SDK 接口</p> |
| <div class="mt-1 flex items-start gap-2"> |
| <p class="min-w-0 flex-1 break-all rounded-2xl border border-border bg-background px-3 py-2 font-mono text-xs"> |
| {{ apiSdkUrl }} |
| </p> |
| <button |
| class="shrink-0 rounded-full border border-border px-3 py-1 text-[11px] text-muted-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| @click="copyText(apiSdkUrl)" |
| > |
| 复制 |
| </button> |
| </div> |
| </div> |
| <div> |
| <p class="text-xs text-muted-foreground">完整接口</p> |
| <div class="mt-1 flex items-start gap-2"> |
| <p class="min-w-0 flex-1 break-all rounded-2xl border border-border bg-background px-3 py-2 font-mono text-xs"> |
| {{ apiFullUrl }} |
| </p> |
| <button |
| class="shrink-0 rounded-full border border-border px-3 py-1 text-[11px] text-muted-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| @click="copyText(apiFullUrl)" |
| > |
| 复制 |
| </button> |
| </div> |
| </div> |
| <div> |
| <p class="text-xs text-muted-foreground">支持模型</p> |
| <div class="mt-1 rounded-2xl border border-border bg-background px-3 py-2 text-xs text-muted-foreground"> |
| <div class="flex flex-wrap gap-2 text-foreground"> |
| <span |
| v-for="model in supportedModels" |
| :key="model" |
| class="rounded-full border border-border px-2 py-0.5 text-[11px]" |
| > |
| {{ model }} |
| </span> |
| </div> |
| </div> |
| </div> |
| <div> |
| <p class="text-xs text-muted-foreground">API 密钥</p> |
| <div class="mt-1 flex items-start gap-2"> |
| <p class="min-w-0 flex-1 rounded-2xl border border-border bg-background px-3 py-2 font-mono text-xs"> |
| {{ apiKeyDisplay }} |
| </p> |
| <button |
| class="shrink-0 rounded-full border border-border px-3 py-1 text-[11px] text-muted-foreground transition-colors |
| hover:border-primary hover:text-primary" |
| @click="copyText(apiKeyDisplay)" |
| > |
| 复制 |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="mt-6 flex items-center justify-end"> |
| <button |
| class="rounded-full bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-opacity |
| hover:opacity-90" |
| @click="isApiInfoOpen = false" |
| > |
| 知道了 |
| </button> |
| </div> |
| </div> |
| </div> |
| </Teleport> |
| </div> |
| </template> |
|
|
| <script setup lang="ts"> |
| import { computed, ref, watch } from 'vue' |
| import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router' |
| import { useAuthStore, useSettingsStore } from '@/stores' |
| import ConfirmDialog from '@/components/ui/ConfirmDialog.vue' |
| import { useConfirmDialog } from '@/composables/useConfirmDialog' |
|
|
| const router = useRouter() |
| const route = useRoute() |
| const authStore = useAuthStore() |
| const settingsStore = useSettingsStore() |
| const isSidebarOpen = ref(false) |
| const isSidebarCollapsed = ref(false) |
| const confirmDialog = useConfirmDialog() |
| const isApiInfoOpen = ref(false) |
|
|
| const menuItems = [ |
| { |
| path: '/', |
| label: '概览', |
| icon: 'M4 4h7v7H4V4zm9 0h7v4h-7V4zm0 6h7v10h-7V10zM4 13h7v7H4v-7z', |
| }, |
| { |
| path: '/accounts', |
| label: '账号管理', |
| icon: 'M12 12a3.5 3.5 0 1 0-3.5-3.5A3.5 3.5 0 0 0 12 12zm0 2c-4.1 0-7.5 2.2-7.5 5v1h15v-1c0-2.8-3.4-5-7.5-5z', |
| }, |
| { |
| path: '/settings', |
| label: '系统设置', |
| icon: 'M4 6h10v2H4V6zm12 0h4v2h-4V6zM4 11h6v2H4v-2zm8 0h8v2h-8v-2zM4 16h10v2H4v-2zm12 0h4v2h-4v-2z', |
| }, |
| { |
| path: '/monitor', |
| label: '监控状态', |
| icon: 'M3 12h4l2-4 4 8 3-6h5v2h-4l-4 8-4-8-2 4H3v-2z', |
| }, |
| { |
| path: '/logs', |
| label: '运行日志', |
| icon: 'M4 6h16v2H4V6zm0 5h16v2H4v-2zm0 5h10v2H4v-2z', |
| }, |
| { |
| path: '/docs', |
| label: '文档中心', |
| icon: 'M6 3h9l4 4v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm8 1.5V8h3.5L14 4.5zM8 11h8v2H8v-2zm0 4h8v2H8v-2z', |
| }, |
| ] |
|
|
| const currentPageTitle = computed(() => { |
| const item = menuItems.find(item => item.path === route.path) |
| return item?.label || '概览' |
| }) |
|
|
| const navItemClass = (path: string) => { |
| const baseLayout = isSidebarCollapsed.value ? 'px-2 justify-center gap-0' : 'px-3 gap-3' |
| const base = `transition-colors ${baseLayout}` |
| if (route.path === path) { |
| return `${base} bg-primary text-primary-foreground` |
| } |
| return `${base} text-muted-foreground hover:bg-accent hover:text-accent-foreground` |
| } |
|
|
| const navIconClass = (path: string) => { |
| if (route.path === path) { |
| return 'bg-primary-foreground/15 text-primary-foreground border-primary-foreground/40' |
| } |
| return 'bg-secondary text-muted-foreground group-hover:text-accent-foreground' |
| } |
|
|
|
|
| const apiBaseUrl = computed(() => { |
| const raw = settingsStore.settings?.basic?.base_url |
| || import.meta.env.VITE_API_URL |
| || window.location.origin |
| return raw.replace(/\/$/, '') |
| }) |
|
|
| const apiSdkUrl = computed(() => `${apiBaseUrl.value}/v1`) |
| const apiFullUrl = computed(() => `${apiBaseUrl.value}/v1/chat/completions`) |
| const apiKeyDisplay = computed(() => settingsStore.settings?.basic?.api_key || '未设置') |
| const supportedModels = [ |
| 'gemini-auto', |
| 'gemini-2.5-flash', |
| 'gemini-2.5-pro', |
| 'gemini-3-flash-preview', |
| 'gemini-3-pro-preview', |
| ] |
|
|
| watch( |
| () => route.path, |
| () => { |
| isSidebarOpen.value = false |
| } |
| ) |
|
|
| const storedCollapse = localStorage.getItem('sidebar-collapsed') |
| if (storedCollapse) { |
| isSidebarCollapsed.value = storedCollapse === 'true' |
| } |
|
|
| watch(isSidebarCollapsed, (value) => { |
| localStorage.setItem('sidebar-collapsed', value ? 'true' : 'false') |
| }) |
|
|
| async function handleLogout() { |
| const confirmed = await confirmDialog.ask({ |
| title: '退出登录', |
| message: '确定退出管理控制台吗?', |
| }) |
| if (!confirmed) return |
| await authStore.logout() |
| router.push({ name: 'login' }) |
| } |
|
|
| function refreshPage() { |
| window.location.reload() |
| } |
|
|
| async function openApiInfo() { |
| isApiInfoOpen.value = true |
| if (!settingsStore.settings && !settingsStore.isLoading) { |
| await settingsStore.loadSettings() |
| } |
| } |
|
|
| async function copyText(value: string) { |
| if (!value) return |
| try { |
| await navigator.clipboard.writeText(value) |
| } catch (error) { |
| console.error('Copy failed', error) |
| } |
| } |
|
|
| </script> |
|
|