Compare commits

...

9 Commits

Author SHA1 Message Date
yinpeng
ede27a5d70 refactor: 移除 retry_handler 中未使用的 KeyManager 导入 2025-02-12 17:48:09 +08:00
yinpeng
5a4619444b fix: 修复 Gemini 多段文本响应内容拼接问题 2025-02-12 17:47:03 +08:00
yinpeng
b3851441f1 refactor: 优化 RetryHandler 装饰器以支持动态 KeyManager 注入 2025-02-12 17:10:02 +08:00
yinpeng
44f956e4e4 feat: Add PWA support with manifest and ServiceWorker integration
- Mounted static files directory to serve PWA assets like manifest.json and ServiceWorker scripts.
- Updated `auth.html` and `keys_status.html` templates:
  - Added `<link>` for manifest and icons to support Progressive Web App (PWA) features.
  - Added meta tags for theme color and Apple web app capabilities.
  - Integrated ServiceWorker registration script for offline capabilities.
2025-02-12 16:20:34 +08:00
yinpeng
3aa4384b9d feat: Add responsive styles for auth and keys status pages
- Implement media queries to improve layout and UI for smaller screen sizes on `auth.html` and `keys_status.html`.
- Adjust container widths, font sizes, padding, and other styles for screen widths below 768px and 480px.
- Enhance mobile usability by making elements stack vertically, resizing fonts, and optimizing spacing for better readability and interaction.
2025-02-12 15:46:37 +08:00
yinpeng
6db4b56186 Refactor keys_status.html for improved layout and scrolling behavior
- Removed duplicated padding and simplified CSS for `body`, ensuring proper spacing with 20px padding.
- Adjusted `.container` styles:
  - Removed custom scrollbar styles and overflow-related attributes.
  - Centered the element with `margin: 20px auto`.
- Updated scroll behavior:
  - Changed scroll functions to operate on `window` instead of `.container`.
  - Modified event listeners to use `window` for detecting scroll events.
- Cleaned up redundant or unused styles and improved readability.
2025-02-12 15:30:44 +08:00
yinpeng
8e77773d5a Enhance UI/UX for keys_status.html
- Added smooth scroll functionality with "Scroll to Top" and "Scroll to Bottom" buttons.
- Introduced a `scroll-buttons` section with styled buttons for scrolling.
- Improved `#copyStatus` styling for better visibility and alignment.
- Adjusted `.container` to support scrollable content with hidden scrollbars and a max-height.
- Ensured proper z-index for new elements to prevent overlapping issues.
- Enhanced hover and active states for scroll buttons to improve user experience.
- Added event listeners to dynamically show/hide scroll buttons based on user scroll position.
2025-02-12 15:16:22 +08:00
yinpeng
343f40476a feat: Improve UI/UX for API Key Status page and add enhancements
- Updated the overall design aesthetics of the authentication page.
  - Added `fadeIn`, `slideDown`, `slideUp`, and `shake` animations for better user interaction.
  - Improved error message styling with a subtle background, padding, and animation.

- Enhanced "API Key Status" page:
  - Implemented new theme with gradient backgrounds and glassmorphism effect.
  - Redesigned headings with underlines and improved hierarchy.
  - Added FontAwesome icons to improve the visual appeal and clarity (e.g., checkmarks, warnings, keys).
  - Applied better spacing, padding, and hover effects to list items and buttons.
  - Introduced animations for key lists to create fluid transitions on page load.
  - Differentiated valid and invalid keys using badges with appropriate colors and icons.

- Copy Key Interaction:
  - Improved key copying functionality:
    - Added animations and hover effects to "Copy" buttons.
    - Updated the copied key selector logic to target `.key-text` for cleaner code.
    - Changed copy confirmation message for better clarity.
  - Styled the copy success message (`#copyStatus`) to appear fixed at the bottom with a blur effect.

- Key List Enhancements:
  - Added fail count badges for individual keys with red warning styles.
  - Styled buttons for batch copying to display icons alongside text, matching the overall design.

- Accessibility and Readability:
  - Refactored text sizes, weights, and alignments for smoother readability.
  - Enhanced color contrast and alignment for better accessibility.

