From e909ea6fe3f71658ee228051c2a98b2ac6ccc437 Mon Sep 17 00:00:00 2001 From: Wha1eChai Date: Fri, 9 Jan 2026 07:54:50 +0800 Subject: [PATCH] style(webui): refine UI polish and enhance component interactions --- public/app.js | 44 +++++- public/css/style.css | 187 ++++++++++++++++------- public/index.html | 48 +++++- public/js/components/account-manager.js | 42 +++++- public/js/components/dashboard.js | 16 +- public/js/components/logs-viewer.js | 21 ++- public/js/data-store.js | 8 +- public/js/store.js | 56 +++++++ public/views/accounts.html | 161 ++++++++++++++++---- public/views/dashboard.html | 117 ++++++++++----- public/views/logs.html | 35 +++-- public/views/models.html | 135 +++++++++++------ public/views/settings.html | 191 ++++++++++++++++-------- 13 files changed, 808 insertions(+), 253 deletions(-) diff --git a/public/app.js b/public/app.js index e240dd1..9b055cd 100644 --- a/public/app.js +++ b/public/app.js @@ -114,16 +114,47 @@ document.addEventListener('alpine:init', () => { Alpine.store('global').showToast(Alpine.store('global').t('oauthInProgress'), 'info'); // Open OAuth window - window.open(data.url, 'google_oauth', 'width=600,height=700,scrollbars=yes'); + const oauthWindow = window.open(data.url, 'google_oauth', 'width=600,height=700,scrollbars=yes'); // Poll for account changes instead of relying on postMessage // (since OAuth callback is now on port 51121, not this server) const initialAccountCount = Alpine.store('data').accounts.length; let pollCount = 0; const maxPolls = 60; // 2 minutes (2 second intervals) + let cancelled = false; + + // Show progress modal + Alpine.store('global').oauthProgress = { + active: true, + current: 0, + max: maxPolls, + cancel: () => { + cancelled = true; + clearInterval(pollInterval); + Alpine.store('global').oauthProgress.active = false; + Alpine.store('global').showToast(Alpine.store('global').t('oauthCancelled'), 'info'); + if (oauthWindow && !oauthWindow.closed) { + oauthWindow.close(); + } + } + }; const pollInterval = setInterval(async () => { + if (cancelled) { + clearInterval(pollInterval); + return; + } + pollCount++; + Alpine.store('global').oauthProgress.current = pollCount; + + // Check if OAuth window was closed manually + if (oauthWindow && oauthWindow.closed && !cancelled) { + clearInterval(pollInterval); + Alpine.store('global').oauthProgress.active = false; + Alpine.store('global').showToast(Alpine.store('global').t('oauthWindowClosed'), 'warning'); + return; + } // Refresh account list await Alpine.store('data').fetchData(); @@ -132,6 +163,8 @@ document.addEventListener('alpine:init', () => { const currentAccountCount = Alpine.store('data').accounts.length; if (currentAccountCount > initialAccountCount) { clearInterval(pollInterval); + Alpine.store('global').oauthProgress.active = false; + const actionKey = reAuthEmail ? 'reauthenticated' : 'added'; const action = Alpine.store('global').t(actionKey); const successfully = Alpine.store('global').t('successfully'); @@ -140,11 +173,20 @@ document.addEventListener('alpine:init', () => { 'success' ); document.getElementById('add_account_modal')?.close(); + + if (oauthWindow && !oauthWindow.closed) { + oauthWindow.close(); + } } // Stop polling after max attempts if (pollCount >= maxPolls) { clearInterval(pollInterval); + Alpine.store('global').oauthProgress.active = false; + Alpine.store('global').showToast( + Alpine.store('global').t('oauthTimeout'), + 'warning' + ); } }, 2000); // Poll every 2 seconds } else { diff --git a/public/css/style.css b/public/css/style.css index 781c4a1..919d80c 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,18 +1,34 @@ :root { + /* === Background Layers === */ --color-space-950: #09090b; --color-space-900: #0f0f11; --color-space-850: #121214; --color-space-800: #18181b; --color-space-border: #27272a; + + /* === Neon Accents (Full Saturation) === */ --color-neon-purple: #a855f7; --color-neon-green: #22c55e; --color-neon-cyan: #06b6d4; --color-neon-yellow: #eab308; --color-neon-red: #ef4444; - --color-text-main: #d1d5db; /* gray-300 */ - --color-text-dim: #71717a; /* zinc-400 */ - --color-text-muted: #6b7280; /* gray-500 */ - --color-text-bright: #ffffff; + + /* === Soft Neon (Reduced Saturation for Fills) === */ + --color-neon-purple-soft: #9333ea; + --color-neon-green-soft: #16a34a; + --color-neon-cyan-soft: #0891b2; + + /* === Text Hierarchy (WCAG AA Compliant) === */ + --color-text-primary: #ffffff; /* Emphasis: Titles, Key Numbers */ + --color-text-secondary: #d4d4d8; /* Content: Body Text (zinc-300) */ + --color-text-tertiary: #a1a1aa; /* Metadata: Timestamps, Labels (zinc-400) */ + --color-text-quaternary: #71717a; /* Subtle: Decorative (zinc-500) */ + + /* === Legacy Aliases (Backward Compatibility) === */ + --color-text-main: var(--color-text-secondary); + --color-text-dim: var(--color-text-tertiary); + --color-text-muted: var(--color-text-tertiary); + --color-text-bright: var(--color-text-primary); /* Gradient Accents */ --color-green-400: #4ade80; @@ -44,20 +60,25 @@ /* Custom Scrollbar */ ::-webkit-scrollbar { - width: 6px; - height: 6px; + width: 8px; + height: 8px; } ::-webkit-scrollbar-track { - background: transparent; + background: rgba(9, 9, 11, 0.3); + border-radius: 4px; } ::-webkit-scrollbar-thumb { - @apply bg-space-800 rounded-full; + background: linear-gradient(180deg, #27272a 0%, #18181b 100%); + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.05); + transition: background 0.2s ease; } ::-webkit-scrollbar-thumb:hover { - @apply bg-space-border; + background: linear-gradient(180deg, #3f3f46 0%, #27272a 100%); + border-color: rgba(168, 85, 247, 0.3); } /* Animations */ @@ -82,9 +103,23 @@ /* Utility */ .glass-panel { - background: theme('colors.space.900 / 70%'); + background: linear-gradient(135deg, + rgba(15, 15, 17, 0.75) 0%, + rgba(18, 18, 20, 0.70) 100% + ); backdrop-filter: blur(12px); - border: 1px solid theme('colors.white / 8%'); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.02) inset, + 0 4px 24px rgba(0, 0, 0, 0.4); + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.glass-panel:hover { + border-color: rgba(255, 255, 255, 0.12); + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.04) inset, + 0 8px 32px rgba(0, 0, 0, 0.5); } .nav-item.active { @@ -128,23 +163,77 @@ /* Refactored Global Utilities */ /* -------------------------------------------------------------------------- */ -/* View Containers */ -.view-container { - @apply mx-auto p-6 space-y-6 animate-fade-in; - /* Responsive max-width: use most of screen on small displays, - but cap at 1600px on large displays for reading comfort */ - max-width: min(95%, 1600px); +/* Standard Layout Constants */ +:root { + --view-padding: 2rem; /* 32px - Standard Padding */ + --view-gap: 1.5rem; /* 24px - Standard component gap */ + --card-radius: 0.75rem; /* 12px */ } -/* Section Headers */ -.section-header { - @apply flex justify-between items-center mb-6; +@media (max-width: 768px) { + :root { + --view-padding: 1rem; + --view-gap: 1rem; + } } -.section-title { + +/* Base View Container */ +.view-container { + @apply mx-auto w-full animate-fade-in flex flex-col; + padding: var(--view-padding); + gap: var(--view-gap); + min-height: calc(100vh - 56px); /* Align with navbar height */ + max-width: 1400px; + scrollbar-gutter: stable; +} + +/* Specialized container for data-heavy pages (Logs) */ +.view-container-full { + @apply w-full animate-fade-in flex flex-col; + padding: var(--view-padding); + gap: var(--view-gap); + min-height: calc(100vh - 56px); + max-width: 100%; +} + +/* Centered container for form-heavy pages (Settings/Accounts) */ +.view-container-centered { + @apply mx-auto w-full animate-fade-in flex flex-col; + padding: var(--view-padding); + gap: var(--view-gap); + min-height: calc(100vh - 56px); + max-width: 900px; /* Comfortable reading width for forms */ +} + +/* Standard Section Header */ +.view-header { + @apply flex flex-col md:flex-row md:items-end justify-between mb-2; + gap: 1rem; +} + +.view-header-title { + @apply flex flex-col; +} + +.view-header-title h2 { @apply text-2xl font-bold text-white tracking-tight; } -.section-desc { - @apply text-gray-500 text-sm; + +.view-header-title p { + @apply text-sm text-gray-500 mt-1; +} + +.view-header-actions { + @apply flex items-center gap-3; +} + +/* Standard Card Panel */ +.view-card { + @apply glass-panel rounded-xl p-6 border border-space-border/50 relative overflow-hidden; +} + +.view-card-header { + @apply flex items-center justify-between mb-4 pb-4 border-b border-space-border/30; } /* Component Unification */ @@ -155,77 +244,73 @@ @apply bg-space-900/50 text-gray-500 font-mono text-xs uppercase border-b border-space-border; } .standard-table tbody tr { - @apply hover:bg-white/5 transition-colors border-b border-space-border/30 last:border-0; + @apply transition-all duration-200 border-b border-space-border/30 last:border-0; } -/* Custom Range Slider */ +.standard-table tbody tr:hover { + background: linear-gradient(90deg, + rgba(255, 255, 255, 0.03) 0%, + rgba(255, 255, 255, 0.05) 50%, + rgba(255, 255, 255, 0.03) 100% + ); + border-color: rgba(255, 255, 255, 0.08); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +/* Custom Range Slider - Simplified */ .custom-range { -webkit-appearance: none; appearance: none; width: 100%; - height: 6px; + height: 4px; background: var(--color-space-800); border-radius: 999px; outline: none; cursor: pointer; - position: relative; - background-image: linear-gradient(to right, var(--range-color) 0%, var(--range-color) 100%); - background-repeat: no-repeat; - background-size: 0% 100%; } .custom-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; - width: 18px; - height: 18px; + width: 14px; + height: 14px; border-radius: 50%; - background: #ffffff; - box-shadow: 0 0 10px var(--range-color-glow); + background: var(--range-color, var(--color-neon-purple)); cursor: pointer; - margin-top: -6px; - transition: transform 0.1s ease, box-shadow 0.2s ease; + transition: transform 0.1s ease; } .custom-range::-webkit-slider-thumb:hover { - transform: scale(1.1); - box-shadow: 0 0 15px var(--range-color-glow); + transform: scale(1.15); } .custom-range::-moz-range-thumb { - width: 18px; - height: 18px; + width: 14px; + height: 14px; border: none; border-radius: 50%; - background: #ffffff; - box-shadow: 0 0 10px var(--range-color-glow); + background: var(--range-color, var(--color-neon-purple)); cursor: pointer; - transition: transform 0.1s ease, box-shadow 0.2s ease; + transition: transform 0.1s ease; } .custom-range::-moz-range-thumb:hover { - transform: scale(1.1); - box-shadow: 0 0 15px var(--range-color-glow); + transform: scale(1.15); } /* Color Variants */ .custom-range-purple { --range-color: var(--color-neon-purple); - --range-color-glow: rgba(168, 85, 247, 0.5); } .custom-range-green { --range-color: var(--color-neon-green); - --range-color-glow: rgba(34, 197, 94, 0.5); } .custom-range-cyan { --range-color: var(--color-neon-cyan); - --range-color-glow: rgba(6, 182, 212, 0.5); } .custom-range-yellow { --range-color: var(--color-neon-yellow); - --range-color-glow: rgba(234, 179, 8, 0.5); } .custom-range-accent { - --range-color: var(--color-neon-cyan); /* Default accent to cyan if needed, or match DaisyUI */ - --range-color-glow: rgba(6, 182, 212, 0.5); + --range-color: var(--color-neon-cyan); } diff --git a/public/index.html b/public/index.html index 12d3d50..a21305f 100644 --- a/public/index.html +++ b/public/index.html @@ -225,32 +225,32 @@ -
+
+ class="w-full">
+ class="w-full">
+ x-transition:enter-start="fade-enter-from" class="w-full h-full">
+ class="w-full">
+ class="w-full"> @@ -309,6 +309,42 @@ + + + + + diff --git a/public/js/components/account-manager.js b/public/js/components/account-manager.js index cb564b3..fd5c043 100644 --- a/public/js/components/account-manager.js +++ b/public/js/components/account-manager.js @@ -5,6 +5,36 @@ window.Components = window.Components || {}; window.Components.accountManager = () => ({ + searchQuery: '', + deleteTarget: '', + + get filteredAccounts() { + const accounts = Alpine.store('data').accounts || []; + if (!this.searchQuery || this.searchQuery.trim() === '') { + return accounts; + } + + const query = this.searchQuery.toLowerCase().trim(); + return accounts.filter(acc => { + return acc.email.toLowerCase().includes(query) || + (acc.projectId && acc.projectId.toLowerCase().includes(query)) || + (acc.source && acc.source.toLowerCase().includes(query)); + }); + }, + + formatEmail(email) { + if (!email || email.length <= 40) return email; + + const [user, domain] = email.split('@'); + if (!domain) return email; + + // Preserve domain integrity, truncate username if needed + if (user.length > 20) { + return `${user.substring(0, 10)}...${user.slice(-5)}@${domain}`; + } + return email; + }, + async refreshAccount(email) { const store = Alpine.store('global'); store.showToast(store.t('refreshingAccount', { email }), 'info'); @@ -88,10 +118,16 @@ window.Components.accountManager = () => ({ } }, - async deleteAccount(email) { + confirmDeleteAccount(email) { + this.deleteTarget = email; + document.getElementById('delete_account_modal').showModal(); + }, + + async executeDelete() { + const email = this.deleteTarget; const store = Alpine.store('global'); - if (!confirm(store.t('confirmDelete'))) return; const password = store.webuiPassword; + try { const { response, newPassword } = await window.utils.request(`/api/accounts/${encodeURIComponent(email)}`, { method: 'DELETE' }, password); if (newPassword) store.webuiPassword = newPassword; @@ -100,6 +136,8 @@ window.Components.accountManager = () => ({ if (data.status === 'ok') { store.showToast(store.t('deletedAccount', { email }), 'success'); Alpine.store('data').fetchData(); + document.getElementById('delete_account_modal').close(); + this.deleteTarget = ''; } else { store.showToast(data.error || store.t('deleteFailed'), 'error'); } diff --git a/public/js/components/dashboard.js b/public/js/components/dashboard.js index 3ef00df..4a0dc74 100644 --- a/public/js/components/dashboard.js +++ b/public/js/components/dashboard.js @@ -464,7 +464,9 @@ window.Components.dashboard = () => ({ createDataset(label, data, color, ctx) { const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200); - gradient.addColorStop(0, this.hexToRgba(color, 0.3)); + // Reduced opacity from 0.3 to 0.12 for less visual noise + gradient.addColorStop(0, this.hexToRgba(color, 0.12)); + gradient.addColorStop(0.6, this.hexToRgba(color, 0.05)); gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); return { @@ -472,12 +474,14 @@ window.Components.dashboard = () => ({ data, borderColor: color, backgroundColor: gradient, - borderWidth: 2, - tension: 0.4, + borderWidth: 2.5, // Slightly thicker line for better visibility + tension: 0.35, // Smoother curves fill: true, - pointRadius: 3, - pointHoverRadius: 5, - pointBackgroundColor: color + pointRadius: 2.5, + pointHoverRadius: 6, + pointBackgroundColor: color, + pointBorderColor: 'rgba(9, 9, 11, 0.8)', + pointBorderWidth: 1.5 }; }, diff --git a/public/js/components/logs-viewer.js b/public/js/components/logs-viewer.js index 403968c..28c24a5 100644 --- a/public/js/components/logs-viewer.js +++ b/public/js/components/logs-viewer.js @@ -18,15 +18,28 @@ window.Components.logsViewer = () => ({ }, get filteredLogs() { - const query = this.searchQuery.toLowerCase(); + const query = this.searchQuery.trim(); + if (!query) { + return this.logs.filter(log => this.filters[log.level]); + } + + // Try regex first, fallback to plain text search + let matcher; + try { + const regex = new RegExp(query, 'i'); + matcher = (msg) => regex.test(msg); + } catch (e) { + // Invalid regex, fallback to case-insensitive string search + const lowerQuery = query.toLowerCase(); + matcher = (msg) => msg.toLowerCase().includes(lowerQuery); + } + return this.logs.filter(log => { // Level Filter if (!this.filters[log.level]) return false; // Search Filter - if (query && !log.message.toLowerCase().includes(query)) return false; - - return true; + return matcher(log.message); }); }, diff --git a/public/js/data-store.js b/public/js/data-store.js index 36414de..5b80947 100644 --- a/public/js/data-store.js +++ b/public/js/data-store.js @@ -76,17 +76,17 @@ document.addEventListener('alpine:init', () => { const config = this.modelConfig[modelId] || {}; const family = this.getModelFamily(modelId); - // Visibility Logic for Models Tab (quotaRows): - // 1. If explicitly hidden via config, always hide + // Visibility Logic for Models Page (quotaRows): + // 1. If explicitly hidden via config, ALWAYS hide (clean interface) // 2. If no config, default 'unknown' families to HIDDEN // 3. Known families (Claude/Gemini) default to VISIBLE - // Note: showHiddenModels toggle is for Settings page only, NOT here + // Note: To manage hidden models, use Settings → Models tab let isHidden = config.hidden; if (isHidden === undefined) { isHidden = (family === 'other' || family === 'unknown'); } - // Models Tab: ALWAYS hide hidden models (no toggle check) + // Models Page: ALWAYS hide hidden models (use Settings to restore) if (isHidden) return; // Filters diff --git a/public/js/store.js b/public/js/store.js index 24a99a1..49ff770 100644 --- a/public/js/store.js +++ b/public/js/store.js @@ -219,6 +219,30 @@ document.addEventListener('alpine:init', () => { cancel: "Cancel", passwordsNotMatch: "Passwords do not match", passwordTooShort: "Password must be at least 6 characters", + // Dashboard drill-down + clickToViewAllAccounts: "Click to view all accounts", + clickToViewModels: "Click to view Models page", + clickToViewLimitedAccounts: "Click to view rate-limited accounts", + clickToFilterClaude: "Click to filter Claude models", + clickToFilterGemini: "Click to filter Gemini models", + // Accounts page + searchAccounts: "Search accounts...", + noAccountsYet: "No Accounts Yet", + noAccountsDesc: "Get started by adding a Google account via OAuth, or use the CLI command to import credentials.", + addFirstAccount: "Add Your First Account", + noSearchResults: "No accounts match your search", + clearSearch: "Clear Search", + disabledAccountsNote: "Disabled accounts will not be used for request routing but remain in the configuration. Dashboard statistics only include enabled accounts.", + dangerousOperation: "⚠️ Dangerous Operation", + confirmDeletePrompt: "Are you sure you want to delete account", + deleteWarning: "⚠️ This action cannot be undone. All configuration and historical records will be permanently deleted.", + // OAuth progress + oauthWaiting: "Waiting for OAuth authorization...", + oauthWaitingDesc: "Please complete the authentication in the popup window. This may take up to 2 minutes.", + oauthCancelled: "OAuth authorization cancelled", + oauthTimeout: "⏱️ OAuth authorization timed out. Please try again.", + oauthWindowClosed: "OAuth window was closed. Authorization may be incomplete.", + cancelOAuth: "Cancel", }, zh: { dashboard: "仪表盘", @@ -427,12 +451,44 @@ document.addEventListener('alpine:init', () => { cancel: "取消", passwordsNotMatch: "密码不匹配", passwordTooShort: "密码至少需要 6 个字符", + // Dashboard drill-down + clickToViewAllAccounts: "点击查看所有账号", + clickToViewModels: "点击查看模型页面", + clickToViewLimitedAccounts: "点击查看受限账号", + clickToFilterClaude: "点击筛选 Claude 模型", + clickToFilterGemini: "点击筛选 Gemini 模型", + // 账号页面 + searchAccounts: "搜索账号...", + noAccountsYet: "还没有添加任何账号", + noAccountsDesc: "点击上方的 \"添加节点\" 按钮通过 OAuth 添加 Google 账号,或者使用 CLI 命令导入凭证。", + addFirstAccount: "添加第一个账号", + noSearchResults: "没有找到匹配的账号", + clearSearch: "清除搜索", + disabledAccountsNote: "已禁用的账号不会用于请求路由,但仍保留在配置中。仪表盘统计数据仅包含已启用的账号。", + dangerousOperation: "⚠️ 危险操作", + confirmDeletePrompt: "确定要删除账号", + deleteWarning: "⚠️ 此操作不可撤销,账号的所有配置和历史记录将永久删除。", + // OAuth 进度 + oauthWaiting: "等待 OAuth 授权中...", + oauthWaitingDesc: "请在弹出窗口中完成认证。此过程最长可能需要 2 分钟。", + oauthCancelled: "已取消 OAuth 授权", + oauthTimeout: "⏱️ OAuth 授权超时,请重试。", + oauthWindowClosed: "OAuth 窗口已关闭,授权可能未完成。", + cancelOAuth: "取消", } }, // Toast Messages toast: null, + // OAuth Progress + oauthProgress: { + active: false, + current: 0, + max: 60, + cancel: null + }, + t(key, params = {}) { let str = this.translations[this.lang][key] || key; if (typeof str === 'string') { diff --git a/public/views/accounts.html b/public/views/accounts.html index f8ac8e1..c682aee 100644 --- a/public/views/accounts.html +++ b/public/views/accounts.html @@ -1,25 +1,40 @@
- -
-
-

+ +
+ +
+

Access Credentials -

-

+

+ Manage OAuth tokens and session states -

+
+ +
- -
- -
+ +
- - - - - - + + + + + -
EnabledIdentity (Email)SourceProject ID
EnabledIdentity (Email)Source Health Operations