refactor(ui): 优化无效密钥列表头部布局,使“全选”组件右对齐

这个消息表明了以下几点:
1.  **类型 (Type)**: `refactor` - 这是一次重构,主要改进了现有用户界面元素的布局,而不是添加新功能或修复错误。
2.  **范围 (Scope)**: `ui` - 表明更改影响的是用户界面部分。
3.  **主题 (Subject)**:
    *   `优化无效密钥列表头部布局`: 指出更改的具体位置是“无效密钥列表”的头部区域,并且是对其布局的优化。
    *   `使“全选”组件右对齐`: 明确了主要的视觉变化是将“全选”复选框及其标签对齐到该区域的右侧。
This commit is contained in:
snaily
2025-05-08 19:06:46 +08:00
parent e1c068ed9e
commit 4ad18e43ef
8 changed files with 4289 additions and 2576 deletions

View File

@@ -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` |

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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">&times;</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"
>
&times;
</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">&times;</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"
>
&times;
</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