gemini / app /templates /index.html
drriver's picture
Upload 28 files
60016b2 verified
<!DOCTYPE html>
<html>
<head>
<title>Gemini API 代理服务</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
background-color: #f8f9fa;
}
h1, h2, h3 {
color: #333;
text-align: center;
margin-bottom: 20px;
}
.info-box {
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.status {
color: #28a745;
font-weight: bold;
font-size: 18px;
margin-bottom: 20px;
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 15px;
margin-bottom: 20px;
}
.stat-card {
background-color: #e9ecef;
padding: 15px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.stat-label {
font-size: 14px;
color: #6c757d;
margin-top: 5px;
}
.section-title {
color: #495057;
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
margin-bottom: 20px;
}
.log-container {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
max-height: 500px;
overflow-y: auto;
font-family: monospace;
font-size: 14px;
line-height: 1.5;
}
.log-entry {
margin-bottom: 8px;
padding: 8px;
border-radius: 4px;
}
.log-entry.INFO {
background-color: #e8f4f8;
border-left: 4px solid #17a2b8;
}
.log-entry.WARNING {
background-color: #fff3cd;
border-left: 4px solid #ffc107;
}
.log-entry.ERROR {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
}
.log-entry.DEBUG {
background-color: #d1ecf1;
border-left: 4px solid #17a2b8;
}
.log-timestamp {
color: #6c757d;
font-size: 12px;
margin-right: 10px;
}
.log-level {
font-weight: bold;
margin-right: 10px;
}
.log-level.INFO {
color: #17a2b8;
}
.log-level.WARNING {
color: #ffc107;
}
.log-level.ERROR {
color: #dc3545;
}
.log-level.DEBUG {
color: #17a2b8;
}
.log-message {
color: #212529;
}
.refresh-button {
display: block;
margin: 20px auto;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.refresh-button:hover {
background-color: #0069d9;
}
.log-filter {
display: flex;
justify-content: center;
margin-bottom: 15px;
gap: 10px;
}
.log-filter button {
padding: 5px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f8f9fa;
cursor: pointer;
}
.log-filter button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
/* API密钥统计样式 */
.api-key-stats-container {
margin-top: 20px;
}
.api-key-stats-list {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 电脑上显示为三列 */
gap: 15px;
margin-top: 15px;
}
/* 在中等屏幕上显示为两列 */
@media (max-width: 992px) {
.api-key-stats-list {
grid-template-columns: repeat(2, 1fr);
}
}
/* 在小屏幕上显示为一列 */
@media (max-width: 576px) {
.api-key-stats-list {
grid-template-columns: 1fr;
}
}
.api-key-item {
background-color: #f8f9fa;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.api-key-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.api-key-name {
font-weight: bold;
color: #495057;
}
.api-key-usage {
display: flex;
align-items: center;
gap: 10px;
}
.api-key-count {
font-weight: bold;
color: #007bff;
}
.progress-container {
width: 100%;
height: 10px;
background-color: #e9ecef;
border-radius: 5px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 5px;
transition: width 0.3s ease;
}
.progress-bar.low {
background-color: #28a745; /* 绿色 - 低使用率 */
}
.progress-bar.medium {
background-color: #ffc107; /* 黄色 - 中等使用率 */
}
.progress-bar.high {
background-color: #dc3545; /* 红色 - 高使用率 */
}
</style>
</head>
<body>
<h1>🤖 Gemini API 代理服务</h1>
<div class="info-box">
<h2 class="section-title">🟢 运行状态</h2>
<p class="status">服务运行中</p>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ key_count }}</div>
<div class="stat-label">可用API密钥数量</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ model_count }}</div>
<div class="stat-label">可用模型数量</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ retry_count }}</div>
<div class="stat-label">最大重试次数</div>
</div>
</div>
<h3 class="section-title">API调用统计</h3>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="last-24h-calls">{{ last_24h_calls }}</div>
<div class="stat-label">24小时内调用次数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="hourly-calls">{{ hourly_calls }}</div>
<div class="stat-label">一小时内调用次数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="minute-calls">{{ minute_calls }}</div>
<div class="stat-label">一分钟内调用次数</div>
</div>
</div>
<div class="api-key-stats-container">
<h3 class="section-title" style="cursor: pointer; user-select: none;" onclick="toggleApiKeyStats()">
API密钥使用统计 <span id="toggle-icon"></span>
</h3>
<div id="api-key-stats" style="display: block;">
<div class="api-key-stats-list" id="api-key-stats-list">
<!-- API密钥统计将在这里动态生成 -->
</div>
</div>
</div>
</div>
<div class="info-box">
<h2 class="section-title">⚙️ 环境配置</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">{{ max_requests_per_minute }}</div>
<div class="stat-label">每分钟请求限制</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ max_requests_per_day_per_ip }}</div>
<div class="stat-label">每IP每日请求限制</div>
</div>
<div class="stat-card">
<div class="stat-value" id="current-time">{{ current_time }}</div>
<div class="stat-label">当前服务器时间</div>
</div>
</div>
</div>
<div class="info-box">
<h2 class="section-title">📦 版本信息</h2>
<div class="version-info" style="text-align: center; margin-bottom: 15px;">
<div style="font-size: 18px; margin-bottom: 10px;">
当前版本: <span style="font-weight: bold; color: #007bff;">{{ local_version }}</span>
</div>
{% if has_update %}
<div style="display: flex; align-items: center; justify-content: center; margin-top: 15px;">
<div style="background-color: #fef6e0; border: 1px solid #ffeeba; border-radius: 4px; padding: 10px 15px; display: inline-flex; align-items: center;">
<span style="color: #ff9800; margin-right: 10px;">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</span>
<span>
<strong>发现新版本!</strong> 最新版本: <span style="font-weight: bold; color: #28a745;">{{ remote_version }}</span>
</span>
</div>
</div>
{% endif %}
</div>
</div>
<div class="info-box">
<h2 class="section-title"> 系统日志</h2>
<div class="log-filter">
<button class="active" data-level="ALL">全部</button>
<button data-level="INFO">信息</button>
<button data-level="WARNING">警告</button>
<button data-level="ERROR">错误</button>
</div>
<div class="log-container" id="log-container">
{% for log in logs %}
<div class="log-entry {{ log.level }}" data-level="{{ log.level }}">
<span class="log-timestamp">{{ log.timestamp }}</span>
<span class="log-level {{ log.level }}">{{ log.level }}</span>
<span class="log-message">
{% if log.key != 'N/A' %}[{{ log.key }}]{% endif %}
{% if log.request_type != 'N/A' %}{{ log.request_type }}{% endif %}
{% if log.model != 'N/A' %}[{{ log.model }}]{% endif %}
{% if log.status_code != 'N/A' %}{{ log.status_code }}{% endif %}
: {{ log.message }}
{% if log.error_message %}
- {{ log.error_message }}
{% endif %}
</span>
</div>
{% endfor %}
</div>
<button class="refresh-button" id="refresh-button">刷新数据</button>
</div>
<script>
// 日志过滤功能
document.querySelectorAll('.log-filter button').forEach(button => {
button.addEventListener('click', function() {
// 移除所有按钮的active类
document.querySelectorAll('.log-filter button').forEach(btn => {
btn.classList.remove('active');
});
// 为当前按钮添加active类
this.classList.add('active');
const level = this.getAttribute('data-level');
// 显示或隐藏日志条目
document.querySelectorAll('.log-entry').forEach(entry => {
if (level === 'ALL' || entry.getAttribute('data-level') === level) {
entry.style.display = 'block';
} else {
entry.style.display = 'none';
}
});
});
});
// 动态刷新功能
let refreshInterval;
const refreshRate = 1000; // 1秒刷新一次
let isRefreshing = false;
// 开始自动刷新
function startAutoRefresh() {
if (!refreshInterval) {
refreshInterval = setInterval(fetchDashboardData, refreshRate);
console.log('自动刷新已启动');
}
}
// 停止自动刷新
function stopAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
console.log('自动刷新已停止');
}
}
// 获取仪表盘数据
async function fetchDashboardData() {
if (isRefreshing) return; // 防止重复请求
isRefreshing = true;
try {
const response = await fetch('/api/dashboard-data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
updateDashboard(data);
} catch (error) {
console.error('获取数据失败:', error);
} finally {
isRefreshing = false;
}
}
// 更新仪表盘数据
function updateDashboard(data) {
// 更新时间
document.getElementById('current-time').textContent = data.current_time;
// 更新调用统计
document.getElementById('last-24h-calls').textContent = data.last_24h_calls;
document.getElementById('hourly-calls').textContent = data.hourly_calls;
document.getElementById('minute-calls').textContent = data.minute_calls;
// 更新API密钥统计
if (data.api_key_stats) {
updateApiKeyStats(data.api_key_stats);
}
// 更新日志
updateLogs(data.logs);
}
// 更新API密钥统计
function updateApiKeyStats(apiKeyStats) {
const container = document.getElementById('api-key-stats-list');
container.innerHTML = '';
if (!apiKeyStats || apiKeyStats.length === 0) {
container.innerHTML = '<div class="api-key-item">没有API密钥使用数据</div>';
return;
}
apiKeyStats.forEach(stat => {
const item = document.createElement('div');
item.className = 'api-key-item';
// 确定进度条颜色
let barClass = 'low';
if (stat.usage_percent > 75) {
barClass = 'high';
} else if (stat.usage_percent > 50) {
barClass = 'medium';
}
item.innerHTML = `
<div class="api-key-header">
<div class="api-key-name">API密钥: ${stat.api_key}</div>
<div class="api-key-usage">
<span class="api-key-count">${stat.calls_24h}</span> /
<span class="api-key-limit">${stat.limit}</span>
<span class="api-key-percent">(${stat.usage_percent}%)</span>
</div>
</div>
<div class="progress-container">
<div class="progress-bar ${barClass}" style="width: ${Math.min(stat.usage_percent, 100)}%"></div>
</div>
`;
container.appendChild(item);
});
}
// 切换API密钥统计显示/隐藏
function toggleApiKeyStats() {
const statsDiv = document.getElementById('api-key-stats');
const toggleIcon = document.getElementById('toggle-icon');
if (statsDiv.style.display === 'none') {
statsDiv.style.display = 'block';
toggleIcon.textContent = '▼';
} else {
statsDiv.style.display = 'none';
toggleIcon.textContent = '▶';
}
}
// 更新日志
function updateLogs(logs) {
const logContainer = document.getElementById('log-container');
const currentFilter = document.querySelector('.log-filter button.active').getAttribute('data-level');
// 保存当前滚动位置
const wasScrolledToBottom = logContainer.scrollHeight - logContainer.clientHeight <= logContainer.scrollTop + 5;
// 清空现有日志
logContainer.innerHTML = '';
// 添加新日志
logs.forEach(log => {
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${log.level}`;
logEntry.setAttribute('data-level', log.level);
// 根据当前过滤器设置显示状态
if (currentFilter !== 'ALL' && log.level !== currentFilter) {
logEntry.style.display = 'none';
}
const timestampSpan = document.createElement('span');
timestampSpan.className = 'log-timestamp';
timestampSpan.textContent = log.timestamp;
const levelSpan = document.createElement('span');
levelSpan.className = `log-level ${log.level}`;
levelSpan.textContent = log.level;
const messageSpan = document.createElement('span');
messageSpan.className = 'log-message';
// 构建消息内容
let messageContent = '';
if (log.key !== 'N/A') messageContent += `[${log.key}] `;
if (log.request_type !== 'N/A') messageContent += log.request_type + ' ';
if (log.model !== 'N/A') messageContent += `[${log.model}] `;
if (log.status_code !== 'N/A') messageContent += log.status_code + ' ';
messageContent += ': ' + log.message;
if (log.error_message) messageContent += ' - ' + log.error_message;
messageSpan.textContent = messageContent;
logEntry.appendChild(timestampSpan);
logEntry.appendChild(levelSpan);
logEntry.appendChild(messageSpan);
logContainer.appendChild(logEntry);
});
// 如果之前是滚动到底部的,则保持滚动到底部
if (wasScrolledToBottom) {
logContainer.scrollTop = logContainer.scrollHeight;
}
}
// 页面加载时自动滚动到日志底部并启动自动刷新
window.onload = function() {
const logContainer = document.getElementById('log-container');
logContainer.scrollTop = logContainer.scrollHeight;
// 启动自动刷新
startAutoRefresh();
};
// 手动刷新按钮
document.getElementById('refresh-button').addEventListener('click', fetchDashboardData);
</script>
</body>
</html>