mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-04 23:19:17 +08:00
feat(api,ui): 新增按Key调用详情与错误日志查找并联动前端
引入按密钥维度的请求详情及错误日志关联,新增错误日志精确 查找接口,并扩展统计时间维度,提升故障定位与可观测性。 - 新增 /api/logs/errors/lookup 接口:支持按 gemini_key / timestamp / status_code 与时间窗口查找最接近的错误日志;ErrorLogDetailResponse 增加 error_code 字段 - Stats 接口增强:get_api_call_details 返回 status_code、latency_ms, 并在失败时尝试匹配 error_log_id;新增 /api/stats/key-details 获取指 定密钥调用详情;新增 8h 时间段 - DB 层:add_error_log 支持传入 request_datetime(默认使用 UTC);新增 find_error_log_by_info 封装按 key/时间窗口/状态码的查询 - 前端 keys_status:趋势图支持 8 小时区间;调用详情表新增状态码/耗时与 失败详情按钮;可按 key 查看期内调用详情并查看匹配错误日志;优化统计 摘要展示与模态层级(z-index) - OpenAIChatService:错误记录携带请求时间;改进日志与健壮性处理
This commit is contained in:
+225
-66
@@ -346,7 +346,8 @@ function showResetModal(type) {
|
||||
// 设置确认按钮事件
|
||||
confirmButton.onclick = () => executeResetAll(type);
|
||||
|
||||
// 显示模态框
|
||||
// 显示模态框,确保位于最上层
|
||||
modalElement.style.zIndex = '1001';
|
||||
modalElement.classList.remove("hidden");
|
||||
}
|
||||
|
||||
@@ -1508,6 +1509,12 @@ function bucketizeDetails(period, details) {
|
||||
const HH = String(d.getHours()).padStart(2,'0');
|
||||
const mm = String(d.getMinutes()).padStart(2,'0');
|
||||
return `${HH}:${mm}`;
|
||||
} else if (period === '8h') {
|
||||
// bucket by hour for 8h window (same as 24h)
|
||||
const MM = String(d.getMonth()+1).padStart(2,'0');
|
||||
const DD = String(d.getDate()).padStart(2,'0');
|
||||
const HH = String(d.getHours()).padStart(2,'0');
|
||||
return `${MM}-${DD} ${HH}:00`;
|
||||
} else {
|
||||
// 24h: bucket by hour
|
||||
const MM = String(d.getMonth()+1).padStart(2,'0');
|
||||
@@ -1553,11 +1560,11 @@ async function renderApiChart(period) {
|
||||
}
|
||||
|
||||
function initChartControls() {
|
||||
const btn1m = document.getElementById('chartBtn1m');
|
||||
const btn1h = document.getElementById('chartBtn1h');
|
||||
const btn8h = document.getElementById('chartBtn8h');
|
||||
const btn24h = document.getElementById('chartBtn24h');
|
||||
const setActive = (activeBtn) => {
|
||||
[btn1m, btn1h, btn24h].forEach(btn => {
|
||||
[btn1h, btn8h, btn24h].forEach(btn => {
|
||||
if (!btn) return;
|
||||
if (btn === activeBtn) {
|
||||
btn.classList.remove('bg-gray-200');
|
||||
@@ -1569,8 +1576,8 @@ function initChartControls() {
|
||||
});
|
||||
};
|
||||
|
||||
if (btn1m) btn1m.addEventListener('click', async () => { setActive(btn1m); await renderApiChart('1m'); });
|
||||
if (btn1h) btn1h.addEventListener('click', async () => { setActive(btn1h); await renderApiChart('1h'); });
|
||||
if (btn8h) btn8h.addEventListener('click', async () => { setActive(btn8h); await renderApiChart('8h'); });
|
||||
if (btn24h) btn24h.addEventListener('click', async () => { setActive(btn24h); await renderApiChart('24h'); });
|
||||
|
||||
// default period
|
||||
@@ -1844,6 +1851,82 @@ async function showApiCallDetails(
|
||||
}
|
||||
}
|
||||
|
||||
// 获取并显示错误日志详情(通过日志ID)
|
||||
async function fetchAndShowErrorDetail(logId) {
|
||||
try {
|
||||
const detail = await fetchAPI(`/api/logs/errors/${logId}/details`);
|
||||
if (!detail) {
|
||||
showResultModal(false, `未找到日志 ${logId}`, false);
|
||||
return;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
container.className = 'space-y-3 text-sm';
|
||||
const basic = document.createElement('div');
|
||||
basic.innerHTML = `
|
||||
<div><span class="font-semibold">Key:</span> ${detail.gemini_key ? detail.gemini_key.substring(0,4)+'...'+detail.gemini_key.slice(-4) : 'N/A'}</div>
|
||||
<div><span class="font-semibold">模型:</span> ${detail.model_name || 'N/A'}</div>
|
||||
<div><span class="font-semibold">时间:</span> ${detail.request_time ? new Date(detail.request_time).toLocaleString() : 'N/A'}</div>
|
||||
<div><span class="font-semibold">错误类型:</span> ${detail.error_type || 'N/A'}</div>
|
||||
`;
|
||||
const codeBlock = document.createElement('pre');
|
||||
codeBlock.className = 'bg-red-50 border border-red-200 rounded p-3 whitespace-pre-wrap break-words text-red-700';
|
||||
codeBlock.textContent = detail.error_log || '无错误日志内容';
|
||||
const reqBlock = document.createElement('pre');
|
||||
reqBlock.className = 'bg-gray-50 border border-gray-200 rounded p-3 whitespace-pre-wrap break-words';
|
||||
reqBlock.textContent = detail.request_msg || '';
|
||||
container.appendChild(basic);
|
||||
container.appendChild(codeBlock);
|
||||
if (detail.request_msg) container.appendChild(reqBlock);
|
||||
showResultModal(false, container, false);
|
||||
} catch (e) {
|
||||
showResultModal(false, `加载日志详情失败: ${e.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:根据 key / 状态码 / 时间窗口(±100秒) 查询并显示错误日志详情
|
||||
async function fetchAndShowErrorDetailByInfo(geminiKey, statusCode, timestampISO) {
|
||||
try {
|
||||
if (!geminiKey || !timestampISO) {
|
||||
showResultModal(false, '缺少必要参数,无法查询错误详情', false);
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.set('gemini_key', geminiKey);
|
||||
params.set('timestamp', timestampISO);
|
||||
if (statusCode !== null && statusCode !== undefined) {
|
||||
params.set('status_code', String(statusCode));
|
||||
}
|
||||
params.set('window_seconds', '100');
|
||||
const detail = await fetchAPI(`/api/logs/errors/lookup?${params.toString()}`);
|
||||
if (!detail) {
|
||||
showResultModal(false, '未找到匹配的错误日志', false);
|
||||
return;
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
container.className = 'space-y-3 text-sm';
|
||||
const basic = document.createElement('div');
|
||||
basic.innerHTML = `
|
||||
<div><span class="font-semibold">Key:</span> ${detail.gemini_key ? detail.gemini_key.substring(0,4)+'...'+detail.gemini_key.slice(-4) : 'N/A'}</div>
|
||||
<div><span class="font-semibold">模型:</span> ${detail.model_name || 'N/A'}</div>
|
||||
<div><span class="font-semibold">时间:</span> ${detail.request_time ? new Date(detail.request_time).toLocaleString() : 'N/A'}</div>
|
||||
<div><span class="font-semibold">错误码:</span> ${detail.error_code ?? 'N/A'}</div>
|
||||
<div><span class="font-semibold">错误类型:</span> ${detail.error_type || 'N/A'}</div>
|
||||
`;
|
||||
const codeBlock = document.createElement('pre');
|
||||
codeBlock.className = 'bg-red-50 border border-red-200 rounded p-3 whitespace-pre-wrap break-words text-red-700';
|
||||
codeBlock.textContent = detail.error_log || '无错误日志内容';
|
||||
const reqBlock = document.createElement('pre');
|
||||
reqBlock.className = 'bg-gray-50 border border-gray-200 rounded p-3 whitespace-pre-wrap break-words';
|
||||
reqBlock.textContent = detail.request_msg || '';
|
||||
container.appendChild(basic);
|
||||
container.appendChild(codeBlock);
|
||||
if (detail.request_msg) container.appendChild(reqBlock);
|
||||
showResultModal(false, container, false);
|
||||
} catch (e) {
|
||||
showResultModal(false, `加载日志详情失败: ${e.message}`, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 API 调用详情模态框
|
||||
function closeApiCallDetailsModal() {
|
||||
const modal = document.getElementById("apiCallDetailsModal");
|
||||
@@ -1867,23 +1950,33 @@ function renderApiCallDetails(
|
||||
successCalls !== undefined &&
|
||||
failureCalls !== undefined
|
||||
) {
|
||||
const total = Number(totalCalls) || 0;
|
||||
const succ = Number(successCalls) || 0;
|
||||
const fail = Number(failureCalls) || 0;
|
||||
const denom = total > 0 ? total : succ + fail;
|
||||
const succRate = denom > 0 ? ((succ / denom) * 100).toFixed(1) : '0.0';
|
||||
const failRate = denom > 0 ? ((fail / denom) * 100).toFixed(1) : '0.0';
|
||||
|
||||
summaryHtml = `
|
||||
<div class="mb-4 p-3 bg-white dark:bg-gray-700 rounded-lg">
|
||||
<h4 class="font-semibold text-gray-700 dark:text-gray-200 mb-2 text-md border-b pb-1.5 dark:border-gray-600">期间调用概览:</h4>
|
||||
<div class="grid grid-cols-3 gap-2 text-center">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">总计</p>
|
||||
<p class="text-lg font-bold text-primary-600 dark:text-primary-400">${totalCalls}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">成功</p>
|
||||
<p class="text-lg font-bold text-success-600 dark:text-success-400">${successCalls}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">失败</p>
|
||||
<p class="text-lg font-bold text-danger-600 dark:text-danger-400">${failureCalls}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 rounded-lg overflow-hidden">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">总计</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">成功</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">失败</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">成功率</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm font-bold text-primary-600 dark:text-primary-400">${totalCalls}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm font-bold text-success-600 dark:text-success-400">${successCalls}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm font-bold text-danger-600 dark:text-danger-400">${failureCalls}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm font-bold text-success-600 dark:text-success-400">${succRate}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1907,7 +2000,10 @@ function renderApiCallDetails(
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时间</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">密钥 (部分)</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">模型</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态码</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">耗时(ms)</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态</th>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">详情</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
@@ -1928,17 +2024,25 @@ function renderApiCallDetails(
|
||||
const statusIcon =
|
||||
call.status === "success" ? "fa-check-circle" : "fa-times-circle";
|
||||
|
||||
const detailsBtn =
|
||||
call.status === "failure"
|
||||
? `<button class="px-2 py-1 rounded bg-red-100 hover:bg-red-200 text-red-700 text-xs" onclick="fetchAndShowErrorDetailByInfo('${call.key}', ${call.status_code ?? 'null'}, '${call.timestamp}')">
|
||||
<i class="fas fa-info-circle mr-1"></i>详情
|
||||
</button>`
|
||||
: "-";
|
||||
|
||||
tableHtml += `
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">${timestamp}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">${keyDisplay}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">${
|
||||
call.model || "N/A"
|
||||
}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">${call.model || "N/A"}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">${call.status_code ?? "-"}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">${call.latency_ms ?? "-"}</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm ${statusClass}">
|
||||
<i class="fas ${statusIcon} mr-1"></i>
|
||||
${call.status}
|
||||
</td>
|
||||
<td class="px-4 py-2 whitespace-nowrap text-sm">${detailsBtn}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
@@ -1967,67 +2071,122 @@ window.showKeyUsageDetails = async function (key) {
|
||||
return;
|
||||
}
|
||||
|
||||
// renderKeyUsageDetails 变为 showKeyUsageDetails 的局部函数
|
||||
function renderKeyUsageDetails(data, container) {
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
// 构建内容框架(时间范围按钮 + 图表 + 表格容器)
|
||||
const controlsHtml = `
|
||||
<div class="flex items-center gap-2 mb-3 text-xs">
|
||||
<button id="keyBtn1h" class="px-2 py-1 rounded bg-gray-200 hover:bg-gray-300 text-gray-700">1小时</button>
|
||||
<button id="keyBtn8h" class="px-2 py-1 rounded bg-gray-200 hover:bg-gray-300 text-gray-700">8小时</button>
|
||||
<button id="keyBtn24h" class="px-2 py-1 rounded bg-gray-200 hover:bg-gray-300 text-gray-700">24小时</button>
|
||||
</div>
|
||||
<div class="h-48 mb-4">
|
||||
<canvas id="keyUsageChart"></canvas>
|
||||
</div>
|
||||
<div id="keyUsageTable"></div>`;
|
||||
contentArea.innerHTML = controlsHtml;
|
||||
|
||||
// 设置标题
|
||||
titleElement.textContent = `密钥 ${keyDisplay} - 请求详情`;
|
||||
|
||||
// 显示模态框
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
let keyUsageChart = null;
|
||||
function buildKeyChartConfig(labels, successData, failureData) {
|
||||
return buildChartConfig(labels, successData, failureData);
|
||||
}
|
||||
function bucketizeKeyDetails(period, details) {
|
||||
return bucketizeDetails(period, details);
|
||||
}
|
||||
function renderKeyUsageTable(data) {
|
||||
const container = document.getElementById('keyUsageTable');
|
||||
if (!container) return;
|
||||
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">该密钥在最近24小时内没有调用记录。</p>
|
||||
<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-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>
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">时间</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">模型</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态码</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">耗时(ms)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">详情</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]) => {
|
||||
data.forEach((row) => {
|
||||
const timestamp = new Date(row.timestamp).toLocaleString();
|
||||
const statusClass = row.status === 'success' ? 'text-success-600' : 'text-danger-600';
|
||||
const statusIcon = row.status === 'success' ? 'fa-check-circle' : 'fa-times-circle';
|
||||
const detailsBtn = row.status === 'failure'
|
||||
? `<button class="px-2 py-1 rounded bg-red-100 hover:bg-red-200 text-red-700 text-xs" onclick="fetchAndShowErrorDetailByInfo('${row.key}', ${row.status_code ?? 'null'}, '${row.timestamp}')">
|
||||
<i class="fas fa-info-circle mr-1"></i>详情
|
||||
</button>`
|
||||
: '-';
|
||||
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>`;
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${timestamp}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.model || 'N/A'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.status_code ?? '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${row.latency_ms ?? '-'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm ${statusClass}"><i class="fas ${statusIcon} mr-1"></i>${row.status}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">${detailsBtn}</td>
|
||||
</tr>`;
|
||||
});
|
||||
tableHtml += `
|
||||
</tbody>
|
||||
</table>`;
|
||||
tableHtml += `</tbody></table>`;
|
||||
container.innerHTML = tableHtml;
|
||||
}
|
||||
|
||||
// 设置标题
|
||||
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 {
|
||||
const data = await fetchAPI(`/api/key-usage-details/${key}`);
|
||||
if (data) {
|
||||
renderKeyUsageDetails(data, contentArea);
|
||||
} else {
|
||||
renderKeyUsageDetails({}, contentArea); // Show empty state if no data
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error("获取密钥使用详情失败:", apiError);
|
||||
contentArea.innerHTML = `
|
||||
<div class="text-center py-10 text-danger-500">
|
||||
async function renderForPeriod(period) {
|
||||
try {
|
||||
const details = await fetchAPI(`/api/stats/key-details?key=${encodeURIComponent(key)}&period=${period}`);
|
||||
const { labels, successData, failureData } = bucketizeKeyDetails(period, details || []);
|
||||
const canvas = document.getElementById('keyUsageChart');
|
||||
if (canvas && typeof Chart !== 'undefined') {
|
||||
const cfg = buildKeyChartConfig(labels, successData, failureData);
|
||||
if (keyUsageChart) keyUsageChart.destroy();
|
||||
keyUsageChart = new Chart(canvas.getContext('2d'), cfg);
|
||||
}
|
||||
renderKeyUsageTable(details || []);
|
||||
} catch (e) {
|
||||
console.error('加载密钥期内详情失败:', e);
|
||||
const tableContainer = document.getElementById('keyUsageTable');
|
||||
if (tableContainer) {
|
||||
tableContainer.innerHTML = `<div class="text-center py-10 text-danger-500">
|
||||
<i class="fas fa-exclamation-triangle text-3xl"></i>
|
||||
<p class="mt-2">加载失败: ${apiError.message}</p>
|
||||
<p class="mt-2">加载失败: ${e.message}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定按钮事件与默认加载
|
||||
const btn1h = document.getElementById('keyBtn1h');
|
||||
const btn8h = document.getElementById('keyBtn8h');
|
||||
const btn24h = document.getElementById('keyBtn24h');
|
||||
const setActive = (activeBtn) => {
|
||||
[btn1h, btn8h, btn24h].forEach((btn) => {
|
||||
if (!btn) return;
|
||||
if (btn === activeBtn) {
|
||||
btn.classList.remove('bg-gray-200');
|
||||
btn.classList.add('bg-primary-600','text-white');
|
||||
} else {
|
||||
btn.classList.add('bg-gray-200');
|
||||
btn.classList.remove('bg-primary-600','text-white');
|
||||
}
|
||||
});
|
||||
};
|
||||
if (btn1h) btn1h.addEventListener('click', () => { setActive(btn1h); renderForPeriod('1h'); });
|
||||
if (btn8h) btn8h.addEventListener('click', () => { setActive(btn8h); renderForPeriod('8h'); });
|
||||
if (btn24h) btn24h.addEventListener('click', () => { setActive(btn24h); renderForPeriod('24h'); });
|
||||
if (btn1h) setActive(btn1h);
|
||||
renderForPeriod('1h');
|
||||
};
|
||||
|
||||
// 关闭密钥使用详情模态框
|
||||
|
||||
Reference in New Issue
Block a user