gemini-gateway / admin_v2.html
shadowfh96's picture
Upload admin_v2.html with huggingface_hub
b930ad9 verified
Raw
History Blame Contribute Delete
90.9 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini API Gateway - 管理后台</title>
<style>
/* === Tailwind-compatible Utility CSS (inline, no external deps) === */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; line-height: 1.5; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; tab-size: 4; }
body { margin: 0; }
img, svg { display: block; max-width: 100%; }
table { border-collapse: collapse; border-spacing: 0; }
input, button, textarea, select { font: inherit; }
a { color: inherit; text-decoration: inherit; }
code, pre { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
/* Layout */
.block { display: block; }
.inline-block { display: inline-block; }
.inline-flex { display: inline-flex; }
.flex { display: flex; }
.grid { display: grid; }
.hidden { display: none; }
.table { display: table; }
/* Flex */
.flex-col { flex-direction: column; }
.flex-row { flex-direction: row; }
.flex-wrap { flex-wrap: wrap; }
.flex-1 { flex: 1 1 0%; }
.flex-shrink-0 { flex-shrink: 0; }
.items-center { align-items: center; }
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.justify-start { justify-content: flex-start; }
.self-center { align-self: center; }
/* Grid */
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* Gap */
.gap-0\.5 { gap: 0.125rem; }
.gap-1 { gap: 0.25rem; }
.gap-1\.5 { gap: 0.375rem; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }
.gap-4 { gap: 1rem; }
.gap-6 { gap: 1.5rem; }
/* Sizing */
.w-2 { width: 0.5rem; }
.w-3 { width: 0.75rem; }
.w-3\.5 { width: 0.875rem; }
.w-4 { width: 1rem; }
.w-5 { width: 1.25rem; }
.w-6 { width: 1.5rem; }
.w-8 { width: 2rem; }
.w-9 { width: 2.25rem; }
.w-12 { width: 3rem; }
.w-16 { width: 4rem; }
.w-20 { width: 5rem; }
.w-40 { width: 10rem; }
.w-64 { width: 16rem; }
.w-full { width: 100%; }
.min-w-0 { min-width: 0; }
.min-w-full { min-width: 100%; }
.max-w-md { max-width: 28rem; }
.max-w-4xl { max-width: 56rem; }
.max-w-sm { max-width: 24rem; }
.h-2 { height: 0.5rem; }
.h-3 { height: 0.75rem; }
.h-3\.5 { height: 0.875rem; }
.h-4 { height: 1rem; }
.h-5 { height: 1.25rem; }
.h-6 { height: 1.5rem; }
.h-8 { height: 2rem; }
.h-10 { height: 2.5rem; }
.h-12 { height: 3rem; }
.h-16 { height: 4rem; }
.h-20 { height: 5rem; }
.h-64 { height: 16rem; }
.h-full { height: 100%; }
.h-screen { height: 100vh; }
.min-h-screen { min-height: 100vh; }
/* Spacing */
.space-y-1 > * + * { margin-top: 0.25rem; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-3 > * + * { margin-top: 0.75rem; }
.space-y-4 > * + * { margin-top: 1rem; }
.space-y-6 > * + * { margin-top: 1.5rem; }
.space-x-2 > * + * { margin-left: 0.5rem; }
.m-0 { margin: 0; }
.mx-auto { margin-left: auto; margin-right: auto; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-4 { margin-top: 1rem; }
.mt-6 { margin-top: 1.5rem; }
.mb-1\.5 { margin-bottom: 0.375rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.mb-5 { margin-bottom: 1.25rem; }
.mb-6 { margin-bottom: 1.5rem; }
.mb-8 { margin-bottom: 2rem; }
.ml-1 { margin-left: 0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.p-1 { padding: 0.25rem; }
.p-1\.5 { padding: 0.375rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 0.75rem; }
.p-4 { padding: 1rem; }
.p-5 { padding: 1.25rem; }
.p-6 { padding: 1.5rem; }
.p-8 { padding: 2rem; }
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-2\.5 { padding-left: 0.625rem; padding-right: 0.625rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-0\.5 { padding-top: 0.125rem; padding-bottom: 0.125rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-1\.5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
.py-5 { padding-top: 1.25rem; padding-bottom: 1.25rem; }
.py-16 { padding-top: 4rem; padding-bottom: 4rem; }
.pr-12 { padding-right: 3rem; }
/* Typography */
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.font-bold { font-weight: 700; }
.font-mono { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; }
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.uppercase { text-transform: uppercase; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.whitespace-nowrap { white-space: nowrap; }
.leading-none { line-height: 1; }
.antialiased { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
.select-none { -webkit-user-select: none; user-select: none; }
/* Text Colors */
.text-white { color: #fff; }
.text-slate-200 { color: #e2e8f0; }
.text-slate-300 { color: #cbd5e1; }
.text-slate-400 { color: #94a3b8; }
.text-slate-500 { color: #64748b; }
.text-slate-600 { color: #475569; }
.text-indigo-300 { color: #a5b4fc; }
.text-indigo-400 { color: #818cf8; }
.text-green-300 { color: #86efac; }
.text-green-400 { color: #4ade80; }
.text-red-300 { color: #fca5a5; }
.text-red-400 { color: #f87171; }
.text-amber-300 { color: #fcd34d; }
.text-amber-400 { color: #fbbf24; }
.text-blue-300 { color: #93c5fd; }
.text-blue-400 { color: #60a5fa; }
.text-purple-400 { color: #c084fc; }
.text-cyan-400 { color: #22d3ee; }
/* Background Colors */
.bg-green-500\/10 { background-color: rgba(34,197,94,0.1); }
.bg-blue-500\/10 { background-color: rgba(59,130,246,0.1); }
.bg-red-500\/10 { background-color: rgba(239,68,68,0.1); }
.bg-amber-500\/10, .bg-amber-500\/15 { background-color: rgba(245,158,11,0.12); }
.bg-indigo-500\/10 { background-color: rgba(99,102,241,0.1); }
.bg-indigo-500\/20 { background-color: rgba(99,102,241,0.2); }
.bg-slate-700\/50 { background-color: rgba(51,65,85,0.5); }
.bg-slate-800 { background-color: #1e293b; }
.bg-slate-900\/50 { background-color: rgba(15,23,42,0.5); }
.bg-green-500\/20 { background-color: rgba(34,197,94,0.2); }
.bg-red-500\/20 { background-color: rgba(239,68,68,0.2); }
.bg-blue-500\/20 { background-color: rgba(59,130,246,0.2); }
/* Border */
.border { border-width: 1px; border-style: solid; }
.border-t { border-top-width: 1px; border-top-style: solid; }
.border-b { border-bottom-width: 1px; border-bottom-style: solid; }
.border-r { border-right-width: 1px; border-right-style: solid; }
.border-slate-600 { border-color: #475569; }
.border-slate-700 { border-color: #334155; }
.border-slate-700\/20 { border-color: rgba(51,65,85,0.2); }
.border-slate-700\/30 { border-color: rgba(51,65,85,0.3); }
.border-slate-700\/40 { border-color: rgba(51,65,85,0.4); }
.border-slate-700\/50 { border-color: rgba(51,65,85,0.5); }
.border-indigo-500\/30 { border-color: rgba(99,102,241,0.3); }
.border-green-500\/20 { border-color: rgba(34,197,94,0.2); }
.border-green-500\/30 { border-color: rgba(34,197,94,0.3); }
.border-red-500\/20 { border-color: rgba(239,68,68,0.2); }
.border-red-500\/30 { border-color: rgba(239,68,68,0.3); }
.border-red-500\/40 { border-color: rgba(239,68,68,0.4); }
.border-blue-500\/30 { border-color: rgba(59,130,246,0.3); }
.border-amber-500\/30 { border-color: rgba(245,158,11,0.3); }
.border-purple-500\/30 { border-color: rgba(168,85,247,0.3); }
.border-cyan-500\/30 { border-color: rgba(6,182,212,0.3); }
.border-green-500\/30 { border-color: rgba(34,197,94,0.3); }
/* Rounded */
.rounded { border-radius: 0.25rem; }
.rounded-lg { border-radius: 0.5rem; }
.rounded-xl { border-radius: 0.75rem; }
.rounded-2xl { border-radius: 1rem; }
.rounded-full { border-radius: 9999px; }
/* Position */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.sticky { position: sticky; }
.inset-0 { inset: 0; }
.top-0 { top: 0; }
.top-5 { top: 1.25rem; }
.right-3 { right: 0.75rem; }
.right-5 { right: 1.25rem; }
.bottom-5 { bottom: 1.25rem; }
.z-10 { z-index: 10; }
.z-40 { z-index: 40; }
.z-50 { z-index: 50; }
.z-\[70\] { z-index: 70; }
.z-\[90\] { z-index: 90; }
.z-\[100\] { z-index: 100; }
.z-\[999\] { z-index: 999; }
/* Overflow */
.overflow-hidden { overflow: hidden; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }
/* Opacity */
.opacity-0 { opacity: 0; }
.opacity-40 { opacity: 0.4; }
.opacity-60 { opacity: 0.6; }
/* Shadow */
.shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); }
/* Transition */
.transition-all { transition-property: all; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4,0,0.2,1); }
.transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4,0,0.2,1); }
.transition-opacity { transition-property: opacity; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4,0,0.2,1); }
.transition { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4,0,0.2,1); }
.duration-200 { transition-duration: 200ms; }
.duration-300 { transition-duration: 300ms; }
.ease-out { transition-timing-function: cubic-bezier(0,0,0.2,1); }
.ease-in { transition-timing-function: cubic-bezier(0.4,0,1,1); }
/* Transform */
.-translate-y-1\/2 { transform: translateY(-50%); }
.translate-y-2 { transform: translateY(0.5rem); }
/* Cursor */
.cursor-pointer { cursor: pointer; }
.pointer-events-none { pointer-events: none; }
/* Hover states */
.hover\:text-white:hover { color: #fff; }
.hover\:text-slate-200:hover { color: #e2e8f0; }
.hover\:text-slate-300:hover { color: #cbd5e1; }
.hover\:text-red-400:hover { color: #f87171; }
.hover\:text-red-300:hover { color: #fca5a5; }
.hover\:text-green-400:hover { color: #4ade80; }
.hover\:text-green-300:hover { color: #86efac; }
.hover\:text-blue-300:hover { color: #93c5fd; }
.hover\:text-blue-400:hover { color: #60a5fa; }
.hover\:text-indigo-400:hover { color: #818cf8; }
.hover\:text-amber-300:hover { color: #fcd34d; }
.hover\:text-purple-300:hover { color: #d8b4fe; }
.hover\:text-cyan-300:hover { color: #67e8f9; }
.hover\:bg-slate-700\/50:hover { background-color: rgba(51,65,85,0.5); }
.hover\:border-slate-500:hover { border-color: #64748b; }
.hover\:border-green-500\/30:hover { border-color: rgba(34,197,94,0.3); }
.hover\:border-blue-500\/30:hover { border-color: rgba(59,130,246,0.3); }
/* Focus */
.focus\:border-indigo-500:focus { border-color: #6366f1 !important; }
.focus\:outline-none:focus { outline: none; }
.focus\:ring-2:focus { box-shadow: 0 0 0 2px rgba(99,102,241,0.5); }
/* Disabled */
.disabled\:opacity-40:disabled { opacity: 0.4; }
.disabled\:cursor-not-allowed:disabled { cursor: not-allowed; }
/* Important overrides for inputs */
.\!bg-slate-800 { background-color: #1e293b !important; }
.\!border { border-width: 1px !important; border-style: solid !important; }
.\!w-4 { width: 1rem !important; }
.\!h-4 { height: 1rem !important; }
/* Animations */
.animate-spin { animation: spin 1s linear infinite; }
.animate-pulse { animation: pulse 2s cubic-bezier(0.4,0,0.6,1) infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
/* --- Missing classes added for completeness --- */
/* Text colors */
.text-pink-400 { color: #f472b6; }
/* Backgrounds with opacity */
.bg-green-500\/15 { background-color: rgba(34,197,94,0.15); }
.bg-red-500\/15 { background-color: rgba(239,68,68,0.15); }
.bg-green-400 { background-color: #4ade80; }
.bg-slate-700 { background-color: #334155; }
.bg-slate-800\/60 { background-color: rgba(30,41,59,0.6); }
.bg-slate-900\/60 { background-color: rgba(15,23,42,0.6); }
.bg-slate-900\/70 { background-color: rgba(15,23,42,0.7); }
/* Border colors with opacity */
.border-indigo-500\/20 { border-color: rgba(99,102,241,0.2); }
.border-amber-500\/20 { border-color: rgba(245,158,11,0.2); }
.border-slate-700\/40 { border-color: rgba(51,65,85,0.4); }
/* Hover states */
.hover\:border-indigo-500\/30:hover { border-color: rgba(99,102,241,0.3); }
.hover\:border-amber-500\/30:hover { border-color: rgba(245,158,11,0.3); }
.hover\:bg-indigo-500\/10:hover { background-color: rgba(99,102,241,0.1); }
.hover\:bg-green-500\/10:hover { background-color: rgba(34,197,94,0.1); }
.hover\:text-indigo-300:hover { color: #a5b4fc; }
/* Typography */
.font-sans { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; }
.tracking-tight { letter-spacing: -0.025em; }
.list-decimal { list-style-type: decimal; }
.list-inside { list-style-position: inside; }
/* Text breaking */
.break-all { word-break: break-all; }
.break-words { overflow-wrap: break-word; }
/* Selection */
.select-all { -webkit-user-select: all; user-select: all; }
/* Cursor */
.cursor-not-allowed { cursor: not-allowed; }
/* Opacity */
.opacity-25 { opacity: 0.25; }
.opacity-75 { opacity: 0.75; }
/* Spacing */
.mt-0\.5 { margin-top: 0.125rem; }
.px-1\.5 { padding-left: 0.375rem; padding-right: 0.375rem; }
.ml-2 { margin-left: 0.5rem; }
.space-y-1\.5 > * + * { margin-top: 0.375rem; }
.space-y-5 > * + * { margin-top: 1.25rem; }
/* Positioning */
.top-1\/2 { top: 50%; }
/* Sizing - arbitrary values */
.w-\[18px\] { width: 18px; }
.h-\[18px\] { height: 18px; }
.max-w-lg { max-width: 32rem; }
.max-w-2xl { max-width: 42rem; }
.max-w-\[200px\] { max-width: 200px; }
.max-h-\[90vh\] { max-height: 90vh; }
.max-h-48 { max-height: 12rem; }
.h-48 { height: 12rem; }
.h-56 { height: 14rem; }
.h-72 { height: 18rem; }
.w-10 { width: 2.5rem; }
.h-9 { height: 2.25rem; }
.h-32 { height: 8rem; }
/* Focus ring - indigo */
.focus\:ring-indigo-500:focus { box-shadow: 0 0 0 3px rgba(99,102,241,0.5); }
/* Responsive - lg prefix (min-width: 1024px) */
@media (min-width: 1024px) {
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.lg\:col-span-2 { grid-column: span 2 / span 2; }
}
/* Responsive - sm prefix for height (min-width: 640px) */
@media (min-width: 640px) {
.sm\:h-72 { height: 18rem; }
.sm\:h-56 { height: 14rem; }
}
/* Loading overlay: auto-fades out after 3s via CSS (no JS dependency) */
@keyframes overlayFadeOut { from { opacity: 1; visibility: visible; } to { opacity: 0; visibility: hidden; } }
#app-loading { animation: overlayFadeOut 0.5s 3s forwards; }
/* === Custom Component Styles === */
body { background: #0f172a; }
.glass { background: rgba(30,41,59,0.7); backdrop-filter: blur(16px); border: 1px solid rgba(71,85,105,0.4); }
.glass-light { background: rgba(51,65,85,0.5); border: 1px solid rgba(71,85,105,0.3); }
.glass-sidebar { background: rgba(15,23,42,0.95); backdrop-filter: blur(20px); border-right: 1px solid rgba(71,85,105,0.3); }
.gradient-border { position: relative; }
.gradient-border::before {
content:''; position:absolute; inset:-2px; border-radius:1.25rem; padding:2px;
background: linear-gradient(135deg,#6366f1,#8b5cf6,#06b6d4,#6366f1);
background-size:300% 300%; animation: gradientShift 4s ease infinite;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor; mask-composite: exclude;
}
@keyframes gradientShift { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
.btn-primary { background: linear-gradient(135deg,#6366f1,#4f46e5); transition: all 0.2s; }
.btn-primary:hover { background: linear-gradient(135deg,#818cf8,#6366f1); box-shadow: 0 0 24px rgba(99,102,241,0.35); }
.btn-success { background: linear-gradient(135deg,#22c55e,#16a34a); transition: all 0.2s; }
.btn-success:hover { background: linear-gradient(135deg,#4ade80,#22c55e); box-shadow: 0 0 24px rgba(34,197,94,0.35); }
.btn-danger { background: linear-gradient(135deg,#ef4444,#dc2626); transition: all 0.2s; }
.btn-danger:hover { background: linear-gradient(135deg,#f87171,#ef4444); box-shadow: 0 0 24px rgba(239,68,68,0.35); }
.btn-ghost { transition: all 0.2s; }
.btn-ghost:hover { background: rgba(51,65,85,0.5); }
.badge-active { background: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.3); }
.badge-inactive { background: rgba(100,116,139,0.15); color: #94a3b8; border: 1px solid rgba(100,116,139,0.3); }
.badge-error { background: rgba(239,68,68,0.15); color: #f87171; border: 1px solid rgba(239,68,68,0.3); }
.badge-testing { background: rgba(245,158,11,0.15); color: #fbbf24; border: 1px solid rgba(245,158,11,0.3); }
.tbl-row:hover { background: rgba(51,65,85,0.4); }
.nav-item { transition: all 0.2s; }
.nav-item:hover { background: rgba(99,102,241,0.1); }
.nav-active { background: rgba(99,102,241,0.15) !important; color: #a5b4fc !important; border-right: 3px solid #6366f1; }
input, textarea, select {
background: rgba(15,23,42,0.8) !important;
border: 1px solid rgba(71,85,105,0.5) !important;
color: #e2e8f0 !important;
}
input:focus, textarea:focus, select:focus {
border-color: #6366f1 !important; outline: none;
box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
}
.scr::-webkit-scrollbar { width: 6px; height: 6px; }
.scr::-webkit-scrollbar-track { background: rgba(30,41,59,0.5); }
.scr::-webkit-scrollbar-thumb { background: rgba(100,116,139,0.5); border-radius: 3px; }
.scr::-webkit-scrollbar-thumb:hover { background: rgba(100,116,139,0.8); }
.fade-in { animation: fadeIn 0.3s ease-out; }
@keyframes fadeIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
.slide-in { animation: slideIn 0.25s ease-out; }
@keyframes slideIn { from { opacity:0; transform:translateX(-12px); } to { opacity:1; transform:translateX(0); } }
.pulse-dot { animation: pulseDot 2s infinite; }
@keyframes pulseDot { 0%,100%{opacity:1} 50%{opacity:0.4} }
.pulse-badge { animation: pulseBadge 1.5s infinite; }
@keyframes pulseBadge { 0%,100%{opacity:1} 50%{opacity:0.6} }
.skeleton { background: linear-gradient(90deg,rgba(51,65,85,0.3) 25%,rgba(51,65,85,0.5) 50%,rgba(51,65,85,0.3) 75%);
background-size: 200% 100%; animation: skeletonPulse 1.5s ease-in-out infinite; border-radius: 6px;
}
@keyframes skeletonPulse { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
.modal-bg { background: rgba(0,0,0,0.65); backdrop-filter: blur(6px); }
.toast-container { pointer-events: none; }
.toast-container > * { pointer-events: auto; }
/* Responsive sidebar */
@media (max-width: 768px) {
.sidebar-desktop { transform: translateX(-100%); position: fixed; z-index: 60; }
.sidebar-desktop.open { transform: translateX(0); }
.sidebar-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 55; }
.sidebar-overlay.open { display: block; }
}
@media (min-width: 769px) {
.sidebar-mobile-toggle { display: none !important; }
.sidebar-overlay { display: none !important; }
}
</style>
<script async src="https://registry.npmmirror.com/chart.js/4.4.7/files/dist/chart.umd.js"></script>
<script async src="https://registry.npmmirror.com/lucide/0.460.0/files/dist/umd/lucide.min.js"></script>
</head>
<body class="min-h-screen text-slate-200 font-sans antialiased">
<script>window.onerror=function(m,s,l,c,e){console.error('[JS Error]',m,'at',s,'line:',l);return true};</script>
<div id="app-loading" class="fixed inset-0 z-[999] flex items-center justify-center" style="background:#0f172a">
<div class="text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-indigo-500/20 border border-indigo-500/30 mb-4">
<svg class="w-8 h-8 text-indigo-400 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/></svg>
</div>
<p class="text-slate-400 text-sm">正在加载...</p>
</div>
</div>
<script>
window.__INITIAL_STATS__ = {"active_accounts": 0, "active_keys": 0, "total_requests": 0, "today_requests": 0, "daily_usage": [{"date": "2026-06-09", "requests": 0, "tokens": 0}, {"date": "2026-06-10", "requests": 0, "tokens": 0}, {"date": "2026-06-11", "requests": 0, "tokens": 0}, {"date": "2026-06-12", "requests": 0, "tokens": 0}, {"date": "2026-06-13", "requests": 0, "tokens": 0}, {"date": "2026-06-14", "requests": 0, "tokens": 0}, {"date": "2026-06-15", "requests": 0, "tokens": 0}]};
window.__INITIAL_DETAILED__ = {"models": [], "accounts": [], "hourly": [{"hour": "09:00", "requests": 0}, {"hour": "10:00", "requests": 0}, {"hour": "11:00", "requests": 0}, {"hour": "12:00", "requests": 0}, {"hour": "13:00", "requests": 0}, {"hour": "14:00", "requests": 0}, {"hour": "15:00", "requests": 0}, {"hour": "16:00", "requests": 0}, {"hour": "17:00", "requests": 0}, {"hour": "18:00", "requests": 0}, {"hour": "19:00", "requests": 0}, {"hour": "20:00", "requests": 0}, {"hour": "21:00", "requests": 0}, {"hour": "22:00", "requests": 0}, {"hour": "23:00", "requests": 0}, {"hour": "00:00", "requests": 0}, {"hour": "01:00", "requests": 0}, {"hour": "02:00", "requests": 0}, {"hour": "03:00", "requests": 0}, {"hour": "04:00", "requests": 0}, {"hour": "05:00", "requests": 0}, {"hour": "06:00", "requests": 0}, {"hour": "07:00", "requests": 0}, {"hour": "08:00", "requests": 0}], "error_rate": 0, "total_7d": 0, "errors_7d": 0};
window.__INITIAL_ACCOUNTS__ = [];
window.__INITIAL_KEYS__ = [];
window.__INITIAL_LOGS__ = [];
window.__INITIAL_SETTINGS__ = {"gemini_bl": "boq_assistant-bard-web-server_20260525.09_p0", "default_model": "gemini-3.5-flash"};
window.__INITIAL_SYSINFO__ = {"python": "3.12.13", "flask": "3.x", "uptime": "2026-06-15 08:00:42", "gemini_bl": "boq_assistant-bard-web-server_20260525.09_p0", "data_dir": "/data", "db_size": 32768};
</script>
<script>
var App={page:'overview',sidebarOpen:false,navItems:[{id:'overview',label:'总览',icon:'layout-dashboard'},{id:'accounts',label:'账号管理',icon:'users'},{id:'keys',label:'密钥管理',icon:'key'},{id:'logs',label:'用量日志',icon:'scroll-text'},{id:'settings',label:'系统设置',icon:'settings'}],clock:'',_clockTimer:null,stats:{},detailed:{},accounts:[],keys:[],logs:[],modelList:[],sysInfo:{},settingsForm:{gemini_bl:'',default_model:''},logFilter:'',autoRefresh:false,loading:{overview:false,accounts:false,keys:false,logs:false,settings:false,addAccount:false,editAccount:false,createKey:false,editKey:false,bulkImport:false,saveSettings:false,changePw:false,cookieHelper:false},accountForm:{name:'',email:'',cookie:'',plan:'free'},editForm:{id:null,name:'',email:'',plan:'free',cookie:''},newKeyName:'',editKeyId:null,editKeyName:'',generatedKey:'',bulkImportData:'',pwForm:{old_password:'',new_password:'',confirm_password:''},cookieHelperCode:'',cookieHelperResult:'',testResult:{ok:false,reply:'',duration:0,error:''},toasts:[],_toastId:0,_refreshTimer:null,_hasInitialData:false,_navigatedPages:null,_chart7:null,_chart24:null,_chartModel:null,
boot:function(){var o=document.getElementById('app-loading');if(o)o.remove();try{if(window.__INITIAL_STATS__&&Object.keys(window.__INITIAL_STATS__).length>0){console.log('[Boot] Using server-injected data');this.page='overview';this.sidebarOpen=false;this._hasInitialData=true;this._navigatedPages=new Set(['overview']);this.stats=window.__INITIAL_STATS__;this.loading.overview=false;if(window.__INITIAL_DETAILED__&&Object.keys(window.__INITIAL_DETAILED__).length>0){this.detailed=window.__INITIAL_DETAILED__;if(this.detailed.models)this.modelList=this.detailed.models.map(function(m){return m.model})}if(window.__INITIAL_ACCOUNTS__){this.accounts=window.__INITIAL_ACCOUNTS__;this.loading.accounts=false}if(window.__INITIAL_KEYS__){this.keys=window.__INITIAL_KEYS__;this.loading.keys=false}if(window.__INITIAL_LOGS__){this.logs=window.__INITIAL_LOGS__;this.loading.logs=false;var ms=new Set(this.logs.map(function(l){return l.model}).filter(Boolean));if(ms.size>0){var ex=new Set(this.modelList);ms.forEach(function(m){ex.add(m)});this.modelList=Array.from(ex)}}if(window.__INITIAL_SETTINGS__){this.settingsForm.gemini_bl=window.__INITIAL_SETTINGS__.gemini_bl||'';this.settingsForm.default_model=window.__INITIAL_SETTINGS__.default_model||''}if(window.__INITIAL_SYSINFO__)this.sysInfo=window.__INITIAL_SYSINFO__;this.loading.settings=false;this._tryRenderCharts()}else{this._hasInitialData=false;this.go('overview')}this._updateClock();this._clockTimer=setInterval(function(){App._updateClock()},1000);this.render();this.renderNav();this.refreshIcons()}catch(e){console.error('[Boot] Error:',e);this.loading.overview=false;this.loading.accounts=false;this.loading.keys=false;this.loading.logs=false;this.loading.settings=false}},
_updateClock:function(){var d=new Date();this.clock=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0')+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0');var c=document.getElementById('desktop-clock');if(c)c.textContent='当前时间: '+this.clock},
closeSidebar:function(){this.sidebarOpen=false;var o=document.getElementById('sidebar-overlay'),s=document.getElementById('sidebar');if(o)o.classList.remove('open');if(s)s.classList.remove('open')},
toggleSidebar:function(){this.sidebarOpen=!this.sidebarOpen;var o=document.getElementById('sidebar-overlay'),s=document.getElementById('sidebar');if(this.sidebarOpen){if(o)o.classList.add('open');if(s)s.classList.add('open')}else{if(o)o.classList.remove('open');if(s)s.classList.remove('open')}},
go:function(p){this.page=p;this.sidebarOpen=false;var o=document.getElementById('sidebar-overlay'),s=document.getElementById('sidebar');if(o)o.classList.remove('open');if(s)s.classList.remove('open');if(this._hasInitialData&&!this._navigatedPages)this._navigatedPages=new Set(['overview']);var skip=this._hasInitialData&&this._navigatedPages&&!this._navigatedPages.has(p);if(this._navigatedPages)this._navigatedPages.add(p);if(!skip){if(p==='overview')this.loadOverview();if(p==='accounts')this.loadAccounts();if(p==='keys')this.loadKeys();if(p==='logs')this.loadLogs();if(p==='settings')this.loadSettings()}else{if(p==='overview')this._tryRenderCharts()}this.render();this.renderNav();this.refreshIcons()},
refresh:function(){var p=this.page;if(p==='overview')this.loadOverview();if(p==='accounts')this.loadAccounts();if(p==='keys')this.loadKeys();if(p==='logs')this.loadLogs();if(p==='settings')this.loadSettings()},
_tryRenderCharts:function(){if(typeof Chart==='undefined'){console.log('[Charts] Chart.js not loaded, retry in 2s');setTimeout(function(){App._tryRenderCharts()},2000);return}this._render7day(this.stats.daily_usage||[]);this._render24h(this.detailed.hourly||[]);this._renderModelChart(this.detailed.models||[]);this.refreshIcons()},
refreshIcons:function(){try{if(window.lucide)lucide.createIcons()}catch(e){console.warn('[Icons] lucide error:',e)}},
api:function(url,opts){opts=opts||{};var st=Date.now();return new Promise(function(resolve){var xhr=new XMLHttpRequest(),method=opts.method||'GET',fu=method==='GET'?url+(url.indexOf('?')>=0?'&':'?')+'_t='+Date.now():url;xhr.open(method,fu,true);xhr.withCredentials=true;xhr.timeout=10000;if(method!=='GET'&&method!=='HEAD')xhr.setRequestHeader('Content-Type','application/json');xhr.onload=function(){if(xhr.status===401){window.location.href='/login';resolve(null);return}if(xhr.status>=200&&xhr.status<300){try{resolve(JSON.parse(xhr.responseText))}catch(e){resolve(null)}}else{var em='Server error '+xhr.status;try{var ej=JSON.parse(xhr.responseText);if(ej.error)em=ej.error}catch(e){}App.toast(em,'error');resolve(null)}};xhr.onerror=function(){App.toast('网络错误: '+url,'error');resolve(null)};xhr.ontimeout=function(){App.toast('请求超时: '+url,'error');resolve(null)};xhr.send(opts.body||null)})},
toast:function(msg,type){type=type||'success';var id=++this._toastId,t={id:id,message:msg,type:type,visible:true};this.toasts.push(t);this.renderToasts();this.refreshIcons();setTimeout(function(){t.visible=false;App.renderToasts();setTimeout(function(){App.toasts=App.toasts.filter(function(x){return x.id!==id});App.renderToasts()},300)},4000)},
loadOverview:function(){this.loading.overview=true;this.render();Promise.all([this.api('/api/admin/stats'),this.api('/api/admin/stats/detailed')]).then(function(r){var s=r[0],d=r[1];App.loading.overview=false;if(s)App.stats=s;if(d){App.detailed=d;if(d.models)App.modelList=d.models.map(function(m){return m.model})}App.render();App._tryRenderCharts()})},
_render7day:function(data){var el=document.getElementById('chart7day');if(!el)return;if(this._chart7)this._chart7.destroy();var labels=data.map(function(d){var p=d.date.split('-');return p[1]+'/'+p[2]});this._chart7=new Chart(el.getContext('2d'),{type:'bar',data:{labels:labels,datasets:[{label:'请求数',data:data.map(function(d){return d.requests}),backgroundColor:'rgba(99,102,241,0.6)',borderColor:'rgba(99,102,241,1)',borderWidth:1,borderRadius:6,yAxisID:'y',order:2},{label:'Token数',data:data.map(function(d){return d.tokens}),type:'line',borderColor:'rgba(34,211,238,0.8)',backgroundColor:'rgba(34,211,238,0.08)',borderWidth:2,pointBackgroundColor:'rgba(34,211,238,1)',pointRadius:4,pointHoverRadius:6,fill:true,tension:0.4,yAxisID:'y1',order:1}]},options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},plugins:{legend:{labels:{color:'#94a3b8',font:{size:11},usePointStyle:true,padding:16}},tooltip:{backgroundColor:'rgba(15,23,42,0.95)',borderColor:'rgba(71,85,105,0.5)',borderWidth:1,titleColor:'#e2e8f0',bodyColor:'#94a3b8',padding:12,cornerRadius:8}},scales:{x:{ticks:{color:'#64748b',font:{size:11}},grid:{color:'rgba(51,65,85,0.3)'}},y:{type:'linear',display:true,position:'left',title:{display:true,text:'请求数',color:'#818cf8',font:{size:11}},ticks:{color:'#818cf8',font:{size:11}},grid:{color:'rgba(51,65,85,0.3)'}},y1:{type:'linear',display:true,position:'right',title:{display:true,text:'Token数',color:'#22d3ee',font:{size:11}},ticks:{color:'#22d3ee',font:{size:11}},grid:{drawOnChartArea:false}}}}})},
_render24h:function(data){var el=document.getElementById('chart24h');if(!el)return;if(this._chart24)this._chart24.destroy();if(!data||data.length===0)return;var labels=data.map(function(d){return d.hour+':00'});this._chart24=new Chart(el.getContext('2d'),{type:'line',data:{labels:labels,datasets:[{label:'请求数',data:data.map(function(d){return d.requests}),borderColor:'rgba(139,92,246,0.8)',backgroundColor:'rgba(139,92,246,0.08)',borderWidth:2,pointRadius:2,pointHoverRadius:5,fill:true,tension:0.3}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{backgroundColor:'rgba(15,23,42,0.95)',borderColor:'rgba(71,85,105,0.5)',borderWidth:1,titleColor:'#e2e8f0',bodyColor:'#94a3b8',padding:10,cornerRadius:8}},scales:{x:{ticks:{color:'#64748b',font:{size:10},maxTicksLimit:12},grid:{color:'rgba(51,65,85,0.2)'}},y:{ticks:{color:'#8b5cf6',font:{size:11}},grid:{color:'rgba(51,65,85,0.3)'}}}}})},
_renderModelChart:function(data){var el=document.getElementById("chartModel");if(!el)return;if(this._chartModel)this._chartModel.destroy();if(!data||data.length===0)return;var colors=["#6366f1","#22d3ee","#f59e0b","#ef4444","#22c55e","#a855f7","#ec4899","#14b8a6"];this._chartModel=new Chart(el.getContext("2d"),{type:"doughnut",data:{labels:data.map(function(d){return d.model}),datasets:[{data:data.map(function(d){return d.requests}),backgroundColor:colors.slice(0,data.length),borderColor:"rgba(15,23,42,0.8)",borderWidth:2}]},options:{responsive:true,maintainAspectRatio:false,cutout:"55%",plugins:{legend:{position:"bottom",labels:{color:"#94a3b8",font:{size:11},padding:12,usePointStyle:true}},tooltip:{backgroundColor:"rgba(15,23,42,0.95)",borderColor:"rgba(71,85,105,0.5)",borderWidth:1,titleColor:"#e2e8f0",bodyColor:"#94a3b8",padding:10,cornerRadius:8}}}})},
loadAccounts:function(){this.loading.accounts=true;this.render();this.api('/api/admin/accounts').then(function(data){App.loading.accounts=false;if(data)App.accounts=data;App.render();App.refreshIcons()})},
resetAccountForm:function(){this.accountForm={name:'',email:'',cookie:'',plan:'free'}},
addAccount:function(){this.accountForm.name=document.getElementById('acc-name').value;this.accountForm.email=document.getElementById('acc-email').value;this.accountForm.cookie=document.getElementById('acc-cookie').value;this.accountForm.plan=document.getElementById('acc-plan').value;if(!this.accountForm.name||!this.accountForm.cookie)return;this.loading.addAccount=true;this.showModal('addAccount');this.api('/api/admin/accounts',{method:'POST',body:JSON.stringify(this.accountForm)}).then(function(data){App.loading.addAccount=false;if(data&&data.ok){App.hideModal();App.resetAccountForm();App.toast('账号添加成功','success');App.loadAccounts()}else{App.showModal('addAccount')}})},
openEditAccount:function(id){var a=this.accounts.find(function(x){return x.id===id});if(!a)return;this.editForm={id:a.id,name:a.name,email:a.email||'',plan:a.plan||'free',cookie:''};this.showModal('editAccount')},
saveEditAccount:function(){this.editForm.name=document.getElementById('edit-name').value;this.editForm.email=document.getElementById('edit-email').value;this.editForm.plan=document.getElementById('edit-plan').value;this.editForm.cookie=document.getElementById('edit-cookie').value;if(!this.editForm.id)return;this.loading.editAccount=true;this.showModal('editAccount');var body={name:this.editForm.name,email:this.editForm.email,plan:this.editForm.plan};if(this.editForm.cookie)body.cookie=this.editForm.cookie;this.api('/api/admin/accounts/'+this.editForm.id,{method:'PUT',body:JSON.stringify(body)}).then(function(data){App.loading.editAccount=false;if(data&&data.ok){App.hideModal();App.toast('账号已更新','success');App.loadAccounts()}else{App.showModal('editAccount')}})},
deleteAccount:function(id){var a=this.accounts.find(function(x){return x.id===id});if(!a)return;if(!confirm('确定要删除账号 "'+a.name+'" 吗?此操作不可恢复。'))return;this.api('/api/admin/accounts/'+id,{method:'DELETE'}).then(function(data){if(data&&data.ok){App.toast('账号已删除','success');App.loadAccounts()}})},
toggleAccountStatus:function(id){var a=this.accounts.find(function(x){return x.id===id});if(!a)return;var ns=a.status==='active'?'inactive':'active';this.api('/api/admin/accounts/'+id+'/status',{method:'PATCH',body:JSON.stringify({status:ns})}).then(function(data){if(data&&data.ok){App.toast('状态已更新为: '+((ns==='active')?'活跃':'停用'),'success');App.loadAccounts()}})},
testAccount:function(id){var a=this.accounts.find(function(x){return x.id===id});if(!a)return;a._testing=true;this.render();this.api('/api/admin/accounts/'+id+'/test',{method:'POST'}).then(function(data){a._testing=false;if(data){App.testResult=data;App.showModal('testResult')}App.render();App.refreshIcons()})},
loadCookieHelper:function(){this.loading.cookieHelper=true;this.showModal('cookieHelper');this.api('/api/admin/cookie-helper').then(function(data){App.loading.cookieHelper=false;if(data&&data.js)App.cookieHelperCode=data.js;else if(data&&typeof data==='string')App.cookieHelperCode=data;else App.cookieHelperCode='// 无法加载 Cookie 助手代码\n// 请手动在 Gemini 页面提取 Cookie';App.showModal('cookieHelper')})},
useCookieResult:function(){var el=document.getElementById('cookie-result-input');if(el)this.cookieHelperResult=el.value;if(!this.cookieHelperResult)return;this.accountForm.cookie=this.cookieHelperResult;this.cookieHelperResult='';this.showModal('addAccount');this.toast('Cookie 已填入表单','info')},
doBulkImport:function(){var el=document.getElementById('bulk-import-input');if(el)this.bulkImportData=el.value;var arr;try{arr=JSON.parse(this.bulkImportData)}catch(e){this.toast('JSON 格式错误: '+e.message,'error');return}if(!Array.isArray(arr)||arr.length===0){this.toast('请提供有效的 JSON 数组','error');return}this.loading.bulkImport=true;this.showModal('bulkImport');this.api('/api/admin/accounts/bulk',{method:'POST',body:JSON.stringify(arr)}).then(function(data){App.loading.bulkImport=false;if(data&&data.ok!==undefined){App.hideModal();App.bulkImportData='';var cnt=data.imported||data.count||arr.length;App.toast('成功导入 '+cnt+' 个账号','success');App.loadAccounts()}else{App.showModal('bulkImport')}})},
loadKeys:function(){this.loading.keys=true;this.render();this.api('/api/admin/keys').then(function(data){App.loading.keys=false;if(data)App.keys=data;App.render();App.refreshIcons()})},
createKey:function(){var el=document.getElementById('new-key-name');if(el)this.newKeyName=el.value;if(!this.newKeyName)return;this.loading.createKey=true;this.showModal('createKey');this.api('/api/admin/keys',{method:'POST',body:JSON.stringify({name:this.newKeyName})}).then(function(data){App.loading.createKey=false;if(data&&data.ok){App.generatedKey=data.key;App.newKeyName='';App.showModal('generatedKey');App.loadKeys();App.refreshIcons()}else{App.showModal('createKey')}})},
openEditKey:function(id){var k=this.keys.find(function(x){return x.id===id});if(!k)return;this.editKeyId=k.id;this.editKeyName=k.name;this.showModal('editKey')},
saveEditKey:function(){var el=document.getElementById('edit-key-name-input');if(el)this.editKeyName=el.value;if(!this.editKeyId||!this.editKeyName)return;this.loading.editKey=true;this.showModal('editKey');this.api('/api/admin/keys/'+this.editKeyId,{method:'PUT',body:JSON.stringify({name:this.editKeyName})}).then(function(data){App.loading.editKey=false;if(data&&data.ok){App.hideModal();App.toast('密钥名称已更新','success');App.loadKeys()}else{App.showModal('editKey')}})},
deleteKey:function(id){if(!confirm('确定要删除此密钥吗?此操作不可恢复。'))return;this.api('/api/admin/keys/'+id,{method:'DELETE'}).then(function(data){if(data&&data.ok){App.toast('密钥已删除','success');App.loadKeys()}})},
toggleKeyStatus:function(id){var k=this.keys.find(function(x){return x.id===id});if(!k)return;var ns=k.status==='active'?'inactive':'active';this.api('/api/admin/keys/'+id,{method:'PUT',body:JSON.stringify({name:k.name,status:ns})}).then(function(data){if(data&&data.ok){App.toast('密钥状态已更新','success');App.loadKeys()}})},
loadLogs:function(){this.loading.logs=true;this.render();this.api('/api/admin/usage?limit=200').then(function(data){App.loading.logs=false;if(data){App.logs=data;var ms=new Set(data.map(function(l){return l.model}).filter(Boolean));if(ms.size>0)App.modelList=Array.from(ms)}App.render();App.refreshIcons()})},
onLogFilterChange:function(){var s=document.getElementById('log-filter-select');if(s)this.logFilter=s.value;this.render()},
toggleAutoRefresh:function(){var cb=document.getElementById('auto-refresh-cb');if(cb)this.autoRefresh=cb.checked;if(this.autoRefresh){this._refreshTimer=setInterval(function(){if(App.page==='logs')App.loadLogs()},5000)}else{if(this._refreshTimer){clearInterval(this._refreshTimer);this._refreshTimer=null}}},
loadSettings:function(){this.loading.settings=true;this.render();Promise.all([this.api('/api/admin/settings'),this.api('/api/admin/system/info')]).then(function(r){var s=r[0],i=r[1];App.loading.settings=false;if(s){App.settingsForm.gemini_bl=s.gemini_bl||'';App.settingsForm.default_model=s.default_model||''}if(i)App.sysInfo=i;App.render();App.refreshIcons()})},
saveSettings:function(){var b=document.getElementById('settings-bl'),m=document.getElementById('settings-model');if(b)this.settingsForm.gemini_bl=b.value;if(m)this.settingsForm.default_model=m.value;this.loading.saveSettings=true;this.render();this.api('/api/admin/settings',{method:'POST',body:JSON.stringify(this.settingsForm)}).then(function(data){App.loading.saveSettings=false;if(data&&data.ok)App.toast('设置已保存','success');App.render()})},
changePassword:function(){var o=document.getElementById('pw-old'),n=document.getElementById('pw-new'),c=document.getElementById('pw-confirm');if(o)this.pwForm.old_password=o.value;if(n)this.pwForm.new_password=n.value;if(c)this.pwForm.confirm_password=c.value;if(!this.pwForm.old_password||!this.pwForm.new_password){this.toast('请填写完整密码信息','warning');return}if(this.pwForm.new_password!==this.pwForm.confirm_password){this.toast('两次输入的新密码不一致','error');return}if(this.pwForm.new_password.length<4){this.toast('新密码长度不能少于4位','warning');return}this.loading.changePw=true;this.render();this.api('/api/admin/settings/password',{method:'POST',body:JSON.stringify({old_password:this.pwForm.old_password,new_password:this.pwForm.new_password})}).then(function(data){App.loading.changePw=false;if(data&&data.ok){App.toast('密码修改成功','success');App.pwForm={old_password:'',new_password:'',confirm_password:''}}App.render()})},
fmtNum:function(n){if(n===undefined||n===null)return'--';return Number(n).toLocaleString()},
maskKey:function(k){if(!k||k.length<=10)return'******';return k.substring(0,6)+'...'+k.slice(-4)},
fmtTime:function(ts){if(!ts)return'--';try{var d=new Date(ts.replace(' ','T'));if(isNaN(d.getTime()))return ts;return String(d.getMonth()+1).padStart(2,'0')+'/'+String(d.getDate()).padStart(2,'0')+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0')}catch(e){return ts}},
relTime:function(ts){if(!ts)return'--';try{var d=new Date(ts.replace(' ','T'));if(isNaN(d.getTime()))return ts;var diff=Math.floor((Date.now()-d.getTime())/1000);if(diff<0)return'刚刚';if(diff<60)return diff+'秒前';if(diff<3600)return Math.floor(diff/60)+'分钟前';if(diff<86400)return Math.floor(diff/3600)+'小时前';if(diff<2592000)return Math.floor(diff/86400)+'天前';return this.fmtTime(ts)}catch(e){return ts}},
statusBadge:function(s){if(s==='active')return'badge-active';if(s==='error')return'badge-error';if(s==='testing')return'badge-testing';return'badge-inactive'},
statusLabel:function(s){if(s==='active')return'活跃';if(s==='error')return'异常';if(s==='testing')return'测试中';if(s==='inactive')return'停用';return s||'--'},
planLabel:function(p){if(p==='free')return'免费';if(p==='pro')return'Pro';if(p==='ultra')return'Ultra';return p||'--'},
statusColor:function(c){if(c>=200&&c<300)return'badge-active';if(c>=400&&c<500)return'badge-testing';if(c>=500)return'badge-error';return'badge-inactive'},
copyText:function(text,msg){try{navigator.clipboard.writeText(text).then(function(){App.toast(msg||'已复制到剪贴板','success')})}catch(e){var el=document.createElement('textarea');el.value=text;document.body.appendChild(el);el.select();document.execCommand('copy');document.body.removeChild(el);App.toast(msg||'已复制到剪贴板','success')}},
render:function(){var c=document.getElementById('page-content');if(!c)return;var p=this.page;if(p==='overview')c.innerHTML=this.renderOverview();else if(p==='accounts')c.innerHTML=this.renderAccounts();else if(p==='keys')c.innerHTML=this.renderKeys();else if(p==='logs')c.innerHTML=this.renderLogs();else if(p==='settings')c.innerHTML=this.renderSettings();this.refreshIcons()},
renderNav:function(){var nav=document.getElementById('sidebar-nav');if(!nav)return;var h='';for(var i=0;i<this.navItems.length;i++){var it=this.navItems[i],cls=this.page===it.id?'nav-active':'text-slate-400';h+='<button type="button" onclick="App.go(\''+it.id+'\')" class="nav-item w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium '+cls+'"><i data-lucide="'+it.icon+'" class="w-[18px] h-[18px] flex-shrink-0"></i><span class="truncate">'+it.label+'</span></button>'}nav.innerHTML=h;this.refreshIcons();var ci=this.navItems.find(function(n){return n.id===App.page});var ml=document.getElementById('mobile-page-label');if(ml)ml.textContent=ci?ci.label:'';var dl=document.getElementById('desktop-page-label');if(dl)dl.textContent=ci?ci.label:''},
renderOverview:function(){var s=this.stats,d=this.detailed,L=this.loading.overview;if(L)return'<div class="space-y-6"><div class="grid grid-cols-2 lg:grid-cols-4 gap-4"><div class="glass rounded-xl p-5 space-y-3"><div class="skeleton h-4 w-20"></div><div class="skeleton h-8 w-16"></div></div><div class="glass rounded-xl p-5 space-y-3"><div class="skeleton h-4 w-20"></div><div class="skeleton h-8 w-16"></div></div><div class="glass rounded-xl p-5 space-y-3"><div class="skeleton h-4 w-20"></div><div class="skeleton h-8 w-16"></div></div><div class="glass rounded-xl p-5 space-y-3"><div class="skeleton h-4 w-20"></div><div class="skeleton h-8 w-16"></div></div></div><div class="glass rounded-xl p-6"><div class="skeleton h-64 w-full"></div></div></div>';var er=(d.error_rate||0),erB=er<5?'bg-green-500/15':(er<15?'bg-amber-500/15':'bg-red-500/15'),erT=er<5?'text-green-400':(er<15?'text-amber-400':'text-red-400'),erS=er<5?'text-green-400':'text-amber-400',erSt=er<5?'运行正常':'需要关注';var h='<div class="space-y-6"><div class="grid grid-cols-2 lg:grid-cols-4 gap-4">';
h+='<div class="glass rounded-xl p-5 hover:border-green-500/30 transition-colors"><div class="flex items-center justify-between mb-3"><span class="text-slate-400 text-xs font-medium">活跃账号</span><div class="w-8 h-8 rounded-lg bg-green-500/10 flex items-center justify-center"><i data-lucide="users" class="w-4 h-4 text-green-400"></i></div></div><p class="text-3xl font-bold text-white">'+this.fmtNum(s.active_accounts)+'</p>';if(d.account_trend!==undefined){var tI=d.account_trend>=0?'trending-up':'trending-down',tC=(d.account_trend||0)>=0?'text-green-400':'text-red-400';h+='<div class="mt-2 flex items-center gap-1 text-xs"><i data-lucide="'+tI+'" class="w-3 h-3 '+tC+'"></i><span class="'+tC+'">'+Math.abs(d.account_trend||0)+'%</span></div>'}h+='</div>';
h+='<div class="glass rounded-xl p-5 hover:border-blue-500/30 transition-colors"><div class="flex items-center justify-between mb-3"><span class="text-slate-400 text-xs font-medium">活跃密钥</span><div class="w-8 h-8 rounded-lg bg-blue-500/10 flex items-center justify-center"><i data-lucide="key" class="w-4 h-4 text-blue-400"></i></div></div><p class="text-3xl font-bold text-white">'+this.fmtNum(s.active_keys)+'</p>';if(d.key_trend!==undefined){var kI=d.key_trend>=0?'trending-up':'trending-down',kC=(d.key_trend||0)>=0?'text-green-400':'text-red-400';h+='<div class="mt-2 flex items-center gap-1 text-xs"><i data-lucide="'+kI+'" class="w-3 h-3 '+kC+'"></i><span class="'+kC+'">'+Math.abs(d.key_trend||0)+'%</span></div>'}h+='</div>';
h+='<div class="glass rounded-xl p-5 hover:border-indigo-500/30 transition-colors"><div class="flex items-center justify-between mb-3"><span class="text-slate-400 text-xs font-medium">总请求数</span><div class="w-8 h-8 rounded-lg bg-indigo-500/10 flex items-center justify-center"><i data-lucide="bar-chart-3" class="w-4 h-4 text-indigo-400"></i></div></div><p class="text-3xl font-bold text-white">'+this.fmtNum(s.total_requests)+'</p>';if(d.request_trend!==undefined){var rI=d.request_trend>=0?'trending-up':'trending-down',rC=(d.request_trend||0)>=0?'text-green-400':'text-red-400';h+='<div class="mt-2 flex items-center gap-1 text-xs"><i data-lucide="'+rI+'" class="w-3 h-3 '+rC+'"></i><span class="'+rC+'">'+Math.abs(d.request_trend||0)+'%</span></div>'}h+='</div>';
h+='<div class="glass rounded-xl p-5 hover:border-amber-500/30 transition-colors"><div class="flex items-center justify-between mb-3"><span class="text-slate-400 text-xs font-medium">今日请求数</span><div class="w-8 h-8 rounded-lg bg-amber-500/10 flex items-center justify-center"><i data-lucide="clock" class="w-4 h-4 text-amber-400"></i></div></div><p class="text-3xl font-bold text-white">'+this.fmtNum(s.today_requests)+'</p>';if(d.today_tokens!==undefined)h+='<div class="mt-2 text-xs text-slate-500"><span>今日Token: '+this.fmtNum(d.today_tokens)+'</span></div>';h+='</div></div>';
if(d.error_rate!==undefined)h+='<div class="glass rounded-xl p-5 flex items-center gap-6"><div class="w-16 h-16 rounded-full flex items-center justify-center '+erB+'"><span class="text-2xl font-bold '+erT+'">'+er.toFixed(1)+'%</span></div><div><h4 class="text-white font-semibold text-sm">错误率</h4><p class="text-slate-400 text-xs mt-0.5">错误请求: '+this.fmtNum(d.error_count||0)+' / '+this.fmtNum(s.total_requests||0)+'</p><p class="text-xs mt-0.5 '+erS+'">'+erSt+'</p></div></div>';
h+='<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"><div class="glass rounded-xl p-6 lg:col-span-2"><div class="flex items-center justify-between mb-4"><h3 class="text-white font-semibold text-sm">近 7 日使用趋势</h3><button onclick="App.loadOverview()" class="btn-ghost text-slate-400 hover:text-white p-1.5 rounded-lg"><i data-lucide="refresh-cw" class="w-4 h-4"></i></button></div><div class="relative h-64 sm:h-72"><canvas id="chart7day"></canvas></div></div><div class="glass rounded-xl p-6"><h3 class="text-white font-semibold text-sm mb-4">模型使用分布</h3><div class="relative h-64 sm:h-72 flex items-center justify-center"><canvas id="chartModel"></canvas>';if(!d.models||d.models.length===0)h+='<p class="text-slate-500 text-sm absolute">暂无数据</p>';h+='</div></div></div>';
h+='<div class="glass rounded-xl p-6"><h3 class="text-white font-semibold text-sm mb-4">24 小时请求趋势</h3><div class="relative h-48 sm:h-56"><canvas id="chart24h"></canvas></div>';if(!d.hourly||d.hourly.length===0)h+='<p class="text-slate-500 text-sm text-center mt-4">暂无数据</p>';h+='</div></div>';return h},
renderAccounts:function(){var a=this.accounts,L=this.loading.accounts,h='';h+='<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-5"><h2 class="text-xl font-bold text-white">账号管理</h2><div class="flex items-center gap-2 flex-wrap"><button onclick="App.loadCookieHelper()" class="btn-ghost text-purple-400 hover:text-purple-300 text-sm font-medium px-3 py-2 rounded-lg flex items-center gap-1.5 border border-purple-500/30"><i data-lucide="wand-2" class="w-4 h-4"></i><span>Cookie助手</span></button><button onclick="App.showModal(\'bulkImport\')" class="btn-ghost text-cyan-400 hover:text-cyan-300 text-sm font-medium px-3 py-2 rounded-lg flex items-center gap-1.5 border border-cyan-500/30"><i data-lucide="upload" class="w-4 h-4"></i><span>批量导入</span></button><button onclick="App.resetAccountForm();App.showModal(\'addAccount\')" class="btn-primary text-white text-sm font-medium px-4 py-2 rounded-lg flex items-center gap-1.5"><i data-lucide="plus" class="w-4 h-4"></i><span>添加账号</span></button></div></div>';
if(L){h+='<div class="glass rounded-xl p-6 space-y-4">';for(var i=0;i<5;i++)h+='<div class="skeleton h-10 w-full"></div>';h+='</div>';return h}
h+='<div class="glass rounded-xl overflow-hidden"><div class="overflow-x-auto scr"><table class="w-full text-sm"><thead><tr class="border-b border-slate-700/50"><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">ID</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">名称</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">邮箱</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">状态</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">计划</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">今日请求</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">总请求</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">最后使用</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">操作</th></tr></thead><tbody>';
if(a.length===0)h+='<tr><td colspan="9" class="px-4 py-16 text-center text-slate-500"><i data-lucide="inbox" class="w-12 h-12 mx-auto mb-3 text-slate-600 block"></i><p>暂无账号,请点击"添加账号"开始配置</p></td></tr>';
else{for(var j=0;j<a.length;j++){var ac=a[j],sb=this.statusBadge(ac.status),st=this.statusLabel(ac.status),pb=ac.status==='testing'?'pulse-badge':'',ti=ac.status==='active'?'ban':'check-circle',tc=ac.status==='active'?'text-amber-400 hover:text-amber-300':'text-green-400 hover:text-green-300',ts=ac._testing?'animate-pulse':'';h+='<tr class="border-b border-slate-700/20 tbl-row transition-colors"><td class="px-4 py-3 text-slate-300">'+ac.id+'</td><td class="px-4 py-3 text-white font-medium">'+ac.name+'</td><td class="px-4 py-3 text-slate-400">'+(ac.email||'--')+'</td><td class="px-4 py-3"><span class="px-2.5 py-1 rounded-full text-xs font-medium '+sb+' '+pb+'">'+st+'</span></td><td class="px-4 py-3"><span class="text-slate-300 text-xs px-2 py-1 rounded bg-slate-700/50">'+this.planLabel(ac.plan)+'</span></td><td class="px-4 py-3 text-slate-300">'+this.fmtNum(ac.requests_today)+'</td><td class="px-4 py-3 text-slate-300">'+this.fmtNum(ac.total_requests)+'</td><td class="px-4 py-3 text-slate-400 text-xs whitespace-nowrap">'+(ac.last_used?this.relTime(ac.last_used):'--')+'</td><td class="px-4 py-3"><div class="flex items-center gap-0.5"><button onclick="App.testAccount('+ac.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 text-blue-400 hover:text-blue-300 transition-colors" title="测试账号"><i data-lucide="zap" class="w-4 h-4 '+ts+'"></i></button><button onclick="App.openEditAccount('+ac.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors" title="编辑"><i data-lucide="pencil" class="w-4 h-4"></i></button><button onclick="App.toggleAccountStatus('+ac.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 '+tc+' transition-colors" title="'+(ac.status==='active'?'停用':'启用')+'"><i data-lucide="'+ti+'" class="w-4 h-4"></i></button><button onclick="App.deleteAccount('+ac.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 text-red-400 hover:text-red-300 transition-colors" title="删除"><i data-lucide="trash-2" class="w-4 h-4"></i></button></div></td></tr>'}}
h+='</tbody></table></div></div>';return h},
renderKeys:function(){var ks=this.keys,L=this.loading.keys,h='<div class="flex items-center justify-between mb-5"><h2 class="text-xl font-bold text-white">密钥管理</h2><button onclick="App.newKeyName=\'\';App.showModal(\'createKey\')" class="btn-primary text-white text-sm font-medium px-4 py-2 rounded-lg flex items-center gap-1.5"><i data-lucide="plus" class="w-4 h-4"></i><span>创建密钥</span></button></div>';
if(L){h+='<div class="glass rounded-xl p-6 space-y-4">';for(var i=0;i<3;i++)h+='<div class="skeleton h-10 w-full"></div>';h+='</div>';return h}
h+='<div class="glass rounded-xl overflow-hidden"><div class="overflow-x-auto scr"><table class="w-full text-sm"><thead><tr class="border-b border-slate-700/50"><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">密钥</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">名称</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">状态</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">总请求数</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">创建时间</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">操作</th></tr></thead><tbody>';
if(ks.length===0)h+='<tr><td colspan="6" class="px-4 py-16 text-center text-slate-500"><i data-lucide="key-round" class="w-12 h-12 mx-auto mb-3 text-slate-600 block"></i><p>暂无密钥,请点击"创建密钥"</p></td></tr>';
else{for(var j=0;j<ks.length;j++){var k=ks[j],bc=k.status==='active'?'badge-active':'badge-inactive',st=k.status==='active'?'活跃':'停用',ti=k.status==='active'?'ban':'check-circle',tc=k.status==='active'?'text-amber-400 hover:text-amber-300':'text-green-400 hover:text-green-300',ke=(k.key||'').replace(/'/g,"\\'").replace(/"/g,'&quot;');h+='<tr class="border-b border-slate-700/20 tbl-row transition-colors"><td class="px-4 py-3"><div class="flex items-center gap-2"><code class="text-indigo-300 text-xs bg-indigo-500/10 px-2 py-1 rounded font-mono">'+this.maskKey(k.key)+'</code><button onclick="App.copyText(\''+ke+'\',\'密钥已复制\')" class="text-slate-500 hover:text-indigo-400 transition-colors p-1" title="复制密钥"><i data-lucide="copy" class="w-3.5 h-3.5"></i></button></div></td><td class="px-4 py-3 text-white font-medium">'+k.name+'</td><td class="px-4 py-3"><span class="px-2.5 py-1 rounded-full text-xs font-medium '+bc+'">'+st+'</span></td><td class="px-4 py-3 text-slate-300">'+this.fmtNum(k.total_requests)+'</td><td class="px-4 py-3 text-slate-400 text-xs whitespace-nowrap">'+(k.created_at?this.relTime(k.created_at):'--')+'</td><td class="px-4 py-3"><div class="flex items-center gap-0.5"><button onclick="App.copyText(\''+ke+'\',\'密钥已复制\')" class="p-1.5 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-indigo-400 transition-colors" title="复制"><i data-lucide="copy" class="w-4 h-4"></i></button><button onclick="App.openEditKey('+k.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors" title="编辑"><i data-lucide="pencil" class="w-4 h-4"></i></button><button onclick="App.toggleKeyStatus('+k.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 '+tc+' transition-colors" title="'+(k.status==='active'?'停用':'启用')+'"><i data-lucide="'+ti+'" class="w-4 h-4"></i></button><button onclick="App.deleteKey('+k.id+')" class="p-1.5 rounded-lg hover:bg-slate-700/50 text-red-400 hover:text-red-300 transition-colors" title="删除"><i data-lucide="trash-2" class="w-4 h-4"></i></button></div></td></tr>'}}
h+='</tbody></table></div></div>';return h},
renderLogs:function(){var logs=this.logs,L=this.loading.logs,filtered=logs;if(this.logFilter)filtered=logs.filter(function(l){return l.model===App.logFilter});var h='<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-5"><h2 class="text-xl font-bold text-white">用量日志</h2><div class="flex items-center gap-3"><select id="log-filter-select" onchange="App.onLogFilterChange()" class="px-3 py-1.5 rounded-lg text-xs" style="min-width:140px"><option value="">全部模型</option>';for(var m=0;m<this.modelList.length;m++){var sel=this.logFilter===this.modelList[m]?' selected':'';h+='<option value="'+this.modelList[m]+'"'+sel+'>'+this.modelList[m]+'</option>'}h+='</select><label class="flex items-center gap-2 text-xs text-slate-400 cursor-pointer select-none"><input id="auto-refresh-cb" type="checkbox" onchange="App.toggleAutoRefresh()" class="!bg-slate-800 rounded border-slate-600 text-indigo-500 focus:ring-indigo-500 !border !w-4 !h-4"'+(this.autoRefresh?' checked':'')+'><span>自动刷新</span></label><button onclick="App.loadLogs()" class="btn-ghost text-slate-400 hover:text-white p-2 rounded-lg border border-slate-700/50" title="刷新"><i data-lucide="refresh-cw" class="w-4 h-4'+(L?' animate-spin':'')+'"></i></button></div></div>';
if(L){h+='<div class="glass rounded-xl p-6 space-y-4">';for(var i=0;i<8;i++)h+='<div class="skeleton h-8 w-full"></div>';h+='</div>';return h}
h+='<div class="glass rounded-xl overflow-hidden"><div class="overflow-x-auto scr"><table class="w-full text-sm"><thead><tr class="border-b border-slate-700/50"><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">时间</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">模型</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">账号</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">密钥</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">Token输入</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">Token输出</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">耗时(ms)</th><th class="text-left px-4 py-3 text-slate-400 font-medium text-xs">状态码</th></tr></thead><tbody>';
if(filtered.length===0)h+='<tr><td colspan="8" class="px-4 py-16 text-center text-slate-500"><i data-lucide="scroll-text" class="w-12 h-12 mx-auto mb-3 text-slate-600 block"></i><p>暂无日志记录</p></td></tr>';
else{for(var j=0;j<filtered.length;j++){var l=filtered[j];h+='<tr class="border-b border-slate-700/20 tbl-row transition-colors"><td class="px-4 py-3 text-slate-400 text-xs whitespace-nowrap">'+this.fmtTime(l.created_at)+'</td><td class="px-4 py-3"><code class="text-indigo-300 text-xs bg-indigo-500/10 px-2 py-0.5 rounded">'+l.model+'</code></td><td class="px-4 py-3 text-slate-300">'+(l.account_name||'--')+'</td><td class="px-4 py-3 text-slate-400 text-xs">'+(l.key_name||'--')+'</td><td class="px-4 py-3 text-slate-300">'+this.fmtNum(l.tokens_in)+'</td><td class="px-4 py-3 text-slate-300">'+this.fmtNum(l.tokens_out)+'</td><td class="px-4 py-3 text-slate-400 text-xs">'+(l.duration_ms?l.duration_ms+'ms':'--')+'</td><td class="px-4 py-3"><span class="px-2 py-0.5 rounded text-xs font-mono font-medium '+this.statusColor(l.status_code)+'">'+l.status_code+'</span></td></tr>'}}
h+='</tbody></table></div><div class="px-4 py-3 border-t border-slate-700/30 text-xs text-slate-500 flex items-center justify-between"><span>共 '+filtered.length+' 条记录</span><span>显示最近 200 条</span></div></div>';return h},
renderSettings:function(){var sI=this.sysInfo,sf=this.settingsForm,L=this.loading.settings,or=window.location?window.location.origin:'',h='<div class="space-y-6 max-w-4xl">';if(L){h+='<div class="space-y-6">';for(var i=0;i<4;i++)h+='<div class="glass rounded-xl p-6"><div class="skeleton h-6 w-40 mb-4"></div><div class="skeleton h-20 w-full"></div></div>';h+='</div></div>';return h}h+='<div class="space-y-6">';
h+='<div class="glass rounded-xl p-6"><h3 class="text-white font-semibold mb-4 flex items-center gap-2"><i data-lucide="info" class="w-4 h-4 text-indigo-400"></i>基本信息</h3><div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"><div class="flex justify-between py-2 border-b border-slate-700/30"><span class="text-slate-400">Python 版本</span><span class="text-white font-mono text-xs">'+(sI.python_version||'--')+'</span></div><div class="flex justify-between py-2 border-b border-slate-700/30"><span class="text-slate-400">Flask 版本</span><span class="text-white font-mono text-xs">'+(sI.flask_version||'--')+'</span></div><div class="flex justify-between py-2 border-b border-slate-700/30"><span class="text-slate-400">数据目录</span><span class="text-white font-mono text-xs truncate max-w-[200px]">'+(sI.data_dir||'--')+'</span></div><div class="flex justify-between py-2 border-b border-slate-700/30"><span class="text-slate-400">数据库大小</span><span class="text-white font-mono text-xs">'+(sI.db_size||'--')+'</span></div><div class="flex justify-between py-2 border-b border-slate-700/30"><span class="text-slate-400">Gemini BL 版本</span><span class="text-white font-mono text-xs">'+(sI.gemini_bl_version||'--')+'</span></div></div></div>';
h+='<div class="glass rounded-xl p-6"><h3 class="text-white font-semibold mb-4 flex items-center gap-2"><i data-lucide="sliders-horizontal" class="w-4 h-4 text-indigo-400"></i>系统参数</h3><div class="space-y-4"><div><label class="block text-sm text-slate-400 mb-1.5">gemini_bl</label><input id="settings-bl" type="text" value="'+(sf.gemini_bl||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm font-mono" placeholder="gemini_bl 值"></div><div><label class="block text-sm text-slate-400 mb-1.5">default_model</label><input id="settings-model" type="text" value="'+(sf.default_model||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm font-mono" placeholder="默认模型"></div><div class="flex justify-end"><button onclick="App.saveSettings()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg flex items-center gap-2'+(this.loading.saveSettings?' opacity-40':'')+'"><i data-lucide="save" class="w-4 h-4"></i><span>'+((this.loading.saveSettings)?'保存中...':'保存设置')+'</span></button></div></div></div>';
h+='<div class="glass rounded-xl p-6"><h3 class="text-white font-semibold mb-4 flex items-center gap-2"><i data-lucide="shield" class="w-4 h-4 text-indigo-400"></i>修改密码</h3><div class="space-y-4"><div><label class="block text-sm text-slate-400 mb-1.5">当前密码</label><input id="pw-old" type="password" value="" class="w-full px-4 py-2.5 rounded-lg text-sm" placeholder="请输入当前密码"></div><div><label class="block text-sm text-slate-400 mb-1.5">新密码</label><input id="pw-new" type="password" value="" class="w-full px-4 py-2.5 rounded-lg text-sm" placeholder="请输入新密码"></div><div><label class="block text-sm text-slate-400 mb-1.5">确认新密码</label><input id="pw-confirm" type="password" value="" class="w-full px-4 py-2.5 rounded-lg text-sm" placeholder="请再次输入新密码"></div><div class="flex justify-end"><button onclick="App.changePassword()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg flex items-center gap-2'+(this.loading.changePw?' opacity-40':'')+'"><i data-lucide="key-round" class="w-4 h-4"></i><span>'+((this.loading.changePw)?'修改中...':'修改密码')+'</span></button></div></div></div>';
h+='<div class="glass rounded-xl p-6"><h3 class="text-white font-semibold mb-4 flex items-center gap-2"><i data-lucide="book-open" class="w-4 h-4 text-indigo-400"></i>API 文档</h3><div class="space-y-4 text-sm"><div><label class="block text-slate-400 mb-1">Base URL</label><div class="flex items-center gap-2"><code id="api-base-url" class="flex-1 bg-slate-900/60 px-4 py-2.5 rounded-lg text-indigo-300 font-mono text-xs border border-slate-700/50">'+or+'/v1</code><button onclick="App.copyText(document.getElementById(\'api-base-url\').textContent,\'地址已复制\')" class="text-slate-400 hover:text-indigo-400 p-2 rounded-lg hover:bg-slate-700/50 transition-colors"><i data-lucide="copy" class="w-4 h-4"></i></button></div></div><div><label class="block text-slate-400 mb-1">认证示例 (curl)</label><pre class="bg-slate-900/60 px-4 py-3 rounded-lg text-green-300 font-mono text-xs overflow-x-auto scr border border-slate-700/50 whitespace-pre-wrap">curl -X POST '+or+'/v1/chat/completions \\\n -H "Authorization: Bearer YOUR_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"gemini-2.5-flash","messages":[{"role":"user","content":"Hello"}]}\'</pre></div>';
h+='<div><label class="block text-slate-400 mb-1">可用模型</label>';if(sI.models&&sI.models.length>0){h+='<div class="flex flex-wrap gap-2">';for(var mi=0;mi<sI.models.length;mi++)h+='<span class="bg-indigo-500/10 text-indigo-300 px-3 py-1 rounded-lg text-xs font-mono border border-indigo-500/20">'+sI.models[mi]+'</span>';h+='</div>'}else h+='<p class="text-slate-500 text-xs">加载中...</p>';h+='</div></div></div>';
h+='<div class="glass rounded-xl p-6"><h3 class="text-white font-semibold mb-4 flex items-center gap-2"><i data-lucide="heart" class="w-4 h-4 text-pink-400"></i>关于</h3><div class="text-sm space-y-2"><div class="flex items-center gap-2"><span class="text-slate-400">项目名称:</span><span class="text-white font-medium">Gemini API Gateway</span></div><div class="flex items-center gap-2"><span class="text-slate-400">版本:</span><span class="text-white font-mono text-xs">'+(sI.version||'v1.0.0')+'</span></div><div class="flex items-center gap-2"><span class="text-slate-400">开源协议:</span><span class="text-slate-300">MIT</span></div></div></div>';
h+='</div></div>';return h},
showModal:function(name){var c=document.getElementById('modal-container');if(!c)return;var h='';var af=this.accountForm,ef=this.editForm,tr=this.testResult,sp=this.spinnerHTML;
if(name==='addAccount'){h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-lg p-6 shadow-2xl fade-in max-h-[90vh] overflow-y-auto scr"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white">添加账号</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div class="space-y-4"><div><label class="block text-sm font-medium text-slate-300 mb-1.5">名称 <span class="text-red-400">*</span></label><input id="acc-name" type="text" value="'+(af.name||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm" placeholder="账号名称"></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">邮箱</label><input id="acc-email" type="email" value="'+(af.email||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm" placeholder="example@gmail.com"></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">Cookie <span class="text-red-400">*</span> <button onclick="App.hideModal();App.loadCookieHelper()" class="ml-2 text-xs text-purple-400 hover:text-purple-300 inline-flex items-center gap-1"><i data-lucide="wand-2" class="w-3 h-3"></i>Cookie助手</button></label><textarea id="acc-cookie" rows="4" class="w-full px-4 py-2.5 rounded-lg text-sm font-mono" placeholder="粘贴 Gemini Cookie 字符串...">'+(af.cookie||'')+'</textarea></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">计划</label><select id="acc-plan" class="w-full px-4 py-2.5 rounded-lg text-sm"><option value="free"'+(af.plan==='free'?' selected':'')+'>免费</option><option value="pro"'+(af.plan==='pro'?' selected':'')+'>Pro</option><option value="ultra"'+(af.plan==='ultra'?' selected':'')+'>Ultra</option></select></div></div><div class="flex items-center justify-end gap-3 mt-6"><button onclick="App.hideModal()" class="px-4 py-2 text-sm text-slate-400 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">取消</button><button onclick="App.addAccount()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg'+(this.loading.addAccount?' opacity-40 cursor-not-allowed':'')+' flex items-center gap-2">'+(this.loading.addAccount?sp:'')+'<span>'+((this.loading.addAccount)?'添加中...':'确认添加')+'</span></button></div></div></div>'}
else if(name==='editAccount'){h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-lg p-6 shadow-2xl fade-in"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white">编辑账号</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div class="space-y-4"><div><label class="block text-sm font-medium text-slate-300 mb-1.5">名称</label><input id="edit-name" type="text" value="'+(ef.name||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm"></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">邮箱</label><input id="edit-email" type="email" value="'+(ef.email||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm"></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">计划</label><select id="edit-plan" class="w-full px-4 py-2.5 rounded-lg text-sm"><option value="free"'+(ef.plan==='free'?' selected':'')+'>免费</option><option value="pro"'+(ef.plan==='pro'?' selected':'')+'>Pro</option><option value="ultra"'+(ef.plan==='ultra'?' selected':'')+'>Ultra</option></select></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">更新 Cookie <span class="text-slate-500">(留空则不修改)</span></label><textarea id="edit-cookie" rows="3" class="w-full px-4 py-2.5 rounded-lg text-sm font-mono" placeholder="输入新 Cookie 以更新...">'+(ef.cookie||'')+'</textarea></div></div><div class="flex items-center justify-end gap-3 mt-6"><button onclick="App.hideModal()" class="px-4 py-2 text-sm text-slate-400 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">取消</button><button onclick="App.saveEditAccount()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg'+(this.loading.editAccount?' opacity-40':'')+' flex items-center gap-2">'+(this.loading.editAccount?sp:'')+'<span>'+((this.loading.editAccount)?'保存中...':'保存修改')+'</span></button></div></div></div>'}
else if(name==='cookieHelper'){var cd=this.loading.cookieHelper?'<div class="skeleton h-32 w-full rounded-lg"></div>':'<pre class="bg-slate-900/70 px-4 py-3 rounded-lg text-green-300 font-mono text-xs overflow-x-auto scr border border-slate-700/50 whitespace-pre-wrap break-all max-h-48 overflow-y-auto">'+(this.cookieHelperCode||'')+'</pre>';h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-2xl p-6 shadow-2xl fade-in max-h-[90vh] overflow-y-auto scr"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i data-lucide="wand-2" class="w-5 h-5 text-purple-400"></i>Cookie 助手</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div class="space-y-5"><div class="p-4 rounded-xl bg-amber-500/10 border border-amber-500/20"><h4 class="text-amber-300 font-medium text-sm mb-2 flex items-center gap-1.5"><i data-lucide="lightbulb" class="w-4 h-4"></i>使用说明</h4><ol class="text-slate-300 text-xs space-y-1.5 list-decimal list-inside"><li>打开浏览器,访问 <code class="text-indigo-300">gemini.google.com</code> 并登录</li><li><kbd class="bg-slate-700 px-1.5 py-0.5 rounded text-slate-200">F12</kbd> 打开开发者工具</li><li>切换到 "控制台" (Console) 标签</li><li>将以下代码粘贴到控制台并按回车执行</li><li>将复制的结果粘贴到下方的输入框中</li></ol></div><div><div class="flex items-center justify-between mb-2"><label class="text-sm font-medium text-slate-300">提取代码</label><button onclick="App.copyText(App.cookieHelperCode,\'代码已复制\')" class="text-xs text-indigo-400 hover:text-indigo-300 flex items-center gap-1 px-2 py-1 rounded-lg hover:bg-indigo-500/10 transition-colors"><i data-lucide="copy" class="w-3 h-3"></i>复制代码</button></div>'+cd+'</div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">粘贴提取结果</label><textarea id="cookie-result-input" rows="4" class="w-full px-4 py-2.5 rounded-lg text-sm font-mono" placeholder="将控制台输出的 Cookie 粘贴到这里...">'+(this.cookieHelperResult||'')+'</textarea></div><div class="flex items-center justify-end gap-3"><button onclick="App.hideModal()" class="px-4 py-2 text-sm text-slate-400 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">关闭</button><button onclick="App.useCookieResult()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg'+(!this.cookieHelperResult?' opacity-40 cursor-not-allowed':'')+' flex items-center gap-2"><i data-lucide="check" class="w-4 h-4"></i><span>使用该 Cookie</span></button></div></div></div></div>'}
else if(name==='bulkImport'){h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-2xl p-6 shadow-2xl fade-in max-h-[90vh] overflow-y-auto scr"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i data-lucide="upload" class="w-5 h-5 text-cyan-400"></i>批量导入</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div class="space-y-4"><div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/40"><p class="text-slate-400 text-xs mb-2">JSON 格式示例:</p><pre class="text-green-300 font-mono text-xs whitespace-pre-wrap">[\n {\n "name": "账号1",\n "email": "user1@gmail.com",\n "cookie": "cookie_string_here",\n "plan": "free"\n },\n {\n "name": "账号2",\n "email": "user2@gmail.com",\n "cookie": "cookie_string_here",\n "plan": "pro"\n }\n]</pre></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">JSON 数据</label><textarea id="bulk-import-input" rows="8" class="w-full px-4 py-2.5 rounded-lg text-sm font-mono" placeholder="粘贴 JSON 数组...">'+(this.bulkImportData||'')+'</textarea></div></div><div class="flex items-center justify-end gap-3 mt-6"><button onclick="App.hideModal()" class="px-4 py-2 text-sm text-slate-400 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">取消</button><button onclick="App.doBulkImport()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg'+(!this.bulkImportData||this.loading.bulkImport?' opacity-40 cursor-not-allowed':'')+' flex items-center gap-2">'+(this.loading.bulkImport?sp:'')+'<span>'+((this.loading.bulkImport)?'导入中...':'开始导入')+'</span></button></div></div></div>'}
else if(name==='createKey'){h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-md p-6 shadow-2xl fade-in"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white">创建密钥</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">密钥名称</label><input id="new-key-name" type="text" value="'+(this.newKeyName||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm" placeholder="例如: 生产环境密钥"></div><div class="flex items-center justify-end gap-3 mt-6"><button onclick="App.hideModal()" class="px-4 py-2 text-sm text-slate-400 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">取消</button><button onclick="App.createKey()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg'+(!this.newKeyName||this.loading.createKey?' opacity-40 cursor-not-allowed':'')+' flex items-center gap-2">'+(this.loading.createKey?sp:'')+'<span>'+((this.loading.createKey)?'创建中...':'创建')+'</span></button></div></div></div>'}
else if(name==='generatedKey'){var ge=(this.generatedKey||'').replace(/'/g,"\\'").replace(/"/g,'&quot;');h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-md p-6 shadow-2xl fade-in"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white flex items-center gap-2"><i data-lucide="check-circle" class="w-5 h-5 text-green-400"></i>密钥创建成功</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div class="p-4 rounded-xl bg-green-500/10 border border-green-500/20 mb-5"><p class="text-xs text-amber-300 mb-3 font-medium flex items-center gap-1.5"><i data-lucide="alert-triangle" class="w-4 h-4"></i>此密钥只显示一次,请妥善保存!</p><div class="flex items-start gap-2"><code class="text-green-300 text-sm font-mono break-all flex-1 select-all">'+(this.generatedKey||'')+'</code><button onclick="App.copyText(\''+ge+'\',\'密钥已复制\')" class="text-green-400 hover:text-green-300 p-1.5 rounded-lg hover:bg-green-500/10 flex-shrink-0"><i data-lucide="copy" class="w-4 h-4"></i></button></div></div><div class="flex justify-end"><button onclick="App.hideModal()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg">我已保存,关闭</button></div></div></div>'}
else if(name==='editKey'){h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-md p-6 shadow-2xl fade-in"><div class="flex items-center justify-between mb-6"><h3 class="text-lg font-semibold text-white">编辑密钥</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div><label class="block text-sm font-medium text-slate-300 mb-1.5">密钥名称</label><input id="edit-key-name-input" type="text" value="'+(this.editKeyName||'')+'" class="w-full px-4 py-2.5 rounded-lg text-sm"></div><div class="flex items-center justify-end gap-3 mt-6"><button onclick="App.hideModal()" class="px-4 py-2 text-sm text-slate-400 hover:text-white rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">取消</button><button onclick="App.saveEditKey()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg'+(!this.editKeyName||this.loading.editKey?' opacity-40 cursor-not-allowed':'')+'"><span>'+((this.loading.editKey)?'保存中...':'保存')+'</span></button></div></div></div>'}
else if(name==='testResult'){var tBg=tr.ok?'bg-green-500/10 border-green-500/20':'bg-red-500/10 border-red-500/20',tIc=tr.ok?'check-circle':'x-circle',tIcC=tr.ok?'text-green-400':'text-red-400',tLbC=tr.ok?'text-green-300':'text-red-300',tLb=tr.ok?'测试成功':'测试失败';h='<div class="fixed inset-0 z-[70] flex items-center justify-center p-4 modal-bg" onclick="if(event.target===this)App.hideModal()"><div class="glass rounded-2xl w-full max-w-md p-6 shadow-2xl fade-in"><div class="flex items-center justify-between mb-4"><h3 class="text-lg font-semibold text-white">测试结果</h3><button onclick="App.hideModal()" class="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-700/50"><i data-lucide="x" class="w-5 h-5"></i></button></div><div class="p-4 rounded-xl border '+tBg+'"><div class="flex items-center gap-2 mb-3"><i data-lucide="'+tIc+'" class="w-5 h-5 '+tIcC+'"></i><span class="'+tLbC+' font-medium text-sm">'+tLb+'</span></div><div class="text-xs text-slate-300 space-y-2">';if(tr.reply)h+='<div><span class="text-slate-400">回复内容:</span><p class="mt-1 bg-slate-900/50 p-2 rounded text-slate-200 break-words">'+tr.reply+'</p></div>';if(tr.duration)h+='<div><span class="text-slate-400">响应时间:</span><span class="text-white ml-1">'+tr.duration+'ms</span></div>';if(tr.error)h+='<div><span class="text-slate-400">错误信息:</span><p class="mt-1 bg-slate-900/50 p-2 rounded text-red-300 break-words">'+tr.error+'</p></div>';h+='</div></div><div class="flex justify-end mt-4"><button onclick="App.hideModal()" class="btn-primary text-white text-sm font-medium px-6 py-2 rounded-lg">关闭</button></div></div></div>'}
c.innerHTML=h;this.refreshIcons()},
spinnerHTML:'<svg class="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>',
hideModal:function(){var c=document.getElementById('modal-container');if(c)c.innerHTML=''},
renderToasts:function(){var c=document.getElementById('toast-container');if(!c)return;var h='';for(var i=0;i<this.toasts.length;i++){var t=this.toasts[i];if(!t.visible)continue;var bC='',ic='',iC='',tC='';if(t.type==='success'){bC='border-green-500/30';ic='check-circle';iC='text-green-400';tC='text-green-300'}else if(t.type==='error'){bC='border-red-500/30';ic='x-circle';iC='text-red-400';tC='text-red-300'}else if(t.type==='info'){bC='border-blue-500/30';ic='info';iC='text-blue-400';tC='text-blue-300'}else if(t.type==='warning'){bC='border-amber-500/30';ic='alert-triangle';iC='text-amber-400';tC='text-amber-300'}h+='<div class="glass rounded-xl px-5 py-3.5 flex items-center gap-3 shadow-2xl max-w-sm '+bC+'"><i data-lucide="'+ic+'" class="w-5 h-5 '+iC+' flex-shrink-0"></i><span class="text-sm font-medium '+tC+'">'+t.message+'</span></div>'}c.innerHTML=h;this.refreshIcons()},
};
</script>
<div id="sidebar-overlay" class="sidebar-overlay" onclick="App.closeSidebar()"></div>
<aside id="sidebar" class="glass-sidebar w-64 min-h-screen flex flex-col flex-shrink-0 fixed md:sticky top-0 h-screen z-50 sidebar-desktop">
<div class="px-5 py-5 border-b border-slate-700/40 flex items-center gap-3">
<div class="w-9 h-9 rounded-xl bg-indigo-500/20 border border-indigo-500/30 flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-indigo-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.455 2.456L21.75 6l-1.036.259a3.375 3.375 0 00-2.455 2.456z"/></svg>
</div>
<div class="overflow-hidden"><h1 class="text-white font-bold text-sm truncate">Gemini Gateway</h1><p class="text-slate-500 text-xs">管理后台</p></div>
</div>
<nav id="sidebar-nav" class="flex-1 py-4 px-3 space-y-1 overflow-y-auto scr"></nav>
<div class="px-4 py-4 border-t border-slate-700/40 space-y-2">
<div class="flex items-center gap-2 text-xs text-slate-500"><span class="w-2 h-2 rounded-full bg-green-400 pulse-dot flex-shrink-0"></span><span>系统运行中</span></div>
<a href="/logout" class="flex items-center gap-2 text-xs text-slate-500 hover:text-red-400 transition-colors px-1 py-1"><i data-lucide="log-out" class="w-3.5 h-3.5"></i><span>退出登录</span></a>
</div>
</aside>
<div class="flex-1 min-w-0 flex flex-col">
<header class="sticky top-0 z-40 border-b border-slate-700/40 px-4 py-3 flex items-center justify-between md:hidden sidebar-mobile-toggle" style="background:rgba(15,23,42,0.92);backdrop-filter:blur(12px)">
<button type="button" onclick="App.toggleSidebar()" class="text-slate-400 hover:text-white p-1"><i data-lucide="menu" class="w-6 h-6"></i></button>
<span id="mobile-page-label" class="text-white font-semibold text-sm"></span>
<a href="/logout" class="text-slate-400 hover:text-red-400 p-1"><i data-lucide="log-out" class="w-5 h-5"></i></a>
</header>
<header class="hidden md:flex sticky top-0 z-40 border-b border-slate-700/40 px-6 py-3 items-center justify-between" style="background:rgba(15,23,42,0.92);backdrop-filter:blur(12px)">
<h2 id="desktop-page-label" class="text-white font-semibold text-lg"></h2>
<div class="flex items-center gap-3">
<span id="desktop-clock" class="text-xs text-slate-500"></span>
<button type="button" onclick="App.refresh()" class="btn-ghost text-slate-400 hover:text-white p-1.5 rounded-lg border border-slate-700/50" title="刷新数据"><i data-lucide="refresh-cw" class="w-4 h-4"></i></button>
</div>
</header>
<main id="page-content" class="flex-1 p-4 md:p-6 overflow-y-auto scr"></main>
</div>
<div id="modal-container"></div>
<div id="toast-container" class="fixed bottom-5 right-5 z-[100] space-y-2 toast-container"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
App.boot();
if (window.lucide) {
lucide.createIcons();
var observer = new MutationObserver(function() { lucide.createIcons(); });
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(function() { observer.disconnect(); }, 30000);
}
});
</script>
</body>
</html>