style(webui): refine UI polish and enhance component interactions
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: "<strong>Disabled accounts</strong> 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: "<strong>已禁用的账号</strong>不会用于请求路由,但仍保留在配置中。仪表盘统计数据仅包含已启用的账号。",
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user