新增暗黑风格

This commit is contained in:
Von committed 2026-03-11 00:26:55 +08:00
1 parent 009c86e089
commit d06ba0f438
7 files changed
+378 -16

No files matched your search

+116
View File
@@ -24,6 +24,27 @@
--transition: 180ms ease;
}
:root[data-theme="dark"] {
--bg-page: #0b1220;
--surface: #111b2e;
--surface-soft: #0f1728;
--surface-hover: #17233a;
--text-primary: #e6edf7;
--text-secondary: #a5b4cc;
--text-muted: #93a4c2;
--line: #24324c;
--line-strong: #314665;
--primary: #78a7d8;
--primary-hover: #8ab4de;
--primary-active: #6798cb;
--danger: #e0828f;
--danger-hover: #d46e7c;
--success: #6ab79f;
--focus: rgba(120, 167, 216, 0.3);
--shadow-soft: 0 10px 28px rgba(0, 0, 0, 0.38);
--shadow-card: 0 4px 14px rgba(0, 0, 0, 0.32);
}
* {
margin: 0;
padding: 0;
@@ -66,6 +87,10 @@ body.app-page {
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
}
:root[data-theme="dark"] .app-page .header {
background: linear-gradient(180deg, #121d31 0%, #0f1728 100%);
}
.app-page .header-title h1 {
margin: 0;
font-size: 28px;
@@ -102,6 +127,10 @@ body.app-page {
white-space: nowrap;
}
:root[data-theme="dark"] .app-page .header-user {
background: #0d1627;
}
.app-page .header-nav {
display: inline-flex;
align-items: center;
@@ -112,6 +141,10 @@ body.app-page {
background: #fff;
}
:root[data-theme="dark"] .app-page .header-nav {
background: #0d1627;
}
.app-page .header-nav-link {
display: inline-flex;
align-items: center;
@@ -145,6 +178,11 @@ body.app-page {
background: #fef5f6;
}
:root[data-theme="dark"] .app-page .header-nav-link.header-nav-link-logout {
background: #321b24;
border-color: #5c2d3b;
}
.app-page .header-nav-link.header-nav-link-logout:hover {
color: #fff;
border-color: var(--danger-hover);
@@ -219,6 +257,14 @@ body.app-page {
transition: border-color var(--transition), box-shadow var(--transition), background-color var(--transition);
}
:root[data-theme="dark"] .app-page .form-group input,
:root[data-theme="dark"] .app-page .form-group textarea,
:root[data-theme="dark"] .app-page .form-control,
:root[data-theme="dark"] .app-page .quick-jump input,
:root[data-theme="dark"] .app-page .page-size-controls select {
background: #0d1627;
}
.app-page .form-group textarea {
min-height: 82px;
padding-top: 10px;
@@ -281,6 +327,10 @@ body.app-page {
transition: border-color var(--transition), box-shadow var(--transition), background-color var(--transition);
}
:root[data-theme="dark"] .app-page .custom-select-trigger {
background: #0d1627;
}
.app-page .custom-select-trigger::after {
content: "";
position: absolute;
@@ -327,6 +377,10 @@ body.app-page {
overflow-y: auto;
}
:root[data-theme="dark"] .app-page .custom-select-menu {
background: #0d1627;
}
.app-page .custom-select.open .custom-select-menu {
display: block;
}
@@ -368,6 +422,10 @@ body.app-page {
align-items: flex-end;
}
:root[data-theme="dark"] .app-page .table-footer {
background: #111a2c;
}
/* Shared column semantics: numeric columns stay on one line */
.app-page th.num-col,
.app-page td.num-col {
@@ -412,6 +470,10 @@ body.app-page {
transition: background-color var(--transition), border-color var(--transition), color var(--transition);
}
:root[data-theme="dark"] .app-page .page-nav-text {
background: #0d1627;
}
.app-page .page-nav-text:hover:not(.disabled) {
background: var(--surface-hover);
border-color: #b5c9de;
@@ -438,6 +500,60 @@ body.app-page {
white-space: nowrap;
}
:root[data-theme="dark"] .app-page .page-info,
:root[data-theme="dark"] .app-page .total-count {
background: #0d1627;
}
.app-page .theme-toggle {
width: 34px;
height: 34px;
border: 1px solid var(--line-strong);
border-radius: 9px;
background: #fff;
color: var(--text-secondary);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
}
.app-page .theme-toggle:hover {
background: var(--surface-hover);
border-color: #b5c9de;
color: var(--text-primary);
}
.app-page .theme-toggle:focus-visible {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--focus);
}
.app-page .theme-toggle svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
}
.app-page .theme-icon-moon {
display: none;
}
:root[data-theme="dark"] .app-page .theme-toggle {
background: #0d1627;
}
:root[data-theme="dark"] .app-page .theme-icon-sun {
display: none;
}
:root[data-theme="dark"] .app-page .theme-icon-moon {
display: inline;
}
.app-page .quick-jump,
.app-page .page-size-controls {
display: flex;
+80
View File
@@ -20,6 +20,10 @@
background: var(--surface);
}
:root[data-theme="dark"] .dashboard-page .filters-section {
background: #0f1728;
}
.dashboard-page .filters {
display: flex;
gap: 14px;
@@ -65,6 +69,26 @@
transition: border-color var(--transition), box-shadow var(--transition), background-color var(--transition);
}
:root[data-theme="dark"] .dashboard-page .filter-group input,
:root[data-theme="dark"] .dashboard-page .filter-group select {
background: #0d1627;
border-color: var(--line-strong);
color: var(--text-primary);
}
:root[data-theme="dark"] .dashboard-page .filter-group input::placeholder {
color: #8ea0be;
}
:root[data-theme="dark"] .dashboard-page .filter-group input[type="date"] {
color-scheme: dark;
}
:root[data-theme="dark"] .dashboard-page .filter-group input[type="date"]::-webkit-calendar-picker-indicator {
filter: invert(85%) sepia(7%) saturate(445%) hue-rotate(185deg) brightness(96%) contrast(92%);
opacity: 0.95;
}
.dashboard-page .filter-group input:hover,
.dashboard-page .filter-group select:hover {
border-color: #b9cbde;
@@ -86,6 +110,11 @@
box-shadow: var(--shadow-card);
}
:root[data-theme="dark"] .dashboard-page .stats-container,
:root[data-theme="dark"] .dashboard-page .table-container {
background: #111b2e;
}
.dashboard-page .stats-table,
.dashboard-page table {
width: 100%;
@@ -109,6 +138,11 @@
overflow-wrap: anywhere;
}
:root[data-theme="dark"] .dashboard-page .stats-table th,
:root[data-theme="dark"] .dashboard-page th {
background: #15233b;
}
.dashboard-page .stats-table td,
.dashboard-page td {
padding: 13px 14px;
@@ -120,22 +154,63 @@
background: #fff;
}
:root[data-theme="dark"] .dashboard-page .stats-table td,
:root[data-theme="dark"] .dashboard-page td {
background: #111b2e;
}
.dashboard-page .stats-table tbody tr:nth-child(even) td,
.dashboard-page tbody tr:nth-child(even) td {
background: #fbfdff;
}
:root[data-theme="dark"] .dashboard-page .stats-table tbody tr:nth-child(even) td,
:root[data-theme="dark"] .dashboard-page tbody tr:nth-child(even) td {
background: #122038;
}
.dashboard-page .stats-table tbody tr:hover td,
.dashboard-page tbody tr:hover td {
background: #f2f7fd;
}
:root[data-theme="dark"] .dashboard-page .stats-table tbody tr:hover td,
:root[data-theme="dark"] .dashboard-page tbody tr:hover td {
background: #1a2b45;
}
.dashboard-page .stats-total-row td {
font-weight: 700;
color: var(--text-primary);
background: #eff5fc;
}
:root[data-theme="dark"] .dashboard-page .stats-total-row td {
background: #1a2a44;
}
.dashboard-page .ip-address {
display: inline-flex;
align-items: center;
border-radius: 9px;
border: 1px solid #c8d8ea;
background: linear-gradient(180deg, #f9fcff 0%, #f1f6fc 100%);
color: #22456f;
padding: 4px 10px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
font-family: "Cascadia Mono", "Consolas", monospace;
white-space: nowrap;
line-height: 1.1;
}
:root[data-theme="dark"] .dashboard-page .ip-address {
border-color: #2b4469;
background: linear-gradient(180deg, #112038 0%, #0d1a30 100%);
color: #a7c8f0;
}
.dashboard-page .loading,
.dashboard-page .empty-state,
.dashboard-page .error {
@@ -150,6 +225,11 @@
background: #fef2f2;
}
:root[data-theme="dark"] .dashboard-page .error {
color: #ffb4bf;
background: #3a1f2b;
}
.dashboard-page .table-footer {
padding: 14px 20px;
}
+19
View File
@@ -13,6 +13,13 @@ body.login-page {
color: var(--text-primary);
}
:root[data-theme="dark"] body.login-page {
background:
radial-gradient(1100px 620px at -10% -15%, #111d34 0%, transparent 60%),
radial-gradient(980px 640px at 100% 0%, #15223b 0%, transparent 62%),
var(--bg-page);
}
.login-page .login-shell {
width: 100%;
max-width: 420px;
@@ -23,12 +30,20 @@ body.login-page {
overflow: hidden;
}
:root[data-theme="dark"] .login-page .login-shell {
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.4);
}
.login-page .login-head {
padding: 22px 24px;
border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
}
:root[data-theme="dark"] .login-page .login-head {
background: linear-gradient(180deg, #121d31 0%, #0f1728 100%);
}
.login-page .login-head h1 {
margin: 0;
font-size: 1.38rem;
@@ -65,6 +80,10 @@ body.login-page {
transition: border-color var(--transition), box-shadow var(--transition), background-color var(--transition);
}
:root[data-theme="dark"] .login-page .form-control {
background: #0d1627;
}
.login-page .form-control:hover {
border-color: #b9cbde;
background: #fcfeff;
+72 -8
View File
@@ -28,6 +28,10 @@
background: #fff;
}
:root[data-theme="dark"] .users-page .users-section {
background: #111b2e;
}
.users-page .users-section h2 {
margin: 0 0 14px;
font-size: 18px;
@@ -95,6 +99,10 @@
overflow-wrap: anywhere;
}
:root[data-theme="dark"] .users-page .users-table th {
background: #15233b;
}
.users-page .users-table th.sortable-th {
padding: 0;
}
@@ -134,26 +142,46 @@
background: #fff;
}
:root[data-theme="dark"] .users-page .users-table td {
background: #111b2e;
}
.users-page .users-table tbody tr:nth-child(even) td {
background: #fbfdff;
}
:root[data-theme="dark"] .users-page .users-table tbody tr:nth-child(even) td {
background: #122038;
}
.users-page .users-table tbody tr:hover td {
background: #f2f7fd;
}
:root[data-theme="dark"] .users-page .users-table tbody tr:hover td {
background: #1a2b45;
}
.users-page .ip-address {
display: inline-flex;
align-items: center;
border-radius: 8px;
border: 1px solid var(--line);
background: #f8fbff;
color: var(--text-secondary);
padding: 3px 8px;
font-size: 12px;
border-radius: 9px;
border: 1px solid #c8d8ea;
background: linear-gradient(180deg, #f9fcff 0%, #f1f6fc 100%);
color: #22456f;
padding: 4px 10px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
font-family: "Cascadia Mono", "Consolas", monospace;
white-space: normal;
overflow-wrap: anywhere;
white-space: nowrap;
line-height: 1.1;
}
:root[data-theme="dark"] .users-page .ip-address {
border-color: #2b4469;
background: linear-gradient(180deg, #112038 0%, #0d1a30 100%);
color: #a7c8f0;
}
.users-page .action-buttons {
@@ -179,6 +207,11 @@
border-color: #d6e4f1;
}
:root[data-theme="dark"] .users-page .btn-edit {
background: #1a2d49;
border-color: #28466d;
}
.users-page .btn-edit:hover {
color: #fff;
background: var(--primary);
@@ -191,6 +224,11 @@
border-color: #f3d7dc;
}
:root[data-theme="dark"] .users-page .btn-delete {
background: #3a1f2b;
border-color: #5c2d3b;
}
.users-page .btn-delete:hover {
color: #fff;
background: var(--danger-hover);
@@ -221,6 +259,11 @@
transition: background-color var(--transition), border-color var(--transition);
}
:root[data-theme="dark"] .users-page .slider {
background: #1a2b45;
border-color: #2d4163;
}
.users-page .slider::before {
content: "";
position: absolute;
@@ -234,6 +277,10 @@
transition: transform var(--transition);
}
:root[data-theme="dark"] .users-page .slider::before {
background: #e8eef8;
}
.users-page .switch input:checked + .slider {
background: #e7f5ef;
border-color: #b7dccf;
@@ -261,18 +308,35 @@
background: #fbfdff;
}
:root[data-theme="dark"] .users-page .loading,
:root[data-theme="dark"] .users-page .empty-state {
background: #0f1728;
}
.users-page .error {
color: #9b2c2c;
border: 1px solid #f1d4d8;
background: #fef2f2;
}
:root[data-theme="dark"] .users-page .error {
color: #ffb4bf;
border-color: #5c2d3b;
background: #3a1f2b;
}
.users-page .success {
color: #24604d;
border: 1px solid #cce4dc;
background: #edf9f4;
}
:root[data-theme="dark"] .users-page .success {
background: #193229;
border-color: #2f5e4e;
color: #a9e2cb;
}
.users-page .table-footer {
padding: 14px 4px 0;
margin-top: 14px;
+43 -5
View File
@@ -2,7 +2,14 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
const theme = savedTheme === 'dark' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<title>LLM使用统计</title>
<link rel="stylesheet" href="/static/vendor/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/admin-base.css">
@@ -17,6 +24,15 @@
<p>实时监控 API 请求和 Token 消耗</p>
</div>
<div class="header-actions">
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Toggle theme" title="Toggle theme">
<svg class="theme-icon-sun" viewBox="0 0 24 24" stroke-width="1.8">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.93 4.93l2.12 2.12M16.95 16.95l2.12 2.12M19.07 4.93l-2.12 2.12M7.05 16.95l-2.12 2.12"></path>
</svg>
<svg class="theme-icon-moon" viewBox="0 0 24 24" stroke-width="1.8">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 1 0 9.8 9.8z"></path>
</svg>
</button>
<nav class="header-nav" aria-label="Main navigation">
<a class="header-nav-link active" href="/">统计概览</a>
<a class="header-nav-link" href="/users">用户管理</a>
@@ -146,6 +162,27 @@
activeCustomSelect = null;
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
themeToggle.setAttribute('title', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
}
}
function initThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) return;
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
applyTheme(currentTheme);
themeToggle.addEventListener('click', function() {
const nextTheme = (document.documentElement.getAttribute('data-theme') || 'light') === 'dark' ? 'light' : 'dark';
applyTheme(nextTheme);
});
}
function normalizeNumber(value) {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
@@ -258,8 +295,9 @@
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
setupCustomSelect('username');
document.addEventListener('DOMContentLoaded', function() {
initThemeToggle();
setupCustomSelect('username');
setupCustomSelect('requestModel');
setupCustomSelect('pageSizeSelect');
setDefaultDateRange();
@@ -329,7 +367,7 @@
html += `
<tr>
<td>${stat.ip_address || '-'}</td>
<td>${stat.ip_address ? `<span class="ip-address">${stat.ip_address}</span>` : '-'}</td>
<td>${stat.username || '-'}</td>
<td>${stat.request_model || '-'}</td>
<td>${stat.response_model || '-'}</td>
@@ -448,7 +486,7 @@
html += `
<tr>
<td>${log.ip_address || '-'}</td>
<td>${log.ip_address ? `<span class="ip-address">${log.ip_address}</span>` : '-'}</td>
<td>${log.username || '-'}</td>
<td>${log.request_model || '-'}</td>
<td>${log.response_model || '-'}</td>
+7
View File
@@ -3,6 +3,13 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
const theme = savedTheme === 'dark' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<title>管理员登录 - LLM Proxy</title>
<link rel="stylesheet" href="/static/vendor/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/admin-base.css">
+41 -3
View File
@@ -2,7 +2,14 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>
(function() {
const savedTheme = localStorage.getItem('theme');
const theme = savedTheme === 'dark' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
<title>用户管理</title>
<link rel="stylesheet" href="/static/vendor/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/admin-base.css">
@@ -16,6 +23,15 @@
<p>统一管理白名单用户与 IP</p>
</div>
<div class="header-actions">
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Toggle theme" title="Toggle theme">
<svg class="theme-icon-sun" viewBox="0 0 24 24" stroke-width="1.8">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.93 4.93l2.12 2.12M16.95 16.95l2.12 2.12M19.07 4.93l-2.12 2.12M7.05 16.95l-2.12 2.12"></path>
</svg>
<svg class="theme-icon-moon" viewBox="0 0 24 24" stroke-width="1.8">
<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 1 0 9.8 9.8z"></path>
</svg>
</button>
<nav class="header-nav" aria-label="Main navigation">
<a class="header-nav-link" href="/">统计概览</a>
<a class="header-nav-link active" href="/users">用户管理</a>
@@ -124,6 +140,27 @@
activeCustomSelect = null;
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
themeToggle.setAttribute('title', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
}
}
function initThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) return;
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
applyTheme(currentTheme);
themeToggle.addEventListener('click', function() {
const nextTheme = (document.documentElement.getAttribute('data-theme') || 'light') === 'dark' ? 'light' : 'dark';
applyTheme(nextTheme);
});
}
function normalizeNumber(value) {
const n = Number(value);
return Number.isFinite(n) ? n : 0;
@@ -236,8 +273,9 @@
}
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
setupCustomSelect('pageSizeSelect');
document.addEventListener('DOMContentLoaded', function() {
initThemeToggle();
setupCustomSelect('pageSizeSelect');
loadUsers();
document.getElementById('pageSizeSelect').value = pageSize;
syncCustomSelectValue('pageSizeSelect');