Upload 37 files
Browse files- app.py +46 -0
- config.py +47 -0
- models/__pycache__/api_key.cpython-313.pyc +0 -0
- models/api_key.py +110 -0
- routes/__pycache__/api.cpython-313.pyc +0 -0
- routes/__pycache__/web.cpython-313.pyc +0 -0
- routes/api.py +66 -0
- routes/web.py +77 -0
- static/css/animations.css +92 -0
- static/css/base.css +19 -0
- static/css/components.css +143 -0
- static/css/responsive.css +21 -0
- static/css/scrollbars.css +55 -0
- static/css/style.css +25 -0
- static/css/tags.css +95 -0
- static/css/themes.css +33 -0
- static/img/favicon.ico +0 -0
- static/js/api-key-manager/bulk-actions.js +188 -0
- static/js/api-key-manager/core.js +79 -0
- static/js/api-key-manager/key-operations.js +501 -0
- static/js/api-key-manager/main-manager.js +171 -0
- static/js/api-key-manager/modals.js +82 -0
- static/js/api-key-manager/platform-utils.js +78 -0
- static/js/api-key-manager/ui-utils.js +78 -0
- static/js/main.js +253 -0
- templates/base.html +184 -0
- templates/components/api_key_list.html +310 -0
- templates/components/modals/add_key_modal.html +194 -0
- templates/components/modals/delete_confirm_modal.html +229 -0
- templates/components/modals/edit_key_modal.html +164 -0
- templates/components/scripts-bridge.html +15 -0
- templates/components/states.html +70 -0
- templates/components/toolbar.html +70 -0
- templates/index.html +26 -0
- templates/login.html +141 -0
- utils/__pycache__/auth.cpython-313.pyc +0 -0
- utils/auth.py +80 -0
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
|