Upload 41 files
Browse files- app.py +101 -89
- models/__pycache__/api_key.cpython-313.pyc +0 -0
- models/api_key.py +51 -7
- routes/__pycache__/api.cpython-313.pyc +0 -0
- routes/__pycache__/web.cpython-313.pyc +0 -0
- routes/api.py +18 -0
- static/js/api-key-manager/api-key-creator.js +155 -0
- static/js/api-key-manager/api-key-deleter.js +144 -0
- static/js/api-key-manager/api-key-editor.js +148 -0
- static/js/api-key-manager/api-key-loader.js +55 -0
- static/js/api-key-manager/bulk-actions.js +9 -3
- static/js/api-key-manager/core.js +3 -0
- static/js/api-key-manager/key-operations.js +13 -493
- static/js/api-key-manager/main-manager.js +11 -3
- static/js/api-key-manager/platform-utils.js +29 -0
- templates/base.html +1 -1
- templates/components/api_key_list.html +66 -66
- templates/login.html +1 -1
- utils/__pycache__/auth.cpython-313.pyc +0 -0
app.py
CHANGED
|
@@ -1,89 +1,101 @@
|
|
| 1 |
-
"""
|
| 2 |
-
API密钥管理系统 - 主应用文件
|
| 3 |
-
提供API密钥的添加、编辑、删除和管理功能
|
| 4 |
-
"""
|
| 5 |
-
import os
|
| 6 |
-
import time
|
| 7 |
-
import
|
| 8 |
-
import
|
| 9 |
-
from
|
| 10 |
-
from werkzeug.middleware.proxy_fix import ProxyFix
|
| 11 |
-
|
| 12 |
-
# 导入配置
|
| 13 |
-
from config import SECRET_KEY
|
| 14 |
-
|
| 15 |
-
# 设置时区为UTC+8 (亚洲/上海),兼容Linux和Windows环境
|
| 16 |
-
os.environ['TZ'] = 'Asia/Shanghai'
|
| 17 |
-
try:
|
| 18 |
-
# Linux环境设置
|
| 19 |
-
time.tzset()
|
| 20 |
-
except AttributeError:
|
| 21 |
-
# Windows环境不支持tzset,使用pytz设置
|
| 22 |
-
pass
|
| 23 |
-
|
| 24 |
-
# 确保datetime使用正确的时区
|
| 25 |
-
default_tz = pytz.timezone('Asia/Shanghai')
|
| 26 |
-
|
| 27 |
-
# 导入路由蓝图
|
| 28 |
-
from routes.web import web_bp
|
| 29 |
-
from routes.api import api_bp
|
| 30 |
-
|
| 31 |
-
# 导入认证模块
|
| 32 |
-
from utils.auth import AuthManager
|
| 33 |
-
|
| 34 |
-
# 创建Flask应用
|
| 35 |
-
app = Flask(__name__)
|
| 36 |
-
#
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
#
|
| 88 |
-
if
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
API密钥管理系统 - 主应用文件
|
| 3 |
+
提供API密钥的添加、编辑、删除和管理功能
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
import pytz
|
| 8 |
+
from flask import Flask, redirect, url_for, request, jsonify
|
| 9 |
+
from flask_compress import Compress
|
| 10 |
+
from werkzeug.middleware.proxy_fix import ProxyFix
|
| 11 |
+
|
| 12 |
+
# 导入配置
|
| 13 |
+
from config import SECRET_KEY
|
| 14 |
+
|
| 15 |
+
# 设置时区为UTC+8 (亚洲/上海),兼容Linux和Windows环境
|
| 16 |
+
os.environ['TZ'] = 'Asia/Shanghai'
|
| 17 |
+
try:
|
| 18 |
+
# Linux环境设置
|
| 19 |
+
time.tzset()
|
| 20 |
+
except AttributeError:
|
| 21 |
+
# Windows环境不支持tzset,使用pytz设置
|
| 22 |
+
pass
|
| 23 |
+
|
| 24 |
+
# 确保datetime使用正确的时区
|
| 25 |
+
default_tz = pytz.timezone('Asia/Shanghai')
|
| 26 |
+
|
| 27 |
+
# 导入路由蓝图
|
| 28 |
+
from routes.web import web_bp
|
| 29 |
+
from routes.api import api_bp
|
| 30 |
+
|
| 31 |
+
# 导入认证模块
|
| 32 |
+
from utils.auth import AuthManager
|
| 33 |
+
|
| 34 |
+
# 创建Flask应用
|
| 35 |
+
app = Flask(__name__)
|
| 36 |
+
# 初始化Compress
|
| 37 |
+
compress = Compress()
|
| 38 |
+
# 配置Compress
|
| 39 |
+
app.config['COMPRESS_MIMETYPES'] = [
|
| 40 |
+
'text/html', 'text/css', 'text/xml', 'application/json',
|
| 41 |
+
'application/javascript', 'text/javascript', 'text/plain'
|
| 42 |
+
]
|
| 43 |
+
app.config['COMPRESS_LEVEL'] = 6 # gzip压缩级别 (1-9)
|
| 44 |
+
app.config['COMPRESS_MIN_SIZE'] = 500 # 最小压缩尺寸(字节)
|
| 45 |
+
app.config['COMPRESS_ALGORITHM'] = 'br,gzip' # 优先使用brotli,然后是gzip
|
| 46 |
+
# 应用压缩
|
| 47 |
+
compress.init_app(app)
|
| 48 |
+
# 应用ProxyFix中间件,使应用能够获取用户真实IP
|
| 49 |
+
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
|
| 50 |
+
app.secret_key = SECRET_KEY
|
| 51 |
+
|
| 52 |
+
# 设置静态文件缓存控制
|
| 53 |
+
@app.after_request
|
| 54 |
+
def add_cache_headers(response):
|
| 55 |
+
"""为静态资源添加缓存控制头"""
|
| 56 |
+
if request.path.startswith('/static/'):
|
| 57 |
+
# 设置缓存时间 - CSS、JS和图片缓存1年
|
| 58 |
+
max_age = 31536000 # 1年的秒数
|
| 59 |
+
|
| 60 |
+
# 根据文件类型设置不同的缓存策略
|
| 61 |
+
if request.path.endswith(('.css', '.js')):
|
| 62 |
+
# CSS和JS文件缓存1年
|
| 63 |
+
response.headers['Cache-Control'] = f'public, max-age={max_age}'
|
| 64 |
+
elif request.path.endswith(('.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg')):
|
| 65 |
+
# 图片文件缓存1年
|
| 66 |
+
response.headers['Cache-Control'] = f'public, max-age={max_age}'
|
| 67 |
+
else:
|
| 68 |
+
# 其他静态文件缓存1周
|
| 69 |
+
response.headers['Cache-Control'] = 'public, max-age=604800'
|
| 70 |
+
|
| 71 |
+
# 添加其他有用的缓存头
|
| 72 |
+
response.headers['Vary'] = 'Accept-Encoding'
|
| 73 |
+
|
| 74 |
+
return response
|
| 75 |
+
|
| 76 |
+
# 认证中间件 - 验证所有请求
|
| 77 |
+
@app.before_request
|
| 78 |
+
def authenticate():
|
| 79 |
+
"""请求拦截器 - 验证所有需要认证的请求"""
|
| 80 |
+
# 登录和静态资源路径不需要验证
|
| 81 |
+
if request.path == '/login' or request.path.startswith('/static/'):
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
# 从Cookie中获取令牌
|
| 85 |
+
token = request.cookies.get('auth_token')
|
| 86 |
+
|
| 87 |
+
# 验证令牌
|
| 88 |
+
if not AuthManager.verify_token(token):
|
| 89 |
+
# 如果是AJAX请求,返回401状态码
|
| 90 |
+
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.path.startswith('/api/'):
|
| 91 |
+
return jsonify({"success": False, "error": "未授权访问"}), 401
|
| 92 |
+
# 否则重定向到登录页面
|
| 93 |
+
return redirect(url_for('web.login'))
|
| 94 |
+
|
| 95 |
+
# 注册蓝图
|
| 96 |
+
app.register_blueprint(web_bp)
|
| 97 |
+
app.register_blueprint(api_bp)
|
| 98 |
+
|
| 99 |
+
# 入口点
|
| 100 |
+
if __name__ == '__main__':
|
| 101 |
+
app.run(debug=True, host='0.0.0.0', port=7860)
|
models/__pycache__/api_key.cpython-313.pyc
CHANGED
|
Binary files a/models/__pycache__/api_key.cpython-313.pyc and b/models/__pycache__/api_key.cpython-313.pyc differ
|
|
|
models/api_key.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
| 5 |
import uuid
|
| 6 |
from datetime import datetime
|
| 7 |
import os
|
|
|
|
| 8 |
from config import API_KEYS_FILE
|
| 9 |
|
| 10 |
class ApiKeyManager:
|
|
@@ -40,16 +41,16 @@ class ApiKeyManager:
|
|
| 40 |
"""添加新的API密钥"""
|
| 41 |
api_keys_data = ApiKeyManager.load_keys()
|
| 42 |
|
| 43 |
-
# 过滤掉key
|
| 44 |
-
if key
|
| 45 |
-
key = key.replace("'", "")
|
| 46 |
|
| 47 |
new_key = {
|
| 48 |
"id": str(uuid.uuid4()),
|
| 49 |
"platform": platform,
|
| 50 |
"name": name,
|
| 51 |
"key": key,
|
| 52 |
-
"created_at": datetime.now().isoformat()
|
| 53 |
}
|
| 54 |
|
| 55 |
api_keys_data["api_keys"].append(new_key)
|
|
@@ -87,14 +88,57 @@ class ApiKeyManager:
|
|
| 87 |
|
| 88 |
return deleted_count
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
@staticmethod
|
| 91 |
def update_key(key_id, name, key):
|
| 92 |
"""更新API密钥信息"""
|
| 93 |
api_keys_data = ApiKeyManager.load_keys()
|
| 94 |
|
| 95 |
-
# 过滤掉key
|
| 96 |
-
if key
|
| 97 |
-
key = key.replace("'", "")
|
| 98 |
|
| 99 |
updated_key = None
|
| 100 |
for k in api_keys_data["api_keys"]:
|
|
|
|
| 5 |
import uuid
|
| 6 |
from datetime import datetime
|
| 7 |
import os
|
| 8 |
+
import pytz
|
| 9 |
from config import API_KEYS_FILE
|
| 10 |
|
| 11 |
class ApiKeyManager:
|
|
|
|
| 41 |
"""添加新的API密钥"""
|
| 42 |
api_keys_data = ApiKeyManager.load_keys()
|
| 43 |
|
| 44 |
+
# 过滤掉key中的单引号、双引号、小括号、方括号和空格,防止存储时出错
|
| 45 |
+
if key:
|
| 46 |
+
key = key.replace("'", "").replace('"', "").replace('(', "").replace(')', "").replace('[', "").replace(']', "").replace(' ', "")
|
| 47 |
|
| 48 |
new_key = {
|
| 49 |
"id": str(uuid.uuid4()),
|
| 50 |
"platform": platform,
|
| 51 |
"name": name,
|
| 52 |
"key": key,
|
| 53 |
+
"created_at": datetime.now(pytz.timezone('Asia/Shanghai')).isoformat()
|
| 54 |
}
|
| 55 |
|
| 56 |
api_keys_data["api_keys"].append(new_key)
|
|
|
|
| 88 |
|
| 89 |
return deleted_count
|
| 90 |
|
| 91 |
+
@staticmethod
|
| 92 |
+
def bulk_add_keys(keys_data):
|
| 93 |
+
"""批量添加多个API密钥
|
| 94 |
+
|
| 95 |
+
Args:
|
| 96 |
+
keys_data: 包含多个密钥信息的列表,每个元素包含platform、name、key
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
添加的密钥列表
|
| 100 |
+
"""
|
| 101 |
+
if not keys_data:
|
| 102 |
+
return []
|
| 103 |
+
|
| 104 |
+
api_keys_data = ApiKeyManager.load_keys()
|
| 105 |
+
added_keys = []
|
| 106 |
+
|
| 107 |
+
now = datetime.now(pytz.timezone('Asia/Shanghai')).isoformat()
|
| 108 |
+
|
| 109 |
+
for key_info in keys_data:
|
| 110 |
+
platform = key_info.get("platform")
|
| 111 |
+
name = key_info.get("name")
|
| 112 |
+
key = key_info.get("key")
|
| 113 |
+
|
| 114 |
+
# 过滤掉key中的单引号、双引号、小括号、方括号和空格,防止存储时出错
|
| 115 |
+
if key:
|
| 116 |
+
key = key.replace("'", "").replace('"', "").replace('(', "").replace(')', "").replace('[', "").replace(']', "").replace(' ', "")
|
| 117 |
+
|
| 118 |
+
new_key = {
|
| 119 |
+
"id": str(uuid.uuid4()),
|
| 120 |
+
"platform": platform,
|
| 121 |
+
"name": name,
|
| 122 |
+
"key": key,
|
| 123 |
+
"created_at": now
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
api_keys_data["api_keys"].append(new_key)
|
| 127 |
+
added_keys.append(new_key)
|
| 128 |
+
|
| 129 |
+
# 一次性保存所有添加的密钥
|
| 130 |
+
ApiKeyManager.save_keys(api_keys_data)
|
| 131 |
+
|
| 132 |
+
return added_keys
|
| 133 |
+
|
| 134 |
@staticmethod
|
| 135 |
def update_key(key_id, name, key):
|
| 136 |
"""更新API密钥信息"""
|
| 137 |
api_keys_data = ApiKeyManager.load_keys()
|
| 138 |
|
| 139 |
+
# 过滤掉key中的单引号、双引号、小括号、方括号和空格,防止存储时出错
|
| 140 |
+
if key:
|
| 141 |
+
key = key.replace("'", "").replace('"', "").replace('(', "").replace(')', "").replace('[', "").replace(']', "").replace(' ', "")
|
| 142 |
|
| 143 |
updated_key = None
|
| 144 |
for k in api_keys_data["api_keys"]:
|
routes/__pycache__/api.cpython-313.pyc
CHANGED
|
Binary files a/routes/__pycache__/api.cpython-313.pyc and b/routes/__pycache__/api.cpython-313.pyc differ
|
|
|
routes/__pycache__/web.cpython-313.pyc
CHANGED
|
Binary files a/routes/__pycache__/web.cpython-313.pyc and b/routes/__pycache__/web.cpython-313.pyc differ
|
|
|
routes/api.py
CHANGED
|
@@ -49,6 +49,24 @@ def bulk_delete_api_keys():
|
|
| 49 |
"message": f"成功删除 {deleted_count} 个API密钥"
|
| 50 |
})
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
@api_bp.route('/keys/<key_id>', methods=['PUT'])
|
| 53 |
def edit_api_key(key_id):
|
| 54 |
"""更新API密钥信息"""
|
|
|
|
| 49 |
"message": f"成功删除 {deleted_count} 个API密钥"
|
| 50 |
})
|
| 51 |
|
| 52 |
+
@api_bp.route('/keys/bulk-add', methods=['POST'])
|
| 53 |
+
def bulk_add_api_keys():
|
| 54 |
+
"""批量添加多个API密钥"""
|
| 55 |
+
data = request.json
|
| 56 |
+
keys_data = data.get("keys", [])
|
| 57 |
+
|
| 58 |
+
if not keys_data:
|
| 59 |
+
return jsonify({"success": False, "error": "没有提供要添加的密钥数据"}), 400
|
| 60 |
+
|
| 61 |
+
added_keys = ApiKeyManager.bulk_add_keys(keys_data)
|
| 62 |
+
|
| 63 |
+
return jsonify({
|
| 64 |
+
"success": True,
|
| 65 |
+
"added_count": len(added_keys),
|
| 66 |
+
"keys": added_keys,
|
| 67 |
+
"message": f"成功添加 {len(added_keys)} 个API密钥"
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
@api_bp.route('/keys/<key_id>', methods=['PUT'])
|
| 71 |
def edit_api_key(key_id):
|
| 72 |
"""更新API密钥信息"""
|
static/js/api-key-manager/api-key-creator.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API密钥管理器 - 密钥创建模块
|
| 3 |
+
* 包含API密钥的添加功能
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// 添加API密钥
|
| 7 |
+
async function addApiKey() {
|
| 8 |
+
if (!this.newKey.platform || !this.newKey.key) {
|
| 9 |
+
this.errorMessage = '请填写所有必填字段。';
|
| 10 |
+
return;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// 如果名称为空,生成自动名称
|
| 14 |
+
if (!this.newKey.name.trim()) {
|
| 15 |
+
const date = new Date();
|
| 16 |
+
const dateStr = date.toLocaleDateString('zh-CN', {
|
| 17 |
+
year: 'numeric',
|
| 18 |
+
month: '2-digit',
|
| 19 |
+
day: '2-digit'
|
| 20 |
+
}).replace(/\//g, '-');
|
| 21 |
+
const timeStr = date.toLocaleTimeString('zh-CN', {
|
| 22 |
+
hour: '2-digit',
|
| 23 |
+
minute: '2-digit'
|
| 24 |
+
});
|
| 25 |
+
this.newKey.name = `${dateStr} ${timeStr}`;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// 保存当前选择的平台类型
|
| 29 |
+
localStorage.setItem('lastSelectedPlatform', this.newKey.platform);
|
| 30 |
+
|
| 31 |
+
this.isSubmitting = true;
|
| 32 |
+
this.errorMessage = '';
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
// 处理输入文本:去除单引号、双引号、小括号、方括号、空格,然后分行
|
| 36 |
+
const lines = this.newKey.key
|
| 37 |
+
.split('\n')
|
| 38 |
+
.map(line => line.replace(/['"\(\)\[\]\s]/g, '')) // 去除单引号、双引号、小括号、方括号和空格
|
| 39 |
+
.filter(line => line.length > 0); // 过滤掉空行
|
| 40 |
+
|
| 41 |
+
// 从每一行中提取逗号分隔的非空元素,作为单独的key
|
| 42 |
+
let keysWithDuplicates = [];
|
| 43 |
+
for (const line of lines) {
|
| 44 |
+
const lineKeys = line.split(',')
|
| 45 |
+
.filter(item => item.length > 0); // 过滤掉空元素
|
| 46 |
+
|
| 47 |
+
// 将每个非空元素添加到数组
|
| 48 |
+
keysWithDuplicates.push(...lineKeys);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if (keysWithDuplicates.length === 0) {
|
| 52 |
+
this.errorMessage = '请输入至少一个有效的API密钥。';
|
| 53 |
+
this.isSubmitting = false;
|
| 54 |
+
return;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// 去除输入中重复的key(同一次提交中的重复)
|
| 58 |
+
const inputDuplicatesCount = keysWithDuplicates.length - new Set(keysWithDuplicates).size;
|
| 59 |
+
const keys = [...new Set(keysWithDuplicates)]; // 使用Set去重,得到唯一的keys数组
|
| 60 |
+
|
| 61 |
+
// 过滤掉已存在于同一平台的重复key
|
| 62 |
+
const currentPlatform = this.newKey.platform;
|
| 63 |
+
const existingKeys = this.apiKeys
|
| 64 |
+
.filter(apiKey => apiKey.platform === currentPlatform)
|
| 65 |
+
.map(apiKey => apiKey.key);
|
| 66 |
+
|
| 67 |
+
const uniqueKeys = keys.filter(key => !existingKeys.includes(key));
|
| 68 |
+
const duplicateCount = keys.length - uniqueKeys.length;
|
| 69 |
+
|
| 70 |
+
// 如果所有key都重复,显示错误消息并退出
|
| 71 |
+
if (uniqueKeys.length === 0) {
|
| 72 |
+
this.errorMessage = '所有输入的API密钥在当前平台中已存在。';
|
| 73 |
+
this.isSubmitting = false;
|
| 74 |
+
return;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// 准备批量添加的数据
|
| 78 |
+
const keysData = uniqueKeys.map(keyText => ({
|
| 79 |
+
platform: this.newKey.platform,
|
| 80 |
+
name: this.newKey.name,
|
| 81 |
+
key: keyText
|
| 82 |
+
}));
|
| 83 |
+
|
| 84 |
+
// 记录重复和唯一的key数量,用于显示通知
|
| 85 |
+
const skippedCount = duplicateCount;
|
| 86 |
+
const addedCount = uniqueKeys.length;
|
| 87 |
+
|
| 88 |
+
// 使用批量添加API一次性添加所有密钥
|
| 89 |
+
const response = await fetch('/api/keys/bulk-add', {
|
| 90 |
+
method: 'POST',
|
| 91 |
+
headers: {
|
| 92 |
+
'Content-Type': 'application/json',
|
| 93 |
+
},
|
| 94 |
+
body: JSON.stringify({ keys: keysData }),
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
const data = await response.json();
|
| 98 |
+
|
| 99 |
+
if (data.success) {
|
| 100 |
+
// 关闭模态框并重置表单
|
| 101 |
+
this.showAddModal = false;
|
| 102 |
+
this.newKey = {
|
| 103 |
+
platform: this.newKey.platform, // 保留平台选择
|
| 104 |
+
name: '',
|
| 105 |
+
key: ''
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
// 使用Toast风格的通知提示
|
| 109 |
+
const Toast = Swal.mixin({
|
| 110 |
+
toast: true,
|
| 111 |
+
position: 'top-end',
|
| 112 |
+
showConfirmButton: false,
|
| 113 |
+
timer: 2500,
|
| 114 |
+
timerProgressBar: true,
|
| 115 |
+
didOpen: (toast) => {
|
| 116 |
+
toast.onmouseenter = Swal.stopTimer;
|
| 117 |
+
toast.onmouseleave = Swal.resumeTimer;
|
| 118 |
+
}
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
// 重新加载API密钥数据而不刷新页面
|
| 122 |
+
this.loadApiKeys();
|
| 123 |
+
|
| 124 |
+
// 构建通知消息
|
| 125 |
+
let title = `已添加 ${addedCount} 个API密钥`;
|
| 126 |
+
|
| 127 |
+
// 根据不同情况显示通知
|
| 128 |
+
if (inputDuplicatesCount > 0 && skippedCount > 0) {
|
| 129 |
+
// 既有输入中的重复,也有数据库中的重复
|
| 130 |
+
title += `,跳过 ${inputDuplicatesCount} 个输入重复和 ${skippedCount} 个已存在密钥`;
|
| 131 |
+
} else if (inputDuplicatesCount > 0) {
|
| 132 |
+
// 只有输入中的重复
|
| 133 |
+
title += `,跳过 ${inputDuplicatesCount} 个输入重复密钥`;
|
| 134 |
+
} else if (skippedCount > 0) {
|
| 135 |
+
// 只有数据库中的重复
|
| 136 |
+
title += `,跳过 ${skippedCount} 个已存在密钥`;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
Toast.fire({
|
| 140 |
+
icon: 'success',
|
| 141 |
+
title: title,
|
| 142 |
+
background: '#f0fdf4',
|
| 143 |
+
iconColor: '#16a34a'
|
| 144 |
+
});
|
| 145 |
+
} else {
|
| 146 |
+
// 处理批量添加失败
|
| 147 |
+
this.errorMessage = data.error || `添加操作失败: ${data.message || '未知错误'}`;
|
| 148 |
+
}
|
| 149 |
+
} catch (error) {
|
| 150 |
+
console.error('添加API密钥失败:', error);
|
| 151 |
+
this.errorMessage = '服务器错误,请重试。';
|
| 152 |
+
} finally {
|
| 153 |
+
this.isSubmitting = false;
|
| 154 |
+
}
|
| 155 |
+
}
|
static/js/api-key-manager/api-key-deleter.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API密钥管理器 - 密钥删除模块
|
| 3 |
+
* 包含API密钥的删除功能
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// 删除API密钥
|
| 7 |
+
function deleteApiKey(id, name) {
|
| 8 |
+
this.deleteKeyId = id;
|
| 9 |
+
this.deleteKeyName = name;
|
| 10 |
+
this.showDeleteConfirm = true;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// 确认删除(单个或批量)
|
| 14 |
+
async function confirmDelete() {
|
| 15 |
+
if (this.isBulkDelete) {
|
| 16 |
+
if (this.selectedKeys.length === 0) return;
|
| 17 |
+
|
| 18 |
+
this.isDeleting = true;
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
const response = await fetch('/api/keys/bulk-delete', {
|
| 22 |
+
method: 'POST',
|
| 23 |
+
headers: {
|
| 24 |
+
'Content-Type': 'application/json',
|
| 25 |
+
},
|
| 26 |
+
body: JSON.stringify({ ids: this.selectedKeys }),
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
const data = await response.json();
|
| 30 |
+
|
| 31 |
+
if (data.success) {
|
| 32 |
+
// 关闭模态框,清空选中数组
|
| 33 |
+
this.showDeleteConfirm = false;
|
| 34 |
+
this.isBulkDelete = false;
|
| 35 |
+
const deletedCount = data.deleted_count || this.selectedKeys.length;
|
| 36 |
+
|
| 37 |
+
// 清空选中数组
|
| 38 |
+
this.selectedKeys = [];
|
| 39 |
+
this.selectedPlatforms = [];
|
| 40 |
+
|
| 41 |
+
// 使用Toast风格的通知提示
|
| 42 |
+
const Toast = Swal.mixin({
|
| 43 |
+
toast: true,
|
| 44 |
+
position: 'top-end',
|
| 45 |
+
showConfirmButton: false,
|
| 46 |
+
timer: 1500,
|
| 47 |
+
timerProgressBar: true,
|
| 48 |
+
didOpen: (toast) => {
|
| 49 |
+
toast.onmouseenter = Swal.stopTimer;
|
| 50 |
+
toast.onmouseleave = Swal.resumeTimer;
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
// 重新加载API密钥数据而不刷新页面
|
| 55 |
+
this.loadApiKeys();
|
| 56 |
+
|
| 57 |
+
Toast.fire({
|
| 58 |
+
icon: 'success',
|
| 59 |
+
title: `成功删除 ${deletedCount} 个API密钥`,
|
| 60 |
+
background: '#fee2e2',
|
| 61 |
+
iconColor: '#ef4444'
|
| 62 |
+
});
|
| 63 |
+
} else {
|
| 64 |
+
Swal.fire({
|
| 65 |
+
icon: 'error',
|
| 66 |
+
title: '批量删除失败',
|
| 67 |
+
text: data.error || '删除操作未能完成,请重试',
|
| 68 |
+
confirmButtonColor: '#0284c7'
|
| 69 |
+
});
|
| 70 |
+
}
|
| 71 |
+
} catch (error) {
|
| 72 |
+
console.error('批量删除API密钥失败:', error);
|
| 73 |
+
Swal.fire({
|
| 74 |
+
icon: 'error',
|
| 75 |
+
title: '服务器错误',
|
| 76 |
+
text: '无法完成删除操作,请稍后重试',
|
| 77 |
+
confirmButtonColor: '#0284c7'
|
| 78 |
+
});
|
| 79 |
+
} finally {
|
| 80 |
+
this.isDeleting = false;
|
| 81 |
+
}
|
| 82 |
+
} else {
|
| 83 |
+
// 单个删除逻辑
|
| 84 |
+
if (!this.deleteKeyId) return;
|
| 85 |
+
|
| 86 |
+
this.isDeleting = true;
|
| 87 |
+
|
| 88 |
+
try {
|
| 89 |
+
const response = await fetch(`/api/keys/${this.deleteKeyId}`, {
|
| 90 |
+
method: 'DELETE',
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
const data = await response.json();
|
| 94 |
+
|
| 95 |
+
if (data.success) {
|
| 96 |
+
// 从本地数组中移除 (创建新数组)
|
| 97 |
+
this.apiKeys = [...this.apiKeys.filter(key => key.id !== this.deleteKeyId)];
|
| 98 |
+
|
| 99 |
+
// 关闭模态框
|
| 100 |
+
this.showDeleteConfirm = false;
|
| 101 |
+
|
| 102 |
+
// 使用Toast风格的通知提示
|
| 103 |
+
const Toast = Swal.mixin({
|
| 104 |
+
toast: true,
|
| 105 |
+
position: 'top-end',
|
| 106 |
+
showConfirmButton: false,
|
| 107 |
+
timer: 1500,
|
| 108 |
+
timerProgressBar: true,
|
| 109 |
+
didOpen: (toast) => {
|
| 110 |
+
toast.onmouseenter = Swal.stopTimer;
|
| 111 |
+
toast.onmouseleave = Swal.resumeTimer;
|
| 112 |
+
}
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
// 重新加载API密钥数据而不刷新页面
|
| 116 |
+
this.loadApiKeys();
|
| 117 |
+
|
| 118 |
+
Toast.fire({
|
| 119 |
+
icon: 'success',
|
| 120 |
+
title: 'API密钥已删除',
|
| 121 |
+
background: '#fee2e2',
|
| 122 |
+
iconColor: '#ef4444'
|
| 123 |
+
});
|
| 124 |
+
} else {
|
| 125 |
+
Swal.fire({
|
| 126 |
+
icon: 'error',
|
| 127 |
+
title: '删除失败',
|
| 128 |
+
text: data.message || '删除操作未能完成,请重试',
|
| 129 |
+
confirmButtonColor: '#0284c7'
|
| 130 |
+
});
|
| 131 |
+
}
|
| 132 |
+
} catch (error) {
|
| 133 |
+
console.error('删除API密钥失败:', error);
|
| 134 |
+
Swal.fire({
|
| 135 |
+
icon: 'error',
|
| 136 |
+
title: '服务器错误',
|
| 137 |
+
text: '无法完成删除操作,请稍后重试',
|
| 138 |
+
confirmButtonColor: '#0284c7'
|
| 139 |
+
});
|
| 140 |
+
} finally {
|
| 141 |
+
this.isDeleting = false;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
static/js/api-key-manager/api-key-editor.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API密钥管理器 - 密钥编辑模块
|
| 3 |
+
* 包含API密钥的编辑和更新功能
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// 打开编辑API密钥模态框
|
| 7 |
+
function editApiKey(id, name, key, platform) {
|
| 8 |
+
// 如果platform参数不存在,尝试从apiKeys中查找
|
| 9 |
+
if (!platform) {
|
| 10 |
+
const apiKey = this.apiKeys.find(key => key.id === id);
|
| 11 |
+
if (apiKey) {
|
| 12 |
+
platform = apiKey.platform;
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
this.editKey = {
|
| 17 |
+
id: id,
|
| 18 |
+
name: name,
|
| 19 |
+
key: key,
|
| 20 |
+
platform: platform
|
| 21 |
+
};
|
| 22 |
+
this.showEditModal = true;
|
| 23 |
+
this.errorMessage = '';
|
| 24 |
+
|
| 25 |
+
// 聚焦到名称输入框
|
| 26 |
+
setTimeout(() => {
|
| 27 |
+
document.getElementById('edit-name').focus();
|
| 28 |
+
}, 100);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// 更新API密钥
|
| 32 |
+
async function updateApiKey() {
|
| 33 |
+
if (!this.editKey.key) {
|
| 34 |
+
this.errorMessage = '请填写API密钥值。';
|
| 35 |
+
return;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
this.isSubmitting = true;
|
| 39 |
+
this.errorMessage = '';
|
| 40 |
+
|
| 41 |
+
try {
|
| 42 |
+
// 检查修改后的key是否与同一平台下的其他key重复
|
| 43 |
+
const currentPlatform = this.editKey.platform;
|
| 44 |
+
const currentId = this.editKey.id;
|
| 45 |
+
// 过滤掉单引号、双引号、小括号、方括号和空格,与添加密钥时保持一致
|
| 46 |
+
const editedKey = this.editKey.key.replace(/['"\,\(\)\[\]\s]/g, '');
|
| 47 |
+
|
| 48 |
+
// 获取同平台下除当前key外的所有key
|
| 49 |
+
const duplicateKey = this.apiKeys.find(apiKey =>
|
| 50 |
+
apiKey.platform === currentPlatform &&
|
| 51 |
+
apiKey.id !== currentId &&
|
| 52 |
+
apiKey.key === editedKey
|
| 53 |
+
);
|
| 54 |
+
|
| 55 |
+
// 如果发现重复key,则自动删除当前key
|
| 56 |
+
if (duplicateKey) {
|
| 57 |
+
// 删除当前key
|
| 58 |
+
const deleteResponse = await fetch(`/api/keys/${currentId}`, {
|
| 59 |
+
method: 'DELETE',
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
const deleteData = await deleteResponse.json();
|
| 63 |
+
|
| 64 |
+
if (deleteData.success) {
|
| 65 |
+
// 关闭模态框
|
| 66 |
+
this.showEditModal = false;
|
| 67 |
+
|
| 68 |
+
// 使用Toast风格的通知提示
|
| 69 |
+
const Toast = Swal.mixin({
|
| 70 |
+
toast: true,
|
| 71 |
+
position: 'top-end',
|
| 72 |
+
showConfirmButton: false,
|
| 73 |
+
timer: 2500,
|
| 74 |
+
timerProgressBar: true,
|
| 75 |
+
didOpen: (toast) => {
|
| 76 |
+
toast.onmouseenter = Swal.stopTimer;
|
| 77 |
+
toast.onmouseleave = Swal.resumeTimer;
|
| 78 |
+
}
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
// 重新加载API密钥数据而不刷新页面
|
| 82 |
+
this.loadApiKeys();
|
| 83 |
+
|
| 84 |
+
Toast.fire({
|
| 85 |
+
icon: 'info',
|
| 86 |
+
title: '发现重复密钥,已自动删除',
|
| 87 |
+
background: '#e0f2fe',
|
| 88 |
+
iconColor: '#0284c7'
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
return;
|
| 92 |
+
} else {
|
| 93 |
+
this.errorMessage = '发现重复密钥,但自动删除失败,请手动处理。';
|
| 94 |
+
this.isSubmitting = false;
|
| 95 |
+
return;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// 如果没有重复,正常更新
|
| 100 |
+
const response = await fetch(`/api/keys/${this.editKey.id}`, {
|
| 101 |
+
method: 'PUT',
|
| 102 |
+
headers: {
|
| 103 |
+
'Content-Type': 'application/json',
|
| 104 |
+
},
|
| 105 |
+
body: JSON.stringify({
|
| 106 |
+
name: this.editKey.name,
|
| 107 |
+
key: editedKey
|
| 108 |
+
}),
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
const data = await response.json();
|
| 112 |
+
|
| 113 |
+
if (data.success) {
|
| 114 |
+
// 关闭模态框
|
| 115 |
+
this.showEditModal = false;
|
| 116 |
+
|
| 117 |
+
// 使用Toast风格的通知提示
|
| 118 |
+
const Toast = Swal.mixin({
|
| 119 |
+
toast: true,
|
| 120 |
+
position: 'top-end',
|
| 121 |
+
showConfirmButton: false,
|
| 122 |
+
timer: 1500,
|
| 123 |
+
timerProgressBar: true,
|
| 124 |
+
didOpen: (toast) => {
|
| 125 |
+
toast.onmouseenter = Swal.stopTimer;
|
| 126 |
+
toast.onmouseleave = Swal.resumeTimer;
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
|
| 130 |
+
// 重新加载API密钥数据而不刷新页面
|
| 131 |
+
this.loadApiKeys();
|
| 132 |
+
|
| 133 |
+
Toast.fire({
|
| 134 |
+
icon: 'success',
|
| 135 |
+
title: 'API密钥已更新',
|
| 136 |
+
background: '#f0fdf4',
|
| 137 |
+
iconColor: '#16a34a'
|
| 138 |
+
});
|
| 139 |
+
} else {
|
| 140 |
+
this.errorMessage = data.error || '更新失败,请重试。';
|
| 141 |
+
}
|
| 142 |
+
} catch (error) {
|
| 143 |
+
console.error('更新API密钥失败:', error);
|
| 144 |
+
this.errorMessage = '服务器错误,请重试。';
|
| 145 |
+
} finally {
|
| 146 |
+
this.isSubmitting = false;
|
| 147 |
+
}
|
| 148 |
+
}
|
static/js/api-key-manager/api-key-loader.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API密钥管理器 - 密钥加载模块
|
| 3 |
+
* 包含API密钥的加载功能
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
// 加载API密钥
|
| 7 |
+
async function loadApiKeys() {
|
| 8 |
+
this.isLoading = true;
|
| 9 |
+
const startTime = Date.now();
|
| 10 |
+
try {
|
| 11 |
+
// 通过AJAX获取完整的HTML部分而不仅仅是JSON数据
|
| 12 |
+
const response = await fetch('/?ajax=1');
|
| 13 |
+
const html = await response.text();
|
| 14 |
+
|
| 15 |
+
// 创建一个临时容器来解析HTML
|
| 16 |
+
const tempContainer = document.createElement('div');
|
| 17 |
+
tempContainer.innerHTML = html;
|
| 18 |
+
|
| 19 |
+
// 提取新的API密钥列表HTML
|
| 20 |
+
const newKeyListHtml = tempContainer.querySelector('.space-y-6').outerHTML;
|
| 21 |
+
|
| 22 |
+
// 替换当前页面上的API密钥列表
|
| 23 |
+
document.querySelector('.space-y-6').outerHTML = newKeyListHtml;
|
| 24 |
+
|
| 25 |
+
// 重新初始化必要的事件监听器和组件
|
| 26 |
+
initScrollContainers();
|
| 27 |
+
|
| 28 |
+
// 同时更新本地数据
|
| 29 |
+
const jsonResponse = await fetch('/api/keys');
|
| 30 |
+
const data = await jsonResponse.json();
|
| 31 |
+
this.apiKeys = [...(data.api_keys || [])];
|
| 32 |
+
|
| 33 |
+
// 显式重置 selectedKeys 和 selectedPlatforms
|
| 34 |
+
this.selectedKeys = [];
|
| 35 |
+
this.selectedPlatforms = [];
|
| 36 |
+
|
| 37 |
+
// 确保加载动画至少显示200毫秒,使体验更平滑
|
| 38 |
+
const elapsedTime = Date.now() - startTime;
|
| 39 |
+
const minLoadTime = 200; // 最小加载时间(毫秒)
|
| 40 |
+
|
| 41 |
+
if (elapsedTime < minLoadTime) {
|
| 42 |
+
await new Promise(resolve => setTimeout(resolve, minLoadTime - elapsedTime));
|
| 43 |
+
}
|
| 44 |
+
} catch (error) {
|
| 45 |
+
console.error('加载API密钥失败:', error);
|
| 46 |
+
Swal.fire({
|
| 47 |
+
icon: 'error',
|
| 48 |
+
title: '加载失败',
|
| 49 |
+
text: '无法加载API密钥,请刷新页面重试',
|
| 50 |
+
confirmButtonColor: '#0284c7'
|
| 51 |
+
});
|
| 52 |
+
} finally {
|
| 53 |
+
this.isLoading = false;
|
| 54 |
+
}
|
| 55 |
+
}
|
static/js/api-key-manager/bulk-actions.js
CHANGED
|
@@ -82,15 +82,21 @@ function getAllVisibleKeyIds() {
|
|
| 82 |
function toggleSelectAll() {
|
| 83 |
const allIds = this.getAllVisibleKeyIds();
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
if (this.isAllSelected) {
|
| 86 |
// 如果当前是全选状态,则取消全选
|
| 87 |
-
this.selectedKeys = this.selectedKeys.filter(id => !
|
| 88 |
} else {
|
| 89 |
-
//
|
| 90 |
// 先合并当前已选中的ID
|
| 91 |
const newSelection = [...this.selectedKeys];
|
| 92 |
// 添加所有未选中的可见ID
|
| 93 |
-
|
| 94 |
if (!newSelection.includes(id)) {
|
| 95 |
newSelection.push(id);
|
| 96 |
}
|
|
|
|
| 82 |
function toggleSelectAll() {
|
| 83 |
const allIds = this.getAllVisibleKeyIds();
|
| 84 |
|
| 85 |
+
// 过滤出只属于选中平台的密钥ID
|
| 86 |
+
const filteredIds = allIds.filter(id => {
|
| 87 |
+
const key = this.apiKeys.find(k => k.id === id);
|
| 88 |
+
return key && this.platformFilters[key.platform] === true;
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
if (this.isAllSelected) {
|
| 92 |
// 如果当前是全选状态,则取消全选
|
| 93 |
+
this.selectedKeys = this.selectedKeys.filter(id => !filteredIds.includes(id));
|
| 94 |
} else {
|
| 95 |
+
// 否则选中所有可见且属于选中平台的密钥
|
| 96 |
// 先合并当前已选中的ID
|
| 97 |
const newSelection = [...this.selectedKeys];
|
| 98 |
// 添加所有未选中的可见ID
|
| 99 |
+
filteredIds.forEach(id => {
|
| 100 |
if (!newSelection.includes(id)) {
|
| 101 |
newSelection.push(id);
|
| 102 |
}
|
static/js/api-key-manager/core.js
CHANGED
|
@@ -7,6 +7,9 @@ function initApiKeyManager() {
|
|
| 7 |
// 初始化各平台的折叠状态和筛选状态
|
| 8 |
const platforms = JSON.parse(platformsData);
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
// 从localStorage读取平台展开状态,如果有的话
|
| 11 |
const savedPlatformStates = localStorage.getItem('platformStates');
|
| 12 |
const parsedPlatformStates = savedPlatformStates ? JSON.parse(savedPlatformStates) : {};
|
|
|
|
| 7 |
// 初始化各平台的折叠状态和筛选状态
|
| 8 |
const platforms = JSON.parse(platformsData);
|
| 9 |
|
| 10 |
+
// 初始化平台ID列表
|
| 11 |
+
this.platformIds = platforms.map(platform => platform.id);
|
| 12 |
+
|
| 13 |
// 从localStorage读取平台展开状态,如果有的话
|
| 14 |
const savedPlatformStates = localStorage.getItem('platformStates');
|
| 15 |
const parsedPlatformStates = savedPlatformStates ? JSON.parse(savedPlatformStates) : {};
|
static/js/api-key-manager/key-operations.js
CHANGED
|
@@ -1,501 +1,21 @@
|
|
| 1 |
/**
|
| 2 |
-
* API密钥管理器 -
|
| 3 |
-
*
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
//
|
| 7 |
-
async function loadApiKeys() {
|
| 8 |
-
this.isLoading = true;
|
| 9 |
-
const startTime = Date.now();
|
| 10 |
-
try {
|
| 11 |
-
// 通过AJAX获取完整的HTML部分而不仅仅是JSON数据
|
| 12 |
-
const response = await fetch('/?ajax=1');
|
| 13 |
-
const html = await response.text();
|
| 14 |
-
|
| 15 |
-
// 创建一个临时容器来解析HTML
|
| 16 |
-
const tempContainer = document.createElement('div');
|
| 17 |
-
tempContainer.innerHTML = html;
|
| 18 |
-
|
| 19 |
-
// 提取新的API密钥列表HTML
|
| 20 |
-
const newKeyListHtml = tempContainer.querySelector('.space-y-6').outerHTML;
|
| 21 |
-
|
| 22 |
-
// 替换当前页面上的API密钥列表
|
| 23 |
-
document.querySelector('.space-y-6').outerHTML = newKeyListHtml;
|
| 24 |
-
|
| 25 |
-
// 重新初始化必要的事件监听器和组件
|
| 26 |
-
initScrollContainers();
|
| 27 |
-
|
| 28 |
-
// 同时更新本地数据
|
| 29 |
-
const jsonResponse = await fetch('/api/keys');
|
| 30 |
-
const data = await jsonResponse.json();
|
| 31 |
-
this.apiKeys = [...(data.api_keys || [])];
|
| 32 |
-
|
| 33 |
-
// 显式重置 selectedKeys 和 selectedPlatforms
|
| 34 |
-
this.selectedKeys = [];
|
| 35 |
-
this.selectedPlatforms = [];
|
| 36 |
-
|
| 37 |
-
// 确保加载动画至少显示200毫秒,使体验更平滑
|
| 38 |
-
const elapsedTime = Date.now() - startTime;
|
| 39 |
-
const minLoadTime = 200; // 最小加载时间(毫秒)
|
| 40 |
-
|
| 41 |
-
if (elapsedTime < minLoadTime) {
|
| 42 |
-
await new Promise(resolve => setTimeout(resolve, minLoadTime - elapsedTime));
|
| 43 |
-
}
|
| 44 |
-
} catch (error) {
|
| 45 |
-
console.error('加载API密钥失败:', error);
|
| 46 |
-
Swal.fire({
|
| 47 |
-
icon: 'error',
|
| 48 |
-
title: '加载失败',
|
| 49 |
-
text: '无法加载API密钥,请刷新页面重试',
|
| 50 |
-
confirmButtonColor: '#0284c7'
|
| 51 |
-
});
|
| 52 |
-
} finally {
|
| 53 |
-
this.isLoading = false;
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
|
| 57 |
-
//
|
| 58 |
-
|
| 59 |
-
if (!this.newKey.platform || !this.newKey.key) {
|
| 60 |
-
this.errorMessage = '请填写所有必填字段。';
|
| 61 |
-
return;
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
// 如果名称为空,生成自动名称
|
| 65 |
-
if (!this.newKey.name.trim()) {
|
| 66 |
-
const date = new Date();
|
| 67 |
-
const dateStr = date.toLocaleDateString('zh-CN', {
|
| 68 |
-
year: 'numeric',
|
| 69 |
-
month: '2-digit',
|
| 70 |
-
day: '2-digit'
|
| 71 |
-
}).replace(/\//g, '-');
|
| 72 |
-
const timeStr = date.toLocaleTimeString('zh-CN', {
|
| 73 |
-
hour: '2-digit',
|
| 74 |
-
minute: '2-digit'
|
| 75 |
-
});
|
| 76 |
-
this.newKey.name = `${dateStr} ${timeStr}`;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
// 保存当前选择的平台类型
|
| 80 |
-
localStorage.setItem('lastSelectedPlatform', this.newKey.platform);
|
| 81 |
-
|
| 82 |
-
this.isSubmitting = true;
|
| 83 |
-
this.errorMessage = '';
|
| 84 |
-
|
| 85 |
-
try {
|
| 86 |
-
// 处理输入文本:去除单引号、双引号、小括号、方括号、空格,然后分行
|
| 87 |
-
const lines = this.newKey.key
|
| 88 |
-
.split('\n')
|
| 89 |
-
.map(line => line.replace(/['"\(\)\[\]\s]/g, '')) // 去除单引号、双引号、小括号、方括号和空格
|
| 90 |
-
.filter(line => line.length > 0); // 过滤掉空行
|
| 91 |
-
|
| 92 |
-
// 从每一行中提取逗号分隔的非空元素,作为单独的key
|
| 93 |
-
let keysWithDuplicates = [];
|
| 94 |
-
for (const line of lines) {
|
| 95 |
-
const lineKeys = line.split(',')
|
| 96 |
-
.filter(item => item.length > 0); // 过滤掉空元素
|
| 97 |
-
|
| 98 |
-
// 将每个非空元素添加到数组
|
| 99 |
-
keysWithDuplicates.push(...lineKeys);
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
if (keysWithDuplicates.length === 0) {
|
| 103 |
-
this.errorMessage = '请输入至少一个有效的API密钥。';
|
| 104 |
-
this.isSubmitting = false;
|
| 105 |
-
return;
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
// 去除输入中重复的key(同一次提交中的重复)
|
| 109 |
-
const inputDuplicatesCount = keysWithDuplicates.length - new Set(keysWithDuplicates).size;
|
| 110 |
-
const keys = [...new Set(keysWithDuplicates)]; // 使用Set去重,得到唯一的keys数组
|
| 111 |
-
|
| 112 |
-
// 过滤掉已存在于同一平台的重复key
|
| 113 |
-
const currentPlatform = this.newKey.platform;
|
| 114 |
-
const existingKeys = this.apiKeys
|
| 115 |
-
.filter(apiKey => apiKey.platform === currentPlatform)
|
| 116 |
-
.map(apiKey => apiKey.key);
|
| 117 |
-
|
| 118 |
-
const uniqueKeys = keys.filter(key => !existingKeys.includes(key));
|
| 119 |
-
const duplicateCount = keys.length - uniqueKeys.length;
|
| 120 |
-
|
| 121 |
-
// 如果所有key都重复,显示错误消息并退出
|
| 122 |
-
if (uniqueKeys.length === 0) {
|
| 123 |
-
this.errorMessage = '所有输入的API密钥在当前平台中已存在。';
|
| 124 |
-
this.isSubmitting = false;
|
| 125 |
-
return;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
// 批量添加API密钥(只添加不重复的key)
|
| 129 |
-
const results = [];
|
| 130 |
-
let allSuccess = true;
|
| 131 |
-
|
| 132 |
-
// 记录重复和唯一的key数量,用于显示通知
|
| 133 |
-
const skippedCount = duplicateCount;
|
| 134 |
-
const addedCount = uniqueKeys.length;
|
| 135 |
-
|
| 136 |
-
for (const keyText of uniqueKeys) {
|
| 137 |
-
const keyData = {
|
| 138 |
-
platform: this.newKey.platform,
|
| 139 |
-
name: this.newKey.name,
|
| 140 |
-
key: keyText
|
| 141 |
-
};
|
| 142 |
-
|
| 143 |
-
const response = await fetch('/api/keys', {
|
| 144 |
-
method: 'POST',
|
| 145 |
-
headers: {
|
| 146 |
-
'Content-Type': 'application/json',
|
| 147 |
-
},
|
| 148 |
-
body: JSON.stringify(keyData),
|
| 149 |
-
});
|
| 150 |
-
|
| 151 |
-
const data = await response.json();
|
| 152 |
-
results.push(data);
|
| 153 |
-
|
| 154 |
-
if (!data.success) {
|
| 155 |
-
allSuccess = false;
|
| 156 |
-
}
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
if (allSuccess) {
|
| 160 |
-
// 关闭模态框并重置表单
|
| 161 |
-
this.showAddModal = false;
|
| 162 |
-
this.newKey = {
|
| 163 |
-
platform: this.newKey.platform, // 保留平台选择
|
| 164 |
-
name: '',
|
| 165 |
-
key: ''
|
| 166 |
-
};
|
| 167 |
-
|
| 168 |
-
// 使用Toast风格的通知提示
|
| 169 |
-
const Toast = Swal.mixin({
|
| 170 |
-
toast: true,
|
| 171 |
-
position: 'top-end',
|
| 172 |
-
showConfirmButton: false,
|
| 173 |
-
timer: 2500,
|
| 174 |
-
timerProgressBar: true,
|
| 175 |
-
didOpen: (toast) => {
|
| 176 |
-
toast.onmouseenter = Swal.stopTimer;
|
| 177 |
-
toast.onmouseleave = Swal.resumeTimer;
|
| 178 |
-
}
|
| 179 |
-
});
|
| 180 |
-
|
| 181 |
-
// 重新加载API密钥数据而不刷新页面
|
| 182 |
-
this.loadApiKeys();
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
// 根据不同情况显示通知
|
| 188 |
-
if (inputDuplicatesCount > 0 && skippedCount > 0) {
|
| 189 |
-
// 既有输入中的重复,也有数据库中的重复
|
| 190 |
-
title += `,跳过 ${inputDuplicatesCount} 个输入重复和 ${skippedCount} 个已存在密钥`;
|
| 191 |
-
} else if (inputDuplicatesCount > 0) {
|
| 192 |
-
// 只有输入中的重复
|
| 193 |
-
title += `,跳过 ${inputDuplicatesCount} 个输入重复密钥`;
|
| 194 |
-
} else if (skippedCount > 0) {
|
| 195 |
-
// 只有数据库中的重复
|
| 196 |
-
title += `,跳过 ${skippedCount} 个已存在密钥`;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
Toast.fire({
|
| 200 |
-
icon: 'success',
|
| 201 |
-
title: title,
|
| 202 |
-
background: '#f0fdf4',
|
| 203 |
-
iconColor: '#16a34a'
|
| 204 |
-
});
|
| 205 |
-
} else {
|
| 206 |
-
// 部分失败或全部失败
|
| 207 |
-
const successCount = results.filter(r => r.success).length;
|
| 208 |
-
const failCount = results.length - successCount;
|
| 209 |
-
|
| 210 |
-
this.errorMessage = `添加操作部分失败: 成功 ${successCount} 个, 失败 ${failCount} 个`;
|
| 211 |
-
}
|
| 212 |
-
} catch (error) {
|
| 213 |
-
console.error('添加API密钥失败:', error);
|
| 214 |
-
this.errorMessage = '服务器错误,请重试。';
|
| 215 |
-
} finally {
|
| 216 |
-
this.isSubmitting = false;
|
| 217 |
-
}
|
| 218 |
-
}
|
| 219 |
|
| 220 |
-
//
|
| 221 |
-
|
| 222 |
-
this.deleteKeyId = id;
|
| 223 |
-
this.deleteKeyName = name;
|
| 224 |
-
this.showDeleteConfirm = true;
|
| 225 |
-
}
|
| 226 |
|
| 227 |
-
//
|
| 228 |
-
|
| 229 |
-
if (this.isBulkDelete) {
|
| 230 |
-
if (this.selectedKeys.length === 0) return;
|
| 231 |
-
|
| 232 |
-
this.isDeleting = true;
|
| 233 |
-
|
| 234 |
-
try {
|
| 235 |
-
const response = await fetch('/api/keys/bulk-delete', {
|
| 236 |
-
method: 'POST',
|
| 237 |
-
headers: {
|
| 238 |
-
'Content-Type': 'application/json',
|
| 239 |
-
},
|
| 240 |
-
body: JSON.stringify({ ids: this.selectedKeys }),
|
| 241 |
-
});
|
| 242 |
-
|
| 243 |
-
const data = await response.json();
|
| 244 |
-
|
| 245 |
-
if (data.success) {
|
| 246 |
-
// 关闭模态框,清空选中数组
|
| 247 |
-
this.showDeleteConfirm = false;
|
| 248 |
-
this.isBulkDelete = false;
|
| 249 |
-
const deletedCount = data.deleted_count || this.selectedKeys.length;
|
| 250 |
-
|
| 251 |
-
// 清空选中数组
|
| 252 |
-
this.selectedKeys = [];
|
| 253 |
-
this.selectedPlatforms = [];
|
| 254 |
-
|
| 255 |
-
// 使用Toast风格的通知提示
|
| 256 |
-
const Toast = Swal.mixin({
|
| 257 |
-
toast: true,
|
| 258 |
-
position: 'top-end',
|
| 259 |
-
showConfirmButton: false,
|
| 260 |
-
timer: 1500,
|
| 261 |
-
timerProgressBar: true,
|
| 262 |
-
didOpen: (toast) => {
|
| 263 |
-
toast.onmouseenter = Swal.stopTimer;
|
| 264 |
-
toast.onmouseleave = Swal.resumeTimer;
|
| 265 |
-
}
|
| 266 |
-
});
|
| 267 |
-
|
| 268 |
-
// 重新加载API密钥数据而不刷新页面
|
| 269 |
-
this.loadApiKeys();
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
title: `成功删除 ${deletedCount} 个API密钥`,
|
| 274 |
-
background: '#fee2e2',
|
| 275 |
-
iconColor: '#ef4444'
|
| 276 |
-
});
|
| 277 |
-
} else {
|
| 278 |
-
Swal.fire({
|
| 279 |
-
icon: 'error',
|
| 280 |
-
title: '批量删除失败',
|
| 281 |
-
text: data.error || '删除操作未能完成,请重试',
|
| 282 |
-
confirmButtonColor: '#0284c7'
|
| 283 |
-
});
|
| 284 |
-
}
|
| 285 |
-
} catch (error) {
|
| 286 |
-
console.error('批量删除API密钥失败:', error);
|
| 287 |
-
Swal.fire({
|
| 288 |
-
icon: 'error',
|
| 289 |
-
title: '服务器错误',
|
| 290 |
-
text: '无法完成删除操作,请稍后重试',
|
| 291 |
-
confirmButtonColor: '#0284c7'
|
| 292 |
-
});
|
| 293 |
-
} finally {
|
| 294 |
-
this.isDeleting = false;
|
| 295 |
-
}
|
| 296 |
-
} else {
|
| 297 |
-
// 单个删除逻辑
|
| 298 |
-
if (!this.deleteKeyId) return;
|
| 299 |
-
|
| 300 |
-
this.isDeleting = true;
|
| 301 |
-
|
| 302 |
-
try {
|
| 303 |
-
const response = await fetch(`/api/keys/${this.deleteKeyId}`, {
|
| 304 |
-
method: 'DELETE',
|
| 305 |
-
});
|
| 306 |
-
|
| 307 |
-
const data = await response.json();
|
| 308 |
-
|
| 309 |
-
if (data.success) {
|
| 310 |
-
// 从本地数组中移除 (创建新数组)
|
| 311 |
-
this.apiKeys = [...this.apiKeys.filter(key => key.id !== this.deleteKeyId)];
|
| 312 |
-
|
| 313 |
-
// 关闭模态框
|
| 314 |
-
this.showDeleteConfirm = false;
|
| 315 |
-
|
| 316 |
-
// 使用Toast风格的通知提示
|
| 317 |
-
const Toast = Swal.mixin({
|
| 318 |
-
toast: true,
|
| 319 |
-
position: 'top-end',
|
| 320 |
-
showConfirmButton: false,
|
| 321 |
-
timer: 1500,
|
| 322 |
-
timerProgressBar: true,
|
| 323 |
-
didOpen: (toast) => {
|
| 324 |
-
toast.onmouseenter = Swal.stopTimer;
|
| 325 |
-
toast.onmouseleave = Swal.resumeTimer;
|
| 326 |
-
}
|
| 327 |
-
});
|
| 328 |
-
|
| 329 |
-
// 重新加载API密钥数据而不刷新页面
|
| 330 |
-
this.loadApiKeys();
|
| 331 |
-
|
| 332 |
-
Toast.fire({
|
| 333 |
-
icon: 'success',
|
| 334 |
-
title: 'API密钥已删除',
|
| 335 |
-
background: '#fee2e2',
|
| 336 |
-
iconColor: '#ef4444'
|
| 337 |
-
});
|
| 338 |
-
} else {
|
| 339 |
-
Swal.fire({
|
| 340 |
-
icon: 'error',
|
| 341 |
-
title: '删除失败',
|
| 342 |
-
text: data.message || '删除操作未能完成,请重试',
|
| 343 |
-
confirmButtonColor: '#0284c7'
|
| 344 |
-
});
|
| 345 |
-
}
|
| 346 |
-
} catch (error) {
|
| 347 |
-
console.error('删除API密钥失败:', error);
|
| 348 |
-
Swal.fire({
|
| 349 |
-
icon: 'error',
|
| 350 |
-
title: '服务器错误',
|
| 351 |
-
text: '无法完成删除操作,请稍后重试',
|
| 352 |
-
confirmButtonColor: '#0284c7'
|
| 353 |
-
});
|
| 354 |
-
} finally {
|
| 355 |
-
this.isDeleting = false;
|
| 356 |
-
}
|
| 357 |
-
}
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
// 打开编辑API密钥模态框
|
| 361 |
-
function editApiKey(id, name, key, platform) {
|
| 362 |
-
// 如果platform参数不存在,尝试从apiKeys中查找
|
| 363 |
-
if (!platform) {
|
| 364 |
-
const apiKey = this.apiKeys.find(key => key.id === id);
|
| 365 |
-
if (apiKey) {
|
| 366 |
-
platform = apiKey.platform;
|
| 367 |
-
}
|
| 368 |
-
}
|
| 369 |
-
|
| 370 |
-
this.editKey = {
|
| 371 |
-
id: id,
|
| 372 |
-
name: name,
|
| 373 |
-
key: key,
|
| 374 |
-
platform: platform
|
| 375 |
-
};
|
| 376 |
-
this.showEditModal = true;
|
| 377 |
-
this.errorMessage = '';
|
| 378 |
-
|
| 379 |
-
// 聚焦到名称输入框
|
| 380 |
-
setTimeout(() => {
|
| 381 |
-
document.getElementById('edit-name').focus();
|
| 382 |
-
}, 100);
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
// 更新API密钥
|
| 386 |
-
async function updateApiKey() {
|
| 387 |
-
if (!this.editKey.key) {
|
| 388 |
-
this.errorMessage = '请填写API密钥值。';
|
| 389 |
-
return;
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
this.isSubmitting = true;
|
| 393 |
-
this.errorMessage = '';
|
| 394 |
-
|
| 395 |
-
try {
|
| 396 |
-
// 检查修改后的key是否与同一平台下的其他key重复
|
| 397 |
-
const currentPlatform = this.editKey.platform;
|
| 398 |
-
const currentId = this.editKey.id;
|
| 399 |
-
const editedKey = this.editKey.key.trim();
|
| 400 |
-
|
| 401 |
-
// 获取同平台下除当前key外的所有key
|
| 402 |
-
const duplicateKey = this.apiKeys.find(apiKey =>
|
| 403 |
-
apiKey.platform === currentPlatform &&
|
| 404 |
-
apiKey.id !== currentId &&
|
| 405 |
-
apiKey.key === editedKey
|
| 406 |
-
);
|
| 407 |
-
|
| 408 |
-
// 如果发现重复key,则自动删除当前key
|
| 409 |
-
if (duplicateKey) {
|
| 410 |
-
// 删除当前key
|
| 411 |
-
const deleteResponse = await fetch(`/api/keys/${currentId}`, {
|
| 412 |
-
method: 'DELETE',
|
| 413 |
-
});
|
| 414 |
-
|
| 415 |
-
const deleteData = await deleteResponse.json();
|
| 416 |
-
|
| 417 |
-
if (deleteData.success) {
|
| 418 |
-
// 关闭模态框
|
| 419 |
-
this.showEditModal = false;
|
| 420 |
-
|
| 421 |
-
// 使用Toast风格的通知提示
|
| 422 |
-
const Toast = Swal.mixin({
|
| 423 |
-
toast: true,
|
| 424 |
-
position: 'top-end',
|
| 425 |
-
showConfirmButton: false,
|
| 426 |
-
timer: 2500,
|
| 427 |
-
timerProgressBar: true,
|
| 428 |
-
didOpen: (toast) => {
|
| 429 |
-
toast.onmouseenter = Swal.stopTimer;
|
| 430 |
-
toast.onmouseleave = Swal.resumeTimer;
|
| 431 |
-
}
|
| 432 |
-
});
|
| 433 |
-
|
| 434 |
-
// 重新加载API密钥数据而不刷新页面
|
| 435 |
-
this.loadApiKeys();
|
| 436 |
-
|
| 437 |
-
Toast.fire({
|
| 438 |
-
icon: 'info',
|
| 439 |
-
title: '发现重复密钥,已自动删除',
|
| 440 |
-
background: '#e0f2fe',
|
| 441 |
-
iconColor: '#0284c7'
|
| 442 |
-
});
|
| 443 |
-
|
| 444 |
-
return;
|
| 445 |
-
} else {
|
| 446 |
-
this.errorMessage = '发现重复密钥,但自动删除失败,请手动处理。';
|
| 447 |
-
this.isSubmitting = false;
|
| 448 |
-
return;
|
| 449 |
-
}
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
// 如果没有重复,正常更新
|
| 453 |
-
const response = await fetch(`/api/keys/${this.editKey.id}`, {
|
| 454 |
-
method: 'PUT',
|
| 455 |
-
headers: {
|
| 456 |
-
'Content-Type': 'application/json',
|
| 457 |
-
},
|
| 458 |
-
body: JSON.stringify({
|
| 459 |
-
name: this.editKey.name,
|
| 460 |
-
key: editedKey
|
| 461 |
-
}),
|
| 462 |
-
});
|
| 463 |
-
|
| 464 |
-
const data = await response.json();
|
| 465 |
-
|
| 466 |
-
if (data.success) {
|
| 467 |
-
// 关闭模态框
|
| 468 |
-
this.showEditModal = false;
|
| 469 |
-
|
| 470 |
-
// 使用Toast风格的通知提示
|
| 471 |
-
const Toast = Swal.mixin({
|
| 472 |
-
toast: true,
|
| 473 |
-
position: 'top-end',
|
| 474 |
-
showConfirmButton: false,
|
| 475 |
-
timer: 1500,
|
| 476 |
-
timerProgressBar: true,
|
| 477 |
-
didOpen: (toast) => {
|
| 478 |
-
toast.onmouseenter = Swal.stopTimer;
|
| 479 |
-
toast.onmouseleave = Swal.resumeTimer;
|
| 480 |
-
}
|
| 481 |
-
});
|
| 482 |
-
|
| 483 |
-
// 重新加载API密钥数据而不刷新页面
|
| 484 |
-
this.loadApiKeys();
|
| 485 |
-
|
| 486 |
-
Toast.fire({
|
| 487 |
-
icon: 'success',
|
| 488 |
-
title: 'API密钥已更新',
|
| 489 |
-
background: '#f0fdf4',
|
| 490 |
-
iconColor: '#16a34a'
|
| 491 |
-
});
|
| 492 |
-
} else {
|
| 493 |
-
this.errorMessage = data.error || '更新失败,请重试。';
|
| 494 |
-
}
|
| 495 |
-
} catch (error) {
|
| 496 |
-
console.error('更新API密钥失败:', error);
|
| 497 |
-
this.errorMessage = '服务器错误,请重试。';
|
| 498 |
-
} finally {
|
| 499 |
-
this.isSubmitting = false;
|
| 500 |
-
}
|
| 501 |
-
}
|
|
|
|
| 1 |
/**
|
| 2 |
+
* API密钥管理器 - 密钥操作模块(入口文件)
|
| 3 |
+
* 整合所有API密钥操作模块的功能
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
// 从各个模块导入功能
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
// 从加载模块导入
|
| 9 |
+
document.write('<script src="/static/js/api-key-manager/api-key-loader.js"></script>');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
// 从创建模块导入
|
| 12 |
+
document.write('<script src="/static/js/api-key-manager/api-key-creator.js"></script>');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
+
// 从编辑模块导入
|
| 15 |
+
document.write('<script src="/static/js/api-key-manager/api-key-editor.js"></script>');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
// 从删除模块导入
|
| 18 |
+
document.write('<script src="/static/js/api-key-manager/api-key-deleter.js"></script>');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
// 注意:上述导入方式让各个模块中的函数直接在全局范围可用
|
| 21 |
+
// 这样保持了与原始文件相同的使用方式,确保兼容性
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/js/api-key-manager/main-manager.js
CHANGED
|
@@ -6,6 +6,7 @@ function apiKeyManager() {
|
|
| 6 |
apiKeys: [],
|
| 7 |
platformStates: {},
|
| 8 |
platformFilters: {}, // 平台筛选状态
|
|
|
|
| 9 |
allPlatformsSelected: true, // 是否选择所有平台
|
| 10 |
searchTerm: '',
|
| 11 |
showAddModal: false,
|
|
@@ -78,9 +79,16 @@ function apiKeyManager() {
|
|
| 78 |
get isAllSelected() {
|
| 79 |
// 获取所有可见密钥的ID
|
| 80 |
const allVisibleKeyIds = this.getAllVisibleKeyIds();
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
},
|
| 85 |
|
| 86 |
// 切换全选/取消全选
|
|
|
|
| 6 |
apiKeys: [],
|
| 7 |
platformStates: {},
|
| 8 |
platformFilters: {}, // 平台筛选状态
|
| 9 |
+
platformIds: [], // 所有平台ID列表
|
| 10 |
allPlatformsSelected: true, // 是否选择所有平台
|
| 11 |
searchTerm: '',
|
| 12 |
showAddModal: false,
|
|
|
|
| 79 |
get isAllSelected() {
|
| 80 |
// 获取所有可见密钥的ID
|
| 81 |
const allVisibleKeyIds = this.getAllVisibleKeyIds();
|
| 82 |
+
|
| 83 |
+
// 过滤出只属于选中平台的密钥ID
|
| 84 |
+
const filteredIds = allVisibleKeyIds.filter(id => {
|
| 85 |
+
const key = this.apiKeys.find(k => k.id === id);
|
| 86 |
+
return key && this.platformFilters[key.platform] === true;
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
// 全选需要满足:有属于选中平台的可见密钥,且这些密钥都被选中
|
| 90 |
+
return filteredIds.length > 0 &&
|
| 91 |
+
filteredIds.every(id => this.selectedKeys.includes(id));
|
| 92 |
},
|
| 93 |
|
| 94 |
// 切换全选/取消全选
|
static/js/api-key-manager/platform-utils.js
CHANGED
|
@@ -42,6 +42,11 @@ function getPlatformStyles() {
|
|
| 42 |
return JSON.parse(platformStylesData);
|
| 43 |
}
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
// 获取特定平台的样式
|
| 46 |
function getPlatformStyle(platformId) {
|
| 47 |
const styles = getPlatformStyles();
|
|
@@ -52,6 +57,21 @@ function getPlatformStyle(platformId) {
|
|
| 52 |
function togglePlatformFilter(platformId) {
|
| 53 |
this.platformFilters[platformId] = !this.platformFilters[platformId];
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
// 检查是否所有平台都被选中
|
| 56 |
const platforms = this.getPlatforms();
|
| 57 |
this.allPlatformsSelected = platforms.every(platform =>
|
|
@@ -73,6 +93,15 @@ function toggleAllPlatformFilters() {
|
|
| 73 |
this.platformFilters[platform.id] = newState;
|
| 74 |
});
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
// 保存筛选状态到localStorage
|
| 77 |
localStorage.setItem('platformFilters', JSON.stringify(this.platformFilters));
|
| 78 |
}
|
|
|
|
| 42 |
return JSON.parse(platformStylesData);
|
| 43 |
}
|
| 44 |
|
| 45 |
+
// 获取所有平台的ID
|
| 46 |
+
function getPlatformIds() {
|
| 47 |
+
return this.getPlatforms().map(platform => platform.id);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
// 获取特定平台的样式
|
| 51 |
function getPlatformStyle(platformId) {
|
| 52 |
const styles = getPlatformStyles();
|
|
|
|
| 57 |
function togglePlatformFilter(platformId) {
|
| 58 |
this.platformFilters[platformId] = !this.platformFilters[platformId];
|
| 59 |
|
| 60 |
+
// 如果取消平台筛选,同时取消该平台及其下所有密钥的选择
|
| 61 |
+
if (this.platformFilters[platformId] === false) {
|
| 62 |
+
// 如果平台在选中列表中,移除它
|
| 63 |
+
const platformIndex = this.selectedPlatforms.indexOf(platformId);
|
| 64 |
+
if (platformIndex !== -1) {
|
| 65 |
+
this.selectedPlatforms.splice(platformIndex, 1);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// 取消选中该平台下的所有密钥
|
| 69 |
+
this.selectedKeys = this.selectedKeys.filter(keyId => {
|
| 70 |
+
const key = this.apiKeys.find(k => k.id === keyId);
|
| 71 |
+
return key && key.platform !== platformId;
|
| 72 |
+
});
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
// 检查是否所有平台都被选中
|
| 76 |
const platforms = this.getPlatforms();
|
| 77 |
this.allPlatformsSelected = platforms.every(platform =>
|
|
|
|
| 93 |
this.platformFilters[platform.id] = newState;
|
| 94 |
});
|
| 95 |
|
| 96 |
+
// 如果取消全部平台筛选,同时取消所有平台和密钥的选择
|
| 97 |
+
if (newState === false) {
|
| 98 |
+
// 清空选中的平台
|
| 99 |
+
this.selectedPlatforms = [];
|
| 100 |
+
|
| 101 |
+
// 清空选中的密钥
|
| 102 |
+
this.selectedKeys = [];
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
// 保存筛选状态到localStorage
|
| 106 |
localStorage.setItem('platformFilters', JSON.stringify(this.platformFilters));
|
| 107 |
}
|
templates/base.html
CHANGED
|
@@ -65,7 +65,7 @@
|
|
| 65 |
}
|
| 66 |
</script>
|
| 67 |
<!-- Alpine.js -->
|
| 68 |
-
<script defer src="https://unpkg.com/alpinejs@3.
|
| 69 |
<!-- Clipboard.js -->
|
| 70 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
|
| 71 |
<!-- SweetAlert2 -->
|
|
|
|
| 65 |
}
|
| 66 |
</script>
|
| 67 |
<!-- Alpine.js -->
|
| 68 |
+
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
| 69 |
<!-- Clipboard.js -->
|
| 70 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
|
| 71 |
<!-- SweetAlert2 -->
|
templates/components/api_key_list.html
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<!-- 分组的API密钥列表 -->
|
| 2 |
<div class="space-y-6">
|
| 3 |
<!-- 遍历每个平台 -->
|
|
@@ -17,7 +82,7 @@
|
|
| 17 |
@click="togglePlatform('{{ platform.id }}')"
|
| 18 |
class="px-6 py-4 cursor-pointer flex justify-between items-center border-b border-gray-200 transition-all duration-300"
|
| 19 |
:class="{
|
| 20 |
-
'bg-gray-50 border-gray-200': !
|
| 21 |
}"
|
| 22 |
:style="{
|
| 23 |
backgroundColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['background-color'] : 'rgba(243, 244, 246, 0.5)',
|
|
@@ -84,71 +149,6 @@
|
|
| 84 |
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
| 85 |
</svg>
|
| 86 |
</div>
|
| 87 |
-
|
| 88 |
-
<!-- 美化的批量操作悬浮工具栏 -->
|
| 89 |
-
<div
|
| 90 |
-
x-cloak
|
| 91 |
-
x-show="selectedKeys.length > 0"
|
| 92 |
-
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg shadow-xl px-4 py-3 flex items-center space-x-4 z-30 bulk-toolbar"
|
| 93 |
-
style="backdrop-filter: blur(8px); border: 1px solid rgba(99, 102, 241, 0.2);"
|
| 94 |
-
x-transition:enter="transition ease-out duration-300"
|
| 95 |
-
x-transition:enter-start="opacity-0 transform translate-y-4"
|
| 96 |
-
x-transition:enter-end="opacity-100 transform translate-y-0"
|
| 97 |
-
x-transition:leave="transition ease-in duration-200"
|
| 98 |
-
x-transition:leave-start="opacity-100 transform translate-y-0"
|
| 99 |
-
x-transition:leave-end="opacity-0 transform translate-y-4"
|
| 100 |
-
>
|
| 101 |
-
<!-- 精美的选中数量显示 -->
|
| 102 |
-
<div class="flex items-center space-x-3">
|
| 103 |
-
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-gradient-to-r from-blue-400 to-indigo-500 text-white font-semibold text-sm shadow-sm">
|
| 104 |
-
<span x-text="selectedKeys.length"></span>
|
| 105 |
-
</div>
|
| 106 |
-
|
| 107 |
-
<div class="flex flex-col">
|
| 108 |
-
<span class="text-xs text-gray-500 uppercase tracking-wide">已选项目</span>
|
| 109 |
-
<!-- 全选/取消全选 -->
|
| 110 |
-
<button
|
| 111 |
-
@click="toggleSelectAll"
|
| 112 |
-
class="text-xs text-indigo-600 hover:text-indigo-800 transition-colors font-medium"
|
| 113 |
-
x-text="isAllSelected ? '取消全选' : '全选'"
|
| 114 |
-
></button>
|
| 115 |
-
</div>
|
| 116 |
-
</div>
|
| 117 |
-
|
| 118 |
-
<!-- 精美分隔线 -->
|
| 119 |
-
<div class="h-10 w-px bg-gradient-to-b from-transparent via-indigo-200 to-transparent"></div>
|
| 120 |
-
|
| 121 |
-
<!-- 批量操作按钮 -->
|
| 122 |
-
<div class="flex space-x-2 justify-end">
|
| 123 |
-
<!-- 美化的批量复制按钮 -->
|
| 124 |
-
<button
|
| 125 |
-
@click="bulkCopyApiKeys()"
|
| 126 |
-
class="group relative inline-flex items-center px-4 py-2 overflow-hidden border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 focus:outline-none transition-all duration-300 transform hover:scale-105"
|
| 127 |
-
>
|
| 128 |
-
<!-- 背景动画效果 -->
|
| 129 |
-
<span class="absolute inset-0 w-full h-full bg-gradient-to-r from-indigo-600 to-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
| 130 |
-
|
| 131 |
-
<svg xmlns="http://www.w3.org/2000/svg" class="relative z-10 h-4 w-4 mr-2 group-hover:animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 132 |
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
| 133 |
-
</svg>
|
| 134 |
-
<span class="relative z-10">批量复制</span>
|
| 135 |
-
</button>
|
| 136 |
-
|
| 137 |
-
<!-- 美化的批量删除按钮 -->
|
| 138 |
-
<button
|
| 139 |
-
@click="bulkDeleteApiKeys()"
|
| 140 |
-
class="group relative inline-flex items-center px-4 py-2 overflow-hidden border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 focus:outline-none transition-all duration-300 transform hover:scale-105"
|
| 141 |
-
>
|
| 142 |
-
<!-- 背景动画效果 -->
|
| 143 |
-
<span class="absolute inset-0 w-full h-full bg-gradient-to-r from-pink-600 to-red-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
| 144 |
-
|
| 145 |
-
<svg xmlns="http://www.w3.org/2000/svg" class="relative z-10 h-4 w-4 mr-2 group-hover:animate-pulse" viewBox="0 0 20 20" fill="currentColor">
|
| 146 |
-
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
| 147 |
-
</svg>
|
| 148 |
-
<span class="relative z-10">批量删除</span>
|
| 149 |
-
</button>
|
| 150 |
-
</div>
|
| 151 |
-
</div>
|
| 152 |
|
| 153 |
<!-- 平台内容 - 可折叠 -->
|
| 154 |
<div
|
|
|
|
| 1 |
+
<!-- 美化的批量操作悬浮工具栏 - 独立于平台循环外部 -->
|
| 2 |
+
<div
|
| 3 |
+
x-cloak
|
| 4 |
+
x-show="selectedKeys.length > 0"
|
| 5 |
+
class="fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-gradient-to-r from-blue-50 to-purple-50 rounded-lg shadow-xl px-4 py-3 flex items-center space-x-4 z-30 bulk-toolbar"
|
| 6 |
+
style="backdrop-filter: blur(8px); border: 1px solid rgba(99, 102, 241, 0.2);"
|
| 7 |
+
x-transition:enter="transition ease-out duration-300"
|
| 8 |
+
x-transition:enter-start="opacity-0 transform translate-y-4"
|
| 9 |
+
x-transition:enter-end="opacity-100 transform translate-y-0"
|
| 10 |
+
x-transition:leave="transition ease-in duration-200"
|
| 11 |
+
x-transition:leave-start="opacity-100 transform translate-y-0"
|
| 12 |
+
x-transition:leave-end="opacity-0 transform translate-y-4"
|
| 13 |
+
>
|
| 14 |
+
<!-- 精美的选中数量显示 -->
|
| 15 |
+
<div class="flex items-center space-x-3">
|
| 16 |
+
<div class="flex items-center justify-center h-8 w-8 rounded-full bg-gradient-to-r from-blue-400 to-indigo-500 text-white font-semibold text-sm shadow-sm">
|
| 17 |
+
<span x-text="selectedKeys.length"></span>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<div class="flex flex-col">
|
| 21 |
+
<span class="text-xs text-gray-500 uppercase tracking-wide">已选项目</span>
|
| 22 |
+
<!-- 全选/取消全选 -->
|
| 23 |
+
<button
|
| 24 |
+
@click="toggleSelectAll"
|
| 25 |
+
class="text-xs text-indigo-600 hover:text-indigo-800 transition-colors font-medium"
|
| 26 |
+
x-text="isAllSelected ? '取消全选' : '全选'"
|
| 27 |
+
></button>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<!-- 精美分隔线 -->
|
| 32 |
+
<div class="h-10 w-px bg-gradient-to-b from-transparent via-indigo-200 to-transparent"></div>
|
| 33 |
+
|
| 34 |
+
<!-- 批量操作按钮 -->
|
| 35 |
+
<div class="flex space-x-2 justify-end">
|
| 36 |
+
<!-- 美化的批量复制按钮 -->
|
| 37 |
+
<button
|
| 38 |
+
@click="bulkCopyApiKeys()"
|
| 39 |
+
class="group relative inline-flex items-center px-4 py-2 overflow-hidden border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 focus:outline-none transition-all duration-300 transform hover:scale-105"
|
| 40 |
+
>
|
| 41 |
+
<!-- 背景动画效果 -->
|
| 42 |
+
<span class="absolute inset-0 w-full h-full bg-gradient-to-r from-indigo-600 to-blue-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
| 43 |
+
|
| 44 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="relative z-10 h-4 w-4 mr-2 group-hover:animate-pulse" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 45 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
| 46 |
+
</svg>
|
| 47 |
+
<span class="relative z-10">批量复制</span>
|
| 48 |
+
</button>
|
| 49 |
+
|
| 50 |
+
<!-- 美化的批量删除按钮 -->
|
| 51 |
+
<button
|
| 52 |
+
@click="bulkDeleteApiKeys()"
|
| 53 |
+
class="group relative inline-flex items-center px-4 py-2 overflow-hidden border border-transparent rounded-lg shadow-md text-sm font-medium text-white bg-gradient-to-r from-red-500 to-pink-500 hover:from-red-600 hover:to-pink-600 focus:outline-none transition-all duration-300 transform hover:scale-105"
|
| 54 |
+
>
|
| 55 |
+
<!-- 背景动画效果 -->
|
| 56 |
+
<span class="absolute inset-0 w-full h-full bg-gradient-to-r from-pink-600 to-red-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
| 57 |
+
|
| 58 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="relative z-10 h-4 w-4 mr-2 group-hover:animate-pulse" viewBox="0 0 20 20" fill="currentColor">
|
| 59 |
+
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
| 60 |
+
</svg>
|
| 61 |
+
<span class="relative z-10">批量删除</span>
|
| 62 |
+
</button>
|
| 63 |
+
</div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
<!-- 分组的API密钥列表 -->
|
| 67 |
<div class="space-y-6">
|
| 68 |
<!-- 遍历每个平台 -->
|
|
|
|
| 82 |
@click="togglePlatform('{{ platform.id }}')"
|
| 83 |
class="px-6 py-4 cursor-pointer flex justify-between items-center border-b border-gray-200 transition-all duration-300"
|
| 84 |
:class="{
|
| 85 |
+
'bg-gray-50 border-gray-200': !platformIds.includes('{{ platform.id }}')
|
| 86 |
}"
|
| 87 |
:style="{
|
| 88 |
backgroundColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['background-color'] : 'rgba(243, 244, 246, 0.5)',
|
|
|
|
| 149 |
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
| 150 |
</svg>
|
| 151 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
<!-- 平台内容 - 可折叠 -->
|
| 154 |
<div
|
templates/login.html
CHANGED
|
@@ -42,7 +42,7 @@
|
|
| 42 |
},
|
| 43 |
}
|
| 44 |
</script>
|
| 45 |
-
<script defer src="https://unpkg.com/alpinejs@3.
|
| 46 |
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
| 47 |
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 48 |
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
|
|
|
| 42 |
},
|
| 43 |
}
|
| 44 |
</script>
|
| 45 |
+
<script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
| 46 |
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
| 47 |
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 48 |
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
utils/__pycache__/auth.cpython-313.pyc
CHANGED
|
Binary files a/utils/__pycache__/auth.cpython-313.pyc and b/utils/__pycache__/auth.cpython-313.pyc differ
|
|
|