mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-13 04:31:07 +08:00
- 后端新增 ResetSelectedKeysRequest、VerifySelectedKeysRequest 数据模型及相关 API 路由,实现批量重置选定密钥失败计数功能 - 前端 keys_status.js/keys_status.html 新增批量验证按钮、批量验证弹窗及交互逻辑,支持对筛选后密钥进行批量验证 - 自动刷新功能支持开关,优化用户体验 - UI 细节优化,提升密钥管理便捷性
635 lines
32 KiB
HTML
635 lines
32 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}API密钥状态 - Gemini Balance{% endblock %}
|
|
|
|
{% block head_extra_styles %}
|
|
<style>
|
|
/* keys_status.html specific styles */
|
|
.key-content {
|
|
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
|
}
|
|
.key-content.collapsed {
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
}
|
|
.toggle-icon {
|
|
transition: transform 0.3s ease;
|
|
}
|
|
.toggle-icon.collapsed {
|
|
transform: rotate(-90deg);
|
|
}
|
|
/* Copy status styling is handled by base.html's notification */
|
|
|
|
/* 现代数据统计样式 */
|
|
.stats-dashboard {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
position: relative;
|
|
z-index: 10;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.stats-dashboard {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
/* 统计卡片样式 */
|
|
.stats-card {
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease-in-out;
|
|
}
|
|
|
|
.stats-card:hover {
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.stats-card-header {
|
|
background-color: rgba(255, 255, 255, 0.3);
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid rgba(243, 244, 246, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.stats-card-title {
|
|
display: flex;
|
|
align-items: center;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
|
|
.stats-card-title i {
|
|
margin-right: 0.5rem;
|
|
color: #4F46E5;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.5rem;
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
/* 统计项样式 */
|
|
.stat-item {
|
|
padding: 0.75rem;
|
|
border-radius: 0.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
transition: all 0.3s ease-in-out;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0.05;
|
|
background-color: currentColor;
|
|
z-index: 0;
|
|
transition: opacity 0.3s ease-in-out;
|
|
}
|
|
|
|
.stat-item:hover::before {
|
|
opacity: 0.1;
|
|
}
|
|
|
|
.stat-item:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
z-index: 10;
|
|
position: relative;
|
|
color: #1F2937;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-top: 0.25rem;
|
|
z-index: 10;
|
|
position: relative;
|
|
color: #6B7280;
|
|
}
|
|
|
|
.stat-icon {
|
|
position: absolute;
|
|
right: 0.5rem;
|
|
bottom: 0.25rem;
|
|
opacity: 0.1;
|
|
font-size: 1.875rem;
|
|
transform: rotate(12deg);
|
|
transition: all 0.3s ease-in-out;
|
|
}
|
|
|
|
.stat-item:hover .stat-icon {
|
|
opacity: 0.2;
|
|
transform: scale(1.1) rotate(0deg);
|
|
}
|
|
|
|
/* 统计类型样式 */
|
|
.stat-primary {
|
|
color: #4F46E5;
|
|
background-color: rgba(238, 242, 255, 0.5);
|
|
}
|
|
|
|
.stat-success {
|
|
color: #10B981;
|
|
background-color: rgba(236, 253, 245, 0.5);
|
|
}
|
|
|
|
.stat-danger {
|
|
color: #EF4444;
|
|
background-color: rgba(254, 242, 242, 0.5);
|
|
}
|
|
|
|
.stat-warning {
|
|
color: #F59E0B;
|
|
background-color: rgba(255, 251, 235, 0.5);
|
|
}
|
|
|
|
.stat-info {
|
|
color: #3B82F6;
|
|
background-color: rgba(239, 246, 255, 0.5);
|
|
}
|
|
|
|
/* 响应式调整 */
|
|
@media (max-width: 640px) {
|
|
.stats-dashboard {
|
|
gap: 1rem;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 0.5rem;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.stat-item {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.625rem;
|
|
}
|
|
}
|
|
/* Tailwind Toggle Switch Helper CSS from config_editor.html */
|
|
.toggle-checkbox:checked {
|
|
@apply: right-0 border-primary-600;
|
|
right: 0;
|
|
border-color: #4F46E5;
|
|
}
|
|
.toggle-checkbox:checked + .toggle-label {
|
|
@apply: bg-primary-600;
|
|
background-color: #4F46E5;
|
|
}
|
|
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block head_extra_scripts %}
|
|
<!-- keys_status.js needs to be loaded in head because it might be used by inline scripts -->
|
|
<script src="/static/js/keys_status.js"></script>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container max-w-6xl mx-auto px-4"> <!-- Increased max-width -->
|
|
<div class="glass-card rounded-2xl shadow-xl p-6 md:p-8">
|
|
<div class="absolute top-6 right-6 flex items-center gap-3">
|
|
<!-- 自动刷新开关 -->
|
|
<div class="flex items-center text-sm text-gray-600 select-none">
|
|
<span class="mr-2">自动刷新</span>
|
|
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
|
|
<input type="checkbox" name="autoRefreshToggle" id="autoRefreshToggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
|
|
<label for="autoRefreshToggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
|
|
</div>
|
|
</div>
|
|
<!-- 手动刷新按钮 -->
|
|
<button class="bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full w-8 h-8 flex items-center justify-center text-primary-600 transition-all duration-300" onclick="refreshPage(this)" title="手动刷新">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<h1 class="text-3xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-primary-700 mb-4">
|
|
<img src="/static/icons/logo.png" alt="Gemini Balance Logo" class="h-9 inline-block align-middle mr-2">
|
|
Gemini Balance - 监控面板
|
|
</h1>
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div class="flex justify-center mb-8 overflow-x-auto pb-2 gap-2">
|
|
<a href="/config" class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-white bg-opacity-50 hover:bg-opacity-70 text-gray-700 transition-all duration-200">
|
|
<i class="fas fa-cog"></i> 配置编辑
|
|
</a>
|
|
<a href="/keys" class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-primary-600 text-white shadow-md">
|
|
<i class="fas fa-tachometer-alt"></i> 监控面板
|
|
</a>
|
|
<a href="/logs" class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-white bg-opacity-50 hover:bg-opacity-70 text-gray-700 transition-all duration-200">
|
|
<i class="fas fa-exclamation-triangle"></i> 错误日志
|
|
</a>
|
|
</div>
|
|
|
|
<!-- 现代化统计面板 -->
|
|
<div class="stats-dashboard animate-fade-in" style="animation-delay: 0.1s">
|
|
<!-- 密钥统计卡片 -->
|
|
<div class="stats-card">
|
|
<div class="stats-card-header">
|
|
<h3 class="stats-card-title">
|
|
<i class="fas fa-key"></i>
|
|
<span>密钥统计</span>
|
|
</h3>
|
|
<span class="text-xs text-gray-500">总计: {{ total_keys }}</span>
|
|
</div>
|
|
<div class="stats-grid">
|
|
<div class="stat-item stat-primary" title="总密钥数">
|
|
<div class="stat-value">{{ total_keys }}</div>
|
|
<div class="stat-label">总密钥数</div>
|
|
<i class="stat-icon fas fa-key"></i>
|
|
</div>
|
|
<div class="stat-item stat-success" title="有效密钥">
|
|
<div class="stat-value">{{ valid_key_count }}</div>
|
|
<div class="stat-label">有效密钥</div>
|
|
<i class="stat-icon fas fa-check-circle"></i>
|
|
</div>
|
|
<div class="stat-item stat-danger" title="无效密钥">
|
|
<div class="stat-value">{{ invalid_key_count }}</div>
|
|
<div class="stat-label">无效密钥</div>
|
|
<i class="stat-icon fas fa-times-circle"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API调用统计卡片 -->
|
|
<div class="stats-card">
|
|
<div class="stats-card-header">
|
|
<h3 class="stats-card-title">
|
|
<i class="fas fa-chart-line"></i>
|
|
<span>API调用统计</span>
|
|
</h3>
|
|
<span class="text-xs text-gray-500">本月: {{ api_stats.calls_month }}</span>
|
|
</div>
|
|
<div class="stats-grid">
|
|
<div class="stat-item stat-warning cursor-pointer hover:bg-amber-100" title="点击查看1分钟内调用详情" data-period="1m" onclick="showApiCallDetails('1m')">
|
|
<div class="stat-value">{{ api_stats.calls_1m }}</div>
|
|
<div class="stat-label">1分钟调用</div>
|
|
<i class="stat-icon fas fa-stopwatch"></i>
|
|
</div>
|
|
<div class="stat-item stat-info cursor-pointer hover:bg-blue-100" title="点击查看1小时内调用详情" data-period="1h" onclick="showApiCallDetails('1h')">
|
|
<div class="stat-value">{{ api_stats.calls_1h }}</div>
|
|
<div class="stat-label">1小时调用</div>
|
|
<i class="stat-icon fas fa-hourglass-half"></i>
|
|
</div>
|
|
<div class="stat-item stat-primary cursor-pointer hover:bg-indigo-100" title="点击查看24小时内调用详情" data-period="24h" onclick="showApiCallDetails('24h')">
|
|
<div class="stat-value">{{ api_stats.calls_24h }}</div>
|
|
<div class="stat-label">24小时调用</div>
|
|
<i class="stat-icon fas fa-calendar-day"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 有效密钥区域 -->
|
|
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.2s">
|
|
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'validKeys')">
|
|
<div class="flex items-center gap-3">
|
|
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
|
<i class="fas fa-check-circle text-success-500"></i>
|
|
<h2 class="text-lg font-semibold">有效密钥列表 ({{ valid_key_count }})</h2>
|
|
<div class="flex items-center gap-2 ml-4">
|
|
<label for="failCountThreshold" class="text-sm text-gray-600 select-none">失败次数≥</label>
|
|
<input type="number" id="failCountThreshold" value="0" min="0" class="form-input h-7 w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500" onclick="event.stopPropagation();">
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('valid', event)"> <!-- 新增批量验证按钮 -->
|
|
<i class="fas fa-check-double"></i>
|
|
批量验证
|
|
</button>
|
|
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('valid', event)" data-reset-type="valid">
|
|
<i class="fas fa-redo-alt"></i>
|
|
批量重置
|
|
</button>
|
|
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); copyKeys('valid')">
|
|
<i class="fas fa-copy"></i>
|
|
批量复制
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="key-content p-4 bg-white bg-opacity-40">
|
|
<ul id="validKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{% if valid_keys %}
|
|
{% for key, fail_count in valid_keys.items() %}
|
|
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-success-300 transform hover:-translate-y-1" data-fail-count="{{ fail_count }}">
|
|
<div class="flex flex-col justify-between h-full gap-3">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-600">
|
|
<i class="fas fa-check mr-1"></i> 有效
|
|
</span>
|
|
<div class="flex items-center gap-1">
|
|
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
|
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
失败: {{ fail_count }}
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
|
<i class="fas fa-check-circle"></i>
|
|
验证
|
|
</button>
|
|
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
|
<i class="fas fa-redo-alt"></i>
|
|
重置
|
|
</button>
|
|
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
|
<i class="fas fa-copy"></i>
|
|
复制
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
{% else %}
|
|
<li class="text-center text-gray-500 py-4">暂无有效密钥</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 无效密钥区域 -->
|
|
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.4s">
|
|
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'invalidKeys')">
|
|
<div class="flex items-center gap-3">
|
|
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
|
<i class="fas fa-times-circle text-danger-500"></i>
|
|
<h2 class="text-lg font-semibold">无效密钥列表 ({{ invalid_key_count }})</h2>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('invalid', event)"> <!-- 新增批量验证按钮 -->
|
|
<i class="fas fa-check-double"></i>
|
|
批量验证
|
|
</button>
|
|
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('invalid', event)" data-reset-type="invalid">
|
|
<i class="fas fa-redo-alt"></i>
|
|
批量重置
|
|
</button>
|
|
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); copyKeys('invalid')">
|
|
<i class="fas fa-copy"></i>
|
|
批量复制
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="key-content p-4 bg-white bg-opacity-40">
|
|
<ul id="invalidKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{% if invalid_keys %}
|
|
{% for key, fail_count in invalid_keys.items() %}
|
|
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-danger-300 transform hover:-translate-y-1">
|
|
<div class="flex flex-col justify-between h-full gap-3">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-50 text-danger-600">
|
|
<i class="fas fa-times mr-1"></i> 无效
|
|
</span>
|
|
<div class="flex items-center gap-1">
|
|
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
|
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
</div>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
失败: {{ fail_count }}
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
|
<i class="fas fa-check-circle"></i>
|
|
验证
|
|
</button>
|
|
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
|
<i class="fas fa-redo-alt"></i>
|
|
重置
|
|
</button>
|
|
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
|
<i class="fas fa-copy"></i>
|
|
复制
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
{% endfor %}
|
|
{% else %}
|
|
<li class="text-center text-gray-500 py-4">暂无无效密钥</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Removed old total keys display -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scroll buttons are now in base.html -->
|
|
<div class="scroll-buttons">
|
|
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
|
|
<i class="fas fa-chevron-up"></i>
|
|
</button>
|
|
<button class="scroll-button" onclick="scrollToBottom()" title="滚动到底部">
|
|
<i class="fas fa-chevron-down"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Notification component is now in base.html (use id="notification") -->
|
|
<div id="notification" class="notification"></div>
|
|
<!-- 重置确认模态框 -->
|
|
<div id="resetModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-lg p-6 shadow-xl max-w-md w-full animate-fade-in">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-800" id="resetModalTitle">批量重置失败次数</h3>
|
|
<button onclick="closeResetModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-6">
|
|
<p class="text-gray-600" id="resetModalMessage"></p>
|
|
</div>
|
|
<div class="flex justify-end gap-3">
|
|
<button onclick="closeResetModal()" class="px-4 py-2 bg-gray-300 hover:bg-gray-400 text-gray-800 rounded-lg transition-colors">
|
|
取消
|
|
</button>
|
|
<button id="confirmResetBtn" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors">
|
|
确认
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 验证确认模态框移到 resetModal 外部,避免嵌套导致显示异常 -->
|
|
<div id="verifyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-lg p-6 shadow-xl max-w-md w-full animate-fade-in">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold text-gray-800" id="verifyModalTitle">批量验证密钥</h3>
|
|
<button onclick="closeVerifyModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mb-6">
|
|
<p class="text-gray-600" id="verifyModalMessage"></p>
|
|
</div>
|
|
<div class="flex justify-end gap-3">
|
|
<button onclick="closeVerifyModal()" class="px-4 py-2 bg-gray-300 hover:bg-gray-400 text-gray-800 rounded-lg transition-colors">
|
|
取消
|
|
</button>
|
|
<button id="confirmVerifyBtn" class="px-4 py-2 bg-teal-500 hover:bg-teal-600 text-white rounded-lg transition-colors">
|
|
确认验证
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 操作结果模态框 -->
|
|
<div id="resultModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-2xl p-0 shadow-2xl max-w-lg w-full animate-fade-in border border-gray-200">
|
|
<div class="flex items-center justify-between px-6 pt-6 pb-2 border-b">
|
|
<h3 class="text-xl font-bold text-gray-800 text-center w-full" id="resultModalTitle" style="letter-spacing:0.05em;">操作结果</h3>
|
|
<button onclick="closeResultModal()" class="absolute right-6 top-6 text-gray-400 hover:text-gray-700 focus:outline-none text-2xl">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="flex flex-col items-center px-8 pt-6 pb-2">
|
|
<div id="resultIcon" class="text-6xl mb-3"></div>
|
|
</div>
|
|
<div class="px-8 pb-2 w-full">
|
|
<div id="resultModalMessage"
|
|
class="text-gray-700 text-base leading-relaxed break-all whitespace-pre-line max-h-60 overflow-y-auto border border-gray-100 rounded-lg bg-gray-50 p-4 shadow-inner"
|
|
style="font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';">
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-center px-8 pb-6 pt-2">
|
|
<button id="resultModalConfirmBtn" onclick="closeResultModal()" class="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-semibold text-base shadow transition-colors">
|
|
确定
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API 调用详情模态框 -->
|
|
<div id="apiCallDetailsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
|
<div class="bg-white rounded-lg p-6 shadow-xl max-w-3xl w-full animate-fade-in"> <!-- Increased max-width -->
|
|
<div class="flex items-center justify-between mb-4 border-b pb-3">
|
|
<h3 class="text-xl font-semibold text-gray-800" id="apiCallDetailsModalTitle">API 调用详情</h3>
|
|
<button onclick="closeApiCallDetailsModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none text-xl">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div id="apiCallDetailsContent" class="mb-6 max-h-[60vh] overflow-y-auto pr-2"> <!-- Increased max-height and added padding-right -->
|
|
<!-- 详细数据将加载到这里 -->
|
|
<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>
|
|
</div>
|
|
<div class="flex justify-end pt-4 border-t">
|
|
<button onclick="closeApiCallDetailsModal()" class="px-5 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-lg transition-colors text-sm font-medium">
|
|
关闭
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer is now in base.html -->
|
|
|
|
{% endblock %}
|
|
|
|
{% block body_scripts %}
|
|
<script>
|
|
// keys_status.html specific JavaScript initialization
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Filter functionality based on fail count threshold
|
|
const thresholdInput = document.getElementById('failCountThreshold');
|
|
const validKeysList = document.getElementById('validKeys');
|
|
|
|
function filterValidKeys() {
|
|
const threshold = parseInt(thresholdInput.value, 10);
|
|
if (isNaN(threshold)) return; // Do nothing if input is not a number
|
|
|
|
const keys = validKeysList.querySelectorAll('li');
|
|
let visibleCount = 0;
|
|
keys.forEach(keyItem => {
|
|
// Check if it's a key item (has data-fail-count) before processing
|
|
if (keyItem.hasAttribute('data-fail-count')) {
|
|
const failCount = parseInt(keyItem.getAttribute('data-fail-count'), 10);
|
|
if (failCount >= threshold) {
|
|
keyItem.style.display = ''; // Show item
|
|
visibleCount++;
|
|
} else {
|
|
keyItem.style.display = 'none'; // Hide item
|
|
}
|
|
}
|
|
});
|
|
// Optional: Show a message if no keys match the filter
|
|
const noMatchMsgId = 'no-valid-keys-msg';
|
|
let noMatchMsg = validKeysList.querySelector(`#${noMatchMsgId}`);
|
|
if (visibleCount === 0 && keys.length > 0) { // Only show if there were keys initially
|
|
if (!noMatchMsg) {
|
|
noMatchMsg = document.createElement('li');
|
|
noMatchMsg.id = noMatchMsgId;
|
|
noMatchMsg.className = 'text-center text-gray-500 py-4';
|
|
noMatchMsg.textContent = '没有符合条件的有效密钥';
|
|
validKeysList.appendChild(noMatchMsg);
|
|
}
|
|
noMatchMsg.style.display = '';
|
|
} else if (noMatchMsg) {
|
|
noMatchMsg.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
if (thresholdInput && validKeysList) {
|
|
thresholdInput.addEventListener('input', filterValidKeys);
|
|
// Initial filter on load
|
|
filterValidKeys();
|
|
}
|
|
|
|
// Initialize other elements or event listeners if needed
|
|
// The main logic (verifyKey, resetKeyFailCount, copyKey, etc.) is in keys_status.js
|
|
// The toggleSection logic is now specific to this page
|
|
window.toggleSection = function(header, sectionId) {
|
|
const toggleIcon = header.querySelector('.toggle-icon');
|
|
const content = header.nextElementSibling; // Assumes content is immediately after header
|
|
if (toggleIcon && content) {
|
|
toggleIcon.classList.toggle('collapsed');
|
|
content.classList.toggle('collapsed');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|