yangtb24 commited on
Commit
ef8784d
·
verified ·
1 Parent(s): 67d755a

Upload 41 files

Browse files
app.py CHANGED
@@ -1,89 +1,101 @@
1
- """
2
- API密钥管理系统 - 主应用文件
3
- 提供API密钥的添加、编辑、删除和管理功能
4
- """
5
- import os
6
- import time
7
- import datetime
8
- import pytz
9
- from flask import Flask, redirect, url_for, request, jsonify
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
- # 应用ProxyFix中间件,使应用能够获取用户真实IP
37
- app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
38
- app.secret_key = SECRET_KEY
39
-
40
- # 设置静态文件缓存控制
41
- @app.after_request
42
- def add_cache_headers(response):
43
- """为静态资源添加缓存控制头"""
44
- if request.path.startswith('/static/'):
45
- # 设置缓存时间 - CSS、JS和图片缓存1年
46
- max_age = 31536000 # 1年的秒数
47
-
48
- # 根据文件类型设置不同的缓存策略
49
- if request.path.endswith(('.css', '.js')):
50
- # CSS和JS文件缓存1年
51
- response.headers['Cache-Control'] = f'public, max-age={max_age}'
52
- elif request.path.endswith(('.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg')):
53
- # 图片文件缓存1年
54
- response.headers['Cache-Control'] = f'public, max-age={max_age}'
55
- else:
56
- # 其他静态文件缓存1周
57
- response.headers['Cache-Control'] = 'public, max-age=604800'
58
-
59
- # 添加其他有用的缓存头
60
- response.headers['Vary'] = 'Accept-Encoding'
61
-
62
- return response
63
-
64
- # 认证中间件 - 验证所有请求
65
- @app.before_request
66
- def authenticate():
67
- """请求拦截器 - 验证所有需要认证的请求"""
68
- # 登录和静态资源路径不需要验证
69
- if request.path == '/login' or request.path.startswith('/static/'):
70
- return
71
-
72
- # 从Cookie中获取令牌
73
- token = request.cookies.get('auth_token')
74
-
75
- # 验证令牌
76
- if not AuthManager.verify_token(token):
77
- # 如果是AJAX请求,返回401状态码
78
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.path.startswith('/api/'):
79
- return jsonify({"success": False, "error": "未授权访问"}), 401
80
- # 否则重定向到登录页面
81
- return redirect(url_for('web.login'))
82
-
83
- # 注册蓝图
84
- app.register_blueprint(web_bp)
85
- app.register_blueprint(api_bp)
86
-
87
- # 入口点
88
- if __name__ == '__main__':
89
- app.run(debug=True, host='0.0.0.0', port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
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 and "'" in 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 and "'" in 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 => !allIds.includes(id));
88
  } else {
89
- // 否则选中所有可见密钥
90
  // 先合并当前已选中的ID
91
  const newSelection = [...this.selectedKeys];
92
  // 添加所有未选中的可见ID
93
- allIds.forEach(id => {
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
- * 包含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
- }
56
 
57
- // 添加API密钥
58
- async function addApiKey() {
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
- let title = `已添加 ${addedCount} 个API密钥`;
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
- // 删除API密钥
221
- function deleteApiKey(id, name) {
222
- this.deleteKeyId = id;
223
- this.deleteKeyName = name;
224
- this.showDeleteConfirm = true;
225
- }
226
 
227
- // 确认删除(单个或批量)
228
- async function confirmDelete() {
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
- Toast.fire({
272
- icon: 'success',
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
- return allVisibleKeyIds.length > 0 &&
83
- allVisibleKeyIds.every(id => this.selectedKeys.includes(id));
 
 
 
 
 
 
 
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.x.x/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 -->
 
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': !['anthropic', 'openai', 'google'].includes('{{ platform.id }}')
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.x.x/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') }}">
 
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