mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-07-06 07:11:30 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb40848c04 | ||
|
|
7098c8755f | ||
|
|
705d602dee | ||
|
|
cd257a9406 | ||
|
|
cd54650431 | ||
|
|
a5602c602e | ||
|
|
dd70fd4c44 | ||
|
|
dbe50628b3 |
@@ -11,6 +11,7 @@ from app.exception.exceptions import setup_exception_handlers
|
|||||||
from app.router.routes import setup_routers
|
from app.router.routes import setup_routers
|
||||||
from app.service.key.key_manager import get_key_manager_instance
|
from app.service.key.key_manager import get_key_manager_instance
|
||||||
from app.database.connection import connect_to_db, disconnect_from_db
|
from app.database.connection import connect_to_db, disconnect_from_db
|
||||||
|
from app.utils.helpers import get_current_version # Import from helpers
|
||||||
from app.database.initialization import initialize_database
|
from app.database.initialization import initialize_database
|
||||||
from app.scheduler.key_checker import start_scheduler, stop_scheduler
|
from app.scheduler.key_checker import start_scheduler, stop_scheduler
|
||||||
from app.service.update.update_service import check_for_updates
|
from app.service.update.update_service import check_for_updates
|
||||||
@@ -20,28 +21,11 @@ logger = get_application_logger()
|
|||||||
# Define project paths using pathlib
|
# Define project paths using pathlib
|
||||||
# Assuming this file is at app/core/application.py
|
# Assuming this file is at app/core/application.py
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
VERSION_FILE_PATH = PROJECT_ROOT / "VERSION"
|
# VERSION_FILE_PATH = PROJECT_ROOT / "VERSION" # Removed: Defined in helpers.py
|
||||||
STATIC_DIR = PROJECT_ROOT / "app" / "static"
|
STATIC_DIR = PROJECT_ROOT / "app" / "static"
|
||||||
TEMPLATES_DIR = PROJECT_ROOT / "app" / "templates"
|
TEMPLATES_DIR = PROJECT_ROOT / "app" / "templates"
|
||||||
|
|
||||||
|
# Removed _get_current_version function definition, moved to helpers.py
|
||||||
def _get_current_version(default_version: str = "0.0.0") -> str:
|
|
||||||
"""Reads the current version from the VERSION file."""
|
|
||||||
version_file = VERSION_FILE_PATH # Use Path object
|
|
||||||
try:
|
|
||||||
# Use Path object's open method
|
|
||||||
with version_file.open('r', encoding='utf-8') as f:
|
|
||||||
version = f.read().strip()
|
|
||||||
if not version:
|
|
||||||
logger.warning(f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'.")
|
|
||||||
return default_version
|
|
||||||
return version
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.warning(f"VERSION file not found at '{version_file}'. Using default version '{default_version}'.")
|
|
||||||
return default_version
|
|
||||||
except IOError as e:
|
|
||||||
logger.error(f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'.")
|
|
||||||
return default_version
|
|
||||||
|
|
||||||
# 初始化模板引擎,并添加全局变量
|
# 初始化模板引擎,并添加全局变量
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
@@ -70,7 +54,6 @@ async def _setup_database_and_config(app_settings):
|
|||||||
async def _shutdown_database():
|
async def _shutdown_database():
|
||||||
"""Disconnects from the database."""
|
"""Disconnects from the database."""
|
||||||
await disconnect_from_db()
|
await disconnect_from_db()
|
||||||
logger.info("Disconnected from database.")
|
|
||||||
|
|
||||||
def _start_scheduler():
|
def _start_scheduler():
|
||||||
"""Starts the background scheduler."""
|
"""Starts the background scheduler."""
|
||||||
@@ -83,12 +66,11 @@ def _start_scheduler():
|
|||||||
def _stop_scheduler():
|
def _stop_scheduler():
|
||||||
"""Stops the background scheduler."""
|
"""Stops the background scheduler."""
|
||||||
stop_scheduler()
|
stop_scheduler()
|
||||||
logger.info("Scheduler stopped.")
|
|
||||||
|
|
||||||
async def _perform_update_check(app: FastAPI):
|
async def _perform_update_check(app: FastAPI):
|
||||||
"""Checks for updates and stores the info in app.state."""
|
"""Checks for updates and stores the info in app.state."""
|
||||||
update_available, latest_version, error_message = await check_for_updates()
|
update_available, latest_version, error_message = await check_for_updates()
|
||||||
current_version = _get_current_version() # Read from VERSION file
|
current_version = get_current_version() # Use imported function
|
||||||
update_info = {
|
update_info = {
|
||||||
"update_available": update_available,
|
"update_available": update_available,
|
||||||
"latest_version": latest_version,
|
"latest_version": latest_version,
|
||||||
@@ -119,7 +101,7 @@ async def lifespan(app: FastAPI):
|
|||||||
await _setup_database_and_config(settings) # Pass settings object
|
await _setup_database_and_config(settings) # Pass settings object
|
||||||
|
|
||||||
# Perform update check after core components are ready
|
# Perform update check after core components are ready
|
||||||
await _perform_update_check(app)
|
# await _perform_update_check(app) # Removed: Version check moved to frontend API call
|
||||||
|
|
||||||
# Start the scheduler
|
# Start the scheduler
|
||||||
_start_scheduler()
|
_start_scheduler()
|
||||||
@@ -148,7 +130,7 @@ def create_app() -> FastAPI:
|
|||||||
|
|
||||||
# 创建FastAPI应用
|
# 创建FastAPI应用
|
||||||
# Read version from file for consistency
|
# Read version from file for consistency
|
||||||
current_version = _get_current_version()
|
current_version = get_current_version() # Use imported function
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Gemini Balance API",
|
title="Gemini Balance API",
|
||||||
description="Gemini API代理服务,支持负载均衡和密钥管理",
|
description="Gemini API代理服务,支持负载均衡和密钥管理",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
from databases import Database
|
from databases import Database
|
||||||
from sqlalchemy import create_engine, MetaData
|
from sqlalchemy import create_engine, MetaData
|
||||||
|
# from sqlalchemy.orm import sessionmaker # 不再需要
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from app.config.config import settings
|
from app.config.config import settings
|
||||||
@@ -31,7 +32,9 @@ Base = declarative_base(metadata=metadata)
|
|||||||
# databases 库会自动处理连接失效后的重连尝试。
|
# databases 库会自动处理连接失效后的重连尝试。
|
||||||
database = Database(DATABASE_URL, min_size=5, max_size=20, pool_recycle=1800) # Reduced recycle time to 30 mins
|
database = Database(DATABASE_URL, min_size=5, max_size=20, pool_recycle=1800) # Reduced recycle time to 30 mins
|
||||||
|
|
||||||
|
# 移除了 SessionLocal 和 get_db 函数
|
||||||
|
|
||||||
|
# --- Async connection functions for lifespan/async routes ---
|
||||||
async def connect_to_db():
|
async def connect_to_db():
|
||||||
"""
|
"""
|
||||||
连接到数据库
|
连接到数据库
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
数据库服务模块
|
数据库服务模块
|
||||||
"""
|
"""
|
||||||
|
from typing import List, Optional, Dict, Any, Union
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import func, desc, asc, select, insert, update, delete
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List, Optional, Any, Union
|
|
||||||
from datetime import datetime # Keep this import
|
|
||||||
|
|
||||||
from sqlalchemy import select, insert, update, func
|
|
||||||
|
|
||||||
from app.database.connection import database
|
from app.database.connection import database
|
||||||
from app.database.models import Settings, ErrorLog, RequestLog # Import RequestLog
|
from app.database.models import Settings, ErrorLog, RequestLog
|
||||||
from app.log.logger import get_database_logger
|
from app.log.logger import get_database_logger
|
||||||
|
|
||||||
logger = get_database_logger()
|
logger = get_database_logger()
|
||||||
@@ -157,19 +155,25 @@ async def get_error_logs(
|
|||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
key_search: Optional[str] = None,
|
key_search: Optional[str] = None,
|
||||||
error_search: Optional[str] = None,
|
error_search: Optional[str] = None,
|
||||||
|
error_code_search: Optional[str] = None,
|
||||||
start_date: Optional[datetime] = None,
|
start_date: Optional[datetime] = None,
|
||||||
end_date: Optional[datetime] = None
|
end_date: Optional[datetime] = None,
|
||||||
|
sort_by: str = 'id', # 新增排序字段
|
||||||
|
sort_order: str = 'desc' # 新增排序顺序 ('asc' or 'desc')
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
获取错误日志,支持搜索和日期过滤
|
获取错误日志,支持搜索、日期过滤和排序
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
limit (int): 限制数量
|
limit (int): 限制数量
|
||||||
offset (int): 偏移量
|
offset (int): 偏移量
|
||||||
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
|
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
|
||||||
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
|
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
|
||||||
|
error_code_search (Optional[str]): 错误码搜索词 (精确匹配)
|
||||||
start_date (Optional[datetime]): 开始日期时间
|
start_date (Optional[datetime]): 开始日期时间
|
||||||
end_date (Optional[datetime]): 结束日期时间
|
end_date (Optional[datetime]): 结束日期时间
|
||||||
|
sort_by (str): 排序字段 (例如 'id', 'request_time')
|
||||||
|
sort_order (str): 排序顺序 ('asc' or 'desc')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict[str, Any]]: 错误日志列表
|
List[Dict[str, Any]]: 错误日志列表
|
||||||
@@ -198,10 +202,28 @@ async def get_error_logs(
|
|||||||
if end_date:
|
if end_date:
|
||||||
# Use the datetime object directly for comparison
|
# Use the datetime object directly for comparison
|
||||||
query = query.where(ErrorLog.request_time < end_date)
|
query = query.where(ErrorLog.request_time < end_date)
|
||||||
|
if error_code_search:
|
||||||
|
try:
|
||||||
|
# Attempt to convert search string to integer for exact match
|
||||||
|
error_code_int = int(error_code_search)
|
||||||
|
query = query.where(ErrorLog.error_code == error_code_int)
|
||||||
|
except ValueError:
|
||||||
|
# If conversion fails, log a warning and potentially skip this filter
|
||||||
|
# or handle as needed (e.g., return no results for invalid code format)
|
||||||
|
logger.warning(f"Invalid format for error_code_search: '{error_code_search}'. Expected an integer. Skipping error code filter.")
|
||||||
|
# Optionally, force no results if the format is invalid:
|
||||||
|
# query = query.where(False) # This ensures no rows are returned
|
||||||
|
|
||||||
|
# 添加排序逻辑
|
||||||
|
sort_column = getattr(ErrorLog, sort_by, ErrorLog.id) # 获取排序字段,默认为 id
|
||||||
|
if sort_order.lower() == 'asc':
|
||||||
|
query = query.order_by(asc(sort_column))
|
||||||
|
else:
|
||||||
|
query = query.order_by(desc(sort_column))
|
||||||
|
|
||||||
|
# Apply limit and offset
|
||||||
|
query = query.limit(limit).offset(offset)
|
||||||
|
|
||||||
# Apply ordering, limit, and offset
|
|
||||||
query = query.order_by(ErrorLog.id.desc()).limit(limit).offset(offset)
|
|
||||||
|
|
||||||
result = await database.fetch_all(query)
|
result = await database.fetch_all(query)
|
||||||
return [dict(row) for row in result]
|
return [dict(row) for row in result]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -212,6 +234,7 @@ async def get_error_logs(
|
|||||||
async def get_error_logs_count(
|
async def get_error_logs_count(
|
||||||
key_search: Optional[str] = None,
|
key_search: Optional[str] = None,
|
||||||
error_search: Optional[str] = None,
|
error_search: Optional[str] = None,
|
||||||
|
error_code_search: Optional[str] = None, # Added error code search
|
||||||
start_date: Optional[datetime] = None,
|
start_date: Optional[datetime] = None,
|
||||||
end_date: Optional[datetime] = None
|
end_date: Optional[datetime] = None
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -221,6 +244,7 @@ async def get_error_logs_count(
|
|||||||
Args:
|
Args:
|
||||||
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
|
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
|
||||||
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
|
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
|
||||||
|
error_code_search (Optional[str]): 错误码搜索词 (精确匹配)
|
||||||
start_date (Optional[datetime]): 开始日期时间
|
start_date (Optional[datetime]): 开始日期时间
|
||||||
end_date (Optional[datetime]): 结束日期时间
|
end_date (Optional[datetime]): 结束日期时间
|
||||||
|
|
||||||
@@ -243,6 +267,16 @@ async def get_error_logs_count(
|
|||||||
if end_date:
|
if end_date:
|
||||||
# Use the datetime object directly for comparison
|
# Use the datetime object directly for comparison
|
||||||
query = query.where(ErrorLog.request_time < end_date)
|
query = query.where(ErrorLog.request_time < end_date)
|
||||||
|
if error_code_search:
|
||||||
|
try:
|
||||||
|
# Attempt to convert search string to integer for exact match
|
||||||
|
error_code_int = int(error_code_search)
|
||||||
|
query = query.where(ErrorLog.error_code == error_code_int)
|
||||||
|
except ValueError:
|
||||||
|
# If conversion fails, log a warning and potentially skip this filter
|
||||||
|
logger.warning(f"Invalid format for error_code_search in count: '{error_code_search}'. Expected an integer. Skipping error code filter.")
|
||||||
|
# Optionally, force count to 0 if the format is invalid:
|
||||||
|
# return 0 # Or query = query.where(False) before fetching
|
||||||
|
|
||||||
count_result = await database.fetch_one(query)
|
count_result = await database.fetch_one(query)
|
||||||
return count_result[0] if count_result else 0
|
return count_result[0] if count_result else 0
|
||||||
@@ -281,6 +315,68 @@ async def get_error_log_details(log_id: int) -> Optional[Dict[str, Any]]:
|
|||||||
logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}")
|
logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# --- 异步删除函数 (使用 databases 库) ---
|
||||||
|
|
||||||
|
async def delete_error_logs_by_ids(log_ids: List[int]) -> int:
|
||||||
|
"""
|
||||||
|
根据提供的 ID 列表批量删除错误日志 (异步)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_ids: 要删除的错误日志 ID 列表。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: 实际删除的日志数量。
|
||||||
|
"""
|
||||||
|
if not log_ids:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
# 使用 databases 执行删除
|
||||||
|
query = delete(ErrorLog).where(ErrorLog.id.in_(log_ids))
|
||||||
|
# execute 返回受影响的行数,但 databases 库的 execute 不直接返回 rowcount
|
||||||
|
# 我们需要先查询是否存在,或者依赖数据库约束/触发器(如果适用)
|
||||||
|
# 或者,我们可以执行删除并假设成功,除非抛出异常
|
||||||
|
# 为了简单起见,我们执行删除并记录日志,不精确返回删除数量
|
||||||
|
# 如果需要精确数量,需要先执行 SELECT COUNT(*)
|
||||||
|
await database.execute(query)
|
||||||
|
# 注意:databases 的 execute 不返回 rowcount,所以我们不能直接返回删除的数量
|
||||||
|
# 返回 log_ids 的长度作为尝试删除的数量,或者返回 0/1 表示操作尝试
|
||||||
|
logger.info(f"Attempted bulk deletion for error logs with IDs: {log_ids}")
|
||||||
|
return len(log_ids) # 返回尝试删除的数量
|
||||||
|
except Exception as e:
|
||||||
|
# 数据库连接或执行错误
|
||||||
|
logger.error(f"Error during bulk deletion of error logs {log_ids}: {e}", exc_info=True)
|
||||||
|
raise # Re-raise the exception for the router to handle
|
||||||
|
|
||||||
|
async def delete_error_log_by_id(log_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
根据 ID 删除单个错误日志 (异步)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_id: 要删除的错误日志 ID。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果成功删除返回 True,否则返回 False。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 先检查是否存在 (可选,但更明确)
|
||||||
|
check_query = select(ErrorLog.id).where(ErrorLog.id == log_id)
|
||||||
|
exists = await database.fetch_one(check_query)
|
||||||
|
|
||||||
|
if not exists:
|
||||||
|
logger.warning(f"Attempted to delete non-existent error log with ID: {log_id}")
|
||||||
|
return False # 或者可以抛出 404 异常,由路由处理
|
||||||
|
|
||||||
|
# 执行删除
|
||||||
|
delete_query = delete(ErrorLog).where(ErrorLog.id == log_id)
|
||||||
|
await database.execute(delete_query)
|
||||||
|
logger.info(f"Successfully deleted error log with ID: {log_id}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting error log with ID {log_id}: {e}", exc_info=True)
|
||||||
|
raise # Re-raise the exception for the router to handle
|
||||||
|
|
||||||
|
# --- RequestLog Services (保持异步) ---
|
||||||
|
|
||||||
# 新增函数:添加请求日志
|
# 新增函数:添加请求日志
|
||||||
async def add_request_log(
|
async def add_request_log(
|
||||||
model_name: Optional[str],
|
model_name: Optional[str],
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
日志路由模块
|
日志路由模块
|
||||||
"""
|
"""
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import APIRouter, HTTPException, Request, Query, Path
|
from fastapi import APIRouter, HTTPException, Request, Query, Path, Body, Response, status
|
||||||
|
|
||||||
from app.core.security import verify_auth_token
|
from app.core.security import verify_auth_token
|
||||||
from app.log.logger import get_log_routes_logger
|
from app.log.logger import get_log_routes_logger
|
||||||
# 假设这些服务函数已更新或添加
|
# 假设这些服务函数已更新或添加
|
||||||
from app.database.services import get_error_logs, get_error_logs_count, get_error_log_details
|
from app.database.services import (
|
||||||
|
get_error_logs,
|
||||||
|
get_error_logs_count,
|
||||||
|
get_error_log_details,
|
||||||
|
delete_error_logs_by_ids, # 新增导入
|
||||||
|
delete_error_log_by_id # 新增导入
|
||||||
|
)
|
||||||
|
# Removed get_db import comment as it's fully removed now
|
||||||
|
|
||||||
# 创建路由
|
# 创建路由
|
||||||
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
||||||
@@ -38,11 +45,14 @@ async def get_error_logs_api(
|
|||||||
offset: int = Query(0, ge=0),
|
offset: int = Query(0, ge=0),
|
||||||
key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"),
|
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"), # 数据库查询需处理
|
error_search: Optional[str] = Query(None, description="Search term for error type or log message"), # 数据库查询需处理
|
||||||
|
error_code_search: Optional[str] = Query(None, description="Search term for error code"), # Added error code search parameter
|
||||||
start_date: Optional[datetime] = Query(None, description="Start datetime for filtering"),
|
start_date: Optional[datetime] = Query(None, description="Start datetime for filtering"),
|
||||||
end_date: Optional[datetime] = Query(None, description="End datetime for filtering")
|
end_date: Optional[datetime] = Query(None, description="End datetime for filtering"),
|
||||||
|
sort_by: str = Query('id', description="Field to sort by (e.g., 'id', 'request_time')"), # 新增排序参数
|
||||||
|
sort_order: str = Query('desc', description="Sort order ('asc' or 'desc')") # 新增排序参数
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
获取错误日志列表 (返回错误码)
|
获取错误日志列表 (返回错误码),支持过滤和排序
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: 请求对象
|
request: 请求对象
|
||||||
@@ -50,8 +60,11 @@ async def get_error_logs_api(
|
|||||||
offset: 偏移量
|
offset: 偏移量
|
||||||
key_search: 密钥搜索
|
key_search: 密钥搜索
|
||||||
error_search: 错误搜索 (可能搜索类型或日志内容,由DB层决定)
|
error_search: 错误搜索 (可能搜索类型或日志内容,由DB层决定)
|
||||||
|
error_code_search: 错误码搜索
|
||||||
start_date: 开始日期
|
start_date: 开始日期
|
||||||
end_date: 结束日期
|
end_date: 结束日期
|
||||||
|
sort_by: 排序字段
|
||||||
|
sort_order: 排序顺序
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ErrorLogListResponse: An object containing the list of logs (with error_code) and the total count.
|
ErrorLogListResponse: An object containing the list of logs (with error_code) and the total count.
|
||||||
@@ -70,13 +83,17 @@ async def get_error_logs_api(
|
|||||||
offset=offset,
|
offset=offset,
|
||||||
key_search=key_search,
|
key_search=key_search,
|
||||||
error_search=error_search, # 数据库查询需要处理这个
|
error_search=error_search, # 数据库查询需要处理这个
|
||||||
|
error_code_search=error_code_search, # Pass error code search to DB function
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
|
sort_by=sort_by, # 传递排序参数
|
||||||
|
sort_order=sort_order # 传递排序参数
|
||||||
)
|
)
|
||||||
# Fetch total count with the same search parameters
|
# Fetch total count with the same search parameters
|
||||||
total_count = await get_error_logs_count(
|
total_count = await get_error_logs_count(
|
||||||
key_search=key_search,
|
key_search=key_search,
|
||||||
error_search=error_search,
|
error_search=error_search,
|
||||||
|
error_code_search=error_code_search, # Pass error code search to DB count function
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date
|
end_date=end_date
|
||||||
)
|
)
|
||||||
@@ -122,3 +139,63 @@ async def get_error_log_detail_api(request: Request, log_id: int = Path(..., ge=
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}")
|
logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to get error log details: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to get error log details: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# 新增:批量删除错误日志
|
||||||
|
@router.delete("/errors", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_error_logs_bulk_api(
|
||||||
|
request: Request,
|
||||||
|
payload: Dict[str, List[int]] = Body(...) # Expects {"ids": [1, 2, 3]}
|
||||||
|
# Ensure db dependency is fully removed
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
批量删除错误日志 (异步)
|
||||||
|
"""
|
||||||
|
auth_token = request.cookies.get("auth_token")
|
||||||
|
if not auth_token or not verify_auth_token(auth_token):
|
||||||
|
logger.warning("Unauthorized access attempt to bulk delete error logs")
|
||||||
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
|
log_ids = payload.get("ids")
|
||||||
|
if not log_ids:
|
||||||
|
raise HTTPException(status_code=400, detail="No log IDs provided for deletion.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用异步服务函数
|
||||||
|
deleted_count = await delete_error_logs_by_ids(log_ids)
|
||||||
|
# 注意:异步函数返回的是尝试删除的数量,可能不是精确值
|
||||||
|
logger.info(f"Attempted bulk deletion for {deleted_count} error logs with IDs: {log_ids}")
|
||||||
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error bulk deleting error logs with IDs {log_ids}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error during bulk deletion")
|
||||||
|
|
||||||
|
|
||||||
|
# 新增:删除单个错误日志
|
||||||
|
@router.delete("/errors/{log_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_error_log_api(
|
||||||
|
request: Request,
|
||||||
|
log_id: int = Path(..., ge=1)
|
||||||
|
# Ensure db dependency is fully removed
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
删除单个错误日志 (异步)
|
||||||
|
"""
|
||||||
|
auth_token = request.cookies.get("auth_token")
|
||||||
|
if not auth_token or not verify_auth_token(auth_token):
|
||||||
|
logger.warning(f"Unauthorized access attempt to delete error log ID: {log_id}")
|
||||||
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用异步服务函数
|
||||||
|
success = await delete_error_log_by_id(log_id)
|
||||||
|
if not success:
|
||||||
|
# 服务层现在在未找到时返回 False,我们在这里转换为 404
|
||||||
|
raise HTTPException(status_code=404, detail=f"Error log with ID {log_id} not found")
|
||||||
|
logger.info(f"Successfully deleted error log with ID: {log_id}")
|
||||||
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
except HTTPException as http_exc:
|
||||||
|
raise http_exc # Re-raise 404 or other HTTP exceptions
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error deleting error log with ID {log_id}: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail="Internal server error during deletion")
|
||||||
@@ -325,13 +325,12 @@ async def verify_selected_keys(
|
|||||||
if not keys_to_verify:
|
if not keys_to_verify:
|
||||||
return JSONResponse({"success": False, "message": "没有提供需要验证的密钥"}, status_code=400)
|
return JSONResponse({"success": False, "message": "没有提供需要验证的密钥"}, status_code=400)
|
||||||
|
|
||||||
valid_count = 0
|
successful_keys = []
|
||||||
invalid_count = 0
|
failed_keys = {} # 存储失败的 key 和错误信息
|
||||||
verification_errors = {} # 存储验证过程中的错误
|
|
||||||
|
|
||||||
async def _verify_single_key(api_key: str):
|
async def _verify_single_key(api_key: str):
|
||||||
"""内部函数,用于验证单个密钥并处理异常"""
|
"""内部函数,用于验证单个密钥并处理异常"""
|
||||||
nonlocal valid_count, invalid_count # 允许修改外部计数器
|
nonlocal successful_keys, failed_keys # 允许修改外部列表和字典
|
||||||
try:
|
try:
|
||||||
# 重用单密钥验证逻辑的核心部分
|
# 重用单密钥验证逻辑的核心部分
|
||||||
gemini_request = GeminiRequest(
|
gemini_request = GeminiRequest(
|
||||||
@@ -344,7 +343,7 @@ async def verify_selected_keys(
|
|||||||
api_key
|
api_key
|
||||||
)
|
)
|
||||||
# 如果上面没有抛出异常,则认为密钥有效
|
# 如果上面没有抛出异常,则认为密钥有效
|
||||||
valid_count += 1
|
successful_keys.append(api_key)
|
||||||
return api_key, "valid", None
|
return api_key, "valid", None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
@@ -358,7 +357,7 @@ async def verify_selected_keys(
|
|||||||
# 如果密钥不在计数中(可能刚添加或从未失败),初始化为1
|
# 如果密钥不在计数中(可能刚添加或从未失败),初始化为1
|
||||||
key_manager.key_failure_counts[api_key] = 1
|
key_manager.key_failure_counts[api_key] = 1
|
||||||
logger.warning(f"Bulk verification exception for key: {api_key}, initializing failure count to 1")
|
logger.warning(f"Bulk verification exception for key: {api_key}, initializing failure count to 1")
|
||||||
invalid_count += 1
|
failed_keys[api_key] = error_message # 记录失败的 key 和错误信息
|
||||||
return api_key, "invalid", error_message
|
return api_key, "invalid", error_message
|
||||||
|
|
||||||
# 并发执行所有密钥的验证
|
# 并发执行所有密钥的验证
|
||||||
@@ -373,28 +372,39 @@ async def verify_selected_keys(
|
|||||||
# 可以选择如何处理这种任务级别的错误,这里我们简单记录
|
# 可以选择如何处理这种任务级别的错误,这里我们简单记录
|
||||||
# 也可以将其计入 invalid_count 或单独记录
|
# 也可以将其计入 invalid_count 或单独记录
|
||||||
elif result:
|
elif result:
|
||||||
key, status, error = result
|
# result 可能是 (key, status, error) 或 Exception
|
||||||
if status == "invalid" and error:
|
if not isinstance(result, Exception) and result:
|
||||||
verification_errors[key] = error # 记录具体的验证错误信息
|
key, status, error = result
|
||||||
|
# 失败信息已在 _verify_single_key 中记录到 failed_keys
|
||||||
|
elif isinstance(result, Exception):
|
||||||
|
# 记录任务本身的异常,可以关联到一个特定的 key 如果可能的话
|
||||||
|
# 这里简化处理,只记录日志
|
||||||
|
logger.error(f"Task execution error during bulk verification: {result}")
|
||||||
|
|
||||||
|
valid_count = len(successful_keys)
|
||||||
|
invalid_count = len(failed_keys)
|
||||||
logger.info(f"Bulk verification finished. Valid: {valid_count}, Invalid: {invalid_count}")
|
logger.info(f"Bulk verification finished. Valid: {valid_count}, Invalid: {invalid_count}")
|
||||||
|
|
||||||
# 根据是否有错误决定最终消息和状态
|
# 根据是否有失败的 key 决定最终消息和状态
|
||||||
if verification_errors or valid_count + invalid_count != len(keys_to_verify): # 检查是否有错误或任务异常
|
if failed_keys:
|
||||||
error_summary = "; ".join([f"{k}: {v}" for k, v in verification_errors.items()])
|
message = f"批量验证完成。成功: {valid_count}, 失败: {invalid_count}。"
|
||||||
message = f"批量验证完成,但出现问题。有效: {valid_count}, 无效: {invalid_count}。错误详情: {error_summary or '任务执行异常'}"
|
# 即使有失败也认为是部分成功,返回 200 OK,让前端处理详细结果
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"success": False, # 标记为失败,因为有错误
|
"success": True, # 表示请求处理完成,具体结果看内容
|
||||||
"message": message,
|
"message": message,
|
||||||
"valid_count": valid_count,
|
"successful_keys": successful_keys,
|
||||||
"invalid_count": invalid_count,
|
"failed_keys": failed_keys,
|
||||||
"errors": verification_errors
|
"valid_count": valid_count, # 保留计数方便前端快速展示
|
||||||
}, status_code=207) # 207 Multi-Status 表示部分成功/失败
|
"invalid_count": invalid_count
|
||||||
|
})
|
||||||
else:
|
else:
|
||||||
# 完全成功
|
# 完全成功
|
||||||
|
message = f"批量验证成功完成。所有 {valid_count} 个密钥均有效。"
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"批量验证成功完成。有效: {valid_count}, 无效: {invalid_count}",
|
"message": message,
|
||||||
|
"successful_keys": successful_keys,
|
||||||
|
"failed_keys": {},
|
||||||
"valid_count": valid_count,
|
"valid_count": valid_count,
|
||||||
"invalid_count": invalid_count
|
"invalid_count": 0
|
||||||
})
|
})
|
||||||
@@ -8,7 +8,7 @@ from fastapi.templating import Jinja2Templates
|
|||||||
|
|
||||||
from app.core.security import verify_auth_token
|
from app.core.security import verify_auth_token
|
||||||
from app.log.logger import get_routes_logger
|
from app.log.logger import get_routes_logger
|
||||||
from app.router import gemini_routes, openai_routes, config_routes, log_routes, scheduler_routes, stats_routes # 新增导入 stats_routes
|
from app.router import error_log_routes, gemini_routes, openai_routes, config_routes, scheduler_routes, stats_routes, version_routes # 新增导入 version_routes
|
||||||
from app.service.key.key_manager import get_key_manager_instance
|
from app.service.key.key_manager import get_key_manager_instance
|
||||||
from app.service.stats_service import StatsService
|
from app.service.stats_service import StatsService
|
||||||
|
|
||||||
@@ -30,9 +30,10 @@ def setup_routers(app: FastAPI) -> None:
|
|||||||
app.include_router(gemini_routes.router)
|
app.include_router(gemini_routes.router)
|
||||||
app.include_router(gemini_routes.router_v1beta)
|
app.include_router(gemini_routes.router_v1beta)
|
||||||
app.include_router(config_routes.router)
|
app.include_router(config_routes.router)
|
||||||
app.include_router(log_routes.router)
|
app.include_router(error_log_routes.router)
|
||||||
app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由
|
app.include_router(scheduler_routes.router) # 新增包含 scheduler 路由
|
||||||
app.include_router(stats_routes.router) # 包含 stats API 路由
|
app.include_router(stats_routes.router) # 包含 stats API 路由
|
||||||
|
app.include_router(version_routes.router) # 包含 version API 路由
|
||||||
|
|
||||||
# 添加页面路由
|
# 添加页面路由
|
||||||
setup_page_routes(app)
|
setup_page_routes(app)
|
||||||
|
|||||||
38
app/router/version_routes.py
Normal file
38
app/router/version_routes.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from app.service.update.update_service import check_for_updates
|
||||||
|
from app.utils.helpers import get_current_version
|
||||||
|
from app.log.logger import get_update_logger
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/version", tags=["Version"])
|
||||||
|
logger = get_update_logger()
|
||||||
|
|
||||||
|
class VersionInfo(BaseModel):
|
||||||
|
current_version: str = Field(..., description="当前应用程序版本")
|
||||||
|
latest_version: Optional[str] = Field(None, description="可用的最新版本")
|
||||||
|
update_available: bool = Field(False, description="是否有可用更新")
|
||||||
|
error_message: Optional[str] = Field(None, description="检查更新时发生的错误信息")
|
||||||
|
|
||||||
|
@router.get("/check", response_model=VersionInfo, summary="检查应用程序更新")
|
||||||
|
async def get_version_info():
|
||||||
|
"""
|
||||||
|
检查当前应用程序版本与最新的 GitHub release 版本。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
current_version = get_current_version() # Use imported function
|
||||||
|
update_available, latest_version, error_message = await check_for_updates()
|
||||||
|
|
||||||
|
# Log the result for debugging
|
||||||
|
logger.info(f"Version check API result: current={current_version}, latest={latest_version}, available={update_available}, error='{error_message}'")
|
||||||
|
|
||||||
|
return VersionInfo(
|
||||||
|
current_version=current_version,
|
||||||
|
latest_version=latest_version,
|
||||||
|
update_available=update_available,
|
||||||
|
error_message=error_message
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in /api/version/check endpoint: {e}", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail="检查版本信息时发生内部错误")
|
||||||
@@ -90,8 +90,10 @@ scheduler_instance = None
|
|||||||
|
|
||||||
def start_scheduler():
|
def start_scheduler():
|
||||||
global scheduler_instance
|
global scheduler_instance
|
||||||
if scheduler_instance is None:
|
if scheduler_instance is None or not scheduler_instance.running:
|
||||||
|
logger.info("Starting scheduler...")
|
||||||
scheduler_instance = setup_scheduler()
|
scheduler_instance = setup_scheduler()
|
||||||
|
logger.info("Scheduler is already running.")
|
||||||
|
|
||||||
def stop_scheduler():
|
def stop_scheduler():
|
||||||
global scheduler_instance
|
global scheduler_instance
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ let currentPage = 1;
|
|||||||
let pageSize = 10;
|
let pageSize = 10;
|
||||||
// let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length
|
// 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 errorLogs = []; // Store fetched logs for details view
|
||||||
|
let currentSort = { // 新增:存储当前排序状态
|
||||||
|
field: 'id', // 默认按 ID 排序
|
||||||
|
order: 'desc' // 默认降序
|
||||||
|
};
|
||||||
let currentSearch = { // Store current search parameters
|
let currentSearch = { // Store current search parameters
|
||||||
key: '',
|
key: '',
|
||||||
error: '',
|
error: '',
|
||||||
|
errorCode: '', // Added error code search
|
||||||
startDate: '',
|
startDate: '',
|
||||||
endDate: ''
|
endDate: ''
|
||||||
};
|
};
|
||||||
@@ -36,11 +41,24 @@ let logDetailModal;
|
|||||||
let modalCloseBtns; // Collection of close buttons for the modal
|
let modalCloseBtns; // Collection of close buttons for the modal
|
||||||
let keySearchInput;
|
let keySearchInput;
|
||||||
let errorSearchInput;
|
let errorSearchInput;
|
||||||
|
let errorCodeSearchInput; // Added error code input
|
||||||
let startDateInput;
|
let startDateInput;
|
||||||
let endDateInput;
|
let endDateInput;
|
||||||
let searchBtn;
|
let searchBtn;
|
||||||
let pageInput; // 新增:页码输入框
|
let pageInput;
|
||||||
let goToPageBtn; // 新增:跳转按钮
|
let goToPageBtn;
|
||||||
|
let selectAllCheckbox; // 新增:全选复选框
|
||||||
|
let copySelectedKeysBtn; // 新增:复制选中按钮
|
||||||
|
let deleteSelectedBtn; // 新增:批量删除按钮
|
||||||
|
let sortByIdHeader; // 新增:ID 排序表头
|
||||||
|
let sortIcon; // 新增:排序图标
|
||||||
|
let selectedCountSpan; // 新增:选中计数显示
|
||||||
|
let deleteConfirmModal; // 新增:删除确认模态框
|
||||||
|
let closeDeleteConfirmModalBtn; // 新增:关闭删除模态框按钮
|
||||||
|
let cancelDeleteBtn; // 新增:取消删除按钮
|
||||||
|
let confirmDeleteBtn; // 新增:确认删除按钮
|
||||||
|
let deleteConfirmMessage; // 新增:删除确认消息元素
|
||||||
|
let idsToDeleteGlobally = []; // 新增:存储待删除的ID
|
||||||
|
|
||||||
// 页面加载完成后执行
|
// 页面加载完成后执行
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -57,11 +75,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
modalCloseBtns = document.querySelectorAll('#closeLogDetailModalBtn, #closeModalFooterBtn');
|
modalCloseBtns = document.querySelectorAll('#closeLogDetailModalBtn, #closeModalFooterBtn');
|
||||||
keySearchInput = document.getElementById('keySearch');
|
keySearchInput = document.getElementById('keySearch');
|
||||||
errorSearchInput = document.getElementById('errorSearch');
|
errorSearchInput = document.getElementById('errorSearch');
|
||||||
|
errorCodeSearchInput = document.getElementById('errorCodeSearch'); // Get error code input
|
||||||
startDateInput = document.getElementById('startDate');
|
startDateInput = document.getElementById('startDate');
|
||||||
endDateInput = document.getElementById('endDate');
|
endDateInput = document.getElementById('endDate');
|
||||||
searchBtn = document.getElementById('searchBtn');
|
searchBtn = document.getElementById('searchBtn');
|
||||||
pageInput = document.getElementById('pageInput'); // 新增
|
pageInput = document.getElementById('pageInput');
|
||||||
goToPageBtn = document.getElementById('goToPageBtn'); // 新增
|
goToPageBtn = document.getElementById('goToPageBtn');
|
||||||
|
selectAllCheckbox = document.getElementById('selectAllCheckbox'); // 新增
|
||||||
|
copySelectedKeysBtn = document.getElementById('copySelectedKeysBtn'); // 新增
|
||||||
|
deleteSelectedBtn = document.getElementById('deleteSelectedBtn'); // 新增
|
||||||
|
sortByIdHeader = document.getElementById('sortById'); // 新增
|
||||||
|
if (sortByIdHeader) {
|
||||||
|
sortIcon = sortByIdHeader.querySelector('i'); // 新增
|
||||||
|
}
|
||||||
|
selectedCountSpan = document.getElementById('selectedCount'); // 新增
|
||||||
|
deleteConfirmModal = document.getElementById('deleteConfirmModal'); // 新增
|
||||||
|
closeDeleteConfirmModalBtn = document.getElementById('closeDeleteConfirmModalBtn'); // 新增
|
||||||
|
cancelDeleteBtn = document.getElementById('cancelDeleteBtn'); // 新增
|
||||||
|
confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); // 新增
|
||||||
|
deleteConfirmMessage = document.getElementById('deleteConfirmMessage'); // 新增
|
||||||
|
|
||||||
// Initialize page size selector
|
// Initialize page size selector
|
||||||
if (pageSizeSelector) {
|
if (pageSizeSelector) {
|
||||||
@@ -81,6 +113,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Update search parameters from input fields
|
// Update search parameters from input fields
|
||||||
currentSearch.key = keySearchInput ? keySearchInput.value.trim() : '';
|
currentSearch.key = keySearchInput ? keySearchInput.value.trim() : '';
|
||||||
currentSearch.error = errorSearchInput ? errorSearchInput.value.trim() : '';
|
currentSearch.error = errorSearchInput ? errorSearchInput.value.trim() : '';
|
||||||
|
currentSearch.errorCode = errorCodeSearchInput ? errorCodeSearchInput.value.trim() : ''; // Get error code value
|
||||||
currentSearch.startDate = startDateInput ? startDateInput.value : '';
|
currentSearch.startDate = startDateInput ? startDateInput.value : '';
|
||||||
currentSearch.endDate = endDateInput ? endDateInput.value : '';
|
currentSearch.endDate = endDateInput ? endDateInput.value : '';
|
||||||
currentPage = 1; // Reset to first page on new search
|
currentPage = 1; // Reset to first page on new search
|
||||||
@@ -104,8 +137,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Initial load of error logs
|
// Initial load of error logs
|
||||||
loadErrorLogs();
|
loadErrorLogs();
|
||||||
|
|
||||||
// Add event listeners for copy buttons inside the modal
|
// Add event listeners for copy buttons inside the modal and table
|
||||||
setupCopyButtons();
|
setupCopyButtons(); // This will now also handle table copy buttons if called after render
|
||||||
|
|
||||||
|
// Add event listeners for bulk selection
|
||||||
|
setupBulkSelectionListeners(); // 新增:设置批量选择监听器
|
||||||
|
|
||||||
// 新增:为页码跳转按钮添加事件监听器
|
// 新增:为页码跳转按钮添加事件监听器
|
||||||
if (goToPageBtn && pageInput) {
|
if (goToPageBtn && pageInput) {
|
||||||
@@ -132,8 +168,63 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新增:为批量删除按钮添加事件监听器
|
||||||
|
if (deleteSelectedBtn) {
|
||||||
|
deleteSelectedBtn.addEventListener('click', handleDeleteSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:为 ID 排序表头添加事件监听器
|
||||||
|
if (sortByIdHeader) {
|
||||||
|
sortByIdHeader.addEventListener('click', handleSortById);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:为删除确认模态框按钮添加事件监听器
|
||||||
|
if (closeDeleteConfirmModalBtn) {
|
||||||
|
closeDeleteConfirmModalBtn.addEventListener('click', hideDeleteConfirmModal);
|
||||||
|
}
|
||||||
|
if (cancelDeleteBtn) {
|
||||||
|
cancelDeleteBtn.addEventListener('click', hideDeleteConfirmModal);
|
||||||
|
}
|
||||||
|
if (confirmDeleteBtn) {
|
||||||
|
confirmDeleteBtn.addEventListener('click', handleConfirmDelete);
|
||||||
|
}
|
||||||
|
// Optional: Close modal if clicking outside the content
|
||||||
|
if (deleteConfirmModal) {
|
||||||
|
deleteConfirmModal.addEventListener('click', function(event) {
|
||||||
|
if (event.target === deleteConfirmModal) {
|
||||||
|
hideDeleteConfirmModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 新增:显示删除确认模态框
|
||||||
|
function showDeleteConfirmModal(message) {
|
||||||
|
if (deleteConfirmModal && deleteConfirmMessage) {
|
||||||
|
deleteConfirmMessage.textContent = message;
|
||||||
|
deleteConfirmModal.classList.add('show');
|
||||||
|
document.body.style.overflow = 'hidden'; // Prevent body scrolling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:隐藏删除确认模态框
|
||||||
|
function hideDeleteConfirmModal() {
|
||||||
|
if (deleteConfirmModal) {
|
||||||
|
deleteConfirmModal.classList.remove('show');
|
||||||
|
document.body.style.overflow = ''; // Restore body scrolling
|
||||||
|
idsToDeleteGlobally = []; // 清空待删除ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:处理确认删除按钮点击
|
||||||
|
function handleConfirmDelete() {
|
||||||
|
if (idsToDeleteGlobally.length > 0) {
|
||||||
|
performActualDelete(idsToDeleteGlobally);
|
||||||
|
}
|
||||||
|
hideDeleteConfirmModal(); // 关闭模态框
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback copy function using document.execCommand
|
// Fallback copy function using document.execCommand
|
||||||
function fallbackCopyTextToClipboard(text) {
|
function fallbackCopyTextToClipboard(text) {
|
||||||
const textArea = document.createElement("textarea");
|
const textArea = document.createElement("textarea");
|
||||||
@@ -174,44 +265,315 @@ function handleCopyResult(buttonElement, success) {
|
|||||||
setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class
|
setTimeout(() => { iconElement.className = originalIcon; }, success ? 2000 : 3000); // Restore original icon class
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to set up copy button listeners (using modern API with fallback)
|
// Function to set up copy button listeners (using modern API with fallback) - Updated to handle table copy buttons
|
||||||
function setupCopyButtons() {
|
function setupCopyButtons(containerSelector = 'body') {
|
||||||
const copyButtons = document.querySelectorAll('.copy-btn');
|
// Find buttons within the specified container (defaults to body)
|
||||||
|
const container = document.querySelector(containerSelector);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const copyButtons = container.querySelectorAll('.copy-btn');
|
||||||
copyButtons.forEach(button => {
|
copyButtons.forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
// Remove existing listener to prevent duplicates if called multiple times
|
||||||
const targetId = this.getAttribute('data-target');
|
button.removeEventListener('click', handleCopyButtonClick);
|
||||||
const targetElement = document.getElementById(targetId);
|
// Add the listener
|
||||||
|
button.addEventListener('click', handleCopyButtonClick);
|
||||||
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');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extracted click handler logic for reusability and removing listeners
|
||||||
|
function handleCopyButtonClick() {
|
||||||
|
const button = this; // 'this' refers to the button clicked
|
||||||
|
const targetId = button.getAttribute('data-target');
|
||||||
|
const textToCopyDirect = button.getAttribute('data-copy-text'); // For direct text copy (e.g., table key)
|
||||||
|
let textToCopy = '';
|
||||||
|
|
||||||
|
if (textToCopyDirect) {
|
||||||
|
textToCopy = textToCopyDirect;
|
||||||
|
} else if (targetId) {
|
||||||
|
const targetElement = document.getElementById(targetId);
|
||||||
|
if (targetElement) {
|
||||||
|
textToCopy = targetElement.textContent;
|
||||||
|
} else {
|
||||||
|
console.error('Target element not found:', targetId);
|
||||||
|
showNotification('复制出错:找不到目标元素', 'error');
|
||||||
|
return; // Exit if target element not found
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('No data-target or data-copy-text attribute found on button:', button);
|
||||||
|
showNotification('复制出错:未指定复制内容', 'error');
|
||||||
|
return; // Exit if no source specified
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (textToCopy) {
|
||||||
|
let copySuccess = false;
|
||||||
|
// Try modern clipboard API first (requires HTTPS or localhost)
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
|
handleCopyResult(button, 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(button, 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(button, copySuccess); // Use helper for feedback
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('No text found to copy for target:', targetId || 'direct text');
|
||||||
|
showNotification('没有内容可复制', 'warning');
|
||||||
|
}
|
||||||
|
} // End of handleCopyButtonClick function
|
||||||
|
|
||||||
|
// Function to set up copy button listeners (using modern API with fallback) - Updated to handle table copy buttons
|
||||||
|
function setupCopyButtons(containerSelector = 'body') {
|
||||||
|
// Find buttons within the specified container (defaults to body)
|
||||||
|
const container = document.querySelector(containerSelector);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const copyButtons = container.querySelectorAll('.copy-btn');
|
||||||
|
copyButtons.forEach(button => {
|
||||||
|
// Remove existing listener to prevent duplicates if called multiple times
|
||||||
|
button.removeEventListener('click', handleCopyButtonClick);
|
||||||
|
// Add the listener
|
||||||
|
button.addEventListener('click', handleCopyButtonClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:设置批量选择相关的事件监听器
|
||||||
|
function setupBulkSelectionListeners() {
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.addEventListener('change', handleSelectAllChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableBody) {
|
||||||
|
// 使用事件委托处理行复选框的点击
|
||||||
|
tableBody.addEventListener('change', handleRowCheckboxChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copySelectedKeysBtn) {
|
||||||
|
copySelectedKeysBtn.addEventListener('click', handleCopySelectedKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:为批量删除按钮添加事件监听器 (如果尚未添加)
|
||||||
|
// 通常在 DOMContentLoaded 中添加一次即可
|
||||||
|
// if (deleteSelectedBtn && !deleteSelectedBtn.hasListener) {
|
||||||
|
// deleteSelectedBtn.addEventListener('click', handleDeleteSelected);
|
||||||
|
// deleteSelectedBtn.hasListener = true; // 标记已添加
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:处理“全选”复选框变化的函数
|
||||||
|
function handleSelectAllChange() {
|
||||||
|
const isChecked = selectAllCheckbox.checked;
|
||||||
|
const rowCheckboxes = tableBody.querySelectorAll('.row-checkbox');
|
||||||
|
rowCheckboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = isChecked;
|
||||||
|
});
|
||||||
|
updateSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:处理行复选框变化的函数 (事件委托)
|
||||||
|
function handleRowCheckboxChange(event) {
|
||||||
|
if (event.target.classList.contains('row-checkbox')) {
|
||||||
|
updateSelectedState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:更新选中状态(计数、按钮状态、全选框状态)
|
||||||
|
function updateSelectedState() {
|
||||||
|
const rowCheckboxes = tableBody.querySelectorAll('.row-checkbox');
|
||||||
|
const selectedCheckboxes = tableBody.querySelectorAll('.row-checkbox:checked');
|
||||||
|
const selectedCount = selectedCheckboxes.length;
|
||||||
|
|
||||||
|
// 移除了数字显示,不再更新selectedCountSpan
|
||||||
|
// 仍然更新复制按钮的禁用状态
|
||||||
|
if (copySelectedKeysBtn) {
|
||||||
|
copySelectedKeysBtn.disabled = selectedCount === 0;
|
||||||
|
|
||||||
|
// 可选:根据选中项数量更新按钮标题属性
|
||||||
|
copySelectedKeysBtn.setAttribute('title', `复制${selectedCount}项选中密钥`);
|
||||||
|
}
|
||||||
|
// 新增:更新批量删除按钮的禁用状态
|
||||||
|
if (deleteSelectedBtn) {
|
||||||
|
deleteSelectedBtn.disabled = selectedCount === 0;
|
||||||
|
deleteSelectedBtn.setAttribute('title', `删除${selectedCount}项选中日志`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新“全选”复选框的状态
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
if (rowCheckboxes.length > 0 && selectedCount === rowCheckboxes.length) {
|
||||||
|
selectAllCheckbox.checked = true;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
} else if (selectedCount > 0) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = true; // 部分选中状态
|
||||||
|
} else {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:处理“复制选中密钥”按钮点击的函数
|
||||||
|
function handleCopySelectedKeys() {
|
||||||
|
const selectedCheckboxes = tableBody.querySelectorAll('.row-checkbox:checked');
|
||||||
|
const keysToCopy = [];
|
||||||
|
selectedCheckboxes.forEach(checkbox => {
|
||||||
|
const key = checkbox.getAttribute('data-key');
|
||||||
|
if (key) {
|
||||||
|
keysToCopy.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keysToCopy.length > 0) {
|
||||||
|
const textToCopy = keysToCopy.join('\n'); // 每行一个密钥
|
||||||
|
copyTextToClipboard(textToCopy, copySelectedKeysBtn); // 使用通用复制函数
|
||||||
|
} else {
|
||||||
|
showNotification('没有选中的密钥可复制', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:通用的文本复制函数(结合现有逻辑)
|
||||||
|
function copyTextToClipboard(text, buttonElement = null) {
|
||||||
|
let copySuccess = false;
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
if (buttonElement) handleCopyResult(buttonElement, true);
|
||||||
|
else showNotification('已复制到剪贴板', 'success');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Clipboard API failed, attempting fallback:', err);
|
||||||
|
copySuccess = fallbackCopyTextToClipboard(text);
|
||||||
|
if (buttonElement) handleCopyResult(buttonElement, copySuccess);
|
||||||
|
else showNotification(copySuccess ? '已复制到剪贴板' : '复制失败', copySuccess ? 'success' : 'error');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("Clipboard API not available or context insecure. Using fallback copy method.");
|
||||||
|
copySuccess = fallbackCopyTextToClipboard(text);
|
||||||
|
if (buttonElement) handleCopyResult(buttonElement, copySuccess);
|
||||||
|
else showNotification(copySuccess ? '已复制到剪贴板' : '复制失败', copySuccess ? 'success' : 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改:处理批量删除按钮点击的函数 - 改为显示模态框
|
||||||
|
function handleDeleteSelected() {
|
||||||
|
const selectedCheckboxes = tableBody.querySelectorAll('.row-checkbox:checked');
|
||||||
|
const logIdsToDelete = [];
|
||||||
|
selectedCheckboxes.forEach(checkbox => {
|
||||||
|
const logId = checkbox.getAttribute('data-log-id'); // 需要在渲染时添加 data-log-id
|
||||||
|
if (logId) {
|
||||||
|
logIdsToDelete.push(parseInt(logId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (logIdsToDelete.length === 0) {
|
||||||
|
showNotification('没有选中的日志可删除', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logIdsToDelete.length === 0) {
|
||||||
|
showNotification('没有选中的日志可删除', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储待删除ID并显示模态框
|
||||||
|
idsToDeleteGlobally = logIdsToDelete;
|
||||||
|
const message = `确定要删除选中的 ${logIdsToDelete.length} 条日志吗?此操作不可恢复!`;
|
||||||
|
showDeleteConfirmModal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:执行实际的删除操作(提取自原 handleDeleteSelected 和 handleDeleteLogRow)
|
||||||
|
async function performActualDelete(logIds) {
|
||||||
|
if (!logIds || logIds.length === 0) return;
|
||||||
|
|
||||||
|
const isSingleDelete = logIds.length === 1;
|
||||||
|
const url = isSingleDelete ? `/api/logs/errors/${logIds[0]}` : '/api/logs/errors';
|
||||||
|
const method = 'DELETE';
|
||||||
|
const body = isSingleDelete ? null : JSON.stringify({ ids: logIds });
|
||||||
|
const headers = isSingleDelete ? {} : { 'Content-Type': 'application/json' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Rename 'response' to 'deleteResponse' and remove duplicate fetch
|
||||||
|
const deleteResponse = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: headers,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
// Removed duplicate fetch call below
|
||||||
|
|
||||||
|
if (!deleteResponse.ok) {
|
||||||
|
let errorData;
|
||||||
|
try { errorData = await deleteResponse.json(); } catch (e) { /* ignore */ }
|
||||||
|
const actionText = isSingleDelete ? `删除该条日志` : `批量删除 ${logIds.length} 条日志`;
|
||||||
|
throw new Error(errorData?.detail || `${actionText}失败: ${deleteResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const successMessage = isSingleDelete ? `成功删除该日志` : `成功删除 ${logIds.length} 条日志`;
|
||||||
|
showNotification(successMessage, 'success');
|
||||||
|
// 取消全选
|
||||||
|
if (selectAllCheckbox) selectAllCheckbox.checked = false;
|
||||||
|
// 重新加载当前页数据
|
||||||
|
loadErrorLogs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除错误日志失败:', error);
|
||||||
|
showNotification(`批量删除失败: ${error.message}`, 'error', 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改:处理单行删除按钮点击的函数 - 改为显示模态框
|
||||||
|
function handleDeleteLogRow(logId) {
|
||||||
|
if (!logId) return;
|
||||||
|
|
||||||
|
// 存储待删除ID并显示模态框
|
||||||
|
idsToDeleteGlobally = [parseInt(logId)]; // 存储为数组
|
||||||
|
// 使用通用确认消息,不显示具体ID
|
||||||
|
const message = `确定要删除这条日志吗?此操作不可恢复!`;
|
||||||
|
showDeleteConfirmModal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:处理 ID 排序点击的函数
|
||||||
|
function handleSortById() {
|
||||||
|
if (currentSort.field === 'id') {
|
||||||
|
// 如果当前是按 ID 排序,切换顺序
|
||||||
|
currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
// 如果当前不是按 ID 排序,切换到按 ID 排序,默认为降序
|
||||||
|
currentSort.field = 'id';
|
||||||
|
currentSort.order = 'desc';
|
||||||
|
}
|
||||||
|
// 更新图标
|
||||||
|
updateSortIcon();
|
||||||
|
// 重新加载第一页数据
|
||||||
|
currentPage = 1;
|
||||||
|
loadErrorLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:更新排序图标的函数
|
||||||
|
function updateSortIcon() {
|
||||||
|
if (!sortIcon) return;
|
||||||
|
// 移除所有可能的排序类
|
||||||
|
sortIcon.classList.remove('fa-sort', 'fa-sort-up', 'fa-sort-down', 'text-gray-400', 'text-primary-600');
|
||||||
|
|
||||||
|
if (currentSort.field === 'id') {
|
||||||
|
sortIcon.classList.add(currentSort.order === 'asc' ? 'fa-sort-up' : 'fa-sort-down');
|
||||||
|
sortIcon.classList.add('text-primary-600'); // 高亮显示
|
||||||
|
} else {
|
||||||
|
// 如果不是按 ID 排序,显示默认图标
|
||||||
|
sortIcon.classList.add('fa-sort', 'text-gray-400');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载错误日志数据
|
// 加载错误日志数据
|
||||||
async function loadErrorLogs() {
|
async function loadErrorLogs() {
|
||||||
|
// 重置选择状态
|
||||||
|
if (selectAllCheckbox) selectAllCheckbox.checked = false;
|
||||||
|
if (selectAllCheckbox) selectAllCheckbox.indeterminate = false;
|
||||||
|
updateSelectedState(); // 更新按钮状态和计数
|
||||||
|
|
||||||
showLoading(true);
|
showLoading(true);
|
||||||
showError(false);
|
showError(false);
|
||||||
showNoData(false);
|
showNoData(false);
|
||||||
@@ -219,14 +581,21 @@ async function loadErrorLogs() {
|
|||||||
const offset = (currentPage - 1) * pageSize;
|
const offset = (currentPage - 1) * pageSize;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Construct the API URL with search parameters
|
// Construct the API URL with search and sort parameters
|
||||||
let apiUrl = `/api/logs/errors?limit=${pageSize}&offset=${offset}`;
|
let apiUrl = `/api/logs/errors?limit=${pageSize}&offset=${offset}`;
|
||||||
|
// 添加排序参数
|
||||||
|
apiUrl += `&sort_by=${currentSort.field}&sort_order=${currentSort.order}`;
|
||||||
|
|
||||||
|
// 添加搜索参数
|
||||||
if (currentSearch.key) {
|
if (currentSearch.key) {
|
||||||
apiUrl += `&key_search=${encodeURIComponent(currentSearch.key)}`;
|
apiUrl += `&key_search=${encodeURIComponent(currentSearch.key)}`;
|
||||||
}
|
}
|
||||||
if (currentSearch.error) {
|
if (currentSearch.error) {
|
||||||
apiUrl += `&error_search=${encodeURIComponent(currentSearch.error)}`;
|
apiUrl += `&error_search=${encodeURIComponent(currentSearch.error)}`;
|
||||||
}
|
}
|
||||||
|
if (currentSearch.errorCode) { // Add error code to API request
|
||||||
|
apiUrl += `&error_code_search=${encodeURIComponent(currentSearch.errorCode)}`;
|
||||||
|
}
|
||||||
if (currentSearch.startDate) {
|
if (currentSearch.startDate) {
|
||||||
apiUrl += `&start_date=${encodeURIComponent(currentSearch.startDate)}`;
|
apiUrl += `&start_date=${encodeURIComponent(currentSearch.startDate)}`;
|
||||||
}
|
}
|
||||||
@@ -274,6 +643,12 @@ function renderErrorLogs(logs) {
|
|||||||
if (!tableBody) return;
|
if (!tableBody) return;
|
||||||
tableBody.innerHTML = ''; // Clear previous entries
|
tableBody.innerHTML = ''; // Clear previous entries
|
||||||
|
|
||||||
|
// 重置全选复选框状态(在清空表格后)
|
||||||
|
if (selectAllCheckbox) {
|
||||||
|
selectAllCheckbox.checked = false;
|
||||||
|
selectAllCheckbox.indeterminate = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!logs || logs.length === 0) {
|
if (!logs || logs.length === 0) {
|
||||||
// Handled by showNoData
|
// Handled by showNoData
|
||||||
return;
|
return;
|
||||||
@@ -306,17 +681,30 @@ function renderErrorLogs(logs) {
|
|||||||
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
|
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
|
||||||
};
|
};
|
||||||
const maskedKey = maskKey(log.gemini_key);
|
const maskedKey = maskKey(log.gemini_key);
|
||||||
|
const fullKey = log.gemini_key || ''; // Store the full key
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td>${sequentialId}</td> <!-- Use sequential ID -->
|
<td class="text-center px-3 py-3"> <!-- Checkbox column -->
|
||||||
<td title="${log.gemini_key || ''}">${maskedKey}</td>
|
<input type="checkbox" class="row-checkbox form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500" data-key="${fullKey}" data-log-id="${log.id}"> <!-- 添加 data-log-id -->
|
||||||
|
</td>
|
||||||
|
<td>${sequentialId}</td> <!-- 显示从1开始的序号 -->
|
||||||
|
<td class="relative group" title="${fullKey}"> <!-- Added relative/group for button positioning -->
|
||||||
|
${maskedKey}
|
||||||
|
<!-- Added copy button for the key in the table row -->
|
||||||
|
<button class="copy-btn absolute top-1/2 right-2 transform -translate-y-1/2 bg-gray-200 hover:bg-gray-300 text-gray-600 p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity text-xs" data-copy-text="${log.gemini_key || ''}" title="复制完整密钥">
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td>${log.error_type || '未知'}</td>
|
<td>${log.error_type || '未知'}</td>
|
||||||
<td class="error-code-content" title="${log.error_code || ''}">${errorCodeContent}</td>
|
<td class="error-code-content" title="${log.error_code || ''}">${errorCodeContent}</td>
|
||||||
<td>${log.model_name || '未知'}</td>
|
<td>${log.model_name || '未知'}</td>
|
||||||
<td>${formattedTime}</td>
|
<td>${formattedTime}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn-view-details" data-log-id="${log.id}">
|
<button class="btn-view-details mr-2" data-log-id="${log.id}"> <!-- 添加 mr-2 -->
|
||||||
查看详情
|
<i class="fas fa-eye mr-1"></i>详情
|
||||||
|
</button>
|
||||||
|
<button class="btn-delete-row text-danger-600 hover:text-danger-800" data-log-id="${log.id}" title="删除此日志">
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
@@ -331,6 +719,19 @@ function renderErrorLogs(logs) {
|
|||||||
showLogDetails(logId);
|
showLogDetails(logId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 新增:为新渲染的删除按钮添加事件监听器
|
||||||
|
document.querySelectorAll('.btn-delete-row').forEach(button => {
|
||||||
|
button.addEventListener('click', function() {
|
||||||
|
const logId = this.getAttribute('data-log-id');
|
||||||
|
handleDeleteLogRow(logId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-initialize copy buttons specifically for the newly rendered table rows
|
||||||
|
setupCopyButtons('#errorLogsTable');
|
||||||
|
// Update selected state after rendering
|
||||||
|
updateSelectedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示错误日志详情 (从 API 获取)
|
// 显示错误日志详情 (从 API 获取)
|
||||||
@@ -403,6 +804,9 @@ async function showLogDetails(logId) {
|
|||||||
document.getElementById('modalModelName').textContent = logDetails.model_name || '未知';
|
document.getElementById('modalModelName').textContent = logDetails.model_name || '未知';
|
||||||
document.getElementById('modalRequestTime').textContent = formattedTime;
|
document.getElementById('modalRequestTime').textContent = formattedTime;
|
||||||
|
|
||||||
|
// Re-initialize copy buttons specifically for the modal after content is loaded
|
||||||
|
setupCopyButtons('#logDetailModal');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取日志详情失败:', error);
|
console.error('获取日志详情失败:', error);
|
||||||
// Show error in modal
|
// Show error in modal
|
||||||
@@ -547,10 +951,17 @@ function showError(show, message = '加载错误日志失败,请稍后重试
|
|||||||
|
|
||||||
// Function to show temporary status notifications (like copy success)
|
// Function to show temporary status notifications (like copy success)
|
||||||
function showNotification(message, type = 'success', duration = 3000) {
|
function showNotification(message, type = 'success', duration = 3000) {
|
||||||
const notificationElement = document.getElementById('copyStatus'); // Or a more generic ID if needed
|
const notificationElement = document.getElementById('notification'); // Use the correct ID from base.html
|
||||||
if (!notificationElement) return;
|
if (!notificationElement) {
|
||||||
|
console.error("Notification element with ID 'notification' not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set message and type class
|
||||||
notificationElement.textContent = message;
|
notificationElement.textContent = message;
|
||||||
|
// Remove previous type classes before adding the new one
|
||||||
|
notificationElement.classList.remove('success', 'error', 'warning', 'info');
|
||||||
|
notificationElement.classList.add(type); // Add the type class for styling
|
||||||
notificationElement.className = `notification ${type} show`; // Add 'show' class
|
notificationElement.className = `notification ${type} show`; // Add 'show' class
|
||||||
|
|
||||||
// Hide after duration
|
// Hide after duration
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -141,7 +141,7 @@
|
|||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 500; /* font-medium */
|
font-weight: 500; /* font-medium */
|
||||||
z-index: 50;
|
z-index: 1000; /* Increased z-index */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
@@ -202,20 +202,9 @@
|
|||||||
<span class="text-xs text-yellow-600 font-semibold">
|
<span class="text-xs text-yellow-600 font-semibold">
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗
|
<i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗
|
||||||
</span>
|
</span>
|
||||||
{% if request and request.app.state.update_info %}
|
<span id="version-info-container" class="inline-block">
|
||||||
{% set update_info = request.app.state.update_info %}
|
<!-- Version info will be loaded here by JavaScript -->
|
||||||
<span class="mx-1">|</span>
|
</span>
|
||||||
<span class="text-xs text-gray-500">v{{ update_info.current_version }}</span>
|
|
||||||
{% if update_info.update_available %}
|
|
||||||
<span class="mx-1">|</span>
|
|
||||||
<a href="https://github.com/snailyp/gemini-balance/releases/latest" target="_blank" class="text-yellow-600 hover:text-yellow-800 transition duration-300 animate-pulse">
|
|
||||||
<i class="fas fa-arrow-up"></i> 新版本: v{{ update_info.latest_version }}
|
|
||||||
</a>
|
|
||||||
{% elif update_info.error_message and update_info.error_message != 'Checking...' %}
|
|
||||||
<span class="mx-1">|</span>
|
|
||||||
<span class="text-xs text-red-500" title="{{ update_info.error_message }}">更新检查失败</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 通用JS -->
|
<!-- 通用JS -->
|
||||||
@@ -279,6 +268,48 @@
|
|||||||
}, 300); // Short delay to show spinner
|
}, 300); // Short delay to show spinner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Version Check ---
|
||||||
|
const versionInfoContainer = document.getElementById('version-info-container');
|
||||||
|
|
||||||
|
async function fetchVersionInfo() {
|
||||||
|
if (!versionInfoContainer) return;
|
||||||
|
versionInfoContainer.innerHTML = '<span class="mx-1">|</span><span class="text-xs text-gray-400">检查更新中...</span>'; // Initial loading state
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/version/check');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
let versionHtml = `<span class="mx-1">|</span><span class="text-xs text-gray-500">v${data.current_version}</span>`;
|
||||||
|
if (data.update_available) {
|
||||||
|
versionHtml += `
|
||||||
|
<span class="mx-1">|</span>
|
||||||
|
<a href="https://github.com/snailyp/gemini-balance/releases/latest" target="_blank" class="text-yellow-600 hover:text-yellow-800 transition duration-300 animate-pulse">
|
||||||
|
<i class="fas fa-arrow-up"></i> 新版本: v${data.latest_version}
|
||||||
|
</a>`;
|
||||||
|
} else if (data.error_message) {
|
||||||
|
versionHtml += `
|
||||||
|
<span class="mx-1">|</span>
|
||||||
|
<span class="text-xs text-red-500" title="${data.error_message}">更新检查失败</span>`;
|
||||||
|
} else {
|
||||||
|
versionHtml += `<span class="mx-1">|</span><span class="text-xs text-green-500">已是最新</span>`; // Indicate up-to-date
|
||||||
|
}
|
||||||
|
versionInfoContainer.innerHTML = versionHtml;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching version info:', error);
|
||||||
|
versionInfoContainer.innerHTML = `<span class="mx-1">|</span><span class="text-xs text-red-500" title="无法连接到服务器或解析响应">更新检查失败</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch immediately on load
|
||||||
|
fetchVersionInfo();
|
||||||
|
|
||||||
|
// Fetch periodically (e.g., every hour)
|
||||||
|
setInterval(fetchVersionInfo, 3600000); // 3600000 ms = 1 hour
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% block body_scripts %}{% endblock %}
|
{% block body_scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -48,6 +48,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Modal styles are in base.html */
|
/* Modal styles are in base.html */
|
||||||
|
|
||||||
|
/* 确保输入框和按钮高度一致 */
|
||||||
|
input[type="text"], input[type="datetime-local"], select, button {
|
||||||
|
height: 36px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 日期选择器样式优化 */
|
||||||
|
.date-range-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保所有输入框在小屏幕上正确显示 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
input[type="datetime-local"] {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -83,32 +103,56 @@
|
|||||||
<!-- 控制区域 (Refresh button removed, page size moved below) -->
|
<!-- 控制区域 (Refresh button removed, page size moved below) -->
|
||||||
<!-- Removed the original controls div -->
|
<!-- Removed the original controls div -->
|
||||||
|
|
||||||
<!-- 搜索控件 -->
|
<!-- 搜索与操作控件 -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 mb-6">
|
<div class="grid grid-cols-1 lg:grid-cols-[1fr_auto] items-center gap-4 mb-6"> <!-- 修改为items-center -->
|
||||||
<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">
|
<!-- Left side: Search inputs and date range -->
|
||||||
<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="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full"> <!-- 修改为3列布局 -->
|
||||||
<div class="flex items-center gap-2 col-span-1 lg:col-span-2">
|
<input type="text" id="keySearch" placeholder="搜索密钥 (部分)" style="height: 36px;" class="px-3 py-1 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
|
||||||
<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">
|
<input type="text" id="errorSearch" placeholder="搜索错误类型/日志" style="height: 36px;" class="px-3 py-1 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
|
||||||
<span class="text-gray-700">至</span>
|
<input type="text" id="errorCodeSearch" placeholder="搜索错误码" style="height: 36px;" class="px-3 py-1 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50">
|
||||||
<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 class="grid grid-cols-1 sm:grid-cols-2 gap-2 col-span-1 sm:col-span-2 lg:col-span-3 mt-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-700 whitespace-nowrap">开始时间:</label>
|
||||||
|
<input type="datetime-local" id="startDate" style="height: 36px;" class="px-3 py-1 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 text-sm w-full">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label class="text-sm text-gray-700 whitespace-nowrap">结束时间:</label>
|
||||||
|
<input type="datetime-local" id="endDate" style="height: 36px;" class="px-3 py-1 rounded-lg border border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 text-sm w-full">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Right side: Action buttons -->
|
||||||
|
<div class="flex items-center gap-3 flex-shrink-0"> <!-- 移除上边距 -->
|
||||||
|
<button id="searchBtn" class="flex items-center justify-center px-4 py-1.5 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md whitespace-nowrap" style="height: 36px;">
|
||||||
|
<i class="fas fa-search mr-1.5"></i>搜索
|
||||||
|
</button>
|
||||||
|
<button id="copySelectedKeysBtn" class="flex items-center justify-center px-4 py-1.5 bg-success-600 hover:bg-success-700 text-white rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md whitespace-nowrap" style="height: 36px;" disabled>
|
||||||
|
<i class="far fa-copy mr-1.5"></i>复制
|
||||||
|
</button>
|
||||||
|
<button id="deleteSelectedBtn" class="flex items-center justify-center px-4 py-1.5 bg-danger-600 hover:bg-danger-700 text-white rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md whitespace-nowrap" style="height: 36px;" disabled>
|
||||||
|
<i class="fas fa-trash-alt mr-1.5"></i>删除
|
||||||
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- 表格容器 - Enhanced Styling -->
|
<!-- 表格容器 - Enhanced Styling -->
|
||||||
<div class="overflow-x-auto rounded-lg border border-gray-200 mb-6 bg-white"> <!-- Removed shadow, added border -->
|
<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 -->
|
<table class="styled-table w-full min-w-full text-sm"> <!-- Added text-sm -->
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-primary-50 text-left text-primary-800"> <!-- Changed header background and text color -->
|
<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-3 py-3 font-semibold rounded-tl-lg w-12 text-center"> <!-- Adjusted padding and width -->
|
||||||
|
<input type="checkbox" id="selectAllCheckbox" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500">
|
||||||
|
</th>
|
||||||
|
<th class="px-5 py-3 font-semibold cursor-pointer" id="sortById">
|
||||||
|
ID <i class="fas fa-sort ml-1 text-gray-400"></i>
|
||||||
|
</th>
|
||||||
<th class="px-5 py-3 font-semibold">Gemini密钥</th>
|
<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">模型名称</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 -->
|
<th class="px-5 py-3 font-semibold rounded-tr-lg text-center">操作</th> <!-- Adjusted rounding and centered -->
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="errorLogsTable" class="divide-y divide-gray-200">
|
<tbody id="errorLogsTable" class="divide-y divide-gray-200">
|
||||||
@@ -186,17 +230,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 max-h-[60vh] overflow-y-auto p-1">
|
<div class="space-y-4 max-h-[60vh] overflow-y-auto p-1">
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<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">Gemini密钥:</h6>
|
<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>
|
<pre id="modalGeminiKey" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto"></pre>
|
||||||
</div>
|
<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="modalGeminiKey" title="复制密钥">
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
</button>
|
||||||
<h6 class="text-sm font-semibold text-gray-600 mb-1">错误类型:</h6>
|
|
||||||
<p id="modalErrorType" class="text-danger-600 font-medium"></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg relative group"> <!-- Added relative and group -->
|
<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>
|
||||||
|
<p id="modalErrorType" class="text-danger-600 font-medium pr-8"></p> <!-- Added padding right for button -->
|
||||||
|
<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="modalErrorType" title="复制错误类型">
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg relative group">
|
||||||
<h6 class="text-sm font-semibold text-gray-600 mb-1">错误日志:</h6>
|
<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>
|
<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="复制错误日志">
|
<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="复制错误日志">
|
||||||
@@ -204,7 +254,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg relative group"> <!-- Added relative and group -->
|
<div class="bg-gray-50 p-4 rounded-lg relative group">
|
||||||
<h6 class="text-sm font-semibold text-gray-600 mb-1">请求消息:</h6>
|
<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>
|
<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="复制请求消息">
|
<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="复制请求消息">
|
||||||
@@ -212,14 +262,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<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>
|
<h6 class="text-sm font-semibold text-gray-600 mb-1">模型名称:</h6>
|
||||||
<p id="modalModelName" class="font-medium"></p>
|
<p id="modalModelName" class="font-medium pr-8"></p> <!-- Added padding right for button -->
|
||||||
|
<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="modalModelName" title="复制模型名称">
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg">
|
<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>
|
<h6 class="text-sm font-semibold text-gray-600 mb-1">请求时间:</h6>
|
||||||
<p id="modalRequestTime" class="font-medium"></p>
|
<p id="modalRequestTime" class="font-medium pr-8"></p> <!-- Added padding right for button -->
|
||||||
|
<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="modalRequestTime" title="复制请求时间">
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -230,6 +286,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除确认模态框 -->
|
||||||
|
<div id="deleteConfirmModal" class="modal">
|
||||||
|
<div class="w-full max-w-md mx-auto bg-white rounded-xl shadow-xl overflow-hidden animate-fade-in">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center border-b border-gray-200 pb-3 mb-4">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-800">确认删除</h2>
|
||||||
|
<button id="closeDeleteConfirmModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">×</button>
|
||||||
|
</div>
|
||||||
|
<p id="deleteConfirmMessage" class="text-gray-700 mb-6">你确定要删除选中的项目吗?此操作不可恢复!</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button id="cancelDeleteBtn" type="button" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-5 py-2 rounded-lg font-medium transition">取消</button>
|
||||||
|
<button id="confirmDeleteBtn" type="button" class="bg-danger-600 hover:bg-danger-700 text-white px-5 py-2 rounded-lg font-medium transition">确认删除</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_scripts %}
|
{% block body_scripts %}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
<style>
|
<style>
|
||||||
/* keys_status.html specific styles */
|
/* keys_status.html specific styles */
|
||||||
.key-content {
|
.key-content {
|
||||||
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out, padding 0.3s ease-in-out; /* Added padding transition */
|
||||||
|
overflow: hidden; /* Keep hidden initially and during collapse */
|
||||||
}
|
}
|
||||||
.key-content.collapsed {
|
.key-content.collapsed {
|
||||||
max-height: 0;
|
max-height: 0 !important; /* Use important to override inline style during transition */
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
padding-top: 0 !important; /* Collapse padding */
|
||||||
|
padding-bottom: 0 !important; /* Collapse padding */
|
||||||
|
/* overflow: hidden; */ /* Already set above */
|
||||||
}
|
}
|
||||||
.toggle-icon {
|
.toggle-icon {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
@@ -30,13 +33,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.stats-dashboard {
|
.stats-dashboard {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计卡片样式 */
|
/* 统计卡片样式 */
|
||||||
.stats-card {
|
.stats-card {
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
@@ -48,11 +51,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card:hover {
|
.stats-card:hover {
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card-header {
|
.stats-card-header {
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
@@ -60,8 +63,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap; /* Allow wrapping for smaller screens */
|
||||||
|
gap: 0.5rem; /* Add gap between items */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card-title {
|
.stats-card-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -69,19 +74,19 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #374151;
|
color: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-card-title i {
|
.stats-card-title i {
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
color: #4F46E5;
|
color: #4F46E5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计项样式 */
|
/* 统计项样式 */
|
||||||
.stat-item {
|
.stat-item {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
@@ -95,7 +100,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item::before {
|
.stat-item::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -105,15 +110,15 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item:hover::before {
|
.stat-item:hover::before {
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item:hover {
|
.stat-item:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -121,7 +126,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
color: #1F2937;
|
color: #1F2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -132,7 +137,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
color: #6B7280;
|
color: #6B7280;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
@@ -142,63 +147,30 @@
|
|||||||
transform: rotate(12deg);
|
transform: rotate(12deg);
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item:hover .stat-icon {
|
.stat-item:hover .stat-icon {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
transform: scale(1.1) rotate(0deg);
|
transform: scale(1.1) rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统计类型样式 */
|
/* 统计类型样式 */
|
||||||
.stat-primary {
|
.stat-primary { color: #4F46E5; background-color: rgba(238, 242, 255, 0.5); }
|
||||||
color: #4F46E5;
|
.stat-success { color: #10B981; background-color: rgba(236, 253, 245, 0.5); }
|
||||||
background-color: rgba(238, 242, 255, 0.5);
|
.stat-danger { color: #EF4444; background-color: rgba(254, 242, 242, 0.5); }
|
||||||
}
|
.stat-warning { color: #F59E0B; background-color: rgba(255, 251, 235, 0.5); }
|
||||||
|
.stat-info { color: #3B82F6; background-color: rgba(239, 246, 255, 0.5); }
|
||||||
.stat-success {
|
|
||||||
color: #10B981;
|
|
||||||
background-color: rgba(236, 253, 245, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-danger {
|
|
||||||
color: #EF4444;
|
|
||||||
background-color: rgba(254, 242, 242, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-warning {
|
|
||||||
color: #F59E0B;
|
|
||||||
background-color: rgba(255, 251, 235, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-info {
|
|
||||||
color: #3B82F6;
|
|
||||||
background-color: rgba(239, 246, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
/* 响应式调整 */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.stats-dashboard {
|
.stats-dashboard { gap: 1rem; }
|
||||||
gap: 1rem;
|
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 0.5rem; padding: 0.5rem; }
|
||||||
}
|
.stat-item { padding: 0.5rem; }
|
||||||
|
.stat-value { font-size: 1.25rem; }
|
||||||
.stats-grid {
|
.stat-label { font-size: 0.625rem; }
|
||||||
grid-template-columns: repeat(2, 1fr);
|
.stats-card-header { padding: 0.5rem 0.75rem; } /* Adjust header padding */
|
||||||
gap: 0.5rem;
|
.key-content ul { grid-template-columns: 1fr; } /* Stack keys vertically on small screens */
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.625rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/* Tailwind Toggle Switch Helper CSS from config_editor.html */
|
/* Tailwind Toggle Switch Helper CSS */
|
||||||
.toggle-checkbox:checked {
|
.toggle-checkbox:checked {
|
||||||
@apply: right-0 border-primary-600;
|
@apply: right-0 border-primary-600;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -209,6 +181,34 @@
|
|||||||
background-color: #4F46E5;
|
background-color: #4F46E5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pagination Controls */
|
||||||
|
#validPaginationControls, #invalidPaginationControls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem; /* mt-4 */
|
||||||
|
gap: 0.5rem; /* space-x-2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure list items are flex for alignment */
|
||||||
|
#validKeys li, #invalidKeys li {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start; /* Align checkbox with top of content */
|
||||||
|
gap: 0.75rem; /* gap-3 */
|
||||||
|
}
|
||||||
|
/* Ensure grid layout for key lists */
|
||||||
|
#validKeys, #invalidKeys {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr; /* Default single column */
|
||||||
|
gap: 0.75rem; /* gap-3 */
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) { /* md breakpoint */
|
||||||
|
#validKeys, #invalidKeys {
|
||||||
|
grid-template-columns: repeat(2, 1fr); /* Two columns on medium screens and up */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API调用统计卡片 -->
|
<!-- API调用统计卡片 -->
|
||||||
<div class="stats-card">
|
<div class="stats-card">
|
||||||
<div class="stats-card-header">
|
<div class="stats-card-header">
|
||||||
@@ -311,161 +311,217 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 有效密钥区域 -->
|
<!-- 有效密钥区域 -->
|
||||||
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.2s">
|
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.2s">
|
||||||
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'validKeys')">
|
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'validKeys')">
|
||||||
<div class="flex items-center gap-3">
|
<!-- Left side: Title and Toggle Icon -->
|
||||||
|
<div class="flex items-center gap-3 flex-shrink-0"> <!-- Prevent shrinking -->
|
||||||
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
||||||
<i class="fas fa-check-circle text-success-500"></i>
|
<i class="fas fa-check-circle text-success-500"></i>
|
||||||
<h2 class="text-lg font-semibold">有效密钥列表 ({{ valid_key_count }})</h2>
|
<h2 class="text-lg font-semibold whitespace-nowrap">有效密钥列表 ({{ valid_key_count }})</h2>
|
||||||
<div class="flex items-center gap-2 ml-4">
|
</div>
|
||||||
<label for="failCountThreshold" class="text-sm text-gray-600 select-none">失败次数≥</label>
|
<!-- Middle: Filters and Search (Allow wrapping) -->
|
||||||
|
<div class="flex items-center gap-x-4 gap-y-2 flex-grow flex-wrap justify-start md:justify-center"> <!-- Allow wrapping, center on medium+ -->
|
||||||
|
<!-- 失败次数筛选 -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<label for="failCountThreshold" class="text-sm text-gray-600 select-none whitespace-nowrap">失败次数≥</label>
|
||||||
<input type="number" id="failCountThreshold" value="0" min="0" class="form-input h-7 w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500" onclick="event.stopPropagation();">
|
<input type="number" id="failCountThreshold" value="0" min="0" class="form-input h-7 w-16 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500" onclick="event.stopPropagation();">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 密钥搜索 -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<label for="keySearchInput" class="text-sm text-gray-600 select-none whitespace-nowrap"><i class="fas fa-search mr-1"></i>搜索</label>
|
||||||
|
<input type="search" id="keySearchInput" placeholder="输入密钥..." class="form-input h-7 w-32 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500" onclick="event.stopPropagation();">
|
||||||
|
</div>
|
||||||
|
<!-- 每页显示数量 -->
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<label for="itemsPerPageSelect" class="text-sm text-gray-600 select-none whitespace-nowrap">每页</label>
|
||||||
|
<select id="itemsPerPageSelect" class="form-select h-7 px-2 py-1 text-sm border border-gray-300 rounded focus:ring-primary-500 focus:border-primary-500 bg-white" onclick="event.stopPropagation();">
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
<span class="text-sm text-gray-600 select-none">项</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<!-- Right side: Select All -->
|
||||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('valid', event)"> <!-- 新增批量验证按钮 -->
|
<div class="flex items-center gap-1 flex-shrink-0" onclick="event.stopPropagation();"> <!-- Prevent shrinking -->
|
||||||
<i class="fas fa-check-double"></i>
|
<input type="checkbox" id="selectAllValid" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500" onchange="toggleSelectAll('valid', this.checked)">
|
||||||
批量验证
|
<label for="selectAllValid" class="text-sm text-gray-600 select-none whitespace-nowrap">全选</label>
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('valid', event)" data-reset-type="valid">
|
|
||||||
<i class="fas fa-redo-alt"></i>
|
|
||||||
批量重置
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); copyKeys('valid')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
批量复制
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 批量操作按钮组 (仅在选中时显示) -->
|
||||||
|
<div id="validBatchActions" class="p-3 bg-gray-50 border-t border-gray-200 hidden flex items-center flex-wrap gap-3"> <!-- Added flex-wrap -->
|
||||||
|
<span class="text-sm font-medium text-gray-700 whitespace-nowrap">已选择 <span id="validSelectedCount">0</span> 项</span>
|
||||||
|
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); showVerifyModal('valid', event)" disabled>
|
||||||
|
<i class="fas fa-check-double"></i> 批量验证
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); resetAllKeysFailCount('valid', event)" data-reset-type="valid" disabled>
|
||||||
|
<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 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); copySelectedKeys('valid')" disabled>
|
||||||
|
<i class="fas fa-copy"></i> 批量复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="key-content p-4 bg-white bg-opacity-40">
|
<div class="key-content p-4 bg-white bg-opacity-40">
|
||||||
|
<!-- Key list will be populated by JS -->
|
||||||
<ul id="validKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<ul id="validKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{# Initial keys rendered by server-side for non-JS users or initial load #}
|
||||||
|
{# JS will replace this content with paginated/filtered results #}
|
||||||
{% if valid_keys %}
|
{% if valid_keys %}
|
||||||
{% for key, fail_count in valid_keys.items() %}
|
{% for key, fail_count in valid_keys.items() %}
|
||||||
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-success-300 transform hover:-translate-y-1" data-fail-count="{{ fail_count }}">
|
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-success-300 transform hover:-translate-y-1" data-fail-count="{{ fail_count }}" data-key="{{ key }}">
|
||||||
<div class="flex flex-col justify-between h-full gap-3">
|
<!-- Checkbox -->
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<input type="checkbox" class="form-checkbox h-5 w-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500 mt-1 key-checkbox" data-key-type="valid" value="{{ key }}">
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-600">
|
<!-- Key Info -->
|
||||||
<i class="fas fa-check mr-1"></i> 有效
|
<div class="flex-grow">
|
||||||
</span>
|
<div class="flex flex-col justify-between h-full gap-3">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-50 text-success-600">
|
||||||
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
<i class="fas fa-check mr-1"></i> 有效
|
||||||
<i class="fas fa-eye"></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)" title="显示/隐藏密钥">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||||
|
失败: {{ fail_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
验证
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
||||||
|
<i class="fas fa-redo-alt"></i>
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
详情
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
失败: {{ fail_count }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
|
||||||
<i class="fas fa-check-circle"></i>
|
|
||||||
验证
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
|
||||||
<i class="fas fa-redo-alt"></i>
|
|
||||||
重置
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
复制
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
|
||||||
<i class="fas fa-chart-pie"></i>
|
|
||||||
详情
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="text-center text-gray-500 py-4">暂无有效密钥</li>
|
<li class="text-center text-gray-500 py-4 col-span-full">暂无有效密钥</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
<!-- 有效密钥分页控件容器 -->
|
||||||
|
<div id="validPaginationControls" class="flex justify-center items-center mt-4 space-x-2">
|
||||||
|
<!-- Pagination controls will be generated by JS -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 无效密钥区域 -->
|
<!-- 无效密钥区域 -->
|
||||||
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.4s">
|
<div class="stats-card mb-6 animate-fade-in" style="animation-delay: 0.4s">
|
||||||
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'invalidKeys')">
|
<div class="stats-card-header cursor-pointer" onclick="toggleSection(this, 'invalidKeys')">
|
||||||
<div class="flex items-center gap-3">
|
<!-- Left side: Title and Toggle Icon -->
|
||||||
|
<div class="flex items-center gap-3 flex-shrink-0"> <!-- Prevent shrinking -->
|
||||||
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
<i class="fas fa-chevron-down toggle-icon text-primary-600"></i>
|
||||||
<i class="fas fa-times-circle text-danger-500"></i>
|
<i class="fas fa-times-circle text-danger-500"></i>
|
||||||
<h2 class="text-lg font-semibold">无效密钥列表 ({{ invalid_key_count }})</h2>
|
<h2 class="text-lg font-semibold whitespace-nowrap">无效密钥列表 ({{ invalid_key_count }})</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<!-- Right side: Select All -->
|
||||||
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); showVerifyModal('invalid', event)"> <!-- 新增批量验证按钮 -->
|
<div class="flex items-center gap-1 ml-auto flex-shrink-0" onclick="event.stopPropagation();"> <!-- Use ml-auto, Prevent shrinking -->
|
||||||
<i class="fas fa-check-double"></i>
|
<input type="checkbox" id="selectAllInvalid" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500" onchange="toggleSelectAll('invalid', this.checked)">
|
||||||
批量验证
|
<label for="selectAllInvalid" class="text-sm text-gray-600 select-none whitespace-nowrap">全选</label>
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); resetAllKeysFailCount('invalid', event)" data-reset-type="invalid">
|
|
||||||
<i class="fas fa-redo-alt"></i>
|
|
||||||
批量重置
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-2 bg-primary-600 hover:bg-primary-700 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="event.stopPropagation(); copyKeys('invalid')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
批量复制
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 批量操作按钮组 (仅在选中时显示) -->
|
||||||
|
<div id="invalidBatchActions" class="p-3 bg-gray-50 border-t border-gray-200 hidden flex items-center flex-wrap gap-3"> <!-- Added flex-wrap -->
|
||||||
|
<span class="text-sm font-medium text-gray-700 whitespace-nowrap">已选择 <span id="invalidSelectedCount">0</span> 项</span>
|
||||||
|
<button class="flex items-center gap-2 bg-teal-500 hover:bg-teal-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); showVerifyModal('invalid', event)" disabled>
|
||||||
|
<i class="fas fa-check-double"></i> 批量验证
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); resetAllKeysFailCount('invalid', event)" data-reset-type="invalid" disabled>
|
||||||
|
<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 disabled:opacity-50 disabled:cursor-not-allowed" onclick="event.stopPropagation(); copySelectedKeys('invalid')" disabled>
|
||||||
|
<i class="fas fa-copy"></i> 批量复制
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="key-content p-4 bg-white bg-opacity-40">
|
<div class="key-content p-4 bg-white bg-opacity-40">
|
||||||
|
<!-- Key list will be populated by JS -->
|
||||||
<ul id="invalidKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<ul id="invalidKeys" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{# Initial keys rendered by server-side #}
|
||||||
|
{# JS will replace this content with paginated results #}
|
||||||
{% if invalid_keys %}
|
{% if invalid_keys %}
|
||||||
{% for key, fail_count in invalid_keys.items() %}
|
{% for key, fail_count in invalid_keys.items() %}
|
||||||
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-danger-300 transform hover:-translate-y-1">
|
<li class="bg-white rounded-lg p-3 shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-danger-300 transform hover:-translate-y-1" data-key="{{ key }}">
|
||||||
<div class="flex flex-col justify-between h-full gap-3">
|
<!-- Checkbox -->
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<input type="checkbox" class="form-checkbox h-5 w-5 text-primary-600 border-gray-300 rounded focus:ring-primary-500 mt-1 key-checkbox" data-key-type="invalid" value="{{ key }}">
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-50 text-danger-600">
|
<!-- Key Info -->
|
||||||
<i class="fas fa-times mr-1"></i> 无效
|
<div class="flex-grow">
|
||||||
</span>
|
<div class="flex flex-col justify-between h-full gap-3">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<span class="key-text font-mono text-gray-700" data-full-key="{{ key }}">{{ key[:4] + '...' + key[-4:] }}</span>
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-50 text-danger-600">
|
||||||
<button class="text-gray-500 hover:text-primary-600 transition-colors" onclick="toggleKeyVisibility(this)">
|
<i class="fas fa-times mr-1"></i> 无效
|
||||||
<i class="fas fa-eye"></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)" title="显示/隐藏密钥">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||||
|
失败: {{ fail_count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
验证
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
||||||
|
<i class="fas fa-redo-alt"></i>
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
复制
|
||||||
|
</button>
|
||||||
|
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
详情
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-600">
|
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
||||||
失败: {{ fail_count }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
|
||||||
<button class="flex items-center gap-1 bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="verifyKey('{{ key }}', this)">
|
|
||||||
<i class="fas fa-check-circle"></i>
|
|
||||||
验证
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-1 bg-blue-500 hover:bg-blue-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="resetKeyFailCount('{{ key }}', this)">
|
|
||||||
<i class="fas fa-redo-alt"></i>
|
|
||||||
重置
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-1 bg-gray-500 hover:bg-gray-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="copyKey('{{ key }}')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
复制
|
|
||||||
</button>
|
|
||||||
<button class="flex items-center gap-1 bg-purple-500 hover:bg-purple-600 text-white px-3 py-1.5 rounded-lg text-sm font-medium transition-all duration-200" onclick="showKeyUsageDetails('{{ key }}')">
|
|
||||||
<i class="fas fa-chart-pie"></i>
|
|
||||||
详情
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="text-center text-gray-500 py-4">暂无无效密钥</li>
|
<li class="text-center text-gray-500 py-4 col-span-full">暂无无效密钥</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
<!-- 无效密钥分页控件容器 -->
|
||||||
|
<div id="invalidPaginationControls" class="flex justify-center items-center mt-4 space-x-2">
|
||||||
|
<!-- Pagination controls will be generated by JS -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Removed old total keys display -->
|
<!-- Removed old total keys display -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scroll buttons are now in base.html -->
|
<!-- Scroll buttons are now in base.html -->
|
||||||
<div class="scroll-buttons">
|
<div class="scroll-buttons">
|
||||||
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
|
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
|
||||||
@@ -475,7 +531,7 @@
|
|||||||
<i class="fas fa-chevron-down"></i>
|
<i class="fas fa-chevron-down"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notification component is now in base.html (use id="notification") -->
|
<!-- Notification component is now in base.html (use id="notification") -->
|
||||||
<div id="notification" class="notification"></div>
|
<div id="notification" class="notification"></div>
|
||||||
<!-- 重置确认模态框 -->
|
<!-- 重置确认模态框 -->
|
||||||
@@ -500,7 +556,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 验证确认模态框移到 resetModal 外部,避免嵌套导致显示异常 -->
|
<!-- 验证确认模态框 -->
|
||||||
<div id="verifyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
<div id="verifyModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||||
<div class="bg-white rounded-lg p-6 shadow-xl max-w-md w-full animate-fade-in">
|
<div class="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">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@@ -522,7 +578,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作结果模态框 -->
|
<!-- 操作结果模态框 -->
|
||||||
<div id="resultModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
<div id="resultModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||||
<div class="bg-white rounded-2xl p-0 shadow-2xl max-w-lg w-full animate-fade-in border border-gray-200">
|
<div class="bg-white rounded-2xl p-0 shadow-2xl max-w-lg w-full animate-fade-in border border-gray-200">
|
||||||
@@ -537,8 +593,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-8 pb-2 w-full">
|
<div class="px-8 pb-2 w-full">
|
||||||
<div id="resultModalMessage"
|
<div id="resultModalMessage"
|
||||||
class="text-gray-700 text-base leading-relaxed break-all whitespace-pre-line max-h-60 overflow-y-auto border border-gray-100 rounded-lg bg-gray-50 p-4 shadow-inner"
|
class="text-gray-700 text-base leading-relaxed break-words whitespace-pre-line max-h-80 overflow-y-auto border border-gray-100 rounded-lg bg-gray-50 p-4 shadow-inner"
|
||||||
style="font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';">
|
style="font-family: 'JetBrains Mono', 'Fira Mono', 'Consolas', 'monospace';">
|
||||||
|
<!-- Content is dynamically generated by JS -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center px-8 pb-6 pt-2">
|
<div class="flex justify-center px-8 pb-6 pt-2">
|
||||||
@@ -596,71 +653,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer is now in base.html -->
|
<!-- Footer is now in base.html -->
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body_scripts %}
|
{% block body_scripts %}
|
||||||
<script>
|
<script>
|
||||||
// keys_status.html specific JavaScript initialization
|
// keys_status.html specific JavaScript initialization is now handled by keys_status.js
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
// The DOMContentLoaded listener in keys_status.js will execute after the DOM is ready.
|
||||||
// Filter functionality based on fail count threshold
|
// No inline script needed here anymore.
|
||||||
const thresholdInput = document.getElementById('failCountThreshold');
|
|
||||||
const validKeysList = document.getElementById('validKeys');
|
|
||||||
|
|
||||||
function filterValidKeys() {
|
|
||||||
const threshold = parseInt(thresholdInput.value, 10);
|
|
||||||
if (isNaN(threshold)) return; // Do nothing if input is not a number
|
|
||||||
|
|
||||||
const keys = validKeysList.querySelectorAll('li');
|
|
||||||
let visibleCount = 0;
|
|
||||||
keys.forEach(keyItem => {
|
|
||||||
// Check if it's a key item (has data-fail-count) before processing
|
|
||||||
if (keyItem.hasAttribute('data-fail-count')) {
|
|
||||||
const failCount = parseInt(keyItem.getAttribute('data-fail-count'), 10);
|
|
||||||
if (failCount >= threshold) {
|
|
||||||
keyItem.style.display = ''; // Show item
|
|
||||||
visibleCount++;
|
|
||||||
} else {
|
|
||||||
keyItem.style.display = 'none'; // Hide item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Optional: Show a message if no keys match the filter
|
|
||||||
const noMatchMsgId = 'no-valid-keys-msg';
|
|
||||||
let noMatchMsg = validKeysList.querySelector(`#${noMatchMsgId}`);
|
|
||||||
if (visibleCount === 0 && keys.length > 0) { // Only show if there were keys initially
|
|
||||||
if (!noMatchMsg) {
|
|
||||||
noMatchMsg = document.createElement('li');
|
|
||||||
noMatchMsg.id = noMatchMsgId;
|
|
||||||
noMatchMsg.className = 'text-center text-gray-500 py-4';
|
|
||||||
noMatchMsg.textContent = '没有符合条件的有效密钥';
|
|
||||||
validKeysList.appendChild(noMatchMsg);
|
|
||||||
}
|
|
||||||
noMatchMsg.style.display = '';
|
|
||||||
} else if (noMatchMsg) {
|
|
||||||
noMatchMsg.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thresholdInput && validKeysList) {
|
|
||||||
thresholdInput.addEventListener('input', filterValidKeys);
|
|
||||||
// Initial filter on load
|
|
||||||
filterValidKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize other elements or event listeners if needed
|
|
||||||
// The main logic (verifyKey, resetKeyFailCount, copyKey, etc.) is in keys_status.js
|
|
||||||
// The toggleSection logic is now specific to this page
|
|
||||||
window.toggleSection = function(header, sectionId) {
|
|
||||||
const toggleIcon = header.querySelector('.toggle-icon');
|
|
||||||
const content = header.nextElementSibling; // Assumes content is immediately after header
|
|
||||||
if (toggleIcon && content) {
|
|
||||||
toggleIcon.classList.toggle('collapsed');
|
|
||||||
content.classList.toggle('collapsed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -6,9 +6,19 @@ import re
|
|||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
|
from pathlib import Path
|
||||||
|
import logging # Import logging
|
||||||
|
|
||||||
from app.core.constants import DATA_URL_PATTERN, IMAGE_URL_PATTERN, VALID_IMAGE_RATIOS
|
from app.core.constants import DATA_URL_PATTERN, IMAGE_URL_PATTERN, VALID_IMAGE_RATIOS
|
||||||
|
|
||||||
|
# Define logger for helper functions if needed, or use specific loggers
|
||||||
|
helper_logger = logging.getLogger("app.utils") # Or use a more specific logger if available
|
||||||
|
|
||||||
|
# Define project root and version file path here for get_current_version
|
||||||
|
# Assuming this file is at app/utils/helpers.py
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
VERSION_FILE_PATH = PROJECT_ROOT / "VERSION"
|
||||||
|
|
||||||
|
|
||||||
def extract_mime_type_and_data(base64_string: str) -> Tuple[Optional[str], str]:
|
def extract_mime_type_and_data(base64_string: str) -> Tuple[Optional[str], str]:
|
||||||
"""
|
"""
|
||||||
@@ -146,3 +156,21 @@ def is_valid_api_key(key: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_version(default_version: str = "0.0.0") -> str:
|
||||||
|
"""Reads the current version from the VERSION file."""
|
||||||
|
version_file = VERSION_FILE_PATH # Use Path object defined above
|
||||||
|
try:
|
||||||
|
# Use Path object's open method
|
||||||
|
with version_file.open('r', encoding='utf-8') as f:
|
||||||
|
version = f.read().strip()
|
||||||
|
if not version:
|
||||||
|
helper_logger.warning(f"VERSION file ('{version_file}') is empty. Using default version '{default_version}'.")
|
||||||
|
return default_version
|
||||||
|
return version
|
||||||
|
except FileNotFoundError:
|
||||||
|
helper_logger.warning(f"VERSION file not found at '{version_file}'. Using default version '{default_version}'.")
|
||||||
|
return default_version
|
||||||
|
except IOError as e:
|
||||||
|
helper_logger.error(f"Error reading VERSION file ('{version_file}'): {e}. Using default version '{default_version}'.")
|
||||||
|
return default_version
|
||||||
|
|||||||
Reference in New Issue
Block a user