mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-08-25 02:00:36 +08:00
refactor(ui): 优化无效密钥列表头部布局,使“全选”组件右对齐
这个消息表明了以下几点:
1. **类型 (Type)**: `refactor` - 这是一次重构,主要改进了现有用户界面元素的布局,而不是添加新功能或修复错误。
2. **范围 (Scope)**: `ui` - 表明更改影响的是用户界面部分。
3. **主题 (Subject)**:
* `优化无效密钥列表头部布局`: 指出更改的具体位置是“无效密钥列表”的头部区域,并且是对其布局的优化。
* `使“全选”组件右对齐`: 明确了主要的视觉变化是将“全选”复选框及其标签对齐到该区域的右侧。
This commit is contained in:
@@ -172,6 +172,10 @@ app/
|
|||||||
| `TIME_OUT` | 可选,请求超时时间 (秒) | `300` |
|
| `TIME_OUT` | 可选,请求超时时间 (秒) | `300` |
|
||||||
| `PROXIES` | 可选,代理服务器列表 (例如 `http://user:pass@host:port`, `socks5://host:port`) | `[]` |
|
| `PROXIES` | 可选,代理服务器列表 (例如 `http://user:pass@host:port`, `socks5://host:port`) | `[]` |
|
||||||
| `LOG_LEVEL` | 可选,日志级别,例如 DEBUG, INFO, WARNING, ERROR, CRITICAL | `INFO` |
|
| `LOG_LEVEL` | 可选,日志级别,例如 DEBUG, INFO, WARNING, ERROR, CRITICAL | `INFO` |
|
||||||
|
| `AUTO_DELETE_ERROR_LOGS_ENABLED` | 可选,是否开启自动删除错误日志 | `true` |
|
||||||
|
| `AUTO_DELETE_ERROR_LOGS_DAYS` | 可选,自动删除多少天前的错误日志 (例如 1, 7, 30) | `7` |
|
||||||
|
| `AUTO_DELETE_REQUEST_LOGS_ENABLED`| 可选,是否开启自动删除请求日志 | `false` |
|
||||||
|
| `AUTO_DELETE_REQUEST_LOGS_DAYS` | 可选,自动删除多少天前的请求日志 (例如 1, 7, 30) | `30` |
|
||||||
| `SAFETY_SETTINGS` | 可选,安全设置 (JSON 字符串格式),用于配置内容安全阈值。示例值可能需要根据实际模型支持情况调整。 | `[{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}]` |
|
| `SAFETY_SETTINGS` | 可选,安全设置 (JSON 字符串格式),用于配置内容安全阈值。示例值可能需要根据实际模型支持情况调整。 | `[{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}]` |
|
||||||
| **图像生成相关** | | |
|
| **图像生成相关** | | |
|
||||||
| `PAID_KEY` | 可选,付费版API Key,用于图片生成等高级功能 | `your-paid-api-key` |
|
| `PAID_KEY` | 可选,付费版API Key,用于图片生成等高级功能 | `your-paid-api-key` |
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# app/service/stats_service.py
|
# app/service/stats_service.py
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
from sqlalchemy import select, func
|
|
||||||
|
from sqlalchemy import and_, case, func, or_, select
|
||||||
|
|
||||||
from app.database.connection import database
|
from app.database.connection import database
|
||||||
from app.database.models import RequestLog
|
from app.database.models import RequestLog
|
||||||
@@ -13,66 +14,129 @@ logger = get_stats_logger()
|
|||||||
class StatsService:
|
class StatsService:
|
||||||
"""Service class for handling statistics related operations."""
|
"""Service class for handling statistics related operations."""
|
||||||
|
|
||||||
async def get_calls_in_last_seconds(self, seconds: int) -> int:
|
async def get_calls_in_last_seconds(self, seconds: int) -> dict[str, int]:
|
||||||
"""获取过去 N 秒内的调用次数 (包括成功和失败)"""
|
"""获取过去 N 秒内的调用次数 (总数、成功、失败)"""
|
||||||
try:
|
try:
|
||||||
cutoff_time = datetime.datetime.now() - datetime.timedelta(seconds=seconds)
|
cutoff_time = datetime.datetime.now() - datetime.timedelta(seconds=seconds)
|
||||||
query = select(func.count(RequestLog.id)).where(
|
query = select(
|
||||||
RequestLog.request_time >= cutoff_time
|
func.count(RequestLog.id).label("total"),
|
||||||
)
|
func.sum(
|
||||||
count_result = await database.fetch_one(query)
|
case(
|
||||||
return count_result[0] if count_result else 0
|
(
|
||||||
|
and_(
|
||||||
|
RequestLog.status_code >= 200,
|
||||||
|
RequestLog.status_code < 300,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
).label("success"),
|
||||||
|
func.sum(
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
or_(
|
||||||
|
RequestLog.status_code < 200,
|
||||||
|
RequestLog.status_code >= 300,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(RequestLog.status_code == None, 1), # type: ignore
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
).label("failure"),
|
||||||
|
).where(RequestLog.request_time >= cutoff_time)
|
||||||
|
result = await database.fetch_one(query)
|
||||||
|
if result:
|
||||||
|
return {
|
||||||
|
"total": result["total"] or 0,
|
||||||
|
"success": result["success"] or 0,
|
||||||
|
"failure": result["failure"] or 0,
|
||||||
|
}
|
||||||
|
return {"total": 0, "success": 0, "failure": 0}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get calls in last {seconds} seconds: {e}")
|
logger.error(f"Failed to get calls in last {seconds} seconds: {e}")
|
||||||
return 0 # Return 0 on error
|
return {"total": 0, "success": 0, "failure": 0}
|
||||||
|
|
||||||
async def get_calls_in_last_minutes(self, minutes: int) -> int:
|
async def get_calls_in_last_minutes(self, minutes: int) -> dict[str, int]:
|
||||||
"""获取过去 N 分钟内的调用次数 (包括成功和失败)"""
|
"""获取过去 N 分钟内的调用次数 (总数、成功、失败)"""
|
||||||
return await self.get_calls_in_last_seconds(minutes * 60)
|
return await self.get_calls_in_last_seconds(minutes * 60)
|
||||||
|
|
||||||
async def get_calls_in_last_hours(self, hours: int) -> int:
|
async def get_calls_in_last_hours(self, hours: int) -> dict[str, int]:
|
||||||
"""获取过去 N 小时内的调用次数 (包括成功和失败)"""
|
"""获取过去 N 小时内的调用次数 (总数、成功、失败)"""
|
||||||
return await self.get_calls_in_last_seconds(hours * 3600)
|
return await self.get_calls_in_last_seconds(hours * 3600)
|
||||||
|
|
||||||
async def get_calls_in_current_month(self) -> int:
|
async def get_calls_in_current_month(self) -> dict[str, int]:
|
||||||
"""获取当前自然月内的调用次数 (包括成功和失败)"""
|
"""获取当前自然月内的调用次数 (总数、成功、失败)"""
|
||||||
try:
|
try:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
start_of_month = now.replace(
|
||||||
query = select(func.count(RequestLog.id)).where(
|
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||||
RequestLog.request_time >= start_of_month
|
|
||||||
)
|
)
|
||||||
count_result = await database.fetch_one(query)
|
query = select(
|
||||||
return count_result[0] if count_result else 0
|
func.count(RequestLog.id).label("total"),
|
||||||
|
func.sum(
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
and_(
|
||||||
|
RequestLog.status_code >= 200,
|
||||||
|
RequestLog.status_code < 300,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
).label("success"),
|
||||||
|
func.sum(
|
||||||
|
case(
|
||||||
|
(
|
||||||
|
or_(
|
||||||
|
RequestLog.status_code < 200,
|
||||||
|
RequestLog.status_code >= 300,
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(RequestLog.status_code == None, 1), # type: ignore
|
||||||
|
else_=0,
|
||||||
|
)
|
||||||
|
).label("failure"),
|
||||||
|
).where(RequestLog.request_time >= start_of_month)
|
||||||
|
result = await database.fetch_one(query)
|
||||||
|
if result:
|
||||||
|
return {
|
||||||
|
"total": result["total"] or 0,
|
||||||
|
"success": result["success"] or 0,
|
||||||
|
"failure": result["failure"] or 0,
|
||||||
|
}
|
||||||
|
return {"total": 0, "success": 0, "failure": 0}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get calls in current month: {e}")
|
logger.error(f"Failed to get calls in current month: {e}")
|
||||||
return 0 # Return 0 on error
|
return {"total": 0, "success": 0, "failure": 0}
|
||||||
|
|
||||||
async def get_api_usage_stats(self) -> dict:
|
async def get_api_usage_stats(self) -> dict:
|
||||||
"""获取所有需要的 API 使用统计数据"""
|
"""获取所有需要的 API 使用统计数据 (总数、成功、失败)"""
|
||||||
try:
|
try:
|
||||||
calls_1m = await self.get_calls_in_last_minutes(1)
|
stats_1m = await self.get_calls_in_last_minutes(1)
|
||||||
calls_1h = await self.get_calls_in_last_hours(1)
|
stats_1h = await self.get_calls_in_last_hours(1)
|
||||||
calls_24h = await self.get_calls_in_last_hours(24)
|
stats_24h = await self.get_calls_in_last_hours(24)
|
||||||
calls_month = await self.get_calls_in_current_month()
|
stats_month = await self.get_calls_in_current_month()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"calls_1m": calls_1m,
|
"calls_1m": stats_1m,
|
||||||
"calls_1h": calls_1h,
|
"calls_1h": stats_1h,
|
||||||
"calls_24h": calls_24h,
|
"calls_24h": stats_24h,
|
||||||
"calls_month": calls_month,
|
"calls_month": stats_month,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get API usage stats: {e}")
|
logger.error(f"Failed to get API usage stats: {e}")
|
||||||
# Return default values on error
|
default_stat = {"total": 0, "success": 0, "failure": 0}
|
||||||
return {
|
return {
|
||||||
"calls_1m": 0,
|
"calls_1m": default_stat.copy(),
|
||||||
"calls_1h": 0,
|
"calls_1h": default_stat.copy(),
|
||||||
"calls_24h": 0,
|
"calls_24h": default_stat.copy(),
|
||||||
"calls_month": 0,
|
"calls_month": default_stat.copy(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def get_api_call_details(self, period: str) -> list[dict]:
|
async def get_api_call_details(self, period: str) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
获取指定时间段内的 API 调用详情
|
获取指定时间段内的 API 调用详情
|
||||||
@@ -87,40 +151,48 @@ class StatsService:
|
|||||||
ValueError: 如果 period 无效
|
ValueError: 如果 period 无效
|
||||||
"""
|
"""
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
if period == '1m':
|
if period == "1m":
|
||||||
start_time = now - datetime.timedelta(minutes=1)
|
start_time = now - datetime.timedelta(minutes=1)
|
||||||
elif period == '1h':
|
elif period == "1h":
|
||||||
start_time = now - datetime.timedelta(hours=1)
|
start_time = now - datetime.timedelta(hours=1)
|
||||||
elif period == '24h':
|
elif period == "24h":
|
||||||
start_time = now - datetime.timedelta(hours=24)
|
start_time = now - datetime.timedelta(hours=24)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"无效的时间段标识: {period}")
|
raise ValueError(f"无效的时间段标识: {period}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = select(
|
query = (
|
||||||
RequestLog.request_time.label("timestamp"),
|
select(
|
||||||
RequestLog.api_key.label("key"),
|
RequestLog.request_time.label("timestamp"),
|
||||||
RequestLog.model_name.label("model"),
|
RequestLog.api_key.label("key"),
|
||||||
RequestLog.status_code # We might need to map this to 'success'/'failure' later
|
RequestLog.model_name.label("model"),
|
||||||
).where(
|
RequestLog.status_code, # We might need to map this to 'success'/'failure' later
|
||||||
RequestLog.request_time >= start_time
|
)
|
||||||
).order_by(RequestLog.request_time.desc()) # Order by most recent first
|
.where(RequestLog.request_time >= start_time)
|
||||||
|
.order_by(RequestLog.request_time.desc())
|
||||||
|
) # Order by most recent first
|
||||||
|
|
||||||
results = await database.fetch_all(query)
|
results = await database.fetch_all(query)
|
||||||
|
|
||||||
# Convert results to list of dicts and map status_code
|
# Convert results to list of dicts and map status_code
|
||||||
details = []
|
details = []
|
||||||
for row in results:
|
for row in results:
|
||||||
status = 'failure' # 默认状态为 failure,如果 status_code 有效且在 200-299 范围内则更新为 success
|
status = "failure" # 默认状态为 failure,如果 status_code 有效且在 200-299 范围内则更新为 success
|
||||||
if row['status_code'] is not None: # 检查 status_code 是否为空
|
if row["status_code"] is not None: # 检查 status_code 是否为空
|
||||||
status = 'success' if 200 <= row['status_code'] < 300 else 'failure'
|
status = "success" if 200 <= row["status_code"] < 300 else "failure"
|
||||||
details.append({
|
details.append(
|
||||||
"timestamp": row['timestamp'].isoformat(), # Use ISO format for JS compatibility
|
{
|
||||||
"key": row['key'],
|
"timestamp": row[
|
||||||
"model": row['model'],
|
"timestamp"
|
||||||
"status": status
|
].isoformat(), # Use ISO format for JS compatibility
|
||||||
})
|
"key": row["key"],
|
||||||
logger.info(f"Retrieved {len(details)} API call details for period '{period}'")
|
"model": row["model"],
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Retrieved {len(details)} API call details for period '{period}'"
|
||||||
|
)
|
||||||
return details
|
return details
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -140,35 +212,44 @@ class StatsService:
|
|||||||
如果查询出错或没有找到记录,可能返回 None 或空字典。
|
如果查询出错或没有找到记录,可能返回 None 或空字典。
|
||||||
Example: {"gemini-pro": 10, "gemini-1.5-pro-latest": 5}
|
Example: {"gemini-pro": 10, "gemini-1.5-pro-latest": 5}
|
||||||
"""
|
"""
|
||||||
logger.info(f"Fetching usage details for key ending in ...{key[-4:]} for the last 24h.")
|
logger.info(
|
||||||
|
f"Fetching usage details for key ending in ...{key[-4:]} for the last 24h."
|
||||||
|
)
|
||||||
cutoff_time = datetime.datetime.now() - datetime.timedelta(hours=24)
|
cutoff_time = datetime.datetime.now() - datetime.timedelta(hours=24)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
query = select(
|
query = (
|
||||||
RequestLog.model_name,
|
select(
|
||||||
func.count(RequestLog.id).label("call_count")
|
RequestLog.model_name, func.count(RequestLog.id).label("call_count")
|
||||||
).where(
|
)
|
||||||
RequestLog.api_key == key,
|
.where(
|
||||||
RequestLog.request_time >= cutoff_time,
|
RequestLog.api_key == key,
|
||||||
RequestLog.model_name.isnot(None) # Ensure model_name is not null
|
RequestLog.request_time >= cutoff_time,
|
||||||
).group_by(
|
RequestLog.model_name.isnot(None), # Ensure model_name is not null
|
||||||
RequestLog.model_name
|
)
|
||||||
).order_by(
|
.group_by(RequestLog.model_name)
|
||||||
func.count(RequestLog.id).desc() # Order by count descending
|
.order_by(func.count(RequestLog.id).desc()) # Order by count descending
|
||||||
)
|
)
|
||||||
|
|
||||||
results = await database.fetch_all(query)
|
results = await database.fetch_all(query)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
logger.info(f"No usage details found for key ending in ...{key[-4:]} in the last 24h.")
|
logger.info(
|
||||||
return {} # Return empty dict if no records found
|
f"No usage details found for key ending in ...{key[-4:]} in the last 24h."
|
||||||
|
)
|
||||||
|
return {} # Return empty dict if no records found
|
||||||
|
|
||||||
usage_details = {row['model_name']: row['call_count'] for row in results}
|
usage_details = {row["model_name"]: row["call_count"] for row in results}
|
||||||
logger.info(f"Successfully fetched usage details for key ending in ...{key[-4:]}: {usage_details}")
|
logger.info(
|
||||||
|
f"Successfully fetched usage details for key ending in ...{key[-4:]}: {usage_details}"
|
||||||
|
)
|
||||||
return usage_details
|
return usage_details
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get key usage details for key ending in ...{key[-4:]}: {e}", exc_info=True)
|
logger.error(
|
||||||
|
f"Failed to get key usage details for key ending in ...{key[-4:]}: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
# Depending on requirements, you might return None or raise the exception
|
# Depending on requirements, you might return None or raise the exception
|
||||||
# Raising allows the route handler to return a 500 error.
|
# Raising allows the route handler to return a 500 error.
|
||||||
raise # Re-raise the exception
|
raise # Re-raise the exception
|
||||||
|
|||||||
@@ -1070,7 +1070,7 @@ function createArrayInput(key, value, isSensitive, modelId = null) {
|
|||||||
input.type = "text";
|
input.type = "text";
|
||||||
input.name = `${key}[]`; // Used for form submission if not handled by JS
|
input.name = `${key}[]`; // Used for form submission if not handled by JS
|
||||||
input.value = value;
|
input.value = value;
|
||||||
let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none`;
|
let inputClasses = `${ARRAY_INPUT_CLASS} flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none form-input-themed`;
|
||||||
if (isSensitive) {
|
if (isSensitive) {
|
||||||
inputClasses += ` ${SENSITIVE_INPUT_CLASS}`;
|
inputClasses += ` ${SENSITIVE_INPUT_CLASS}`;
|
||||||
}
|
}
|
||||||
@@ -1153,7 +1153,10 @@ function addArrayItemWithValue(key, value) {
|
|||||||
|
|
||||||
const inputWrapper = document.createElement("div");
|
const inputWrapper = document.createElement("div");
|
||||||
inputWrapper.className =
|
inputWrapper.className =
|
||||||
"flex items-center flex-grow border border-gray-300 rounded-md focus-within:border-primary-500 focus-within:ring focus-within:ring-primary-200 focus-within:ring-opacity-50";
|
"flex items-center flex-grow rounded-md focus-within:border-violet-400 focus-within:ring focus-within:ring-violet-400 focus-within:ring-opacity-50";
|
||||||
|
// Apply themed border directly via style, and ensure it has a border
|
||||||
|
inputWrapper.style.border = "1px solid rgba(120, 100, 200, 0.5)";
|
||||||
|
inputWrapper.style.backgroundColor = "transparent"; // Ensure wrapper is transparent
|
||||||
|
|
||||||
const input = createArrayInput(
|
const input = createArrayInput(
|
||||||
key,
|
key,
|
||||||
@@ -1682,7 +1685,7 @@ function addSafetySettingItem(category = "", threshold = "") {
|
|||||||
|
|
||||||
const categorySelect = document.createElement("select");
|
const categorySelect = document.createElement("select");
|
||||||
categorySelect.className =
|
categorySelect.className =
|
||||||
"safety-category-select flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white";
|
"safety-category-select flex-grow px-3 py-2 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-select-themed";
|
||||||
harmCategories.forEach((cat) => {
|
harmCategories.forEach((cat) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = cat;
|
option.value = cat;
|
||||||
@@ -1693,7 +1696,7 @@ function addSafetySettingItem(category = "", threshold = "") {
|
|||||||
|
|
||||||
const thresholdSelect = document.createElement("select");
|
const thresholdSelect = document.createElement("select");
|
||||||
thresholdSelect.className =
|
thresholdSelect.className =
|
||||||
"safety-threshold-select w-48 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white";
|
"safety-threshold-select w-48 px-3 py-2 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 form-select-themed";
|
||||||
harmThresholds.forEach((thr) => {
|
harmThresholds.forEach((thr) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = thr;
|
option.value = thr;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,316 +1,356 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{% block title %}Gemini Balance{% endblock %}</title>
|
<title>{% block title %}Gemini Balance{% endblock %}</title>
|
||||||
<link rel="manifest" href="/static/manifest.json">
|
<link rel="manifest" href="/static/manifest.json" />
|
||||||
<meta name="theme-color" content="#4F46E5">
|
<meta name="theme-color" content="#4F46E5" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
<meta name="apple-mobile-web-app-title" content="GBalance">
|
<meta name="apple-mobile-web-app-title" content="GBalance" />
|
||||||
<link rel="icon" href="/static/icons/icon-192x192.png">
|
<link rel="icon" href="/static/icons/icon-192x192.png" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
<link
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||||
|
/>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
primary: {
|
primary: {
|
||||||
50: '#eef2ff',
|
50: "#eef2ff",
|
||||||
100: '#e0e7ff',
|
100: "#e0e7ff",
|
||||||
200: '#c7d2fe',
|
200: "#c7d2fe",
|
||||||
300: '#a5b4fc',
|
300: "#a5b4fc",
|
||||||
400: '#818cf8',
|
400: "#818cf8",
|
||||||
500: '#6366f1',
|
500: "#6366f1",
|
||||||
600: '#4f46e5',
|
600: "#4f46e5",
|
||||||
700: '#4338ca',
|
700: "#4338ca",
|
||||||
800: '#3730a3',
|
800: "#3730a3",
|
||||||
900: '#312e81',
|
900: "#312e81",
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
50: '#ecfdf5',
|
50: "#ecfdf5",
|
||||||
500: '#10b981',
|
500: "#10b981",
|
||||||
600: '#059669'
|
600: "#059669",
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
50: '#fef2f2',
|
50: "#fef2f2",
|
||||||
500: '#ef4444',
|
500: "#ef4444",
|
||||||
600: '#dc2626'
|
600: "#dc2626",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ["Inter", "sans-serif"],
|
||||||
mono: ['JetBrains Mono', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
|
mono: [
|
||||||
},
|
"JetBrains Mono",
|
||||||
animation: {
|
"SFMono-Regular",
|
||||||
'fade-in': 'fadeIn 0.5s ease-out',
|
"Menlo",
|
||||||
'slide-up': 'slideUp 0.5s ease-out',
|
"Monaco",
|
||||||
'slide-down': 'slideDown 0.5s ease-out',
|
"Consolas",
|
||||||
'shake': 'shake 0.5s ease-in-out',
|
"monospace",
|
||||||
'spin': 'spin 1s linear infinite',
|
],
|
||||||
},
|
},
|
||||||
keyframes: {
|
animation: {
|
||||||
fadeIn: {
|
"fade-in": "fadeIn 0.5s ease-out",
|
||||||
'0%': { opacity: '0' },
|
"slide-up": "slideUp 0.5s ease-out",
|
||||||
'100%': { opacity: '1' },
|
"slide-down": "slideDown 0.5s ease-out",
|
||||||
},
|
shake: "shake 0.5s ease-in-out",
|
||||||
slideUp: {
|
spin: "spin 1s linear infinite",
|
||||||
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
},
|
||||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
keyframes: {
|
||||||
},
|
fadeIn: {
|
||||||
slideDown: {
|
"0%": { opacity: "0" },
|
||||||
'0%': { transform: 'translateY(-20px)', opacity: '0' },
|
"100%": { opacity: "1" },
|
||||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
},
|
||||||
},
|
slideUp: {
|
||||||
shake: {
|
"0%": { transform: "translateY(20px)", opacity: "0" },
|
||||||
'0%, 100%': { transform: 'translateX(0)' },
|
"100%": { transform: "translateY(0)", opacity: "1" },
|
||||||
'25%': { transform: 'translateX(-5px)' },
|
},
|
||||||
'75%': { transform: 'translateX(5px)' },
|
slideDown: {
|
||||||
},
|
"0%": { transform: "translateY(-20px)", opacity: "0" },
|
||||||
spin: {
|
"100%": { transform: "translateY(0)", opacity: "1" },
|
||||||
'0%': { transform: 'rotate(0deg)' },
|
},
|
||||||
'100%': { transform: 'rotate(360deg)' },
|
shake: {
|
||||||
},
|
"0%, 100%": { transform: "translateX(0)" },
|
||||||
},
|
"25%": { transform: "translateX(-5px)" },
|
||||||
}
|
"75%": { transform: "translateX(5px)" },
|
||||||
}
|
},
|
||||||
}
|
spin: {
|
||||||
|
"0%": { transform: "rotate(0deg)" },
|
||||||
|
"100%": { transform: "rotate(360deg)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.glass-card {
|
.glass-card {
|
||||||
background: rgba(255, 255, 255, 0.85); /* Slightly increased opacity for better readability */
|
background: rgba(255, 255, 255, 0.85); /* Slightly increased opacity for better readability */
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18); /* Subtle border */
|
border: 1px solid rgba(255, 255, 255, 0.18); /* Subtle border */
|
||||||
}
|
}
|
||||||
.bg-gradient {
|
.bg-gradient {
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 50%, #EC4899 100%);
|
background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 50%, #EC4899 100%);
|
||||||
}
|
}
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: rgba(243, 244, 246, 0.8); /* bg-gray-100 with opacity */
|
background: rgba(243, 244, 246, 0.8); /* bg-gray-100 with opacity */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: rgba(79, 70, 229, 0.4); /* primary-600 with opacity */
|
background: rgba(79, 70, 229, 0.4); /* primary-600 with opacity */
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(79, 70, 229, 0.6); /* primary-600 with more opacity */
|
background: rgba(79, 70, 229, 0.6); /* primary-600 with more opacity */
|
||||||
}
|
}
|
||||||
/* Basic modal styles */
|
/* Basic modal styles */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0,0,0,0.5);
|
background-color: rgba(0,0,0,0.5);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
.modal.show {
|
.modal.show {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
/* Loading spinner */
|
/* Loading spinner */
|
||||||
.loading-spin {
|
.loading-spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from { transform: rotate(0deg); }
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
/* Notification */
|
/* Notification */
|
||||||
.notification {
|
.notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 5rem; /* Adjusted from bottom-20 */
|
bottom: 5rem; /* Adjusted from bottom-20 */
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
padding: 0.75rem 1.25rem; /* px-5 py-3 */
|
padding: 0.75rem 1.25rem; /* px-5 py-3 */
|
||||||
border-radius: 0.5rem; /* rounded-lg */
|
border-radius: 0.5rem; /* rounded-lg */
|
||||||
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: 1000; /* Increased z-index */
|
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;
|
||||||
}
|
}
|
||||||
.notification.show {
|
.notification.show {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
}
|
}
|
||||||
.notification.error {
|
.notification.error {
|
||||||
background-color: rgba(220, 38, 38, 0.8); /* danger-600 with opacity */
|
background-color: rgba(220, 38, 38, 0.8); /* danger-600 with opacity */
|
||||||
}
|
}
|
||||||
/* Scroll buttons */
|
/* Scroll buttons */
|
||||||
.scroll-buttons {
|
.scroll-buttons {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 1.25rem; /* right-5 */
|
right: 1.25rem; /* right-5 */
|
||||||
bottom: 5rem; /* bottom-20 */
|
bottom: 5rem; /* bottom-20 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem; /* gap-2 */
|
gap: 0.5rem; /* gap-2 */
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.scroll-button {
|
.scroll-button {
|
||||||
width: 2.5rem; /* w-10 */
|
width: 2.5rem; /* w-10 */
|
||||||
height: 2.5rem; /* h-10 */
|
height: 2.5rem; /* h-10 */
|
||||||
background-color: #4f46e5; /* bg-primary-600 */
|
background-color: #4f46e5; /* bg-primary-600 */
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 9999px; /* rounded-full */
|
border-radius: 9999px; /* rounded-full */
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); /* shadow-md */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
.scroll-button:hover {
|
.scroll-button:hover {
|
||||||
background-color: #4338ca; /* hover:bg-primary-700 */
|
background-color: #4338ca; /* hover:bg-primary-700 */
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* hover:shadow-lg */
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* hover:shadow-lg */
|
||||||
}
|
}
|
||||||
{% block head_extra_styles %}
|
{% block head_extra_styles %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</style>
|
</style>
|
||||||
{% block head_extra_scripts %}{% endblock %}
|
{% block head_extra_scripts %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gradient min-h-screen text-gray-800 pt-6 pb-16">
|
<body class="bg-gradient min-h-screen text-gray-800 pt-6 pb-16">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
<!-- 底部版权 -->
|
<!-- 底部版权 -->
|
||||||
<div class="fixed bottom-0 left-0 w-full py-3 bg-white bg-opacity-80 backdrop-blur-md text-center text-sm text-gray-600 border-t border-gray-200">
|
<div
|
||||||
© <span id="copyright-year"></span> by
|
class="fixed bottom-0 left-0 w-full py-3 bg-white bg-opacity-80 backdrop-blur-md text-center text-sm text-gray-800 border-t border-gray-200"
|
||||||
<a href="https://linux.do/u/snaily" target="_blank" class="text-primary-600 hover:text-primary-800 transition duration-300">
|
>
|
||||||
<img src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif" alt="snaily" class="inline-block w-5 h-5 rounded-full align-middle mr-1">snaily
|
© <span id="copyright-year"></span> by
|
||||||
</a> |
|
<a
|
||||||
<a href="https://github.com/snailyp/gemini-balance" target="_blank" class="text-primary-600 hover:text-primary-800 transition duration-300">
|
href="https://linux.do/u/snaily"
|
||||||
<i class="fab fa-github"></i> GitHub
|
target="_blank"
|
||||||
</a> |
|
class="text-primary-600 hover:text-primary-800 transition duration-300"
|
||||||
<a href="https://afdian.com/a/snaily" target="_blank" class="text-primary-600 hover:text-primary-800 transition duration-300">
|
>
|
||||||
<i class="fas fa-drumstick-bite text-yellow-600"></i> 给作者加鸡腿
|
<img
|
||||||
</a>
|
src="https://linux.do/user_avatar/linux.do/snaily/288/306510_2.gif"
|
||||||
<span class="mx-1">|</span>
|
alt="snaily"
|
||||||
<span class="text-xs text-yellow-600 font-semibold">
|
class="inline-block w-5 h-5 rounded-full align-middle mr-1"
|
||||||
<i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗
|
/>snaily
|
||||||
</span>
|
</a>
|
||||||
<span id="version-info-container" class="inline-block">
|
|
|
||||||
<!-- Version info will be loaded here by JavaScript -->
|
<a
|
||||||
</span>
|
href="https://github.com/snailyp/gemini-balance"
|
||||||
|
target="_blank"
|
||||||
|
class="text-primary-600 hover:text-primary-800 transition duration-300"
|
||||||
|
>
|
||||||
|
<i class="fab fa-github"></i> GitHub
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
<a
|
||||||
|
href="https://afdian.com/a/snaily"
|
||||||
|
target="_blank"
|
||||||
|
class="text-primary-600 hover:text-primary-800 transition duration-300"
|
||||||
|
>
|
||||||
|
<i class="fas fa-drumstick-bite text-yellow-600"></i> 给作者加鸡腿
|
||||||
|
</a>
|
||||||
|
<span class="mx-1">|</span>
|
||||||
|
<span class="text-xs text-yellow-600 font-semibold">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-1"></i>免费项目,谨防诈骗
|
||||||
|
</span>
|
||||||
|
<span id="version-info-container" class="inline-block">
|
||||||
|
<!-- Version info will be loaded here by JavaScript -->
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 通用JS -->
|
<!-- 通用JS -->
|
||||||
<script>
|
<script>
|
||||||
// 设置版权年份
|
// 设置版权年份
|
||||||
document.getElementById('copyright-year').textContent = new Date().getFullYear();
|
document.getElementById("copyright-year").textContent =
|
||||||
|
new Date().getFullYear();
|
||||||
|
|
||||||
// 滚动到顶部/底部函数 (如果页面需要)
|
// 滚动到顶部/底部函数 (如果页面需要)
|
||||||
function scrollToTop() {
|
function scrollToTop() {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
|
window.scrollTo({
|
||||||
|
top: document.body.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示通知
|
||||||
|
function showNotification(message, type = "success", duration = 3000) {
|
||||||
|
const notification =
|
||||||
|
document.getElementById("notification") ||
|
||||||
|
createNotificationElement();
|
||||||
|
if (!notification) return;
|
||||||
|
|
||||||
|
notification.textContent = message;
|
||||||
|
notification.className = "notification show"; // Reset classes
|
||||||
|
if (type === "error") {
|
||||||
|
notification.classList.add("error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示通知
|
// Clear previous timeout if exists
|
||||||
function showNotification(message, type = 'success', duration = 3000) {
|
if (notification.timeoutId) {
|
||||||
const notification = document.getElementById('notification') || createNotificationElement();
|
clearTimeout(notification.timeoutId);
|
||||||
if (!notification) return;
|
|
||||||
|
|
||||||
notification.textContent = message;
|
|
||||||
notification.className = 'notification show'; // Reset classes
|
|
||||||
if (type === 'error') {
|
|
||||||
notification.classList.add('error');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear previous timeout if exists
|
|
||||||
if (notification.timeoutId) {
|
|
||||||
clearTimeout(notification.timeoutId);
|
|
||||||
}
|
|
||||||
|
|
||||||
notification.timeoutId = setTimeout(() => {
|
|
||||||
notification.classList.remove('show');
|
|
||||||
// Optional: remove the element after fade out if dynamically created
|
|
||||||
// setTimeout(() => notification.remove(), 300);
|
|
||||||
}, duration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to create notification element if it doesn't exist
|
notification.timeoutId = setTimeout(() => {
|
||||||
function createNotificationElement() {
|
notification.classList.remove("show");
|
||||||
let notification = document.getElementById('notification');
|
// Optional: remove the element after fade out if dynamically created
|
||||||
if (!notification) {
|
// setTimeout(() => notification.remove(), 300);
|
||||||
notification = document.createElement('div');
|
}, duration);
|
||||||
notification.id = 'notification';
|
}
|
||||||
notification.className = 'notification';
|
|
||||||
document.body.appendChild(notification);
|
// Helper to create notification element if it doesn't exist
|
||||||
}
|
function createNotificationElement() {
|
||||||
return notification;
|
let notification = document.getElementById("notification");
|
||||||
|
if (!notification) {
|
||||||
|
notification = document.createElement("div");
|
||||||
|
notification.id = "notification";
|
||||||
|
notification.className = "notification";
|
||||||
|
document.body.appendChild(notification);
|
||||||
}
|
}
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
// 页面刷新带加载状态
|
// 页面刷新带加载状态
|
||||||
function refreshPage(button) {
|
function refreshPage(button) {
|
||||||
if (button) {
|
if (button) {
|
||||||
const icon = button.querySelector('i');
|
const icon = button.querySelector("i");
|
||||||
if (icon) {
|
if (icon) {
|
||||||
icon.classList.add('loading-spin');
|
icon.classList.add("loading-spin");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 300); // Short delay to show spinner
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300); // Short delay to show spinner
|
||||||
|
}
|
||||||
|
|
||||||
// --- Version Check ---
|
// --- Version Check ---
|
||||||
const versionInfoContainer = document.getElementById('version-info-container');
|
const versionInfoContainer = document.getElementById(
|
||||||
|
"version-info-container"
|
||||||
|
);
|
||||||
|
|
||||||
async function fetchVersionInfo() {
|
async function fetchVersionInfo() {
|
||||||
if (!versionInfoContainer) return;
|
if (!versionInfoContainer) return;
|
||||||
versionInfoContainer.innerHTML = '<span class="mx-1">|</span><span class="text-xs text-gray-400">检查更新中...</span>'; // Initial loading state
|
versionInfoContainer.innerHTML =
|
||||||
|
'<span class="mx-1">|</span><span class="text-xs text-gray-700">检查更新中...</span>'; // Initial loading state
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/version/check');
|
const response = await fetch("/api/version/check");
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
let versionHtml = `<span class="mx-1">|</span><span class="text-xs text-gray-500">v${data.current_version}</span>`;
|
let versionHtml = `<span class="mx-1">|</span><span class="text-xs text-gray-800">v${data.current_version}</span>`;
|
||||||
if (data.update_available) {
|
if (data.update_available) {
|
||||||
versionHtml += `
|
versionHtml += `
|
||||||
<span class="mx-1">|</span>
|
<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">
|
<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}
|
<i class="fas fa-arrow-up"></i> 新版本: v${data.latest_version}
|
||||||
</a>`;
|
</a>`;
|
||||||
} else if (data.error_message) {
|
} else if (data.error_message) {
|
||||||
versionHtml += `
|
versionHtml += `
|
||||||
<span class="mx-1">|</span>
|
<span class="mx-1">|</span>
|
||||||
<span class="text-xs text-red-500" title="${data.error_message}">更新检查失败</span>`;
|
<span class="text-xs text-red-500" title="${data.error_message}">更新检查失败</span>`;
|
||||||
} else {
|
} else {
|
||||||
versionHtml += `<span class="mx-1">|</span><span class="text-xs text-green-500">已是最新</span>`; // Indicate up-to-date
|
versionHtml += `<span class="mx-1">|</span><span class="text-xs text-green-500">已是最新</span>`; // Indicate up-to-date
|
||||||
}
|
}
|
||||||
versionInfoContainer.innerHTML = versionHtml;
|
versionInfoContainer.innerHTML = versionHtml;
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("Error fetching version info:", error);
|
||||||
console.error('Error fetching version info:', error);
|
versionInfoContainer.innerHTML = `<span class="mx-1">|</span><span class="text-xs text-red-500" title="无法连接到服务器或解析响应">更新检查失败</span>`;
|
||||||
versionInfoContainer.innerHTML = `<span class="mx-1">|</span><span class="text-xs text-red-500" title="无法连接到服务器或解析响应">更新检查失败</span>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch immediately on load
|
// Fetch immediately on load
|
||||||
fetchVersionInfo();
|
fetchVersionInfo();
|
||||||
|
|
||||||
// Fetch periodically (e.g., every hour)
|
|
||||||
setInterval(fetchVersionInfo, 3600000); // 3600000 ms = 1 hour
|
|
||||||
|
|
||||||
|
// 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>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,314 +1,636 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}错误日志管理 - Gemini Balance{%
|
||||||
|
endblock %} {% block head_extra_styles %}
|
||||||
{% block title %}错误日志管理 - Gemini Balance{% endblock %}
|
|
||||||
|
|
||||||
{% block head_extra_styles %}
|
|
||||||
<style>
|
<style>
|
||||||
/* error_logs.html specific styles */
|
/* error_logs.html specific styles */
|
||||||
/* Table styles */
|
.styled-table th {
|
||||||
.styled-table th {
|
position: sticky;
|
||||||
position: sticky;
|
top: 0;
|
||||||
top: 0;
|
background-color: rgba(80, 60, 160, 0.8); /* theming: table header bg */
|
||||||
background: #f3f4f6; /* bg-gray-100 */
|
color: #ffffff !important; /* theming: table header text, ensured light */
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
border-bottom: 1px solid rgba(120, 100, 200, 0.4);
|
||||||
|
}
|
||||||
|
.styled-table tbody tr:hover {
|
||||||
|
background-color: rgba(90, 70, 170, 0.4); /* theming: table row hover */
|
||||||
|
}
|
||||||
|
.styled-table td {
|
||||||
|
padding: 12px 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 250px;
|
||||||
|
color: #d1d5db; /* theming: table cell text (gray-300) */
|
||||||
|
border-bottom: 1px solid rgba(120, 100, 200, 0.2); /* theming: cell border */
|
||||||
|
}
|
||||||
|
.styled-table td:nth-child(4) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-view-details {
|
||||||
|
background-color: rgba(107, 70, 193, 0.4); /* theming */
|
||||||
|
color: #c4b5fd; /* theming */
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
border: 1px solid rgba(120, 100, 200, 0.6); /* theming */
|
||||||
|
}
|
||||||
|
.btn-view-details:hover {
|
||||||
|
background-color: rgba(120, 100, 200, 0.6); /* theming */
|
||||||
|
color: #ede9fe; /* theming */
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.search-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.styled-table tbody tr:hover {
|
}
|
||||||
background-color: #f9fafb; /* bg-gray-50 */
|
|
||||||
}
|
input[type="text"],
|
||||||
.styled-table td {
|
input[type="datetime-local"],
|
||||||
padding: 12px 20px;
|
select,
|
||||||
vertical-align: middle;
|
button {
|
||||||
white-space: nowrap;
|
height: 36px !important;
|
||||||
overflow: hidden;
|
}
|
||||||
text-overflow: ellipsis;
|
.form-input-themed,
|
||||||
max-width: 250px;
|
input[type="datetime-local"],
|
||||||
}
|
select#pageSize {
|
||||||
/* Ensure error log column does not wrap and remove max-width */
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
.styled-table td:nth-child(4) { /* Assuming error log is the 4th column */
|
border-color: rgba(120, 100, 200, 0.5) !important;
|
||||||
/* max-width: none; */
|
color: #ffffff !important;
|
||||||
white-space: nowrap;
|
}
|
||||||
}
|
.form-input-themed::placeholder,
|
||||||
.btn-view-details {
|
input[type="datetime-local"]::placeholder {
|
||||||
background-color: #eef2ff; /* primary-50 */
|
color: #a0aec0 !important;
|
||||||
color: #4f46e5; /* primary-600 */
|
}
|
||||||
padding: 6px 12px;
|
.form-input-themed:focus,
|
||||||
border-radius: 6px;
|
input[type="datetime-local"]:focus,
|
||||||
font-weight: 500;
|
select#pageSize:focus {
|
||||||
transition: all 0.2s ease-in-out;
|
border-color: #a78bfa !important;
|
||||||
border: 1px solid #c7d2fe; /* primary-200 */
|
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.4) !important;
|
||||||
}
|
}
|
||||||
.btn-view-details:hover {
|
select#pageSize {
|
||||||
background-color: #c7d2fe; /* primary-200 */
|
/* Styles from config_editor.html .form-select-themed, adapted for select#pageSize */
|
||||||
color: #4338ca; /* primary-700 */
|
background-color: rgba(60, 40, 130, 0.6) !important;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
border: 1px solid rgba(167, 139, 250, 0.7) !important;
|
||||||
}
|
color: #ffffff !important;
|
||||||
@media (max-width: 768px) {
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%23d8b4fe' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M6 8l4 4 4-4'/%3e%3c/svg%3e") !important;
|
||||||
.search-container {
|
appearance: none !important;
|
||||||
grid-template-columns: 1fr;
|
padding: 0.6rem 2.5rem 0.6rem 0.8rem !important;
|
||||||
}
|
background-repeat: no-repeat !important;
|
||||||
}
|
background-position: right 0.6rem center !important;
|
||||||
/* Modal styles are in base.html */
|
background-size: 1.5em 1.5em !important;
|
||||||
|
border-radius: 0.5rem !important;
|
||||||
/* 确保输入框和按钮高度一致 */
|
font-weight: 500 !important;
|
||||||
input[type="text"], input[type="datetime-local"], select, button {
|
height: 36px !important; /* Retain original height or use auto */
|
||||||
height: 36px !important;
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
|
||||||
}
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
/* 日期选择器样式优化 */
|
|
||||||
.date-range-container {
|
select#pageSize:focus {
|
||||||
display: flex;
|
border-color: #d8b4fe !important; /* violet-300 */
|
||||||
align-items: center;
|
box-shadow: 0 0 0 3px rgba(216, 180, 254, 0.4) !important; /* ring-violet-300 */
|
||||||
gap: 0.5rem;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 确保所有输入框在小屏幕上正确显示 */
|
select#pageSize option {
|
||||||
@media (max-width: 640px) {
|
background-color: rgba(76, 29, 149, 0.95) !important; /* 暗紫色背景 */
|
||||||
input[type="datetime-local"] {
|
color: #ffffff !important;
|
||||||
min-width: 0;
|
padding: 8px !important;
|
||||||
width: 100%;
|
}
|
||||||
}
|
|
||||||
|
.date-range-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
input[type="datetime-local"] {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
color: #e2e8f0 !important; /* Light gray/white for labels */
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 导航链接悬停样式 (从 config_editor.html 复制) */
|
||||||
|
.nav-link {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: rgba(120, 100, 200, 0.6) !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure text around pageSize select is light */
|
||||||
|
.pagination-text {
|
||||||
|
color: #e2e8f0 !important; /* Light gray/white for text */
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination custom styles */
|
||||||
|
.pagination li a, .pagination li span { /* Assuming 'span' might be used for non-clickable items like '...' */
|
||||||
|
display: flex; /* For centering content if icons are used */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5rem 0.75rem; /* Adjust padding as needed */
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #e2e8f0; /* Light gray/white text */
|
||||||
|
background-color: rgba(107, 70, 193, 0.4); /* Consistent with other buttons */
|
||||||
|
border: 1px solid rgba(120, 100, 200, 0.6); /* Consistent with other buttons */
|
||||||
|
border-radius: 0.375rem; /* Tailwind's rounded-md */
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
min-width: 36px; /* Ensure minimum width for small numbers */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination li a:hover, .pagination li span:hover:not(.disabled) { /* Avoid hover on disabled spans */
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: rgba(120, 100, 200, 0.6); /* Consistent with other button hovers */
|
||||||
|
border-color: rgba(167, 139, 250, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination li.active a, .pagination li.active span { /* Assuming 'active' class for current page */
|
||||||
|
color: #ffffff !important;
|
||||||
|
background-color: #7c3aed !important; /* Violet-600, ensure it overrides */
|
||||||
|
border-color: #7c3aed !important;
|
||||||
|
font-weight: 600; /* Make active page number bolder */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination li.disabled a, .pagination li.disabled span { /* Assuming 'disabled' class */
|
||||||
|
color: rgba(226, 232, 240, 0.6) !important;
|
||||||
|
background-color: rgba(80, 60, 160, 0.3) !important; /* Slightly more visible than pure disabled */
|
||||||
|
border-color: rgba(120, 100, 200, 0.4) !important;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %} {% block content %}
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div
|
||||||
|
class="rounded-2xl shadow-xl p-6 md:p-8"
|
||||||
|
style="
|
||||||
|
background-color: rgba(80, 60, 160, 0.3);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(150, 130, 230, 0.3);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="text-3xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-violet-400 to-pink-400 mb-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/static/icons/logo.png"
|
||||||
|
alt="Gemini Balance Logo"
|
||||||
|
class="h-9 inline-block align-middle mr-2"
|
||||||
|
/>
|
||||||
|
Gemini Balance - 错误日志
|
||||||
|
</h1>
|
||||||
|
|
||||||
{% block content %}
|
<!-- Navigation Tabs -->
|
||||||
<div class="container mx-auto px-4"> <!-- Removed max-width-7xl for wider content -->
|
<div class="flex justify-center mb-8 overflow-x-auto pb-2 gap-2">
|
||||||
<div class="glass-card rounded-2xl shadow-xl p-6 md:p-8">
|
<a
|
||||||
<!-- Removed refresh button from top right -->
|
href="/config"
|
||||||
|
class="nav-link whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg text-gray-200 hover:text-white transition-all duration-200"
|
||||||
<h1 class="text-3xl font-extrabold text-center text-transparent bg-clip-text bg-gradient-to-r from-primary-600 to-primary-700 mb-4">
|
style="background-color: rgba(107, 70, 193, 0.4)"
|
||||||
<img src="/static/icons/logo.png" alt="Gemini Balance Logo" class="h-9 inline-block align-middle mr-2">
|
>
|
||||||
Gemini Balance - 错误日志
|
<i class="fas fa-cog"></i> 配置编辑
|
||||||
</h1>
|
</a>
|
||||||
|
<a
|
||||||
<!-- Navigation Tabs -->
|
href="/keys"
|
||||||
<div class="flex justify-center mb-8 overflow-x-auto pb-2 gap-2">
|
class="nav-link whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg text-gray-200 hover:text-white transition-all duration-200"
|
||||||
<a href="/config" class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-white bg-opacity-50 hover:bg-opacity-70 text-gray-700 transition-all duration-200">
|
style="background-color: rgba(107, 70, 193, 0.4)"
|
||||||
<i class="fas fa-cog"></i> 配置编辑
|
>
|
||||||
</a>
|
<i class="fas fa-tachometer-alt"></i> 监控面板
|
||||||
<a href="/keys" class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-white bg-opacity-50 hover:bg-opacity-70 text-gray-700 transition-all duration-200">
|
</a>
|
||||||
<i class="fas fa-tachometer-alt"></i> 监控面板
|
<a
|
||||||
</a>
|
href="/logs"
|
||||||
<a href="/logs" class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-primary-600 text-white shadow-md">
|
class="whitespace-nowrap flex items-center justify-center gap-2 px-6 py-3 font-medium rounded-lg bg-violet-600 text-white shadow-md"
|
||||||
<i class="fas fa-exclamation-triangle"></i> 错误日志
|
>
|
||||||
</a>
|
<i class="fas fa-exclamation-triangle"></i> 错误日志
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
<!-- 主内容区域 -->
|
|
||||||
<div class="bg-white bg-opacity-70 rounded-xl p-6 shadow-lg animate-fade-in">
|
|
||||||
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
|
|
||||||
<i class="fas fa-bug text-primary-600"></i> 错误日志列表
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<!-- 控制区域 (Refresh button removed, page size moved below) -->
|
|
||||||
<!-- Removed the original controls div -->
|
|
||||||
|
|
||||||
<!-- 搜索与操作控件 -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-[1fr_auto] items-center gap-4 mb-6"> <!-- 修改为items-center -->
|
|
||||||
<!-- Left side: Search inputs and date range -->
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full"> <!-- 修改为3列布局 -->
|
|
||||||
<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="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">
|
|
||||||
<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">
|
|
||||||
<!-- 日期选择器单独一行 -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 表格容器 - Enhanced Styling -->
|
<!-- 主内容区域 -->
|
||||||
<div class="overflow-x-auto rounded-lg border border-gray-200 mb-6 bg-white"> <!-- Removed shadow, added border -->
|
<div
|
||||||
<table class="styled-table w-full min-w-full text-sm"> <!-- Added text-sm -->
|
class="rounded-xl p-6 shadow-lg animate-fade-in"
|
||||||
<thead>
|
style="
|
||||||
<tr class="bg-primary-50 text-left text-primary-800"> <!-- Changed header background and text color -->
|
background-color: rgba(70, 50, 150, 0.5);
|
||||||
<th class="px-3 py-3 font-semibold rounded-tl-lg w-12 text-center"> <!-- Adjusted padding and width -->
|
backdrop-filter: blur(5px);
|
||||||
<input type="checkbox" id="selectAllCheckbox" class="form-checkbox h-4 w-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500">
|
-webkit-backdrop-filter: blur(5px);
|
||||||
</th>
|
border: 1px solid rgba(120, 100, 200, 0.2);
|
||||||
<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>
|
<h2
|
||||||
<th class="px-5 py-3 font-semibold">Gemini密钥</th>
|
class="text-xl font-bold mb-6 pb-3 border-b flex items-center gap-2 text-gray-100 border-violet-300 border-opacity-30"
|
||||||
<th class="px-5 py-3 font-semibold">错误类型</th>
|
>
|
||||||
<th class="px-5 py-3 font-semibold">错误码</th>
|
<i class="fas fa-bug text-violet-400"></i> 错误日志列表
|
||||||
<th class="px-5 py-3 font-semibold">模型名称</th>
|
</h2>
|
||||||
<th class="px-5 py-3 font-semibold">请求时间</th>
|
|
||||||
<th class="px-5 py-3 font-semibold rounded-tr-lg text-center">操作</th> <!-- Adjusted rounding and centered -->
|
<!-- 搜索与操作控件 -->
|
||||||
</tr>
|
<div
|
||||||
</thead>
|
class="grid grid-cols-1 lg:grid-cols-[1fr_auto] items-center gap-4 mb-6"
|
||||||
<tbody id="errorLogsTable" class="divide-y divide-gray-200">
|
>
|
||||||
<!-- 错误日志数据将通过JavaScript动态加载 -->
|
<div
|
||||||
</tbody>
|
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 w-full"
|
||||||
</table>
|
>
|
||||||
</div>
|
<input
|
||||||
|
type="text"
|
||||||
<!-- 状态指示器 -->
|
id="keySearch"
|
||||||
<div id="loadingIndicator" class="flex items-center justify-center p-8 hidden">
|
placeholder="搜索密钥 (部分)"
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
class="px-3 py-1 rounded-lg border form-input-themed"
|
||||||
<p class="ml-4 text-lg text-gray-700 font-medium">加载中,请稍候...</p>
|
/>
|
||||||
</div>
|
<input
|
||||||
|
type="text"
|
||||||
<div id="noDataMessage" class="text-center py-12 text-gray-500 hidden">
|
id="errorSearch"
|
||||||
<i class="fas fa-inbox text-5xl mb-3"></i>
|
placeholder="搜索错误类型/日志"
|
||||||
<p class="text-lg">暂无错误日志数据</p>
|
class="px-3 py-1 rounded-lg border form-input-themed"
|
||||||
</div>
|
/>
|
||||||
|
<input
|
||||||
<div id="errorMessage" class="bg-danger-50 text-danger-600 p-4 rounded-lg font-medium text-center hidden">
|
type="text"
|
||||||
<i class="fas fa-exclamation-circle mr-2"></i>
|
id="errorCodeSearch"
|
||||||
加载错误日志失败,请稍后重试。
|
placeholder="搜索错误码"
|
||||||
</div>
|
class="px-3 py-1 rounded-lg border form-input-themed"
|
||||||
|
/>
|
||||||
<!-- 分页与每页显示控件 -->
|
<div
|
||||||
<div class="flex flex-col sm:flex-row justify-between items-center mt-6 gap-4">
|
class="grid grid-cols-1 sm:grid-cols-2 gap-2 col-span-1 sm:col-span-2 lg:col-span-3 mt-2"
|
||||||
<!-- 每页显示控件 (Moved here) -->
|
>
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-700">
|
<div class="flex items-center gap-2">
|
||||||
<label for="pageSize" class="font-medium">每页显示:</label>
|
<label class="text-sm text-gray-300 whitespace-nowrap"
|
||||||
<select id="pageSize" class="rounded-md border border-gray-300 focus:ring focus:ring-primary-200 focus:border-primary-500 px-2 py-1 bg-white text-sm">
|
>开始时间:</label
|
||||||
<option value="10">10</option>
|
>
|
||||||
<option value="20" selected>20</option>
|
<input
|
||||||
<option value="50">50</option>
|
type="datetime-local"
|
||||||
<option value="100">100</option>
|
id="startDate"
|
||||||
</select>
|
class="px-3 py-1 rounded-lg border text-sm w-full"
|
||||||
<span>条</span>
|
/>
|
||||||
</div>
|
|
||||||
<!-- 分页控件 -->
|
|
||||||
<div class="flex items-center gap-4"> <!-- Wrapper for pagination and input -->
|
|
||||||
<ul class="pagination flex items-center gap-1" id="pagination">
|
|
||||||
<!-- 分页控件将通过JavaScript动态加载 -->
|
|
||||||
</ul>
|
|
||||||
<!-- 页码输入跳转 -->
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<input type="number" id="pageInput" min="1" class="w-16 px-2 py-1 rounded-md border border-gray-300 text-sm focus:ring focus:ring-primary-200 focus:border-primary-500" placeholder="页码">
|
|
||||||
<button id="goToPageBtn" class="px-3 py-1 bg-primary-600 hover:bg-primary-700 text-white text-sm rounded-md transition">跳转</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<label class="text-sm text-gray-300 whitespace-nowrap"
|
||||||
|
>结束时间:</label
|
||||||
<!-- Scroll buttons are now in base.html -->
|
>
|
||||||
<div class="scroll-buttons">
|
<input
|
||||||
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
|
type="datetime-local"
|
||||||
<i class="fas fa-chevron-up"></i>
|
id="endDate"
|
||||||
</button>
|
class="px-3 py-1 rounded-lg border text-sm w-full"
|
||||||
<button class="scroll-button" onclick="scrollToBottom()" title="滚动到底部">
|
/>
|
||||||
<i class="fas fa-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Notification component is now in base.html (use id="notification") -->
|
|
||||||
<div id="notification" class="notification"></div>
|
|
||||||
<!-- Footer is now in base.html -->
|
|
||||||
|
|
||||||
<!-- 日志详情模态框 -->
|
|
||||||
<div id="logDetailModal" class="modal">
|
|
||||||
<div class="w-full max-w-6xl mx-auto bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in"> <!-- Increased max-width to 6xl -->
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex justify-between items-center border-b border-gray-200 pb-4 mb-4">
|
|
||||||
<h2 class="text-xl font-bold text-gray-800">错误日志详情</h2>
|
|
||||||
<button id="closeLogDetailModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4 max-h-[60vh] overflow-y-auto p-1">
|
|
||||||
<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>
|
|
||||||
<pre id="modalGeminiKey" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto"></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="modalGeminiKey" title="复制密钥">
|
|
||||||
<i class="far fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg relative group"> <!-- Added relative and group -->
|
|
||||||
<h6 class="text-sm font-semibold text-gray-600 mb-1">错误类型:</h6>
|
|
||||||
<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>
|
|
||||||
<pre id="modalErrorLog" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto whitespace-pre-wrap"></pre>
|
|
||||||
<button class="copy-btn absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 text-gray-600 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity" data-target="modalErrorLog" title="复制错误日志">
|
|
||||||
<i class="far fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg relative group">
|
|
||||||
<h6 class="text-sm font-semibold text-gray-600 mb-1">请求消息:</h6>
|
|
||||||
<pre id="modalRequestMsg" class="font-mono text-sm bg-gray-100 p-3 rounded overflow-x-auto whitespace-pre-wrap"></pre>
|
|
||||||
<button class="copy-btn absolute top-2 right-2 bg-gray-200 hover:bg-gray-300 text-gray-600 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity" data-target="modalRequestMsg" title="复制请求消息">
|
|
||||||
<i class="far fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg relative group"> <!-- Added relative and group -->
|
|
||||||
<h6 class="text-sm font-semibold text-gray-600 mb-1">模型名称:</h6>
|
|
||||||
<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 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="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 class="flex justify-end mt-6">
|
|
||||||
<button type="button" id="closeModalFooterBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-6 py-2 rounded-lg font-medium transition">关闭</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<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-violet-600 hover:bg-violet-700 text-white rounded-lg font-medium transition-all duration-200 shadow-sm hover:shadow-md whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<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-sky-600 hover:bg-sky-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"
|
||||||
|
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-red-600 hover:bg-red-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"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash-alt mr-1.5"></i>删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格容器 -->
|
||||||
|
<div
|
||||||
|
class="overflow-x-auto rounded-lg border mb-6"
|
||||||
|
style="border-color: rgba(120, 100, 200, 0.3)"
|
||||||
|
>
|
||||||
|
<table class="styled-table w-full min-w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-left">
|
||||||
|
<th
|
||||||
|
class="px-3 py-3 font-semibold rounded-tl-lg w-12 text-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="selectAllCheckbox"
|
||||||
|
class="form-checkbox h-4 w-4 text-violet-500 border-gray-500 rounded focus:ring-violet-500 bg-transparent"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th class="px-5 py-3 font-semibold cursor-pointer" id="sortById">
|
||||||
|
ID <i class="fas fa-sort ml-1"></i>
|
||||||
|
</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 rounded-tr-lg text-center">
|
||||||
|
操作
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody
|
||||||
|
id="errorLogsTable"
|
||||||
|
class="divide-y"
|
||||||
|
style="border-color: rgba(120, 100, 200, 0.2)"
|
||||||
|
>
|
||||||
|
<!-- 错误日志数据将通过JavaScript动态加载 -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 状态指示器 -->
|
||||||
|
<div
|
||||||
|
id="loadingIndicator"
|
||||||
|
class="flex items-center justify-center p-8 hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="animate-spin rounded-full h-12 w-12 border-b-2 border-violet-400"
|
||||||
|
></div>
|
||||||
|
<p class="ml-4 text-lg text-gray-300 font-medium">加载中,请稍候...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="noDataMessage" class="text-center py-12 text-gray-400 hidden">
|
||||||
|
<i class="fas fa-inbox text-5xl mb-3"></i>
|
||||||
|
<p class="text-lg">暂无错误日志数据</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="errorMessage"
|
||||||
|
class="p-4 rounded-lg font-medium text-center hidden"
|
||||||
|
style="background-color: rgba(220, 38, 38, 0.2); color: #fca5a5"
|
||||||
|
>
|
||||||
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
||||||
|
加载错误日志失败,请稍后重试。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页与每页显示控件 -->
|
||||||
|
<div
|
||||||
|
class="flex flex-col sm:flex-row justify-between items-center mt-6 gap-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-300">
|
||||||
|
<label for="pageSize" class="font-medium pagination-text"
|
||||||
|
>每页显示:</label
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id="pageSize"
|
||||||
|
class="rounded-md border focus:ring focus:border-violet-400 px-2 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="20" selected>20</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
<span class="pagination-text">条</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<ul class="pagination flex items-center gap-1" id="pagination">
|
||||||
|
<!-- 分页控件将通过JavaScript动态加载 -->
|
||||||
|
</ul>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="pageInput"
|
||||||
|
min="1"
|
||||||
|
class="w-16 px-2 py-1 rounded-md border text-sm focus:ring focus:border-violet-400 form-input-themed"
|
||||||
|
placeholder="页码"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="goToPageBtn"
|
||||||
|
class="px-3 py-1 bg-violet-600 hover:bg-violet-700 text-white text-sm rounded-md transition"
|
||||||
|
>
|
||||||
|
跳转
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scroll buttons are now in base.html -->
|
||||||
|
<div class="scroll-buttons">
|
||||||
|
<button class="scroll-button" onclick="scrollToTop()" title="回到顶部">
|
||||||
|
<i class="fas fa-chevron-up"></i>
|
||||||
|
</button>
|
||||||
|
<button class="scroll-button" onclick="scrollToBottom()" title="滚动到底部">
|
||||||
|
<i class="fas fa-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notification component is now in base.html (use id="notification") -->
|
||||||
|
<div id="notification" class="notification"></div>
|
||||||
|
<!-- Footer is now in base.html -->
|
||||||
|
|
||||||
|
<!-- 日志详情模态框 -->
|
||||||
|
<div id="logDetailModal" class="modal">
|
||||||
|
<div
|
||||||
|
class="w-full max-w-6xl mx-auto rounded-2xl shadow-2xl overflow-hidden animate-fade-in"
|
||||||
|
style="
|
||||||
|
background-color: rgba(70, 50, 150, 0.95);
|
||||||
|
color: #ffffff;
|
||||||
|
border: 1px solid rgba(120, 100, 200, 0.4);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="p-6">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center pb-4 mb-4"
|
||||||
|
style="border-bottom: 1px solid rgba(120, 100, 200, 0.4)"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-bold text-gray-100">错误日志详情</h2>
|
||||||
|
<button
|
||||||
|
id="closeLogDetailModalBtn"
|
||||||
|
class="text-gray-300 hover:text-gray-100 text-xl"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 max-h-[60vh] overflow-y-auto p-1">
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg relative group"
|
||||||
|
style="background-color: rgba(80, 60, 160, 0.3)"
|
||||||
|
>
|
||||||
|
<h6 class="text-sm font-semibold text-violet-200 mb-1">
|
||||||
|
Gemini密钥:
|
||||||
|
</h6>
|
||||||
|
<pre
|
||||||
|
id="modalGeminiKey"
|
||||||
|
class="font-mono text-sm p-3 rounded overflow-x-auto"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.2); color: #e5e7eb"
|
||||||
|
></pre>
|
||||||
|
<button
|
||||||
|
class="copy-btn absolute top-2 right-2 hover:bg-gray-600 text-gray-300 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.3)"
|
||||||
|
data-target="modalGeminiKey"
|
||||||
|
title="复制密钥"
|
||||||
|
>
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg relative group"
|
||||||
|
style="background-color: rgba(80, 60, 160, 0.3)"
|
||||||
|
>
|
||||||
|
<h6 class="text-sm font-semibold text-violet-200 mb-1">错误类型:</h6>
|
||||||
|
<p id="modalErrorType" class="text-red-300 font-medium pr-8"></p>
|
||||||
|
<button
|
||||||
|
class="copy-btn absolute top-2 right-2 hover:bg-gray-600 text-gray-300 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.3)"
|
||||||
|
data-target="modalErrorType"
|
||||||
|
title="复制错误类型"
|
||||||
|
>
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg relative group"
|
||||||
|
style="background-color: rgba(80, 60, 160, 0.3)"
|
||||||
|
>
|
||||||
|
<h6 class="text-sm font-semibold text-violet-200 mb-1">错误日志:</h6>
|
||||||
|
<pre
|
||||||
|
id="modalErrorLog"
|
||||||
|
class="font-mono text-sm p-3 rounded overflow-x-auto whitespace-pre-wrap"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.2); color: #e5e7eb"
|
||||||
|
></pre>
|
||||||
|
<button
|
||||||
|
class="copy-btn absolute top-2 right-2 hover:bg-gray-600 text-gray-300 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.3)"
|
||||||
|
data-target="modalErrorLog"
|
||||||
|
title="复制错误日志"
|
||||||
|
>
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg relative group"
|
||||||
|
style="background-color: rgba(80, 60, 160, 0.3)"
|
||||||
|
>
|
||||||
|
<h6 class="text-sm font-semibold text-violet-200 mb-1">请求消息:</h6>
|
||||||
|
<pre
|
||||||
|
id="modalRequestMsg"
|
||||||
|
class="font-mono text-sm p-3 rounded overflow-x-auto whitespace-pre-wrap"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.2); color: #e5e7eb"
|
||||||
|
></pre>
|
||||||
|
<button
|
||||||
|
class="copy-btn absolute top-2 right-2 hover:bg-gray-600 text-gray-300 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.3)"
|
||||||
|
data-target="modalRequestMsg"
|
||||||
|
title="复制请求消息"
|
||||||
|
>
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg relative group"
|
||||||
|
style="background-color: rgba(80, 60, 160, 0.3)"
|
||||||
|
>
|
||||||
|
<h6 class="text-sm font-semibold text-violet-200 mb-1">模型名称:</h6>
|
||||||
|
<p id="modalModelName" class="font-medium pr-8 text-gray-200"></p>
|
||||||
|
<button
|
||||||
|
class="copy-btn absolute top-2 right-2 hover:bg-gray-600 text-gray-300 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.3)"
|
||||||
|
data-target="modalModelName"
|
||||||
|
title="复制模型名称"
|
||||||
|
>
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="p-4 rounded-lg relative group"
|
||||||
|
style="background-color: rgba(80, 60, 160, 0.3)"
|
||||||
|
>
|
||||||
|
<h6 class="text-sm font-semibold text-violet-200 mb-1">请求时间:</h6>
|
||||||
|
<p id="modalRequestTime" class="font-medium pr-8 text-gray-200"></p>
|
||||||
|
<button
|
||||||
|
class="copy-btn absolute top-2 right-2 hover:bg-gray-600 text-gray-300 p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style="background-color: rgba(0, 0, 0, 0.3)"
|
||||||
|
data-target="modalRequestTime"
|
||||||
|
title="复制请求时间"
|
||||||
|
>
|
||||||
|
<i class="far fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex justify-end mt-6 pt-4"
|
||||||
|
style="border-top: 1px solid rgba(120, 100, 200, 0.4)"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="closeModalFooterBtn"
|
||||||
|
class="bg-gray-500 bg-opacity-50 hover:bg-opacity-70 text-gray-200 px-6 py-2 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 删除确认模态框 -->
|
<!-- 删除确认模态框 -->
|
||||||
<div id="deleteConfirmModal" class="modal">
|
<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
|
||||||
<div class="p-6">
|
class="w-full max-w-md mx-auto rounded-xl shadow-xl overflow-hidden animate-fade-in"
|
||||||
<div class="flex justify-between items-center border-b border-gray-200 pb-3 mb-4">
|
style="
|
||||||
<h2 class="text-lg font-semibold text-gray-800">确认删除</h2>
|
background-color: rgba(70, 50, 150, 0.95);
|
||||||
<button id="closeDeleteConfirmModalBtn" class="text-gray-400 hover:text-gray-600 text-xl">×</button>
|
color: #ffffff;
|
||||||
</div>
|
border: 1px solid rgba(120, 100, 200, 0.4);
|
||||||
<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>
|
<div class="p-6">
|
||||||
<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>
|
class="flex justify-between items-center pb-3 mb-4"
|
||||||
</div>
|
style="border-bottom: 1px solid rgba(120, 100, 200, 0.4)"
|
||||||
</div>
|
>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-100">确认删除</h2>
|
||||||
|
<button
|
||||||
|
id="closeDeleteConfirmModalBtn"
|
||||||
|
class="text-gray-300 hover:text-gray-100 text-xl"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p id="deleteConfirmMessage" class="text-gray-300 mb-6">
|
||||||
|
你确定要删除选中的项目吗?此操作不可恢复!
|
||||||
|
</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
id="cancelDeleteBtn"
|
||||||
|
type="button"
|
||||||
|
class="bg-gray-500 bg-opacity-50 hover:bg-opacity-70 text-gray-200 px-5 py-2 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="confirmDeleteBtn"
|
||||||
|
type="button"
|
||||||
|
class="bg-red-600 hover:bg-red-700 text-white px-5 py-2 rounded-lg font-medium transition"
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
</div>
|
||||||
|
</div>
|
||||||
{% block body_scripts %}
|
{% endblock %} {% block body_scripts %}
|
||||||
<script src="/static/js/error_logs.js"></script>
|
<script src="/static/js/error_logs.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// error_logs.html specific JS initialization (if any)
|
// error_logs.html specific JS initialization (if any)
|
||||||
// e.g., initialize date pickers or other elements if needed
|
// e.g., initialize date pickers or other elements if needed
|
||||||
// The main logic is in error_logs.js
|
// The main logic is in error_logs.js
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user