Files
gemini-balance/app/static/js/keys_status.js

932 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 统计数据可视化交互效果
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
} else {
return new Promise((resolve, reject) => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
resolve();
} else {
reject(new Error('复制失败'));
}
} catch (err) {
document.body.removeChild(textArea);
reject(err);
}
});
}
}
// 添加统计项动画效果
function initStatItemAnimations() {
const statItems = document.querySelectorAll('.stat-item');
statItems.forEach(item => {
item.addEventListener('mouseenter', () => {
item.style.transform = 'scale(1.05)';
const icon = item.querySelector('.stat-icon');
if (icon) {
icon.style.opacity = '0.2';
icon.style.transform = 'scale(1.1) rotate(0deg)';
}
});
item.addEventListener('mouseleave', () => {
item.style.transform = '';
const icon = item.querySelector('.stat-icon');
if (icon) {
icon.style.opacity = '';
icon.style.transform = '';
}
});
});
}
function copyKeys(type) {
// 选择对应区域内所有可见的 li 元素下的 key-text span
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-text`);
const keys = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
if (keys.length === 0) {
showNotification('没有可复制的筛选后密钥', 'warning'); // 修改提示信息
return;
}
const keysText = keys.join('\n');
copyToClipboard(keysText)
.then(() => {
showNotification(`已成功复制 ${keys.length} 个筛选后的${type === 'valid' ? '有效' : '无效'}密钥`); // 修改提示信息
})
.catch((err) => {
console.error('无法复制文本: ', err);
showNotification('复制失败,请重试', 'error');
});
}
function copyKey(key) {
copyToClipboard(key)
.then(() => {
showNotification(`已成功复制密钥`);
})
.catch((err) => {
console.error('无法复制文本: ', err);
showNotification('复制失败,请重试', 'error');
});
}
// 移除 showCopyStatus 函数,因为它已被 showNotification 替代
async function verifyKey(key, button) {
try {
// 禁用按钮并显示加载状态
button.disabled = true;
const originalHtml = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 验证中';
try {
const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
method: 'POST'
});
const data = await response.json();
// 根据验证结果更新UI并显示模态提示框
if (data.success || data.status === 'valid') {
// 验证成功,显示成功结果
button.style.backgroundColor = '#27ae60';
// 使用结果模态框显示成功消息
showResultModal(true, '密钥验证成功');
// 模态框关闭时会自动刷新页面
} else {
// 验证失败,显示失败结果
const errorMsg = data.error || '密钥无效';
button.style.backgroundColor = '#e74c3c';
// 使用结果模态框显示失败消息,但不自动刷新页面
showResultModal(false, '密钥验证失败: ' + errorMsg, true); // 改为true以在关闭时刷新
}
} catch (fetchError) {
console.error('API请求失败:', fetchError);
showResultModal(false, '验证请求失败: ' + fetchError.message, true); // 改为true以在关闭时刷新
} finally {
// 1秒后恢复按钮原始状态
setTimeout(() => {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = '';
}, 1000);
}
} catch (error) {
console.error('验证失败:', error);
button.disabled = false;
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
showResultModal(false, '验证处理失败: ' + error.message, true); // 改为true以在关闭时刷新
}
}
async function resetKeyFailCount(key, button) {
try {
// 禁用按钮并显示加载状态
button.disabled = true;
const originalHtml = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
const response = await fetch(`/gemini/v1beta/reset-fail-count/${key}`, {
method: 'POST'
});
const data = await response.json();
// 根据重置结果更新UI
if (data.success) {
showNotification('失败计数重置成功');
// 成功时保留绿色背景一会儿
button.style.backgroundColor = '#27ae60';
// 稍后刷新页面
setTimeout(() => location.reload(), 1000);
} else {
const errorMsg = data.message || '重置失败';
showNotification('重置失败: ' + errorMsg, 'error');
// 失败时保留红色背景一会儿
button.style.backgroundColor = '#e74c3c';
}
// 立即恢复按钮状态,除非成功或失败时需要短暂显示颜色
if (!data.success) {
// 如果失败1秒后恢复按钮
setTimeout(() => {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = '';
}, 1000);
} else {
// 如果成功,在刷新前恢复按钮(虽然用户可能看不到)
button.innerHTML = originalHtml;
button.disabled = false;
// 背景色会在刷新时重置
}
} catch (error) {
console.error('重置失败:', error);
showNotification('重置请求失败: ' + error.message, 'error');
// 确保在捕获到错误时恢复按钮状态
button.innerHTML = originalHtml; // 需要确保 originalHtml 在此作用域可用
button.disabled = false;
button.innerHTML = '<i class="fas fa-redo-alt"></i> 重置';
}
}
function showResetModal(type) {
const modalElement = document.getElementById('resetModal');
const titleElement = document.getElementById('resetModalTitle');
const messageElement = document.getElementById('resetModalMessage');
const confirmButton = document.getElementById('confirmResetBtn');
// 获取当前筛选后可见的、且包含 data-fail-count 属性的密钥数量
// 根据密钥类型选择合适的选择器
let keySelector;
if (type === 'valid') {
// 对于有效密钥,可能需要基于失败次数筛选,保留 data-fail-count (虽然批量重置通常不需要筛选)
// 如果批量重置有效密钥也应重置所有可见的,可以将此行改为下面 else 中的选择器
keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"])`;
} else {
// 对于无效密钥,我们想要重置所有可见的无效密钥,不依赖 data-fail-count
keySelector = `#${type}Keys li:not([style*="display: none"])`;
}
const visibleKeyItems = document.querySelectorAll(keySelector);
const count = visibleKeyItems.length;
// 设置标题和消息
titleElement.textContent = '批量重置失败次数';
if (count > 0) {
messageElement.textContent = `确定要批量重置筛选出的 ${count}${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`;
confirmButton.disabled = false; // 确保按钮可用
} else {
messageElement.textContent = `当前没有筛选出可重置的${type === 'valid' ? '有效' : '无效'}密钥。`;
confirmButton.disabled = true; // 没有可重置的密钥时禁用确认按钮
}
// 设置确认按钮事件
confirmButton.onclick = () => executeResetAll(type);
// 显示模态框
modalElement.classList.remove('hidden');
}
function closeResetModal() {
document.getElementById('resetModal').classList.add('hidden');
}
// 触发显示模态框
function resetAllKeysFailCount(type, event) {
// 阻止事件冒泡
if (event) {
event.stopPropagation();
}
// 显示模态确认框
showResetModal(type);
}
// 执行批量重置
// 关闭模态框并根据参数决定是否刷新页面
function closeResultModal(reload = true) {
document.getElementById('resultModal').classList.add('hidden');
if (reload) {
location.reload(); // 操作完成后刷新页面
}
}
// 显示操作结果模态框
function showResultModal(success, message, autoReload = true) {
const modalElement = document.getElementById('resultModal');
const titleElement = document.getElementById('resultModalTitle');
const messageElement = document.getElementById('resultModalMessage');
const iconElement = document.getElementById('resultIcon');
const confirmButton = document.getElementById('resultModalConfirmBtn');
// 设置标题
titleElement.textContent = success ? '操作成功' : '操作失败';
// 设置图标
if (success) {
iconElement.innerHTML = '<i class="fas fa-check-circle text-success-500"></i>';
iconElement.className = 'text-5xl mb-3 text-success-500';
} else {
iconElement.innerHTML = '<i class="fas fa-times-circle"></i>';
iconElement.className = 'text-5xl mb-3 text-danger-500';
}
// 设置消息
// 支持长文本和换行内容插入到div而不是p
if (typeof message === 'string') {
// 如果内容包含换行或长文本,自动转为可滚动
messageElement.textContent = '';
messageElement.innerText = message;
} else if (message instanceof Node) {
messageElement.innerHTML = '';
messageElement.appendChild(message);
} else {
messageElement.textContent = String(message);
}
// 设置确认按钮点击事件
confirmButton.onclick = () => closeResultModal(autoReload);
// 显示模态框
modalElement.classList.remove('hidden');
}
async function executeResetAll(type) {
try {
// 关闭确认模态框
closeResetModal();
// 找到对应的重置按钮以显示加载状态
const resetButton = document.querySelector(`button[data-reset-type="${type}"]`);
if (!resetButton) {
showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`);
return;
}
// 获取筛选后可见的密钥
const visibleKeyItems = document.querySelectorAll(`#${type}Keys li:not([style*="display: none"]) .key-text`);
const keysToReset = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
if (keysToReset.length === 0) {
showNotification(`没有需要重置的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning');
return;
}
// 禁用按钮并显示加载状态
resetButton.disabled = true;
const originalHtml = resetButton.innerHTML;
resetButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 重置中';
try {
// 调用新的后端 API 来重置选定的密钥
const response = await fetch(`/gemini/v1beta/reset-selected-fail-counts`, { // 假设的新 API 端点
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ keys: keysToReset, key_type: type }) // 发送密钥列表和类型
});
if (!response.ok) {
// 尝试解析错误信息
let errorMsg = `服务器返回错误: ${response.status}`;
try {
const errorData = await response.json();
errorMsg = errorData.message || errorMsg;
} catch (e) {
// 如果解析失败,使用原始错误信息
}
throw new Error(errorMsg);
}
const data = await response.json();
// 根据重置结果显示模态框
if (data.success) {
const message = data.reset_count !== undefined ? // 检查 reset_count 是否存在
`成功重置 ${data.reset_count} 个筛选后的${type === 'valid' ? '有效' : '无效'}密钥的失败次数` :
`成功重置 ${keysToReset.length} 个筛选后的密钥`; // 如果后端没返回数量,使用前端计算的数量
showResultModal(true, message); // 成功后刷新页面
} else {
const errorMsg = data.message || '批量重置失败';
// 失败后不自动刷新页面,让用户看到错误信息
showResultModal(false, '批量重置失败: ' + errorMsg, false);
}
} catch (fetchError) {
console.error('API请求失败:', fetchError);
showResultModal(false, '批量重置请求失败: ' + fetchError.message, false); // 失败后不自动刷新
} finally {
// 恢复按钮状态
resetButton.innerHTML = originalHtml;
resetButton.disabled = false;
}
} catch (error) {
console.error('批量重置处理失败:', error);
showResultModal(false, '批量重置处理失败: ' + error.message, false); // 失败后不自动刷新
}
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
// 移除这个函数,因为它可能正在干扰按钮的显示
// HTML中已经设置了滚动按钮为flex显示不需要JavaScript额外控制
function updateScrollButtons() {
// 不执行任何操作
}
function refreshPage(button) {
button.classList.add('loading');
button.disabled = true;
setTimeout(() => {
window.location.reload();
}, 300);
}
// 重写切换区域显示/隐藏函数,以更好地支持新样式
function toggleSection(header, sectionId) {
const toggleIcon = header.querySelector('.toggle-icon');
const content = header.nextElementSibling;
if (toggleIcon && content) {
// 添加旋转动画
toggleIcon.classList.toggle('collapsed');
// 控制内容区域的可见性
if (!content.classList.contains('collapsed')) {
// 收起内容
content.style.maxHeight = '0px';
content.style.opacity = '0';
content.style.overflow = 'hidden';
content.classList.add('collapsed');
// 为动画添加延迟
setTimeout(() => {
content.style.padding = '0';
}, 100);
} else {
// 展开内容
content.classList.remove('collapsed');
content.style.padding = '1rem';
content.style.maxHeight = '2000px'; // 使用足够大的高度
content.style.opacity = '1';
// 为动画添加延迟
setTimeout(() => {
content.style.overflow = 'visible';
}, 300);
}
}
}
// 筛选有效密钥(根据失败次数阈值)
function filterValidKeys() {
const thresholdInput = document.getElementById('failCountThreshold');
const validKeyItems = document.querySelectorAll('#validKeys li');
// 读取阈值如果输入无效或为空则默认为0不过滤
const threshold = parseInt(thresholdInput.value, 10);
const filterThreshold = isNaN(threshold) || threshold < 0 ? 0 : threshold;
validKeyItems.forEach(item => {
const failCount = parseInt(item.dataset.failCount, 10);
// 如果失败次数大于等于阈值,则显示,否则隐藏
if (failCount >= filterThreshold) {
item.style.display = ''; // 显示
} else {
item.style.display = 'none'; // 隐藏
}
});
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 初始化统计区块动画
initStatItemAnimations();
// 添加数字滚动动画效果
const animateCounters = () => {
const statValues = document.querySelectorAll('.stat-value');
statValues.forEach(valueElement => {
const finalValue = parseInt(valueElement.textContent, 10);
if (!isNaN(finalValue)) {
// 保存原始值以便稍后恢复
if (!valueElement.dataset.originalValue) {
valueElement.dataset.originalValue = valueElement.textContent;
}
// 数字滚动动画
let startValue = 0;
const duration = 1500;
const startTime = performance.now();
const updateCounter = (currentTime) => {
const elapsedTime = currentTime - startTime;
if (elapsedTime < duration) {
const progress = elapsedTime / duration;
// 使用缓动函数使动画更自然
const easeOutValue = 1 - Math.pow(1 - progress, 3);
const currentValue = Math.floor(easeOutValue * finalValue);
valueElement.textContent = currentValue;
requestAnimationFrame(updateCounter);
} else {
// 恢复为原始值,以确保准确性
valueElement.textContent = valueElement.dataset.originalValue;
}
};
requestAnimationFrame(updateCounter);
}
});
};
// 在页面加载后启动数字动画
setTimeout(animateCounters, 300);
// 添加卡片悬停效果
document.querySelectorAll('.stats-card').forEach(card => {
card.addEventListener('mouseenter', () => {
card.classList.add('shadow-lg');
card.style.transform = 'translateY(-2px)';
});
card.addEventListener('mouseleave', () => {
card.classList.remove('shadow-lg');
card.style.transform = '';
});
});
// 监听展开/折叠事件
document.querySelectorAll('.stats-card-title').forEach(header => {
header.addEventListener('click', () => {
const card = header.closest('.stats-card');
if (card) {
card.classList.toggle('active');
}
});
});
// 添加筛选输入框事件监听
const thresholdInput = document.getElementById('failCountThreshold');
if (thresholdInput) {
// 使用 'input' 事件实时响应输入变化
thresholdInput.addEventListener('input', filterValidKeys);
// 初始加载时应用一次筛选
filterValidKeys();
}
// --- 批量验证相关函数 (明确挂载到 window) ---
window.showVerifyModal = function(type, event) {
// 阻止事件冒泡(如果从按钮点击触发)
if (event) {
event.stopPropagation();
}
const modalElement = document.getElementById('verifyModal');
const titleElement = document.getElementById('verifyModalTitle');
const messageElement = document.getElementById('verifyModalMessage');
const confirmButton = document.getElementById('confirmVerifyBtn');
// 根据密钥类型选择合适的选择器来确定可验证的密钥
let keySelector;
if (type === 'valid') {
// 对于有效密钥,可以根据需要决定是否只验证有失败记录的,或所有可见的
// 暂时保留 data-fail-count如果需要验证所有可见有效密钥则移除
keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"])`;
} else { // type === 'invalid'
// 对于无效密钥,应该验证所有可见的无效密钥
keySelector = `#${type}Keys li:not([style*="display: none"])`;
}
const visibleKeyItems = document.querySelectorAll(keySelector);
const count = visibleKeyItems.length;
// 设置标题和消息
titleElement.textContent = '批量验证密钥';
if (count > 0) {
messageElement.textContent = `确定要批量验证筛选出的 ${count}${type === 'valid' ? '有效' : '无效'}密钥吗?此操作可能需要一些时间。`;
confirmButton.disabled = false; // 确保按钮可用
} else {
messageElement.textContent = `当前没有筛选出可验证的${type === 'valid' ? '有效' : '无效'}密钥。`;
confirmButton.disabled = true; // 没有可验证的密钥时禁用确认按钮
}
// 设置确认按钮事件
confirmButton.onclick = () => executeVerifyAll(type);
// 显示模态框
modalElement.classList.remove('hidden');
}
window.closeVerifyModal = function() {
document.getElementById('verifyModal').classList.add('hidden');
}
window.executeVerifyAll = async function(type) {
try {
// 关闭确认模态框
closeVerifyModal();
// 找到对应的验证按钮以显示加载状态 (需要给按钮添加 data-verify-type 属性)
// 或者,我们可以暂时禁用所有按钮或显示一个全局加载指示器
// 这里我们暂时只记录日志实际UI反馈可以后续增强
console.log(`Starting bulk verification for ${type} keys...`);
// 根据密钥类型选择合适的选择器来获取待验证的密钥
let keySelector;
if (type === 'valid') {
// 同上,根据需求决定是否保留 data-fail-count
keySelector = `#${type}Keys li[data-fail-count]:not([style*="display: none"]) .key-text`;
} else { // type === 'invalid'
keySelector = `#${type}Keys li:not([style*="display: none"]) .key-text`;
}
const visibleKeyItems = document.querySelectorAll(keySelector);
const keysToVerify = Array.from(visibleKeyItems).map(span => span.dataset.fullKey);
if (keysToVerify.length === 0) {
showNotification(`没有需要验证的筛选后${type === 'valid' ? '有效' : '无效'}密钥`, 'warning');
return;
}
// 显示一个通用的加载提示
showNotification('开始批量验证,请稍候...', 'info');
// 调用新的后端 API 来验证选定的密钥
const response = await fetch(`/gemini/v1beta/verify-selected-keys`, { // 假设的新 API 端点
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ keys: keysToVerify }) // 只发送密钥列表
});
if (!response.ok) {
let errorMsg = `服务器返回错误: ${response.status}`;
try {
const errorData = await response.json();
errorMsg = errorData.message || errorMsg;
} catch (e) { /*忽略解析错误*/ }
throw new Error(errorMsg);
}
const data = await response.json();
// 根据验证结果显示模态框
if (data.success) {
// 可以在这里构建更详细的消息,例如显示多少有效多少无效
const message = `批量验证完成。有效: ${data.valid_count}, 无效: ${data.invalid_count}。页面即将刷新。`;
// 验证成功后通常需要刷新页面以更新状态
showResultModal(true, message, true); // autoReload = true
} else {
const errorMsg = data.message || '批量验证失败';
// 失败后不自动刷新
showResultModal(false, '批量验证失败: ' + errorMsg, false);
}
} catch (error) {
console.error('批量验证处理失败:', error);
// 失败后不自动刷新
showResultModal(false, '批量验证处理失败: ' + error.message, false);
} finally {
// 可以在这里移除加载指示器
console.log("Bulk verification process finished.");
}
}
// --- 滚动和页面控制 ---
// --- 自动刷新控制 ---
const autoRefreshToggle = document.getElementById('autoRefreshToggle');
const autoRefreshIntervalTime = 60000; // 60秒
let autoRefreshTimer = null;
function startAutoRefresh() {
if (autoRefreshTimer) return; // 防止重复启动
console.log('启动自动刷新...');
autoRefreshTimer = setInterval(() => {
console.log('自动刷新 keys_status 页面...');
location.reload();
}, autoRefreshIntervalTime);
}
function stopAutoRefresh() {
if (autoRefreshTimer) {
console.log('停止自动刷新...');
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
}
if (autoRefreshToggle) {
// 从 localStorage 读取状态并初始化
const isAutoRefreshEnabled = localStorage.getItem('autoRefreshEnabled') === 'true';
autoRefreshToggle.checked = isAutoRefreshEnabled;
if (isAutoRefreshEnabled) {
startAutoRefresh();
}
// 添加事件监听器
autoRefreshToggle.addEventListener('change', () => {
if (autoRefreshToggle.checked) {
localStorage.setItem('autoRefreshEnabled', 'true');
startAutoRefresh();
} else {
localStorage.setItem('autoRefreshEnabled', 'false');
stopAutoRefresh();
}
});
}
});
// Service Worker registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/service-worker.js')
.then(registration => {
console.log('ServiceWorker注册成功:', registration.scope);
})
.catch(error => {
console.log('ServiceWorker注册失败:', error);
});
});
}
function toggleKeyVisibility(button) {
const keyContainer = button.closest('.flex.items-center.gap-1');
const keyTextSpan = keyContainer.querySelector('.key-text');
const eyeIcon = button.querySelector('i');
const fullKey = keyTextSpan.dataset.fullKey;
const maskedKey = fullKey.substring(0, 4) + '...' + fullKey.substring(fullKey.length - 4);
if (keyTextSpan.textContent === maskedKey) {
keyTextSpan.textContent = fullKey;
eyeIcon.classList.remove('fa-eye');
eyeIcon.classList.add('fa-eye-slash');
button.title = '隐藏密钥';
} else {
keyTextSpan.textContent = maskedKey;
eyeIcon.classList.remove('fa-eye-slash');
eyeIcon.classList.add('fa-eye');
button.title = '显示密钥';
}
}
// --- API 调用详情模态框逻辑 ---
// 显示 API 调用详情模态框
async function showApiCallDetails(period) {
const modal = document.getElementById('apiCallDetailsModal');
const contentArea = document.getElementById('apiCallDetailsContent');
const titleElement = document.getElementById('apiCallDetailsModalTitle');
if (!modal || !contentArea || !titleElement) {
console.error('无法找到 API 调用详情模态框元素');
showNotification('无法显示详情,页面元素缺失', 'error');
return;
}
// 设置标题
let periodText = '';
switch (period) {
case '1m': periodText = '最近 1 分钟'; break;
case '1h': periodText = '最近 1 小时'; break;
case '24h': periodText = '最近 24 小时'; break;
default: periodText = '指定时间段';
}
titleElement.textContent = `${periodText} API 调用详情`;
// 显示模态框并设置加载状态
modal.classList.remove('hidden');
contentArea.innerHTML = `
<div class="text-center py-10">
<i class="fas fa-spinner fa-spin text-primary-600 text-3xl"></i>
<p class="text-gray-500 mt-2">加载中...</p>
</div>`;
try {
// 调用后端 API 获取数据
const response = await fetch(`/api/stats/details?period=${period}`);
if (!response.ok) {
throw new Error(`服务器错误: ${response.status}`);
}
const data = await response.json();
// 渲染数据
renderApiCallDetails(data, contentArea);
} catch (error) {
console.error('获取 API 调用详情失败:', error);
contentArea.innerHTML = `
<div class="text-center py-10 text-danger-500">
<i class="fas fa-exclamation-triangle text-3xl"></i>
<p class="mt-2">加载失败: ${error.message}</p>
</div>`;
}
}
// 关闭 API 调用详情模态框
function closeApiCallDetailsModal() {
const modal = document.getElementById('apiCallDetailsModal');
if (modal) {
modal.classList.add('hidden');
}
}
// 渲染 API 调用详情到模态框
function renderApiCallDetails(data, container) {
if (!data || data.length === 0) {
container.innerHTML = `
<div class="text-center py-10 text-gray-500">
<i class="fas fa-info-circle text-3xl"></i>
<p class="mt-2">该时间段内没有 API 调用记录。</p>
</div>`;
return;
}
// 创建表格
let tableHtml = `
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">密钥 (部分)</th>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型</th>
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
`;
// 填充表格行
data.forEach(call => {
const timestamp = new Date(call.timestamp).toLocaleString();
const keyDisplay = call.key ? `${call.key.substring(0, 4)}...${call.key.substring(call.key.length - 4)}` : 'N/A';
const statusClass = call.status === 'success' ? 'text-success-600' : 'text-danger-600';
const statusIcon = call.status === 'success' ? 'fa-check-circle' : 'fa-times-circle';
tableHtml += `
<tr>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-700">${timestamp}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 font-mono">${keyDisplay}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500">${call.model || 'N/A'}</td>
<td class="px-4 py-2 whitespace-nowrap text-sm ${statusClass}">
<i class="fas ${statusIcon} mr-1"></i>
${call.status}
</td>
</tr>
`;
});
tableHtml += `
</tbody>
</table>
`;
container.innerHTML = tableHtml;
}
// --- 密钥使用详情模态框逻辑 ---
// 显示密钥使用详情模态框
window.showKeyUsageDetails = async function(key) {
const modal = document.getElementById('keyUsageDetailsModal');
const contentArea = document.getElementById('keyUsageDetailsContent');
const titleElement = document.getElementById('keyUsageDetailsModalTitle');
const keyDisplay = key.substring(0, 4) + '...' + key.substring(key.length - 4);
if (!modal || !contentArea || !titleElement) {
console.error('无法找到密钥使用详情模态框元素');
showNotification('无法显示详情,页面元素缺失', 'error');
return;
}
// 设置标题
titleElement.textContent = `密钥 ${keyDisplay} - 最近24小时请求详情`;
// 显示模态框并设置加载状态
modal.classList.remove('hidden');
contentArea.innerHTML = `
<div class="text-center py-10">
<i class="fas fa-spinner fa-spin text-primary-600 text-3xl"></i>
<p class="text-gray-500 mt-2">加载中...</p>
</div>`;
try {
// 调用新的后端 API 获取数据
// 注意:后端需要实现 /api/key-usage-details/{key} 端点
const response = await fetch(`/api/key-usage-details/${key}`);
if (!response.ok) {
let errorMsg = `服务器错误: ${response.status}`;
try {
const errorData = await response.json();
errorMsg = errorData.detail || errorMsg; // 假设后端错误信息在 detail 字段
} catch (e) { /* 忽略解析错误 */ }
throw new Error(errorMsg);
}
const data = await response.json();
// 渲染数据
renderKeyUsageDetails(data, contentArea);
} catch (error) {
console.error('获取密钥使用详情失败:', error);
contentArea.innerHTML = `
<div class="text-center py-10 text-danger-500">
<i class="fas fa-exclamation-triangle text-3xl"></i>
<p class="mt-2">加载失败: ${error.message}</p>
</div>`;
}
}
// 关闭密钥使用详情模态框
window.closeKeyUsageDetailsModal = function() {
const modal = document.getElementById('keyUsageDetailsModal');
if (modal) {
modal.classList.add('hidden');
}
}
// 渲染密钥使用详情到模态框 (这个函数主要由 showKeyUsageDetails 调用,不一定需要全局,但保持一致性)
window.renderKeyUsageDetails = function(data, container) {
// data 预期格式: { "model_name1": count1, "model_name2": count2, ... }
if (!data || Object.keys(data).length === 0) {
container.innerHTML = `
<div class="text-center py-10 text-gray-500">
<i class="fas fa-info-circle text-3xl"></i>
<p class="mt-2">该密钥在最近24小时内没有调用记录。</p>
</div>`;
return;
}
// 创建表格
let tableHtml = `
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型名称</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">调用次数 (24h)</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
`;
// 排序模型(可选,按调用次数降序)
const sortedModels = Object.entries(data).sort(([, countA], [, countB]) => countB - countA);
// 填充表格行
sortedModels.forEach(([model, count]) => {
tableHtml += `
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${model}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${count}</td>
</tr>
`;
});
tableHtml += `
</tbody>
</table>
`;
container.innerHTML = tableHtml;
}