feat: 添加密钥检查调度器并重构前端UI

主要变更:

- **调度器功能:**
    - 集成 APScheduler 实现定时任务,用于定期检查API密钥的有效性。
    - 在 `.env.example` 和 `app/config/config.py` 中添加了 `CHECK_INTERVAL_HOURS` 和 `TIMEZONE` 配置项。
    - 在应用生命周期 (`app/core/application.py`) 中添加了调度器的启动和停止逻辑。
    - 新增 `app/scheduler/` 目录及相关实现 (`key_checker.py`)。
    - 新增 `app/router/scheduler_routes.py` 用于调度器相关API (如果未来需要)。
    - 在 `requirements.txt` 中添加 `apscheduler` 依赖。

- **前端重构与改进:**
    - 引入 `app/templates/base.html` 作为基础模板,统一页面结构和样式引入。
    - 使用新的样式(推测为Tailwind CSS)重构了 `auth.html`, `config_editor.html`, `error_logs.html`, `keys_status.html` 页面,提升了UI一致性和响应式布局。
    - 删除了旧的CSS文件 (`auth.css`, `config_editor.css`, `error_logs.css`, `keys_status.css`)。
    - 更新了对应的 JavaScript 文件 (`config_editor.js`, `error_logs.js`, `keys_status.js`) 以适应新的HTML结构和交互。
    - 在 `keys_status.html` 页面增加了按失败次数过滤密钥、批量重置失败次数、确认模态框等功能。
    - 添加了新的 Logo 图片 (`logo.png`, `logo1.png`)。

- **其他:**
    - 更新了 `app/router/routes.py` 以包含新的路由。
    - 对 `app/service/key/key_manager.py` 和 `app/database/services.py` 进行了相关调整以支持新功能。
