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