/* global Vue, Chart, ElementPlus, ElementPlusLocaleZhCn, FileReader, document, localStorage, location, navigator, window */ const { createApp } = Vue; const app = createApp({ data() { return { isLoggedIn: false, authToken: localStorage.getItem('authToken'), activeTab: 'dashboard', // Toast 通知 toasts: [], toastIdCounter: 0, // 登录相关 loginForm: { username: '', password: '' }, loginLoading: false, loginError: '', // 标签页 tabs: [ { key: 'dashboard', name: '仪表板', icon: 'fas fa-tachometer-alt' }, { key: 'apiKeys', name: 'API Keys', icon: 'fas fa-key' }, { key: 'accounts', name: '账户管理', icon: 'fas fa-user-circle' }, { key: 'tutorial', name: '使用教程', icon: 'fas fa-graduation-cap' }, { key: 'settings', name: '其他设置', icon: 'fas fa-cogs' } ], // 教程系统选择 activeTutorialSystem: 'windows', tutorialSystems: [ { key: 'windows', name: 'Windows', icon: 'fab fa-windows' }, { key: 'macos', name: 'macOS', icon: 'fab fa-apple' }, { key: 'linux', name: 'Linux / WSL2', icon: 'fab fa-ubuntu' } ], // 模型统计 modelStats: [], modelStatsLoading: false, modelStatsPeriod: 'daily', // 数据 dashboardData: { totalApiKeys: 0, activeApiKeys: 0, totalAccounts: 0, activeAccounts: 0, todayRequests: 0, totalRequests: 0, todayTokens: 0, todayInputTokens: 0, todayOutputTokens: 0, totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheCreateTokens: 0, totalCacheReadTokens: 0, todayCacheCreateTokens: 0, todayCacheReadTokens: 0, systemRPM: 0, systemTPM: 0, systemStatus: '正常', uptime: 0 }, // 价格数据 costsData: { todayCosts: { totalCost: 0, formatted: { totalCost: '$0.000000' } }, totalCosts: { totalCost: 0, formatted: { totalCost: '$0.000000' } } }, // 仪表盘模型统计 dashboardModelStats: [], dashboardModelPeriod: 'daily', modelUsageChart: null, usageTrendChart: null, trendPeriod: 7, trendData: [], trendGranularity: 'day', // 新增:趋势图粒度(day/hour) // API Keys 使用趋势 apiKeysUsageTrendChart: null, apiKeysTrendData: { data: [], topApiKeys: [], totalApiKeys: 0 }, apiKeysTrendMetric: 'requests', // 'requests' 或 'tokens' - 默认显示请求次数 // 统一的日期筛选 dateFilter: { type: 'preset', // preset 或 custom preset: '7days', // today, 7days, 30days customStart: '', customEnd: '', customRange: null, // Element Plus日期范围选择器的值 presetOptions: [ { value: 'today', label: '今天', days: 1 }, { value: '7days', label: '近7天', days: 7 }, { value: '30days', label: '近30天', days: 30 } ] }, defaultTime: [ new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59), ], showDateRangePicker: false, // 日期范围选择器显示状态 dateRangeInputValue: '', // 日期范围显示文本 // API Keys apiKeys: [], apiKeysLoading: false, apiKeyStatsTimeRange: 'all', // API Key统计时间范围:all, 7days, monthly apiKeysSortBy: '', // 当前排序字段 apiKeysSortOrder: 'asc', // 排序顺序 'asc' 或 'desc' showCreateApiKeyModal: false, createApiKeyLoading: false, apiKeyForm: { name: '', tokenLimit: '', description: '', concurrencyLimit: '', rateLimitWindow: '', rateLimitRequests: '', claudeAccountId: '', geminiAccountId: '', permissions: 'all', // 'claude', 'gemini', 'all' enableModelRestriction: false, restrictedModels: [], modelInput: '', enableClientRestriction: false, allowedClients: [], expireDuration: '', // 过期时长选择 customExpireDate: '', // 自定义过期日期 expiresAt: null, // 实际的过期时间戳 dailyCostLimit: '' // 每日费用限制 }, apiKeyModelStats: {}, // 存储每个key的模型统计数据 expandedApiKeys: {}, // 跟踪展开的API Keys apiKeyModelPeriod: 'monthly', // API Key模型统计期间 // API Keys的日期筛选(每个API Key独立) apiKeyDateFilters: {}, // 存储每个API Key的独立日期筛选状态 apiKeyDateFilterDefaults: { type: 'preset', // preset 或 custom preset: '7days', // today, 7days, 30days customStart: '', customEnd: '', customRange: null, // Element Plus日期范围选择器的值 presetOptions: [ { value: 'today', label: '今天', days: 1 }, { value: '7days', label: '近7天', days: 7 }, { value: '30days', label: '近30天', days: 30 } ] }, // 新创建的API Key展示弹窗 showNewApiKeyModal: false, newApiKey: { key: '', name: '', description: '', showFullKey: false }, // API Key续期 showRenewApiKeyModal: false, renewApiKeyLoading: false, renewApiKeyForm: { id: '', name: '', currentExpiresAt: null, renewDuration: '30d', customExpireDate: '', newExpiresAt: null }, // 编辑API Key showEditApiKeyModal: false, editApiKeyLoading: false, editApiKeyForm: { id: '', name: '', tokenLimit: '', concurrencyLimit: '', rateLimitWindow: '', rateLimitRequests: '', claudeAccountId: '', geminiAccountId: '', permissions: 'all', enableModelRestriction: false, restrictedModels: [], modelInput: '', enableClientRestriction: false, allowedClients: [], dailyCostLimit: '' }, // 支持的客户端列表 supportedClients: [], // 账户 accounts: [], accountsLoading: false, accountSortBy: 'dailyTokens', // 默认按今日Token排序 accountsSortOrder: 'asc', // 排序顺序 'asc' 或 'desc' showCreateAccountModal: false, createAccountLoading: false, accountForm: { platform: 'claude', // 'claude' 或 'gemini' name: '', description: '', addType: 'oauth', // 'oauth' 或 'manual' accountType: 'shared', // 'shared' 或 'dedicated' accessToken: '', refreshToken: '', proxyType: '', proxyHost: '', proxyPort: '', proxyUsername: '', proxyPassword: '', projectId: '' // Gemini 项目编号 }, // 编辑账户相关 showEditAccountModal: false, editAccountLoading: false, editAccountForm: { id: '', platform: 'claude', name: '', description: '', accountType: 'shared', originalAccountType: 'shared', accessToken: '', refreshToken: '', proxyType: '', proxyHost: '', proxyPort: '', proxyUsername: '', proxyPassword: '', projectId: '' // Gemini 项目编号 }, // OAuth 相关 oauthStep: 1, authUrlLoading: false, oauthData: { sessionId: '', authUrl: '', callbackUrl: '' }, // Gemini OAuth 相关 geminiOauthPolling: false, geminiOauthInterval: null, geminiOauthData: { sessionId: '', authUrl: '', code: '' }, // 用户菜单和账户修改相关 userMenuOpen: false, currentUser: { username: '' }, showChangePasswordModal: false, changePasswordLoading: false, changePasswordForm: { newUsername: '', currentPassword: '', newPassword: '', confirmPassword: '' }, // 确认弹窗相关 showConfirmModal: false, confirmModal: { title: '', message: '', confirmText: '继续', cancelText: '取消', onConfirm: null, onCancel: null }, // 版本管理相关 versionInfo: { current: '', // 当前版本 latest: '', // 最新版本 hasUpdate: false, // 是否有更新 checkingUpdate: false, // 正在检查更新 lastChecked: null, // 上次检查时间 releaseInfo: null, // 最新版本的发布信息 githubRepo: 'wei-shaw/claude-relay-service', // GitHub仓库 showReleaseNotes: false, // 是否显示发布说明 autoCheckInterval: null, // 自动检查定时器 noUpdateMessage: false // 显示"已是最新版"提醒 }, // OEM设置相关 oemSettings: { siteName: 'Claude Relay Service', siteIcon: '', siteIconData: '', // Base64图标数据 updatedAt: null }, oemSettingsLoading: false, oemSettingsSaving: false } }, computed: { // 动态计算BASE_URL currentBaseUrl() { return `${window.location.protocol}//${window.location.host}/api/`; }, // 排序后的账户列表 sortedAccounts() { if (!this.accountsSortBy) { return this.accounts; } return [...this.accounts].sort((a, b) => { let aValue = a[this.accountsSortBy]; let bValue = b[this.accountsSortBy]; // 特殊处理状态字段 if (this.accountsSortBy === 'status') { aValue = a.isActive ? 1 : 0; bValue = b.isActive ? 1 : 0; } // 处理字符串比较 if (typeof aValue === 'string' && typeof bValue === 'string') { aValue = aValue.toLowerCase(); bValue = bValue.toLowerCase(); } // 排序 if (this.accountsSortOrder === 'asc') { return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; } else { return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; } }); }, // 排序后的API Keys列表 sortedApiKeys() { if (!this.apiKeysSortBy) { return this.apiKeys; } return [...this.apiKeys].sort((a, b) => { let aValue, bValue; // 特殊处理不同字段 switch (this.apiKeysSortBy) { case 'status': aValue = a.isActive ? 1 : 0; bValue = b.isActive ? 1 : 0; break; case 'cost': // 计算费用,转换为数字比较 aValue = this.calculateApiKeyCostNumber(a.usage); bValue = this.calculateApiKeyCostNumber(b.usage); break; case 'createdAt': case 'expiresAt': // 日期比较 aValue = a[this.apiKeysSortBy] ? new Date(a[this.apiKeysSortBy]).getTime() : 0; bValue = b[this.apiKeysSortBy] ? new Date(b[this.apiKeysSortBy]).getTime() : 0; break; default: aValue = a[this.apiKeysSortBy]; bValue = b[this.apiKeysSortBy]; // 处理字符串比较 if (typeof aValue === 'string' && typeof bValue === 'string') { aValue = aValue.toLowerCase(); bValue = bValue.toLowerCase(); } } // 排序 if (this.apiKeysSortOrder === 'asc') { return aValue > bValue ? 1 : aValue < bValue ? -1 : 0; } else { return aValue < bValue ? 1 : aValue > bValue ? -1 : 0; } }); }, // 获取专属账号列表 dedicatedAccounts() { return this.accounts.filter(account => account.accountType === 'dedicated' && account.isActive === true ); }, // 计算最小日期时间(当前时间) minDateTime() { const now = new Date(); now.setMinutes(now.getMinutes() - now.getTimezoneOffset()); return now.toISOString().slice(0, 16); } }, mounted() { console.log('Vue app mounted, authToken:', !!this.authToken, 'activeTab:', this.activeTab); // 从URL参数中读取tab信息 this.initializeTabFromUrl(); // 初始化防抖函数 this.setTrendPeriod = this.debounce(this._setTrendPeriod, 300); // 添加全局点击事件监听器,用于关闭用户菜单 document.addEventListener('click', (event) => { // 检查点击是否在用户菜单区域外部 const isClickInsideUserMenu = event.target.closest('.user-menu-container'); if (!isClickInsideUserMenu) { this.userMenuOpen = false; } }); // 监听浏览器前进后退按钮事件 window.addEventListener('popstate', () => { this.initializeTabFromUrl(); }); if (this.authToken) { this.isLoggedIn = true; // 加载当前用户信息 this.loadCurrentUser(); // 加载版本信息 this.loadCurrentVersion(); // 初始化日期筛选器和图表数据 this.initializeDateFilter(); // 预加载账号列表、API Keys和支持的客户端,以便正确显示绑定关系 Promise.all([ this.loadAccounts(), this.loadApiKeys(), this.loadSupportedClients() ]).then(() => { // 根据当前活跃标签页加载数据 this.loadCurrentTabData(); }); // 如果在仪表盘,等待Chart.js加载后初始化图表 if (this.activeTab === 'dashboard') { this.waitForChartJS().then(() => { this.loadDashboardModelStats(); this.loadUsageTrend(); this.loadApiKeysUsageTrend(); }); } } else { console.log('No auth token found, user needs to login'); } // 始终加载OEM设置,无论登录状态 this.loadOemSettings(); }, beforeUnmount() { this.cleanupCharts(); // 清理版本检查定时器 if (this.versionInfo.autoCheckInterval) { clearInterval(this.versionInfo.autoCheckInterval); } }, watch: { activeTab: { handler(newTab, oldTab) { console.log('Tab changed from:', oldTab, 'to:', newTab); // 如果离开仪表盘标签页,清理图表 if (oldTab === 'dashboard' && newTab !== 'dashboard') { this.cleanupCharts(); } this.loadCurrentTabData(); }, immediate: false }, 'geminiOauthData.code': { handler(newValue) { if (newValue) { this.handleGeminiAuthCodeInput(newValue); } } } }, methods: { // 账户列表排序 sortAccounts(field) { if (this.accountsSortBy === field) { // 如果点击的是当前排序字段,切换排序顺序 this.accountsSortOrder = this.accountsSortOrder === 'asc' ? 'desc' : 'asc'; } else { // 如果点击的是新字段,设置为升序 this.accountsSortBy = field; this.accountsSortOrder = 'asc'; } }, // API Keys列表排序 sortApiKeys(field) { if (this.apiKeysSortBy === field) { // 如果点击的是当前排序字段,切换排序顺序 this.apiKeysSortOrder = this.apiKeysSortOrder === 'asc' ? 'desc' : 'asc'; } else { // 如果点击的是新字段,设置为升序 this.apiKeysSortBy = field; this.apiKeysSortOrder = 'asc'; } }, // 从URL读取tab参数并设置activeTab initializeTabFromUrl() { const urlParams = new URLSearchParams(window.location.search); const tabParam = urlParams.get('tab'); // 检查tab参数是否有效 const validTabs = this.tabs.map(tab => tab.key); if (tabParam && validTabs.includes(tabParam)) { this.activeTab = tabParam; } }, // 切换tab并更新URL switchTab(tabKey) { if (this.activeTab !== tabKey) { this.activeTab = tabKey; this.updateUrlTab(tabKey); } }, // 更新URL中的tab参数 updateUrlTab(tabKey) { const url = new URL(window.location.href); if (tabKey === 'dashboard') { // 如果是默认的dashboard标签,移除tab参数 url.searchParams.delete('tab'); } else { url.searchParams.set('tab', tabKey); } // 使用pushState更新URL但不刷新页面 window.history.pushState({}, '', url.toString()); }, // 统一的API请求方法,处理token过期等错误 async apiRequest(url, options = {}) { try { const defaultOptions = { headers: { 'Authorization': 'Bearer ' + this.authToken, 'Content-Type': 'application/json', ...options.headers }, ...options }; const response = await fetch(url, defaultOptions); const data = await response.json(); // 检查是否是token过期错误 if (!response.ok && (response.status === 401 || (data.error === 'Invalid admin token' || data.message === 'Invalid or expired admin session'))) { // 清理本地存储并刷新页面 localStorage.removeItem('authToken'); this.authToken = null; this.isLoggedIn = false; location.reload(); return null; } return data; } catch (error) { console.error('API request error:', error); throw error; } }, // 显示确认弹窗 showConfirm(title, message, confirmText = '继续', cancelText = '取消') { return new Promise((resolve) => { this.confirmModal = { title, message, confirmText, cancelText, onConfirm: () => { this.showConfirmModal = false; resolve(true); }, onCancel: () => { this.showConfirmModal = false; resolve(false); } }; this.showConfirmModal = true; }); }, // 处理确认弹窗确定按钮 handleConfirmOk() { if (this.confirmModal.onConfirm) { this.confirmModal.onConfirm(); } }, // 处理确认弹窗取消按钮 handleConfirmCancel() { if (this.confirmModal.onCancel) { this.confirmModal.onCancel(); } }, // 获取绑定账号名称 getBoundAccountName(accountId) { const account = this.accounts.find(acc => acc.id === accountId); return account ? account.name : '未知账号'; }, // 获取绑定到特定账号的API Key数量 getBoundApiKeysCount(accountId) { return this.apiKeys.filter(key => key.claudeAccountId === accountId || key.geminiAccountId === accountId ).length; }, // 添加限制模型 addRestrictedModel(form) { const model = form.modelInput.trim(); if (model && !form.restrictedModels.includes(model)) { form.restrictedModels.push(model); form.modelInput = ''; } }, // 移除限制模型 removeRestrictedModel(form, index) { form.restrictedModels.splice(index, 1); }, // Toast 通知方法 showToast(message, type = 'info', title = null, duration = 5000) { const id = ++this.toastIdCounter; const toast = { id, message, type, title, show: false }; this.toasts.push(toast); // 延迟显示动画 setTimeout(() => { const toastIndex = this.toasts.findIndex(t => t.id === id); if (toastIndex !== -1) { this.toasts[toastIndex].show = true; } }, 100); // 自动移除 if (duration > 0) { setTimeout(() => { this.removeToast(id); }, duration); } }, removeToast(id) { const index = this.toasts.findIndex(t => t.id === id); if (index !== -1) { this.toasts[index].show = false; setTimeout(() => { const currentIndex = this.toasts.findIndex(t => t.id === id); if (currentIndex !== -1) { this.toasts.splice(currentIndex, 1); } }, 300); } }, getToastIcon(type) { switch (type) { case 'success': return 'fas fa-check-circle'; case 'error': return 'fas fa-exclamation-circle'; case 'warning': return 'fas fa-exclamation-triangle'; case 'info': return 'fas fa-info-circle'; default: return 'fas fa-info-circle'; } }, // 打开创建API Key模态框 openCreateApiKeyModal() { console.log('Opening API Key modal...'); // 先关闭所有其他模态框 this.showCreateAccountModal = false; // 使用nextTick确保状态更新 this.$nextTick(() => { this.showCreateApiKeyModal = true; }); }, // 更新过期时间 updateExpireAt() { const duration = this.apiKeyForm.expireDuration; if (!duration) { this.apiKeyForm.expiresAt = null; return; } if (duration === 'custom') { // 自定义日期需要用户选择 return; } const now = new Date(); const durationMap = { '1d': 1, '7d': 7, '30d': 30, '90d': 90, '180d': 180, '365d': 365 }; const days = durationMap[duration]; if (days) { const expireDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000); this.apiKeyForm.expiresAt = expireDate.toISOString(); } }, // 更新自定义过期时间 updateCustomExpireAt() { if (this.apiKeyForm.customExpireDate) { const expireDate = new Date(this.apiKeyForm.customExpireDate); this.apiKeyForm.expiresAt = expireDate.toISOString(); } }, // 格式化过期日期 formatExpireDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }, // 格式化日期时间 formatDateTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); }, // 检查 API Key 是否已过期 isApiKeyExpired(expiresAt) { if (!expiresAt) return false; return new Date(expiresAt) < new Date(); }, // 检查 API Key 是否即将过期(7天内) isApiKeyExpiringSoon(expiresAt) { if (!expiresAt) return false; const expireDate = new Date(expiresAt); const now = new Date(); const daysUntilExpire = (expireDate - now) / (1000 * 60 * 60 * 24); return daysUntilExpire > 0 && daysUntilExpire <= 7; }, // 打开创建账户模态框 openCreateAccountModal() { console.log('Opening Account modal...'); // 先关闭所有其他模态框 this.showCreateApiKeyModal = false; // 使用nextTick确保状态更新 this.$nextTick(() => { this.showCreateAccountModal = true; this.resetAccountForm(); }); }, // 关闭创建账户模态框 closeCreateAccountModal() { this.showCreateAccountModal = false; this.resetAccountForm(); }, // 打开编辑账户模态框 openEditAccountModal(account) { this.editAccountForm = { id: account.id, platform: account.platform || 'claude', name: account.name, description: account.description || '', accountType: account.accountType || 'shared', originalAccountType: account.accountType || 'shared', accessToken: '', refreshToken: '', proxyType: account.proxy ? account.proxy.type : '', proxyHost: account.proxy ? account.proxy.host : '', proxyPort: account.proxy ? account.proxy.port : '', proxyUsername: account.proxy ? account.proxy.username : '', proxyPassword: account.proxy ? account.proxy.password : '', projectId: account.projectId || '' // 添加项目编号 }; this.showEditAccountModal = true; }, // 关闭编辑账户模态框 closeEditAccountModal() { this.showEditAccountModal = false; this.editAccountForm = { id: '', platform: 'claude', name: '', description: '', accountType: 'shared', originalAccountType: 'shared', accessToken: '', refreshToken: '', proxyType: '', proxyHost: '', proxyPort: '', proxyUsername: '', proxyPassword: '', projectId: '' // 重置项目编号 }; }, // 更新账户 async updateAccount() { // 对于Gemini账户,检查项目编号 if (this.editAccountForm.platform === 'gemini') { if (!this.editAccountForm.projectId || this.editAccountForm.projectId.trim() === '') { // 使用自定义确认弹窗 const confirmed = await this.showConfirm( '项目编号未填写', '您尚未填写项目编号。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。\n如果您使用的是普通个人账号,可以继续不填写。', '继续保存', '返回填写' ); if (!confirmed) { return; } } } this.editAccountLoading = true; try { // 验证账户类型切换 if (this.editAccountForm.accountType === 'shared' && this.editAccountForm.originalAccountType === 'dedicated') { // 确保API Keys数据已加载,以便正确计算绑定数量 if (this.apiKeys.length === 0) { await this.loadApiKeys(); } const boundKeysCount = this.getBoundApiKeysCount(this.editAccountForm.id); if (boundKeysCount > 0) { this.showToast(`无法切换到共享账户,该账户绑定了 ${boundKeysCount} 个API Key,请先解绑所有API Key`, 'error', '切换失败'); return; } } // 构建更新数据 let updateData = { name: this.editAccountForm.name, description: this.editAccountForm.description, accountType: this.editAccountForm.accountType, projectId: this.editAccountForm.projectId || '' // 添加项目编号 }; // 只在有值时才更新 token if (this.editAccountForm.accessToken.trim()) { if (this.editAccountForm.platform === 'gemini') { // Gemini OAuth 数据格式 // 如果有 Refresh Token,设置10分钟过期;否则设置1年 const expiresInMs = this.editAccountForm.refreshToken ? (10 * 60 * 1000) // 10分钟 : (365 * 24 * 60 * 60 * 1000); // 1年 const newOauthData = { access_token: this.editAccountForm.accessToken, refresh_token: this.editAccountForm.refreshToken || '', scope: 'https://www.googleapis.com/auth/cloud-platform', token_type: 'Bearer', expiry_date: Date.now() + expiresInMs }; updateData.geminiOauth = newOauthData; } else { // Claude OAuth 数据格式 // 如果有 Refresh Token,设置10分钟过期;否则设置1年 const expiresInMs = this.editAccountForm.refreshToken ? (10 * 60 * 1000) // 10分钟 : (365 * 24 * 60 * 60 * 1000); // 1年 const newOauthData = { accessToken: this.editAccountForm.accessToken, refreshToken: this.editAccountForm.refreshToken || '', expiresAt: Date.now() + expiresInMs, scopes: ['user:inference'] }; updateData.claudeAiOauth = newOauthData; } } // 更新代理配置 if (this.editAccountForm.proxyType) { updateData.proxy = { type: this.editAccountForm.proxyType, host: this.editAccountForm.proxyHost, port: parseInt(this.editAccountForm.proxyPort), username: this.editAccountForm.proxyUsername || null, password: this.editAccountForm.proxyPassword || null }; } else { updateData.proxy = null; } // 根据平台选择端点 const endpoint = this.editAccountForm.platform === 'gemini' ? `/admin/gemini-accounts/${this.editAccountForm.id}` : `/admin/claude-accounts/${this.editAccountForm.id}`; const response = await fetch(endpoint, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authToken }, body: JSON.stringify(updateData) }); const data = await response.json(); if (data.success) { this.showToast('账户更新成功!', 'success', '更新成功'); this.closeEditAccountModal(); await this.loadAccounts(); } else { this.showToast(data.message || 'Account update failed', 'error', 'Update Failed'); } } catch (error) { console.error('Error updating account:', error); let errorMessage = '更新失败,请检查网络连接'; if (error.response) { try { const errorData = await error.response.json(); errorMessage = errorData.message || errorMessage; } catch (parseError) { console.error('Failed to parse error response:', parseError); } } else if (error.message) { errorMessage = error.message; } this.showToast(errorMessage, 'error', '网络错误', 8000); } finally { this.editAccountLoading = false; } }, // 重置账户表单 resetAccountForm() { this.accountForm = { platform: 'claude', name: '', description: '', addType: 'oauth', accountType: 'shared', accessToken: '', refreshToken: '', proxyType: '', proxyHost: '', proxyPort: '', proxyUsername: '', proxyPassword: '', projectId: '' // 重置项目编号 }; this.oauthStep = 1; this.oauthData = { sessionId: '', authUrl: '', callbackUrl: '' }; this.geminiOauthData = { sessionId: '', authUrl: '', code: '' }; // 停止 Gemini OAuth 轮询 if (this.geminiOauthInterval) { clearInterval(this.geminiOauthInterval); this.geminiOauthInterval = null; } this.geminiOauthPolling = false; }, // OAuth步骤前进 async nextOAuthStep() { // 对于Gemini账户,检查项目编号 if (this.accountForm.platform === 'gemini' && this.oauthStep === 1 && this.accountForm.addType === 'oauth') { if (!this.accountForm.projectId || this.accountForm.projectId.trim() === '') { // 使用自定义确认弹窗 const confirmed = await this.showConfirm( '项目编号未填写', '您尚未填写项目编号。\n\n如果您的Google账号绑定了Google Cloud或被识别为Workspace账号,需要提供项目编号。\n如果您使用的是普通个人账号,可以继续不填写。', '继续', '返回填写' ); if (!confirmed) { return; } } } if (this.oauthStep < 3) { this.oauthStep++; } }, // 生成OAuth授权URL async generateAuthUrl() { this.authUrlLoading = true; try { // Build proxy configuration let proxy = null; if (this.accountForm.proxyType) { proxy = { type: this.accountForm.proxyType, host: this.accountForm.proxyHost, port: parseInt(this.accountForm.proxyPort), username: this.accountForm.proxyUsername || null, password: this.accountForm.proxyPassword || null }; } const endpoint = this.accountForm.platform === 'gemini' ? '/admin/gemini-accounts/generate-auth-url' : '/admin/claude-accounts/generate-auth-url'; const data = await this.apiRequest(endpoint, { method: 'POST', body: JSON.stringify({ proxy: proxy }) }); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { if (this.accountForm.platform === 'gemini') { this.geminiOauthData.authUrl = data.data.authUrl; this.geminiOauthData.sessionId = data.data.sessionId; // 不再自动开始轮询,改为手动输入授权码 } else { this.oauthData.authUrl = data.data.authUrl; this.oauthData.sessionId = data.data.sessionId; } this.showToast('授权链接生成成功!', 'success', '生成成功'); } else { this.showToast(data.message || '生成失败', 'error', '生成失败'); } } catch (error) { console.error('Error generating auth URL:', error); this.showToast('生成失败,请检查网络连接', 'error', '网络错误'); } finally { this.authUrlLoading = false; } }, // 复制到剪贴板 async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.showToast('已复制到剪贴板', 'success', '复制成功'); } catch (error) { console.error('Copy failed:', error); this.showToast('复制失败', 'error', '复制失败'); } }, // 创建OAuth账户 async createOAuthAccount() { // 如果是 Gemini,不应该调用这个方法 if (this.accountForm.platform === 'gemini') { console.error('createOAuthAccount should not be called for Gemini'); return; } this.createAccountLoading = true; try { // 首先交换authorization code获取token const exchangeData = await this.apiRequest('/admin/claude-accounts/exchange-code', { method: 'POST', body: JSON.stringify({ sessionId: this.oauthData.sessionId, callbackUrl: this.oauthData.callbackUrl }) }); if (!exchangeData) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (!exchangeData.success) { // Display detailed error information const errorMsg = exchangeData.message || 'Token exchange failed'; this.showToast('Authorization failed: ' + errorMsg, 'error', 'Authorization Failed', 8000); console.error('OAuth exchange failed:', exchangeData); return; } // Build proxy configuration let proxy = null; if (this.accountForm.proxyType) { proxy = { type: this.accountForm.proxyType, host: this.accountForm.proxyHost, port: parseInt(this.accountForm.proxyPort), username: this.accountForm.proxyUsername || null, password: this.accountForm.proxyPassword || null }; } // 创建账户 const createData = await this.apiRequest('/admin/claude-accounts', { method: 'POST', body: JSON.stringify({ name: this.accountForm.name, description: this.accountForm.description, claudeAiOauth: exchangeData.data.claudeAiOauth, proxy: proxy, accountType: this.accountForm.accountType }) }); if (!createData) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (createData.success) { this.showToast('OAuth账户创建成功!', 'success', '账户创建成功'); this.closeCreateAccountModal(); await this.loadAccounts(); } else { this.showToast(createData.message || 'Account creation failed', 'error', 'Creation Failed'); } } catch (error) { console.error('Error creating OAuth account:', error); // 尝试从错误响应中提取更详细的信息 let errorMessage = '创建失败,请检查网络连接'; if (error.response) { try { const errorData = await error.response.json(); errorMessage = errorData.message || errorMessage; } catch (parseError) { // 如果无法解析JSON,使用默认消息 console.error('Failed to parse error response:', parseError); } } else if (error.message) { errorMessage = error.message; } this.showToast(errorMessage, 'error', '网络错误', 8000); } finally { this.createAccountLoading = false; } }, // 创建手动账户 async createManualAccount() { this.createAccountLoading = true; try { // 构建代理配置 let proxy = null; if (this.accountForm.proxyType) { proxy = { type: this.accountForm.proxyType, host: this.accountForm.proxyHost, port: parseInt(this.accountForm.proxyPort), username: this.accountForm.proxyUsername || null, password: this.accountForm.proxyPassword || null }; } // 根据平台构建 OAuth 数据 let endpoint, bodyData; if (this.accountForm.platform === 'gemini') { // Gemini 账户 // 如果有 Refresh Token,设置10分钟过期;否则设置1年 const expiresInMs = this.accountForm.refreshToken ? (10 * 60 * 1000) // 10分钟 : (365 * 24 * 60 * 60 * 1000); // 1年 const geminiOauthData = { access_token: this.accountForm.accessToken, refresh_token: this.accountForm.refreshToken || '', scope: 'https://www.googleapis.com/auth/cloud-platform', token_type: 'Bearer', expiry_date: Date.now() + expiresInMs }; endpoint = '/admin/gemini-accounts'; bodyData = { name: this.accountForm.name, description: this.accountForm.description, geminiOauth: geminiOauthData, proxy: proxy, accountType: this.accountForm.accountType, projectId: this.accountForm.projectId || '' // 添加项目编号 }; } else { // Claude 账户 // 如果有 Refresh Token,设置10分钟过期;否则设置1年 const expiresInMs = this.accountForm.refreshToken ? (10 * 60 * 1000) // 10分钟 : (365 * 24 * 60 * 60 * 1000); // 1年 const manualOauthData = { accessToken: this.accountForm.accessToken, refreshToken: this.accountForm.refreshToken || '', expiresAt: Date.now() + expiresInMs, scopes: ['user:inference'] // 默认权限 }; endpoint = '/admin/claude-accounts'; bodyData = { name: this.accountForm.name, description: this.accountForm.description, claudeAiOauth: manualOauthData, proxy: proxy, accountType: this.accountForm.accountType }; } // 创建账户 const createResponse = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authToken }, body: JSON.stringify(bodyData) }); const createData = await createResponse.json(); if (createData.success) { this.showToast('手动账户创建成功!', 'success', '账户创建成功'); this.closeCreateAccountModal(); await this.loadAccounts(); } else { this.showToast(createData.message || 'Account creation failed', 'error', 'Creation Failed'); } } catch (error) { console.error('Error creating manual account:', error); let errorMessage = '创建失败,请检查网络连接'; if (error.response) { try { const errorData = await error.response.json(); errorMessage = errorData.message || errorMessage; } catch (parseError) { console.error('Failed to parse error response:', parseError); } } else if (error.message) { errorMessage = error.message; } this.showToast(errorMessage, 'error', '网络错误', 8000); } finally { this.createAccountLoading = false; } }, // Gemini OAuth 轮询 async startGeminiOAuthPolling() { if (this.geminiOauthPolling) return; this.geminiOauthPolling = true; let attempts = 0; const maxAttempts = 30; // 最多轮询 30 次(60秒) this.geminiOauthInterval = setInterval(async () => { attempts++; try { const data = await this.apiRequest('/admin/gemini-accounts/poll-auth-status', { method: 'POST', body: JSON.stringify({ sessionId: this.geminiOauthData.sessionId }) }); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 this.stopGeminiOAuthPolling(); return; } if (data.success) { // 授权成功 this.stopGeminiOAuthPolling(); this.geminiOauthData.code = 'authorized'; // 自动创建账户 await this.createGeminiOAuthAccount(data.data.tokens); } else if (data.error === 'Authorization timeout' || attempts >= maxAttempts) { // 超时 this.stopGeminiOAuthPolling(); this.showToast('授权超时,请重试', 'error', '授权超时'); } } catch (error) { console.error('Polling error:', error); if (attempts >= maxAttempts) { this.stopGeminiOAuthPolling(); this.showToast('轮询失败,请检查网络连接', 'error', '网络错误'); } } }, 2000); // 每2秒轮询一次 }, stopGeminiOAuthPolling() { if (this.geminiOauthInterval) { clearInterval(this.geminiOauthInterval); this.geminiOauthInterval = null; } this.geminiOauthPolling = false; }, // 创建 Gemini OAuth 账户 async createGeminiOAuthAccount() { this.createAccountLoading = true; try { // 首先交换授权码获取 tokens const tokenData = await this.apiRequest('/admin/gemini-accounts/exchange-code', { method: 'POST', body: JSON.stringify({ code: this.geminiOauthData.code, sessionId: this.geminiOauthData.sessionId }) }); if (!tokenData) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (!tokenData.success) { this.showToast(tokenData.message || '授权码交换失败', 'error', '交换失败'); return; } // 构建代理配置 let proxy = null; if (this.accountForm.proxyType) { proxy = { type: this.accountForm.proxyType, host: this.accountForm.proxyHost, port: parseInt(this.accountForm.proxyPort), username: this.accountForm.proxyUsername || null, password: this.accountForm.proxyPassword || null }; } // 创建账户 const data = await this.apiRequest('/admin/gemini-accounts', { method: 'POST', body: JSON.stringify({ name: this.accountForm.name, description: this.accountForm.description, geminiOauth: tokenData.data.tokens, proxy: proxy, accountType: this.accountForm.accountType, projectId: this.accountForm.projectId || '' // 添加项目编号 }) }); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { this.showToast('Gemini OAuth账户创建成功!', 'success', '账户创建成功'); this.closeCreateAccountModal(); await this.loadAccounts(); } else { this.showToast(data.message || 'Account creation failed', 'error', 'Creation Failed'); } } catch (error) { console.error('Error creating Gemini OAuth account:', error); this.showToast('创建失败,请检查网络连接', 'error', '网络错误', 8000); } finally { this.createAccountLoading = false; } }, // 处理 Gemini OAuth 授权码输入 handleGeminiAuthCodeInput(value, isUserTyping = false) { if (!value || typeof value !== 'string') return; const trimmedValue = value.trim(); // 如果内容为空,不处理 if (!trimmedValue) return; // 检查是否是 URL 格式(包含 http:// 或 https://) const isUrl = trimmedValue.startsWith('http://') || trimmedValue.startsWith('https://'); // 如果是 URL 格式 if (isUrl) { // 检查是否是正确的 localhost:45462 开头的 URL if (trimmedValue.startsWith('http://localhost:45462')) { try { const url = new URL(trimmedValue); const code = url.searchParams.get('code'); if (code) { // 成功提取授权码 this.geminiOauthData.code = code; this.showToast('成功提取授权码!', 'success', '提取成功'); console.log('Successfully extracted authorization code from URL'); } else { // URL 中没有 code 参数 this.showToast('URL 中未找到授权码参数,请检查链接是否正确', 'error', '提取失败'); } } catch (error) { // URL 解析失败 console.error('Failed to parse URL:', error); this.showToast('链接格式错误,请检查是否为完整的 URL', 'error', '解析失败'); } } else { // 错误的 URL(不是 localhost:45462 开头) this.showToast('请粘贴以 http://localhost:45462 开头的链接', 'error', '链接错误'); } } // 如果不是 URL,保持原值(兼容直接输入授权码) }, // 根据当前标签页加载数据 loadCurrentTabData() { console.log('Loading current tab data for:', this.activeTab); switch (this.activeTab) { case 'dashboard': this.loadDashboard(); // 加载图表数据,等待Chart.js this.waitForChartJS().then(() => { this.loadDashboardModelStats(); this.loadUsageTrend(); this.loadApiKeysUsageTrend(); }); break; case 'apiKeys': // 加载API Keys时同时加载账号列表,以便显示绑定账号名称 Promise.all([ this.loadApiKeys(), this.loadAccounts() ]); break; case 'accounts': // 加载账户时同时加载API Keys,以便正确计算绑定数量 Promise.all([ this.loadAccounts(), this.loadApiKeys() ]); break; case 'models': this.loadModelStats(); break; case 'tutorial': // 教程页面不需要加载数据 break; case 'settings': // OEM 设置已在 mounted 时加载,避免重复加载 if (!this.oemSettings.siteName && !this.oemSettings.siteIcon && !this.oemSettings.siteIconData) { this.loadOemSettings(); } break; } }, // 等待Chart.js加载完成 waitForChartJS() { return new Promise((resolve) => { const checkChart = () => { if (typeof Chart !== 'undefined') { resolve(); } else { setTimeout(checkChart, 100); } }; checkChart(); }); }, // 清理所有图表实例 cleanupCharts() { // 清理模型使用图表 if (this.modelUsageChart) { try { // 先停止所有动画 this.modelUsageChart.stop(); // 再销毁图表 this.modelUsageChart.destroy(); } catch (error) { console.warn('Error destroying model usage chart:', error); } this.modelUsageChart = null; } // 清理使用趋势图表 if (this.usageTrendChart) { try { // 先停止所有动画 this.usageTrendChart.stop(); // 再销毁图表 this.usageTrendChart.destroy(); } catch (error) { console.warn('Error destroying usage trend chart:', error); } this.usageTrendChart = null; } // 清理API Keys使用趋势图表 if (this.apiKeysUsageTrendChart) { try { // 先停止所有动画 this.apiKeysUsageTrendChart.stop(); // 再销毁图表 this.apiKeysUsageTrendChart.destroy(); } catch (error) { console.warn('Error destroying API keys usage trend chart:', error); } this.apiKeysUsageTrendChart = null; } }, // 检查DOM元素是否存在且有效 isElementValid(elementId) { const element = document.getElementById(elementId); return element && element.isConnected && element.ownerDocument && element.parentNode; }, // 防抖函数,防止快速点击 debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, async login() { this.loginLoading = true; this.loginError = ''; try { const response = await fetch('/web/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(this.loginForm) }); const data = await response.json(); if (data.success) { this.authToken = data.token; localStorage.setItem('authToken', this.authToken); this.isLoggedIn = true; // 记录当前用户名(使用服务器返回的真实用户名) this.currentUser.username = data.username; // 登录成功后刷新页面以重新加载所有数据 location.reload(); } else { this.loginError = data.message; } } catch (error) { console.error('Login error:', error); this.loginError = '登录失败,请检查网络连接'; } finally { this.loginLoading = false; } }, // 加载当前用户信息 async loadCurrentUser() { try { const data = await this.apiRequest('/web/auth/user'); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { this.currentUser.username = data.user.username; console.log('Loaded current user:', data.user.username); } else { console.warn('Failed to load current user:', data.message); } } catch (error) { console.error('Error loading current user:', error); } }, // 版本管理相关方法 async loadCurrentVersion() { try { const response = await fetch('/health'); const data = await response.json(); if (data.version) { // 从健康检查端点获取当前版本 this.versionInfo.current = data.version; // 检查更新 await this.checkForUpdates(); // 设置自动检查更新(每小时检查一次) this.versionInfo.autoCheckInterval = setInterval(() => { this.checkForUpdates(); }, 3600000); // 1小时 } } catch (error) { console.error('Error loading current version:', error); this.versionInfo.current = '未知'; } }, async checkForUpdates() { if (this.versionInfo.checkingUpdate) { return; } this.versionInfo.checkingUpdate = true; try { // 使用后端接口检查更新 const result = await this.apiRequest('/admin/check-updates'); if (!result) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (result.success) { const data = result.data; this.versionInfo.current = data.current; this.versionInfo.latest = data.latest; this.versionInfo.hasUpdate = data.hasUpdate; this.versionInfo.releaseInfo = data.releaseInfo; this.versionInfo.lastChecked = new Date(); // 保存到localStorage localStorage.setItem('versionInfo', JSON.stringify({ current: data.current, latest: data.latest, lastChecked: this.versionInfo.lastChecked, hasUpdate: data.hasUpdate, releaseInfo: data.releaseInfo })); // 如果没有更新,显示提醒 if (!data.hasUpdate) { this.versionInfo.noUpdateMessage = true; // 3秒后自动隐藏提醒 setTimeout(() => { this.versionInfo.noUpdateMessage = false; }, 3000); } if (data.cached && data.warning) { console.warn('Version check warning:', data.warning); } } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.error('Error checking for updates:', error); // 尝试从localStorage读取缓存的版本信息 const cached = localStorage.getItem('versionInfo'); if (cached) { const cachedInfo = JSON.parse(cached); this.versionInfo.current = cachedInfo.current || this.versionInfo.current; this.versionInfo.latest = cachedInfo.latest; this.versionInfo.hasUpdate = cachedInfo.hasUpdate; this.versionInfo.releaseInfo = cachedInfo.releaseInfo; this.versionInfo.lastChecked = new Date(cachedInfo.lastChecked); } } finally { this.versionInfo.checkingUpdate = false; } }, compareVersions(current, latest) { // 比较语义化版本号 const parseVersion = (v) => { const parts = v.split('.').map(Number); return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 }; }; const currentV = parseVersion(current); const latestV = parseVersion(latest); if (currentV.major !== latestV.major) { return currentV.major - latestV.major; } if (currentV.minor !== latestV.minor) { return currentV.minor - latestV.minor; } return currentV.patch - latestV.patch; }, formatVersionDate(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }); }, // 用户菜单相关方法 openChangePasswordModal() { this.userMenuOpen = false; this.showChangePasswordModal = true; }, closeChangePasswordModal() { this.showChangePasswordModal = false; this.changePasswordForm = { newUsername: '', currentPassword: '', newPassword: '', confirmPassword: '' }; }, async changePassword() { // 验证表单 if (this.changePasswordForm.newPassword !== this.changePasswordForm.confirmPassword) { this.showToast('新密码和确认密码不一致', 'error'); return; } if (this.changePasswordForm.newPassword.length < 8) { this.showToast('新密码长度至少8位', 'error'); return; } this.changePasswordLoading = true; try { const result = await this.apiRequest('/web/auth/change-password', { method: 'POST', body: JSON.stringify({ newUsername: this.changePasswordForm.newUsername || undefined, currentPassword: this.changePasswordForm.currentPassword, newPassword: this.changePasswordForm.newPassword }) }); if (!result) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (result.success) { this.showToast('账户信息修改成功,即将退出登录', 'success'); this.closeChangePasswordModal(); // 将新的用户名更新到本地状态 if (this.changePasswordForm.newUsername) { this.currentUser.username = this.changePasswordForm.newUsername; } // 延迟2秒后自动退出登录 setTimeout(() => { this.logout(); }, 2000); } else { this.showToast(result.message || '修改失败', 'error'); } } catch (error) { console.error('Change password error:', error); this.showToast('网络错误,请稍后再试', 'error'); } finally { this.changePasswordLoading = false; } }, async logout() { if (this.authToken) { try { await fetch('/web/auth/logout', { method: 'POST', headers: { 'Authorization': 'Bearer ' + this.authToken } }); } catch (error) { console.error('Logout error:', error); } } this.authToken = null; localStorage.removeItem('authToken'); this.isLoggedIn = false; this.loginForm = { username: '', password: '' }; this.loginError = ''; }, async loadDashboard() { try { const [dashboardData, todayCostsData, totalCostsData] = await Promise.all([ this.apiRequest('/admin/dashboard'), this.apiRequest('/admin/usage-costs?period=today'), this.apiRequest('/admin/usage-costs?period=all') ]); if (!dashboardData || !todayCostsData || !totalCostsData) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (dashboardData.success) { const overview = dashboardData.data.overview || {}; const recentActivity = dashboardData.data.recentActivity || {}; const systemAverages = dashboardData.data.systemAverages || {}; const systemHealth = dashboardData.data.systemHealth || {}; this.dashboardData = { totalApiKeys: overview.totalApiKeys || 0, activeApiKeys: overview.activeApiKeys || 0, totalAccounts: overview.totalClaudeAccounts || 0, activeAccounts: overview.activeClaudeAccounts || 0, rateLimitedAccounts: overview.rateLimitedClaudeAccounts || 0, todayRequests: recentActivity.requestsToday || 0, totalRequests: overview.totalRequestsUsed || 0, todayTokens: recentActivity.tokensToday || 0, todayInputTokens: recentActivity.inputTokensToday || 0, todayOutputTokens: recentActivity.outputTokensToday || 0, totalTokens: overview.totalTokensUsed || 0, totalInputTokens: overview.totalInputTokensUsed || 0, totalOutputTokens: overview.totalOutputTokensUsed || 0, totalCacheCreateTokens: overview.totalCacheCreateTokensUsed || 0, totalCacheReadTokens: overview.totalCacheReadTokensUsed || 0, todayCacheCreateTokens: recentActivity.cacheCreateTokensToday || 0, todayCacheReadTokens: recentActivity.cacheReadTokensToday || 0, systemRPM: systemAverages.rpm || 0, systemTPM: systemAverages.tpm || 0, systemStatus: systemHealth.redisConnected ? '正常' : '异常', uptime: systemHealth.uptime || 0 }; } // 更新费用数据 if (todayCostsData.success && totalCostsData.success) { this.costsData = { todayCosts: todayCostsData.data.totalCosts || { totalCost: 0, formatted: { totalCost: '$0.000000' } }, totalCosts: totalCostsData.data.totalCosts || { totalCost: 0, formatted: { totalCost: '$0.000000' } } }; } } catch (error) { console.error('Failed to load dashboard:', error); } }, async loadSupportedClients() { try { const data = await this.apiRequest('/admin/supported-clients'); if (data && data.success) { this.supportedClients = data.data || []; console.log('Loaded supported clients:', this.supportedClients); } } catch (error) { console.error('Failed to load supported clients:', error); } }, async loadApiKeys() { this.apiKeysLoading = true; console.log('Loading API Keys with time range:', this.apiKeyStatsTimeRange); try { const data = await this.apiRequest(`/admin/api-keys?timeRange=${this.apiKeyStatsTimeRange}`); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } console.log('API Keys response:', data); if (data.success) { // 确保每个 API key 都有必要的属性 this.apiKeys = (data.data || []).map(key => { const processedKey = { ...key, apiKey: key.apiKey || '', name: key.name || 'Unknown', id: key.id || '', isActive: key.isActive !== undefined ? key.isActive : true, usage: key.usage || { tokensUsed: 0 }, tokenLimit: key.tokenLimit || null, createdAt: key.createdAt || new Date().toISOString() }; // 为每个API Key初始化独立的日期筛选状态 if (!this.apiKeyDateFilters[processedKey.id]) { this.initApiKeyDateFilter(processedKey.id); } return processedKey; }); console.log('Processed API Keys:', this.apiKeys); } else { console.error('API Keys load failed:', data.message); this.apiKeys = []; } } catch (error) { console.error('Failed to load API keys:', error); this.apiKeys = []; } finally { this.apiKeysLoading = false; } }, async loadAccounts() { this.accountsLoading = true; try { // 并行加载 Claude 和 Gemini 账户 const [claudeData, geminiData] = await Promise.all([ this.apiRequest('/admin/claude-accounts'), this.apiRequest('/admin/gemini-accounts') ]); if (!claudeData || !geminiData) { // 如果token过期,apiRequest会返回null并刷新页面 return; } // 合并账户数据 const allAccounts = []; if (claudeData.success) { const claudeAccounts = (claudeData.data || []).map(acc => ({ ...acc, platform: 'claude' })); allAccounts.push(...claudeAccounts); } if (geminiData.success) { const geminiAccounts = (geminiData.data || []).map(acc => ({ ...acc, platform: 'gemini' })); allAccounts.push(...geminiAccounts); } this.accounts = allAccounts; // 为每个账号计算绑定的API Key数量 this.accounts.forEach(account => { if (account.platform === 'claude') { account.boundApiKeysCount = this.apiKeys.filter(key => key.claudeAccountId === account.id).length; } else { account.boundApiKeysCount = this.apiKeys.filter(key => key.geminiAccountId === account.id).length; } }); // 加载完成后自动排序 this.sortAccounts(); } catch (error) { console.error('Failed to load accounts:', error); } finally { this.accountsLoading = false; } }, // 账户排序 sortAccounts() { if (!this.accounts || this.accounts.length === 0) return; this.accounts.sort((a, b) => { switch (this.accountSortBy) { case 'name': return a.name.localeCompare(b.name); case 'dailyTokens': const aTokens = (a.usage && a.usage.daily && a.usage.daily.allTokens) || 0; const bTokens = (b.usage && b.usage.daily && b.usage.daily.allTokens) || 0; return bTokens - aTokens; // 降序 case 'dailyRequests': const aRequests = (a.usage && a.usage.daily && a.usage.daily.requests) || 0; const bRequests = (b.usage && b.usage.daily && b.usage.daily.requests) || 0; return bRequests - aRequests; // 降序 case 'totalTokens': const aTotalTokens = (a.usage && a.usage.total && a.usage.total.allTokens) || 0; const bTotalTokens = (b.usage && b.usage.total && b.usage.total.allTokens) || 0; return bTotalTokens - aTotalTokens; // 降序 case 'lastUsed': const aLastUsed = a.lastUsedAt ? new Date(a.lastUsedAt) : new Date(0); const bLastUsed = b.lastUsedAt ? new Date(b.lastUsedAt) : new Date(0); return bLastUsed - aLastUsed; // 降序(最近使用的在前) default: return 0; } }); }, async loadModelStats() { this.modelStatsLoading = true; try { const data = await this.apiRequest('/admin/model-stats?period=' + this.modelStatsPeriod); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { this.modelStats = data.data || []; } else { this.modelStats = []; } } catch (error) { console.error('Failed to load model stats:', error); this.modelStats = []; } finally { this.modelStatsLoading = false; } }, async createApiKey() { this.createApiKeyLoading = true; try { const data = await this.apiRequest('/admin/api-keys', { method: 'POST', body: JSON.stringify({ name: this.apiKeyForm.name, tokenLimit: this.apiKeyForm.tokenLimit && this.apiKeyForm.tokenLimit.toString().trim() ? parseInt(this.apiKeyForm.tokenLimit) : null, description: this.apiKeyForm.description || '', concurrencyLimit: this.apiKeyForm.concurrencyLimit && this.apiKeyForm.concurrencyLimit.toString().trim() ? parseInt(this.apiKeyForm.concurrencyLimit) : 0, rateLimitWindow: this.apiKeyForm.rateLimitWindow && this.apiKeyForm.rateLimitWindow.toString().trim() ? parseInt(this.apiKeyForm.rateLimitWindow) : null, rateLimitRequests: this.apiKeyForm.rateLimitRequests && this.apiKeyForm.rateLimitRequests.toString().trim() ? parseInt(this.apiKeyForm.rateLimitRequests) : null, claudeAccountId: this.apiKeyForm.claudeAccountId || null, geminiAccountId: this.apiKeyForm.geminiAccountId || null, permissions: this.apiKeyForm.permissions || 'all', enableModelRestriction: this.apiKeyForm.enableModelRestriction, restrictedModels: this.apiKeyForm.restrictedModels, enableClientRestriction: this.apiKeyForm.enableClientRestriction, allowedClients: this.apiKeyForm.allowedClients, expiresAt: this.apiKeyForm.expiresAt, dailyCostLimit: this.apiKeyForm.dailyCostLimit && this.apiKeyForm.dailyCostLimit.toString().trim() ? parseFloat(this.apiKeyForm.dailyCostLimit) : 0 }) }); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { // 设置新API Key数据并显示弹窗 this.newApiKey = { key: data.data.apiKey, name: data.data.name, description: data.data.description || '无描述', showFullKey: false }; this.showNewApiKeyModal = true; // 关闭创建弹窗并清理表单 this.showCreateApiKeyModal = false; this.apiKeyForm = { name: '', tokenLimit: '', description: '', concurrencyLimit: '', rateLimitWindow: '', rateLimitRequests: '', claudeAccountId: '', geminiAccountId: '', permissions: 'all', enableModelRestriction: false, restrictedModels: [], modelInput: '', enableClientRestriction: false, allowedClients: [], expireDuration: '', customExpireDate: '', expiresAt: null, dailyCostLimit: '' }; // 重新加载API Keys列表 await this.loadApiKeys(); } else { this.showToast(data.message || '创建失败', 'error', '创建失败'); } } catch (error) { console.error('Error creating API key:', error); this.showToast('创建失败,请检查网络连接', 'error', '网络错误'); } finally { this.createApiKeyLoading = false; } }, async deleteApiKey(keyId) { const confirmed = await this.showConfirm( '删除 API Key', '确定要删除这个 API Key 吗?\n\n此操作不可撤销,删除后将无法恢复。', '确认删除', '取消' ); if (!confirmed) return; try { const data = await this.apiRequest('/admin/api-keys/' + keyId, { method: 'DELETE' }); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { this.showToast('API Key 删除成功', 'success', '删除成功'); await this.loadApiKeys(); } else { this.showToast(data.message || '删除失败', 'error', '删除失败'); } } catch (error) { console.error('Error deleting API key:', error); this.showToast('删除失败,请检查网络连接', 'error', '网络错误'); } }, // 打开续期弹窗 openRenewApiKeyModal(key) { this.renewApiKeyForm = { id: key.id, name: key.name, currentExpiresAt: key.expiresAt, renewDuration: '30d', customExpireDate: '', newExpiresAt: null }; this.showRenewApiKeyModal = true; // 立即计算新的过期时间 this.updateRenewExpireAt(); }, // 关闭续期弹窗 closeRenewApiKeyModal() { this.showRenewApiKeyModal = false; this.renewApiKeyForm = { id: '', name: '', currentExpiresAt: null, renewDuration: '30d', customExpireDate: '', newExpiresAt: null }; }, // 更新续期过期时间 updateRenewExpireAt() { const duration = this.renewApiKeyForm.renewDuration; if (duration === 'permanent') { this.renewApiKeyForm.newExpiresAt = null; return; } if (duration === 'custom') { // 自定义日期需要用户选择 return; } // 计算新的过期时间 const baseTime = this.renewApiKeyForm.currentExpiresAt ? new Date(this.renewApiKeyForm.currentExpiresAt) : new Date(); // 如果当前已过期,从现在开始计算 if (baseTime < new Date()) { baseTime.setTime(new Date().getTime()); } const durationMap = { '7d': 7, '30d': 30, '90d': 90, '180d': 180, '365d': 365 }; const days = durationMap[duration]; if (days) { const expireDate = new Date(baseTime.getTime() + days * 24 * 60 * 60 * 1000); this.renewApiKeyForm.newExpiresAt = expireDate.toISOString(); } }, // 更新自定义续期时间 updateCustomRenewExpireAt() { if (this.renewApiKeyForm.customExpireDate) { const expireDate = new Date(this.renewApiKeyForm.customExpireDate); this.renewApiKeyForm.newExpiresAt = expireDate.toISOString(); } }, // 执行续期操作 async renewApiKey() { this.renewApiKeyLoading = true; try { const data = await this.apiRequest('/admin/api-keys/' + this.renewApiKeyForm.id, { method: 'PUT', body: JSON.stringify({ expiresAt: this.renewApiKeyForm.newExpiresAt }) }); if (!data) { return; } if (data.success) { this.showToast('API Key 续期成功', 'success'); this.closeRenewApiKeyModal(); await this.loadApiKeys(); } else { this.showToast(data.message || '续期失败', 'error'); } } catch (error) { console.error('Error renewing API key:', error); this.showToast('续期失败,请检查网络连接', 'error'); } finally { this.renewApiKeyLoading = false; } }, openEditApiKeyModal(key) { this.editApiKeyForm = { id: key.id, name: key.name, tokenLimit: key.tokenLimit || '', concurrencyLimit: key.concurrencyLimit || '', rateLimitWindow: key.rateLimitWindow || '', rateLimitRequests: key.rateLimitRequests || '', claudeAccountId: key.claudeAccountId || '', geminiAccountId: key.geminiAccountId || '', permissions: key.permissions || 'all', enableModelRestriction: key.enableModelRestriction || false, restrictedModels: key.restrictedModels ? [...key.restrictedModels] : [], modelInput: '', enableClientRestriction: key.enableClientRestriction || false, allowedClients: key.allowedClients ? [...key.allowedClients] : [], dailyCostLimit: key.dailyCostLimit || '' }; this.showEditApiKeyModal = true; }, closeEditApiKeyModal() { this.showEditApiKeyModal = false; this.editApiKeyForm = { id: '', name: '', tokenLimit: '', concurrencyLimit: '', rateLimitWindow: '', rateLimitRequests: '', claudeAccountId: '', geminiAccountId: '', permissions: 'all', enableModelRestriction: false, restrictedModels: [], modelInput: '', enableClientRestriction: false, allowedClients: [], dailyCostLimit: '' }; }, async updateApiKey() { this.editApiKeyLoading = true; try { const data = await this.apiRequest('/admin/api-keys/' + this.editApiKeyForm.id, { method: 'PUT', body: JSON.stringify({ tokenLimit: this.editApiKeyForm.tokenLimit && this.editApiKeyForm.tokenLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.tokenLimit) : 0, concurrencyLimit: this.editApiKeyForm.concurrencyLimit && this.editApiKeyForm.concurrencyLimit.toString().trim() !== '' ? parseInt(this.editApiKeyForm.concurrencyLimit) : 0, rateLimitWindow: this.editApiKeyForm.rateLimitWindow && this.editApiKeyForm.rateLimitWindow.toString().trim() !== '' ? parseInt(this.editApiKeyForm.rateLimitWindow) : 0, rateLimitRequests: this.editApiKeyForm.rateLimitRequests && this.editApiKeyForm.rateLimitRequests.toString().trim() !== '' ? parseInt(this.editApiKeyForm.rateLimitRequests) : 0, claudeAccountId: this.editApiKeyForm.claudeAccountId || null, geminiAccountId: this.editApiKeyForm.geminiAccountId || null, permissions: this.editApiKeyForm.permissions || 'all', enableModelRestriction: this.editApiKeyForm.enableModelRestriction, restrictedModels: this.editApiKeyForm.restrictedModels, enableClientRestriction: this.editApiKeyForm.enableClientRestriction, allowedClients: this.editApiKeyForm.allowedClients, dailyCostLimit: this.editApiKeyForm.dailyCostLimit && this.editApiKeyForm.dailyCostLimit.toString().trim() !== '' ? parseFloat(this.editApiKeyForm.dailyCostLimit) : 0 }) }); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } if (data.success) { this.showToast('API Key 更新成功', 'success', '更新成功'); this.closeEditApiKeyModal(); await this.loadApiKeys(); } else { this.showToast(data.message || '更新失败', 'error', '更新失败'); } } catch (error) { console.error('Error updating API key:', error); this.showToast('更新失败,请检查网络连接', 'error', '网络错误'); } finally { this.editApiKeyLoading = false; } }, async deleteAccount(accountId) { // 确保API Keys数据已加载,以便正确计算绑定数量 if (this.apiKeys.length === 0) { await this.loadApiKeys(); } // 查找账户以确定平台类型 const account = this.accounts.find(acc => acc.id === accountId); if (!account) { this.showToast('账户不存在', 'error', '删除失败'); return; } // 检查是否有API Key绑定到此账号 const boundKeysCount = this.getBoundApiKeysCount(accountId); if (boundKeysCount > 0) { this.showToast(`无法删除此账号,有 ${boundKeysCount} 个API Key绑定到此账号,请先解绑所有API Key`, 'error', '删除失败'); return; } const platformName = account.platform === 'gemini' ? 'Gemini' : 'Claude'; const confirmed = await this.showConfirm( `删除 ${platformName} 账户`, `确定要删除这个 ${platformName} 账户吗?\n\n账户名称:${account.name}\n此操作不可撤销,删除后将无法恢复。`, '确认删除', '取消' ); if (!confirmed) return; // 根据平台选择端点 const endpoint = account.platform === 'gemini' ? `/admin/gemini-accounts/${accountId}` : `/admin/claude-accounts/${accountId}`; try { const response = await fetch(endpoint, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + this.authToken } }); const data = await response.json(); if (data.success) { this.showToast('Claude 账户删除成功', 'success', '删除成功'); await this.loadAccounts(); } else { this.showToast(data.message || '删除失败', 'error', '删除失败'); } } catch (error) { console.error('Error deleting account:', error); this.showToast('删除失败,请检查网络连接', 'error', '网络错误'); } }, // 刷新账户 Token async refreshAccountToken(accountId) { const account = this.accounts.find(acc => acc.id === accountId); if (!account) { this.showToast('账户不存在', 'error', '刷新失败'); return; } // 根据平台选择端点 const endpoint = account.platform === 'gemini' ? `/admin/gemini-accounts/${accountId}/refresh` : `/admin/claude-accounts/${accountId}/refresh`; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': 'Bearer ' + this.authToken } }); const data = await response.json(); if (data.success) { const platformName = account.platform === 'gemini' ? 'Gemini' : 'Claude'; this.showToast(`${platformName} Token 刷新成功`, 'success', '刷新成功'); await this.loadAccounts(); } else { this.showToast(data.message || '刷新失败', 'error', '刷新失败'); } } catch (error) { console.error('Error refreshing token:', error); this.showToast('刷新失败,请检查网络连接', 'error', '网络错误'); } }, // API Key 展示相关方法 toggleApiKeyVisibility() { this.newApiKey.showFullKey = !this.newApiKey.showFullKey; }, getDisplayedApiKey() { if (this.newApiKey.showFullKey) { return this.newApiKey.key; } else { // 显示前8个字符和后4个字符,中间用*代替 const key = this.newApiKey.key; if (key.length <= 12) return key; return key.substring(0, 8) + '●'.repeat(Math.max(0, key.length - 12)) + key.substring(key.length - 4); } }, async copyApiKeyToClipboard() { try { await navigator.clipboard.writeText(this.newApiKey.key); this.showToast('API Key 已复制到剪贴板', 'success', '复制成功'); } catch (error) { console.error('Failed to copy:', error); // 降级方案:创建一个临时文本区域 const textArea = document.createElement('textarea'); textArea.value = this.newApiKey.key; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); this.showToast('API Key 已复制到剪贴板', 'success', '复制成功'); } catch (fallbackError) { this.showToast('复制失败,请手动复制', 'error', '复制失败'); } document.body.removeChild(textArea); } }, async closeNewApiKeyModal() { // 显示确认提示 const confirmed = await this.showConfirm( '关闭 API Key', '关闭后将无法再次查看完整的API Key,请确保已经妥善保存。\n\n确定要关闭吗?', '我已保存', '取消' ); if (confirmed) { this.showNewApiKeyModal = false; this.newApiKey = { key: '', name: '', description: '', showFullKey: false }; } }, // 格式化数字,添加千分符 formatNumber(num) { if (num === null || num === undefined) return '0'; const number = Number(num); if (number >= 1000000) { return Math.floor(number / 1000000).toLocaleString() + 'M'; } return number.toLocaleString(); }, // 格式化会话窗口时间 formatSessionWindow(windowStart, windowEnd) { if (!windowStart || !windowEnd) return '--'; const start = new Date(windowStart); const end = new Date(windowEnd); const startHour = start.getHours().toString().padStart(2, '0'); const startMin = start.getMinutes().toString().padStart(2, '0'); const endHour = end.getHours().toString().padStart(2, '0'); const endMin = end.getMinutes().toString().padStart(2, '0'); return `${startHour}:${startMin} - ${endHour}:${endMin}`; }, // 格式化剩余时间 formatRemainingTime(minutes) { if (!minutes || minutes <= 0) return '已结束'; const hours = Math.floor(minutes / 60); const mins = minutes % 60; if (hours > 0) { return `${hours}小时${mins}分钟`; } return `${mins}分钟`; }, // 格式化运行时间 formatUptime(seconds) { if (!seconds) return '0s'; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const mins = Math.floor((seconds % 3600) / 60); if (days > 0) { return days + '天' + hours + '时'; } else if (hours > 0) { return hours + '时' + mins + '分'; } else { return mins + '分'; } }, // 计算百分比 calculatePercentage(value, stats) { const total = stats.reduce((sum, stat) => sum + (stat.allTokens || 0), 0); if (total === 0) return 0; return ((value / total) * 100).toFixed(1); }, // 加载仪表盘模型统计 async loadDashboardModelStats() { console.log('Loading dashboard model stats, period:', this.dashboardModelPeriod, 'authToken:', !!this.authToken); try { const data = await this.apiRequest('/admin/model-stats?period=' + this.dashboardModelPeriod); if (!data) { // 如果token过期,apiRequest会返回null并刷新页面 return; } console.log('Model stats response data:', data); if (data.success) { this.dashboardModelStats = data.data || []; console.log('Loaded model stats:', this.dashboardModelStats.length, 'items'); this.updateModelUsageChart(); } else { console.warn('Model stats API returned success=false:', data); this.dashboardModelStats = []; } } catch (error) { console.error('Failed to load dashboard model stats:', error); this.dashboardModelStats = []; } }, // 更新模型使用饼图 updateModelUsageChart() { if (!this.dashboardModelStats.length) { console.warn('No dashboard model stats data, skipping chart update'); return; } // 检查Chart.js是否已加载 if (typeof Chart === 'undefined') { console.warn('Chart.js not loaded yet, retrying...'); setTimeout(() => this.updateModelUsageChart(), 500); return; } // 严格检查DOM元素是否有效 if (!this.isElementValid('modelUsageChart')) { console.error('Model usage chart canvas element not found or invalid'); return; } const ctx = document.getElementById('modelUsageChart'); // 安全销毁现有图表 if (this.modelUsageChart) { try { this.modelUsageChart.destroy(); } catch (error) { console.warn('Error destroying model usage chart:', error); } this.modelUsageChart = null; } // 再次验证元素在销毁后仍然有效 if (!this.isElementValid('modelUsageChart')) { console.error('Model usage chart canvas element became invalid after cleanup'); return; } const labels = this.dashboardModelStats.map(stat => stat.model); const data = this.dashboardModelStats.map(stat => stat.allTokens || 0); // 生成渐变色 const colors = [ 'rgba(102, 126, 234, 0.8)', 'rgba(118, 75, 162, 0.8)', 'rgba(240, 147, 251, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)', 'rgba(239, 68, 68, 0.8)' ]; try { // 最后一次检查元素有效性 if (!this.isElementValid('modelUsageChart')) { throw new Error('Canvas element is not valid for chart creation'); } this.modelUsageChart = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: colors, borderColor: 'rgba(255, 255, 255, 1)', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, animation: false, // 禁用动画防止异步渲染问题 plugins: { legend: { position: 'bottom', labels: { padding: 15, font: { size: 12 } } }, tooltip: { callbacks: { label: function(context) { const label = context.label || ''; const value = context.parsed || 0; const total = context.dataset.data.reduce((a, b) => a + b, 0); const percentage = ((value / total) * 100).toFixed(1); return label + ': ' + value.toLocaleString() + ' (' + percentage + '%)'; } } } } } }); } catch (error) { console.error('Error creating model usage chart:', error); this.modelUsageChart = null; } }, // 设置趋势图周期(添加防抖) setTrendPeriod: null, // 将在mounted中初始化为防抖函数 // 实际的设置趋势图周期方法 async _setTrendPeriod(days) { console.log('Setting trend period to:', days); // 先清理现有图表,防止竞态条件 if (this.usageTrendChart) { try { this.usageTrendChart.stop(); this.usageTrendChart.destroy(); } catch (error) { console.warn('Error cleaning trend chart:', error); } this.usageTrendChart = null; } this.trendPeriod = days; await this.loadUsageTrend(); }, // 加载API Keys使用趋势数据 async loadApiKeysUsageTrend() { console.log('Loading API keys usage trend data, granularity:', this.trendGranularity); try { let url = '/admin/api-keys-usage-trend?'; if (this.trendGranularity === 'hour') { // 小时粒度,传递开始和结束时间 url += `granularity=hour`; if (this.dateFilter.customRange && this.dateFilter.customRange.length === 2) { url += `&startDate=${encodeURIComponent(this.dateFilter.customRange[0])}`; url += `&endDate=${encodeURIComponent(this.dateFilter.customRange[1])}`; } } else { // 天粒度,传递天数 url += `granularity=day&days=${this.trendPeriod}`; } const response = await fetch(url, { headers: { 'Authorization': 'Bearer ' + this.authToken } }); if (!response.ok) { console.error('API keys usage trend API error:', response.status, response.statusText); return; } const data = await response.json(); if (data.success) { this.apiKeysTrendData = { data: data.data || [], topApiKeys: data.topApiKeys || [], totalApiKeys: data.totalApiKeys || 0 }; console.log('Loaded API keys trend data:', this.apiKeysTrendData); this.updateApiKeysUsageTrendChart(); } } catch (error) { console.error('Failed to load API keys usage trend:', error); } }, // 加载使用趋势数据 async loadUsageTrend() { console.log('Loading usage trend data, period:', this.trendPeriod, 'granularity:', this.trendGranularity, 'authToken:', !!this.authToken); try { let url = '/admin/usage-trend?'; if (this.trendGranularity === 'hour') { // 小时粒度,传递开始和结束时间 url += `granularity=hour`; if (this.dateFilter.customRange && this.dateFilter.customRange.length === 2) { url += `&startDate=${encodeURIComponent(this.dateFilter.customRange[0])}`; url += `&endDate=${encodeURIComponent(this.dateFilter.customRange[1])}`; } } else { // 天粒度,传递天数 url += `granularity=day&days=${this.trendPeriod}`; } const response = await fetch(url, { headers: { 'Authorization': 'Bearer ' + this.authToken } }); console.log('Usage trend response status:', response.status); if (!response.ok) { console.error('Usage trend API error:', response.status, response.statusText); const errorText = await response.text(); console.error('Error response:', errorText); return; } const data = await response.json(); console.log('Usage trend response data:', data); if (data.success) { this.trendData = data.data || []; console.log('Loaded trend data:', this.trendData.length, 'items'); this.updateUsageTrendChart(); } else { console.warn('Usage trend API returned success=false:', data); } } catch (error) { console.error('Failed to load usage trend:', error); } }, // 更新使用趋势图 updateUsageTrendChart() { // 检查Chart.js是否已加载 if (typeof Chart === 'undefined') { console.warn('Chart.js not loaded yet, retrying...'); setTimeout(() => this.updateUsageTrendChart(), 500); return; } // 严格检查DOM元素是否有效 if (!this.isElementValid('usageTrendChart')) { console.error('Usage trend chart canvas element not found or invalid'); return; } const ctx = document.getElementById('usageTrendChart'); // 安全销毁现有图表 if (this.usageTrendChart) { try { this.usageTrendChart.destroy(); } catch (error) { console.warn('Error destroying usage trend chart:', error); } this.usageTrendChart = null; } // 如果没有数据,不创建图表 if (!this.trendData || this.trendData.length === 0) { console.warn('No trend data available, skipping chart creation'); return; } // 再次验证元素在销毁后仍然有效 if (!this.isElementValid('usageTrendChart')) { console.error('Usage trend chart canvas element became invalid after cleanup'); return; } // 根据粒度格式化标签 const labels = this.trendData.map(item => { if (this.trendGranularity === 'hour') { // 小时粒度:从hour字段提取时间 if (item.hour) { const date = new Date(item.hour); return `${String(date.getHours()).padStart(2, '0')}:00`; } // 后备方案:从date字段解析 const [, time] = item.date.split(':'); return `${time}:00`; } else { // 天粒度:显示日期 return item.date; } }); const inputData = this.trendData.map(item => item.inputTokens || 0); const outputData = this.trendData.map(item => item.outputTokens || 0); const cacheCreateData = this.trendData.map(item => item.cacheCreateTokens || 0); const cacheReadData = this.trendData.map(item => item.cacheReadTokens || 0); const requestsData = this.trendData.map(item => item.requests || 0); const costData = this.trendData.map(item => item.cost || 0); try { // 最后一次检查元素有效性 if (!this.isElementValid('usageTrendChart')) { throw new Error('Canvas element is not valid for chart creation'); } this.usageTrendChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: '输入Token', data: inputData, borderColor: 'rgb(102, 126, 234)', backgroundColor: 'rgba(102, 126, 234, 0.1)', tension: 0.3 }, { label: '输出Token', data: outputData, borderColor: 'rgb(240, 147, 251)', backgroundColor: 'rgba(240, 147, 251, 0.1)', tension: 0.3 }, { label: '缓存创建Token', data: cacheCreateData, borderColor: 'rgb(59, 130, 246)', backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.3 }, { label: '缓存读取Token', data: cacheReadData, borderColor: 'rgb(147, 51, 234)', backgroundColor: 'rgba(147, 51, 234, 0.1)', tension: 0.3 }, { label: '费用 (USD)', data: costData, borderColor: 'rgb(34, 197, 94)', backgroundColor: 'rgba(34, 197, 94, 0.1)', tension: 0.3, yAxisID: 'y2' }, { label: '请求数', data: requestsData, borderColor: 'rgb(16, 185, 129)', backgroundColor: 'rgba(16, 185, 129, 0.1)', tension: 0.3, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, animation: false, // 禁用动画防止异步渲染问题 interaction: { mode: 'index', intersect: false, }, scales: { x: { type: 'category', display: true, title: { display: true, text: this.trendGranularity === 'hour' ? '时间' : '日期' }, ticks: { autoSkip: true, maxRotation: this.trendGranularity === 'hour' ? 45 : 0, minRotation: this.trendGranularity === 'hour' ? 45 : 0 } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'Token数量' } }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: '请求数' }, grid: { drawOnChartArea: false, } }, y2: { type: 'linear', display: false, // 隐藏费用轴,在tooltip中显示 position: 'right' } }, plugins: { legend: { position: 'top', }, tooltip: { mode: 'index', intersect: false, callbacks: { title: (tooltipItems) => { if (tooltipItems.length === 0) return ''; const index = tooltipItems[0].dataIndex; const item = this.trendData[index]; if (this.trendGranularity === 'hour' && item.hour) { // 小时粒度:显示完整的日期时间 const date = new Date(item.hour); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } // 天粒度:保持原有标签 return tooltipItems[0].label; }, label: function(context) { const label = context.dataset.label || ''; let value = context.parsed.y; if (label === '费用 (USD)') { // 格式化费用显示 if (value < 0.01) { return label + ': $' + value.toFixed(6); } else { return label + ': $' + value.toFixed(4); } } else if (label === '请求数') { return label + ': ' + value.toLocaleString(); } else { return label + ': ' + value.toLocaleString() + ' tokens'; } } } } } } }); } catch (error) { console.error('Error creating usage trend chart:', error); this.usageTrendChart = null; } }, // 更新API Keys使用趋势图 updateApiKeysUsageTrendChart() { // 检查Chart.js是否已加载 if (typeof Chart === 'undefined') { console.warn('Chart.js not loaded yet, retrying...'); setTimeout(() => this.updateApiKeysUsageTrendChart(), 500); return; } // 严格检查DOM元素是否有效 if (!this.isElementValid('apiKeysUsageTrendChart')) { console.error('API keys usage trend chart canvas element not found or invalid'); return; } const ctx = document.getElementById('apiKeysUsageTrendChart'); // 安全销毁现有图表 if (this.apiKeysUsageTrendChart) { try { this.apiKeysUsageTrendChart.destroy(); } catch (error) { console.warn('Error destroying API keys usage trend chart:', error); } this.apiKeysUsageTrendChart = null; } // 如果没有数据,不创建图表 if (!this.apiKeysTrendData.data || this.apiKeysTrendData.data.length === 0) { console.warn('No API keys trend data available, skipping chart creation'); return; } // 准备数据 const labels = this.apiKeysTrendData.data.map(item => { if (this.trendGranularity === 'hour') { const date = new Date(item.hour); return `${String(date.getHours()).padStart(2, '0')}:00`; } return item.date; }); // 获取所有API Key的数据集 const datasets = []; const colors = [ 'rgb(102, 126, 234)', 'rgb(240, 147, 251)', 'rgb(59, 130, 246)', 'rgb(147, 51, 234)', 'rgb(34, 197, 94)', 'rgb(251, 146, 60)', 'rgb(239, 68, 68)', 'rgb(16, 185, 129)', 'rgb(245, 158, 11)', 'rgb(236, 72, 153)' ]; // 只显示前10个使用量最多的API Key this.apiKeysTrendData.topApiKeys.forEach((apiKeyId, index) => { const data = this.apiKeysTrendData.data.map(item => { if (!item.apiKeys[apiKeyId]) return 0; return this.apiKeysTrendMetric === 'tokens' ? item.apiKeys[apiKeyId].tokens : item.apiKeys[apiKeyId].requests || 0; }); // 获取API Key名称 const apiKeyName = this.apiKeysTrendData.data.find(item => item.apiKeys[apiKeyId] )?.apiKeys[apiKeyId]?.name || `API Key ${apiKeyId}`; datasets.push({ label: apiKeyName, data: data, borderColor: colors[index % colors.length], backgroundColor: colors[index % colors.length] + '20', tension: 0.3, fill: false }); }); try { // 最后一次检查元素有效性 if (!this.isElementValid('apiKeysUsageTrendChart')) { throw new Error('Canvas element is not valid for chart creation'); } this.apiKeysUsageTrendChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, animation: false, // 禁用动画防止异步渲染问题 interaction: { mode: 'index', intersect: false, }, scales: { x: { type: 'category', display: true, title: { display: true, text: this.trendGranularity === 'hour' ? '时间' : '日期' }, ticks: { autoSkip: true, maxRotation: this.trendGranularity === 'hour' ? 45 : 0, minRotation: this.trendGranularity === 'hour' ? 45 : 0 } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: this.apiKeysTrendMetric === 'tokens' ? 'Token 数量' : '请求次数' }, ticks: { callback: function(value) { return value.toLocaleString(); } } } }, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 15 } }, tooltip: { mode: 'index', intersect: false, callbacks: { title: (tooltipItems) => { if (tooltipItems.length === 0) return ''; const index = tooltipItems[0].dataIndex; const item = this.apiKeysTrendData.data[index]; if (this.trendGranularity === 'hour' && item.hour) { const date = new Date(item.hour); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } return tooltipItems[0].label; }, label: (context) => { const label = context.dataset.label || ''; const value = context.parsed.y; const unit = this.apiKeysTrendMetric === 'tokens' ? ' tokens' : ' 次'; return label + ': ' + value.toLocaleString() + unit; } } } } } }); } catch (error) { console.error('Error creating API keys usage trend chart:', error); this.apiKeysUsageTrendChart = null; } }, // 切换API Key模型统计展开状态 toggleApiKeyModelStats(keyId) { if (!keyId) { console.warn('toggleApiKeyModelStats: keyId is null or undefined'); return; } console.log('Toggling API key model stats for:', keyId, 'current state:', this.expandedApiKeys[keyId]); if (this.expandedApiKeys[keyId]) { // 收起展开 this.expandedApiKeys = { ...this.expandedApiKeys }; delete this.expandedApiKeys[keyId]; } else { // 展开并加载数据 this.expandedApiKeys = { ...this.expandedApiKeys, [keyId]: true }; console.log('Expanded keys after toggle:', this.expandedApiKeys); this.loadApiKeyModelStats(keyId); } }, // 加载API Key的模型统计 async loadApiKeyModelStats(keyId, forceReload = false) { if (!keyId) { console.warn('loadApiKeyModelStats: keyId is null or undefined'); return; } // 如果已经有数据且不为空,且不是强制重新加载,则跳过加载 if (!forceReload && this.apiKeyModelStats[keyId] && this.apiKeyModelStats[keyId].length > 0) { console.log('API key model stats already loaded for:', keyId); return; } const filter = this.getApiKeyDateFilter(keyId); console.log('Loading API key model stats for:', keyId, 'period:', this.apiKeyModelPeriod, 'forceReload:', forceReload, 'authToken:', !!this.authToken); console.log('API Key date filter:', filter); // 清除现有数据以显示加载状态 if (forceReload) { const newStats = { ...this.apiKeyModelStats }; delete newStats[keyId]; this.apiKeyModelStats = newStats; } try { // 构建API请求URL,根据筛选类型传递不同参数 let url = '/admin/api-keys/' + keyId + '/model-stats'; const params = new URLSearchParams(); // 检查是否有具体的日期范围设置(包括快捷按钮设置的日期) if (filter.customStart && filter.customEnd) { // 有具体日期范围,使用自定义时间范围方式 params.append('startDate', filter.customStart); params.append('endDate', filter.customEnd); params.append('period', 'custom'); console.log('Using custom date range:', filter.customStart, 'to', filter.customEnd); } else { // 没有具体日期范围,使用预设期间(目前只有 today 会走这里) const period = filter.preset === 'today' ? 'daily' : 'monthly'; params.append('period', period); console.log('Using preset period:', period); } url += '?' + params.toString(); console.log('API request URL:', url); const response = await fetch(url, { headers: { 'Authorization': 'Bearer ' + this.authToken } }); console.log('API key model stats response status:', response.status); if (!response.ok) { console.error('API key model stats API error:', response.status, response.statusText); const errorText = await response.text(); console.error('Error response:', errorText); return; } const data = await response.json(); console.log('API key model stats response data:', data); if (data.success) { console.log('API response success, data:', data.data); console.log('Setting apiKeyModelStats for keyId:', keyId); // 确保响应式更新 - 创建新对象 const newStats = { ...this.apiKeyModelStats }; newStats[keyId] = data.data || []; this.apiKeyModelStats = newStats; console.log('Updated apiKeyModelStats:', this.apiKeyModelStats); console.log('Data for keyId', keyId, ':', this.apiKeyModelStats[keyId]); console.log('Data length:', this.apiKeyModelStats[keyId] ? this.apiKeyModelStats[keyId].length : 'undefined'); // 确保Vue知道数据已经更新 this.$nextTick(() => { console.log('Vue nextTick - stats should be visible now'); }); } else { console.warn('API key model stats API returned success=false:', data); } } catch (error) { console.error('Failed to load API key model stats:', error); } }, // 计算API Key模型使用百分比 calculateApiKeyModelPercentage(value, stats) { const total = stats.reduce((sum, stat) => sum + (stat.allTokens || 0), 0); if (total === 0) return 0; return Math.round((value / total) * 100); }, // 计算单个模型费用 calculateModelCost(stat) { // 优先使用后端返回的费用数据 if (stat.formatted && stat.formatted.total) { return stat.formatted.total; } // 如果后端没有返回费用数据,则使用简单估算(备用方案) const inputTokens = stat.inputTokens || 0; const outputTokens = stat.outputTokens || 0; const cacheCreateTokens = stat.cacheCreateTokens || 0; const cacheReadTokens = stat.cacheReadTokens || 0; // 使用通用估算价格(Claude 3.5 Sonnet价格作为默认) const inputCost = (inputTokens / 1000000) * 3.00; const outputCost = (outputTokens / 1000000) * 15.00; const cacheCreateCost = (cacheCreateTokens / 1000000) * 3.75; const cacheReadCost = (cacheReadTokens / 1000000) * 0.30; const totalCost = inputCost + outputCost + cacheCreateCost + cacheReadCost; if (totalCost < 0.000001) return '$0.000000'; if (totalCost < 0.01) return '$' + totalCost.toFixed(6); return '$' + totalCost.toFixed(4); }, // 计算API Key费用 calculateApiKeyCost(usage) { if (!usage || !usage.total) return '$0.000000'; // 使用后端返回的准确费用数据 if (usage.total.formattedCost) { return usage.total.formattedCost; } // 如果没有后端费用数据,返回默认值 return '$0.000000'; }, // 计算API Key费用数值(用于排序) calculateApiKeyCostNumber(usage) { if (!usage || !usage.total) return 0; // 使用后端返回的准确费用数据 if (usage.total.cost) { return usage.total.cost; } // 如果没有后端费用数据,返回0 return 0; }, // 初始化日期筛选器 initializeDateFilter() { console.log('Initializing date filter, default preset:', this.dateFilter.preset); // 根据默认的日期筛选设置正确的 dashboardModelPeriod if (this.dateFilter.preset === 'today') { this.dashboardModelPeriod = 'daily'; } else { this.dashboardModelPeriod = 'monthly'; } console.log('Set dashboardModelPeriod to:', this.dashboardModelPeriod); }, // 日期筛选方法 setDateFilterPreset(preset) { this.dateFilter.type = 'preset'; this.dateFilter.preset = preset; // 清除自定义日期范围 this.dateFilter.customStart = ''; this.dateFilter.customEnd = ''; // 根据预设计算并设置自定义时间框的值 const option = this.dateFilter.presetOptions.find(opt => opt.value === preset); if (option) { const now = new Date(); let startDate, endDate; if (this.trendGranularity === 'hour') { // 小时粒度的预设处理 if (preset === 'last24h') { endDate = new Date(now); startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); } else if (preset === 'yesterday') { // 昨天的00:00到23:59 startDate = new Date(now); startDate.setDate(startDate.getDate() - 1); startDate.setHours(0, 0, 0, 0); endDate = new Date(startDate); endDate.setHours(23, 59, 59, 999); } else if (preset === 'dayBefore') { // 前天的00:00到23:59 startDate = new Date(now); startDate.setDate(startDate.getDate() - 2); startDate.setHours(0, 0, 0, 0); endDate = new Date(startDate); endDate.setHours(23, 59, 59, 999); } } else { // 天粒度的预设处理(保持原有逻辑) endDate = new Date(now); startDate = new Date(now); startDate.setDate(now.getDate() - (option.days - 1)); startDate.setHours(0, 0, 0, 0); endDate.setHours(23, 59, 59, 999); } // 格式化为 Element Plus 需要的格式 const formatDate = (date) => { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + ' ' + String(date.getHours()).padStart(2, '0') + ':' + String(date.getMinutes()).padStart(2, '0') + ':' + String(date.getSeconds()).padStart(2, '0'); }; this.dateFilter.customRange = [ formatDate(startDate), formatDate(endDate) ]; } this.refreshChartsData(); }, // 获取今日日期字符串 getTodayDate() { return new Date().toISOString().split('T')[0]; }, // 获取自定义范围天数 getCustomRangeDays() { if (!this.dateFilter.customStart || !this.dateFilter.customEnd) return 0; const start = new Date(this.dateFilter.customStart); const end = new Date(this.dateFilter.customEnd); return Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; }, // 验证并设置自定义日期范围 validateAndSetCustomRange() { if (!this.dateFilter.customStart || !this.dateFilter.customEnd) return; const start = new Date(this.dateFilter.customStart); const end = new Date(this.dateFilter.customEnd); const today = new Date(); // 确保结束日期不晚于今天 if (end > today) { this.dateFilter.customEnd = this.getTodayDate(); end.setTime(today.getTime()); } // 确保开始日期不晚于结束日期 if (start > end) { this.dateFilter.customStart = this.dateFilter.customEnd; start.setTime(end.getTime()); } // 限制最大31天 const daysDiff = this.getCustomRangeDays(); if (daysDiff > 31) { // 自动调整开始日期,保持31天范围 const newStart = new Date(end); newStart.setDate(end.getDate() - 30); // 31天范围 this.dateFilter.customStart = newStart.toISOString().split('T')[0]; this.showToast('日期范围已自动调整为最大31天', 'warning', '范围限制'); } // 只有在都有效时才更新 if (this.dateFilter.customStart && this.dateFilter.customEnd) { this.dateFilter.type = 'custom'; this.refreshChartsData(); } }, setDateFilterCustom() { this.validateAndSetCustomRange(); }, // 一体化日期范围选择器相关方法 toggleDateRangePicker() { this.showDateRangePicker = !this.showDateRangePicker; }, getDateRangeDisplayText() { if (this.dateFilter.type === 'preset') { const option = this.dateFilter.presetOptions.find(opt => opt.value === this.dateFilter.preset); return option ? option.label : '自定义范围'; } else if (this.dateFilter.customStart && this.dateFilter.customEnd) { const start = new Date(this.dateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); const end = new Date(this.dateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); return start + ' - ' + end + ' (' + this.getCustomRangeDays() + '天)'; } return '选择日期范围'; }, getCustomDateRangeText() { if (this.dateFilter.type === 'custom' && this.dateFilter.customStart && this.dateFilter.customEnd) { const start = new Date(this.dateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); const end = new Date(this.dateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); return start + ' - ' + end; } return '自定义范围'; }, onDateRangeChange() { // 实时验证日期范围 if (this.dateFilter.customStart && this.dateFilter.customEnd) { const start = new Date(this.dateFilter.customStart); const end = new Date(this.dateFilter.customEnd); const today = new Date(); // 确保结束日期不晚于今天 if (end > today) { this.dateFilter.customEnd = this.getTodayDate(); } // 确保开始日期不晚于结束日期 if (start > end) { this.dateFilter.customStart = this.dateFilter.customEnd; } // 限制最大31天 const daysDiff = this.getCustomRangeDays(); if (daysDiff > 31) { const newStart = new Date(end); newStart.setDate(end.getDate() - 30); this.dateFilter.customStart = newStart.toISOString().split('T')[0]; } } }, clearDateRange() { this.dateFilter.customStart = ''; this.dateFilter.customEnd = ''; this.dateFilter.type = 'preset'; this.dateFilter.preset = '7days'; // 恢复默认 }, applyDateRange() { if (this.dateFilter.customStart && this.dateFilter.customEnd) { this.dateFilter.type = 'custom'; this.dateFilter.preset = ''; // 清除预设选择 this.showDateRangePicker = false; this.refreshChartsData(); } else { this.showToast('请选择完整的日期范围', 'warning', '日期范围'); } }, refreshChartsData() { // 根据当前日期筛选设置更新数据 let days; if (this.dateFilter.type === 'preset') { const option = this.dateFilter.presetOptions.find(opt => opt.value === this.dateFilter.preset); days = option ? option.days : 7; // 设置模型统计期间 if (this.dateFilter.preset === 'today') { this.dashboardModelPeriod = 'daily'; } else { this.dashboardModelPeriod = 'monthly'; } } else { // 自定义日期范围 const start = new Date(this.dateFilter.customStart); const end = new Date(this.dateFilter.customEnd); days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; this.dashboardModelPeriod = 'daily'; // 自定义范围使用日统计 } this.trendPeriod = days; // 重新加载数据 this.loadDashboardModelStats(); this.loadUsageTrend(); this.loadApiKeysUsageTrend(); }, // 设置趋势图粒度 setTrendGranularity(granularity) { console.log('Setting trend granularity to:', granularity); this.trendGranularity = granularity; // 根据粒度更新预设选项 if (granularity === 'hour') { this.dateFilter.presetOptions = [ { value: 'last24h', label: '近24小时', hours: 24 }, { value: 'yesterday', label: '昨天', hours: 24 }, { value: 'dayBefore', label: '前天', hours: 24 } ]; // 检查当前自定义日期范围是否超过24小时 if (this.dateFilter.type === 'custom' && this.dateFilter.customRange && this.dateFilter.customRange.length === 2) { const start = new Date(this.dateFilter.customRange[0]); const end = new Date(this.dateFilter.customRange[1]); const hoursDiff = (end - start) / (1000 * 60 * 60); if (hoursDiff > 24) { this.showToast('切换到小时粒度,日期范围已调整为近24小时', 'info'); this.dateFilter.preset = 'last24h'; this.setDateFilterPreset('last24h'); } } else if (['today', '7days', '30days'].includes(this.dateFilter.preset)) { // 预设不兼容,切换到近24小时 this.dateFilter.preset = 'last24h'; this.setDateFilterPreset('last24h'); } } else { // 恢复天粒度的选项 this.dateFilter.presetOptions = [ { value: 'today', label: '今天', days: 1 }, { value: '7days', label: '近7天', days: 7 }, { value: '30days', label: '近30天', days: 30 } ]; // 如果当前是小时粒度的预设,切换到天粒度的默认预设 if (['last24h', 'yesterday', 'dayBefore'].includes(this.dateFilter.preset)) { this.dateFilter.preset = '7days'; this.setDateFilterPreset('7days'); } else if (this.dateFilter.type === 'custom') { // 自定义日期范围在天粒度下通常不需要调整,因为24小时肯定在31天内 // 只需要重新加载数据 this.refreshChartsData(); return; } } // 重新加载数据 this.loadUsageTrend(); this.loadApiKeysUsageTrend(); }, // API Keys 日期筛选方法 setApiKeyDateFilterPreset(preset, keyId) { console.log('Setting API Key date filter preset:', preset, 'for keyId:', keyId); const filter = this.getApiKeyDateFilter(keyId); console.log('Before preset change - type:', filter.type, 'preset:', filter.preset); filter.type = 'preset'; filter.preset = preset; // 根据预设计算并设置具体的日期范围 const option = filter.presetOptions.find(opt => opt.value === preset); if (option) { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - (option.days - 1)); // 设置为日期字符串格式 YYYY-MM-DD filter.customStart = startDate.toISOString().split('T')[0]; filter.customEnd = today.toISOString().split('T')[0]; // 同时设置customRange,让日期选择器显示当前选中的范围 // 格式化为 Element Plus 需要的格式 const formatDate = (date) => { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + ' 00:00:00'; }; filter.customRange = [ formatDate(startDate), formatDate(today) ]; console.log('Set customStart to:', filter.customStart); console.log('Set customEnd to:', filter.customEnd); console.log('Set customRange to:', filter.customRange); } console.log('After preset change - type:', filter.type, 'preset:', filter.preset); // 立即加载数据 this.loadApiKeyModelStats(keyId, true); }, validateAndSetApiKeyCustomRange(keyId) { const filter = this.getApiKeyDateFilter(keyId); if (!filter.customStart || !filter.customEnd) return; const start = new Date(filter.customStart); const end = new Date(filter.customEnd); const today = new Date(); // 确保结束日期不晚于今天 if (end > today) { filter.customEnd = this.getTodayDate(); end.setTime(today.getTime()); } // 确保开始日期不晚于结束日期 if (start > end) { filter.customStart = filter.customEnd; start.setTime(end.getTime()); } // 限制最大31天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; if (daysDiff > 31) { // 自动调整开始日期,保持31天范围 const newStart = new Date(end); newStart.setDate(end.getDate() - 30); // 31天范围 filter.customStart = newStart.toISOString().split('T')[0]; this.showToast('日期范围已自动调整为最大31天', 'warning', '范围限制'); } // 只有在都有效时才更新 if (filter.customStart && filter.customEnd) { filter.type = 'custom'; this.apiKeyModelPeriod = 'daily'; // 自定义范围使用日统计 // 强制重新加载该API Key的数据 this.loadApiKeyModelStats(keyId, true); } }, // API Keys 一体化日期范围选择器相关方法 toggleApiKeyDateRangePicker() { this.showApiKeyDateRangePicker = !this.showApiKeyDateRangePicker; }, getApiKeyDateRangeDisplayText() { if (this.apiKeyDateFilter.type === 'preset') { const option = this.apiKeyDateFilter.presetOptions.find(opt => opt.value === this.apiKeyDateFilter.preset); return option ? option.label : '自定义'; } else if (this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) { const start = new Date(this.apiKeyDateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); const end = new Date(this.apiKeyDateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); return start + ' - ' + end; } return '自定义'; }, getApiKeyCustomDateRangeText() { if (this.apiKeyDateFilter.type === 'custom' && this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) { const start = new Date(this.apiKeyDateFilter.customStart).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); const end = new Date(this.apiKeyDateFilter.customEnd).toLocaleDateString('zh-CN', {month: 'short', day: 'numeric'}); return start + ' - ' + end; } return '自定义范围'; }, getApiKeyCustomRangeDays() { if (!this.apiKeyDateFilter.customStart || !this.apiKeyDateFilter.customEnd) return 0; const start = new Date(this.apiKeyDateFilter.customStart); const end = new Date(this.apiKeyDateFilter.customEnd); return Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; }, onApiKeyDateRangeChange() { if (this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) { const start = new Date(this.apiKeyDateFilter.customStart); const end = new Date(this.apiKeyDateFilter.customEnd); const today = new Date(); // 确保结束日期不晚于今天 if (end > today) { this.apiKeyDateFilter.customEnd = this.getTodayDate(); } // 确保开始日期不晚于结束日期 if (start > end) { this.apiKeyDateFilter.customStart = this.apiKeyDateFilter.customEnd; } // 限制最大31天 const daysDiff = this.getApiKeyCustomRangeDays(); if (daysDiff > 31) { const newStart = new Date(end); newStart.setDate(end.getDate() - 30); this.apiKeyDateFilter.customStart = newStart.toISOString().split('T')[0]; } } }, clearApiKeyDateRange() { this.apiKeyDateFilter.customStart = ''; this.apiKeyDateFilter.customEnd = ''; this.apiKeyDateFilter.type = 'preset'; this.apiKeyDateFilter.preset = '7days'; // 恢复默认 }, applyApiKeyDateRange(keyId) { if (this.apiKeyDateFilter.customStart && this.apiKeyDateFilter.customEnd) { this.apiKeyDateFilter.type = 'custom'; this.apiKeyDateFilter.preset = ''; // 清除预设选择 this.apiKeyModelPeriod = 'daily'; // 自定义范围使用日统计 this.showApiKeyDateRangePicker = false; // 强制重新加载该API Key的数据 this.loadApiKeyModelStats(keyId, true); } else { this.showToast('请选择完整的日期范围', 'warning', '日期范围'); } }, // Element Plus 日期选择器相关方法 // 禁用未来日期 disabledDate(date) { return date > new Date(); }, // 仪表盘自定义日期范围变化处理 onCustomDateRangeChange(value) { if (value && value.length === 2) { // 清除快捷选择的焦点状态 this.dateFilter.type = 'custom'; this.dateFilter.preset = ''; this.dateFilter.customStart = value[0].split(' ')[0]; this.dateFilter.customEnd = value[1].split(' ')[0]; // 检查日期范围限制 const start = new Date(value[0]); const end = new Date(value[1]); if (this.trendGranularity === 'hour') { // 小时粒度:限制24小时 const hoursDiff = (end - start) / (1000 * 60 * 60); if (hoursDiff > 24) { this.showToast('小时粒度下日期范围不能超过24小时', 'warning', '范围限制'); // 调整结束时间为开始时间后24小时 const newEnd = new Date(start.getTime() + 24 * 60 * 60 * 1000); const formatDate = (date) => { return date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + ' ' + String(date.getHours()).padStart(2, '0') + ':' + String(date.getMinutes()).padStart(2, '0') + ':' + String(date.getSeconds()).padStart(2, '0'); }; this.dateFilter.customRange = [ formatDate(start), formatDate(newEnd) ]; this.dateFilter.customEnd = newEnd.toISOString().split('T')[0]; return; } } else { // 天粒度:限制31天 const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; if (daysDiff > 31) { this.showToast('日期范围不能超过31天', 'warning', '范围限制'); // 重置为默认7天 this.dateFilter.customRange = null; this.dateFilter.type = 'preset'; this.dateFilter.preset = '7days'; return; } } this.refreshChartsData(); } else if (value === null) { // 清空时恢复默认 this.dateFilter.type = 'preset'; this.dateFilter.preset = this.trendGranularity === 'hour' ? 'last24h' : '7days'; this.dateFilter.customStart = ''; this.dateFilter.customEnd = ''; this.refreshChartsData(); } }, // API Keys自定义日期范围变化处理 onApiKeyCustomDateRangeChange(keyId) { return (value) => { const filter = this.getApiKeyDateFilter(keyId); console.log('API Key custom date range change:', value, 'for keyId:', keyId); console.log('Before change - type:', filter.type, 'preset:', filter.preset); // 更新 customRange 值 filter.customRange = value; if (value && value.length === 2) { // 清除快捷选择的焦点状态 filter.type = 'custom'; filter.preset = ''; // 清空preset确保快捷按钮失去焦点 filter.customStart = value[0].split(' ')[0]; filter.customEnd = value[1].split(' ')[0]; console.log('After change - type:', filter.type, 'preset:', filter.preset); console.log('Set customStart to:', filter.customStart); console.log('Set customEnd to:', filter.customEnd); // 检查日期范围限制 const start = new Date(value[0]); const end = new Date(value[1]); const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; if (daysDiff > 31) { this.showToast('日期范围不能超过31天', 'warning', '范围限制'); // 重置到默认7天 this.resetApiKeyDateFilterToDefault(keyId); return; } // 立即加载数据 console.log('Loading model stats after date range selection'); this.loadApiKeyModelStats(keyId, true); } else if (value === null || value === undefined) { // 清空时恢复默认7天 this.resetApiKeyDateFilterToDefault(keyId); console.log('Cleared - type:', filter.type, 'preset:', filter.preset); // 加载数据 this.loadApiKeyModelStats(keyId, true); } }; }, // 初始化API Key的日期筛选器 initApiKeyDateFilter(keyId) { const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 6); // 7天前 // Vue 3 直接赋值即可,不需要 $set this.apiKeyDateFilters[keyId] = { type: 'preset', preset: '7days', customStart: startDate.toISOString().split('T')[0], customEnd: today.toISOString().split('T')[0], customRange: null, presetOptions: this.apiKeyDateFilterDefaults.presetOptions }; }, // 获取API Key的日期筛选器状态 getApiKeyDateFilter(keyId) { if (!this.apiKeyDateFilters[keyId]) { this.initApiKeyDateFilter(keyId); } return this.apiKeyDateFilters[keyId]; }, // 重置API Key日期筛选器为默认值(内部使用) resetApiKeyDateFilterToDefault(keyId) { const filter = this.getApiKeyDateFilter(keyId); // 重置为默认的7天预设 filter.type = 'preset'; filter.preset = '7days'; filter.customRange = null; // 计算7天的具体日期范围 const today = new Date(); const startDate = new Date(today); startDate.setDate(today.getDate() - 6); // 7天前 filter.customStart = startDate.toISOString().split('T')[0]; filter.customEnd = today.toISOString().split('T')[0]; console.log(`Reset API Key ${keyId} to default 7 days range:`, filter.customStart, 'to', filter.customEnd); }, // 重置API Key日期筛选器并刷新 resetApiKeyDateFilter(keyId) { console.log('Resetting API Key date filter for keyId:', keyId); this.resetApiKeyDateFilterToDefault(keyId); // 使用nextTick确保状态更新后再加载数据 this.$nextTick(() => { this.loadApiKeyModelStats(keyId, true); }); this.showToast('已重置筛选条件并刷新数据', 'info', '重置成功'); }, // OEM设置相关方法 async loadOemSettings() { this.oemSettingsLoading = true; try { const result = await this.apiRequest('/admin/oem-settings'); if (result && result.success) { this.oemSettings = { ...this.oemSettings, ...result.data }; // 应用设置到页面 this.applyOemSettings(); } else { // 如果请求失败但不是因为认证问题,使用默认值 console.warn('Failed to load OEM settings, using defaults'); this.applyOemSettings(); } } catch (error) { console.error('Error loading OEM settings:', error); // 加载失败时也应用默认值,确保页面正常显示 this.applyOemSettings(); } finally { this.oemSettingsLoading = false; } }, async saveOemSettings() { // 验证输入 if (!this.oemSettings.siteName || this.oemSettings.siteName.trim() === '') { this.showToast('网站名称不能为空', 'error', '验证失败'); return; } if (this.oemSettings.siteName.length > 100) { this.showToast('网站名称不能超过100个字符', 'error', '验证失败'); return; } this.oemSettingsSaving = true; try { const result = await this.apiRequest('/admin/oem-settings', { method: 'PUT', body: JSON.stringify({ siteName: this.oemSettings.siteName.trim(), siteIcon: this.oemSettings.siteIcon.trim(), siteIconData: this.oemSettings.siteIconData.trim() }) }); if (result && result.success) { this.oemSettings = { ...this.oemSettings, ...result.data }; this.showToast('OEM设置保存成功', 'success', '保存成功'); // 应用设置到页面 this.applyOemSettings(); } else { this.showToast(result?.message || '保存失败', 'error', '保存失败'); } } catch (error) { console.error('Error saving OEM settings:', error); this.showToast('保存OEM设置失败', 'error', '保存失败'); } finally { this.oemSettingsSaving = false; } }, applyOemSettings() { // 更新网站标题 document.title = `${this.oemSettings.siteName} - 管理后台`; // 更新页面中的所有网站名称 const titleElements = document.querySelectorAll('.header-title'); titleElements.forEach(el => { el.textContent = this.oemSettings.siteName; }); // 应用自定义CSS this.applyCustomCss(); // 应用网站图标 this.applyFavicon(); }, applyCustomCss() { // 移除之前的自定义CSS const existingStyle = document.getElementById('custom-oem-css'); if (existingStyle) { existingStyle.remove(); } }, applyFavicon() { const iconData = this.oemSettings.siteIconData || this.oemSettings.siteIcon; if (iconData && iconData.trim()) { // 移除现有的favicon const existingFavicons = document.querySelectorAll('link[rel*="icon"]'); existingFavicons.forEach(link => link.remove()); // 添加新的favicon const link = document.createElement('link'); link.rel = 'icon'; // 根据数据类型设置适当的type if (iconData.startsWith('data:')) { // Base64数据 link.href = iconData; } else { // URL link.type = 'image/x-icon'; link.href = iconData; } document.head.appendChild(link); } }, resetOemSettings() { this.oemSettings = { siteName: 'Claude Relay Service', siteIcon: '', siteIconData: '', updatedAt: null }; }, // 处理图标文件上传 async handleIconUpload(event) { const file = event.target.files[0]; if (!file) return; // 验证文件大小 if (file.size > 350 * 1024) { // 350KB this.showToast('图标文件大小不能超过350KB', 'error', '文件太大'); return; } // 验证文件类型 const allowedTypes = ['image/x-icon', 'image/png', 'image/jpeg', 'image/svg+xml']; if (!allowedTypes.includes(file.type) && !file.name.endsWith('.ico')) { this.showToast('请选择有效的图标文件格式 (.ico, .png, .jpg, .svg)', 'error', '格式错误'); return; } try { // 读取文件为Base64 const reader = new FileReader(); reader.onload = (e) => { this.oemSettings.siteIconData = e.target.result; this.oemSettings.siteIcon = ''; // 清空URL this.showToast('图标上传成功', 'success', '上传成功'); }; reader.onerror = () => { this.showToast('图标文件读取失败', 'error', '读取失败'); }; reader.readAsDataURL(file); } catch (error) { console.error('Icon upload error:', error); this.showToast('图标上传过程中出现错误', 'error', '上传失败'); } }, // 移除图标 removeIcon() { this.oemSettings.siteIcon = ''; this.oemSettings.siteIconData = ''; if (this.$refs.iconFileInput) { this.$refs.iconFileInput.value = ''; } }, // 处理图标加载错误 handleIconError(event) { console.error('Icon load error'); event.target.style.display = 'none'; } } }); // 使用Element Plus,确保正确的语言包配置 if (typeof ElementPlus !== 'undefined') { app.use(ElementPlus, { locale: typeof ElementPlusLocaleZhCn !== 'undefined' ? ElementPlusLocaleZhCn : undefined }); } else { console.warn('Element Plus 未正确加载'); } // 挂载应用 app.mount('#app');