yangtb24 commited on
Commit
bbb6398
·
verified ·
1 Parent(s): 0e12e4e

Upload 37 files

Browse files
app.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API密钥管理系统 - 主应用文件
3
+ 提供API密钥的添加、编辑、删除和管理功能
4
+ """
5
+ from flask import Flask, redirect, url_for, request, jsonify
6
+
7
+ # 导入配置
8
+ from config import SECRET_KEY
9
+
10
+ # 导入路由蓝图
11
+ from routes.web import web_bp
12
+ from routes.api import api_bp
13
+
14
+ # 导入认证模块
15
+ from utils.auth import AuthManager
16
+
17
+ # 创建Flask应用
18
+ app = Flask(__name__)
19
+ app.secret_key = SECRET_KEY
20
+
21
+ # 认证中间件 - 验证所有请求
22
+ @app.before_request
23
+ def authenticate():
24
+ """请求拦截器 - 验证所有需要认证的请求"""
25
+ # 登录和静态资源路径不需要验证
26
+ if request.path == '/login' or request.path.startswith('/static/'):
27
+ return
28
+
29
+ # 从Cookie中获取令牌
30
+ token = request.cookies.get('auth_token')
31
+
32
+ # 验证令牌
33
+ if not AuthManager.verify_token(token):
34
+ # 如果是AJAX请求,返回401状态码
35
+ if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.path.startswith('/api/'):
36
+ return jsonify({"success": False, "error": "未授权访问"}), 401
37
+ # 否则重定向到登录页面
38
+ return redirect(url_for('web.login'))
39
+
40
+ # 注册蓝图
41
+ app.register_blueprint(web_bp)
42
+ app.register_blueprint(api_bp)
43
+
44
+ # 入口点
45
+ if __name__ == '__main__':
46
+ app.run(debug=True, host='0.0.0.0', port=7860)
config.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 配置文件 - 存储应用的所有配置参数
3
+ """
4
+ import os
5
+ import secrets
6
+
7
+ # 基本目录配置
8
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
9
+ DATA_DIR = os.path.join(BASE_DIR, 'data')
10
+ os.makedirs(DATA_DIR, exist_ok=True)
11
+
12
+ # 数据文件路径
13
+ API_KEYS_FILE = os.path.join(DATA_DIR, 'api_keys.json')
14
+ AUTH_FILE = os.path.join(DATA_DIR, 'auth_tokens.json')
15
+
16
+ # 应用密钥配置
17
+ SECRET_KEY = os.environ.get('SECRET_KEY', secrets.token_hex(16))
18
+ ADMIN_PASSWORD = os.environ.get('PASSWORD', '123456')
19
+
20
+ # 认证配置
21
+ TOKEN_EXPIRY_DAYS = 30
22
+
23
+ # 支持的平台
24
+ PLATFORMS = [
25
+ {"id": "anthropic", "name": "Anthropic"},
26
+ {"id": "openai", "name": "OpenAI"},
27
+ {"id": "google", "name": "Google"}
28
+ ]
29
+
30
+ # 平台标签样式
31
+ PLATFORM_STYLES = {
32
+ "anthropic": {
33
+ "background-color": "rgba(236, 72, 153, 0.1)",
34
+ "border-color": "rgba(236, 72, 153, 0.3)",
35
+ "color": "#be185d"
36
+ },
37
+ "openai": {
38
+ "background-color": "rgba(16, 185, 129, 0.1)",
39
+ "border-color": "rgba(16, 185, 129, 0.3)",
40
+ "color": "#047857"
41
+ },
42
+ "google": {
43
+ "background-color": "rgba(59, 130, 246, 0.1)",
44
+ "border-color": "rgba(59, 130, 246, 0.3)",
45
+ "color": "#1d4ed8"
46
+ }
47
+ }
models/__pycache__/api_key.cpython-313.pyc ADDED
Binary file (4.92 kB). View file
 
