// 全局变量 let adminKey = ''; let keysData = []; let currentPage = 1; const keysPerPage = 10; let statsData = null; let dashboardRequestsChart = null; let requestsChart = null; let keysUsageChart = null; let systemSettings = { proxyHost: '', proxyPort: '', proxyUser: '', proxyPass: '' }; // token过期时间 let tokenExpiry = null; // JWT配置 const JWT_EXPIRATION = 3600; // 令牌有效期1小时(秒) // 导入预览数据 let importPreviewData = []; // DOM 加载完成后执行 document.addEventListener('DOMContentLoaded', () => { // 检查是否已登录 checkAuth(); // 绑定登录表单提交事件 document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); await handleLogin(); }); // 绑定退出登录事件 document.getElementById('logout-btn').addEventListener('click', (e) => { e.preventDefault(); handleLogout(); }); // 侧边栏切换 document.getElementById('sidebarCollapse').addEventListener('click', () => { const sidebar = document.getElementById('sidebar'); const content = document.getElementById('content'); sidebar.classList.toggle('active'); content.classList.toggle('active'); }); // 页面切换 document.querySelectorAll('[data-page]').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetPage = link.getAttribute('data-page'); // 验证token是否有效 if (!isTokenValid()) { handleLogout(); showToast('登录已过期,请重新登录', 'warning'); return; } // 更新导航链接激活状态 document.querySelectorAll('#sidebar li').forEach(li => li.classList.remove('active')); link.closest('li').classList.add('active'); // 更新页面标题 document.getElementById('current-page-title').textContent = link.textContent.trim(); // 隐藏所有页面,只显示目标页面 document.querySelectorAll('.content-page').forEach(page => { page.classList.remove('active'); page.style.display = 'none'; }); document.getElementById(`${targetPage}-page`).classList.add('active'); document.getElementById(`${targetPage}-page`).style.display = 'block'; // 加载相应页面的数据 switch (targetPage) { case 'dashboard': loadDashboard(); break; case 'keys': loadKeys(); break; case 'stats': loadStats(); break; case 'settings': loadSettings(); break; } }); }); // 仪表盘页面的"查看全部"按钮 document.querySelectorAll('#dashboard-page [data-page]').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetPage = link.getAttribute('data-page'); // 触发相应导航的点击事件 document.querySelector(`#sidebar a[data-page="${targetPage}"]`).click(); }); }); // 添加密钥按钮事件 document.getElementById('add-key-btn').addEventListener('click', () => { // 重置表单 document.getElementById('key-form').reset(); document.getElementById('key-id').value = ''; document.getElementById('keyModalLabel').textContent = '添加新密钥'; // 显示模态框 const keyModal = new bootstrap.Modal(document.getElementById('keyModal')); keyModal.show(); }); // 保存密钥按钮事件 document.getElementById('save-key-btn').addEventListener('click', saveKey); // 测试密钥按钮事件 document.getElementById('test-key-btn').addEventListener('click', testKey); // 全选/取消全选 document.getElementById('select-all').addEventListener('change', (e) => { const checkboxes = document.querySelectorAll('#keys-table-body input[type="checkbox"]'); checkboxes.forEach(checkbox => checkbox.checked = e.target.checked); }); // 批量操作事件 document.querySelectorAll('.batch-action').forEach(action => { action.addEventListener('click', (e) => { e.preventDefault(); const actionType = action.getAttribute('data-action'); const selectedIds = getSelectedKeyIds(); if (selectedIds.length === 0) { showToast('请至少选择一个密钥', 'warning'); return; } // 显示确认对话框 let message = ''; if (actionType === 'enable') { message = `确定要启用选中的 ${selectedIds.length} 个密钥吗?`; } else if (actionType === 'disable') { message = `确定要禁用选中的 ${selectedIds.length} 个密钥吗?`; } else if (actionType === 'delete') { message = `确定要删除选中的 ${selectedIds.length} 个密钥吗?此操作不可恢复!`; } showConfirmDialog(message, () => { batchOperation(actionType, selectedIds); }); }); }); // 搜索框事件 document.getElementById('search-keys').addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); if (searchTerm) { const filteredKeys = keysData.filter(key => key.name.toLowerCase().includes(searchTerm) || key.key.toLowerCase().includes(searchTerm) ); renderKeysTable(filteredKeys); } else { renderKeysTable(keysData); } }); // 管理员密钥显示/隐藏 document.getElementById('show-admin-key').addEventListener('click', () => { const adminKeyInput = document.getElementById('admin-key'); if (adminKeyInput.type === 'password') { adminKeyInput.type = 'text'; document.getElementById('show-admin-key').innerHTML = ''; } else { adminKeyInput.type = 'password'; document.getElementById('show-admin-key').innerHTML = ''; } }); // 复制管理员密钥 document.getElementById('copy-admin-key').addEventListener('click', () => { const adminKeyInput = document.getElementById('admin-key'); adminKeyInput.type = 'text'; adminKeyInput.select(); document.execCommand('copy'); adminKeyInput.type = 'password'; showToast('管理员密钥已复制到剪贴板', 'success'); }); // 设置表单提交 document.getElementById('settings-form').addEventListener('submit', async (e) => { e.preventDefault(); await saveSettings(); }); // 批量导入密钥按钮事件 document.getElementById('import-keys-btn').addEventListener('click', () => { // 重置导入表单和预览 document.getElementById('keys-text').value = ''; document.getElementById('keys-file').value = ''; document.getElementById('auto-enable-keys').checked = true; document.getElementById('import-preview').style.display = 'none'; document.getElementById('confirm-import-btn').disabled = true; importPreviewData = []; // 显示导入模态框 const importModal = new bootstrap.Modal(document.getElementById('importKeysModal')); importModal.show(); }); // 预览导入按钮事件 document.getElementById('preview-import-btn').addEventListener('click', previewImport); // 确认导入按钮事件 document.getElementById('confirm-import-btn').addEventListener('click', confirmImport); // 文件选择事件 document.getElementById('keys-file').addEventListener('change', async (e) => { if (e.target.files.length > 0) { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = function(event) { document.getElementById('keys-text').value = event.target.result; }; reader.readAsText(file); } }); }); // 检查是否已登录 function checkAuth() { const savedAdminKey = localStorage.getItem('adminKey'); const savedTokenExpiry = localStorage.getItem('tokenExpiry'); if (savedAdminKey && savedTokenExpiry) { adminKey = savedAdminKey; tokenExpiry = parseInt(savedTokenExpiry); // 检查token是否已过期 if (isTokenValid()) { showAdminPanel(); // 初始化其他数据 displayAdminKey(); loadDashboard(); // 初始加载仪表盘页面 // 如果token即将过期(小于5分钟),则刷新 const fiveMinutesInMs = 5 * 60 * 1000; if (tokenExpiry - new Date().getTime() < fiveMinutesInMs) { refreshToken(); } } else { // token已过期,清除存储并显示登录面板 handleLogout(); showToast('登录已过期,请重新登录', 'warning'); } } else { showLoginPanel(); } } // 处理登录 async function handleLogin() { const inputKey = document.getElementById('admin-key-input').value.trim(); if (!inputKey) { showLoginError('请输入管理员密钥'); return; } try { // 显示加载状态 document.querySelector('#login-form button').disabled = true; document.querySelector('#login-form button').innerHTML = ' 登录中...'; // 使用fetch API进行登录请求 const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ admin_key: inputKey }) }); // 恢复按钮状态 document.querySelector('#login-form button').disabled = false; document.querySelector('#login-form button').innerHTML = '登录'; if (response.ok) { const data = await response.json(); adminKey = data.token; tokenExpiry = new Date().getTime() + (data.expires_in * 1000); // 保存至本地存储 localStorage.setItem('adminKey', adminKey); localStorage.setItem('tokenExpiry', tokenExpiry.toString()); // 隐藏错误信息 document.getElementById('login-error').style.display = 'none'; // 显示管理面板 showAdminPanel(); displayAdminKey(); loadDashboard(); } else { // 登录失败 try { const error = await response.json(); showLoginError(error.detail || '登录失败,请检查管理员密钥'); } catch { showLoginError('登录失败,请检查管理员密钥'); } } } catch (error) { // 处理网络错误等异常 document.querySelector('#login-form button').disabled = false; document.querySelector('#login-form button').innerHTML = '登录'; console.error('验证管理员密钥失败:', error); showLoginError('验证失败,请稍后再试'); } } // 显示登录错误 function showLoginError(message) { const errorElement = document.getElementById('login-error'); errorElement.textContent = message; errorElement.style.display = 'block'; // 3秒后自动隐藏 setTimeout(() => { errorElement.style.display = 'none'; }, 3000); } // 处理登出 function handleLogout() { localStorage.removeItem('adminKey'); localStorage.removeItem('tokenExpiry'); adminKey = ''; tokenExpiry = null; showLoginPanel(); document.getElementById('admin-key-input').value = ''; } // 检查token是否有效 function isTokenValid() { if (!tokenExpiry) return false; // 检查token是否过期(留10秒缓冲) return new Date().getTime() < tokenExpiry - 10000; } // 刷新token async function refreshToken() { try { const response = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Authorization': `Bearer ${adminKey}`, 'Content-Type': 'application/json' } }); if (response.ok) { const data = await response.json(); adminKey = data.token; tokenExpiry = new Date().getTime() + (data.expires_in * 1000); localStorage.setItem('adminKey', adminKey); localStorage.setItem('tokenExpiry', tokenExpiry.toString()); return true; } else { return false; } } catch (error) { console.error('刷新token失败:', error); return false; } } // 显示登录面板 function showLoginPanel() { document.getElementById('login-container').style.display = 'flex'; document.getElementById('admin-panel').style.display = 'none'; // 清除可能存在的错误信息 document.getElementById('login-error').style.display = 'none'; } // 显示管理面板 function showAdminPanel() { document.getElementById('login-container').style.display = 'none'; document.getElementById('admin-panel').style.display = 'flex'; // 确保侧边栏正常显示 document.getElementById('sidebar').classList.remove('active'); document.getElementById('content').classList.remove('active'); // 初始化显示仪表盘页面,隐藏其他页面 document.querySelectorAll('.content-page').forEach(page => { page.classList.remove('active'); page.style.display = 'none'; }); document.getElementById('dashboard-page').classList.add('active'); document.getElementById('dashboard-page').style.display = 'block'; // 更新导航栏状态 document.querySelectorAll('#sidebar li').forEach(li => li.classList.remove('active')); document.querySelector('#sidebar li:first-child').classList.add('active'); // 更新页面标题 document.getElementById('current-page-title').textContent = '仪表盘'; } // 显示管理员密钥信息 function displayAdminKey() { // 设置到隐藏输入框 document.getElementById('admin-key').value = adminKey; // 显示密钥部分信息 const keyDisplay = adminKey.substring(0, 6) + '...' + adminKey.substring(adminKey.length - 4); document.getElementById('admin-key-display').textContent = '管理员: ' + keyDisplay; } // API请求包装函数,处理token刷新逻辑 async function apiRequest(url, options = {}) { // 检查token是否即将到期(剩余5分钟以内) const fiveMinutesInMs = 5 * 60 * 1000; if (tokenExpiry && (tokenExpiry - new Date().getTime() < fiveMinutesInMs)) { // 刷新token const refreshSuccess = await refreshToken(); if (!refreshSuccess) { // token刷新失败,需要重新登录 handleLogout(); showToast('登录已过期,请重新登录', 'warning'); return null; } } // 确保options中包含正确的headers options.headers = options.headers || {}; options.headers['Authorization'] = `Bearer ${adminKey}`; // 如果是POST/PUT请求且有body,确保设置正确的Content-Type if ((options.method === 'POST' || options.method === 'PUT') && options.body) { // 确保Content-Type正确设置 if (!options.headers['Content-Type']) { options.headers['Content-Type'] = 'application/json'; } } // 发送请求 try { const response = await fetch(url, options); // 处理401/403错误(未授权) if (response.status === 401 || response.status === 403) { // 尝试刷新token if (await refreshToken()) { // 刷新成功,使用新token重试请求 options.headers['Authorization'] = `Bearer ${adminKey}`; const retryResponse = await fetch(url, options); if (retryResponse.ok) { return await retryResponse.json(); } } // 刷新失败或重试失败,需要重新登录 handleLogout(); showToast('登录已过期,请重新登录', 'warning'); return null; } // 其他错误 if (!response.ok) { // 尝试解析错误消息 try { const errorData = await response.json(); throw new Error(`请求失败: ${response.status} - ${errorData.detail || errorData.message || response.statusText}`); } catch (e) { throw new Error(`请求失败: ${response.status} ${response.statusText}`); } } return await response.json(); } catch (error) { console.error('API请求错误:', error); showToast(`请求失败: ${error.message}`, 'error'); return null; } } // 加载密钥列表 async function loadKeys() { try { // 显示加载中 document.getElementById('keys-table-body').innerHTML = '加载中...'; const data = await apiRequest('/api/keys'); if (data) { keysData = data; renderKeysTable(keysData); } } catch (error) { console.error('加载密钥失败:', error); document.getElementById('keys-table-body').innerHTML = `加载失败: ${error.message}`; } } // 渲染密钥表格 function renderKeysTable(keys) { const tableBody = document.getElementById('keys-table-body'); tableBody.innerHTML = ''; if (keys.length === 0) { tableBody.innerHTML = '暂无密钥数据'; return; } // 分页处理 const startIndex = (currentPage - 1) * keysPerPage; const endIndex = startIndex + keysPerPage; const keysToShow = keys.slice(startIndex, endIndex); // 渲染表格行 keysToShow.forEach(key => { const row = document.createElement('tr'); // 创建时间和最后使用时间格式化 const createDate = key.created_at ? new Date(key.created_at * 1000).toLocaleString() : '未知'; const lastUsedDate = key.last_used ? new Date(key.last_used * 1000).toLocaleString() : '从未使用'; // 确定密钥状态 let statusText = ''; let statusClass = ''; let statusTitle = ''; let remainingTimeText = ''; if (key.temp_disabled_until) { // 临时禁用 - 使用格式化后的时间 statusText = '临时禁用'; statusClass = 'status-temp-disabled'; // 优先使用服务器返回的格式化时间 const enableTimeText = key.temp_disabled_until_formatted || new Date(key.temp_disabled_until * 1000).toLocaleString(); statusTitle = `将于 ${enableTimeText} 恢复`; // 如果有剩余时间信息,添加可读性更强的显示 if (key.temp_disabled_remaining !== undefined) { const remainingSecs = key.temp_disabled_remaining; if (remainingSecs > 0) { // 转换为 小时:分钟:秒 格式 const hours = Math.floor(remainingSecs / 3600); const minutes = Math.floor((remainingSecs % 3600) / 60); const seconds = remainingSecs % 60; remainingTimeText = `剩余 ${hours}小时${minutes}分钟`; } else { remainingTimeText = '即将恢复'; } } } else if (key.is_enabled) { // 启用 statusText = '启用'; statusClass = 'status-enabled'; } else { // 永久禁用 statusText = '禁用'; statusClass = 'status-disabled'; } row.innerHTML = ` ${key.name || '未命名'} ${key.key} ${statusText} ${key.temp_disabled_until ? `
启用于: ${key.temp_disabled_until_formatted || new Date(key.temp_disabled_until * 1000).toLocaleString()} ${remainingTimeText ? `
${remainingTimeText}` : ''}
` : ''} ${key.weight || 1} ${key.max_rpm || 60}/分钟 ${createDate} ${lastUsedDate} `; tableBody.appendChild(row); }); // 绑定操作按钮事件 bindTableEvents(); // 更新分页 renderPagination(keys.length); } // 绑定表格操作事件 function bindTableEvents() { // 编辑按钮 document.querySelectorAll('.edit-key').forEach(btn => { btn.addEventListener('click', async () => { const keyId = btn.getAttribute('data-id'); await loadKeyDetails(keyId); // 显示模态框 document.getElementById('keyModalLabel').textContent = '编辑密钥'; const keyModal = new bootstrap.Modal(document.getElementById('keyModal')); keyModal.show(); }); }); // 删除按钮 document.querySelectorAll('.delete-key').forEach(btn => { btn.addEventListener('click', () => { const keyId = btn.getAttribute('data-id'); const keyName = btn.closest('tr').children[1].textContent; showConfirmDialog(`确定要删除密钥 "${keyName}" 吗?此操作不可恢复!`, () => { deleteKey(keyId); }); }); }); // 复制按钮 document.querySelectorAll('.copy-key').forEach(btn => { btn.addEventListener('click', () => { const keyValue = btn.getAttribute('data-key'); navigator.clipboard.writeText(keyValue) .then(() => showToast('密钥已复制到剪贴板', 'success')) .catch(err => showToast('复制失败: ' + err, 'error')); }); }); } // 渲染分页 function renderPagination(totalKeys) { const pagination = document.getElementById('pagination'); pagination.innerHTML = ''; if (totalKeys <= keysPerPage) { return; } const totalPages = Math.ceil(totalKeys / keysPerPage); const ul = document.createElement('ul'); ul.className = 'pagination'; // 上一页 const prevLi = document.createElement('li'); prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; prevLi.innerHTML = `上一页`; ul.appendChild(prevLi); // 页码 for (let i = 1; i <= totalPages; i++) { const li = document.createElement('li'); li.className = `page-item ${currentPage === i ? 'active' : ''}`; li.innerHTML = `${i}`; ul.appendChild(li); } // 下一页 const nextLi = document.createElement('li'); nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`; nextLi.innerHTML = `下一页`; ul.appendChild(nextLi); pagination.appendChild(ul); // 绑定页码点击事件 document.querySelectorAll('.page-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); if (link.parentElement.classList.contains('disabled')) { return; } currentPage = parseInt(link.getAttribute('data-page')); renderKeysTable(keysData); }); }); } // 加载单个密钥详情 async function loadKeyDetails(keyId) { try { const keyData = await apiRequest(`/api/keys/${keyId}`); if (!keyData) return; // 填充表单 document.getElementById('key-id').value = keyData.id; document.getElementById('key-name').value = keyData.name || ''; document.getElementById('key-value').value = keyData.key || ''; document.getElementById('key-weight').value = keyData.weight || 1; document.getElementById('key-rate-limit').value = keyData.max_rpm || 60; document.getElementById('key-enabled').checked = keyData.is_enabled; document.getElementById('key-notes').value = keyData.notes || ''; } catch (error) { console.error('加载密钥详情失败:', error); showToast('加载密钥详情失败: ' + error.message, 'error'); } } // 保存密钥 async function saveKey() { try { const keyId = document.getElementById('key-id').value; const keyData = { name: document.getElementById('key-name').value, key_value: document.getElementById('key-value').value, weight: parseInt(document.getElementById('key-weight').value), rate_limit: parseInt(document.getElementById('key-rate-limit').value), is_enabled: document.getElementById('key-enabled').checked, notes: document.getElementById('key-notes').value }; if (!keyData.name || !keyData.key_value) { showToast('密钥名称和值不能为空', 'warning'); return; } let url, method; if (keyId) { // 更新现有密钥 url = `/api/keys/${keyId}`; method = 'PUT'; } else { // 创建新密钥 url = '/api/keys'; method = 'POST'; } const options = { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(keyData) }; const result = await apiRequest(url, options); if (!result) return; // 关闭模态框 const keyModal = bootstrap.Modal.getInstance(document.getElementById('keyModal')); keyModal.hide(); // 重新加载密钥列表 await loadKeys(); // 如果是仪表盘页面,也更新仪表盘数据 if (document.getElementById('dashboard-page').classList.contains('active')) { await loadDashboard(); } // 显示成功消息 showToast(keyId ? '密钥更新成功' : '密钥添加成功', 'success'); } catch (error) { console.error('保存密钥失败:', error); showToast('保存密钥失败: ' + error.message, 'error'); } } // 测试密钥连接 async function testKey() { try { const keyValue = document.getElementById('key-value').value; if (!keyValue) { showToast('请输入有效的密钥值', 'warning'); return; } const keyName = document.getElementById('key-name').value || '新密钥'; // 显示测试中状态 const testButton = document.getElementById('test-key-btn'); const originalText = testButton.innerHTML; testButton.innerHTML = ' 测试中...'; testButton.disabled = true; const testData = { name: keyName, key_value: keyValue }; const result = await apiRequest('/api/keys/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(testData) }); // 恢复按钮状态 testButton.innerHTML = originalText; testButton.disabled = false; if (!result) return; if (result.status === "success") { showToast(`测试成功: ${result.message || '密钥可用'}`, 'success'); } else { showToast(`测试失败: ${result.message || '密钥无法连接'}`, 'warning'); } } catch (error) { console.error('测试密钥失败:', error); showToast('测试密钥失败: ' + error.message, 'error'); // 恢复按钮状态 const testButton = document.getElementById('test-key-btn'); testButton.innerHTML = ' 测试连接'; testButton.disabled = false; } } // 删除密钥 async function deleteKey(keyId) { try { const result = await apiRequest(`/api/keys/${keyId}`, { method: 'DELETE' }); if (!result) return; // 重新加载密钥列表 await loadKeys(); // 如果是仪表盘页面,也更新仪表盘数据 if (document.getElementById('dashboard-page').classList.contains('active')) { await loadDashboard(); } // 显示成功消息 showToast('密钥删除成功', 'success'); } catch (error) { console.error('删除密钥失败:', error); showToast('删除密钥失败: ' + error.message, 'error'); } } // 批量操作 async function batchOperation(action, keyIds) { try { const operationData = { action: action, key_ids: keyIds }; const result = await apiRequest('/api/keys/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(operationData) }); if (!result) return; // 重新加载密钥列表 await loadKeys(); // 如果是仪表盘页面,也更新仪表盘数据 if (document.getElementById('dashboard-page').classList.contains('active')) { await loadDashboard(); } // 显示成功消息 let message = ''; if (action === 'enable') { message = '批量启用成功'; } else if (action === 'disable') { message = '批量禁用成功'; } else if (action === 'delete') { message = '批量删除成功'; } showToast(message, 'success'); } catch (error) { console.error('批量操作失败:', error); showToast('批量操作失败: ' + error.message, 'error'); } } // 获取所有选中的密钥ID function getSelectedKeyIds() { const checkboxes = document.querySelectorAll('#keys-table-body input[type="checkbox"]:checked'); return Array.from(checkboxes).map(checkbox => checkbox.getAttribute('data-id')); } // 加载统计数据 async function loadStats() { try { // 显示加载中状态 document.getElementById('total-requests').textContent = '加载中...'; document.getElementById('successful-requests').textContent = '加载中...'; document.getElementById('failed-requests').textContent = '加载中...'; document.getElementById('success-rate').textContent = '加载中...'; const data = await apiRequest('/api/stats'); if (data) { statsData = data; // 渲染统计卡片 renderStats(); // 渲染图表 renderRequestsChart(statsData.daily_usage); renderKeysUsageChart(statsData.keys_usage); } } catch (error) { console.error('加载统计数据失败:', error); // 创建模拟数据 statsData = { total_requests: Math.floor(Math.random() * 5000) + 1000, successful_requests: Math.floor(Math.random() * 4000) + 800, daily_usage: generateMockDailyUsage(), keys_usage: generateMockKeysUsage() }; // 渲染模拟数据 renderStats(); renderRequestsChart(statsData.daily_usage); renderKeysUsageChart(statsData.keys_usage); } } // 渲染统计卡片 function renderStats() { if (!statsData) { // 如果没有统计数据,使用默认值 statsData = { total_requests: 0, successful_requests: 0 }; } const totalRequests = statsData.total_requests || 0; const successfulRequests = statsData.successful_requests || 0; const failedRequests = totalRequests - successfulRequests; const successRate = totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(1) + '%' : '0%'; // 更新统计卡片 document.getElementById('total-requests').textContent = totalRequests; document.getElementById('successful-requests').textContent = successfulRequests; document.getElementById('failed-requests').textContent = failedRequests; document.getElementById('success-rate').textContent = successRate; } // 渲染请求趋势图 function renderRequestsChart(dailyUsage) { // 准备数据 const dates = Object.keys(dailyUsage || {}).sort(); const lastDates = dates.slice(-30); // 最近30天 const chartData = { labels: lastDates.map(date => date.substring(5)), // 只显示月-日 datasets: [{ label: '请求数', data: lastDates.map(date => dailyUsage[date] || 0), backgroundColor: 'rgba(13, 110, 253, 0.4)', borderColor: 'rgba(13, 110, 253, 1)', borderWidth: 2 }] }; // 销毁现有图表 if (requestsChart) { requestsChart.destroy(); } // 创建新图表 const ctx = document.getElementById('requests-chart').getContext('2d'); requestsChart = new Chart(ctx, { type: 'bar', data: chartData, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, ticks: { precision: 0 } } } } }); } // 渲染密钥使用分布图 function renderKeysUsageChart(keysUsage) { // 准备数据 const keys = Object.keys(keysUsage || {}); const values = keys.map(key => keysUsage[key]); // 只显示前8个密钥,其余归为"其他" let displayKeys = keys; let displayValues = values; if (keys.length > 8) { displayKeys = keys.slice(0, 7); displayValues = values.slice(0, 7); // 计算"其他"的总和 const othersSum = values.slice(7).reduce((sum, value) => sum + value, 0); displayKeys.push('其他'); displayValues.push(othersSum); } // 生成颜色 const backgroundColors = [ 'rgba(13, 110, 253, 0.7)', // 主蓝色 'rgba(220, 53, 69, 0.7)', // 红色 'rgba(25, 135, 84, 0.7)', // 绿色 'rgba(255, 193, 7, 0.7)', // 黄色 'rgba(111, 66, 193, 0.7)', // 紫色 'rgba(23, 162, 184, 0.7)', // 青色 'rgba(102, 16, 242, 0.7)', // 靛蓝色 'rgba(108, 117, 125, 0.7)' // 灰色 ]; const chartData = { labels: displayKeys, datasets: [{ data: displayValues, backgroundColor: backgroundColors, borderColor: backgroundColors.map(color => color.replace('0.7', '1')), borderWidth: 1 }] }; // 销毁现有图表 if (keysUsageChart) { keysUsageChart.destroy(); } // 创建新图表 const ctx = document.getElementById('keys-usage-chart').getContext('2d'); keysUsageChart = new Chart(ctx, { type: 'pie', data: chartData, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } } }); } // 加载系统设置 async function loadSettings() { try { const data = await apiRequest('/api/config'); if (!data) return; // 设置现有的代理设置 document.getElementById('proxy-host').value = data.PROXY_HOST || ''; document.getElementById('proxy-port').value = data.PROXY_PORT || ''; document.getElementById('proxy-user').value = data.PROXY_USER || ''; document.getElementById('proxy-pass').value = ''; // 设置基础URL document.getElementById('base-url').value = data.BASE_URL || ''; // 设置图片本地化配置 document.getElementById('image-localization').checked = data.IMAGE_LOCALIZATION || false; document.getElementById('image-save-dir').value = data.IMAGE_SAVE_DIR || 'src/static/images'; } catch (error) { console.error('加载设置失败:', error); showToast('获取设置失败: ' + error.message, 'error'); } } // 保存系统设置 async function saveSettings() { try { // 获取表单数据 const config = { PROXY_HOST: document.getElementById('proxy-host').value, PROXY_PORT: document.getElementById('proxy-port').value, PROXY_USER: document.getElementById('proxy-user').value, PROXY_PASS: document.getElementById('proxy-pass').value, BASE_URL: document.getElementById('base-url').value, IMAGE_LOCALIZATION: document.getElementById('image-localization').checked, IMAGE_SAVE_DIR: document.getElementById('image-save-dir').value || 'src/static/images', save_to_env: true }; // 调用API保存到服务器 const result = await apiRequest('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (!result) return; showToast('设置已保存', 'success'); } catch (error) { console.error('保存设置失败:', error); showToast('保存设置失败: ' + error.message, 'error'); } } // 显示确认对话框 function showConfirmDialog(message, callback) { document.getElementById('confirm-message').textContent = message; const confirmModal = new bootstrap.Modal(document.getElementById('confirmModal')); // 确认按钮事件 document.getElementById('confirm-btn').onclick = () => { confirmModal.hide(); if (typeof callback === 'function') { callback(); } }; confirmModal.show(); } // 显示提示消息 function showToast(message, type = 'info') { // 创建Toast元素 const toastId = 'toast-' + Date.now(); const toastEl = document.createElement('div'); toastEl.className = `toast align-items-center text-white bg-${type}`; toastEl.id = toastId; toastEl.setAttribute('role', 'alert'); toastEl.setAttribute('aria-live', 'assertive'); toastEl.setAttribute('aria-atomic', 'true'); toastEl.innerHTML = `
${message}
`; // 添加到容器 document.getElementById('toast-container').appendChild(toastEl); // 显示Toast const toast = new bootstrap.Toast(toastEl, { autohide: true, delay: 3000 }); toast.show(); // 监听隐藏事件,删除元素 toastEl.addEventListener('hidden.bs.toast', () => { toastEl.remove(); }); } // 加载仪表盘数据 async function loadDashboard() { try { // 先加载密钥统计 await loadKeyStats(); // 再加载使用统计 await loadUsageStats(); // 加载最近的密钥 await loadRecentKeys(); } catch (error) { console.error('加载仪表盘数据失败:', error); showToast('仪表盘数据加载失败', 'error'); } } // 加载密钥统计 async function loadKeyStats() { try { const data = await apiRequest('/api/keys'); if (!data) return; keysData = data; // 计算统计数据 const totalKeys = keysData.length; const activeKeys = keysData.filter(key => key.is_enabled).length; const disabledKeys = totalKeys - activeKeys; // 更新统计卡片 document.getElementById('total-keys').textContent = totalKeys; document.getElementById('active-keys').textContent = activeKeys; document.getElementById('disabled-keys').textContent = disabledKeys; } catch (error) { console.error('加载密钥统计失败:', error); } } // 加载使用统计 async function loadUsageStats() { try { const data = await apiRequest('/api/stats'); if (!data) return; // 更新仪表盘统计数据 const totalRequests = data.total_requests || 0; const successfulRequests = data.successful_requests || 0; const failedRequests = data.failed_requests || 0; // 更新统计卡片 document.getElementById('total-requests').textContent = totalRequests; document.getElementById('successful-requests').textContent = successfulRequests; document.getElementById('failed-requests').textContent = failedRequests; // 计算成功率 const successRate = totalRequests > 0 ? Math.round((successfulRequests / totalRequests) * 100) : 0; document.getElementById('success-rate').textContent = `${successRate}%`; // 获取今日使用量 const today = new Date().toISOString().split('T')[0]; const todayRequests = (data.daily_usage && data.daily_usage[today]) || 0; document.getElementById('today-requests').textContent = todayRequests; // 设置图表数据 statsData = data; // 更新图表 renderDashboardRequestsChart(data.daily_usage || {}); // 渲染请求趋势图和密钥使用分布图 renderRequestsChart(data.daily_usage || {}); renderKeysUsageChart(data.keys_usage || {}); console.log("图表数据已更新:", { daily_usage: data.daily_usage, keys_usage: data.keys_usage }); } catch (error) { console.error('加载使用统计失败:', error); } } // 加载最近的密钥 async function loadRecentKeys() { try { if (!keysData || keysData.length === 0) { const data = await apiRequest('/api/keys'); if (!data) return; keysData = data; } // 按创建时间排序,获取最近的5个 const recentKeys = [...keysData] .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) .slice(0, 5); const recentKeysList = document.getElementById('recent-keys-list'); recentKeysList.innerHTML = ''; if (recentKeys.length === 0) { recentKeysList.innerHTML = '
暂无密钥数据
'; return; } recentKeys.forEach(key => { const createDate = key.created_at ? new Date(key.created_at * 1000).toLocaleDateString() : '未知'; const li = document.createElement('a'); li.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center'; li.href = '#'; li.innerHTML = `
${key.name || '未命名'}
${key.key.substring(0, 10)}...
${key.is_enabled ? '启用' : '禁用'} ${createDate}
`; recentKeysList.appendChild(li); // 点击跳转到密钥管理页面 li.addEventListener('click', (e) => { e.preventDefault(); document.querySelector('#sidebar a[data-page="keys"]').click(); }); }); } catch (error) { console.error('加载最近密钥失败:', error); } } // 渲染仪表盘请求趋势图 function renderDashboardRequestsChart(dailyUsage) { // 获取最近7天的日期 const dates = []; const now = new Date(); for (let i = 6; i >= 0; i--) { const date = new Date(now); date.setDate(now.getDate() - i); dates.push(date.toISOString().split('T')[0]); } // 准备图表数据 const chartData = { labels: dates.map(date => date.substring(5)), // 只显示月-日 datasets: [{ label: '每日请求数', data: dates.map(date => (dailyUsage && dailyUsage[date]) || 0), borderColor: '#0d6efd', backgroundColor: 'rgba(13, 110, 253, 0.1)', borderWidth: 2, fill: true, tension: 0.3 }] }; // 销毁现有图表 if (dashboardRequestsChart) { dashboardRequestsChart.destroy(); } // 创建新图表 const ctx = document.getElementById('dashboard-requests-chart').getContext('2d'); dashboardRequestsChart = new Chart(ctx, { type: 'line', data: chartData, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, ticks: { precision: 0 } } } } }); } // 批量导入密钥预览 async function previewImport() { const keysText = document.getElementById('keys-text').value.trim(); if (!keysText) { showToast('请输入或上传密钥数据', 'warning'); return; } try { // 解析密钥数据 const lines = keysText.split('\n').filter(line => line.trim()); importPreviewData = []; console.log(`开始处理 ${lines.length} 行数据`); for (let i = 0; i < lines.length; i++) { try { const line = lines[i]; // 安全获取子字符串 const safeSubstring = (str, start, end) => { if (!str) return ''; return str.substring(start, Math.min(end, str.length)); }; console.log(`处理第 ${i+1} 行: ${safeSubstring(line, 0, 10)}...`); // 检查是否是单行密钥格式(不包含逗号) if (!line.includes(',')) { const keyValue = line.trim(); console.log(` 单行格式,密钥值: ${safeSubstring(keyValue, 0, 5)}...`); // 几乎不做验证 - 只要不是空字符串或太短就接受 if (!keyValue || keyValue.length < 5) { console.log(` 密钥太短,跳过`); continue; // 跳过太短的密钥 } // 为密钥自动生成名称(使用前5位) const keyName = `密钥_${safeSubstring(keyValue, 0, 5)}`; importPreviewData.push({ name: keyName, key: keyValue, weight: 1, rate_limit: 60, enabled: document.getElementById('auto-enable-keys').checked }); console.log(` 添加到预览数据,当前共 ${importPreviewData.length} 个`); continue; } // 处理标准格式(带逗号分隔) const parts = line.split(',').map(part => part.trim()); console.log(` 标准格式,分割后有 ${parts.length} 部分`); if (parts.length < 2) { console.log(` 部分数量不足2,跳过`); continue; // 跳过格式不正确的行 } const keyName = parts[0] || `密钥_未命名_${i+1}`; const keyValue = parts[1] || ''; const weight = parts.length > 2 ? parseInt(parts[2]) || 1 : 1; const rateLimit = parts.length > 3 ? parseInt(parts[3]) || 60 : 60; console.log(` 名称: ${keyName}, 密钥: ${safeSubstring(keyValue, 0, 5)}..., 权重: ${weight}, 速率: ${rateLimit}`); // 几乎不做验证 - 只要不是空字符串或太短就接受 if (!keyValue || keyValue.length < 5) { console.log(` 密钥太短,跳过`); continue; // 跳过太短的密钥 } importPreviewData.push({ name: keyName, key: keyValue, weight: weight, rate_limit: rateLimit, enabled: document.getElementById('auto-enable-keys').checked }); console.log(` 添加到预览数据,当前共 ${importPreviewData.length} 个`); } catch (lineError) { console.error(`处理第 ${i+1} 行时出错:`, lineError); // 继续处理下一行 continue; } } console.log(`处理完成,共有 ${importPreviewData.length} 个有效密钥`); // 显示预览 if (importPreviewData.length > 0) { renderImportPreview(); document.getElementById('preview-count').textContent = importPreviewData.length; document.getElementById('import-preview').style.display = 'block'; document.getElementById('confirm-import-btn').disabled = false; console.log('预览渲染完成,启用导入按钮'); } else { showToast('未找到有效的密钥数据', 'warning'); document.getElementById('import-preview').style.display = 'none'; document.getElementById('confirm-import-btn').disabled = true; console.log('未找到有效密钥,禁用导入按钮'); } } catch (error) { console.error('预览导入失败:', error); showToast('预览失败: ' + error.message, 'danger'); } } // 渲染导入预览表格 function renderImportPreview() { const tableBody = document.getElementById('preview-table-body'); tableBody.innerHTML = ''; // 限制预览最多显示10行 const displayData = importPreviewData.slice(0, 10); displayData.forEach(key => { const row = document.createElement('tr'); // 名称 const nameCell = document.createElement('td'); nameCell.textContent = key.name; row.appendChild(nameCell); // 密钥(部分隐藏) const keyCell = document.createElement('td'); const maskedKey = key.key.substring(0, 5) + '...' + key.key.substring(key.key.length - 4); keyCell.textContent = maskedKey; row.appendChild(keyCell); // 权重 const weightCell = document.createElement('td'); weightCell.textContent = key.weight; row.appendChild(weightCell); // 速率限制 const rateLimitCell = document.createElement('td'); rateLimitCell.textContent = key.rate_limit; row.appendChild(rateLimitCell); tableBody.appendChild(row); }); // 如果有更多未显示的行 if (importPreviewData.length > 10) { const moreRow = document.createElement('tr'); const moreCell = document.createElement('td'); moreCell.colSpan = 4; moreCell.textContent = `... 另外 ${importPreviewData.length - 10} 个密钥未在预览中显示`; moreCell.className = 'text-center text-muted'; moreRow.appendChild(moreCell); tableBody.appendChild(moreRow); } } // 确认导入密钥 async function confirmImport() { if (importPreviewData.length === 0) { showToast('没有要导入的密钥', 'warning'); return; } try { // 显示加载状态 const importBtn = document.getElementById('confirm-import-btn'); const originalText = importBtn.textContent; importBtn.disabled = true; importBtn.innerHTML = ' 导入中...'; console.log('准备发送批量导入请求,数据预览:', importPreviewData.slice(0, 2)); // 简化数据处理 const requestData = { action: "import", keys: [] }; // 手动将每个importPreviewData项转换为普通对象 for (const item of importPreviewData) { requestData.keys.push({ name: item.name || "", key: item.key || "", weight: item.weight || 1, rate_limit: item.rate_limit || 60, enabled: item.enabled !== undefined ? item.enabled : true }); } console.log('发送请求到 /api/keys/batch,请求数据:', requestData); // 发送请求 const response = await apiRequest('/api/keys/batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }); console.log('收到批量导入响应:', response); if (response && response.success) { // 隐藏模态框 bootstrap.Modal.getInstance(document.getElementById('importKeysModal')).hide(); // 刷新密钥列表 await loadKeys(); // 显示成功消息 const successCount = response.imported || importPreviewData.length; const skippedCount = response.skipped || 0; let message = `成功导入 ${successCount} 个密钥`; if (skippedCount > 0) { message += `,${skippedCount} 个重复密钥已跳过`; } showToast(message, 'success'); console.log('导入成功完成'); } else { const errorMsg = (response && response.message) ? response.message : '未知错误'; showToast('导入失败: ' + errorMsg, 'danger'); console.error('导入失败,服务器响应:', response); } } catch (error) { console.error('导入过程中发生异常:', error); showToast('导入失败: ' + error.message, 'danger'); } finally { // 恢复按钮状态 const importBtn = document.getElementById('confirm-import-btn'); importBtn.disabled = false; importBtn.textContent = '导入'; } }