```
This commit is contained in:
snaily
2025-04-11 03:16:51 +08:00
parent 69261e98de
commit 51bb71bdb5
25 changed files with 2090 additions and 2776 deletions

View File

@@ -11,6 +11,8 @@ SHOW_THINKING_PROCESS=true
BASE_URL=https://generativelanguage.googleapis.com/v1beta
MAX_FAILURES=10
MAX_RETRIES=3
CHECK_INTERVAL_HOURS=1
TIMEZONE=Asia/Shanghai
# 请求超时时间(秒)
TIME_OUT=300
#########################image_generate 相关配置###########################

View File

@@ -62,7 +62,11 @@ class Settings(BaseSettings):
STREAM_SHORT_TEXT_THRESHOLD: int = DEFAULT_STREAM_SHORT_TEXT_THRESHOLD
STREAM_LONG_TEXT_THRESHOLD: int = DEFAULT_STREAM_LONG_TEXT_THRESHOLD
STREAM_CHUNK_SIZE: int = DEFAULT_STREAM_CHUNK_SIZE
# 调度器配置
CHECK_INTERVAL_HOURS: int = 1 # 默认检查间隔为1小时
TIMEZONE: str = "Asia/Shanghai" # 默认时区
def __init__(self, **kwargs):
super().__init__(**kwargs)
# 设置默认AUTH_TOKEN如果未提供

View File

@@ -14,6 +14,7 @@ from app.service.key.key_manager import get_key_manager_instance
from app.core.initialization import initialize_app
from app.database.connection import connect_to_db, disconnect_from_db
from app.database.initialization import initialize_database
from app.scheduler.key_checker import start_scheduler, stop_scheduler # 导入调度器函数
logger = get_application_logger()
@@ -44,12 +45,20 @@ async def lifespan(app: FastAPI):
except Exception as e:
logger.error(f"Failed to initialize application: {str(e)}")
raise
# 启动调度器
start_scheduler()
logger.info("Scheduler started successfully.")
yield # 应用程序运行期间
# 关闭事件
logger.info("Application shutting down...")
# 停止调度器
stop_scheduler()
logger.info("Scheduler stopped.")
# 断开数据库连接
await disconnect_from_db()

View File

@@ -1,10 +1,9 @@
"""
数据库服务模块
"""
import datetime
import json
from typing import Dict, List, Optional, Any, Union
from datetime import date, timedelta # Import date and timedelta
from datetime import datetime
from sqlalchemy import select, insert, update, func # Import func for COUNT
@@ -142,7 +141,7 @@ async def add_error_log(
model_name=model_name,
error_code=error_code,
request_msg=request_msg_json,
request_time=datetime.datetime.now()
request_time=datetime.now()
)
)
await database.execute(query)
@@ -158,8 +157,8 @@ async def get_error_logs(
offset: int = 0,
key_search: Optional[str] = None,
error_search: Optional[str] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> List[Dict[str, Any]]:
"""
获取错误日志,支持搜索和日期过滤
@@ -169,8 +168,8 @@ async def get_error_logs(
offset (int): 偏移量
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
start_date (Optional[date]): 开始日期
end_date (Optional[date]): 结束日期
start_date (Optional[datetime]): 开始日期时间
end_date (Optional[datetime]): 结束日期时间
Returns:
List[Dict[str, Any]]: 错误日志列表
@@ -189,8 +188,8 @@ async def get_error_logs(
if start_date:
query = query.where(ErrorLog.request_time >= start_date)
if end_date:
# Add 1 day to end_date to include the whole day
query = query.where(ErrorLog.request_time < end_date + timedelta(days=1))
# Use the datetime object directly for comparison
query = query.where(ErrorLog.request_time < end_date)
# Apply ordering, limit, and offset
query = query.order_by(ErrorLog.request_time.desc()).limit(limit).offset(offset)
@@ -205,8 +204,8 @@ async def get_error_logs(
async def get_error_logs_count(
key_search: Optional[str] = None,
error_search: Optional[str] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> int:
"""
获取符合条件的错误日志总数
@@ -214,8 +213,8 @@ async def get_error_logs_count(
Args:
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
start_date (Optional[date]): 开始日期
end_date (Optional[date]): 结束日期
start_date (Optional[datetime]): 开始日期时间
end_date (Optional[datetime]): 结束日期时间
Returns:
int: 日志总数
@@ -234,7 +233,8 @@ async def get_error_logs_count(
if start_date:
query = query.where(ErrorLog.request_time >= start_date)
if end_date:
query = query.where(ErrorLog.request_time < end_date + timedelta(days=1))
# Use the datetime object directly for comparison
query = query.where(ErrorLog.request_time < end_date)
count_result = await database.fetch_one(query)
return count_result[0] if count_result else 0

View File

@@ -146,15 +146,67 @@ async def stream_generate_content(
logger.error(f"Streaming request failed: {str(e)}")
raise HTTPException(status_code=500, detail="Streaming request failed") from e
@router.post("/reset-all-fail-counts")
async def reset_all_key_fail_counts(key_type: str = None, key_manager: KeyManager = Depends(get_key_manager)):
"""批量重置Gemini API密钥的失败计数可选择性地仅重置有效或无效密钥"""
logger.info("-" * 50 + "reset_all_gemini_key_fail_counts" + "-" * 50)
logger.info(f"Received reset request with key_type: {key_type}")
try:
# 获取分类后的密钥
keys_by_status = await key_manager.get_keys_by_status()
valid_keys = keys_by_status.get("valid_keys", {})
invalid_keys = keys_by_status.get("invalid_keys", {})
# 根据类型选择要重置的密钥
keys_to_reset = []
if key_type == "valid":
keys_to_reset = list(valid_keys.keys())
logger.info(f"Resetting only valid keys, count: {len(keys_to_reset)}")
elif key_type == "invalid":
keys_to_reset = list(invalid_keys.keys())
logger.info(f"Resetting only invalid keys, count: {len(keys_to_reset)}")
else:
# 重置所有密钥
await key_manager.reset_failure_counts()
return JSONResponse({"success": True, "message": "所有密钥的失败计数已重置"})
# 批量重置指定类型的密钥
for key in keys_to_reset:
await key_manager.reset_key_failure_count(key)
return JSONResponse({
"success": True,
"message": f"{key_type}密钥的失败计数已重置",
"reset_count": len(keys_to_reset)
})
except Exception as e:
logger.error(f"Failed to reset key failure counts: {str(e)}")
return JSONResponse({"success": False, "message": f"批量重置失败: {str(e)}"}, status_code=500)
@router.post("/reset-fail-count/{api_key}")
async def reset_key_fail_count(api_key: str, key_manager: KeyManager = Depends(get_key_manager)):
"""重置指定Gemini API密钥的失败计数"""
logger.info("-" * 50 + "reset_gemini_key_fail_count" + "-" * 50)
logger.info(f"Resetting failure count for API key: {api_key}")
try:
result = await key_manager.reset_key_failure_count(api_key)
if result:
return JSONResponse({"success": True, "message": "失败计数已重置"})
return JSONResponse({"success": False, "message": "未找到指定密钥"}, status_code=404)
except Exception as e:
logger.error(f"Failed to reset key failure count: {str(e)}")
return JSONResponse({"success": False, "message": f"重置失败: {str(e)}"}, status_code=500)
@router.post("/verify-key/{api_key}")
async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get_chat_service)):
async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get_chat_service), key_manager: KeyManager = Depends(get_key_manager)):
"""验证Gemini API密钥的有效性"""
logger.info("-" * 50 + "verify_gemini_key" + "-" * 50)
logger.info("Verifying API key validity")
try:
# 使用generate_content接口测试key的有效性
gemini_request = GeminiRequest(
contents=[
@@ -167,13 +219,19 @@ async def verify_key(api_key: str, chat_service: GeminiChatService = Depends(get
response = await chat_service.generate_content(
settings.TEST_MODEL,
gemini_request,
gemini_request,
api_key
)
if response:
return JSONResponse({"status": "valid"})
return JSONResponse({"status": "invalid"})
return JSONResponse({"status": "valid"})
except Exception as e:
logger.error(f"Key verification failed: {str(e)}")
# 验证出现异常时增加失败计数
async with key_manager.failure_count_lock:
if api_key in key_manager.key_failure_counts:
key_manager.key_failure_counts[api_key] += 1
logger.warning(f"Verification exception for key: {api_key}, incrementing failure count")
return JSONResponse({"status": "invalid", "error": str(e)})

View File

@@ -2,7 +2,7 @@
日志路由模块
"""
from typing import Any, Dict, List, Optional
from datetime import date
from datetime import datetime
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, Request, Query
from fastapi.responses import RedirectResponse
@@ -29,8 +29,8 @@ async def get_error_logs_api(
offset: int = Query(0, ge=0),
key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"),
error_search: Optional[str] = Query(None, description="Search term for error type or log message"),
start_date: Optional[date] = Query(None, description="Start date for filtering (YYYY-MM-DD)"),
end_date: Optional[date] = Query(None, description="End date for filtering (YYYY-MM-DD)")
start_date: Optional[datetime] = Query(None, description="Start datetime for filtering (YYYY-MM-DDTHH:MM)"),
end_date: Optional[datetime] = Query(None, description="End datetime for filtering (YYYY-MM-DDTHH:MM)")
):
"""
获取错误日志

View File

@@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates
from app.core.security import verify_auth_token
from app.log.logger import get_routes_logger
from app.router import gemini_routes, openai_routes, config_routes, log_routes
from app.router import gemini_routes, openai_routes, config_routes, log_routes, scheduler_routes # 新增导入
from app.service.key.key_manager import get_key_manager_instance
logger = get_routes_logger()
@@ -30,6 +30,7 @@ def setup_routers(app: FastAPI) -> None:
app.include_router(gemini_routes.router_v1beta)
app.include_router(config_routes.router)
app.include_router(log_routes.router)
app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由
# 添加页面路由
setup_page_routes(app)

View File

@@ -0,0 +1,63 @@
"""
定时任务控制路由模块
"""
from fastapi import APIRouter, Request, HTTPException, status # 移除 Depends, 添加 Request
from fastapi.responses import JSONResponse
from app.core.security import verify_auth_token # 导入 verify_auth_token
from app.scheduler.key_checker import start_scheduler, stop_scheduler
from app.log.logger import get_routes_logger # 使用路由日志记录器
logger = get_routes_logger()
router = APIRouter(
prefix="/api/scheduler",
tags=["Scheduler"]
# 移除全局依赖
)
# 认证检查的辅助函数
async def verify_token(request: Request):
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning("Unauthorized access attempt to scheduler API")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post("/start", summary="启动定时任务")
async def start_scheduler_endpoint(request: Request): # 添加 request 参数
"""Start the background scheduler task"""
"""
await verify_token(request) # 在函数开始处进行认证检查
"""
try:
logger.info("Received request to start scheduler.")
start_scheduler() # 调用 key_checker 中的函数
return JSONResponse(content={"message": "Scheduler started successfully."}, status_code=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error starting scheduler: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start scheduler: {str(e)}"
)
@router.post("/stop", summary="停止定时任务")
async def stop_scheduler_endpoint(request: Request): # 添加 request 参数
"""Stop the background scheduler task"""
"""
await verify_token(request) # 在函数开始处进行认证检查
"""
try:
logger.info("Received request to stop scheduler.")
stop_scheduler() # 调用 key_checker 中的函数
return JSONResponse(content={"message": "Scheduler stopped successfully."}, status_code=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Error stopping scheduler: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to stop scheduler: {str(e)}"
)

View File

@@ -0,0 +1,100 @@
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from app.service.key.key_manager import get_key_manager_instance
from app.service.chat.gemini_chat_service import GeminiChatService
from app.domain.gemini_models import GeminiRequest, GeminiContent
from app.config.config import settings
from app.log.logger import Logger # 导入 Logger 类
logger = Logger.setup_logger("scheduler") # 使用 Logger.setup_logger
async def check_failed_keys():
"""
定时检查失败次数大于0的API密钥并尝试验证它们。
如果验证成功,重置失败计数;如果失败,增加失败计数。
"""
logger.info("Starting scheduled check for failed API keys...")
try:
key_manager = await get_key_manager_instance()
# 确保 KeyManager 已经初始化
if not key_manager or not hasattr(key_manager, 'key_failure_counts'):
logger.warning("KeyManager instance not available or not initialized. Skipping check.")
return
# 创建 GeminiChatService 实例用于验证
# 注意:这里直接创建实例,而不是通过依赖注入,因为这是后台任务
chat_service = GeminiChatService(settings.BASE_URL, key_manager)
# 获取需要检查的 key 列表 (失败次数 > 0)
keys_to_check = []
async with key_manager.failure_count_lock: # 访问共享数据需要加锁
# 复制一份以避免在迭代时修改字典
failure_counts_copy = key_manager.key_failure_counts.copy()
keys_to_check = [key for key, count in failure_counts_copy.items() if count > 0] # 检查所有失败次数大于0的key
if not keys_to_check:
logger.info("No keys with failure count > 0 found. Skipping verification.")
return
logger.info(f"Found {len(keys_to_check)} keys with failure count > 0 to verify.")
for key in keys_to_check:
# 隐藏部分 key 用于日志记录
log_key = f"{key[:4]}...{key[-4:]}" if len(key) > 8 else key
logger.info(f"Verifying key: {log_key}...")
try:
# 构造测试请求
gemini_request = GeminiRequest(
contents=[
GeminiContent(
role="user",
parts=[{"text": "hi"}] # 使用简单的文本进行验证
)
]
)
# 调用 generate_content 进行验证
await chat_service.generate_content(
settings.TEST_MODEL, # 使用配置中定义的测试模型
gemini_request,
key
)
# 如果没有抛出异常,说明 key 有效
logger.info(f"Key {log_key} verification successful. Resetting failure count.")
await key_manager.reset_key_failure_count(key)
except Exception as e:
# 验证失败,增加失败计数
logger.warning(f"Key {log_key} verification failed: {str(e)}. Incrementing failure count.")
# 直接操作计数器,需要加锁
async with key_manager.failure_count_lock:
# 再次检查 key 是否存在且失败次数未达上限
if key in key_manager.key_failure_counts and key_manager.key_failure_counts[key] < key_manager.MAX_FAILURES:
key_manager.key_failure_counts[key] += 1
logger.info(f"Failure count for key {log_key} incremented to {key_manager.key_failure_counts[key]}.")
elif key in key_manager.key_failure_counts:
logger.warning(f"Key {log_key} reached MAX_FAILURES ({key_manager.MAX_FAILURES}). Not incrementing further.")
except Exception as e:
logger.error(f"An error occurred during the scheduled key check: {str(e)}", exc_info=True)
def setup_scheduler():
"""设置并启动 APScheduler"""
scheduler = AsyncIOScheduler(timezone=str(settings.TIMEZONE)) # 从配置读取时区
# 添加定时任务,例如每小时执行一次 (可以调整)
scheduler.add_job(check_failed_keys, 'interval', hours=settings.CHECK_INTERVAL_HOURS)
scheduler.start()
logger.info(f"Scheduler started. Key check job scheduled to run every {settings.CHECK_INTERVAL_HOURS} hour(s).")
return scheduler
# 可以在这里添加一个全局的 scheduler 实例,以便在应用关闭时优雅地停止
scheduler_instance = None
def start_scheduler():
global scheduler_instance
if scheduler_instance is None:
scheduler_instance = setup_scheduler()
def stop_scheduler():
global scheduler_instance
if scheduler_instance and scheduler_instance.running:
scheduler_instance.shutdown()
logger.info("Scheduler stopped.")

View File

@@ -37,6 +37,16 @@ class KeyManager:
async with self.failure_count_lock:
for key in self.key_failure_counts:
self.key_failure_counts[key] = 0
async def reset_key_failure_count(self, key: str) -> bool:
"""重置指定key的失败计数"""
async with self.failure_count_lock:
if key in self.key_failure_counts:
self.key_failure_counts[key] = 0
logger.info(f"Reset failure count for key: {key}")
return True
logger.warning(f"Attempt to reset failure count for non-existent key: {key}")
return False
async def get_next_working_key(self) -> str:
"""获取下一可用的API key"""

View File

@@ -1,249 +0,0 @@
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
max-width: 400px;
width: 90%;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.container:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0,0,0,0.25);
}
.logo {
text-align: center;
margin-bottom: 30px;
animation: fadeIn 1s ease;
}
.logo i {
font-size: 48px;
color: #764ba2;
margin-bottom: 15px;
}
h2 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
font-weight: 700;
font-size: 24px;
animation: slideDown 0.5s ease;
}
form {
display: flex;
flex-direction: column;
gap: 20px;
}
.input-group {
position: relative;
animation: slideUp 0.5s ease;
}
.input-group i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #764ba2;
font-size: 18px;
}
input {
width: 100%;
padding: 12px 12px 12px 40px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
box-sizing: border-box;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9);
}
input:focus {
border-color: #764ba2;
box-shadow: 0 0 10px rgba(118, 75, 162, 0.2);
outline: none;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 14px;
border-radius: 10px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
button:active {
transform: translateY(0);
}
button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
button:active::after {
width: 200px;
height: 200px;
opacity: 0;
}
.error-message {
color: #e74c3c;
margin-top: 15px;
text-align: center;
font-weight: bold;
padding: 10px;
border-radius: 5px;
background: rgba(231, 76, 60, 0.1);
animation: shake 0.5s ease;
}
.copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.9);
padding: 10px 0;
text-align: center;
font-size: 14px;
color: #2c3e50;
backdrop-filter: blur(5px);
border-top: 1px solid rgba(0,0,0,0.1);
}
.copyright a {
color: #764ba2;
text-decoration: none;
transition: color 0.3s ease;
}
.copyright a:hover {
color: #667eea;
}
.copyright img {
width: 20px;
height: 20px;
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideDown {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@media (max-width: 768px) {
.container {
width: 85%;
padding: 30px;
}
.logo i {
font-size: 40px;
}
h2 {
font-size: 22px;
}
input {
padding: 10px 10px 10px 35px;
font-size: 15px;
}
.input-group i {
font-size: 16px;
}
button {
padding: 12px;
font-size: 15px;
}
}
@media (max-width: 480px) {
.container {
width: 90%;
padding: 25px;
}
.logo i {
font-size: 36px;
}
h2 {
font-size: 20px;
margin-bottom: 25px;
}
form {
gap: 15px;
}
input {
padding: 10px 10px 10px 32px;
font-size: 14px;
}
.input-group i {
font-size: 15px;
left: 10px;
}
button {
padding: 10px;
font-size: 14px;
}
.error-message {
font-size: 14px;
padding: 8px;
margin-top: 12px;
}
}

View File

@@ -1,794 +0,0 @@
* {
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 900px;
width: 95%;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
position: relative;
margin: 20px auto;
overflow-y: auto;
max-height: calc(100vh - 40px);
scrollbar-width: none;
-ms-overflow-style: none;
}
.container::-webkit-scrollbar {
display: none;
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
font-weight: 700;
font-size: 32px;
position: relative;
padding-bottom: 15px;
}
h1::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 1.5em;
padding-bottom: 10px;
border-bottom: 2px solid rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 10px;
}
/* 导航标签样式 */
.nav-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
border-bottom: 2px solid rgba(0,0,0,0.1);
padding-bottom: 10px;
width: 100%;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.tab-link {
color: #2c3e50;
text-decoration: none;
padding: 12px 25px;
border-radius: 8px 8px 0 0;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
position: relative;
margin: 0 5px;
flex: 1;
text-align: center;
}
.tab-link:hover {
background: rgba(102, 126, 234, 0.1);
}
.tab-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.tab-link.active::after {
content: '';
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
.tab-link i {
font-size: 16px;
}
.config-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 10px;
}
.tab-btn {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0,0,0,0.1);
padding: 10px 20px;
border-radius: 30px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
color: #2c3e50;
font-size: 14px;
}
.tab-btn:hover {
background: rgba(255, 255, 255, 0.9);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.config-section {
display: none;
animation: fadeIn 0.5s ease forwards;
background: rgba(248, 249, 250, 0.9);
padding: 25px;
border-radius: 15px;
margin-bottom: 30px;
border: 1px solid rgba(0,0,0,0.1);
}
.config-section.active {
display: block;
}
.config-item {
margin-bottom: 25px;
position: relative;
}
.config-item label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #2c3e50;
}
.config-item input[type="text"],
.config-item input[type="number"],
.config-item select {
width: 100%;
padding: 12px 15px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
box-sizing: border-box;
}
.config-item input[type="text"]:focus,
.config-item input[type="number"]:focus,
.config-item select:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
outline: none;
}
.help-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #7f8c8d;
}
.array-container {
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
padding: 15px;
background: white;
/* margin-bottom: 10px; */ /* Removed as controls are now outside */
width: 100%;
min-height: 60px;
}
/* Specific style for API Keys container to make it scrollable */
#API_KEYS_container {
max-height: 300px; /* Adjust this value as needed */
overflow-y: auto;
/* Optional: Add some padding to the right for the scrollbar */
padding-right: 5px;
margin-bottom: 10px; /* Add margin below the container */
}
/* Search Input Styles */
.search-container {
margin-bottom: 10px;
}
#apiKeySearchInput {
width: 100%;
padding: 10px 15px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
}
#apiKeySearchInput:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
outline: none;
}
.array-item {
display: flex;
margin-bottom: 10px;
gap: 10px;
background-color: rgba(248, 249, 250, 0.5);
padding: 8px;
border-radius: 8px;
border: 1px dashed rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
}
.array-item:hover {
background-color: rgba(248, 249, 250, 0.8);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.array-item input {
flex: 1;
box-sizing: border-box;
padding: 12px 15px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
.array-item input:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
outline: none;
}
.array-controls {
display: flex;
justify-content: flex-end;
margin-top: 10px; /* Increase margin-top for spacing */
}
.add-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 15px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
}
.add-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.remove-btn {
background: #e74c3c;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.remove-btn:hover {
background: #c0392b;
transform: scale(1.1);
}
.toggle {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.toggle label {
margin-bottom: 0;
flex: 1;
padding-right: 15px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
input:checked + .toggle-slider {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
input:focus + .toggle-slider {
box-shadow: 0 0 1px #667eea;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.form-actions {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 40px;
}
.save-btn, .reset-btn {
padding: 12px 30px;
border-radius: 30px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.save-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.save-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.save-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
}
.reset-btn {
background: linear-gradient(135deg, #e0e0e0 0%, #bdc3c7 100%);
color: #2c3e50;
}
.reset-btn:hover {
background: linear-gradient(135deg, #bdc3c7 0%, #e0e0e0 100%);
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.reset-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(0,0,0,0.05);
}
.save-status {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(39, 174, 96, 0.9);
color: white;
padding: 10px 20px;
border-radius: 30px;
display: flex;
align-items: center;
gap: 10px;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
transition: all 0.3s ease;
}
.save-status.show {
opacity: 1;
}
.save-status.error {
background: rgba(231, 76, 60, 0.9);
}
.provider-config {
display: none;
}
.provider-config.active {
display: block;
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 8px;
background: rgba(0,0,0,0.8);
color: white;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateY(0);
}
.notification.success {
background: rgba(39, 174, 96, 0.9);
}
.notification.error {
background: rgba(231, 76, 60, 0.9);
}
.scroll-buttons {
position: fixed;
right: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.scroll-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.scroll-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: scale(1.1);
}
.scroll-btn:active {
transform: scale(0.95);
}
.refresh-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.refresh-btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 20px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.refresh-btn:active {
transform: scale(0.95);
}
.refresh-btn i {
transition: transform 0.5s ease;
}
.refresh-btn.loading i {
animation: spin 1s linear infinite;
}
.copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.9);
padding: 10px 0;
text-align: center;
font-size: 14px;
color: #2c3e50;
backdrop-filter: blur(5px);
border-top: 1px solid rgba(0,0,0,0.1);
}
.copyright a {
color: #764ba2;
text-decoration: none;
transition: color 0.3s ease;
}
.copyright a:hover {
color: #667eea;
}
.copyright img {
width: 20px;
height: 20px;
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}
/* Modal Styles */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Change back to fixed */
z-index: 1001; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
/* overflow: auto; Removed */
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
/* backdrop-filter: blur(5px); Removed */
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: #fefefe;
padding: 30px;
border: 1px solid #888;
width: 80%; /* Could be more or less, depending on screen size */
max-width: 600px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
position: relative;
animation: slideIn 0.3s ease-out;
margin: auto;
}
.modal-content h2 {
margin-top: 0;
color: #2c3e50;
border-bottom: none; /* Remove border from h2 inside modal */
padding-bottom: 0;
}
.modal-content p {
color: #7f8c8d;
font-size: 14px;
margin-bottom: 15px;
}
.modal-content textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 20px;
font-family: monospace; /* Use monospace for keys */
font-size: 14px;
resize: vertical; /* Allow vertical resizing */
min-height: 150px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 10px;
}
.close-btn {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
line-height: 1;
}
.close-btn:hover,
.close-btn:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.container {
width: 100%;
padding: 20px;
margin: 10px auto;
}
body {
padding: 10px;
}
h1 {
font-size: 24px;
}
.nav-tabs {
flex-direction: column;
align-items: center;
gap: 10px;
}
.tab-link {
width: 100%;
justify-content: center;
border-radius: 8px;
}
.tab-link.active::after {
display: none;
}
.config-tabs {
flex-direction: column;
}
.tab-btn {
width: 100%;
text-align: center;
}
.toggle {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.form-actions {
flex-direction: column;
gap: 15px;
}
.save-btn, .reset-btn {
width: 100%;
justify-content: center;
}
.scroll-buttons {
right: 10px;
bottom: 10px;
}
.scroll-btn {
width: 35px;
height: 35px;
font-size: 16px;
}
.refresh-btn {
top: 10px;
right: 10px;
padding: 8px 16px;
font-size: 12px;
}
}
@media (max-width: 480px) {
.container {
padding: 15px;
}
h1 {
font-size: 20px;
}
.config-section {
padding: 15px;
}
.config-item input[type="text"],
.config-item input[type="number"],
.config-item select {
padding: 10px;
font-size: 14px;
}
}

View File

@@ -1,390 +0,0 @@
/* error_logs.css - Styles specific to the error logs page, complementing config_editor.css */
/* Inherit body, container, h1, nav-tabs, scroll-buttons, refresh-btn, copyright from config_editor.css */
/* Add padding to the main content section */
.config-section {
padding: 25px; /* Increased padding */
background-color: #fff; /* Ensure white background */
border-radius: 12px; /* Slightly larger radius */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); /* Softer shadow */
margin-top: 20px;
}
/* Style the controls container */
.controls-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa; /* Lighter background */
border-radius: 6px; /* Slightly smaller radius */
border: 1px solid #e9ecef; /* Lighter border */
flex-wrap: wrap; /* Allow wrapping on smaller screens */
gap: 15px; /* Add gap between items */
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
}
.page-size-selector label {
font-weight: bold;
color: #2c3e50;
margin-bottom: 0; /* Override default label margin */
}
.page-size-selector select {
padding: 8px 12px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 6px;
background-color: white;
font-size: 14px;
min-width: 70px; /* Ensure select has some width */
}
.page-size-selector span {
color: #2c3e50;
}
/* Style the action button (refresh) */
.action-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(118, 75, 162, 0.2);
}
.action-btn i {
font-size: 1em;
}
/* Search Container Styles */
.search-container {
display: flex;
flex-wrap: wrap; /* Allow wrapping on smaller screens */
gap: 10px; /* Space between elements */
align-items: center;
padding: 15px;
background-color: #f8f9fa; /* Consistent light background */
border-radius: 6px;
border: 1px solid #e9ecef; /* Lighter border */
margin-bottom: 25px; /* Increased margin */
}
.search-container input[type="text"],
.search-container input[type="date"] {
padding: 8px 12px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 6px;
font-size: 14px;
flex-grow: 1; /* Allow inputs to grow */
min-width: 150px; /* Minimum width for inputs */
}
.search-container input[type="date"] {
min-width: 130px; /* Slightly less width for date inputs */
flex-grow: 0; /* Don't let date inputs grow excessively */
}
.search-container span {
color: #2c3e50;
margin: 0 5px;
}
.search-container .action-btn {
padding: 8px 15px; /* Slightly smaller padding for search button */
font-size: 14px;
flex-shrink: 0; /* Prevent button from shrinking */
}
/* Table container and styled table */
.table-container {
overflow-x: auto; /* Allow horizontal scrolling for table */
background-color: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06); /* Softer shadow */
border: 1px solid #e9ecef; /* Lighter border */
}
.styled-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
color: #333;
}
.styled-table thead tr {
background-color: #eef2f7; /* Lighter, slightly blueish header */
text-align: left;
font-weight: 600; /* Slightly bolder */
color: #495057; /* Darker grey text */
border-bottom: 1px solid #dee2e6; /* Thinner border */
}
.styled-table th,
.styled-table td {
padding: 14px 15px; /* Increased padding */
border-bottom: 1px solid #f1f3f5; /* Very light border */
vertical-align: middle;
}
.styled-table tbody tr {
transition: background-color 0.2s ease;
}
.styled-table tbody tr:hover {
background-color: #f8f9fa; /* Lighter hover effect */
}
/* Zebra striping for table body */
.styled-table tbody tr:nth-child(even) {
background-color: #fcfdff; /* Very light blue for even rows */
}
.styled-table tbody tr:nth-child(even):hover {
background-color: #f0f4f8; /* Slightly darker hover for even rows */
}
.styled-table tbody tr:last-of-type {
border-bottom: none; /* Remove border from last row */
}
/* Error log content truncation */
.error-log-content {
max-width: 250px; /* Adjust as needed */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: help; /* Indicate it's expandable/clickable */
}
/* Style for the 'View Details' button */
.btn-view-details {
background-color: #e9ecef; /* Light grey background */
color: #495057; /* Dark grey text */
border: 1px solid #dee2e6; /* Subtle border */
padding: 4px 8px; /* Slightly smaller padding */
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.btn-view-details:hover {
background-color: #dee2e6; /* Darker grey on hover */
border-color: #ced4da;
color: #212529;
}
/* Status Indicators (Loading, No Data, Error) */
.status-indicator {
text-align: center;
padding: 30px 15px;
margin: 20px 0;
border-radius: 8px;
display: none; /* Hidden by default */
color: #555;
}
.status-indicator.loading {
background-color: rgba(240, 240, 240, 0.7);
}
.status-indicator.no-data {
background-color: rgba(240, 240, 240, 0.7);
}
.status-indicator.error {
background-color: rgba(231, 76, 60, 0.1); /* Light red background */
color: #c0392b; /* Darker red text */
font-weight: bold;
}
.status-indicator p {
margin-top: 10px;
margin-bottom: 0;
font-size: 1.1em;
}
/* Basic spinner animation */
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left-color: #667eea;
animation: spin 1s ease infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Pagination Styles */
.pagination-container {
display: flex;
justify-content: center;
padding: 20px 0;
margin-top: 20px;
border-top: 1px solid rgba(0,0,0,0.1);
}
.pagination {
list-style: none;
padding: 0;
margin: 0;
display: flex;
gap: 5px;
}
.pagination .page-item {
margin: 0;
}
.pagination .page-link {
display: block;
padding: 8px 14px; /* Slightly wider */
color: #6c757d; /* Grey text */
background-color: white;
border: 1px solid #dee2e6; /* Lighter border */
border-radius: 4px;
text-decoration: none;
transition: all 0.3s ease;
font-size: 14px;
}
.pagination .page-link:hover {
background-color: #e9ecef; /* Light grey hover */
border-color: #dee2e6;
}
.pagination .page-item.active .page-link {
background: linear-gradient(135deg, #708cf3 0%, #8b60d5 100%); /* Adjusted gradient */
color: white;
border-color: #708cf3;
font-weight: 600;
}
.pagination .page-item.disabled .page-link {
color: #aaa;
pointer-events: none;
background-color: #f9f9f9;
border-color: #ddd;
}
/* Modal Styles (Inherit base from config_editor.css, add specifics) */
#logDetailModal .modal-content {
max-width: 800px; /* Wider modal for logs */
}
#logDetailModal h2 {
font-size: 1.5em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.modal-body-content {
max-height: 60vh; /* Limit height and allow scrolling */
overflow-y: auto;
padding-right: 15px; /* Space for scrollbar */
}
.detail-item {
margin-bottom: 15px;
}
.detail-item h6 {
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
}
.detail-item p,
.detail-item pre {
background-color: #e9ecef; /* Lighter background for code blocks */
padding: 12px;
border-radius: 5px;
border: 1px solid #dee2e6; /* Match border color */
font-size: 14px;
color: #333;
margin: 0;
white-space: pre-wrap; /* Ensure pre content wraps */
word-wrap: break-word;
}
.detail-item pre {
max-height: 200px; /* Limit height of individual pre blocks */
overflow-y: auto;
}
/* Notification Style (Inherited from config_editor.css) */
#copyStatus {
/* Uses .notification styles from config_editor.css */
background-color: rgba(39, 174, 96, 0.9); /* Green for success */
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.controls-container {
flex-direction: column;
align-items: stretch; /* Stretch items to full width */
}
.page-size-selector {
justify-content: center; /* Center page size selector */
}
.action-btn {
width: 100%; /* Full width button */
justify-content: center;
}
.styled-table {
font-size: 13px;
}
.styled-table th,
.styled-table td {
padding: 10px 8px;
}
.error-log-content {
max-width: 150px;
}
#logDetailModal .modal-content {
width: 95%;
padding: 20px;
}
}
@media (max-width: 480px) {
.styled-table {
font-size: 12px;
}
.btn-view-details {
padding: 4px 8px;
font-size: 11px;
}
}

View File

@@ -1,563 +0,0 @@
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 900px;
width: 95%;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
position: relative;
margin: 20px auto;
overflow-y: auto;
max-height: calc(100vh - 40px);
scrollbar-width: none;
-ms-overflow-style: none;
}
.container::-webkit-scrollbar {
display: none;
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 15px;
font-weight: 700;
font-size: 32px;
position: relative;
padding-bottom: 15px;
}
h1::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
/* 导航标签样式 */
.nav-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
border-bottom: 2px solid rgba(0,0,0,0.1);
padding-bottom: 10px;
width: 100%;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.tab-link {
color: #2c3e50;
text-decoration: none;
padding: 12px 25px;
border-radius: 8px 8px 0 0;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
position: relative;
margin: 0 5px;
flex: 1;
text-align: center;
}
.tab-link:hover {
background: rgba(102, 126, 234, 0.1);
}
.tab-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.tab-link.active::after {
content: '';
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
.tab-link i {
font-size: 16px;
}
.key-list {
margin-bottom: 30px;
background: rgba(248, 249, 250, 0.9);
padding: 25px;
border-radius: 15px;
transition: all 0.3s ease;
border: 1px solid rgba(0,0,0,0.1);
animation: fadeIn 0.5s ease forwards;
}
.key-list:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.key-list:nth-child(2) {
animation-delay: 0.2s;
}
.key-list h2 {
color: #2c3e50;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.5em;
padding-bottom: 10px;
border-bottom: 2px solid rgba(0,0,0,0.1);
cursor: pointer;
}
.key-list h2 .toggle-icon {
margin-right: 10px;
transition: transform 0.3s ease;
}
.key-list h2 .toggle-icon.collapsed {
transform: rotate(-90deg);
}
.key-list .key-content {
transition: all 0.3s ease-out;
overflow: hidden;
height: auto;
opacity: 1;
}
.key-list .key-content.collapsed {
height: 0;
opacity: 0;
padding-top: 0;
padding-bottom: 0;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
background: white;
border: 1px solid rgba(0,0,0,0.1);
margin-bottom: 12px;
padding: 15px;
border-radius: 10px;
transition: all 0.3s ease;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
li:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.key-info {
display: flex;
align-items: center;
gap: 15px;
flex: 1; /* Allow key-info to take up available space */
min-width: 0; /* Prevent flex item from overflowing */
}
.key-text {
font-family: 'Roboto Mono', monospace;
color: #2c3e50;
word-break: break-all; /* Ensure long keys wrap */
flex-shrink: 1; /* Allow key text to shrink if needed */
margin-right: 10px; /* Add space between key text and toggle button */
}
.fail-count {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
padding: 4px 10px;
border-radius: 15px;
font-size: 0.85em;
display: flex;
align-items: center;
gap: 5px;
}
.fail-count i {
font-size: 12px;
}
.key-actions {
display: flex;
gap: 10px;
align-items: center;
}
.verify-btn, .copy-btn {
color: white;
border: none;
padding: 8px 15px;
border-radius: 30px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.verify-btn {
background: linear-gradient(135deg, #2ecc71, #27ae60);
}
.verify-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(46, 204, 113, 0.3);
}
.verify-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.verify-btn i {
font-size: 14px;
}
.copy-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.copy-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.copy-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
}
.copy-btn i {
font-size: 14px;
}
.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 25px;
border-radius: 10px;
font-weight: bold;
text-align: center;
font-size: 1.2em;
margin-top: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
#copyStatus {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 15px 30px;
border-radius: 25px;
font-weight: bold;
opacity: 0;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
text-align: center;
min-width: 200px;
color: white;
}
#copyStatus.success {
background: rgba(39, 174, 96, 0.95);
}
#copyStatus.error {
background: rgba(231, 76, 60, 0.95);
}
.status-badge {
padding: 4px 12px;
border-radius: 15px;
font-size: 0.9em;
font-weight: bold;
margin-right: 10px;
}
.status-valid {
background: rgba(39, 174, 96, 0.1);
color: #27ae60;
}
.status-invalid {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
.scroll-buttons {
position: fixed;
right: 20px;
bottom: 20px;
display: none;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.scroll-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.scroll-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: scale(1.1);
}
.scroll-btn:active {
transform: scale(0.95);
}
.refresh-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.refresh-btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 20px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.refresh-btn:active {
transform: scale(0.95);
}
.refresh-btn i {
transition: transform 0.5s ease;
}
.refresh-btn.loading i {
animation: spin 1s linear infinite;
}
.copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.9);
padding: 10px 0;
text-align: center;
font-size: 14px;
color: #2c3e50;
backdrop-filter: blur(5px);
border-top: 1px solid rgba(0,0,0,0.1);
}
.copyright a {
color: #764ba2;
text-decoration: none;
transition: color 0.3s ease;
}
.copyright a:hover {
color: #667eea;
}
.copyright img {
width: 20px;
height: 20px;
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Toggle Visibility Button Styles */
.toggle-vis-btn {
background: none;
border: none;
color: #7f8c8d; /* Subtle color */
cursor: pointer;
padding: 5px;
font-size: 16px;
transition: color 0.3s ease;
margin-left: 5px; /* Space from key text */
flex-shrink: 0; /* Prevent button from shrinking */
}
.toggle-vis-btn:hover {
color: #34495e; /* Darker color on hover */
}
.toggle-vis-btn i {
vertical-align: middle;
}
@media (max-width: 768px) {
.container {
width: 100%;
padding: 20px;
margin: 10px auto;
}
body {
padding: 10px;
}
h1 {
font-size: 24px;
}
.nav-tabs {
flex-direction: column;
align-items: center;
gap: 10px;
}
.tab-link {
width: 100%;
justify-content: center;
border-radius: 8px;
}
.tab-link.active::after {
display: none;
}
.key-list h2 {
font-size: 1.2em;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.key-info {
flex-direction: column;
align-items: flex-start;
gap: 8px;
width: 100%; /* Ensure key-info takes full width */
}
li {
flex-direction: column;
gap: 10px;
}
.key-actions {
width: 100%;
flex-direction: column;
}
.verify-btn, .copy-btn {
width: 100%;
justify-content: center;
}
.key-text {
/* word-break: break-all; */ /* Already applied above */
margin-right: 0; /* Remove right margin on smaller screens */
}
.scroll-buttons {
right: 10px;
bottom: 10px;
}
.scroll-btn {
width: 35px;
height: 35px;
font-size: 16px;
}
.refresh-btn {
top: 10px;
right: 10px;
padding: 8px 16px;
font-size: 12px;
}
}
@media (max-width: 480px) {
.container {
padding: 15px;
}
h1 {
font-size: 20px;
}
.key-list {
padding: 15px;
}
.status-badge {
padding: 3px 8px;
font-size: 0.8em;
}
.fail-count {
font-size: 0.8em;
}
.total {
font-size: 1em;
padding: 12px 20px;
}
}

BIN
app/static/icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
app/static/icons/logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -310,9 +310,13 @@ function switchTab(tabId) {
const tabButtons = document.querySelectorAll('.tab-btn');
tabButtons.forEach(button => {
if (button.getAttribute('data-tab') === tabId) {
button.classList.add('active');
// 激活状态:主色背景,白色文字,添加阴影
button.classList.remove('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70');
button.classList.add('bg-primary-600', 'text-white', 'shadow-md');
} else {
button.classList.remove('active');
// 非激活状态:白色背景,灰色文字,无阴影
button.classList.remove('bg-primary-600', 'text-white', 'shadow-md');
button.classList.add('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70');
}
});
@@ -354,18 +358,19 @@ function addArrayItemWithValue(key, value) {
if (!container) return;
const arrayItem = document.createElement('div');
arrayItem.className = 'array-item';
arrayItem.className = 'array-item flex justify-between items-center mb-2'; // 使用 Flexbox 布局,垂直居中,底部增加间距
const input = document.createElement('input');
input.type = 'text';
input.name = `${key}[]`;
input.value = value;
input.className = 'array-input';
input.className = 'array-input flex-grow px-3 py-2 rounded-md border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 mr-2'; // 输入框占据大部分空间,添加样式和右边距
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150 ml-2'; // 新的 Tailwind 样式
removeBtn.innerHTML = '<i class="fas fa-trash-alt"></i>'; // 改用垃圾桶图标
removeBtn.title = '删除'; // 添加悬停提示
removeBtn.addEventListener('click', function() {
arrayItem.remove();
});
@@ -411,12 +416,43 @@ function collectFormData() {
return formData;
}
// 辅助函数:停止定时任务
async function stopScheduler() {
try {
const response = await fetch('/api/scheduler/stop', { method: 'POST' });
if (!response.ok) {
console.warn(`停止定时任务失败: ${response.status}`);
} else {
console.log('定时任务已停止');
}
} catch (error) {
console.error('调用停止定时任务API时出错:', error);
}
}
// 辅助函数:启动定时任务
async function startScheduler() {
try {
const response = await fetch('/api/scheduler/start', { method: 'POST' });
if (!response.ok) {
console.warn(`启动定时任务失败: ${response.status}`);
} else {
console.log('定时任务已启动');
}
} catch (error) {
console.error('调用启动定时任务API时出错:', error);
}
}
// 保存配置
async function saveConfig() {
try {
const formData = collectFormData();
showNotification('正在保存配置...', 'info');
// 1. 停止定时任务
await stopScheduler();
const response = await fetch('/api/config', {
method: 'PUT',
@@ -435,24 +471,37 @@ async function saveConfig() {
// 显示保存状态
const saveStatus = document.getElementById('saveStatus');
saveStatus.classList.add('show');
saveStatus.style.opacity = "1";
saveStatus.style.transform = "translate(-50%, -50%) scale(1.1)";
setTimeout(() => {
saveStatus.classList.remove('show');
saveStatus.style.opacity = "0";
saveStatus.style.transform = "translate(-50%, -50%) scale(0.95)";
}, 3000);
showNotification('配置保存成功', 'success');
// 3. 启动新的定时任务
await startScheduler();
} catch (error) {
console.error('保存配置失败:', error);
// 保存失败时,也尝试重启定时任务,以防万一
await startScheduler();
// 显示错误状态
const saveStatus = document.getElementById('saveStatus');
saveStatus.classList.add('show', 'error');
saveStatus.style.backgroundColor = "#ef4444"; // 红色背景
saveStatus.style.opacity = "1";
saveStatus.style.transform = "translate(-50%, -50%) scale(1.1)";
saveStatus.querySelector('.status-icon i').className = 'fas fa-times-circle';
saveStatus.querySelector('.status-text').textContent = '配置保存失败';
setTimeout(() => {
saveStatus.classList.remove('show', 'error');
saveStatus.style.opacity = "0";
saveStatus.style.transform = "translate(-50%, -50%) scale(0.95)";
setTimeout(() => {
saveStatus.style.backgroundColor = "#22c55e"; // 恢复绿色背景
}, 300);
}, 3000);
showNotification('保存配置失败: ' + error.message, 'error');
@@ -491,6 +540,9 @@ function resetConfig(event) {
async function executeReset() {
try {
showNotification('正在重置配置...', 'info');
// 1. 停止定时任务
await stopScheduler();
const response = await fetch('/api/config/reset', { method: 'POST' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -498,24 +550,48 @@ async function executeReset() {
const config = await response.json();
populateForm(config);
showNotification('配置已重置为默认值', 'success');
// 3. 启动新的定时任务
await startScheduler();
} catch (error) {
console.error('重置配置失败:', error);
showNotification('重置配置失败: ' + error.message, 'error');
// 重置失败时,也尝试重启定时任务
await startScheduler();
}
}
// 显示通知
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = 'notification show';
if (type) {
notification.classList.add(type);
// 设置适当的样式
if (type === 'error') {
notification.classList.add('bg-danger-500');
notification.classList.remove('bg-black');
} else {
notification.classList.remove('bg-danger-500');
notification.classList.add('bg-black');
// 可以为不同类型设置不同的颜色
if (type === 'success') {
notification.style.backgroundColor = '#22c55e'; // 绿色
} else if (type === 'info') {
notification.style.backgroundColor = '#3b82f6'; // 蓝色
} else if (type === 'warning') {
notification.style.backgroundColor = '#f59e0b'; // 橙色
}
}
// 应用过渡效果 - 与keys_status.js中一致
notification.style.opacity = "1";
notification.style.transform = "translate(-50%, 0)";
// 设置自动消失
setTimeout(() => {
notification.classList.remove('show');
notification.style.opacity = "0";
notification.style.transform = "translate(-50%, 10px)";
}, 3000);
}
@@ -527,8 +603,7 @@ function refreshPage(button) {
// 滚动到顶部
function scrollToTop() {
const container = document.querySelector('.container');
container.scrollTo({
window.scrollTo({
top: 0,
behavior: 'smooth'
});
@@ -536,9 +611,8 @@ function scrollToTop() {
// 滚动到底部
function scrollToBottom() {
const container = document.querySelector('.container');
container.scrollTo({
top: container.scrollHeight,
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
}

View File

@@ -9,32 +9,12 @@ function scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
// 刷新页面功能
function refreshPage(button) {
if (button) {
// Use 'loading' class consistent with config_editor.css animation
button.classList.add('loading');
// Disable button while refreshing
button.disabled = true;
}
// Fetch new data instead of full reload for a smoother experience
loadErrorLogs().finally(() => {
if (button) {
// Remove loading class and re-enable button after fetch completes
button.classList.remove('loading');
button.disabled = false;
}
});
// Optional: Keep reload as fallback or if preferred
// setTimeout(() => {
// window.location.reload();
// }, 500);
}
// Refresh function removed as the buttons are gone.
// If refresh functionality is needed elsewhere, it can be triggered directly by calling loadErrorLogs().
// 全局变量
let currentPage = 1;
let pageSize = 20;
let pageSize = 10;
// let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length
let errorLogs = []; // Store fetched logs for details view
let currentSearch = { // Store current search parameters
@@ -46,7 +26,7 @@ let currentSearch = { // Store current search parameters
// DOM Elements Cache
let pageSizeSelector;
let refreshBtn;
// let refreshBtn; // Removed, as the button is deleted
let tableBody;
let paginationElement;
let loadingIndicator;
@@ -59,12 +39,14 @@ let errorSearchInput;
let startDateInput;
let endDateInput;
let searchBtn;
let pageInput; // 新增:页码输入框
let goToPageBtn; // 新增:跳转按钮
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// Cache DOM elements
pageSizeSelector = document.getElementById('pageSize');
refreshBtn = document.getElementById('refreshBtn');
// refreshBtn = document.getElementById('refreshBtn'); // Removed
tableBody = document.getElementById('errorLogsTable');
paginationElement = document.getElementById('pagination');
loadingIndicator = document.getElementById('loadingIndicator');
@@ -78,6 +60,8 @@ document.addEventListener('DOMContentLoaded', function() {
startDateInput = document.getElementById('startDate');
endDateInput = document.getElementById('endDate');
searchBtn = document.getElementById('searchBtn');
pageInput = document.getElementById('pageInput'); // 新增
goToPageBtn = document.getElementById('goToPageBtn'); // 新增
// Initialize page size selector
if (pageSizeSelector) {
@@ -89,18 +73,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// Initialize refresh button (using the one inside the controls container)
if (refreshBtn) {
refreshBtn.addEventListener('click', function() {
// Add loading state to the button itself
this.classList.add('loading');
this.disabled = true;
loadErrorLogs().finally(() => {
this.classList.remove('loading');
this.disabled = false;
});
});
}
// Refresh button event listener removed
// Initialize search button
if (searchBtn) {
@@ -130,8 +103,113 @@ document.addEventListener('DOMContentLoaded', function() {
// Initial load of error logs
loadErrorLogs();
// Add event listeners for copy buttons inside the modal
setupCopyButtons();
// 新增:为页码跳转按钮添加事件监听器
if (goToPageBtn && pageInput) {
goToPageBtn.addEventListener('click', function() {
const targetPage = parseInt(pageInput.value);
// 需要获取总页数来验证输入
// 暂时无法直接获取 totalPages需要在 updatePagination 中存储或重新计算
// 简单的验证:必须是正整数
if (!isNaN(targetPage) && targetPage >= 1) {
// 理想情况下,应检查 targetPage <= totalPages
// 但 totalPages 可能未知,所以暂时只跳转
currentPage = targetPage;
loadErrorLogs();
pageInput.value = ''; // 清空输入框
} else {
showNotification('请输入有效的页码', 'error', 2000);
pageInput.value = ''; // 清空无效输入
}
});
// 允许按 Enter 键跳转
pageInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
goToPageBtn.click(); // 触发按钮点击
}
});
}
});
// Fallback copy function using document.execCommand
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let successful = false;
try {
successful = document.execCommand('copy');
} catch (err) {
console.error('Fallback copy failed:', err);
successful = false;
}
document.body.removeChild(textArea);
return successful;
}
// Helper function to handle feedback after copy attempt (both modern and fallback)
function handleCopyResult(buttonElement, success) {
const originalIcon = buttonElement.querySelector('i').className; // Store original icon class
const iconElement = buttonElement.querySelector('i');
if (success) {
iconElement.className = 'fas fa-check text-success-500'; // Use checkmark icon class
showNotification('已复制到剪贴板', 'success', 2000);
} else {
iconElement.className = 'fas fa-times text-danger-500'; // Use error icon class
showNotification('复制失败', 'error', 3000);
}
setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class
}
// Function to set up copy button listeners (using modern API with fallback)
function setupCopyButtons() {
const copyButtons = document.querySelectorAll('.copy-btn');
copyButtons.forEach(button => {
button.addEventListener('click', function() {
const targetId = this.getAttribute('data-target');
const targetElement = document.getElementById(targetId);
if (targetElement) {
const textToCopy = targetElement.textContent;
let copySuccess = false;
// Try modern clipboard API first (requires HTTPS or localhost)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy).then(() => {
handleCopyResult(this, true); // Use helper for feedback
}).catch(err => {
console.error('Clipboard API failed, attempting fallback:', err);
// Attempt fallback if modern API fails
copySuccess = fallbackCopyTextToClipboard(textToCopy);
handleCopyResult(this, copySuccess); // Use helper for feedback
});
} else {
// Use fallback if modern API is not available or context is insecure
console.warn("Clipboard API not available or context insecure. Using fallback copy method.");
copySuccess = fallbackCopyTextToClipboard(textToCopy);
handleCopyResult(this, copySuccess); // Use helper for feedback
}
} else {
console.error('Target element not found:', targetId);
showNotification('复制出错:找不到目标元素', 'error');
}
});
});
}
// 加载错误日志数据
async function loadErrorLogs() {
showLoading(true);
@@ -389,10 +467,18 @@ function updatePagination(currentItemCount, totalItems) {
// Helper function to add pagination links
function addPaginationLink(parentElement, text, enabled, clickHandler, isActive = false) {
const pageItem = document.createElement('li');
pageItem.className = `page-item ${!enabled ? 'disabled' : ''} ${isActive ? 'active' : ''}`;
// 移除 'page-item' 和 'active' 类,使用 Tailwind 类进行样式化
// pageItem.className = `page-item ${!enabled ? 'disabled' : ''} ${isActive ? 'active' : ''}`;
const pageLink = document.createElement('a');
pageLink.className = 'page-link';
// 使用 Tailwind 类进行样式化
pageLink.className = `px-3 py-1 rounded-md text-sm transition duration-150 ease-in-out ${
isActive
? 'bg-primary-600 text-white font-semibold shadow-md cursor-default' // 突出当前页样式
: enabled
? 'bg-white text-gray-700 hover:bg-primary-50 hover:text-primary-600 border border-gray-300' // 可点击页码样式
: 'bg-gray-100 text-gray-400 cursor-not-allowed border border-gray-200' // 禁用状态样式 (如 '...')
}`;
pageLink.href = '#'; // Prevent page jump
pageLink.innerHTML = text;
@@ -402,12 +488,14 @@ function addPaginationLink(parentElement, text, enabled, clickHandler, isActive
clickHandler();
});
} else if (!enabled) {
pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on disabled
pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on disabled or active
} else if (isActive) {
pageLink.addEventListener('click', e => e.preventDefault()); // Prevent click on active page
}
pageItem.appendChild(pageLink);
parentElement.appendChild(pageItem);
// 不再需要 li 元素,直接将 a 元素添加到父元素
// pageItem.appendChild(pageLink);
parentElement.appendChild(pageLink);
}

View File

@@ -27,15 +27,21 @@ function copyToClipboard(text) {
function copyKeys(type) {
const keys = Array.from(document.querySelectorAll(`#${type}Keys .key-text`)).map(span => span.dataset.fullKey);
const jsonKeys = JSON.stringify(keys);
copyToClipboard(jsonKeys)
if (keys.length === 0) {
showCopyStatus('没有可复制的密钥', true);
return;
}
const keysText = keys.join('\n');
copyToClipboard(keysText)
.then(() => {
showCopyStatus(`已成功复制${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
showCopyStatus(`已成功复制${keys.length}${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
})
.catch((err) => {
console.error('无法复制文本: ', err);
showCopyStatus('复制失败,请重试');
showCopyStatus('复制失败,请重试', true);
});
}
@@ -46,21 +52,32 @@ function copyKey(key) {
})
.catch((err) => {
console.error('无法复制文本: ', err);
showCopyStatus('复制失败,请重试');
showCopyStatus('复制失败,请重试', true);
});
}
function showCopyStatus(message, type = 'success') {
function showCopyStatus(message, isError = false) {
const statusElement = document.getElementById('copyStatus');
statusElement.textContent = message;
statusElement.className = type; // 设置样式类
statusElement.style.opacity = 1;
// 添加适当的样式类
if (isError) {
statusElement.classList.add('bg-danger-500');
statusElement.classList.remove('bg-black');
} else {
statusElement.classList.remove('bg-danger-500');
statusElement.classList.add('bg-black');
}
// 应用过渡效果
statusElement.style.opacity = "1";
statusElement.style.transform = "translate(-50%, 0)";
// 设置自动消失
setTimeout(() => {
statusElement.style.opacity = 0;
setTimeout(() => {
statusElement.className = ''; // 清除样式类
}, 300);
}, 2000);
statusElement.style.opacity = "0";
statusElement.style.transform = "translate(-50%, 10px)";
}, 3000);
}
async function verifyKey(key, button) {
@@ -70,59 +87,223 @@ async function verifyKey(key, button) {
const originalHtml = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 验证中';
const response = await fetch(`/gemini/v1beta/verify-key/${key}`, {
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.status === 'valid') {
showCopyStatus('密钥验证成功', 'success');
// 根据重置结果更新UI
if (data.success) {
showCopyStatus('失败计数重置成功');
button.style.backgroundColor = '#27ae60';
setTimeout(() => location.reload(), 1500);
} else {
showCopyStatus('密钥验证失败', 'error');
const errorMsg = data.message || '重置失败';
showCopyStatus('重置失败: ' + errorMsg, true);
button.style.backgroundColor = '#e74c3c';
}
// 3秒后恢复按钮原始状态
// 1秒后恢复按钮原始状态
setTimeout(() => {
button.innerHTML = originalHtml;
button.disabled = false;
button.style.backgroundColor = '';
}, 3000);
}, 1000);
} catch (error) {
console.error('验证失败:', error);
showCopyStatus('验证请求失败', 'error');
console.error('重置失败:', error);
showCopyStatus('重置请求失败: ' + error.message, true);
button.disabled = false;
button.innerHTML = '<i class="fas fa-check-circle"></i> 验证';
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');
// 设置标题和消息
titleElement.textContent = '批量重置失败次数';
messageElement.textContent = `确定要批量重置${type === 'valid' ? '有效' : '无效'}密钥的失败次数吗?`;
// 设置确认按钮事件
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';
}
// 设置消息
messageElement.textContent = message;
// 设置确认按钮点击事件
confirmButton.onclick = () => closeResultModal(autoReload);
// 显示模态框
modalElement.classList.remove('hidden');
}
async function executeResetAll(type) {
try {
// 关闭确认模态框
closeResetModal();
// 使用data-reset-type属性直接找到对应的重置按钮
const resetButton = document.querySelector(`button[data-reset-type="${type}"]`);
if (!resetButton) {
// 如果找不到按钮,显示错误并返回
showResultModal(false, `找不到${type === 'valid' ? '有效' : '无效'}密钥区域的批量重置按钮`);
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-all-fail-counts?key_type=${type}`, {
method: 'POST'
});
if (!response.ok) {
throw new Error(`服务器返回错误: ${response.status}`);
}
const data = await response.json();
// 根据重置结果显示模态框
if (data.success) {
const message = data.reset_count ?
`成功重置${data.reset_count}${type === 'valid' ? '有效' : '无效'}密钥的失败次数` :
'所有失败次数重置成功';
showResultModal(true, message);
} else {
const errorMsg = data.message || '批量重置失败';
showResultModal(false, '批量重置失败: ' + errorMsg);
}
} catch (fetchError) {
console.error('API请求失败:', fetchError);
showResultModal(false, '批量重置请求失败: ' + fetchError.message);
} finally {
// 恢复按钮原始状态
setTimeout(() => {
resetButton.innerHTML = originalHtml;
resetButton.disabled = false;
}, 500);
}
} catch (error) {
console.error('批量重置失败:', error);
showResultModal(false, '批量重置处理失败: ' + error.message);
}
}
function scrollToTop() {
const container = document.querySelector('.container');
container.scrollTo({
top: 0,
behavior: 'smooth'
});
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function scrollToBottom() {
const container = document.querySelector('.container');
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
// 移除这个函数,因为它可能正在干扰按钮的显示
// HTML中已经设置了滚动按钮为flex显示不需要JavaScript额外控制
function updateScrollButtons() {
const container = document.querySelector('.container');
const scrollButtons = document.querySelector('.scroll-buttons');
if (container.scrollHeight > container.clientHeight) {
scrollButtons.style.display = 'flex';
} else {
scrollButtons.style.display = 'none';
}
// 不执行任何操作
}
function refreshPage(button) {
@@ -142,22 +323,50 @@ function toggleSection(header, sectionId) {
content.classList.toggle('collapsed');
}
// 筛选有效密钥(根据失败次数阈值)
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', () => {
// 检查滚动按钮
updateScrollButtons();
// 移除对滚动按钮显示的控制让它们由HTML/CSS控制
// 监听展开/折叠事件
document.querySelectorAll('.key-list h2').forEach(header => {
header.addEventListener('click', () => {
setTimeout(updateScrollButtons, 300);
// 不再调用updateScrollButtons
});
});
// 更新版权年份
const copyrightYear = document.querySelector('.copyright script');
if (copyrightYear) {
copyrightYear.textContent = new Date().getFullYear();
const copyrightYearElement = document.querySelector('.copyright script');
if (copyrightYearElement && copyrightYearElement.parentNode.classList.contains('copyright')) {
// 确保只更新版权部分的年份
copyrightYearElement.textContent = new Date().getFullYear();
}
// 添加筛选输入框事件监听
const thresholdInput = document.getElementById('failCountThreshold');
if (thresholdInput) {
// 使用 'input' 事件实时响应输入变化
thresholdInput.addEventListener('input', filterValidKeys);
// 初始加载时应用一次筛选基于默认值1
filterValidKeys();
}
});
@@ -174,8 +383,8 @@ if ('serviceWorker' in navigator) {
});
}
function toggleKeyVisibility(button) {
const keyInfoDiv = button.closest('.key-info');
const keyTextSpan = keyInfoDiv.querySelector('.key-text');
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);

View File

@@ -1,42 +1,124 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>验证页面</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/auth.css">
</head>
<body>
<div class="container">
<div class="logo">
<i class="fas fa-shield-alt"></i>
</div>
<h2>安全验证</h2>
<form id="auth-form" action="/auth" method="post">
<div class="input-group">
<i class="fas fa-key"></i>
<input type="password" id="auth-token" name="auth_token" required placeholder="请输入验证令牌">
{% extends "base.html" %}
{% block title %}验证页面 - Gemini Balance{% endblock %}
{% block head_extra_styles %}
<style>
/* auth.html specific styles */
.auth-glass-card { /* Renamed to avoid conflict if base.html has .glass-card */
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.auth-bg-gradient { /* Renamed to avoid conflict if base.html has .bg-gradient */
background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 50%, #EC4899 100%);
}
/* .input-icon class removed, using direct Tailwind classes now */
/* Keep button ripple effect if needed, or remove if base provides similar */
.auth-button { /* Renamed to avoid conflict */
position: relative;
overflow: hidden;
}
.auth-button:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.auth-button:active:after {
width: 300px;
height: 300px;
opacity: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="auth-bg-gradient min-h-screen flex flex-col justify-center items-center p-4">
<div class="glass-card rounded-2xl shadow-2xl p-10 max-w-md w-full mx-auto transform transition duration-500 hover:-translate-y-1 hover:shadow-3xl animate-fade-in">
<div class="flex justify-center mb-8 animate-slide-down">
<div class="rounded-full bg-primary-100 p-4 text-primary-600">
<i class="fas fa-shield-alt text-4xl"></i>
</div>
<button type="submit">
验证访问
</div>
<h2 class="text-3xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-primary-700 mb-8 animate-slide-down">
<img src="/static/icons/logo.png" alt="Gemini Balance Logo" class="h-9 inline-block align-middle mr-2">
Gemini Balance
</h2>
<form id="auth-form" action="/auth" method="post" class="space-y-6 animate-slide-up">
<div class="relative">
<i class="fas fa-key absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500"></i>
<input
type="password"
id="auth-token"
name="auth_token"
required
placeholder="请输入验证令牌"
class="w-full pl-10 pr-4 py-4 rounded-xl border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 transition duration-300 bg-white bg-opacity-90 text-gray-700"
>
</div>
<button
type="submit"
class="w-full py-4 rounded-xl bg-gradient-to-r from-primary-600 to-primary-700 text-white font-semibold transition duration-300 transform hover:-translate-y-1 hover:shadow-lg"
>
登录
</button>
</form>
{% if error %}
<p class="error-message">{{ error }}</p>
<p class="mt-4 text-red-500 text-center font-medium p-3 bg-red-50 rounded-lg border border-red-200 animate-shake">
{{ error }}
</p>
{% endif %}
</div>
<div class="copyright">
© <script>document.write(new Date().getFullYear())</script> by <a href="https://linux.do/u/snaily" target="_blank"><img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily">snaily</a> |
<a href="https://github.com/snailyp/gemini-balance" target="_blank"><i class="fab fa-github"></i> GitHub</a>
</div>
<script src="/static/js/auth.js"></script>
</body>
</html>
</div> <!-- Close auth-bg-gradient div -->
<!-- Notification placeholder for base.html's showNotification -->
<div id="notification" class="notification"></div>
{% endblock %}
{% block body_scripts %}
<script>
// auth.html specific JavaScript
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('auth-form');
if (form) {
form.addEventListener('submit', function(e) {
const token = document.getElementById('auth-token').value.trim();
if (!token) {
e.preventDefault();
// Use the base notification system
showNotification('请输入验证令牌', 'error');
}
});
}
// Apply renamed classes
document.querySelectorAll('button[type="submit"]').forEach(button => {
button.classList.add('auth-button');
});
const card = document.querySelector('.auth-glass-card'); // Find the renamed card
if (card) {
// If the base template also defines .glass-card, remove it first
// card.classList.remove('glass-card');
} else {
// If the card wasn't found by the new name, try the old name and rename
const oldCard = document.querySelector('.glass-card');
if (oldCard) {
oldCard.classList.remove('glass-card');
oldCard.classList.add('auth-glass-card');
}
}
});
</script>
{% endblock %}

265
app/templates/base.html Normal file
View File

@@ -0,0 +1,265 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Gemini Balance{% endblock %}</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#4F46E5">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1',
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
},
success: {
50: '#ecfdf5',
500: '#10b981',
600: '#059669'
},
danger: {
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626'
}
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.5s ease-out',
'slide-down': 'slideDown 0.5s ease-out',
'shake': 'shake 0.5s ease-in-out',
'spin': 'spin 1s linear infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideDown: {
'0%': { transform: 'translateY(-20px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
shake: {
'0%, 100%': { transform: 'translateX(0)' },
'25%': { transform: 'translateX(-5px)' },
'75%': { transform: 'translateX(5px)' },
},
spin: {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
},
}
}
}
</script>
<style>
.glass-card {
background: rgba(255, 255, 255, 0.85); /* Slightly increased opacity for better readability */
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.18); /* Subtle border */
}
.bg-gradient {
background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 50%, #EC4899 100%);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(243, 244, 246, 0.8); /* bg-gray-100 with opacity */
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(79, 70, 229, 0.4); /* primary-600 with opacity */
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(79, 70, 229, 0.6); /* primary-600 with more opacity */
}
/* Basic modal styles */
.modal {
display: none;
position: fixed;
z-index: 50;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
/* Loading spinner */
.loading-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Notification */
.notification {
position: fixed;
bottom: 5rem; /* Adjusted from bottom-20 */
left: 50%;
transform: translateX(-50%);
padding: 0.75rem 1.25rem; /* px-5 py-3 */
border-radius: 0.5rem; /* rounded-lg */
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-weight: 500; /* font-medium */
z-index: 50;
opacity: 0;
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.notification.show {
opacity: 1;
transform: translate(-50%, 0);
}
.notification.error {
background-color: rgba(220, 38, 38, 0.8); /* danger-600 with opacity */
}
/* Scroll buttons */
.scroll-buttons {
position: fixed;
right: 1.25rem; /* right-5 */
bottom: 5rem; /* bottom-20 */
display: flex;
flex-direction: column;
gap: 0.5rem; /* gap-2 */
z-index: 10;
}
.scroll-button {
width: 2.5rem; /* w-10 */
height: 2.5rem; /* h-10 */
background-color: #4f46e5; /* bg-primary-600 */
color: white;
border-radius: 9999px; /* rounded-full */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease-in-out;
}
.scroll-button:hover {
background-color: #4338ca; /* hover:bg-primary-700 */
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* hover:shadow-lg */
}
{% block head_extra_styles %}
{% endblock %}
</style>
{% block head_extra_scripts %}{% endblock %}
</head>
<body class="bg-gradient min-h-screen text-gray-800 pt-6 pb-16">
{% block content %}{% endblock %}
<!-- 底部版权 -->
<div class="fixed bottom-0 left-0 w-full py-3 bg-white bg-opacity-80 backdrop-blur-md text-center text-sm text-gray-600 border-t border-gray-200">
© <span id="copyright-year"></span> by
<a href="https://linux.do/u/snaily" target="_blank" class="text-primary-600 hover:text-primary-800 transition duration-300">
<img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily" class="inline-block w-5 h-5 rounded-full align-middle mr-1">snaily
</a> |
<a href="https://github.com/snailyp/gemini-balance" target="_blank" class="text-primary-600 hover:text-primary-800 transition duration-300">
<i class="fab fa-github"></i> GitHub
</a>
</div>
<!-- 通用JS -->
<script>
// 设置版权年份
document.getElementById('copyright-year').textContent = new Date().getFullYear();
// 滚动到顶部/底部函数 (如果页面需要)
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function scrollToBottom() {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
// 显示通知
function showNotification(message, type = 'success', duration = 3000) {
const notification = document.getElementById('notification') || createNotificationElement();
if (!notification) return;
notification.textContent = message;
notification.className = 'notification show'; // Reset classes
if (type === 'error') {
notification.classList.add('error');
}
// Clear previous timeout if exists
if (notification.timeoutId) {
clearTimeout(notification.timeoutId);
}
notification.timeoutId = setTimeout(() => {
notification.classList.remove('show');
// Optional: remove the element after fade out if dynamically created
// setTimeout(() => notification.remove(), 300);
}, duration);
}
// Helper to create notification element if it doesn't exist
function createNotificationElement() {
let notification = document.getElementById('notification');
if (!notification) {
notification = document.createElement('div');
notification.id = 'notification';
notification.className = 'notification';
document.body.appendChild(notification);
}
return notification;
}
// 页面刷新带加载状态
function refreshPage(button) {
if (button) {
const icon = button.querySelector('i');
if (icon) {
icon.classList.add('loading-spin');
}
}
setTimeout(() => {
window.location.reload();
}, 300); // Short delay to show spinner
}
</script>
{% block body_scripts %}{% endblock %}
</body>
</html>

View File

@@ -1,336 +1,455 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置编辑器</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/config_editor.css">
</head>
<body>
<div class="container">
<button class="refresh-btn" onclick="refreshPage(this)">
<i class="fas fa-sync-alt"></i>
</button>
<h1>Gemini Balance</h1>
<div class="nav-tabs">
<a href="/config" class="tab-link active">
<i class="fas fa-cog"></i> 配置编辑
</a>
<a href="/keys" class="tab-link">
<i class="fas fa-key"></i> 密钥管理
</a>
<a href="/logs" class="tab-link">
<i class="fas fa-exclamation-triangle"></i> 错误日志
</a>
</div>
<div class="config-tabs">
<button class="tab-btn active" data-tab="api">API配置</button>
<button class="tab-btn" data-tab="model">模型配置</button>
<button class="tab-btn" data-tab="image">图像生成</button>
<button class="tab-btn" data-tab="stream">流式输出</button>
</div>
<div class="save-status" id="saveStatus">
<span class="status-icon"><i class="fas fa-check-circle"></i></span>
<span class="status-text">配置已保存</span>
</div>
<form id="configForm">
<!-- API相关配置 -->
<div class="config-section active" id="api-section">
<h2><i class="fas fa-key"></i> API相关配置</h2>
{% extends "base.html" %}
{% block title %}配置编辑器 - Gemini Balance{% endblock %}
{% block head_extra_styles %}
<style>
/* config_editor.html specific styles */
/* Animations (already in base.html, but keep fade-in class usage) */
.fade-in {
animation: fadeIn 0.3s ease forwards;
}
/* Modal specific styles (already in base.html) */
.array-container {
max-height: 300px;
overflow-y: auto;
padding-right: 5px; /* Keep specific padding if needed */
}
#API_KEYS_container { /* Keep specific ID styling if needed */
max-height: 300px;
overflow-y: auto;
}
.config-section {
display: none;
}
.config-section.active {
display: block;
animation: fadeIn 0.3s ease forwards; /* Use base animation */
}
.provider-config {
display: none;
}
.provider-config.active {
display: block;
}
/* Tailwind Toggle Switch Helper CSS */
.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 content %}
<div class="container max-w-4xl mx-auto px-4">
<div class="glass-card rounded-2xl shadow-xl p-6 md:p-8">
<button class="absolute top-6 right-6 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)">
<i class="fas fa-sync-alt"></i>
</button>
<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-primary-600 text-white shadow-md">
<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-white bg-opacity-50 hover:bg-opacity-70 text-gray-700 transition-all duration-200">
<i class="fas fa-key"></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>
<!-- Config Tabs -->
<div class="flex justify-center mb-6 flex-wrap gap-2">
<button class="tab-btn bg-primary-600 text-white px-5 py-2 rounded-full shadow-md font-medium text-sm" data-tab="api">
API配置
</button>
<button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="model">
模型配置
</button>
<button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="image">
图像生成
</button>
<button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="stream">
流式输出
</button>
<button class="tab-btn bg-white bg-opacity-50 text-gray-700 px-5 py-2 rounded-full font-medium text-sm hover:bg-opacity-70 transition-all duration-200" data-tab="scheduler">
定时任务
</button>
</div>
<!-- Save Status Banner -->
<div class="save-status fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-green-500 text-white px-8 py-4 rounded-xl font-medium flex items-center gap-3 shadow-xl z-50 opacity-0 transition-all duration-300 scale-105" id="saveStatus">
<span class="status-icon text-xl"><i class="fas fa-check-circle"></i></span>
<span class="status-text text-lg">配置已保存</span>
</div>
<!-- Configuration Form -->
<form id="configForm" class="mt-6">
<!-- API 相关配置 -->
<div class="config-section active bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="api-section">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-key text-primary-600"></i> API相关配置
</h2>
<!-- API密钥列表 -->
<div class="mb-6">
<label for="API_KEYS" class="block font-semibold mb-2 text-gray-700">API密钥列表</label>
<div class="mb-2">
<input type="search" id="apiKeySearchInput" placeholder="搜索密钥..." class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
</div>
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="API_KEYS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="flex justify-end">
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" id="addApiKeyBtn">
<i class="fas fa-plus"></i> 添加密钥
</button>
</div>
<small class="text-gray-500 mt-1 block">Gemini API密钥列表每行一个</small>
</div>
<!-- 允许的令牌列表 -->
<div class="mb-6">
<label for="ALLOWED_TOKENS" class="block font-semibold mb-2 text-gray-700">允许的令牌列表</label>
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="ALLOWED_TOKENS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="flex justify-end">
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" onclick="addArrayItem('ALLOWED_TOKENS')">
<i class="fas fa-plus"></i> 添加令牌
</button>
</div>
<small class="text-gray-500 mt-1 block">允许访问API的令牌列表</small>
</div>
<!-- 认证令牌 -->
<div class="mb-6">
<label for="AUTH_TOKEN" class="block font-semibold mb-2 text-gray-700">认证令牌</label>
<input type="text" id="AUTH_TOKEN" name="AUTH_TOKEN" placeholder="默认使用ALLOWED_TOKENS中的第一个" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">用于API认证的令牌</small>
</div>
<!-- API基础URL -->
<div class="mb-6">
<label for="BASE_URL" class="block font-semibold mb-2 text-gray-700">API基础URL</label>
<input type="text" id="BASE_URL" name="BASE_URL" placeholder="https://generativelanguage.googleapis.com/v1beta" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">Gemini API的基础URL</small>
</div>
<!-- 最大失败次数 -->
<div class="mb-6">
<label for="MAX_FAILURES" class="block font-semibold mb-2 text-gray-700">最大失败次数</label>
<input type="number" id="MAX_FAILURES" name="MAX_FAILURES" min="1" max="100" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">API密钥失败后标记为无效的次数</small>
</div>
<!-- 请求超时时间 -->
<div class="mb-6">
<label for="TIME_OUT" class="block font-semibold mb-2 text-gray-700">请求超时时间(秒)</label>
<input type="number" id="TIME_OUT" name="TIME_OUT" min="1" max="600" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">API请求的超时时间</small>
</div>
<!-- 最大重试次数 -->
<div class="mb-6">
<label for="MAX_RETRIES" class="block font-semibold mb-2 text-gray-700">最大重试次数</label>
<input type="number" id="MAX_RETRIES" name="MAX_RETRIES" min="0" max="10" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">API请求失败后的最大重试次数</small>
</div>
</div>
<div class="config-item array-input">
<label for="API_KEYS">API密钥列表</label>
<div class="search-container">
<input type="search" id="apiKeySearchInput" placeholder="搜索密钥...">
<!-- 模型相关配置 -->
<div class="config-section bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="model-section">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-robot text-primary-600"></i> 模型相关配置
</h2>
<!-- 测试模型 -->
<div class="mb-6">
<label for="TEST_MODEL" class="block font-semibold mb-2 text-gray-700">测试模型</label>
<input type="text" id="TEST_MODEL" name="TEST_MODEL" placeholder="gemini-1.5-flash" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">用于测试API密钥的模型</small>
</div>
<div class="array-container" id="API_KEYS_container">
<!-- 数组项将在这里动态添加 -->
<!-- 图像模型列表 -->
<div class="mb-6">
<label for="IMAGE_MODELS" class="block font-semibold mb-2 text-gray-700">图像模型列表</label>
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="IMAGE_MODELS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="flex justify-end">
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" onclick="addArrayItem('IMAGE_MODELS')">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
<small class="text-gray-500 mt-1 block">支持图像处理的模型列表</small>
</div>
<div class="array-controls">
<button type="button" class="add-btn" id="addApiKeyBtn">
<i class="fas fa-plus"></i> 添加密钥
</button>
<!-- 搜索模型列表 -->
<div class="mb-6">
<label for="SEARCH_MODELS" class="block font-semibold mb-2 text-gray-700">搜索模型列表</label>
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="SEARCH_MODELS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="flex justify-end">
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" onclick="addArrayItem('SEARCH_MODELS')">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
<small class="text-gray-500 mt-1 block">支持搜索功能的模型列表</small>
</div>
<!-- 过滤模型列表 -->
<div class="mb-6">
<label for="FILTERED_MODELS" class="block font-semibold mb-2 text-gray-700">过滤模型列表</label>
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="FILTERED_MODELS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="flex justify-end">
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" onclick="addArrayItem('FILTERED_MODELS')">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
<small class="text-gray-500 mt-1 block">需要过滤的模型列表</small>
</div>
<!-- 启用代码执行工具 -->
<div class="mb-6 flex items-center justify-between">
<label for="TOOLS_CODE_EXECUTION_ENABLED" class="font-semibold text-gray-700">启用代码执行工具</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="TOOLS_CODE_EXECUTION_ENABLED" id="TOOLS_CODE_EXECUTION_ENABLED" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="TOOLS_CODE_EXECUTION_ENABLED" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- 显示搜索链接 -->
<div class="mb-6 flex items-center justify-between">
<label for="SHOW_SEARCH_LINK" class="font-semibold text-gray-700">显示搜索链接</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="SHOW_SEARCH_LINK" id="SHOW_SEARCH_LINK" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="SHOW_SEARCH_LINK" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- 显示思考过程 -->
<div class="mb-6 flex items-center justify-between">
<label for="SHOW_THINKING_PROCESS" class="font-semibold text-gray-700">显示思考过程</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="SHOW_THINKING_PROCESS" id="SHOW_THINKING_PROCESS" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="SHOW_THINKING_PROCESS" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
</div>
<!-- 图像生成相关配置 -->
<div class="config-section bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="image-section">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-image text-primary-600"></i> 图像生成配置
</h2>
<!-- 付费API密钥 -->
<div class="mb-6">
<label for="PAID_KEY" class="block font-semibold mb-2 text-gray-700">付费API密钥</label>
<input type="text" id="PAID_KEY" name="PAID_KEY" placeholder="AIzaSyxxxxxxxxxxxxxxxxxxx" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">用于图像生成的付费API密钥</small>
</div>
<!-- 图像生成模型 -->
<div class="mb-6">
<label for="CREATE_IMAGE_MODEL" class="block font-semibold mb-2 text-gray-700">图像生成模型</label>
<input type="text" id="CREATE_IMAGE_MODEL" name="CREATE_IMAGE_MODEL" placeholder="imagen-3.0-generate-002" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">用于图像生成的模型</small>
</div>
<!-- 上传提供商 -->
<div class="mb-6">
<label for="UPLOAD_PROVIDER" class="block font-semibold mb-2 text-gray-700">上传提供商</label>
<select id="UPLOAD_PROVIDER" name="UPLOAD_PROVIDER" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white">
<option value="smms" selected>SM.MS</option>
<option value="picgo">PicGo</option>
<option value="cloudflare">Cloudflare</option>
</select>
<small class="text-gray-500 mt-1 block">图片上传服务提供商</small>
</div>
<!-- SM.MS密钥 -->
<div class="mb-6 provider-config active" data-provider="smms">
<label for="SMMS_SECRET_TOKEN" class="block font-semibold mb-2 text-gray-700">SM.MS密钥</label>
<input type="text" id="SMMS_SECRET_TOKEN" name="SMMS_SECRET_TOKEN" placeholder="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">SM.MS图床的密钥</small>
</div>
<!-- PicGo API密钥 -->
<div class="mb-6 provider-config" data-provider="picgo">
<label for="PICGO_API_KEY" class="block font-semibold mb-2 text-gray-700">PicGo API密钥</label>
<input type="text" id="PICGO_API_KEY" name="PICGO_API_KEY" placeholder="xxxx" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">PicGo的API密钥</small>
</div>
<!-- Cloudflare图床URL -->
<div class="mb-6 provider-config" data-provider="cloudflare">
<label for="CLOUDFLARE_IMGBED_URL" class="block font-semibold mb-2 text-gray-700">Cloudflare图床URL</label>
<input type="text" id="CLOUDFLARE_IMGBED_URL" name="CLOUDFLARE_IMGBED_URL" placeholder="https://xxxxxxx.pages.dev/upload" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">Cloudflare图床的URL</small>
</div>
<!-- Cloudflare认证码 -->
<div class="mb-6 provider-config" data-provider="cloudflare">
<label for="CLOUDFLARE_IMGBED_AUTH_CODE" class="block font-semibold mb-2 text-gray-700">Cloudflare认证码</label>
<input type="text" id="CLOUDFLARE_IMGBED_AUTH_CODE" name="CLOUDFLARE_IMGBED_AUTH_CODE" placeholder="xxxxxxxxx" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">Cloudflare图床的认证码</small>
</div>
</div>
<!-- 流式输出优化器配置 -->
<div class="config-section bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="stream-section">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-stream text-primary-600"></i> 流式输出优化器
</h2>
<!-- 启用流式输出优化 -->
<div class="mb-6 flex items-center justify-between">
<label for="STREAM_OPTIMIZER_ENABLED" class="font-semibold text-gray-700">启用流式输出优化</label>
<div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="STREAM_OPTIMIZER_ENABLED" id="STREAM_OPTIMIZER_ENABLED" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="STREAM_OPTIMIZER_ENABLED" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<!-- 最小延迟 -->
<div class="mb-6">
<label for="STREAM_MIN_DELAY" class="block font-semibold mb-2 text-gray-700">最小延迟(秒)</label>
<input type="number" id="STREAM_MIN_DELAY" name="STREAM_MIN_DELAY" min="0" max="1" step="0.001" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">流式输出的最小延迟时间</small>
</div>
<!-- 最大延迟 -->
<div class="mb-6">
<label for="STREAM_MAX_DELAY" class="block font-semibold mb-2 text-gray-700">最大延迟(秒)</label>
<input type="number" id="STREAM_MAX_DELAY" name="STREAM_MAX_DELAY" min="0" max="1" step="0.001" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">流式输出的最大延迟时间</small>
</div>
<!-- 短文本阈值 -->
<div class="mb-6">
<label for="STREAM_SHORT_TEXT_THRESHOLD" class="block font-semibold mb-2 text-gray-700">短文本阈值</label>
<input type="number" id="STREAM_SHORT_TEXT_THRESHOLD" name="STREAM_SHORT_TEXT_THRESHOLD" min="1" max="100" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">短文本的字符阈值</small>
</div>
<!-- 长文本阈值 -->
<div class="mb-6">
<label for="STREAM_LONG_TEXT_THRESHOLD" class="block font-semibold mb-2 text-gray-700">长文本阈值</label>
<input type="number" id="STREAM_LONG_TEXT_THRESHOLD" name="STREAM_LONG_TEXT_THRESHOLD" min="1" max="1000" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">长文本的字符阈值</small>
</div>
<!-- 分块大小 -->
<div class="mb-6">
<label for="STREAM_CHUNK_SIZE" class="block font-semibold mb-2 text-gray-700">分块大小</label>
<input type="number" id="STREAM_CHUNK_SIZE" name="STREAM_CHUNK_SIZE" min="1" max="100" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">流式输出的分块大小</small>
</div>
<small class="help-text">Gemini API密钥列表每行一个</small>
</div>
<div class="config-item array-input">
<label for="ALLOWED_TOKENS">允许的令牌列表</label>
<div class="array-container" id="ALLOWED_TOKENS_container">
<!-- 数组项将在这里动态添加 -->
</div>
<div class="array-controls">
<button type="button" class="add-btn" onclick="addArrayItem('ALLOWED_TOKENS')">
<i class="fas fa-plus"></i> 添加令牌
</button>
</div>
<small class="help-text">允许访问API的令牌列表</small>
</div>
<div class="config-item">
<label for="AUTH_TOKEN">认证令牌</label>
<input type="text" id="AUTH_TOKEN" name="AUTH_TOKEN" placeholder="默认使用ALLOWED_TOKENS中的第一个">
<small class="help-text">用于API认证的令牌</small>
</div>
<div class="config-item">
<label for="BASE_URL">API基础URL</label>
<input type="text" id="BASE_URL" name="BASE_URL" placeholder="https://generativelanguage.googleapis.com/v1beta">
<small class="help-text">Gemini API的基础URL</small>
</div>
<div class="config-item">
<label for="MAX_FAILURES">最大失败次数</label>
<input type="number" id="MAX_FAILURES" name="MAX_FAILURES" min="1" max="100">
<small class="help-text">API密钥失败后标记为无效的次数</small>
</div>
<div class="config-item">
<label for="TIME_OUT">请求超时时间(秒)</label>
<input type="number" id="TIME_OUT" name="TIME_OUT" min="1" max="600">
<small class="help-text">API请求的超时时间</small>
</div>
<div class="config-item">
<label for="MAX_RETRIES">最大重试次数</label>
<input type="number" id="MAX_RETRIES" name="MAX_RETRIES" min="0" max="10">
<small class="help-text">API请求失败后的最大重试次数</small>
</div>
</div>
<!-- 模型相关配置 -->
<div class="config-section" id="model-section">
<h2><i class="fas fa-robot"></i> 模型相关配置</h2>
<div class="config-item">
<label for="TEST_MODEL">测试模型</label>
<input type="text" id="TEST_MODEL" name="TEST_MODEL" placeholder="gemini-1.5-flash">
<small class="help-text">用于测试API密钥的模型</small>
</div>
<div class="config-item array-input">
<label for="IMAGE_MODELS">图像模型列表</label>
<div class="array-container" id="IMAGE_MODELS_container">
<!-- 数组项将在这里动态添加 -->
<div class="array-controls">
<button type="button" class="add-btn" onclick="addArrayItem('IMAGE_MODELS')">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
<!-- 定时任务配置 -->
<div class="config-section bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="scheduler-section">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-clock text-primary-600"></i> 定时任务配置
</h2>
<!-- 检查间隔 -->
<div class="mb-6">
<label for="CHECK_INTERVAL_HOURS" class="block font-semibold mb-2 text-gray-700">检查间隔(小时)</label>
<input type="number" id="CHECK_INTERVAL_HOURS" name="CHECK_INTERVAL_HOURS" min="1" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">定时检查密钥状态的间隔时间(单位:小时)</small>
</div>
<small class="help-text">支持图像处理的模型列表</small>
</div>
<div class="config-item array-input">
<label for="SEARCH_MODELS">搜索模型列表</label>
<div class="array-container" id="SEARCH_MODELS_container">
<!-- 数组项将在这里动态添加 -->
<div class="array-controls">
<button type="button" class="add-btn" onclick="addArrayItem('SEARCH_MODELS')">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
</div>
<small class="help-text">支持搜索功能的模型列表</small>
</div>
<div class="config-item array-input">
<label for="FILTERED_MODELS">过滤模型列表</label>
<div class="array-container" id="FILTERED_MODELS_container">
<!-- 数组项将在这里动态添加 -->
<div class="array-controls">
<button type="button" class="add-btn" onclick="addArrayItem('FILTERED_MODELS')">
<i class="fas fa-plus"></i> 添加模型
</button>
</div>
</div>
<small class="help-text">需要过滤的模型列表</small>
</div>
<div class="config-item toggle">
<label for="TOOLS_CODE_EXECUTION_ENABLED">启用代码执行工具</label>
<div class="toggle-switch">
<input type="checkbox" id="TOOLS_CODE_EXECUTION_ENABLED" name="TOOLS_CODE_EXECUTION_ENABLED">
<span class="toggle-slider"></span>
<!-- 时区 -->
<div class="mb-6">
<label for="TIMEZONE" class="block font-semibold mb-2 text-gray-700">时区</label>
<input type="text" id="TIMEZONE" name="TIMEZONE" placeholder="例如: Asia/Shanghai" class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
<small class="text-gray-500 mt-1 block">定时任务使用的时区,格式如 "Asia/Shanghai" 或 "UTC"</small>
</div>
</div>
<div class="config-item toggle">
<label for="SHOW_SEARCH_LINK">显示搜索链接</label>
<div class="toggle-switch">
<input type="checkbox" id="SHOW_SEARCH_LINK" name="SHOW_SEARCH_LINK">
<span class="toggle-slider"></span>
</div>
<!-- Action Buttons -->
<div class="flex flex-col md:flex-row justify-center gap-4 mt-8">
<button type="button" id="saveBtn" class="bg-gradient-to-r from-primary-600 to-primary-700 text-white px-8 py-3 rounded-xl font-semibold transition-all duration-300 transform hover:-translate-y-1 hover:shadow-lg flex items-center justify-center gap-2">
<i class="fas fa-save"></i> 保存配置
</button>
<button type="button" id="resetBtn" class="bg-gradient-to-r from-gray-400 to-gray-500 text-white px-8 py-3 rounded-xl font-semibold transition-all duration-300 transform hover:-translate-y-1 hover:shadow-lg flex items-center justify-center gap-2">
<i class="fas fa-undo"></i> 重置配置
</button>
</div>
<div class="config-item toggle">
<label for="SHOW_THINKING_PROCESS">显示思考过程</label>
<div class="toggle-switch">
<input type="checkbox" id="SHOW_THINKING_PROCESS" name="SHOW_THINKING_PROCESS">
<span class="toggle-slider"></span>
</div>
</div>
</div>
<!-- 图像生成相关配置 -->
<div class="config-section" id="image-section">
<h2><i class="fas fa-image"></i> 图像生成配置</h2>
<div class="config-item">
<label for="PAID_KEY">付费API密钥</label>
<input type="text" id="PAID_KEY" name="PAID_KEY" placeholder="AIzaSyxxxxxxxxxxxxxxxxxxx">
<small class="help-text">用于图像生成的付费API密钥</small>
</div>
<div class="config-item">
<label for="CREATE_IMAGE_MODEL">图像生成模型</label>
<input type="text" id="CREATE_IMAGE_MODEL" name="CREATE_IMAGE_MODEL" placeholder="imagen-3.0-generate-002">
<small class="help-text">用于图像生成的模型</small>
</div>
<div class="config-item">
<label for="UPLOAD_PROVIDER">上传提供商</label>
<select id="UPLOAD_PROVIDER" name="UPLOAD_PROVIDER">
<option value="smms" selected>SM.MS</option>
<option value="picgo">PicGo</option>
<option value="cloudflare">Cloudflare</option>
</select>
<small class="help-text">图片上传服务提供商</small>
</div>
<div class="config-item provider-config" data-provider="smms">
<label for="SMMS_SECRET_TOKEN">SM.MS密钥</label>
<input type="text" id="SMMS_SECRET_TOKEN" name="SMMS_SECRET_TOKEN" placeholder="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX">
<small class="help-text">SM.MS图床的密钥</small>
</div>
<div class="config-item provider-config" data-provider="picgo">
<label for="PICGO_API_KEY">PicGo API密钥</label>
<input type="text" id="PICGO_API_KEY" name="PICGO_API_KEY" placeholder="xxxx">
<small class="help-text">PicGo的API密钥</small>
</div>
<div class="config-item provider-config" data-provider="cloudflare">
<label for="CLOUDFLARE_IMGBED_URL">Cloudflare图床URL</label>
<input type="text" id="CLOUDFLARE_IMGBED_URL" name="CLOUDFLARE_IMGBED_URL" placeholder="https://xxxxxxx.pages.dev/upload">
<small class="help-text">Cloudflare图床的URL</small>
</div>
<div class="config-item provider-config" data-provider="cloudflare">
<label for="CLOUDFLARE_IMGBED_AUTH_CODE">Cloudflare认证码</label>
<input type="text" id="CLOUDFLARE_IMGBED_AUTH_CODE" name="CLOUDFLARE_IMGBED_AUTH_CODE" placeholder="xxxxxxxxx">
<small class="help-text">Cloudflare图床的认证码</small>
</div>
</div>
<!-- 流式输出优化器配置 -->
<div class="config-section" id="stream-section">
<h2><i class="fas fa-stream"></i> 流式输出优化器</h2>
<div class="config-item toggle">
<label for="STREAM_OPTIMIZER_ENABLED">启用流式输出优化</label>
<div class="toggle-switch">
<input type="checkbox" id="STREAM_OPTIMIZER_ENABLED" name="STREAM_OPTIMIZER_ENABLED">
<span class="toggle-slider"></span>
</div>
</div>
<div class="config-item">
<label for="STREAM_MIN_DELAY">最小延迟(秒)</label>
<input type="number" id="STREAM_MIN_DELAY" name="STREAM_MIN_DELAY" min="0" max="1" step="0.001">
<small class="help-text">流式输出的最小延迟时间</small>
</div>
<div class="config-item">
<label for="STREAM_MAX_DELAY">最大延迟(秒)</label>
<input type="number" id="STREAM_MAX_DELAY" name="STREAM_MAX_DELAY" min="0" max="1" step="0.001">
<small class="help-text">流式输出的最大延迟时间</small>
</div>
<div class="config-item">
<label for="STREAM_SHORT_TEXT_THRESHOLD">短文本阈值</label>
<input type="number" id="STREAM_SHORT_TEXT_THRESHOLD" name="STREAM_SHORT_TEXT_THRESHOLD" min="1" max="100">
<small class="help-text">短文本的字符阈值</small>
</div>
<div class="config-item">
<label for="STREAM_LONG_TEXT_THRESHOLD">长文本阈值</label>
<input type="number" id="STREAM_LONG_TEXT_THRESHOLD" name="STREAM_LONG_TEXT_THRESHOLD" min="1" max="1000">
<small class="help-text">长文本的字符阈值</small>
</div>
<div class="config-item">
<label for="STREAM_CHUNK_SIZE">分块大小</label>
<input type="number" id="STREAM_CHUNK_SIZE" name="STREAM_CHUNK_SIZE" min="1" max="100">
<small class="help-text">流式输出的分块大小</small>
</div>
</div>
<div class="form-actions">
<button type="button" id="saveBtn" class="save-btn">
<i class="fas fa-save"></i> 保存配置
</button>
<button type="button" id="resetBtn" class="reset-btn">
<i class="fas fa-undo"></i> 重置配置
</button>
</div>
</form>
</form>
</div>
</div>
<!-- Scroll buttons are now in base.html -->
<div class="scroll-buttons">
<button class="scroll-btn" onclick="scrollToTop()" title="回到顶部">
<i class="fas fa-chevron-up"></i>
</button>
<button class="scroll-btn" onclick="scrollToBottom()" title="滚动到底部">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<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 -->
<div id="notification" class="notification"></div>
<div class="copyright">
© <script>document.write(new Date().getFullYear())</script> by <a href="https://linux.do/u/snaily" target="_blank"><img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily">snaily</a> |
<a href="https://github.com/snailyp/gemini-balance" target="_blank"><i class="fab fa-github"></i> GitHub</a>
</div>
<!-- API Key Add Modal (Moved outside container) -->
<!-- Footer is now in base.html -->
<!-- API Key Add Modal -->
<div id="apiKeyModal" class="modal">
<div class="modal-content">
<span class="close-btn" id="closeApiKeyModalBtn">&times;</span>
<h2>批量添加 API 密钥</h2>
<p>每行粘贴一个或多个密钥,将自动提取有效密钥并去重。</p>
<textarea id="apiKeyBulkInput" rows="10" placeholder="在此处粘贴 API 密钥..."></textarea>
<div class="modal-actions">
<button type="button" id="confirmAddApiKeyBtn" class="save-btn">确认添加</button>
<button type="button" id="cancelAddApiKeyBtn" class="reset-btn">取消</button>
</div>
</div>
</div>
<!-- Reset Confirmation Modal (Moved outside container) -->
<div id="resetConfirmModal" class="modal">
<div class="modal-content">
<span class="close-btn" id="closeResetModalBtn">&times;</span>
<h2>确认重置配置</h2>
<p>确定要重置所有配置吗?<br>这将恢复到默认值,此操作不可撤销。</p>
<div class="modal-actions">
<button type="button" id="confirmResetBtn" class="reset-btn">确认重置</button>
<button type="button" id="cancelResetBtn" class="save-btn">取消</button> <!-- Using save-btn style for cancel -->
<div class="w-full max-w-lg mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800">批量添加 API 密钥</h2>
<button id="closeApiKeyModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
<p class="text-gray-600 mb-4">每行粘贴一个或多个密钥,将自动提取有效密钥并去重。</p>
<textarea id="apiKeyBulkInput" rows="10" placeholder="在此处粘贴 API 密钥..." class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 font-mono text-sm"></textarea>
<div class="flex justify-end gap-3 mt-6">
<button type="button" id="confirmAddApiKeyBtn" class="bg-primary-600 hover:bg-primary-700 text-white px-6 py-2 rounded-lg font-medium transition">确认添加</button>
<button type="button" id="cancelAddApiKeyBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-6 py-2 rounded-lg font-medium transition">取消</button>
</div>
</div>
</div>
</div>
<!-- Reset Confirmation Modal -->
<div id="resetConfirmModal" class="modal">
<div class="w-full max-w-md mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-gray-800">确认重置配置</h2>
<button id="closeResetModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
<p class="text-gray-600 mb-6">确定要重置所有配置吗?<br>这将恢复到默认值,此操作不可撤销。</p>
<div class="flex justify-end gap-3">
<button type="button" id="confirmResetBtn" class="bg-red-500 hover:bg-red-600 text-white px-6 py-2 rounded-lg font-medium transition">确认重置</button>
<button type="button" id="cancelResetBtn" class="bg-primary-600 hover:bg-primary-700 text-white px-6 py-2 rounded-lg font-medium transition">取消</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block body_scripts %}
<script src="/static/js/config_editor.js"></script>
</body>
</html>
<!-- Add any other page-specific JS initialization here if needed -->
{% endblock %}

View File

@@ -1,165 +1,242 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>错误日志管理</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Use config_editor.css for base styles -->
<link rel="stylesheet" href="{{ url_for('static', path='/css/config_editor.css') }}">
<!-- Keep error_logs.css for specific styles -->
<link rel="stylesheet" href="{{ url_for('static', path='/css/error_logs.css') }}">
</head>
<body>
<div class="container">
<button class="refresh-btn" onclick="refreshPage(this)">
<i class="fas fa-sync-alt"></i>
</button>
<h1>Gemini Balance</h1>
<div class="nav-tabs">
<a href="/config" class="tab-link">
<i class="fas fa-cog"></i> 配置编辑
</a>
<a href="/keys" class="tab-link">
<i class="fas fa-key"></i> 密钥管理
</a>
<a href="/logs" class="tab-link active">
<i class="fas fa-exclamation-triangle"></i> 错误日志
</a>
</div>
{% extends "base.html" %}
<div class="config-section active"> <!-- Use config-section for consistent layout -->
<h2><i class="fas fa-bug"></i> 错误日志列表</h2>
{% block title %}错误日志管理 - Gemini Balance{% endblock %}
<div class="controls-container"> <!-- New container for controls -->
<div class="page-size-selector">
<label for="pageSize">每页显示:</label>
<select id="pageSize">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span></span>
{% block head_extra_styles %}
<style>
/* error_logs.html specific styles */
/* Table styles */
.styled-table th {
position: sticky;
top: 0;
background: #f3f4f6; /* bg-gray-100 */
z-index: 10;
}
.styled-table tbody tr:hover {
background-color: #f9fafb; /* bg-gray-50 */
}
.styled-table td {
padding: 12px 20px;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
}
/* Ensure error log column does not wrap and remove max-width */
.styled-table td:nth-child(4) { /* Assuming error log is the 4th column */
/* max-width: none; */
white-space: nowrap;
}
.btn-view-details {
background-color: #eef2ff; /* primary-50 */
color: #4f46e5; /* primary-600 */
padding: 6px 12px;
border-radius: 6px;
font-weight: 500;
transition: all 0.2s ease-in-out;
border: 1px solid #c7d2fe; /* primary-200 */
}
.btn-view-details:hover {
background-color: #c7d2fe; /* primary-200 */
color: #4338ca; /* primary-700 */
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
@media (max-width: 768px) {
.search-container {
grid-template-columns: 1fr;
}
}
/* Modal styles are in base.html */
</style>
{% endblock %}
{% block content %}
<div class="container mx-auto px-4"> <!-- Removed max-width-7xl for wider content -->
<div class="glass-card rounded-2xl shadow-xl p-6 md:p-8">
<!-- Removed refresh button from top right -->
<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-white bg-opacity-50 hover:bg-opacity-70 text-gray-700 transition-all duration-200">
<i class="fas fa-key"></i> 密钥状态
</a>
<a href="/logs" 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-exclamation-triangle"></i> 错误日志
</a>
</div>
<!-- 主内容区域 -->
<div class="bg-white bg-opacity-70 rounded-xl p-6 shadow-lg animate-fade-in">
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
<i class="fas fa-bug text-primary-600"></i> 错误日志列表
</h2>
<!-- 控制区域 (Refresh button removed, page size moved below) -->
<!-- Removed the original controls div -->
<!-- 搜索控件 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 mb-6">
<input type="text" id="keySearch" placeholder="搜索密钥 (部分)" class="px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 col-span-1 lg:col-span-1">
<input type="text" id="errorSearch" placeholder="搜索错误类型/日志" class="px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 col-span-1 lg:col-span-1">
<div class="flex items-center gap-2 col-span-1 lg:col-span-2">
<input type="datetime-local" id="startDate" class="px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 flex-1 text-sm">
<span class="text-gray-700"></span>
<input type="datetime-local" id="endDate" class="px-4 py-3 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 flex-1 text-sm">
</div>
<button id="searchBtn" class="flex items-center justify-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-4 py-3 rounded-lg font-medium transition-all duration-200 col-span-1">
<i class="fas fa-search"></i> 搜索
</button>
</div>
<!-- 表格容器 - Enhanced Styling -->
<div class="overflow-x-auto rounded-lg border border-gray-200 mb-6 bg-white"> <!-- Removed shadow, added border -->
<table class="styled-table w-full min-w-full text-sm"> <!-- Added text-sm -->
<thead>
<tr class="bg-primary-50 text-left text-primary-800"> <!-- Changed header background and text color -->
<th class="px-5 py-3 font-semibold rounded-tl-lg">ID</th> <!-- Increased padding, adjusted rounding -->
<th class="px-5 py-3 font-semibold">Gemini密钥</th>
<th class="px-5 py-3 font-semibold">错误类型</th>
<th class="px-5 py-3 font-semibold">错误日志</th>
<th class="px-5 py-3 font-semibold">模型名称</th>
<th class="px-5 py-3 font-semibold">请求时间</th>
<th class="px-5 py-3 font-semibold rounded-tr-lg">操作</th> <!-- Adjusted rounding -->
</tr>
</thead>
<tbody id="errorLogsTable" class="divide-y divide-gray-200">
<!-- 错误日志数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
<!-- 状态指示器 -->
<div id="loadingIndicator" class="flex items-center justify-center p-8 hidden">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
<p class="ml-4 text-lg text-gray-700 font-medium">加载中,请稍候...</p>
</div>
<div id="noDataMessage" class="text-center py-12 text-gray-500 hidden">
<i class="fas fa-inbox text-5xl mb-3"></i>
<p class="text-lg">暂无错误日志数据</p>
</div>
<div id="errorMessage" class="bg-danger-50 text-danger-600 p-4 rounded-lg font-medium text-center hidden">
<i class="fas fa-exclamation-circle mr-2"></i>
加载错误日志失败,请稍后重试。
</div>
<!-- 分页与每页显示控件 -->
<div class="flex flex-col sm:flex-row justify-between items-center mt-6 gap-4">
<!-- 每页显示控件 (Moved here) -->
<div class="flex items-center gap-2 text-sm text-gray-700">
<label for="pageSize" class="font-medium">每页显示:</label>
<select id="pageSize" class="rounded-md border border-gray-300 focus:ring focus:ring-primary-200 focus:border-primary-500 px-2 py-1 bg-white text-sm">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span></span>
</div>
<!-- 分页控件 -->
<div class="flex items-center gap-4"> <!-- Wrapper for pagination and input -->
<ul class="pagination flex items-center gap-1" id="pagination">
<!-- 分页控件将通过JavaScript动态加载 -->
</ul>
<!-- 页码输入跳转 -->
<div class="flex items-center gap-1">
<input type="number" id="pageInput" min="1" class="w-16 px-2 py-1 rounded-md border border-gray-300 text-sm focus:ring focus:ring-primary-200 focus:border-primary-500" placeholder="页码">
<button id="goToPageBtn" class="px-3 py-1 bg-primary-600 hover:bg-primary-700 text-white text-sm rounded-md transition">跳转</button>
</div>
</div>
</div>
<button id="refreshBtn" class="action-btn"> <!-- Use a consistent button class -->
<i class="fas fa-sync-alt"></i> 刷新
</button>
</div>
<!-- Search Controls -->
<div class="search-container">
<input type="text" id="keySearch" placeholder="搜索密钥 (部分)">
<input type="text" id="errorSearch" placeholder="搜索错误类型/日志"> <!-- Changed ID -->
<input type="date" id="startDate">
<span></span>
<input type="date" id="endDate">
<button id="searchBtn" class="action-btn">
<i class="fas fa-search"></i> 搜索
</button>
</div>
<div class="table-container"> <!-- New container for table -->
<table class="styled-table"> <!-- Use a custom table class -->
<thead>
<tr>
<th>ID</th>
<th>Gemini密钥</th>
<th>错误类型</th>
<th>错误日志</th>
<th>模型名称</th>
<th>请求时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="errorLogsTable">
<!-- 错误日志数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
<div id="loadingIndicator" class="status-indicator loading"> <!-- Custom loading indicator -->
<div class="spinner"></div>
<p>加载中,请稍候...</p>
</div>
<div id="noDataMessage" class="status-indicator no-data"> <!-- Custom no-data message -->
<p>暂无错误日志数据</p>
</div>
<div id="errorMessage" class="status-indicator error"> <!-- Custom error message -->
<p>加载错误日志失败,请稍后重试。</p>
</div>
<div class="pagination-container"> <!-- Custom pagination container -->
<ul class="pagination" id="pagination">
<!-- 分页控件将通过JavaScript动态加载 -->
</ul>
</div>
</div>
</div>
<!-- Scroll buttons are now in base.html -->
<div class="scroll-buttons">
<button class="scroll-btn" onclick="scrollToTop()" title="回到顶部">
<i class="fas fa-chevron-up"></i>
</button>
<button class="scroll-btn" onclick="scrollToBottom()" title="滚动到底部">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="copyStatus" class="notification"></div> <!-- Use notification class -->
<div class="copyright">
© <script>document.write(new Date().getFullYear())</script> by <a href="https://linux.do/u/snaily" target="_blank"><img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily">snaily</a> |
<a href="https://github.com/snailyp/gemini-balance" target="_blank"><i class="fab fa-github"></i> GitHub</a>
</div>
<!-- Custom Modal for Log Details -->
<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>
<!-- Footer is now in base.html -->
<!-- 日志详情模态框 -->
<div id="logDetailModal" class="modal">
<div class="modal-content">
<span class="close-btn" id="closeLogDetailModalBtn">&times;</span>
<h2>错误日志详情</h2>
<div class="modal-body-content"> <!-- Added wrapper for consistent padding/styling -->
<div class="detail-item">
<h6>Gemini密钥:</h6>
<pre id="modalGeminiKey"></pre>
<div class="w-full max-w-6xl mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in"> <!-- Increased max-width to 6xl -->
<div class="p-6">
<div class="flex justify-between items-center border-b border-gray-200 pb-4 mb-4">
<h2 class="text-xl font-bold text-gray-800">错误日志详情</h2>
<button id="closeLogDetailModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">&times;</button>
</div>
<div class="detail-item">
<h6>错误类型:</h6>
<p id="modalErrorType"></p>
<div class="space-y-4 max-h-[60vh] overflow-y-auto p-1">
<div class="bg-gray-50 p-4 rounded-lg">
<h6 class="text-sm font-semibold text-gray-600 mb-1">Gemini密钥:</h6>
<pre id="modalGeminiKey" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto"></pre>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h6 class="text-sm font-semibold text-gray-600 mb-1">错误类型:</h6>
<p id="modalErrorType" class="text-danger-600 font-medium"></p>
</div>
<div class="bg-gray-50 p-4 rounded-lg relative group"> <!-- Added relative and group -->
<h6 class="text-sm font-semibold text-gray-600 mb-1">错误日志:</h6>
<pre id="modalErrorLog" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto whitespace-pre-wrap"></pre>
<button class="copy-btn absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 text-gray-600 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity" data-target="modalErrorLog" title="复制错误日志">
<i class="far fa-copy"></i>
</button>
</div>
<div class="bg-gray-50 p-4 rounded-lg relative group"> <!-- Added relative and group -->
<h6 class="text-sm font-semibold text-gray-600 mb-1">请求消息:</h6>
<pre id="modalRequestMsg" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto whitespace-pre-wrap"></pre>
<button class="copy-btn absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 text-gray-600 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity" data-target="modalRequestMsg" title="复制请求消息">
<i class="far fa-copy"></i>
</button>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h6 class="text-sm font-semibold text-gray-600 mb-1">模型名称:</h6>
<p id="modalModelName" class="font-medium"></p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h6 class="text-sm font-semibold text-gray-600 mb-1">请求时间:</h6>
<p id="modalRequestTime" class="font-medium"></p>
</div>
</div>
<div class="detail-item">
<h6>错误日志:</h6>
<pre id="modalErrorLog"></pre>
<div class="flex justify-end mt-6">
<button type="button" id="closeModalFooterBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-lg font-medium transition">关闭</button>
</div>
<div class="detail-item">
<h6>请求消息:</h6>
<pre id="modalRequestMsg"></pre>
</div>
<div class="detail-item">
<h6>模型名称:</h6>
<p id="modalModelName"></p>
</div>
<div class="detail-item">
<h6>请求时间:</h6>
<p id="modalRequestTime"></p>
</div>
</div>
<div class="modal-actions">
<button type="button" id="closeModalFooterBtn" class="reset-btn">关闭</button> <!-- Use consistent button style -->
</div>
</div>
</div>
{% endblock %}
<!-- Keep custom JS, remove Bootstrap JS -->
{% block body_scripts %}
<script src="{{ url_for('static', path='/js/error_logs.js') }}"></script>
</body>
</html>
<script>
// error_logs.html specific JS initialization (if any)
// e.g., initialize date pickers or other elements if needed
// The main logic is in error_logs.js
</script>
{% endblock %}

View File

@@ -1,145 +1,293 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API密钥状态</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/keys_status.css">
</head>
<body>
<div class="container">
<button class="refresh-btn" onclick="refreshPage(this)">
<i class="fas fa-sync-alt"></i>
</button>
<h1>Gemini Balance</h1>
<div class="nav-tabs">
<a href="/config" class="tab-link">
<i class="fas fa-cog"></i> 配置编辑
</a>
<a href="/keys" class="tab-link active">
<i class="fas fa-key"></i> 密钥管理
</a>
<a href="/logs" class="tab-link">
<i class="fas fa-exclamation-triangle"></i> 错误日志
</a>
</div>
<div class="key-list">
<h2 onclick="toggleSection(this, 'validKeys')">
<span>
<i class="fas fa-chevron-down toggle-icon"></i>
<i class="fas fa-check-circle" style="color: #27ae60;"></i>
有效密钥
</span>
<button class="copy-btn" onclick="event.stopPropagation(); copyKeys('valid')">
<i class="fas fa-copy"></i>
批量复制
</button>
</h2>
<div class="key-content">
<ul id="validKeys">
{% for key, fail_count in valid_keys.items() %}
<li>
<div class="key-info">
<span class="status-badge status-valid">
<i class="fas fa-check"></i> 有效
</span>
<span class="key-text" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
<button class="toggle-vis-btn" onclick="toggleKeyVisibility(this)">
<i class="fas fa-eye"></i>
</button>
<span class="fail-count">
<i class="fas fa-exclamation-triangle"></i>
失败: {{ fail_count }}
</span>
{% 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 */
</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-4xl mx-auto px-4">
<div class="glass-card rounded-2xl shadow-xl p-6 md:p-8">
<button class="absolute top-6 right-6 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)">
<i class="fas fa-sync-alt"></i>
</button>
<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-key"></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="bg-white bg-opacity-70 rounded-xl shadow-md overflow-hidden mb-6 animate-fade-in">
<div class="flex justify-between items-center p-4 bg-white bg-opacity-80 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 text-xl"></i>
<h2 class="text-lg font-semibold">有效密钥</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-gray-300 rounded focus:ring-primary-500 focus:border-primary-500">
</div>
<div class="key-actions">
<button class="verify-btn" onclick="verifyKey('{{ key }}', this)">
<i class="fas fa-check-circle"></i>
验证
</button>
<button class="copy-btn" onclick="copyKey('{{ key }}')">
<i class="fas fa-copy"></i>
复制
</button>
</div>
</li>
{% endfor %}
</ul>
</div>
<div class="flex gap-2">
<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="space-y-3">
{% for key, fail_count in valid_keys.items() %}
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-100" data-fail-count="{{ fail_count }}">
<div class="flex flex-col sm:flex-row sm:items-center justify-between 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 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 %}
</ul>
</div>
</div>
<!-- 无效密钥区域 -->
<div class="bg-white bg-opacity-70 rounded-xl shadow-md overflow-hidden mb-6 animate-fade-in" style="animation-delay: 0.2s">
<div class="flex justify-between items-center p-4 bg-white bg-opacity-80 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 text-xl"></i>
<h2 class="text-lg font-semibold">无效密钥</h2>
</div>
<div class="flex gap-2">
<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="space-y-3">
{% for key, fail_count in invalid_keys.items() %}
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-100">
<div class="flex flex-col sm:flex-row sm:items-center justify-between 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 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 %}
</ul>
</div>
</div>
<!-- 总密钥数显示 -->
<div class="bg-white bg-opacity-70 rounded-xl shadow-md p-4 text-center animate-fade-in" style="animation-delay: 0.4s">
<div class="flex items-center justify-center gap-2 text-primary-700 font-semibold text-lg">
<i class="fas fa-key"></i> 总密钥数:{{ total }}
</div>
</div>
</div>
<div class="key-list">
<h2 onclick="toggleSection(this, 'invalidKeys')">
<span>
<i class="fas fa-chevron-down toggle-icon"></i>
<i class="fas fa-times-circle" style="color: #e74c3c;"></i>
无效密钥
</span>
<button class="copy-btn" onclick="event.stopPropagation(); copyKeys('invalid')">
<i class="fas fa-copy"></i>
批量复制
</button>
</h2>
<div class="key-content">
<ul id="invalidKeys">
{% for key, fail_count in invalid_keys.items() %}
<li>
<div class="key-info">
<span class="status-badge status-invalid">
<i class="fas fa-times"></i> 无效
</span>
<span class="key-text" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
<button class="toggle-vis-btn" onclick="toggleKeyVisibility(this)">
<i class="fas fa-eye"></i>
</button>
<span class="fail-count">
<i class="fas fa-exclamation-triangle"></i>
失败: {{ fail_count }}
</span>
</div>
<div class="key-actions">
<button class="verify-btn" onclick="verifyKey('{{ key }}', this)">
<i class="fas fa-check-circle"></i>
验证
</button>
<button class="copy-btn" onclick="copyKey('{{ key }}')">
<i class="fas fa-copy"></i>
复制
</button>
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="total">
<i class="fas fa-key"></i> 总密钥数:{{ total }}
</div>
</div>
<div class="scroll-buttons">
<button class="scroll-btn" onclick="scrollToTop()" title="回到顶部">
<i class="fas fa-chevron-up"></i>
</button>
<button class="scroll-btn" onclick="scrollToBottom()" title="滚动到底部">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="copyStatus"></div>
<div class="copyright">
© <script>document.write(new Date().getFullYear())</script> by <a href="https://linux.do/u/snaily" target="_blank"><img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily">snaily</a> |
<a href="https://github.com/snailyp/gemini-balance" target="_blank"><i class="fab fa-github"></i> GitHub</a>
</div>
<script src="/static/js/keys_status.js"></script>
</body>
</html>
<!-- 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>
<!-- 操作结果模态框 -->
<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-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="resultModalTitle">操作结果</h3>
<button onclick="closeResultModal()" class="text-gray-500 hover:text-gray-700 focus:outline-none">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mb-6 text-center">
<div id="resultIcon" class="text-5xl mb-3"></div>
<p class="text-gray-600" id="resultModalMessage"></p>
</div>
<div class="flex justify-center">
<button id="resultModalConfirmBtn" onclick="closeResultModal()" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg transition-colors">
确定
</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');
keys.forEach(keyItem => {
const failCount = parseInt(keyItem.getAttribute('data-fail-count'), 10);
if (failCount >= threshold) {
keyItem.style.display = ''; // Show item
} else {
keyItem.style.display = 'none'; // Hide item
}
});
}
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 %}

View File

@@ -15,3 +15,4 @@ sqlalchemy
aiomysql
databases
python-dotenv
apscheduler # 添加定时任务库