models/api_key.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API密钥模型 - 处理API密钥的CRUD操作
3
+ """
4
+ import json
5
+ import uuid
6
+ from datetime import datetime
7
+ import os
8
+ from config import API_KEYS_FILE
9
+
10
+ class ApiKeyManager:
11
+ """管理API密钥的类"""
12
+
13
+ @staticmethod
14
+ def load_keys():
15
+ """加载所有API密钥"""
16
+ if not os.path.exists(API_KEYS_FILE):
17
+ with open(API_KEYS_FILE, 'w', encoding='utf-8') as f:
18
+ json.dump({"api_keys": []}, f, ensure_ascii=False, indent=2)
19
+ return {"api_keys": []}
20
+
21
+ try:
22
+ with open(API_KEYS_FILE, 'r', encoding='utf-8') as f:
23
+ return json.load(f)
24
+ except json.JSONDecodeError:
25
+ return {"api_keys": []}
26
+
27
+ @staticmethod
28
+ def save_keys(data):
29
+ """保存API密钥数据"""
30
+ with open(API_KEYS_FILE, 'w', encoding='utf-8') as f:
31
+ json.dump(data, f, ensure_ascii=False, indent=2)
32
+
33
+ @staticmethod
34
+ def get_all_keys():
35
+ """获取所有密钥"""
36
+ return ApiKeyManager.load_keys()
37
+
38
+ @staticmethod
39
+ def add_key(platform, name, key):
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)
56
+ ApiKeyManager.save_keys(api_keys_data)
57
+
58
+ return new_key
59
+
60
+ @staticmethod
61
+ def delete_key(key_id):
62
+ """删除指定的API密钥"""
63
+ api_keys_data = ApiKeyManager.load_keys()
64
+
65
+ original_count = len(api_keys_data["api_keys"])
66
+ api_keys_data["api_keys"] = [k for k in api_keys_data["api_keys"] if k.get("id") != key_id]
67
+
68
+ if len(api_keys_data["api_keys"]) < original_count:
69
+ ApiKeyManager.save_keys(api_keys_data)
70
+ return True
71
+ return False
72
+
73
+ @staticmethod
74
+ def bulk_delete(key_ids):
75
+ """批量删除多个API密钥"""
76
+ if not key_ids:
77
+ return 0
78
+
79
+ api_keys_data = ApiKeyManager.load_keys()
80
+
81
+ original_count = len(api_keys_data["api_keys"])
82
+ api_keys_data["api_keys"] = [k for k in api_keys_data["api_keys"] if k.get("id") not in key_ids]
83
+
84
+ deleted_count = original_count - len(api_keys_data["api_keys"])
85
+ if deleted_count > 0:
86
+ ApiKeyManager.save_keys(api_keys_data)
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"]:
101
+ if k.get("id") == key_id:
102
+ k["name"] = name
103
+ k["key"] = key
104
+ updated_key = k
105
+ break
106
+
107
+ if updated_key:
108
+ ApiKeyManager.save_keys(api_keys_data)
109
+
110
+ return updated_key
routes/__pycache__/api.cpython-313.pyc ADDED
Binary file (3.04 kB). View file
 
routes/__pycache__/web.cpython-313.pyc ADDED
Binary file (3.47 kB). View file
 
routes/api.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API路由模块 - 处理所有API端点请求
3
+ """
4
+ from flask import Blueprint, request, jsonify
5
+ from models.api_key import ApiKeyManager
6
+
7
+ # 创建API蓝图
8
+ api_bp = Blueprint('api', __name__, url_prefix='/api')
9
+
10
+ @api_bp.route('/keys', methods=['GET'])
11
+ def get_api_keys():
12
+ """获取所有API密钥"""
13
+ return jsonify(ApiKeyManager.get_all_keys())
14
+
15
+ @api_bp.route('/keys', methods=['POST'])
16
+ def add_api_key():
17
+ """添加新的API密钥"""
18
+ data = request.json
19
+
20
+ new_key = ApiKeyManager.add_key(
21
+ platform=data.get("platform"),
22
+ name=data.get("name"),
23
+ key=data.get("key")
24
+ )
25
+
26
+ return jsonify({"success": True, "key": new_key})
27
+
28
+ @api_bp.route('/keys/<key_id>', methods=['DELETE'])
29
+ def delete_api_key(key_id):
30
+ """删除指定的API密钥"""
31
+ success = ApiKeyManager.delete_key(key_id)
32
+
33
+ return jsonify({"success": success})
34
+
35
+ @api_bp.route('/keys/bulk-delete', methods=['POST'])
36
+ def bulk_delete_api_keys():
37
+ """批量删除多个API密钥"""
38
+ data = request.json
39
+ key_ids = data.get("ids", [])
40
+
41
+ if not key_ids:
42
+ return jsonify({"success": False, "error": "没有提供要删除的密钥ID"}), 400
43
+
44
+ deleted_count = ApiKeyManager.bulk_delete(key_ids)
45
+
46
+ return jsonify({
47
+ "success": True,
48
+ "deleted_count": deleted_count,
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密钥信息"""
55
+ data = request.json
56
+
57
+ updated_key = ApiKeyManager.update_key(
58
+ key_id=key_id,
59
+ name=data.get("name"),
60
+ key=data.get("key")
61
+ )
62
+
63
+ if not updated_key:
64
+ return jsonify({"success": False, "error": "未找到指定的密钥"}), 404
65
+
66
+ return jsonify({"success": True, "key": updated_key})
routes/web.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Web路由模块 - 处理所有页面请求和身份验证
3
+ """
4
+ from flask import Blueprint, render_template, request, redirect, url_for, make_response, session
5
+ from datetime import datetime
6
+ from config import ADMIN_PASSWORD, PLATFORMS, PLATFORM_STYLES, TOKEN_EXPIRY_DAYS
7
+ from utils.auth import AuthManager
8
+ from models.api_key import ApiKeyManager
9
+
10
+ # 创建Web蓝图
11
+ web_bp = Blueprint('web', __name__)
12
+
13
+ @web_bp.route('/')
14
+ def index():
15
+ """首页 - 显示API密钥管理界面"""
16
+ api_keys_data = ApiKeyManager.get_all_keys()
17
+
18
+ # 按平台分组密钥
19
+ grouped_keys = {}
20
+ for platform in PLATFORMS:
21
+ grouped_keys[platform["id"]] = {
22
+ "name": platform["name"],
23
+ "keys": []
24
+ }
25
+
26
+ # 将密钥加入对应的平台组
27
+ for key in api_keys_data.get("api_keys", []):
28
+ platform_id = key.get("platform", "other").lower()
29
+ if platform_id not in grouped_keys:
30
+ platform_id = "other"
31
+ grouped_keys[platform_id]["keys"].append(key)
32
+
33
+ is_ajax = request.args.get('ajax', '0') == '1'
34
+ return render_template('index.html', platforms=PLATFORMS, grouped_keys=grouped_keys, platform_styles=PLATFORM_STYLES)
35
+
36
+ @web_bp.route('/login', methods=['GET', 'POST'])
37
+ def login():
38
+ """用户登录"""
39
+ error = None
40
+ if request.method == 'POST':
41
+ password = request.form.get('password')
42
+ if password == ADMIN_PASSWORD:
43
+ # 密码正确,生成令牌
44
+ token = AuthManager.generate_token()
45
+ AuthManager.store_token(token)
46
+
47
+ # 创建响应并设置Cookie
48
+ response = make_response(redirect(url_for('web.index')))
49
+ response.set_cookie(
50
+ 'auth_token',
51
+ token,
52
+ max_age=TOKEN_EXPIRY_DAYS * 24 * 60 * 60,
53
+ httponly=True,
54
+ secure=request.is_secure
55
+ )
56
+ return response
57
+ else:
58
+ error = "密码错误,请重试"
59
+
60
+ # 传递当前年份以供页脚使用
61
+ current_year = datetime.now().year
62
+ return render_template('login.html', error=error, now={'year': current_year})
63
+
64
+ @web_bp.route('/logout')
65
+ def logout():
66
+ """用户登出"""
67
+ # 从Cookie中获取令牌
68
+ token = request.cookies.get('auth_token')
69
+
70
+ # 删除令牌
71
+ if token:
72
+ AuthManager.remove_token(token)
73
+
74
+ # 返回响应并清除Cookie
75
+ response = make_response(redirect(url_for('web.login')))
76
+ response.delete_cookie('auth_token')
77
+ return response
static/css/animations.css ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 动画与过渡效果
2
+ * 包含网站中所有动画相关样式,如展开/收起、淡入等效果
3
+ */
4
+
5
+ /* 折叠面板动画 - 使用will-change提高性能 */
6
+ .accordion-content {
7
+ will-change: transform, opacity, max-height;
8
+ transform-origin: top;
9
+ backface-visibility: hidden;
10
+ }
11
+
12
+ .accordion-icon {
13
+ will-change: transform;
14
+ transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); /* 弹性过渡 */
15
+ }
16
+
17
+ /* 平滑展开/收起的时间函数 */
18
+ .accordion-expand {
19
+ animation-timing-function: cubic-bezier(0.17, 0.84, 0.44, 1);
20
+ }
21
+
22
+ .accordion-collapse {
23
+ animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
24
+ }
25
+
26
+ /* 展开动画:从0高度逐渐展开到全高 */
27
+ @keyframes smoothExpand {
28
+ 0% {
29
+ max-height: 0;
30
+ opacity: 0;
31
+ transform: scaleY(0.95);
32
+ }
33
+ 50% {
34
+ opacity: 0.5;
35
+ }
36
+ 100% {
37
+ max-height: 2000px;
38
+ opacity: 1;
39
+ transform: scaleY(1);
40
+ }
41
+ }
42
+
43
+ /* 收起动画:从全高逐渐收起到0高度 */
44
+ @keyframes smoothCollapse {
45
+ 0% {
46
+ max-height: 2000px;
47
+ opacity: 1;
48
+ transform: scaleY(1);
49
+ }
50
+ 30% {
51
+ opacity: 0.7;
52
+ }
53
+ 100% {
54
+ max-height: 0;
55
+ opacity: 0;
56
+ transform: scaleY(0.95);
57
+ }
58
+ }
59
+
60
+ /* 淡入动画 - 用于元素出现时的平滑过渡 */
61
+ @keyframes fadeIn {
62
+ from { opacity: 0; }
63
+ to { opacity: 1; }
64
+ }
65
+
66
+ .fade-in {
67
+ animation: fadeIn 0.15s ease-in-out;
68
+ }
69
+
70
+ /* 单次弹跳动画 - 用于吸引注意力 */
71
+ @keyframes ping-once {
72
+ 0% {
73
+ transform: scale(0.95);
74
+ opacity: 0.8;
75
+ }
76
+ 30% {
77
+ transform: scale(1.2);
78
+ opacity: 0.5;
79
+ }
80
+ 70% {
81
+ transform: scale(0.95);
82
+ opacity: 0.3;
83
+ }
84
+ 100% {
85
+ transform: scale(1);
86
+ opacity: 0;
87
+ }
88
+ }
89
+
90
+ .animate-ping-once {
91
+ animation: ping-once 0.8s cubic-bezier(0, 0, 0.2, 1) forwards;
92
+ }
static/css/base.css ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 基础样式 */
2
+ html {
3
+ overflow-y: scroll;
4
+ }
5
+
6
+ /* 防止Alpine.js组件在初始化前闪现 */
7
+ [x-cloak] {
8
+ display: none !important;
9
+ }
10
+
11
+ /* 移除所有元素聚焦时的边框 */
12
+ button:focus,
13
+ input:focus,
14
+ select:focus,
15
+ textarea:focus {
16
+ outline: none !important;
17
+ box-shadow: none !important;
18
+ border-color: inherit !important;
19
+ }
static/css/components.css ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 组件样式
2
+ * 包含各种UI组件的样式定义,如按钮、模态框、标签等
3
+ */
4
+
5
+ /* 浮动添加按钮 - 固定在右下角的主操作按钮 */
6
+ .floating-add-btn {
7
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
8
+ transition: all 0.2s ease-in-out;
9
+ }
10
+
11
+ .floating-add-btn:hover {
12
+ transform: scale(1.05);
13
+ filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.15));
14
+ }
15
+
16
+ .floating-add-btn:active {
17
+ transform: scale(0.98);
18
+ }
19
+
20
+ /* 回到顶部按钮 - 滚动时显示的返回顶部快捷按钮 */
21
+ .back-to-top {
22
+ position: fixed;
23
+ bottom: 20px;
24
+ right: 20px;
25
+ width: 48px;
26
+ height: 48px;
27
+ border-radius: 50%;
28
+ background-color: #0284c7; /* primary-600 */
29
+ color: white;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ cursor: pointer;
34
+ opacity: 0;
35
+ visibility: hidden;
36
+ transition: all 0.3s ease;
37
+ z-index: 999;
38
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
39
+ }
40
+
41
+ .back-to-top.visible {
42
+ opacity: 0.9;
43
+ visibility: visible;
44
+ }
45
+
46
+ .back-to-top:hover {
47
+ background-color: #0369a1; /* primary-700 */
48
+ transform: translateY(-3px);
49
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
50
+ opacity: 1;
51
+ }
52
+
53
+ /* 模态框样式 - 简化的弹窗设计 */
54
+ .simplified-modal {
55
+ max-width: 400px;
56
+ border-radius: 12px;
57
+ }
58
+
59
+ .modal-field-hint {
60
+ font-size: 11px;
61
+ color: #6b7280;
62
+ margin-left: 4px;
63
+ }
64
+
65
+ /* 背景模糊效果 - 增强模态框层次感 */
66
+ .backdrop-blur-md {
67
+ backdrop-filter: blur(8px);
68
+ -webkit-backdrop-filter: blur(8px);
69
+ }
70
+
71
+ /* 卡片式输入组 - 表单元素容器 */
72
+ .input-card {
73
+ transition: all 0.3s ease;
74
+ }
75
+
76
+ .input-card:hover {
77
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
78
+ transform: translateY(-1px);
79
+ }
80
+
81
+ /* 复制按钮 - 用于复制API密钥 */
82
+ .clipboard-btn {
83
+ background: transparent;
84
+ transition: all 0.2s ease;
85
+ border-radius: 50%;
86
+ padding: 4px;
87
+ }
88
+
89
+ .clipboard-btn:hover {
90
+ background: rgba(2, 132, 199, 0.1);
91
+ transform: scale(1.1);
92
+ }
93
+
94
+ /* 快捷键提示 - 显示键盘快捷键 */
95
+ .shortcut-hint {
96
+ position: absolute;
97
+ bottom: -18px;
98
+ right: 4px;
99
+ font-size: 10px;
100
+ color: rgba(255, 255, 255, 0.8);
101
+ background: rgba(0, 0, 0, 0.5);
102
+ padding: 1px 4px;
103
+ border-radius: 3px;
104
+ pointer-events: none;
105
+ }
106
+
107
+ .shortcut-hint-box {
108
+ font-size: 10px;
109
+ color: #9ca3af;
110
+ display: flex;
111
+ align-items: center;
112
+ position: absolute;
113
+ bottom: 3px;
114
+ left: 6px;
115
+ }
116
+
117
+ .shortcut-hint-box svg {
118
+ margin-right: 4px;
119
+ width: 14px;
120
+ height: 14px;
121
+ }
122
+
123
+ /* 批量操作工具栏 - 多选时出现的操作栏 */
124
+ .bulk-toolbar {
125
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(99, 102, 241, 0.1);
126
+ backdrop-filter: blur(10px);
127
+ -webkit-backdrop-filter: blur(10px);
128
+ }
129
+
130
+ /* 选中行样式增强 - 给选中的行添加渐变背景 */
131
+ .bg-blue-50 {
132
+ position: relative;
133
+ overflow: hidden;
134
+ }
135
+
136
+ .bg-blue-50::before {
137
+ content: '';
138
+ position: absolute;
139
+ inset: 0;
140
+ background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, rgba(96, 165, 250, 0.1) 50%, rgba(59, 130, 246, 0.05) 100%);
141
+ z-index: -1;
142
+ opacity: 0.7;
143
+ }
static/css/responsive.css ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 响应式布局
2
+ * 定义网站在不同屏幕尺寸下的显示规则
3
+ */
4
+
5
+ /* 移动设备布局调整 */
6
+ @media (max-width: 640px) {
7
+ /* 水平滚动容器宽度限制 */
8
+ .key-scroll-container {
9
+ max-width: calc(100vw - 3rem);
10
+ }
11
+
12
+ /* 可以在这里添加其他移动端特定的样式调整 */
13
+ }
14
+
15
+ /* 平板设备布局调整可以在此添加 */
16
+ /* @media (min-width: 641px) and (max-width: 1024px) {
17
+ } */
18
+
19
+ /* 大屏幕设备布局调整可以在此添加 */
20
+ /* @media (min-width: 1025px) {
21
+ } */
static/css/scrollbars.css ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 滚动条样式
2
+ * 定制网站所有滚动条外观,提升用户界面一致性
3
+ */
4
+
5
+ /* 主滚动条 - 适配Tailwind主题色 */
6
+ ::-webkit-scrollbar {
7
+ width: 12px;
8
+ height: 12px;
9
+ }
10
+
11
+ ::-webkit-scrollbar-track {
12
+ background-color: #f8fafc; /* gray-50 背景色 */
13
+ border-radius: 8px;
14
+ }
15
+
16
+ ::-webkit-scrollbar-thumb {
17
+ background-color: #0284c7; /* primary-600 主题色 */
18
+ border-radius: 8px;
19
+ border: 3px solid #f8fafc;
20
+ background-clip: padding-box; /* 确保边框透明 */
21
+ }
22
+
23
+ ::-webkit-scrollbar-thumb:hover {
24
+ background-color: #0369a1; /* primary-700 悬停加深 */
25
+ border: 3px solid #f8fafc;
26
+ background-clip: padding-box;
27
+ }
28
+
29
+ ::-webkit-scrollbar-corner {
30
+ background-color: transparent;
31
+ }
32
+
33
+ /* 自定义区域滚动条 - 更小更精致 */
34
+ .custom-scrollbar::-webkit-scrollbar {
35
+ width: 6px;
36
+ height: 6px;
37
+ }
38
+
39
+ .custom-scrollbar::-webkit-scrollbar-track {
40
+ background: rgba(0, 0, 0, 0.05);
41
+ border-radius: 3px;
42
+ }
43
+
44
+ .custom-scrollbar::-webkit-scrollbar-thumb {
45
+ background: rgba(0, 0, 0, 0.2);
46
+ border-radius: 3px;
47
+ }
48
+
49
+ /* 水平滚动容器 - 用于API密钥列表展示 */
50
+ .key-scroll-container {
51
+ max-width: 100%;
52
+ overflow-x: auto;
53
+ white-space: nowrap;
54
+ scrollbar-width: thin; /* Firefox支持 */
55
+ }
static/css/style.css ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* API密钥管理系统样式索引
2
+ * 这个文件作为主入口点,导入所有其他CSS模块
3
+ * 方便统一维护和管理样式
4
+ */
5
+
6
+ /* 基础样式 - 包含通用设置和重置 */
7
+ @import 'base.css';
8
+
9
+ /* 动画效果 - 包含所有动画和过渡效果 */
10
+ @import 'animations.css';
11
+
12
+ /* 滚动条样式 - 定制各种滚动条外观 */
13
+ @import 'scrollbars.css';
14
+
15
+ /* 组件样式 - 包含按钮、模态框等UI元素 */
16
+ @import 'components.css';
17
+
18
+ /* 标签样式 - 包含标签、过滤器等分类元素 */
19
+ @import 'tags.css';
20
+
21
+ /* 响应式布局 - 定义不同屏幕尺寸下的显示规则 */
22
+ @import 'responsive.css';
23
+
24
+ /* 主题样式 - 定义暗色模式等主题变化 */
25
+ @import 'themes.css';
static/css/tags.css ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 标签样式
2
+ * 定义各种标签、过滤器和分类元素的外观
3
+ */
4
+
5
+ /* 平台筛选标签 - 用于筛选不同平台的API密钥 */
6
+ .platform-filter-tag {
7
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
8
+ position: relative;
9
+ overflow: hidden;
10
+ }
11
+
12
+ /* 标签悬停效果 - 渐变背景显示 */
13
+ .platform-filter-tag::before {
14
+ content: '';
15
+ position: absolute;
16
+ top: 0;
17
+ left: 0;
18
+ right: 0;
19
+ bottom: 0;
20
+ background: linear-gradient(45deg, rgba(2, 132, 199, 0.1), rgba(6, 182, 212, 0.1));
21
+ opacity: 0;
22
+ transition: opacity 0.3s ease;
23
+ z-index: -1;
24
+ border-radius: inherit;
25
+ }
26
+
27
+ .platform-filter-tag:hover::before {
28
+ opacity: 1;
29
+ }
30
+
31
+ /* 标签按下效果 */
32
+ .platform-filter-tag:active {
33
+ transform: scale(0.97);
34
+ }
35
+
36
+ /* 标签选中状态 */
37
+ .platform-filter-tag.selected {
38
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
39
+ }
40
+
41
+ /* 标签计数器 - 显示每个平台的API密钥数量 */
42
+ .tag-counter {
43
+ font-weight: 500;
44
+ font-size: 0.75rem;
45
+ font-family: 'Arial', 'Helvetica', sans-serif;
46
+ border-radius: 50%;
47
+ width: 1.6rem;
48
+ height: 1.6rem;
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ margin-left: 0.4rem;
53
+ transition: all 0.2s ease;
54
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
55
+ letter-spacing: 0.02em;
56
+ }
57
+
58
+ /* 全部标签样式 - 显示所有API密钥的标签 */
59
+ .all-tag {
60
+ font-weight: 600;
61
+ }
62
+
63
+ .all-tag.selected {
64
+ border-color: #0284c7;
65
+ box-shadow: 0 2px 5px rgba(2, 132, 199, 0.15);
66
+ }
67
+
68
+ /* 平台样式现在由config.py中的PLATFORM_STYLES动态配置 */
69
+
70
+ /* 平台标题数字计数器 - 美化平台标题旁边的数字显示 */
71
+ .platform-title-counter {
72
+ font-family: 'Arial', 'Helvetica', sans-serif;
73
+ font-weight: 500;
74
+ font-size: 0.75rem;
75
+ letter-spacing: 0.02em;
76
+ width: 1.6rem;
77
+ height: 1.6rem;
78
+ border-radius: 50%;
79
+ display: inline-flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
83
+ transform: translateY(-1px);
84
+ }
85
+
86
+ /* API密钥显示容器样式 - 防止滚动条出现,改用省略号截断 */
87
+ .key-scroll-container {
88
+ overflow: hidden !important; /* 覆盖overflow-x-auto */
89
+ }
90
+
91
+ .key-scroll-container > div:first-of-type {
92
+ white-space: nowrap;
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ }
static/css/themes.css ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 主题模式样式
2
+ * 定义暗色模式和其他可能的主题变化
3
+ */
4
+
5
+ /* 暗色模式调整 */
6
+ @media (prefers-color-scheme: dark) {
7
+ /* 背景色调整 */
8
+ .dark\:bg-gray-800 {
9
+ background-color: #1f2937;
10
+ }
11
+
12
+ /* 文本颜色调整 */
13
+ .dark\:text-white {
14
+ color: #ffffff;
15
+ }
16
+
17
+ /* 边框颜色调整 */
18
+ .dark\:border-gray-700 {
19
+ border-color: #374151;
20
+ }
21
+
22
+ /* 半透明背景 */
23
+ .dark\:bg-gray-700\/30 {
24
+ background-color: rgba(55, 65, 81, 0.3);
25
+ }
26
+
27
+ /* 卡片悬停效果增强 */
28
+ .input-card:hover {
29
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
30
+ }
31
+
32
+ /* 可以在这里添加更多暗色模式特定样式 */
33
+ }
static/img/favicon.ico ADDED
static/js/api-key-manager/bulk-actions.js ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API密钥管理器 - 批量操作模块
3
+ * 包含批量选择、删除和复制等功能
4
+ */
5
+
6
+ // 切换单个密钥的选择状态
7
+ function toggleKeySelection(keyId) {
8
+ const index = this.selectedKeys.indexOf(keyId);
9
+ if (index === -1) {
10
+ // 添加到选中数组
11
+ this.selectedKeys.push(keyId);
12
+ } else {
13
+ // 从选中数组中移除
14
+ this.selectedKeys.splice(index, 1);
15
+ }
16
+
17
+ // 检查是否需要更新平台选择状态
18
+ this.updatePlatformSelectionStates();
19
+ }
20
+
21
+ // 切换平台的选择状态
22
+ function togglePlatformSelection(platformId) {
23
+ const index = this.selectedPlatforms.indexOf(platformId);
24
+
25
+ if (index === -1) {
26
+ // 添加到选中平台数组
27
+ this.selectedPlatforms.push(platformId);
28
+
29
+ // 选中该平台下的所有密钥
30
+ this.apiKeys.forEach(key => {
31
+ if (key.platform === platformId && !this.selectedKeys.includes(key.id)) {
32
+ this.selectedKeys.push(key.id);
33
+ }
34
+ });
35
+ } else {
36
+ // 从选中平台数组中移除
37
+ this.selectedPlatforms.splice(index, 1);
38
+
39
+ // 取消选中该平台下的所有密钥
40
+ this.selectedKeys = this.selectedKeys.filter(keyId => {
41
+ const key = this.apiKeys.find(k => k.id === keyId);
42
+ return key && key.platform !== platformId;
43
+ });
44
+ }
45
+ }
46
+
47
+ // 更新平台选择状态(基于已选择的密钥)
48
+ function updatePlatformSelectionStates() {
49
+ const platforms = this.getPlatforms();
50
+
51
+ // 清空当前选中的平台
52
+ this.selectedPlatforms = [];
53
+
54
+ // 对于每个平台,检查该平台下的所有密钥是否都被选中
55
+ platforms.forEach(platform => {
56
+ const platformKeys = this.apiKeys.filter(key => key.platform === platform.id);
57
+ const allSelected = platformKeys.length > 0 &&
58
+ platformKeys.every(key => this.selectedKeys.includes(key.id));
59
+
60
+ if (allSelected) {
61
+ this.selectedPlatforms.push(platform.id);
62
+ }
63
+ });
64
+ }
65
+
66
+ // 获取所有可见密钥ID
67
+ function getAllVisibleKeyIds() {
68
+ // 获取DOM中的所有可见密钥元素
69
+ const keyElements = document.querySelectorAll('[data-key-id]');
70
+ const keyIds = [];
71
+ keyElements.forEach(el => {
72
+ // 只收集可见元素的ID
73
+ if (window.getComputedStyle(el).display !== 'none') {
74
+ const keyId = el.getAttribute('data-key-id');
75
+ if (keyId) keyIds.push(keyId);
76
+ }
77
+ });
78
+ return keyIds;
79
+ }
80
+
81
+ // 切换全选/取消全选
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
+ }
97
+ });
98
+ this.selectedKeys = newSelection;
99
+ }
100
+
101
+ // 更新平台选择状态
102
+ this.updatePlatformSelectionStates();
103
+ }
104
+
105
+ // 批量删除API密钥
106
+ function bulkDeleteApiKeys() {
107
+ if (this.selectedKeys.length === 0) return;
108
+
109
+ // 设置批量删除标志
110
+ this.isBulkDelete = true;
111
+
112
+ // 显示确认对话框
113
+ this.showDeleteConfirm = true;
114
+ }
115
+
116
+ // 批量复制API密钥
117
+ function bulkCopyApiKeys() {
118
+ if (this.selectedKeys.length === 0) return;
119
+
120
+ // 获取所有选中密钥的值
121
+ const selectedKeyValues = [];
122
+ this.selectedKeys.forEach(keyId => {
123
+ // 查找对应的DOM元素
124
+ const keyElement = document.querySelector(`[data-key-id="${keyId}"]`);
125
+ if (keyElement) {
126
+ // 获取密钥值 - 从DOM中提取
127
+ const keyValueElement = keyElement.querySelector('.text-gray-500.font-mono');
128
+ if (keyValueElement) {
129
+ const keyValue = keyValueElement.textContent.trim();
130
+ selectedKeyValues.push(keyValue);
131
+ }
132
+ }
133
+ });
134
+
135
+ // 如果成功提取了密钥值
136
+ if (selectedKeyValues.length > 0) {
137
+ // 将密钥值格式化为一行一个
138
+ const formattedKeys = selectedKeyValues.join('\n');
139
+
140
+ // 使用临时textarea元素复制文本
141
+ const textarea = document.createElement('textarea');
142
+ textarea.value = formattedKeys;
143
+ textarea.style.position = 'fixed';
144
+ document.body.appendChild(textarea);
145
+ textarea.select();
146
+
147
+ try {
148
+ document.execCommand('copy');
149
+
150
+ // 使用SweetAlert2显示复制成功动画
151
+ const Toast = Swal.mixin({
152
+ toast: true,
153
+ position: 'top-end',
154
+ showConfirmButton: false,
155
+ timer: 1500,
156
+ timerProgressBar: true,
157
+ didOpen: (toast) => {
158
+ toast.onmouseenter = Swal.stopTimer;
159
+ toast.onmouseleave = Swal.resumeTimer;
160
+ }
161
+ });
162
+
163
+ Toast.fire({
164
+ icon: 'success',
165
+ title: `已成功复制 ${selectedKeyValues.length} 个密钥到剪贴板`,
166
+ background: '#f0fdf4',
167
+ iconColor: '#16a34a'
168
+ });
169
+ } catch (err) {
170
+ console.error('复制失败:', err);
171
+ Swal.fire({
172
+ icon: 'error',
173
+ title: '复制失败',
174
+ text: '请手动复制内容',
175
+ confirmButtonColor: '#0284c7'
176
+ });
177
+ } finally {
178
+ document.body.removeChild(textarea);
179
+ }
180
+ } else {
181
+ Swal.fire({
182
+ icon: 'error',
183
+ title: '复制失败',
184
+ text: '无法获取所选密钥的值',
185
+ confirmButtonColor: '#0284c7'
186
+ });
187
+ }
188
+ }
static/js/api-key-manager/core.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API密钥管理器 - 核心模块
3
+ * 包含数据状态定义和初始化功能
4
+ */
5
+
6
+ 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) : {};
13
+
14
+ // 从localStorage读取平台筛选状态,如果有的话
15
+ const savedPlatformFilters = localStorage.getItem('platformFilters');
16
+ const parsedPlatformFilters = savedPlatformFilters ? JSON.parse(savedPlatformFilters) : {};
17
+
18
+ // 初始化平台状态,优先使用保存的状态
19
+ platforms.forEach(platform => {
20
+ this.platformStates[platform.id] = parsedPlatformStates[platform.id] !== undefined
21
+ ? parsedPlatformStates[platform.id]
22
+ : true;
23
+
24
+ this.platformFilters[platform.id] = parsedPlatformFilters[platform.id] !== undefined
25
+ ? parsedPlatformFilters[platform.id]
26
+ : true;
27
+ });
28
+
29
+ // 检查是否所有平台都被选中
30
+ this.allPlatformsSelected = platforms.every(platform =>
31
+ this.platformFilters[platform.id] === true
32
+ );
33
+
34
+ // 加载API密钥
35
+ this.loadApiKeys();
36
+
37
+ // 初始化剪贴板JS
38
+ new ClipboardJS('[data-clipboard-text]');
39
+
40
+ // 设置平台类型为上次选择的值(如果有)
41
+ const lastPlatform = localStorage.getItem('lastSelectedPlatform');
42
+ if (lastPlatform) {
43
+ this.newKey.platform = lastPlatform;
44
+ }
45
+
46
+ // 这里不再恢复搜索词状态,只保留平台状态和页面位置
47
+
48
+ // 监听来自快捷键的打开模态框事件
49
+ window.addEventListener('open-add-modal', () => {
50
+ this.showAddModal = true;
51
+ // 聚焦到平台选择框,方便用户立即操作
52
+ setTimeout(() => {
53
+ document.getElementById('platform').focus();
54
+ }, 100);
55
+ });
56
+
57
+ // 监听模态框内的键盘事件
58
+ document.addEventListener('keydown', (e) => {
59
+ // Alt+Enter 提交表单
60
+ if (e.altKey && e.key === 'Enter') {
61
+ if (this.showAddModal) {
62
+ e.preventDefault();
63
+ this.addApiKey();
64
+ } else if (this.showEditModal) {
65
+ e.preventDefault();
66
+ this.updateApiKey();
67
+ }
68
+ }
69
+
70
+ // Esc 关闭模态框
71
+ if (e.key === 'Escape') {
72
+ if (this.showAddModal) {
73
+ this.showAddModal = false;
74
+ } else if (this.showEditModal) {
75
+ this.showEditModal = false;
76
+ }
77
+ }
78
+ });
79
+ }
static/js/api-key-manager/key-operations.js ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
static/js/api-key-manager/main-manager.js ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API密钥管理器主模块
2
+ // 定义API密钥管理器的主函数
3
+ function apiKeyManager() {
4
+ return {
5
+ // 数据状态
6
+ apiKeys: [],
7
+ platformStates: {},
8
+ platformFilters: {}, // 平台筛选状态
9
+ allPlatformsSelected: true, // 是否选择所有平台
10
+ searchTerm: '',
11
+ showAddModal: false,
12
+ showDeleteConfirm: false,
13
+ isSubmitting: false,
14
+ isDeleting: false,
15
+ isLoading: true, // 添加加载状态
16
+ errorMessage: '',
17
+ copiedId: null,
18
+ deleteKeyId: null,
19
+ deleteKeyName: '',
20
+ selectedKeys: [], // 批量选择的密钥ID数组
21
+ selectedPlatforms: [], // 批量选择的平台ID数组
22
+ isBulkDelete: false, // 是否为批量删除操作
23
+ newKey: {
24
+ platform: '',
25
+ name: '',
26
+ key: ''
27
+ },
28
+ showEditModal: false,
29
+ editKey: {
30
+ id: '',
31
+ name: '',
32
+ key: '',
33
+ platform: ''
34
+ },
35
+
36
+ // 生命周期钩子
37
+ init() {
38
+ // 调用核心模块的初始化函数
39
+ initApiKeyManager.call(this);
40
+ },
41
+
42
+ // 加载API密钥
43
+ loadApiKeys() {
44
+ return loadApiKeys.apply(this, arguments);
45
+ },
46
+
47
+ // 添加API密钥
48
+ addApiKey() {
49
+ return addApiKey.apply(this, arguments);
50
+ },
51
+
52
+ // 删除API密钥
53
+ deleteApiKey(id, name) {
54
+ return deleteApiKey.apply(this, arguments);
55
+ },
56
+
57
+ // 切换单个密钥的选择状态
58
+ toggleKeySelection(keyId) {
59
+ return toggleKeySelection.apply(this, arguments);
60
+ },
61
+
62
+ // 切换平台的选择状态
63
+ togglePlatformSelection(platformId) {
64
+ return togglePlatformSelection.apply(this, arguments);
65
+ },
66
+
67
+ // 更新平台选择状态(基于已选择的密钥)
68
+ updatePlatformSelectionStates() {
69
+ return updatePlatformSelectionStates.apply(this, arguments);
70
+ },
71
+
72
+ // 获取所有可见密钥ID
73
+ getAllVisibleKeyIds() {
74
+ return getAllVisibleKeyIds.apply(this, arguments);
75
+ },
76
+
77
+ // 判断是否全部选中的计算属性
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
+ // 切换全选/取消全选
87
+ toggleSelectAll() {
88
+ return toggleSelectAll.apply(this, arguments);
89
+ },
90
+
91
+ // 批量删除API密钥
92
+ bulkDeleteApiKeys() {
93
+ return bulkDeleteApiKeys.apply(this, arguments);
94
+ },
95
+
96
+ // 批量复制API密钥
97
+ bulkCopyApiKeys() {
98
+ return bulkCopyApiKeys.apply(this, arguments);
99
+ },
100
+
101
+ // 确认删除(单个或批量)
102
+ confirmDelete() {
103
+ return confirmDelete.apply(this, arguments);
104
+ },
105
+
106
+ // 复制到剪贴板
107
+ copyToClipboard(text, id) {
108
+ return copyToClipboard.apply(this, arguments);
109
+ },
110
+
111
+ // 切换平台折叠状态
112
+ togglePlatform(platformId) {
113
+ return togglePlatform.apply(this, arguments);
114
+ },
115
+
116
+ // 获取平台API密钥数量
117
+ getPlatformKeyCount(platformId, filtered = false) {
118
+ return getPlatformKeyCount.apply(this, arguments);
119
+ },
120
+
121
+ // 平台是否有API密钥
122
+ hasPlatformKeys(platformId) {
123
+ return hasPlatformKeys.apply(this, arguments);
124
+ },
125
+
126
+ // 平台是否应该显示(基于筛选条件)
127
+ isPlatformVisible(platformId) {
128
+ return isPlatformVisible.apply(this, arguments);
129
+ },
130
+
131
+ // 获取所有平台
132
+ getPlatforms() {
133
+ return getPlatforms.apply(this, arguments);
134
+ },
135
+
136
+ // 切换平台筛选状态
137
+ togglePlatformFilter(platformId) {
138
+ return togglePlatformFilter.apply(this, arguments);
139
+ },
140
+
141
+ // 切换所有平台筛选状态
142
+ toggleAllPlatformFilters() {
143
+ return toggleAllPlatformFilters.apply(this, arguments);
144
+ },
145
+
146
+ // 搜索匹配
147
+ matchesSearch(name, key, notes) {
148
+ return matchesSearch.apply(this, arguments);
149
+ },
150
+
151
+ // 获取总API密钥数量
152
+ totalKeyCount() {
153
+ return totalKeyCount.apply(this, arguments);
154
+ },
155
+
156
+ // 打开编辑API密钥模态框
157
+ editApiKey(id, name, key) {
158
+ return editApiKey.apply(this, arguments);
159
+ },
160
+
161
+ // 更新API密钥
162
+ updateApiKey() {
163
+ return updateApiKey.apply(this, arguments);
164
+ },
165
+
166
+ // 显示通知
167
+ showNotification(message, type = 'info') {
168
+ return showNotification.apply(this, arguments);
169
+ }
170
+ };
171
+ }
static/js/api-key-manager/modals.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API密钥管理器 - 模态框控制模块
3
+ * 负责统一管理模态框的显示和交互
4
+ */
5
+
6
+ // 打开添加密钥模态框
7
+ function openAddKeyModal() {
8
+ if (!window.apiKeyManagerInstance) return;
9
+ window.apiKeyManagerInstance.showAddModal = true;
10
+
11
+ // 聚焦到平台选择下拉框
12
+ setTimeout(() => {
13
+ document.getElementById('platform')?.focus();
14
+ }, 100);
15
+ }
16
+
17
+ // 打开编辑密钥模态框
18
+ function openEditKeyModal(id, name, key, platform) {
19
+ if (!window.apiKeyManagerInstance) return;
20
+ window.apiKeyManagerInstance.editApiKey(id, name, key, platform);
21
+ }
22
+
23
+ // 打开删除确认模态框
24
+ function openDeleteConfirmModal(id, name) {
25
+ if (!window.apiKeyManagerInstance) return;
26
+ window.apiKeyManagerInstance.deleteApiKey(id, name);
27
+ }
28
+
29
+ // 关闭所有模态框
30
+ function closeAllModals() {
31
+ if (!window.apiKeyManagerInstance) return;
32
+
33
+ window.apiKeyManagerInstance.showAddModal = false;
34
+ window.apiKeyManagerInstance.showEditModal = false;
35
+ window.apiKeyManagerInstance.showDeleteConfirm = false;
36
+ }
37
+
38
+ // 处理键盘快捷键
39
+ function setupModalKeyboardShortcuts() {
40
+ document.addEventListener('keydown', (e) => {
41
+ // ESC键关闭所有模态框
42
+ if (e.key === 'Escape') {
43
+ closeAllModals();
44
+ }
45
+
46
+ // Alt+N 打开添加模态框
47
+ if (e.altKey && e.key === 'n') {
48
+ openAddKeyModal();
49
+ }
50
+ });
51
+ }
52
+
53
+ // 初始化模态框设置
54
+ function initModals() {
55
+ // 存储对API密钥管理器实例的引用,方便全局访问
56
+ window.addEventListener('alpine:initialized', () => {
57
+ const apiKeyManagerEl = document.querySelector('[x-data="apiKeyManager()"]');
58
+ if (apiKeyManagerEl) {
59
+ window.apiKeyManagerInstance = apiKeyManagerEl.__x.getUnobservedData();
60
+ }
61
+ });
62
+
63
+ // 设置键盘快捷键
64
+ setupModalKeyboardShortcuts();
65
+
66
+ // 监听打开添加模态框事件
67
+ window.addEventListener('open-add-modal', openAddKeyModal);
68
+ }
69
+
70
+ // 暴露给外部使用的方法
71
+ window.apiKeyModals = {
72
+ openAddKeyModal,
73
+ openEditKeyModal,
74
+ openDeleteConfirmModal,
75
+ closeAllModals,
76
+ initModals
77
+ };
78
+
79
+ // 注册初始化函数
80
+ document.addEventListener('DOMContentLoaded', () => {
81
+ initModals();
82
+ });
static/js/api-key-manager/platform-utils.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API密钥管理器 - 平台工具模块
3
+ * 包含与平台相关的功能函数
4
+ */
5
+
6
+ // 切换平台折叠状态
7
+ function togglePlatform(platformId) {
8
+ this.platformStates[platformId] = !this.platformStates[platformId];
9
+
10
+ // 保存展开状态到localStorage
11
+ localStorage.setItem('platformStates', JSON.stringify(this.platformStates));
12
+ }
13
+
14
+ // 获取平台API密钥数量
15
+ function getPlatformKeyCount(platformId, filtered = false) {
16
+ if (filtered && this.searchTerm !== '') {
17
+ return this.apiKeys.filter(key =>
18
+ key.platform === platformId &&
19
+ this.matchesSearch(key.name, key.key)
20
+ ).length;
21
+ }
22
+ return this.apiKeys.filter(key => key.platform === platformId).length;
23
+ }
24
+
25
+ // 平台是否有API密钥
26
+ function hasPlatformKeys(platformId) {
27
+ return this.getPlatformKeyCount(platformId) > 0;
28
+ }
29
+
30
+ // 平台是否应该显示(基于筛选条件)
31
+ function isPlatformVisible(platformId) {
32
+ return this.platformFilters[platformId] === true;
33
+ }
34
+
35
+ // 获取所有平台
36
+ function getPlatforms() {
37
+ return JSON.parse(platformsData);
38
+ }
39
+
40
+ // 获取所有平台样式配置
41
+ function getPlatformStyles() {
42
+ return JSON.parse(platformStylesData);
43
+ }
44
+
45
+ // 获取特定平台的样式
46
+ function getPlatformStyle(platformId) {
47
+ const styles = getPlatformStyles();
48
+ return styles[platformId] || {};
49
+ }
50
+
51
+ // 切换平台筛选状态
52
+ function togglePlatformFilter(platformId) {
53
+ this.platformFilters[platformId] = !this.platformFilters[platformId];
54
+
55
+ // 检查是否所有平台都被选中
56
+ const platforms = this.getPlatforms();
57
+ this.allPlatformsSelected = platforms.every(platform =>
58
+ this.platformFilters[platform.id] === true
59
+ );
60
+
61
+ // 保存筛选状态到localStorage
62
+ localStorage.setItem('platformFilters', JSON.stringify(this.platformFilters));
63
+ }
64
+
65
+ // 切换所有平台筛选状态
66
+ function toggleAllPlatformFilters() {
67
+ const newState = !this.allPlatformsSelected;
68
+ this.allPlatformsSelected = newState;
69
+
70
+ // 将所有平台设置为相同的状态
71
+ const platforms = this.getPlatforms();
72
+ platforms.forEach(platform => {
73
+ this.platformFilters[platform.id] = newState;
74
+ });
75
+
76
+ // 保存筛选状态到localStorage
77
+ localStorage.setItem('platformFilters', JSON.stringify(this.platformFilters));
78
+ }
static/js/api-key-manager/ui-utils.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API密钥管理器 - UI工具模块
3
+ * 包含各种UI交互辅助功能
4
+ */
5
+
6
+ // 复制到剪贴板
7
+ function copyToClipboard(text, id) {
8
+ // 使用临时textarea元素复制文本
9
+ const textarea = document.createElement('textarea');
10
+ textarea.value = text;
11
+ textarea.style.position = 'fixed';
12
+ document.body.appendChild(textarea);
13
+ textarea.select();
14
+
15
+ try {
16
+ document.execCommand('copy');
17
+ this.copiedId = id;
18
+
19
+ // 使用SweetAlert2显示复制成功动画
20
+ const Toast = Swal.mixin({
21
+ toast: true,
22
+ position: 'top-end',
23
+ showConfirmButton: false,
24
+ timer: 1500,
25
+ timerProgressBar: true,
26
+ didOpen: (toast) => {
27
+ toast.onmouseenter = Swal.stopTimer;
28
+ toast.onmouseleave = Swal.resumeTimer;
29
+ }
30
+ });
31
+
32
+ Toast.fire({
33
+ icon: 'success',
34
+ title: '已复制到剪贴板',
35
+ background: '#f0fdf4',
36
+ iconColor: '#16a34a'
37
+ });
38
+
39
+ // 2秒后重置复制状态
40
+ setTimeout(() => {
41
+ this.copiedId = null;
42
+ }, 2000);
43
+ } catch (err) {
44
+ console.error('复制失败:', err);
45
+ Swal.fire({
46
+ icon: 'error',
47
+ title: '复制失败',
48
+ text: '请手动复制内容',
49
+ confirmButtonColor: '#0284c7'
50
+ });
51
+ } finally {
52
+ document.body.removeChild(textarea);
53
+ }
54
+ }
55
+
56
+ // 显示通知
57
+ function showNotification(message, type = 'info') {
58
+ const event = new CustomEvent('show-notification', {
59
+ detail: { message, type }
60
+ });
61
+ window.dispatchEvent(event);
62
+ }
63
+
64
+ // 获取总API密钥数量
65
+ function totalKeyCount() {
66
+ return this.apiKeys.length;
67
+ }
68
+
69
+ // 搜索匹配
70
+ function matchesSearch(name, key, notes) {
71
+ if (!this.searchTerm) return true;
72
+
73
+ const searchLower = this.searchTerm.toLowerCase();
74
+ return (
75
+ (name && name.toLowerCase().includes(searchLower)) ||
76
+ (key && key.toLowerCase().includes(searchLower))
77
+ );
78
+ }
static/js/main.js ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API密钥管理系统 - 主JavaScript文件
2
+ // ==============================
3
+
4
+ // 批量操作模块 - Alpine.js集成
5
+ window.bulkOperations = {
6
+ selectedKeys: [], // 存储选中的密钥ID
7
+ isBulkDelete: false, // 标记是否进行批量删除操作
8
+
9
+ // 计算是否所有可见密钥都被选中
10
+ get isAllSelected() {
11
+ const allVisibleKeyIds = this.getAllVisibleKeyIds();
12
+ return allVisibleKeyIds.length > 0 &&
13
+ allVisibleKeyIds.every(id => this.selectedKeys.includes(id));
14
+ },
15
+
16
+ // 获取当前页面中所有可见密钥的ID
17
+ getAllVisibleKeyIds() {
18
+ const keyElements = document.querySelectorAll('[data-key-id]');
19
+ const keyIds = [];
20
+ keyElements.forEach(el => {
21
+ // 只收集DOM中可见的元素ID
22
+ if (el.offsetParent !== null) {
23
+ const keyId = el.getAttribute('data-key-id');
24
+ if (keyId) keyIds.push(keyId);
25
+ }
26
+ });
27
+ return keyIds;
28
+ },
29
+
30
+ // 切换单个密钥的选中状态
31
+ toggleKeySelection(keyId) {
32
+ const index = this.selectedKeys.indexOf(keyId);
33
+ if (index === -1) {
34
+ this.selectedKeys.push(keyId);
35
+ } else {
36
+ this.selectedKeys.splice(index, 1);
37
+ }
38
+ },
39
+
40
+ // 切换全选/取消全选状态
41
+ toggleSelectAll() {
42
+ const allIds = this.getAllVisibleKeyIds();
43
+
44
+ if (this.isAllSelected) {
45
+ // 取消全选 - 从选中列表中移除所有可见ID
46
+ this.selectedKeys = this.selectedKeys.filter(id => !allIds.includes(id));
47
+ } else {
48
+ // 全选 - 将所有可见ID添加到选中列表
49
+ const newSelection = [...this.selectedKeys];
50
+ allIds.forEach(id => {
51
+ if (!newSelection.includes(id)) {
52
+ newSelection.push(id);
53
+ }
54
+ });
55
+ this.selectedKeys = newSelection;
56
+ }
57
+ },
58
+
59
+ // 执行批量删除操作
60
+ bulkDeleteApiKeys() {
61
+ if (this.selectedKeys.length === 0) return;
62
+
63
+ this.isBulkDelete = true;
64
+ this.$store.apiKeys.showDeleteConfirm = true;
65
+ },
66
+
67
+ // 触发批量复制功能 - 调用Alpine.js实例中的方法
68
+ bulkCopyApiKeys() {
69
+ const apiKeyManager = document.querySelector('[x-data]').__x.$data;
70
+
71
+ if (apiKeyManager && typeof apiKeyManager.bulkCopyApiKeys === 'function') {
72
+ apiKeyManager.bulkCopyApiKeys();
73
+ } else {
74
+ console.error('无法找到Alpine.js实例或批量复制方法');
75
+ showNotification('复制功能初始化失败,请刷新页面重试', 'error');
76
+ }
77
+ }
78
+ };
79
+
80
+ // 认证处理模块
81
+ // ==============================
82
+
83
+ // 检测并处理认证失效情况
84
+ function handleAuthenticationError(response) {
85
+ if (response.status === 401) {
86
+ showNotification('会话已过期,正在重定向到登录页面...', 'error');
87
+
88
+ setTimeout(() => {
89
+ window.location.href = '/login';
90
+ }, 2000);
91
+
92
+ return true; // 已处理认证错误
93
+ }
94
+ return false; // 非认证错误
95
+ }
96
+
97
+ // 扩展fetch API,添加认证错误处理
98
+ const originalFetch = window.fetch;
99
+ window.fetch = async function(url, options = {}) {
100
+ try {
101
+ const response = await originalFetch(url, options);
102
+
103
+ if (handleAuthenticationError(response)) {
104
+ return response; // 认证错误已处理,返回原始响应
105
+ }
106
+
107
+ return response;
108
+ } catch (error) {
109
+ console.error('Fetch error:', error);
110
+ throw error;
111
+ }
112
+ };
113
+
114
+ // 初始化XMLHttpRequest拦截器处理认证问题
115
+ function initXHRInterceptor() {
116
+ const originalXHROpen = XMLHttpRequest.prototype.open;
117
+ const originalXHRSend = XMLHttpRequest.prototype.send;
118
+
119
+ // 拦截open方法以保存URL
120
+ XMLHttpRequest.prototype.open = function() {
121
+ this._url = arguments[1];
122
+ originalXHROpen.apply(this, arguments);
123
+ };
124
+
125
+ // 拦截send方法以添加认证检查
126
+ XMLHttpRequest.prototype.send = function() {
127
+ const xhr = this;
128
+ const originalOnReadyStateChange = xhr.onreadystatechange;
129
+
130
+ xhr.onreadystatechange = function() {
131
+ if (xhr.readyState === 4 && xhr.status === 401) {
132
+ showNotification('会话已过期,正在重定向到登录页面...', 'error');
133
+ setTimeout(() => {
134
+ window.location.href = '/login';
135
+ }, 2000);
136
+ }
137
+
138
+ if (originalOnReadyStateChange) {
139
+ originalOnReadyStateChange.apply(this, arguments);
140
+ }
141
+ };
142
+
143
+ originalXHRSend.apply(this, arguments);
144
+ };
145
+ }
146
+
147
+ // 初��化模块
148
+ // ==============================
149
+ document.addEventListener('DOMContentLoaded', () => {
150
+ initScrollContainers(); // 初始化滚动容器
151
+ initKeyboardShortcuts(); // 设置键盘快捷键
152
+ initBackToTop(); // 设置回到顶部按钮
153
+ initXHRInterceptor(); // 启用XHR拦截器处理认证
154
+ });
155
+
156
+ // 设置键盘快捷键
157
+ function initKeyboardShortcuts() {
158
+ // Alt+N: 打开添加密钥模态框
159
+ document.addEventListener('keydown', (e) => {
160
+ if (e.altKey && e.key === 'n') {
161
+ window.dispatchEvent(new CustomEvent('open-add-modal'));
162
+ }
163
+ });
164
+ }
165
+
166
+ // 初始化滚动容器,处理长密钥的滚动显示
167
+ function initScrollContainers() {
168
+ const scrollContainers = document.querySelectorAll('.key-scroll-container');
169
+ scrollContainers.forEach(container => {
170
+ // 鼠标悬停时显示滚动条
171
+ container.addEventListener('mouseenter', () => {
172
+ if (container.scrollWidth > container.clientWidth) {
173
+ container.style.overflowX = 'auto';
174
+ }
175
+ });
176
+
177
+ // 鼠标离开时隐藏滚动条
178
+ container.addEventListener('mouseleave', () => {
179
+ container.style.overflowX = 'hidden';
180
+ });
181
+ });
182
+ }
183
+
184
+ // 工具函数
185
+ // ==============================
186
+
187
+ // 显示通知消息
188
+ // message: 通知内容
189
+ // type: 通知类型 (success, error, info)
190
+ function showNotification(message, type = 'info') {
191
+ const event = new CustomEvent('show-notification', {
192
+ detail: { message, type }
193
+ });
194
+ window.dispatchEvent(event);
195
+ }
196
+
197
+ // UI控件
198
+ // ==============================
199
+
200
+ // 初始化回到顶部按钮功能
201
+ function initBackToTop() {
202
+ const backToTopBtn = document.getElementById('backToTop');
203
+ if (!backToTopBtn) return;
204
+
205
+ const scrollThreshold = 50; // 显示按钮的滚动阈值
206
+
207
+ // 更新按钮可见性
208
+ const updateButtonVisibility = () => {
209
+ if (window.scrollY > scrollThreshold) {
210
+ backToTopBtn.classList.add('visible');
211
+ } else {
212
+ backToTopBtn.classList.remove('visible');
213
+ }
214
+ };
215
+
216
+ // 监听滚动事件
217
+ window.addEventListener('scroll', updateButtonVisibility);
218
+
219
+ // 设置按钮点击行为
220
+ backToTopBtn.addEventListener('click', () => {
221
+ window.scrollTo({
222
+ top: 0,
223
+ behavior: 'smooth'
224
+ });
225
+ });
226
+
227
+ // 初始状态检查
228
+ updateButtonVisibility();
229
+ }
230
+
231
+ // 应用状态管理
232
+ // ==============================
233
+
234
+ // 重置所有本地存储的应用状态
235
+ function resetAppState() {
236
+ // 需要清除的本地存储键
237
+ const stateKeys = [
238
+ 'platformStates',
239
+ 'platformFilters',
240
+ 'lastSelectedPlatform'
241
+ ];
242
+
243
+ // 清除所有状态
244
+ stateKeys.forEach(key => localStorage.removeItem(key));
245
+
246
+ showNotification('已重置所有浏览状态,刷新页面以应用变更。', 'info');
247
+
248
+ // 自动刷新页面应用更改
249
+ setTimeout(() => window.location.reload(), 1000);
250
+ }
251
+
252
+ // 导出全局函数
253
+ window.resetAppState = resetAppState;
templates/base.html ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>API密钥管理系统</title>
7
+ <!-- Tailwind CSS -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <!-- 自定义Tailwind配置 -->
10
+ <script>
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: {
16
+ 50: '#f0f9ff',
17
+ 100: '#e0f2fe',
18
+ 200: '#bae6fd',
19
+ 300: '#7dd3fc',
20
+ 400: '#38bdf8',
21
+ 500: '#0ea5e9',
22
+ 600: '#0284c7',
23
+ 700: '#0369a1',
24
+ 800: '#075985',
25
+ 900: '#0c4a6e',
26
+ 950: '#082f49',
27
+ },
28
+ },
29
+ animation: {
30
+ 'fade-in': 'fadeIn 0.3s ease-in-out',
31
+ 'fade-out': 'fadeOut 0.3s ease-in-out',
32
+ 'slide-down': 'slideDown 0.3s ease-in-out',
33
+ 'slide-up': 'slideUp 0.3s ease-in-out',
34
+ 'expand': 'expand 0.3s ease-in-out',
35
+ 'collapse': 'collapse 0.3s ease-in-out',
36
+ },
37
+ keyframes: {
38
+ fadeIn: {
39
+ '0%': { opacity: '0' },
40
+ '100%': { opacity: '1' },
41
+ },
42
+ fadeOut: {
43
+ '0%': { opacity: '1' },
44
+ '100%': { opacity: '0' },
45
+ },
46
+ slideDown: {
47
+ '0%': { maxHeight: '0', opacity: '0', transform: 'translateY(-10px)' },
48
+ '100%': { maxHeight: '1000px', opacity: '1', transform: 'translateY(0)' }
49
+ },
50
+ slideUp: {
51
+ '0%': { maxHeight: '1000px', opacity: '1', transform: 'translateY(0)' },
52
+ '100%': { maxHeight: '0', opacity: '0', transform: 'translateY(-10px)' }
53
+ },
54
+ expand: {
55
+ '0%': { transform: 'scaleY(0)', transformOrigin: 'top', opacity: '0' },
56
+ '100%': { transform: 'scaleY(1)', transformOrigin: 'top', opacity: '1' }
57
+ },
58
+ collapse: {
59
+ '0%': { transform: 'scaleY(1)', transformOrigin: 'top', opacity: '1' },
60
+ '100%': { transform: 'scaleY(0)', transformOrigin: 'top', opacity: '0' }
61
+ },
62
+ },
63
+ },
64
+ },
65
+ }
66
+ </script>
67
+ <!-- Alpine.js -->
68
+ <script defer src="https://unpkg.com/[email protected]/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 -->
72
+ <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
73
+ <!-- 自定义样式 -->
74
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
75
+ <link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
76
+ {% block head %}{% endblock %}
77
+ </head>
78
+ <body class="bg-gray-50 text-gray-900 min-h-screen">
79
+
80
+ <!-- 顶部导航 -->
81
+ <header class="bg-white shadow">
82
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
83
+ <h1 class="text-2xl font-bold text-primary-700">
84
+ API密钥管理系统
85
+ </h1>
86
+ <div class="flex items-center space-x-4">
87
+ <!-- 退出按钮 -->
88
+ <a href="{{ url_for('web.logout') }}" class="flex items-center px-3 py-2 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-50 rounded-md transition-colors">
89
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
90
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
91
+ </svg>
92
+ 退出登录
93
+ </a>
94
+ </div>
95
+ </div>
96
+ </header>
97
+
98
+ <!-- 主内容区 -->
99
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
100
+ {% block content %}{% endblock %}
101
+ </main>
102
+
103
+ <!-- 回到顶部按钮 -->
104
+ <div id="backToTop" class="back-to-top" title="回到顶部">
105
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
106
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
107
+ </svg>
108
+ </div>
109
+
110
+ <!-- 通知弹出组件 -->
111
+ <div x-data="notificationSystem()"
112
+ x-init="init()"
113
+ @show-notification.window="add($event.detail.message, $event.detail.type)"
114
+ class="fixed bottom-4 right-4 z-50 space-y-4"
115
+ x-cloak>
116
+ <template x-for="notification in notifications" :key="notification.id">
117
+ <div x-show="notification.visible"
118
+ x-transition:enter="transition ease-out duration-300"
119
+ x-transition:enter-start="transform translate-y-2 opacity-0"
120
+ x-transition:enter-end="transform translate-y-0 opacity-100"
121
+ x-transition:leave="transition ease-in duration-200"
122
+ x-transition:leave-start="transform translate-y-0 opacity-100"
123
+ x-transition:leave-end="transform translate-y-2 opacity-0"
124
+ :class="{
125
+ 'bg-green-50 text-green-800 border-green-400': notification.type === 'success',
126
+ 'bg-red-50 text-red-800 border-red-400': notification.type === 'error',
127
+ 'bg-blue-50 text-blue-800 border-blue-400': notification.type === 'info'
128
+ }"
129
+ class="p-4 border-l-4 shadow-md rounded-r-lg flex justify-between items-center min-w-[300px]">
130
+ <div x-text="notification.message"></div>
131
+ <button @click="remove(notification.id)" class="ml-4 text-gray-500 hover:text-gray-700">
132
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
133
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
134
+ </svg>
135
+ </button>
136
+ </div>
137
+ </template>
138
+ </div>
139
+
140
+ <!-- 自定义脚本 -->
141
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
142
+ {% block scripts %}{% endblock %}
143
+
144
+ <script>
145
+ // 通知系统
146
+ function notificationSystem() {
147
+ return {
148
+ notifications: [],
149
+ nextId: 1,
150
+ init() {
151
+ // 初始化通知系统
152
+ },
153
+ add(message, type = 'info') {
154
+ const id = this.nextId++;
155
+ const notification = {
156
+ id,
157
+ message,
158
+ type,
159
+ visible: true
160
+ };
161
+ this.notifications.push(notification);
162
+
163
+ // 3秒后自动移除
164
+ setTimeout(() => {
165
+ this.remove(id);
166
+ }, 3000);
167
+ },
168
+ remove(id) {
169
+ const index = this.notifications.findIndex(n => n.id === id);
170
+ if (index !== -1) {
171
+ // 隐藏通知
172
+ this.notifications[index].visible = false;
173
+
174
+ // 300毫秒后从数组中移除(与过渡动画时间一致)
175
+ setTimeout(() => {
176
+ this.notifications = this.notifications.filter(n => n.id !== id);
177
+ }, 300);
178
+ }
179
+ }
180
+ };
181
+ }
182
+ </script>
183
+ </body>
184
+ </html>
templates/components/api_key_list.html ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- 分组的API密钥列表 -->
2
+ <div class="space-y-6">
3
+ <!-- 遍历每个平台 -->
4
+ {% for platform in platforms %}
5
+ {% set platform_keys = grouped_keys.get(platform.id, {}).get('keys', []) %}
6
+ <!-- 如果平台有API密钥且被筛选器选中,搜索模式下仅当该平台有匹配结果时显示 -->
7
+ <div
8
+ x-cloak
9
+ x-show="!isLoading && isPlatformVisible('{{ platform.id }}') && (searchTerm === '' && hasPlatformKeys('{{ platform.id }}') || (searchTerm !== '' && apiKeys.filter(key => key.platform === '{{ platform.id }}' && matchesSearch(key.name, key.key)).length > 0))"
10
+ x-transition:enter="transition ease-out duration-300"
11
+ x-transition:enter-start="opacity-0 transform scale-95"
12
+ x-transition:enter-end="opacity-100 transform scale-100"
13
+ class="bg-white rounded-lg shadow overflow-hidden"
14
+ >
15
+ <!-- 平台标题 - 可折叠 -->
16
+ <div
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)',
24
+ borderColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['border-color'] : 'rgba(229, 231, 235, 1)'
25
+ }"
26
+ >
27
+ <!-- 平台批量选择复选框 -->
28
+ <div class="mr-3 flex items-center" @click.stop>
29
+ <div class="relative h-5 w-5">
30
+ <!-- 圆形边框 -->
31
+ <div
32
+ class="absolute inset-0 rounded-full border-2 transition-colors duration-200"
33
+ :class="selectedPlatforms.includes('{{ platform.id }}') ? 'border-blue-500' : 'border-gray-300'"
34
+ ></div>
35
+
36
+ <!-- 未选中时的白色背景 -->
37
+ <div
38
+ class="absolute inset-0.5 rounded-full bg-white"
39
+ x-show="!selectedPlatforms.includes('{{ platform.id }}')"
40
+ ></div>
41
+
42
+ <!-- 选中时的蓝色填充 -->
43
+ <div
44
+ x-show="selectedPlatforms.includes('{{ platform.id }}')"
45
+ class="absolute inset-0.5 rounded-full bg-blue-200"
46
+ ></div>
47
+
48
+ <!-- 透明的复选框,用于捕获点击事件 -->
49
+ <input
50
+ type="checkbox"
51
+ :checked="selectedPlatforms.includes('{{ platform.id }}')"
52
+ @click="togglePlatformSelection('{{ platform.id }}')"
53
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
54
+ />
55
+ </div>
56
+ </div>
57
+
58
+ <h2 class="text-lg font-medium flex items-center"
59
+ :style="{
60
+ color: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}').color : '#111827'
61
+ }"
62
+ >
63
+ <span>{{ platform.name }}</span>
64
+ <span
65
+ x-show="getPlatformKeyCount('{{ platform.id }}') > 0"
66
+ x-text="getPlatformKeyCount('{{ platform.id }}')"
67
+ class="ml-2 platform-title-counter rounded-full"
68
+ :style="{
69
+ backgroundColor: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}')['background-color'] : 'rgba(224, 242, 254, 1)',
70
+ color: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}').color : '#0369a1'
71
+ }"
72
+ ></span>
73
+ </h2>
74
+ <svg
75
+ xmlns="http://www.w3.org/2000/svg"
76
+ :class="{'transform rotate-180': platformStates['{{ platform.id }}']}"
77
+ class="h-5 w-5 accordion-icon"
78
+ :style="{
79
+ color: '{{ platform.id }}' in getPlatformStyles() ? getPlatformStyle('{{ platform.id }}').color : '#6b7280'
80
+ }"
81
+ viewBox="0 0 20 20"
82
+ fill="currentColor"
83
+ >
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
155
+ x-show="platformStates['{{ platform.id }}']"
156
+ x-transition:enter="transition-all ease-out duration-400 accordion-expand"
157
+ x-transition:enter-start="opacity-0 max-h-0 overflow-hidden transform scale-y-95 origin-top"
158
+ x-transition:enter-end="opacity-100 max-h-[2000px] transform scale-y-100 origin-top"
159
+ x-transition:leave="transition-all ease-in-out duration-300 accordion-collapse"
160
+ x-transition:leave-start="opacity-100 max-h-[2000px] transform scale-y-100 origin-top"
161
+ x-transition:leave-end="opacity-0 max-h-0 overflow-hidden transform scale-y-95 origin-top"
162
+ class="divide-y divide-gray-200 accordion-content"
163
+ >
164
+ <!-- 平台内的API密钥 -->
165
+ {% for key in platform_keys %}
166
+ <div
167
+ x-show="matchesSearch('{{ key.name }}', '{{ key.key }}')"
168
+ class="px-6 py-4 hover:bg-gray-50 transition-colors duration-150 flex flex-col md:flex-row md:items-center md:justify-between group"
169
+ x-transition:enter="transition ease-out duration-300"
170
+ x-transition:enter-start="opacity-0"
171
+ x-transition:enter-end="opacity-100"
172
+ :class="{'bg-blue-50': selectedKeys.includes('{{ key.id }}')}"
173
+ data-key-id="{{ key.id }}"
174
+ >
175
+ <!-- 简洁的蓝点复选框 -->
176
+ <div class="mr-3 flex items-center">
177
+ <div class="relative h-5 w-5">
178
+ <!-- 圆形边框 -->
179
+ <div
180
+ class="absolute inset-0 rounded-full border-2 transition-colors duration-200"
181
+ :class="selectedKeys.includes('{{ key.id }}') ? 'border-blue-500' : 'border-gray-300'"
182
+ ></div>
183
+
184
+ <!-- 未选中时的白色背景 -->
185
+ <div
186
+ class="absolute inset-0.5 rounded-full bg-white"
187
+ x-show="!selectedKeys.includes('{{ key.id }}')"
188
+ ></div>
189
+
190
+ <!-- 选中时的蓝色填充 -->
191
+ <div
192
+ x-show="selectedKeys.includes('{{ key.id }}')"
193
+ class="absolute inset-0.5 rounded-full bg-blue-200"
194
+ ></div>
195
+
196
+ <!-- 透明的复选框,用于捕获点击事件 -->
197
+ <input
198
+ type="checkbox"
199
+ :checked="selectedKeys.includes('{{ key.id }}')"
200
+ @click.stop="toggleKeySelection('{{ key.id }}')"
201
+ class="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
202
+ />
203
+ </div>
204
+ </div>
205
+
206
+ <div class="flex-grow min-w-0 mb-3 md:mb-0 md:mr-4">
207
+ <!-- 密钥名称 -->
208
+ <h3 class="text-sm font-medium text-gray-900 truncate">{{ key.name }}</h3>
209
+
210
+ <!-- 创建时间 - 重构版 -->
211
+ <div class="mt-1.5 flex items-center">
212
+ <!-- 日期部分 -->
213
+ <div class="flex items-center mr-2">
214
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
215
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
216
+ </svg>
217
+ <span class="text-xs text-gray-600">{{ key.created_at.split('T')[0] }}</span>
218
+ </div>
219
+
220
+ <!-- 时间部分 -->
221
+ <div class="flex items-center">
222
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 text-gray-500 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
223
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
224
+ </svg>
225
+ <span class="text-xs text-gray-600">{{ key.created_at.split('T')[1].split('.')[0] }}</span>
226
+ </div>
227
+ </div>
228
+
229
+ <!-- 密钥值 -->
230
+ <div class="mt-1 relative group key-scroll-container custom-scrollbar overflow-x-auto">
231
+ <div class="text-sm text-gray-500 font-mono py-1 pr-10">
232
+ {{ key.key }}
233
+ </div>
234
+
235
+ <!-- 悬浮时的高亮背景 -->
236
+ <div class="absolute inset-0 bg-primary-50 opacity-0 group-hover:opacity-100 transition-opacity duration-150 -z-10 rounded"></div>
237
+ </div>
238
+
239
+ </div>
240
+
241
+ <!-- 操作按钮 -->
242
+ <div class="flex space-x-2 justify-end">
243
+ <!-- 复制按钮 -->
244
+ <button
245
+ @click="copyToClipboard('{{ key.key }}', '{{ key.id }}')"
246
+ class="p-2 text-gray-500 hover:text-primary-600 rounded-md hover:bg-gray-100 transition-colors duration-150 focus:outline-none"
247
+ :class="{'text-green-600': copiedId === '{{ key.id }}'}"
248
+ data-clipboard-text="{{ key.key }}"
249
+ >
250
+ <span x-show="copiedId !== '{{ key.id }}'">
251
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
252
+ <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" />
253
+ </svg>
254
+ </span>
255
+ <span x-show="copiedId === '{{ key.id }}'">
256
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
257
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
258
+ </svg>
259
+ </span>
260
+ </button>
261
+
262
+ <!-- 编辑按钮 -->
263
+ <button
264
+ @click="editApiKey('{{ key.id }}', '{{ key.name }}', '{{ key.key }}')"
265
+ class="p-2 text-gray-500 hover:text-blue-600 rounded-md hover:bg-gray-100 transition-colors duration-150 focus:outline-none"
266
+ >
267
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
268
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
269
+ </svg>
270
+ </button>
271
+
272
+ <!-- 删除按钮 -->
273
+ <button
274
+ @click="deleteApiKey('{{ key.id }}', '{{ key.name }}')"
275
+ class="p-2 text-gray-500 hover:text-red-600 rounded-md hover:bg-gray-100 transition-colors duration-150 focus:outline-none"
276
+ >
277
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
278
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
279
+ </svg>
280
+ </button>
281
+ </div>
282
+ </div>
283
+ {% endfor %}
284
+
285
+ <!-- 如果此平台没有密钥或者搜索无结果时显示 -->
286
+ <div
287
+ x-show="getPlatformKeyCount('{{ platform.id }}') === 0 || (searchTerm !== '' && getPlatformKeyCount('{{ platform.id }}', true) === 0)"
288
+ class="px-6 py-8 text-center text-gray-500"
289
+ >
290
+ <div class="flex flex-col items-center">
291
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mb-4 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
292
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
293
+ </svg>
294
+ <p x-show="searchTerm === ''">此平台暂无API密钥</p>
295
+ <p x-show="searchTerm !== ''">没有找到匹配的API密钥</p>
296
+ <button
297
+ @click="showAddModal = true; newKey.platform = '{{ platform.id }}'"
298
+ class="mt-4 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none transition-colors duration-200 space-x-2"
299
+ >
300
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
301
+ <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
302
+ </svg>
303
+ <span>添加 {{ platform.name }} 密钥</span>
304
+ </button>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ {% endfor %}
310
+ </div>
templates/components/modals/add_key_modal.html ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- 现代化API密钥添加模态框 -->
2
+ <div
3
+ x-cloak
4
+ x-show="showAddModal"
5
+ class="fixed inset-0 overflow-y-auto z-50"
6
+ x-transition:enter="transition ease-out duration-300"
7
+ x-transition:enter-start="opacity-0"
8
+ x-transition:enter-end="opacity-100"
9
+ x-transition:leave="transition ease-in duration-200"
10
+ x-transition:leave-start="opacity-100"
11
+ x-transition:leave-end="opacity-0"
12
+ >
13
+ <div class="flex items-center justify-center min-h-screen p-4">
14
+ <!-- 高斯模糊背景 -->
15
+ <div
16
+ x-cloak
17
+ @click="showAddModal = false"
18
+ x-show="showAddModal"
19
+ x-transition:enter="transition ease-out duration-300"
20
+ x-transition:enter-start="opacity-0 backdrop-blur-none"
21
+ x-transition:enter-end="opacity-80 backdrop-blur-md"
22
+ x-transition:leave="transition ease-in duration-200"
23
+ x-transition:leave-start="opacity-80 backdrop-blur-md"
24
+ x-transition:leave-end="opacity-0 backdrop-blur-none"
25
+ class="fixed inset-0 bg-gray-900/60 backdrop-blur-md"
26
+ ></div>
27
+
28
+ <!-- 模态框卡片 -->
29
+ <div
30
+ x-cloak
31
+ x-show="showAddModal"
32
+ @click.away="showAddModal = false"
33
+ x-transition:enter="transition ease-out duration-300"
34
+ x-transition:enter-start="opacity-0 transform scale-95 translate-y-4"
35
+ x-transition:enter-end="opacity-100 transform scale-100 translate-y-0"
36
+ x-transition:leave="transition ease-in duration-200"
37
+ x-transition:leave-start="opacity-100 transform scale-100 translate-y-0"
38
+ x-transition:leave-end="opacity-0 transform scale-95 translate-y-4"
39
+ class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden w-4/5 max-w-3xl max-h-[80vh] z-50 relative border border-gray-200 dark:border-gray-700 flex flex-col"
40
+ >
41
+ <!-- 顶部图案装饰 -->
42
+ <div class="absolute top-0 left-0 right-0 h-1.5 bg-gradient-to-r from-primary-400 via-primary-500 to-primary-600"></div>
43
+
44
+ <!-- 模态框标题栏 -->
45
+ <div class="px-6 py-4 flex justify-between items-center">
46
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
47
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-primary-500" viewBox="0 0 20 20" fill="currentColor">
48
+ <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v-1l1-1 1-1-.257-.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd" />
49
+ </svg>
50
+ 添加新API密钥
51
+ </h3>
52
+ <button
53
+ @click="showAddModal = false"
54
+ class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors focus:outline-none rounded-full p-1"
55
+ aria-label="关闭"
56
+ >
57
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
58
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
59
+ </svg>
60
+ </button>
61
+ </div>
62
+
63
+ <!-- 模态框内容 -->
64
+ <div class="px-6 pb-6 flex-grow overflow-y-auto">
65
+ <form @submit.prevent="addApiKey()" class="space-y-5">
66
+ <!-- 平台选择卡片 -->
67
+ <div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 transition-all duration-300 hover:shadow-md border border-gray-100 dark:border-gray-700">
68
+ <label for="platform" class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center">
69
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-primary-500" viewBox="0 0 20 20" fill="currentColor">
70
+ <path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" />
71
+ </svg>
72
+ 选择平台 <span class="text-red-500 ml-1">*</span>
73
+ </label>
74
+ <div class="relative">
75
+ <select
76
+ id="platform"
77
+ x-model="newKey.platform"
78
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm pl-3 pr-10 py-2.5 text-sm transition-colors duration-200 bg-white dark:bg-gray-700 appearance-none focus:outline-none"
79
+ required
80
+ >
81
+ <option value="" disabled>-- 选择平台 --</option>
82
+ {% for platform in platforms %}
83
+ <option value="{{ platform.id }}">{{ platform.name }}</option>
84
+ {% endfor %}
85
+ </select>
86
+ <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500 dark:text-gray-400">
87
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
88
+ <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" />
89
+ </svg>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <!-- 名称输入卡片 -->
95
+ <div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 transition-all duration-300 hover:shadow-md border border-gray-100 dark:border-gray-700">
96
+ <label for="name" class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center">
97
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-primary-500" viewBox="0 0 20 20" fill="currentColor">
98
+ <path fill-rule="evenodd" d="M10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z" clip-rule="evenodd" />
99
+ </svg>
100
+ 密钥标识名称 <span class="text-gray-400 text-xs ml-1">(可选)</span>
101
+ </label>
102
+ <div class="mt-1 relative rounded-md shadow-sm">
103
+ <input
104
+ type="text"
105
+ id="name"
106
+ x-model="newKey.name"
107
+ placeholder="为密钥添加易于识别的名称,留空将自动生成"
108
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm pl-3 pr-10 py-2.5 text-sm transition-colors duration-200 focus:outline-none"
109
+ autocomplete="off"
110
+ >
111
+ <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
112
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
114
+ </svg>
115
+ </div>
116
+ </div>
117
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">好的命名有助于快速找到特定密钥</p>
118
+ </div>
119
+
120
+ <!-- API密钥输入卡片 -->
121
+ <div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 transition-all duration-300 hover:shadow-md border border-gray-100 dark:border-gray-700">
122
+ <label for="key" class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center">
123
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-primary-500" viewBox="0 0 20 20" fill="currentColor">
124
+ <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v-1l1-1 1-1-.257-.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd" />
125
+ </svg>
126
+ API密钥 <span class="text-red-500 ml-1">*</span>
127
+ </label>
128
+ <div class="mt-1">
129
+ <textarea
130
+ id="key"
131
+ x-model="newKey.key"
132
+ placeholder="在此输入您的API密钥,每行一个密钥将被视为单独添加"
133
+ rows="10"
134
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm pl-3 pr-3 py-2.5 text-sm font-mono transition-colors duration-200 resize-none focus:outline-none"
135
+ required
136
+ spellcheck="false"
137
+ autocomplete="off"
138
+ autocorrect="off"
139
+ autocapitalize="off"
140
+ style="min-height: 200px;"
141
+ ></textarea>
142
+ </div>
143
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">多行输入将作为多个密钥添加</p>
144
+ </div>
145
+
146
+ <!-- 错误消息 -->
147
+ <div
148
+ x-show="errorMessage"
149
+ x-text="errorMessage"
150
+ class="p-3 text-sm text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 rounded-lg"
151
+ ></div>
152
+
153
+ <!-- 按钮组 -->
154
+ <div class="flex justify-end space-x-3 mt-6">
155
+ <button
156
+ type="button"
157
+ @click="showAddModal = false"
158
+ class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none transition-colors duration-200"
159
+ >
160
+ 取消
161
+ </button>
162
+ <button
163
+ type="submit"
164
+ :disabled="isSubmitting"
165
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none transition-colors duration-200 space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
166
+ >
167
+ <span x-show="!isSubmitting" class="flex items-center">
168
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" viewBox="0 0 20 20" fill="currentColor">
169
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
170
+ </svg>
171
+ 添加密钥
172
+ </span>
173
+ <span x-show="isSubmitting" class="flex items-center">
174
+ <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
175
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
176
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
177
+ </svg>
178
+ 处理中...
179
+ </span>
180
+ </button>
181
+ </div>
182
+ </form>
183
+ </div>
184
+
185
+ <!-- 键盘快捷键提示 -->
186
+ <div class="absolute bottom-3 left-6 text-xs text-gray-400 dark:text-gray-500 flex items-center">
187
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" viewBox="0 0 20 20" fill="currentColor">
188
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2h-1V9a1 1 0 00-1-1z" clip-rule="evenodd" />
189
+ </svg>
190
+ Esc 关闭 | Alt+Enter 提交
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
templates/components/modals/delete_confirm_modal.html ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- 删除确认模态框 -->
2
+ <div
3
+ x-cloak
4
+ x-show="showDeleteConfirm"
5
+ class="fixed inset-0 overflow-y-auto z-50"
6
+ x-transition:enter="transition ease-out duration-300"
7
+ x-transition:enter-start="opacity-0"
8
+ x-transition:enter-end="opacity-100"
9
+ x-transition:leave="transition ease-in duration-200"
10
+ x-transition:leave-start="opacity-100"
11
+ x-transition:leave-end="opacity-0"
12
+ @keydown.escape.window="showDeleteConfirm = false"
13
+ @keydown.enter.window="if(showDeleteConfirm && !isDeleting) confirmDelete()"
14
+ >
15
+ <div class="flex items-center justify-center min-h-screen p-4">
16
+ <!-- 高斯模糊背景 -->
17
+ <div
18
+ x-cloak
19
+ @click="showDeleteConfirm = false"
20
+ x-show="showDeleteConfirm"
21
+ x-transition:enter="transition ease-out duration-300"
22
+ x-transition:enter-start="opacity-0 backdrop-blur-none"
23
+ x-transition:enter-end="opacity-80 backdrop-blur-md"
24
+ x-transition:leave="transition ease-in duration-200"
25
+ x-transition:leave-start="opacity-80 backdrop-blur-md"
26
+ x-transition:leave-end="opacity-0 backdrop-blur-none"
27
+ class="fixed inset-0 bg-gray-900/60 backdrop-blur-md"
28
+ ></div>
29
+
30
+ <!-- 模态框内容 -->
31
+ <div
32
+ x-cloak
33
+ x-show="showDeleteConfirm"
34
+ @click.away="showDeleteConfirm = false"
35
+ x-transition:enter="transition ease-out duration-300"
36
+ x-transition:enter-start="opacity-0 transform scale-95 translate-y-4"
37
+ x-transition:enter-end="opacity-100 transform scale-100 translate-y-0"
38
+ x-transition:leave="transition ease-in duration-200"
39
+ x-transition:leave-start="opacity-100 transform scale-100 translate-y-0"
40
+ x-transition:leave-end="opacity-0 transform scale-95 translate-y-4"
41
+ class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden max-w-3xl z-50 relative border border-gray-200 dark:border-gray-700 flex flex-col w-full"
42
+ >
43
+ <!-- 顶部装饰条 -->
44
+ <div class="absolute top-0 left-0 right-0 h-1.5 bg-gradient-to-r from-red-400 via-red-500 to-red-600"></div>
45
+
46
+ <!-- 模态框标题 -->
47
+ <div class="px-6 py-4 flex justify-between items-center">
48
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
49
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-red-500" viewBox="0 0 20 20" fill="currentColor">
50
+ <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
51
+ </svg>
52
+ 确认删除
53
+ </h3>
54
+ <button
55
+ @click="showDeleteConfirm = false"
56
+ class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors focus:outline-none rounded-full p-1"
57
+ aria-label="关闭"
58
+ >
59
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
60
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
61
+ </svg>
62
+ </button>
63
+ </div>
64
+
65
+ <!-- 模态框内容 -->
66
+ <div class="px-6 pb-6">
67
+ <div class="bg-red-50 dark:bg-red-900/10 rounded-lg p-4 border border-red-100 dark:border-red-800/30 mb-4 shadow-sm">
68
+ <!-- 单个删除提示 -->
69
+ <template x-if="!isBulkDelete">
70
+ <div>
71
+ <p class="text-gray-700 dark:text-gray-300 mb-1 flex items-center">
72
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-red-500 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
73
+ <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
74
+ </svg>
75
+ 您确定要删除以下API密钥吗?
76
+ </p>
77
+ <p class="text-red-600 dark:text-red-400 text-sm font-medium ml-5.5">此操作无法撤销,删除后数据将无法恢复。</p>
78
+ </div>
79
+ </template>
80
+
81
+ <!-- 批量删除提示 -->
82
+ <template x-if="isBulkDelete">
83
+ <div>
84
+ <p class="text-gray-700 dark:text-gray-300 mb-1 flex items-center">
85
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-red-500 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
86
+ <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
87
+ </svg>
88
+ 您确定要删除 <span class="font-semibold text-red-600 dark:text-red-400" x-text="selectedKeys.length"></span> 个选中的API密钥吗?
89
+ </p>
90
+ <p class="text-red-600 dark:text-red-400 text-sm font-medium ml-5.5">此操作无法撤销,删除后数据将无法恢复。</p>
91
+ </div>
92
+ </template>
93
+ </div>
94
+
95
+ <!-- 单个删除时显示详情 -->
96
+ <div
97
+ x-show="!isBulkDelete"
98
+ class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 border border-gray-100 dark:border-gray-700 transition-all duration-300 hover:shadow-md relative overflow-hidden group"
99
+ >
100
+ <!-- 装饰性背景元素 -->
101
+ <div class="absolute inset-0 opacity-5 pointer-events-none">
102
+ <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
103
+ <defs>
104
+ <pattern id="key-pattern" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
105
+ <path d="M10,0 L10,20 M0,10 L20,10" stroke="currentColor" stroke-width="0.5" stroke-dasharray="2,2"/>
106
+ </pattern>
107
+ </defs>
108
+ <rect x="0" y="0" width="100%" height="100%" fill="url(#key-pattern)" />
109
+ </svg>
110
+ </div>
111
+
112
+ <div class="flex items-start relative">
113
+ <div class="flex-shrink-0 mr-4">
114
+ <div class="bg-red-100 dark:bg-red-800/20 w-10 h-10 rounded-full flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
115
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500 dark:text-red-400" viewBox="0 0 20 20" fill="currentColor">
116
+ <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v-1l1-1 1-1-.257-.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd" />
117
+ </svg>
118
+ </div>
119
+ </div>
120
+ <div class="flex-grow">
121
+ <div class="flex flex-col">
122
+ <span class="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">将要删除的密钥:</span>
123
+ <div class="bg-white dark:bg-gray-800 px-3 py-2 rounded-md border border-gray-200 dark:border-gray-700 shadow-sm">
124
+ <span
125
+ x-text="deleteKeyName && deleteKeyName.length > 60 ? deleteKeyName.substring(0, 60) + '...' : deleteKeyName"
126
+ class="font-medium text-gray-900 dark:text-white"
127
+ :title="deleteKeyName"
128
+ ></span>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- 美化的批量删除时显示数量汇总 -->
136
+ <div
137
+ x-show="isBulkDelete"
138
+ class="bg-gradient-to-r from-red-50 to-pink-50 dark:from-red-900/10 dark:to-pink-900/10 rounded-lg p-4 border border-red-100/50 dark:border-red-800/30 transition-all duration-300 hover:shadow-lg relative overflow-hidden group"
139
+ >
140
+ <!-- 装饰元素 - 微妙的图案背景 -->
141
+ <div class="absolute inset-0 opacity-5 pointer-events-none">
142
+ <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
143
+ <defs>
144
+ <pattern id="deletion-pattern" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
145
+ <path d="M0,10 L20,10 M10,0 L10,20" stroke="currentColor" stroke-width="0.5"/>
146
+ </pattern>
147
+ </defs>
148
+ <rect x="0" y="0" width="100%" height="100%" fill="url(#deletion-pattern)" />
149
+ </svg>
150
+ </div>
151
+
152
+ <div class="flex items-start relative">
153
+ <!-- 装饰图标 -->
154
+ <div class="flex-shrink-0 mr-4">
155
+ <div class="bg-red-100 dark:bg-red-800/20 w-10 h-10 rounded-full flex items-center justify-center">
156
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-red-500 dark:text-red-400" viewBox="0 0 20 20" fill="currentColor">
157
+ <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd" />
158
+ </svg>
159
+ </div>
160
+ </div>
161
+
162
+ <div class="flex-grow">
163
+ <h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200 mb-1">批量删除操作</h4>
164
+ <div class="text-xs text-gray-600 dark:text-gray-300 mb-3">以下项目将被永久删除:</div>
165
+
166
+ <!-- 数量显示 -->
167
+ <div class="flex items-center">
168
+ <div class="inline-flex items-center justify-center bg-red-100 text-red-800 dark:bg-red-800/30 dark:text-red-300 px-3 py-1 rounded-full text-sm font-medium shadow-sm">
169
+ <span x-text="selectedKeys.length" class="text-lg font-bold mr-1"></span>
170
+ <span>个API密钥</span>
171
+ </div>
172
+
173
+ <!-- 动态提示 -->
174
+ <div class="ml-3 text-xs text-gray-500 dark:text-gray-400 italic">
175
+ <template x-if="selectedKeys.length > 10">
176
+ <span>(大批量删除,请谨慎操作)</span>
177
+ </template>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- 按钮组 -->
185
+ <div class="flex justify-end space-x-3 mt-6">
186
+ <button
187
+ type="button"
188
+ @click="showDeleteConfirm = false"
189
+ class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none transition-colors duration-200"
190
+ >
191
+ 取消
192
+ </button>
193
+ <button
194
+ type="button"
195
+ @click="confirmDelete()"
196
+ :disabled="isDeleting"
197
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none transition-colors duration-200 space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
198
+ >
199
+ <template x-if="!isDeleting">
200
+ <span class="flex items-center">
201
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" viewBox="0 0 20 20" fill="currentColor">
202
+ <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" />
203
+ </svg>
204
+ 确认删除
205
+ </span>
206
+ </template>
207
+ <template x-if="isDeleting">
208
+ <span class="flex items-center">
209
+ <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
210
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
211
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
212
+ </svg>
213
+ 处理中...
214
+ </span>
215
+ </template>
216
+ </button>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- 键盘快捷键提示 -->
221
+ <div class="absolute bottom-3 left-6 text-xs text-gray-400 dark:text-gray-500 flex items-center">
222
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" viewBox="0 0 20 20" fill="currentColor">
223
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2h-1V9a1 1 0 00-1-1z" clip-rule="evenodd" />
224
+ </svg>
225
+ Esc 取消 | Enter 确认
226
+ </div>
227
+ </div>
228
+ </div>
229
+ </div>
templates/components/modals/edit_key_modal.html ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- 现代化API密钥编辑模态框 -->
2
+ <div
3
+ x-cloak
4
+ x-show="showEditModal"
5
+ class="fixed inset-0 overflow-y-auto z-50"
6
+ x-transition:enter="transition ease-out duration-300"
7
+ x-transition:enter-start="opacity-0"
8
+ x-transition:enter-end="opacity-100"
9
+ x-transition:leave="transition ease-in duration-200"
10
+ x-transition:leave-start="opacity-100"
11
+ x-transition:leave-end="opacity-0"
12
+ >
13
+ <div class="flex items-center justify-center min-h-screen p-4">
14
+ <!-- 高斯模糊背景 -->
15
+ <div
16
+ x-cloak
17
+ @click="showEditModal = false"
18
+ x-show="showEditModal"
19
+ x-transition:enter="transition ease-out duration-300"
20
+ x-transition:enter-start="opacity-0 backdrop-blur-none"
21
+ x-transition:enter-end="opacity-80 backdrop-blur-md"
22
+ x-transition:leave="transition ease-in duration-200"
23
+ x-transition:leave-start="opacity-80 backdrop-blur-md"
24
+ x-transition:leave-end="opacity-0 backdrop-blur-none"
25
+ class="fixed inset-0 bg-gray-900/60 backdrop-blur-md"
26
+ ></div>
27
+
28
+ <!-- 模态框卡片 -->
29
+ <div
30
+ x-cloak
31
+ x-show="showEditModal"
32
+ @click.away="showEditModal = false"
33
+ x-transition:enter="transition ease-out duration-300"
34
+ x-transition:enter-start="opacity-0 transform scale-95 translate-y-4"
35
+ x-transition:enter-end="opacity-100 transform scale-100 translate-y-0"
36
+ x-transition:leave="transition ease-in duration-200"
37
+ x-transition:leave-start="opacity-100 transform scale-100 translate-y-0"
38
+ x-transition:leave-end="opacity-0 transform scale-95 translate-y-4"
39
+ class="bg-white dark:bg-gray-800 rounded-xl shadow-2xl overflow-hidden w-4/5 max-w-3xl max-h-[80vh] z-50 relative border border-gray-200 dark:border-gray-700 flex flex-col"
40
+ >
41
+ <!-- 顶部图案装饰 -->
42
+ <div class="absolute top-0 left-0 right-0 h-1.5 bg-gradient-to-r from-primary-400 via-primary-500 to-primary-600"></div>
43
+
44
+ <!-- 模态框标题栏 -->
45
+ <div class="px-6 py-4 flex justify-between items-center">
46
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
47
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
49
+ </svg>
50
+ 编辑API密钥
51
+ </h3>
52
+ <button
53
+ @click="showEditModal = false"
54
+ class="text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors focus:outline-none rounded-full p-1"
55
+ aria-label="关闭"
56
+ >
57
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
58
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
59
+ </svg>
60
+ </button>
61
+ </div>
62
+
63
+ <!-- 模态框内容 -->
64
+ <div class="px-6 pb-6 flex-grow overflow-y-auto">
65
+ <form @submit.prevent="updateApiKey()" class="space-y-5">
66
+ <!-- 名称输入卡片 -->
67
+ <div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 transition-all duration-300 hover:shadow-md border border-gray-100 dark:border-gray-700">
68
+ <label for="edit-name" class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center">
69
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-primary-500" viewBox="0 0 20 20" fill="currentColor">
70
+ <path fill-rule="evenodd" d="M10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z" clip-rule="evenodd" />
71
+ </svg>
72
+ 密钥标识名称 <span class="text-gray-400 text-xs ml-1">(可选)</span>
73
+ </label>
74
+ <div class="mt-1 relative rounded-md shadow-sm">
75
+ <input
76
+ type="text"
77
+ id="edit-name"
78
+ x-model="editKey.name"
79
+ placeholder="为密钥添加易于识别的名称"
80
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm pl-3 pr-10 py-2.5 text-sm transition-colors duration-200 focus:outline-none"
81
+ autocomplete="off"
82
+ >
83
+ <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
84
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
86
+ </svg>
87
+ </div>
88
+ </div>
89
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">好的命名有助于快速找到特定密钥</p>
90
+ </div>
91
+
92
+ <!-- API密钥输入卡片 -->
93
+ <div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 transition-all duration-300 hover:shadow-md border border-gray-100 dark:border-gray-700">
94
+ <label for="edit-key" class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 flex items-center">
95
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5 text-primary-500" viewBox="0 0 20 20" fill="currentColor">
96
+ <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v-1l1-1 1-1-.257-.257A6 6 0 1118 8zm-6-4a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd" />
97
+ </svg>
98
+ API密钥 <span class="text-red-500 ml-1">*</span>
99
+ </label>
100
+ <div class="mt-1">
101
+ <input
102
+ type="text"
103
+ id="edit-key"
104
+ x-model="editKey.key"
105
+ placeholder="在此输入您的API密钥"
106
+ class="w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white shadow-sm pl-3 pr-3 py-2.5 text-sm font-mono transition-colors duration-200 focus:outline-none"
107
+ required
108
+ spellcheck="false"
109
+ autocomplete="off"
110
+ autocorrect="off"
111
+ autocapitalize="off"
112
+ >
113
+ </div>
114
+ </div>
115
+
116
+ <!-- 错误消息 -->
117
+ <div
118
+ x-show="errorMessage"
119
+ x-text="errorMessage"
120
+ class="p-3 text-sm text-red-600 bg-red-50 dark:bg-red-900/20 dark:text-red-400 rounded-lg"
121
+ ></div>
122
+
123
+ <!-- 按钮组 -->
124
+ <div class="flex justify-end space-x-3 mt-6">
125
+ <button
126
+ type="button"
127
+ @click="showEditModal = false"
128
+ class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none transition-colors duration-200"
129
+ >
130
+ 取消
131
+ </button>
132
+ <button
133
+ type="submit"
134
+ :disabled="isSubmitting"
135
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none transition-colors duration-200 space-x-2 disabled:opacity-50 disabled:cursor-not-allowed"
136
+ >
137
+ <span x-show="!isSubmitting" class="flex items-center">
138
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1.5" viewBox="0 0 20 20" fill="currentColor">
139
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
140
+ </svg>
141
+ 保存修改
142
+ </span>
143
+ <span x-show="isSubmitting" class="flex items-center">
144
+ <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
145
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
146
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
147
+ </svg>
148
+ 处理中...
149
+ </span>
150
+ </button>
151
+ </div>
152
+ </form>
153
+ </div>
154
+
155
+ <!-- 键盘快捷键提示 -->
156
+ <div class="absolute bottom-3 left-6 text-xs text-gray-400 dark:text-gray-500 flex items-center">
157
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1" viewBox="0 0 20 20" fill="currentColor">
158
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2h-1V9a1 1 0 00-1-1z" clip-rule="evenodd" />
159
+ </svg>
160
+ Esc 关闭 | Alt+Enter 提交
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
templates/components/scripts-bridge.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- API密钥管理器脚本 -->
2
+ <!-- 加载API密钥管理器模块 -->
3
+ <script>
4
+ // 定义平台数据全局变量,供模块使用
5
+ var platformsData = '{{ platforms|tojson }}';
6
+ // 定义平台样式数据全局变量,供模块使用
7
+ var platformStylesData = '{{ platform_styles|tojson }}';
8
+ </script>
9
+ <script src="/static/js/api-key-manager/core.js"></script>
10
+ <script src="/static/js/api-key-manager/key-operations.js"></script>
11
+ <script src="/static/js/api-key-manager/bulk-actions.js"></script>
12
+ <script src="/static/js/api-key-manager/platform-utils.js"></script>
13
+ <script src="/static/js/api-key-manager/ui-utils.js"></script>
14
+ <script src="/static/js/api-key-manager/modals.js"></script>
15
+ <script src="/static/js/api-key-manager/main-manager.js"></script>
templates/components/states.html ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- 加载中状态显示 -->
2
+ <div
3
+ x-cloak
4
+ x-show="isLoading"
5
+ class="bg-white rounded-lg shadow-md p-8 text-center"
6
+ >
7
+ <div class="flex flex-col items-center">
8
+ <svg class="animate-spin h-12 w-12 mb-4 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
9
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
10
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
11
+ </svg>
12
+ <h3 class="text-lg font-semibold text-gray-900 mb-2">数据加载中</h3>
13
+ <p class="text-gray-500 max-w-md mx-auto">
14
+ 请稍候,正在加载您的API密钥数据...
15
+ </p>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- 全局搜索无结果时显示 -->
20
+ <div
21
+ x-cloak
22
+ x-show="!isLoading && searchTerm !== '' && apiKeys.filter(key => platformFilters[key.platform] && matchesSearch(key.name, key.key)).length === 0"
23
+ class="bg-white rounded-lg shadow-md p-8 text-center"
24
+ >
25
+ <div class="flex flex-col items-center">
26
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
28
+ </svg>
29
+ <h3 class="text-lg font-semibold text-gray-900 mb-2">无搜索结果</h3>
30
+ <p class="text-gray-500 mb-6 max-w-md mx-auto">
31
+ 没有找到与 "<span x-text="searchTerm" class="font-medium"></span>" 匹配的API密钥
32
+ </p>
33
+ <button
34
+ @click="searchTerm = ''"
35
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors duration-200 space-x-2"
36
+ >
37
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
38
+ <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
39
+ </svg>
40
+ <span>清除搜索</span>
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+
46
+ <!-- 没有任何API密钥时显示(非搜索状态) -->
47
+ <div
48
+ x-cloak
49
+ x-show="!isLoading && totalKeyCount() === 0 && searchTerm === ''"
50
+ class="bg-white rounded-lg shadow-md p-8 text-center"
51
+ >
52
+ <div class="flex flex-col items-center">
53
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
54
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
55
+ </svg>
56
+ <h3 class="text-lg font-semibold text-gray-900 mb-2">暂无API密钥</h3>
57
+ <p class="text-gray-500 mb-6 max-w-md mx-auto">
58
+ 添加您的第一个API密钥以开始管理。您可以添加来自不同平台的多个密钥,方便快捷地使用。
59
+ </p>
60
+ <button
61
+ @click="showAddModal = true"
62
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors duration-200 space-x-2"
63
+ >
64
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
65
+ <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
66
+ </svg>
67
+ <span>添加第一个API密钥</span>
68
+ </button>
69
+ </div>
70
+ </div>
templates/components/toolbar.html ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- 顶部工具栏 -->
2
+ <div class="mb-6 flex flex-col gap-4">
3
+ <!-- 搜索和添加按钮 -->
4
+ <div class="flex justify-between items-center flex-wrap gap-4">
5
+ <div class="relative w-full md:w-auto flex-grow max-w-md">
6
+ <input
7
+ type="text"
8
+ placeholder="搜索API密钥..."
9
+ x-model="searchTerm"
10
+ class="pl-10 pr-4 py-2 w-full rounded-lg border border-gray-300 focus:ring-2 focus:ring-primary-500 focus:border-transparent"
11
+ >
12
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
13
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
15
+ </svg>
16
+ </div>
17
+ </div>
18
+ <button
19
+ @click="showAddModal = true"
20
+ @keydown.alt.n.stop="showAddModal = true"
21
+ class="inline-flex items-center px-3 py-1.5 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-colors duration-200"
22
+ title="添加API密钥 (Alt+N)"
23
+ >
24
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
25
+ <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
26
+ </svg>
27
+ <span>添加</span>
28
+ </button>
29
+ </div>
30
+
31
+ <!-- 服务商筛选标签 -->
32
+ <div class="flex flex-wrap gap-2 mt-2" x-cloak>
33
+ <!-- 全部标签 -->
34
+ <button
35
+ @click="toggleAllPlatformFilters()"
36
+ class="platform-filter-tag all-tag inline-flex items-center px-3.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-200 border"
37
+ :class="allPlatformsSelected ? 'selected bg-primary-600 text-white border-primary-600' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'"
38
+ >
39
+ <span>全部</span>
40
+ <span
41
+ class="tag-counter"
42
+ :class="allPlatformsSelected ? 'bg-primary-700 text-white' : 'bg-gray-200 text-gray-700'"
43
+ x-text="totalKeyCount()"
44
+ ></span>
45
+ </button>
46
+
47
+ <!-- 动态生成平台标签 -->
48
+ <template x-for="platform in getPlatforms()" :key="platform.id">
49
+ <button
50
+ @click="togglePlatformFilter(platform.id)"
51
+ class="platform-filter-tag inline-flex items-center px-3.5 py-1.5 rounded-full text-sm font-medium transition-colors duration-200 border shadow-sm"
52
+ :class="{
53
+ 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100': !platformFilters[platform.id],
54
+ 'selected': platformFilters[platform.id]
55
+ }"
56
+ :style="platformFilters[platform.id] ? getPlatformStyle(platform.id) : {}"
57
+ >
58
+ <span x-text="platform.name"></span>
59
+ <span
60
+ class="tag-counter"
61
+ :class="{
62
+ 'bg-gray-200 text-gray-700': !platformFilters[platform.id]
63
+ }"
64
+ :style="platformFilters[platform.id] ? {background: 'rgba(' + getPlatformStyle(platform.id).color.replace('#', '').match(/.{2}/g).map(hex => parseInt(hex, 16)).join(',') + ',0.1)'} : {}"
65
+ x-text="getPlatformKeyCount(platform.id)"
66
+ ></span>
67
+ </button>
68
+ </template>
69
+ </div>
70
+ </div>
templates/index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block head %}
4
+ {# 样式已移动到static/css/style.css #}
5
+ {% endblock %}
6
+
7
+ {% block content %}
8
+ <div x-data="apiKeyManager()" x-cloak>
9
+ {% include "components/toolbar.html" %}
10
+
11
+ <!-- 分组的API密钥列表 -->
12
+ {% include "components/api_key_list.html" %}
13
+
14
+ <!-- 加载状态与空状态 -->
15
+ {% include "components/states.html" %}
16
+
17
+ <!-- 模态框 -->
18
+ {% include "components/modals/add_key_modal.html" %}
19
+ {% include "components/modals/delete_confirm_modal.html" %}
20
+ {% include "components/modals/edit_key_modal.html" %}
21
+ </div>
22
+ {% endblock %}
23
+
24
+ {% block scripts %}
25
+ {% include "components/scripts-bridge.html" %}
26
+ {% endblock %}
templates/login.html ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>API密钥管理系统 - 登录</title>
7
+ <!-- 引入框架和库 -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ /* Tailwind配置 - 自定义颜色和动画 */
11
+ tailwind.config = {
12
+ theme: {
13
+ extend: {
14
+ colors: {
15
+ primary: {
16
+ 50: '#f0f9ff', 100: '#e0f2fe', 200: '#bae6fd',
17
+ 300: '#7dd3fc', 400: '#38bdf8', 500: '#0ea5e9',
18
+ 600: '#0284c7', 700: '#0369a1', 800: '#075985',
19
+ 900: '#0c4a6e', 950: '#082f49',
20
+ },
21
+ },
22
+ animation: {
23
+ 'fade-in': 'fadeIn 0.3s ease-in-out',
24
+ 'slide-down': 'slideDown 0.3s ease-in-out',
25
+ 'slide-up': 'slideUp 0.3s ease-in-out',
26
+ },
27
+ keyframes: {
28
+ fadeIn: {
29
+ '0%': { opacity: '0' },
30
+ '100%': { opacity: '1' },
31
+ },
32
+ slideDown: {
33
+ '0%': { maxHeight: '0', opacity: '0', transform: 'translateY(-10px)' },
34
+ '100%': { maxHeight: '1000px', opacity: '1', transform: 'translateY(0)' }
35
+ },
36
+ slideUp: {
37
+ '0%': { maxHeight: '1000px', opacity: '1', transform: 'translateY(0)' },
38
+ '100%': { maxHeight: '0', opacity: '0', transform: 'translateY(-10px)' }
39
+ },
40
+ },
41
+ },
42
+ },
43
+ }
44
+ </script>
45
+ <script defer src="https://unpkg.com/[email protected]/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') }}">
49
+ </head>
50
+ <body class="bg-gray-50 text-gray-900 min-h-screen">
51
+ <!-- 页面顶部导航栏 -->
52
+ <header class="bg-white shadow animate-fade-in">
53
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex justify-between items-center">
54
+ <h1 class="text-2xl font-bold text-primary-700">API密钥管理系统</h1>
55
+ </div>
56
+ </header>
57
+
58
+ <!-- 主内容区域 - 登录卡片 -->
59
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 mt-4">
60
+ <div class="flex justify-center items-center min-h-[70vh]">
61
+ <div class="max-w-md w-full animate-fade-in">
62
+ <div class="bg-white rounded-lg shadow-lg overflow-hidden border border-gray-100">
63
+ <!-- 卡片顶部彩色装饰条 -->
64
+ <div class="h-1.5 bg-gradient-to-r from-primary-400 via-primary-500 to-primary-600"></div>
65
+
66
+ <div class="px-8 py-8">
67
+ <!-- 卡片头部 - 图标和欢迎信息 -->
68
+ <div class="text-center mb-8">
69
+ <div class="inline-flex items-center justify-center h-16 w-16 rounded-full bg-primary-100 mb-4">
70
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
71
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
72
+ </svg>
73
+ </div>
74
+ <h2 class="text-xl font-semibold text-gray-900 mb-2">欢迎使用</h2>
75
+ <p class="text-gray-600 text-sm">请输入密码继续访问系统</p>
76
+ </div>
77
+
78
+ <!-- 登录表单 -->
79
+ <form method="POST" action="{{ url_for('web.login') }}" class="space-y-6">
80
+ <div>
81
+ <label for="password" class="block text-sm font-medium text-gray-700 mb-1">密码</label>
82
+ <div class="relative">
83
+ <!-- 密码输入框前的锁图标 -->
84
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
85
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
86
+ <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
87
+ </svg>
88
+ </div>
89
+ <input
90
+ type="password"
91
+ id="password"
92
+ name="password"
93
+ required
94
+ class="block w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-md shadow-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors"
95
+ placeholder="请输入访问密码"
96
+ autofocus
97
+ >
98
+ </div>
99
+ </div>
100
+
101
+ <!-- 错误信息显示区域 -->
102
+ {% if error %}
103
+ <div class="bg-red-50 border-l-4 border-red-400 p-4 rounded animate-fade-in">
104
+ <div class="flex">
105
+ <div class="flex-shrink-0">
106
+ <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
107
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
108
+ </svg>
109
+ </div>
110
+ <div class="ml-3">
111
+ <p class="text-sm text-red-700">{{ error }}</p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ {% endif %}
116
+
117
+ <!-- 登录按钮 -->
118
+ <button
119
+ type="submit"
120
+ class="w-full flex justify-center items-center py-2.5 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition-all duration-200"
121
+ >
122
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
123
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
124
+ </svg>
125
+ 登录
126
+ </button>
127
+ </form>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </main>
133
+
134
+ <script>
135
+ /* 页面加载完成后自动聚焦到密码输入框 */
136
+ document.addEventListener('DOMContentLoaded', () => {
137
+ document.getElementById('password').focus();
138
+ });
139
+ </script>
140
+ </body>
141
+ </html>
utils/__pycache__/auth.cpython-313.pyc ADDED
Binary file (3.94 kB). View file
 
utils/auth.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 身份验证模块 - 处理用户认证、令牌管理等功能
3
+ """
4
+ import json
5
+ import os
6
+ import secrets
7
+ from datetime import datetime, timedelta
8
+ from config import AUTH_FILE, TOKEN_EXPIRY_DAYS
9
+
10
+ class AuthManager:
11
+ """认证管理器 - 负责处理认证令牌的生成、存储和验证"""
12
+
13
+ @staticmethod
14
+ def load_tokens():
15
+ """加载认证令牌数据"""
16
+ if not os.path.exists(AUTH_FILE):
17
+ with open(AUTH_FILE, 'w', encoding='utf-8') as f:
18
+ json.dump({"tokens": {}}, f, ensure_ascii=False, indent=2)
19
+ return {"tokens": {}}
20
+
21
+ try:
22
+ with open(AUTH_FILE, 'r', encoding='utf-8') as f:
23
+ return json.load(f)
24
+ except json.JSONDecodeError:
25
+ return {"tokens": {}}
26
+
27
+ @staticmethod
28
+ def save_tokens(data):
29
+ """保存认证令牌数据"""
30
+ with open(AUTH_FILE, 'w', encoding='utf-8') as f:
31
+ json.dump(data, f, ensure_ascii=False, indent=2)
32
+
33
+ @staticmethod
34
+ def generate_token():
35
+ """生成安全随机令牌"""
36
+ return secrets.token_hex(32)
37
+
38
+ @staticmethod
39
+ def store_token(token):
40
+ """存储令牌并设置过期时间"""
41
+ auth_data = AuthManager.load_tokens()
42
+ expiry = (datetime.now() + timedelta(days=TOKEN_EXPIRY_DAYS)).isoformat()
43
+ auth_data["tokens"][token] = {"expiry": expiry}
44
+ AuthManager.save_tokens(auth_data)
45
+ return token
46
+
47
+ @staticmethod
48
+ def verify_token(token):
49
+ """验证令牌是否有效"""
50
+ if not token:
51
+ return False
52
+
53
+ auth_data = AuthManager.load_tokens()
54
+ token_data = auth_data["tokens"].get(token)
55
+
56
+ if not token_data:
57
+ return False
58
+
59
+ # 检查令牌是否过期
60
+ expiry = datetime.fromisoformat(token_data["expiry"])
61
+ if datetime.now() > expiry:
62
+ # 删除过期令牌
63
+ del auth_data["tokens"][token]
64
+ AuthManager.save_tokens(auth_data)
65
+ return False
66
+
67
+ return True
68
+
69
+ @staticmethod
70
+ def remove_token(token):
71
+ """从存储中删除令牌"""
72
+ if not token:
73
+ return False
74
+
75
+ auth_data = AuthManager.load_tokens()
76
+ if token in auth_data["tokens"]:
77
+ del auth_data["tokens"][token]
78
+ AuthManager.save_tokens(auth_data)
79
+ return True
80
+ return False