Notes:
- New CSS animations have been smoothly integrated with no breaking changes.
- All changes prioritize maintaining current functionality while enhancing user experience.
2025-02-12 14:46:34 +08:00
yinpeng
e024d55006 feat: update workflows for docker and release processes
- Updated `.github/workflows/docker-publish.yml`:
  - Commented out the branch trigger for `main` in the `push` event to allow only tag-based Docker builds (tags like `v*.*.*`).

- Updated `.github/workflows/release.yml`:
  - Removed default release body template containing placeholder release notes. This simplifies the release creation process and avoids predefined content.
  - No functional changes to release asset upload configurations, minor format adjustment to ensure no missing newline at the file end.
2025-02-12 14:20:46 +08:00
12 changed files with 580 additions and 89 deletions

View File

@@ -2,7 +2,7 @@ name: Docker Image CI
on: on:
push: push:
branches: [ "main" ] # branches: [ "main" ]
tags: [ 'v*.*.*' ] tags: [ 'v*.*.*' ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]

View File

@@ -24,10 +24,6 @@ jobs:
with: with:
tag_name: ${{ github.ref_name }} tag_name: ${{ github.ref_name }}
release_name: ${{ github.ref_name }} release_name: ${{ github.ref_name }}
body: |
## Release Notes
- 自动发布版本。
- 请根据需求更新对应内容。
draft: false draft: false
prerelease: false prerelease: false
@@ -45,4 +41,4 @@ jobs:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./gemini-balance.zip # 替换为你的构建文件路径 asset_path: ./gemini-balance.zip # 替换为你的构建文件路径
asset_name: gemini-balance.zip # 替换为你的文件名 asset_name: gemini-balance.zip # 替换为你的文件名
asset_content_type: application/zip asset_content_type: application/zip

View File

@@ -47,7 +47,7 @@ async def list_models(_=Depends(security_service.verify_key),
@router.post("/models/{model_name}:generateContent") @router.post("/models/{model_name}:generateContent")
@router_v1beta.post("/models/{model_name}:generateContent") @router_v1beta.post("/models/{model_name}:generateContent")
@RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key") @RetryHandler(max_retries=3, key_arg="api_key")
async def generate_content( async def generate_content(
model_name: str, model_name: str,
request: GeminiRequest, request: GeminiRequest,
@@ -77,7 +77,7 @@ async def generate_content(
@router.post("/models/{model_name}:streamGenerateContent") @router.post("/models/{model_name}:streamGenerateContent")
@router_v1beta.post("/models/{model_name}:streamGenerateContent") @router_v1beta.post("/models/{model_name}:streamGenerateContent")
@RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key") @RetryHandler(max_retries=3, key_arg="api_key")
async def stream_generate_content( async def stream_generate_content(
model_name: str, model_name: str,
request: GeminiRequest, request: GeminiRequest,

View File

@@ -46,7 +46,7 @@ async def list_models(
@router.post("/v1/chat/completions") @router.post("/v1/chat/completions")
@router.post("/hf/v1/chat/completions") @router.post("/hf/v1/chat/completions")
@RetryHandler(max_retries=3, key_manager=Depends(get_key_manager), key_arg="api_key") @RetryHandler(max_retries=3, key_arg="api_key")
async def chat_completion( async def chat_completion(
request: ChatRequest, request: ChatRequest,
_=Depends(security_service.verify_authorization), _=Depends(security_service.verify_authorization),

View File

@@ -2,6 +2,7 @@ from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from app.core.logger import get_main_logger from app.core.logger import get_main_logger
from app.core.security import verify_auth_token from app.core.security import verify_auth_token
from app.services.key_manager import get_key_manager_instance from app.services.key_manager import get_key_manager_instance
@@ -19,6 +20,9 @@ app = FastAPI()
# 配置Jinja2模板 # 配置Jinja2模板
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
# 配置静态文件
app.mount("/static", StaticFiles(directory="app/static"), name="static")
# 创建 KeyManager 实例 # 创建 KeyManager 实例
key_manager = None key_manager = None

View File

@@ -232,7 +232,9 @@ def _extract_text(response: Dict[str, Any], model: str, stream: bool = False) ->
else: else:
text = candidate["content"]["parts"][0]["text"] text = candidate["content"]["parts"][0]["text"]
else: else:
text = candidate["content"]["parts"][0]["text"] text = ""
for part in candidate["content"]["parts"]:
text += part["text"]
text = _add_search_link_text(model, candidate, text) text = _add_search_link_text(model, candidate, text)
else: else:
text = "暂无返回" text = "暂无返回"

View File

@@ -3,7 +3,6 @@
from typing import TypeVar, Callable from typing import TypeVar, Callable
from functools import wraps from functools import wraps
from app.core.logger import get_retry_logger from app.core.logger import get_retry_logger
from app.services.key_manager import KeyManager
T = TypeVar('T') T = TypeVar('T')
logger = get_retry_logger() logger = get_retry_logger()
@@ -12,9 +11,8 @@ logger = get_retry_logger()
class RetryHandler: class RetryHandler:
"""重试处理装饰器""" """重试处理装饰器"""
def __init__(self, max_retries: int = 3, key_manager: KeyManager = None, key_arg: str = "api_key"): def __init__(self, max_retries: int = 3, key_arg: str = "api_key"):
self.max_retries = max_retries self.max_retries = max_retries
self.key_manager = key_manager
self.key_arg = key_arg self.key_arg = key_arg
def __call__(self, func: Callable[..., T]) -> Callable[..., T]: def __call__(self, func: Callable[..., T]) -> Callable[..., T]:
@@ -29,9 +27,11 @@ class RetryHandler:
last_exception = e last_exception = e
logger.warning(f"API call failed with error: {str(e)}. Attempt {attempt + 1} of {self.max_retries}") logger.warning(f"API call failed with error: {str(e)}. Attempt {attempt + 1} of {self.max_retries}")
if self.key_manager: # 从函数参数中获取 key_manager
key_manager = kwargs.get('key_manager')
if key_manager:
old_key = kwargs.get(self.key_arg) old_key = kwargs.get(self.key_arg)
new_key = await self.key_manager.handle_api_failure(old_key) new_key = await key_manager.handle_api_failure(old_key)
kwargs[self.key_arg] = new_key kwargs[self.key_arg] = new_key
logger.info(f"Switched to new API key: {new_key}") logger.info(f"Switched to new API key: {new_key}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

17
app/static/manifest.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "Gemini Balance",
"short_name": "GBalance",
"description": "Gemini API密钥管理工具",
"start_url": "/",
"display": "standalone",
"background_color": "#667eea",
"theme_color": "#764ba2",
"icons": [
{
"src": "/static/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
}
]
}

View File

@@ -0,0 +1,43 @@
const CACHE_NAME = 'gbalance-cache-v1';
const urlsToCache = [
'/',
'/static/manifest.json',
'/static/icons/icon-192x192.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});

View File

@@ -4,14 +4,21 @@
<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>验证页面</title> <title>验证页面</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style> <style>
body { body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
line-height: 1.6; line-height: 1.6;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -20,70 +27,222 @@
.container { .container {
max-width: 400px; max-width: 400px;
width: 90%; width: 90%;
background: white; background: rgba(255, 255, 255, 0.95);
padding: 40px; padding: 40px;
border-radius: 15px; border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1); box-shadow: 0 15px 35px rgba(0,0,0,0.2);
transition: all 0.3s ease; backdrop-filter: blur(10px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (max-width: 768px) {
.container {
width: 85%;
padding: 30px;
}
.logo i {
font-size: 40px;
}
h2 {
font-size: 22px;
}
input {
padding: 10px 10px 10px 35px;
font-size: 15px;
}
.input-group i {
font-size: 16px;
}
button {
padding: 12px;
font-size: 15px;
}
}
@media (max-width: 480px) {
.container {
width: 90%;
padding: 25px;
}
.logo i {
font-size: 36px;
}
h2 {
font-size: 20px;
margin-bottom: 25px;
}
form {
gap: 15px;
}
input {
padding: 10px 10px 10px 32px;
font-size: 14px;
}
.input-group i {
font-size: 15px;
left: 10px;
}
button {
padding: 10px;
font-size: 14px;
}
.error-message {
font-size: 14px;
padding: 8px;
margin-top: 12px;
}
} }
.container:hover { .container:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15); box-shadow: 0 20px 40px rgba(0,0,0,0.25);
}
.logo {
text-align: center;
margin-bottom: 30px;
animation: fadeIn 1s ease;
}
.logo i {
font-size: 48px;
color: #764ba2;
margin-bottom: 15px;
} }
h2 { h2 {
color: #2c3e50; color: #2c3e50;
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
font-weight: 700; font-weight: 700;
font-size: 24px;
animation: slideDown 0.5s ease;
} }
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px;
}
.input-group {
position: relative;
animation: slideUp 0.5s ease;
}
.input-group i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #764ba2;
font-size: 18px;
} }
input { input {
margin: 10px 0; width: 100%;
padding: 12px; padding: 12px 12px 12px 40px;
border: 1px solid #e0e0e0; border: 2px solid #e0e0e0;
border-radius: 5px; border-radius: 10px;
font-size: 16px; font-size: 16px;
box-sizing: border-box;
transition: all 0.3s ease; transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.9);
} }
input:focus { input:focus {
border-color: #3498db; border-color: #764ba2;
box-shadow: 0 0 5px rgba(52, 152, 219, 0.5); box-shadow: 0 0 10px rgba(118, 75, 162, 0.2);
outline: none;
} }
button { button {
background-color: #3498db; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border: none; border: none;
padding: 12px; padding: 14px;
border-radius: 5px; border-radius: 10px;
cursor: pointer; cursor: pointer;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
transition: background-color 0.3s ease; transition: all 0.3s ease;
position: relative;
overflow: hidden;
} }
button:hover { button:hover {
background-color: #2980b9; transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
button:active {
transform: translateY(0);
}
button::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
button:active::after {
width: 200px;
height: 200px;
opacity: 0;
} }
.error-message { .error-message {
color: #e74c3c; color: #e74c3c;
margin-top: 15px; margin-top: 15px;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
padding: 10px;
border-radius: 5px;
background: rgba(231, 76, 60, 0.1);
animation: shake 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideDown {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h2>请输入验证令牌</h2> <div class="logo">
<i class="fas fa-shield-alt"></i>
</div>
<h2>安全验证</h2>
<form id="auth-form" action="/auth" method="post"> <form id="auth-form" action="/auth" method="post">
<input type="password" id="auth-token" name="auth_token" required placeholder="输入验证令牌"> <div class="input-group">
<button type="submit">提交</button> <i class="fas fa-key"></i>
<input type="password" id="auth-token" name="auth_token" required placeholder="请输入验证令牌">
</div>
<button type="submit">
验证访问
</button>
</form> </form>
{% if error %} {% if error %}
<p class="error-message">{{ error }}</p> <p class="error-message">{{ error }}</p>
{% endif %} {% endif %}
</div> </div>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/service-worker.js')
.then(registration => {
console.log('ServiceWorker注册成功:', registration.scope);
})
.catch(error => {
console.log('ServiceWorker注册失败:', error);
});
});
}
</script>
</body> </body>
</html> </html>

View File

@@ -4,108 +4,297 @@
<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>API密钥状态</title> <title>API密钥状态</title>
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#764ba2">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="GBalance">
<link rel="icon" href="/static/icons/icon-192x192.png">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style> <style>
body { body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
line-height: 1.6; line-height: 1.6;
margin: 0; margin: 0;
padding: 0; padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
} }
.container { .container {
max-width: 900px; max-width: 900px;
width: 90%; width: 95%;
background: white; background: rgba(255, 255, 255, 0.95);
padding: 40px; padding: 40px;
border-radius: 15px; border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1); box-shadow: 0 15px 35px rgba(0,0,0,0.2);
transition: all 0.3s ease; backdrop-filter: blur(10px);
position: relative;
margin: 20px auto;
} }
.container:hover {
transform: translateY(-5px); @media (max-width: 768px) {
box-shadow: 0 15px 35px rgba(0,0,0,0.15); .container {
width: 100%;
padding: 20px;
margin: 10px auto;
}
body {
padding: 10px;
}
h1 {
font-size: 24px;
}
.key-list h2 {
font-size: 1.2em;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.key-info {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
li {
flex-direction: column;
gap: 10px;
}
.copy-btn {
width: 100%;
justify-content: center;
}
.key-text {
word-break: break-all;
}
.scroll-buttons {
right: 10px;
bottom: 10px;
}
.scroll-btn {
width: 35px;
height: 35px;
font-size: 16px;
}
}
@media (max-width: 480px) {
.container {
padding: 15px;
}
h1 {
font-size: 20px;
}
.key-list {
padding: 15px;
}
.status-badge {
padding: 3px 8px;
font-size: 0.8em;
}
.fail-count {
font-size: 0.8em;
}
.total {
font-size: 1em;
padding: 12px 20px;
}
} }
h1 { h1 {
color: #2c3e50; color: #2c3e50;
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
font-weight: 700; font-weight: 700;
font-size: 32px;
position: relative;
padding-bottom: 15px;
}
h1::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
} }
.key-list { .key-list {
margin-bottom: 30px; margin-bottom: 30px;
background: #f8f9fa; background: rgba(248, 249, 250, 0.9);
padding: 20px; padding: 25px;
border-radius: 10px; border-radius: 15px;
transition: all 0.3s ease; transition: all 0.3s ease;
border: 1px solid rgba(0,0,0,0.1);
} }
.key-list:hover { .key-list:hover {
background: #e9ecef; transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
} }
.key-list h2 { .key-list h2 {
color: #2c3e50; color: #2c3e50;
margin-bottom: 15px; margin-bottom: 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-size: 1.5em; font-size: 1.5em;
padding-bottom: 10px;
border-bottom: 2px solid rgba(0,0,0,0.1);
} }
ul { ul {
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
margin: 0;
} }
li { li {
background: white; background: white;
border: 1px solid #e0e0e0; border: 1px solid rgba(0,0,0,0.1);
margin-bottom: 10px; margin-bottom: 12px;
padding: 12px; padding: 15px;
border-radius: 5px; border-radius: 10px;
transition: all 0.2s ease; transition: all 0.3s ease;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
} }
li:hover { li:hover {
transform: translateX(5px); transform: translateX(5px);
box-shadow: 2px 2px 5px rgba(0,0,0,0.1); box-shadow: 0 5px 15px rgba(0,0,0,0.1);
} }
.total { .key-info {
font-weight: bold; display: flex;
margin-top: 20px; align-items: center;
text-align: center; gap: 15px;
font-size: 1.2em; flex: 1;
}
.key-text {
font-family: 'Roboto Mono', monospace;
color: #2c3e50; color: #2c3e50;
} }
.fail-count {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
padding: 4px 10px;
border-radius: 15px;
font-size: 0.85em;
display: flex;
align-items: center;
gap: 5px;
}
.fail-count i {
font-size: 12px;
}
.copy-btn { .copy-btn {
background-color: #3498db; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
border: none; border: none;
padding: 8px 15px; padding: 8px 15px;
border-radius: 5px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
transition: background-color 0.3s ease; transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
} }
.copy-btn:hover { .copy-btn:hover {
background-color: #2980b9; transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.copy-btn:active {
transform: translateY(0);
}
.copy-btn i {
font-size: 14px;
}
.total {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 25px;
border-radius: 10px;
font-weight: bold;
text-align: center;
font-size: 1.2em;
margin-top: 30px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
} }
#copyStatus { #copyStatus {
text-align: center; position: fixed;
margin-top: 20px; top: 50%;
color: #27ae60; left: 50%;
transform: translate(-50%, -50%);
background: rgba(39, 174, 96, 0.95);
color: white;
padding: 15px 30px;
border-radius: 25px;
font-weight: bold; font-weight: bold;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease; transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
text-align: center;
min-width: 200px;
} }
.fail-count { .status-badge {
color: #e74c3c; padding: 4px 12px;
border-radius: 15px;
font-size: 0.9em; font-size: 0.9em;
margin-left: 10px; font-weight: bold;
margin-right: 10px;
}
.status-valid {
background: rgba(39, 174, 96, 0.1);
color: #27ae60;
}
.status-invalid {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.key-list {
animation: fadeIn 0.5s ease forwards;
}
.key-list:nth-child(2) {
animation-delay: 0.2s;
}
.scroll-buttons {
position: fixed;
right: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.scroll-btn {
background: rgba(118, 75, 162, 0.9);
color: white;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.scroll-btn:hover {
background: rgba(102, 126, 234, 0.9);
transform: scale(1.1);
}
.scroll-btn:active {
transform: scale(0.95);
} }
</style> </style>
</head> </head>
@@ -114,40 +303,84 @@
<h1>API密钥状态</h1> <h1>API密钥状态</h1>
<div class="key-list"> <div class="key-list">
<h2> <h2>
有效密钥 <span>
<button class="copy-btn" onclick="copyKeys('valid')">一键复制</button> <i class="fas fa-check-circle" style="color: #27ae60;"></i>
有效密钥
</span>
<button class="copy-btn" onclick="copyKeys('valid')">
<i class="fas fa-copy"></i>
批量复制
</button>
</h2> </h2>
<ul id="validKeys"> <ul id="validKeys">
{% for key, fail_count in valid_keys.items() %} {% for key, fail_count in valid_keys.items() %}
<li> <li>
<span>{{ key }}</span> <div class="key-info">
<span class="fail-count">(失败次数: {{ fail_count }})</span> <span class="status-badge status-valid">
<button class="copy-btn" onclick="copyKey('{{ key }}')">复制</button> <i class="fas fa-check"></i> 有效
</span>
<span class="key-text">{{ key }}</span>
<span class="fail-count">
<i class="fas fa-exclamation-triangle"></i>
失败: {{ fail_count }}
</span>
</div>
<button class="copy-btn" onclick="copyKey('{{ key }}')">
<i class="fas fa-copy"></i>
复制
</button>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="key-list"> <div class="key-list">
<h2> <h2>
无效密钥 <span>
<button class="copy-btn" onclick="copyKeys('invalid')">一键复制</button> <i class="fas fa-times-circle" style="color: #e74c3c;"></i>
无效密钥
</span>
<button class="copy-btn" onclick="copyKeys('invalid')">
<i class="fas fa-copy"></i>
批量复制
</button>
</h2> </h2>
<ul id="invalidKeys"> <ul id="invalidKeys">
{% for key, fail_count in invalid_keys.items() %} {% for key, fail_count in invalid_keys.items() %}
<li> <li>
<span>{{ key }}</span> <div class="key-info">
<span class="fail-count">(失败次数: {{ fail_count }})</span> <span class="status-badge status-invalid">
<button class="copy-btn" onclick="copyKey('{{ key }}')">复制</button> <i class="fas fa-times"></i> 无效
</span>
<span class="key-text">{{ key }}</span>
<span class="fail-count">
<i class="fas fa-exclamation-triangle"></i>
失败: {{ fail_count }}
</span>
</div>
<button class="copy-btn" onclick="copyKey('{{ key }}')">
<i class="fas fa-copy"></i>
复制
</button>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="total"> <div class="total">
总密钥数:{{ total }} <i class="fas fa-key"></i> 总密钥数:{{ total }}
</div> </div>
<div id="copyStatus"></div>
</div> </div>
<div class="scroll-buttons">
<button class="scroll-btn" onclick="scrollToTop()" title="回到顶部">
<i class="fas fa-chevron-up"></i>
</button>
<button class="scroll-btn" onclick="scrollToBottom()" title="滚动到底部">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div id="copyStatus"></div>
<script> <script>
function copyToClipboard(text) { function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) { if (navigator.clipboard && navigator.clipboard.writeText) {
@@ -177,12 +410,12 @@
} }
function copyKeys(type) { function copyKeys(type) {
const keys = Array.from(document.querySelectorAll(`#${type}Keys li span:first-child`)).map(span => span.textContent.trim()); const keys = Array.from(document.querySelectorAll(`#${type}Keys .key-text`)).map(span => span.textContent.trim());
const jsonKeys = JSON.stringify(keys); const jsonKeys = JSON.stringify(keys);
copyToClipboard(jsonKeys) copyToClipboard(jsonKeys)
.then(() => { .then(() => {
showCopyStatus(`已成功复制 ${type === 'valid' ? '有效' : '无效'} 密钥到剪贴板`); showCopyStatus(`已成功复制${type === 'valid' ? '有效' : '无效'}密钥到剪贴板`);
}) })
.catch((err) => { .catch((err) => {
console.error('无法复制文本: ', err); console.error('无法复制文本: ', err);
@@ -209,6 +442,43 @@
statusElement.style.opacity = 0; statusElement.style.opacity = 0;
}, 2000); }, 2000);
} }
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
function scrollToBottom() {
window.scrollTo({
top: document.documentElement.scrollHeight,
behavior: 'smooth'
});
}
// 监听窗口滚动事件来显示/隐藏滚动按钮
window.addEventListener('scroll', function() {
const scrollButtons = document.querySelector('.scroll-buttons');
if (window.scrollY > 100) {
scrollButtons.style.display = 'flex';
} else {
scrollButtons.style.display = 'none';
}
});
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/service-worker.js')
.then(registration => {
console.log('ServiceWorker注册成功:', registration.scope);
})
.catch(error => {
console.log('ServiceWorker注册失败:', error);
});
});
}
</script> </script>
</body> </body>
</html> </html>