feat: 添加Web配置编辑器界面

新增 `/config` 路由,提供一个可视化的配置编辑页面 (`config_editor.html`)。
用户现在可以通过网页界面管理:
- API 密钥(包括批量添加和重置确认)
- API 基础配置 (允许的令牌, 认证令牌, 基础URL, 最大失败次数, 超时)
- 模型相关配置 (测试模型, 图像/搜索/过滤模型列表, 代码执行/搜索链接/思考过程开关)
- 图像生成配置 (付费密钥, 模型, 上传提供商及相关密钥/URL)
- 流式输出优化器配置 (开关, 延迟, 阈值, 分块大小)

同时更新了 `/keys` 页面 (`keys_status.html`):
- 页面主标题更改为 "Gemini Balance"。
- 添加了顶部导航选项卡,方便在 "配置编辑" (`/config`) 和 "密钥管理" (`/keys`) 之间切换。
This commit is contained in:
snaily
2025-04-05 21:52:58 +08:00
parent ba38f14cd8
commit 619f81cce4
9 changed files with 1944 additions and 9 deletions
+792
View File
@@ -0,0 +1,792 @@
* {
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 900px;
width: 95%;
background: rgba(255, 255, 255, 0.95);
padding: 40px;
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
position: relative;
margin: 20px auto;
overflow-y: auto;
max-height: calc(100vh - 40px);
scrollbar-width: none;
-ms-overflow-style: none;
}
.container::-webkit-scrollbar {
display: none;
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
font-weight: 700;
font-size: 32px;
position: relative;
padding-bottom: 15px;
}
h1::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 1.5em;
padding-bottom: 10px;
border-bottom: 2px solid rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 10px;
}
/* 导航标签样式 */
.nav-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
border-bottom: 2px solid rgba(0,0,0,0.1);
padding-bottom: 10px;
width: 100%;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.tab-link {
color: #2c3e50;
text-decoration: none;
padding: 12px 25px;
border-radius: 8px 8px 0 0;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
position: relative;
margin: 0 5px;
flex: 1;
text-align: center;
}
.tab-link:hover {
background: rgba(102, 126, 234, 0.1);
}
.tab-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.tab-link.active::after {
content: '';
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
.tab-link i {
font-size: 16px;
}
.config-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 10px;
}
.tab-btn {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(0,0,0,0.1);
padding: 10px 20px;
border-radius: 30px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
color: #2c3e50;
font-size: 14px;
}
.tab-btn:hover {
background: rgba(255, 255, 255, 0.9);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.tab-btn.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.config-section {
display: none;
animation: fadeIn 0.5s ease forwards;
background: rgba(248, 249, 250, 0.9);
padding: 25px;
border-radius: 15px;
margin-bottom: 30px;
border: 1px solid rgba(0,0,0,0.1);
}
.config-section.active {
display: block;
}
.config-item {
margin-bottom: 25px;
position: relative;
}
.config-item label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #2c3e50;
}
.config-item input[type="text"],
.config-item input[type="number"],
.config-item select {
width: 100%;
padding: 12px 15px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
box-sizing: border-box;
}
.config-item input[type="text"]:focus,
.config-item input[type="number"]:focus,
.config-item select:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
outline: none;
}
.help-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #7f8c8d;
}
.array-container {
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
padding: 15px;
background: white;
/* margin-bottom: 10px; */ /* Removed as controls are now outside */
width: 100%;
min-height: 60px;
}
/* Specific style for API Keys container to make it scrollable */
#API_KEYS_container {
max-height: 300px; /* Adjust this value as needed */
overflow-y: auto;
/* Optional: Add some padding to the right for the scrollbar */
padding-right: 5px;
margin-bottom: 10px; /* Add margin below the container */
}
/* Search Input Styles */
.search-container {
margin-bottom: 10px;
}
#apiKeySearchInput {
width: 100%;
padding: 10px 15px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
font-size: 14px;
box-sizing: border-box;
}
#apiKeySearchInput:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
outline: none;
}
.array-item {
display: flex;
margin-bottom: 10px;
gap: 10px;
background-color: rgba(248, 249, 250, 0.5);
padding: 8px;
border-radius: 8px;
border: 1px dashed rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
}
.array-item:hover {
background-color: rgba(248, 249, 250, 0.8);
border-color: rgba(102, 126, 234, 0.5);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.array-item input {
flex: 1;
box-sizing: border-box;
padding: 12px 15px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
background: white;
}
.array-item input:focus {
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
outline: none;
}
.array-controls {
display: flex;
justify-content: flex-end;
margin-top: 10px; /* Increase margin-top for spacing */
}
.add-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 15px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
}
.add-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.remove-btn {
background: #e74c3c;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.remove-btn:hover {
background: #c0392b;
transform: scale(1.1);
}
.toggle {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
}
.toggle label {
margin-bottom: 0;
flex: 1;
padding-right: 15px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
input:checked + .toggle-slider {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
input:focus + .toggle-slider {
box-shadow: 0 0 1px #667eea;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.form-actions {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 40px;
}
.save-btn, .reset-btn {
padding: 12px 30px;
border-radius: 30px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border: none;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.save-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.save-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.save-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
}
.reset-btn {
background: linear-gradient(135deg, #e0e0e0 0%, #bdc3c7 100%);
color: #2c3e50;
}
.reset-btn:hover {
background: linear-gradient(135deg, #bdc3c7 0%, #e0e0e0 100%);
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.reset-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(0,0,0,0.05);
}
.save-status {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(39, 174, 96, 0.9);
color: white;
padding: 10px 20px;
border-radius: 30px;
display: flex;
align-items: center;
gap: 10px;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
transition: all 0.3s ease;
}
.save-status.show {
opacity: 1;
}
.save-status.error {
background: rgba(231, 76, 60, 0.9);
}
.provider-config {
display: none;
}
.provider-config.active {
display: block;
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 8px;
background: rgba(0,0,0,0.8);
color: white;
font-weight: bold;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1000;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateY(0);
}
.notification.success {
background: rgba(39, 174, 96, 0.9);
}
.notification.error {
background: rgba(231, 76, 60, 0.9);
}
.scroll-buttons {
position: fixed;
right: 20px;
bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.scroll-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
.scroll-btn:hover {
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
transform: scale(1.1);
}
.scroll-btn:active {
transform: scale(0.95);
}
.refresh-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.refresh-btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 20px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.refresh-btn:active {
transform: scale(0.95);
}
.refresh-btn i {
transition: transform 0.5s ease;
}
.refresh-btn.loading i {
animation: spin 1s linear infinite;
}
.copyright {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.9);
padding: 10px 0;
text-align: center;
font-size: 14px;
color: #2c3e50;
backdrop-filter: blur(5px);
border-top: 1px solid rgba(0,0,0,0.1);
}
.copyright a {
color: #764ba2;
text-decoration: none;
transition: color 0.3s ease;
}
.copyright a:hover {
color: #667eea;
}
.copyright img {
width: 20px;
height: 20px;
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}
/* Modal Styles */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1001; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
backdrop-filter: blur(5px);
}
.modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: #fefefe;
padding: 30px;
border: 1px solid #888;
width: 80%; /* Could be more or less, depending on screen size */
max-width: 600px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
position: relative;
animation: slideIn 0.3s ease-out;
margin: auto;
}
.modal-content h2 {
margin-top: 0;
color: #2c3e50;
border-bottom: none; /* Remove border from h2 inside modal */
padding-bottom: 0;
}
.modal-content p {
color: #7f8c8d;
font-size: 14px;
margin-bottom: 15px;
}
.modal-content textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 8px;
margin-bottom: 20px;
font-family: monospace; /* Use monospace for keys */
font-size: 14px;
resize: vertical; /* Allow vertical resizing */
min-height: 150px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 10px;
}
.close-btn {
color: #aaa;
position: absolute;
top: 10px;
right: 20px;
font-size: 28px;
font-weight: bold;
line-height: 1;
}
.close-btn:hover,
.close-btn:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.container {
width: 100%;
padding: 20px;
margin: 10px auto;
}
body {
padding: 10px;
}
h1 {
font-size: 24px;
}
.nav-tabs {
flex-direction: column;
align-items: center;
gap: 10px;
}
.tab-link {
width: 100%;
justify-content: center;
border-radius: 8px;
}
.tab-link.active::after {
display: none;
}
.config-tabs {
flex-direction: column;
}
.tab-btn {
width: 100%;
text-align: center;
}
.toggle {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.form-actions {
flex-direction: column;
gap: 15px;
}
.save-btn, .reset-btn {
width: 100%;
justify-content: center;
}
.scroll-buttons {
right: 10px;
bottom: 10px;
}
.scroll-btn {
width: 35px;
height: 35px;
font-size: 16px;
}
.refresh-btn {
top: 10px;
right: 10px;
padding: 8px 16px;
font-size: 12px;
}
}
@media (max-width: 480px) {
.container {
padding: 15px;
}
h1 {
font-size: 20px;
}
.config-section {
padding: 15px;
}
.config-item input[type="text"],
.config-item input[type="number"],
.config-item select {
padding: 10px;
font-size: 14px;
}
}
+87 -6
View File
@@ -30,7 +30,7 @@ body {
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
margin-bottom: 15px;
font-weight: 700;
font-size: 32px;
position: relative;
@@ -49,6 +49,61 @@ h1::after {
border-radius: 2px;
}
/* 导航标签样式 */
.nav-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
border-bottom: 2px solid rgba(0,0,0,0.1);
padding-bottom: 10px;
width: 100%;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
.tab-link {
color: #2c3e50;
text-decoration: none;
padding: 12px 25px;
border-radius: 8px 8px 0 0;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
position: relative;
margin: 0 5px;
flex: 1;
text-align: center;
}
.tab-link:hover {
background: rgba(102, 126, 234, 0.1);
}
.tab-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
}
.tab-link.active::after {
content: '';
position: absolute;
bottom: -12px;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 2px;
}
.tab-link i {
font-size: 16px;
}
.key-list {
margin-bottom: 30px;
background: rgba(248, 249, 250, 0.9);
@@ -161,27 +216,34 @@ li:hover {
}
.verify-btn, .copy-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 15px;
border-radius: 8px;
border-radius: 30px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.verify-btn {
background: linear-gradient(135deg, #2ecc71, #27ae60);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.verify-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(46, 204, 113, 0.3);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.verify-btn:active {
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
}
.verify-btn:disabled {
@@ -195,13 +257,19 @@ li:hover {
font-size: 14px;
}
.copy-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.copy-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
}
.copy-btn:active {
transform: translateY(0);
transform: translateY(1px);
box-shadow: 0 5px 10px rgba(118, 75, 162, 0.2);
}
.copy-btn i {
@@ -393,6 +461,19 @@ li:hover {
h1 {
font-size: 24px;
}
.nav-tabs {
flex-direction: column;
align-items: center;
gap: 10px;
}
.tab-link {
width: 100%;
justify-content: center;
border-radius: 8px;
}
.tab-link.active::after {
display: none;
}
.key-list h2 {
font-size: 1.2em;
flex-direction: column;
+555
View File
@@ -0,0 +1,555 @@
document.addEventListener('DOMContentLoaded', function() {
// 初始化配置
initConfig();
// 标签切换
const tabButtons = document.querySelectorAll('.tab-btn');
tabButtons.forEach(button => {
button.addEventListener('click', function(e) {
// 防止事件冒泡
e.stopPropagation();
const tabId = this.getAttribute('data-tab');
switchTab(tabId);
});
});
// 上传提供商切换
const uploadProviderSelect = document.getElementById('UPLOAD_PROVIDER');
if (uploadProviderSelect) {
uploadProviderSelect.addEventListener('change', function() {
toggleProviderConfig(this.value);
});
}
// 切换按钮事件
const toggleSwitches = document.querySelectorAll('.toggle-switch');
toggleSwitches.forEach(toggleSwitch => {
toggleSwitch.addEventListener('click', function(e) {
// 防止事件冒泡
e.stopPropagation();
const checkbox = this.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = !checkbox.checked;
}
});
});
// 保存按钮
const saveBtn = document.getElementById('saveBtn');
if (saveBtn) {
saveBtn.addEventListener('click', saveConfig);
}
// 重置按钮
const resetBtn = document.getElementById('resetBtn');
if (resetBtn) {
resetBtn.addEventListener('click', resetConfig);
}
// 滚动按钮
window.addEventListener('scroll', toggleScrollButtons);
// --- 新增:API Key 模态框和搜索相关 ---
const apiKeyModal = document.getElementById('apiKeyModal');
const addApiKeyBtn = document.getElementById('addApiKeyBtn');
const closeApiKeyModalBtn = document.getElementById('closeApiKeyModalBtn');
const cancelAddApiKeyBtn = document.getElementById('cancelAddApiKeyBtn');
const confirmAddApiKeyBtn = document.getElementById('confirmAddApiKeyBtn');
const apiKeyBulkInput = document.getElementById('apiKeyBulkInput');
const apiKeySearchInput = document.getElementById('apiKeySearchInput');
// --- 新增:重置确认模态框相关 ---
const resetConfirmModal = document.getElementById('resetConfirmModal');
const closeResetModalBtn = document.getElementById('closeResetModalBtn');
const cancelResetBtn = document.getElementById('cancelResetBtn');
const confirmResetBtn = document.getElementById('confirmResetBtn');
// --- 结束:新增 ---
// 打开模态框
if (addApiKeyBtn) {
addApiKeyBtn.addEventListener('click', () => {
if (apiKeyModal) {
apiKeyModal.classList.add('show');
}
if (apiKeyBulkInput) apiKeyBulkInput.value = ''; // 清空输入框
});
}
// 关闭模态框 (X 按钮)
if (closeApiKeyModalBtn) {
closeApiKeyModalBtn.addEventListener('click', () => {
if (apiKeyModal) {
apiKeyModal.classList.remove('show');
}
});
}
// 关闭模态框 (取消按钮)
if (cancelAddApiKeyBtn) {
cancelAddApiKeyBtn.addEventListener('click', () => {
if (apiKeyModal) {
apiKeyModal.classList.remove('show');
}
});
}
// 点击模态框外部关闭 (处理两个模态框)
window.addEventListener('click', (event) => {
if (event.target == apiKeyModal) {
apiKeyModal.classList.remove('show');
}
if (event.target == resetConfirmModal) { // 新增对重置模态框的处理
resetConfirmModal.classList.remove('show');
}
});
// 确认添加 API Key
if (confirmAddApiKeyBtn) {
confirmAddApiKeyBtn.addEventListener('click', handleBulkAddApiKeys);
}
// API Key 搜索 (稍后实现具体逻辑)
if (apiKeySearchInput) {
apiKeySearchInput.addEventListener('input', handleApiKeySearch);
}
// --- 结束:API Key 相关 ---
// --- 新增:重置确认模态框事件监听 (移到 DOMContentLoaded 内部) ---
if (closeResetModalBtn) {
closeResetModalBtn.addEventListener('click', () => {
if (resetConfirmModal) {
resetConfirmModal.classList.remove('show');
}
});
}
if (cancelResetBtn) {
cancelResetBtn.addEventListener('click', () => {
if (resetConfirmModal) {
resetConfirmModal.classList.remove('show');
}
});
}
if (confirmResetBtn) {
// 调用之前定义的 executeReset 函数
confirmResetBtn.addEventListener('click', () => {
if (resetConfirmModal) {
resetConfirmModal.classList.remove('show'); // 关闭模态框
}
executeReset(); // 执行重置逻辑
});
}
// --- 结束:重置相关 ---
}); // <-- DOMContentLoaded 结束括号
// 初始化配置
async function initConfig() {
try {
showNotification('正在加载配置...', 'info');
const response = await fetch('/api/config');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const config = await response.json();
// 确保数组字段有默认值
if (!config.API_KEYS || !Array.isArray(config.API_KEYS) || config.API_KEYS.length === 0) {
config.API_KEYS = ['请在此处输入 API 密钥'];
}
if (!config.ALLOWED_TOKENS || !Array.isArray(config.ALLOWED_TOKENS) || config.ALLOWED_TOKENS.length === 0) {
config.ALLOWED_TOKENS = [''];
}
if (!config.IMAGE_MODELS || !Array.isArray(config.IMAGE_MODELS) || config.IMAGE_MODELS.length === 0) {
config.IMAGE_MODELS = ['gemini-1.5-pro-latest'];
}
if (!config.SEARCH_MODELS || !Array.isArray(config.SEARCH_MODELS) || config.SEARCH_MODELS.length === 0) {
config.SEARCH_MODELS = ['gemini-1.5-flash-latest'];
}
if (!config.FILTERED_MODELS || !Array.isArray(config.FILTERED_MODELS) || config.FILTERED_MODELS.length === 0) {
config.FILTERED_MODELS = ['gemini-1.0-pro-latest'];
}
populateForm(config);
// 确保上传提供商有默认值
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
if (uploadProvider && !uploadProvider.value) {
uploadProvider.value = 'smms'; // 设置默认值为 smms
toggleProviderConfig('smms');
}
showNotification('配置加载成功', 'success');
} catch (error) {
console.error('加载配置失败:', error);
showNotification('加载配置失败: ' + error.message, 'error');
// 加载失败时,使用默认配置
const defaultConfig = {
API_KEYS: [''],
ALLOWED_TOKENS: [''],
IMAGE_MODELS: ['gemini-1.5-pro-latest'],
SEARCH_MODELS: ['gemini-1.5-flash-latest'],
FILTERED_MODELS: ['gemini-1.0-pro-latest'],
UPLOAD_PROVIDER: 'smms'
};
populateForm(defaultConfig);
toggleProviderConfig('smms');
}
}
// 填充表单
function populateForm(config) {
for (const [key, value] of Object.entries(config)) {
// 首先检查是否是数组类型
if (Array.isArray(value)) {
const container = document.getElementById(`${key}_container`);
if (container) {
// 清除现有项
const existingItems = container.querySelectorAll('.array-item');
existingItems.forEach(item => item.remove());
// 添加数组项
value.forEach(item => {
// 确保只添加非空字符串项(如果需要)
// if (item && typeof item === 'string' && item.trim() !== '') {
addArrayItemWithValue(key, item);
// }
});
}
// 处理完数组后,跳过本次循环的剩余部分
continue;
}
// 如果不是数组,再尝试查找对应的单个元素
const element = document.getElementById(key);
if (element) {
if (typeof value === 'boolean') {
element.checked = value;
} else {
// 处理其他类型 (确保 value 不是 null 或 undefined)
element.value = value ?? ''; // 使用空字符串作为默认值
}
}
// 如果既不是数组,也找不到对应 ID 的元素,则忽略该配置项
}
// 初始化上传提供商配置 (保持不变)
const uploadProvider = document.getElementById('UPLOAD_PROVIDER');
if (uploadProvider) {
toggleProviderConfig(uploadProvider.value);
}
}
// --- 新增:处理批量添加 API Key 的逻辑 ---
function handleBulkAddApiKeys() {
const apiKeyBulkInput = document.getElementById('apiKeyBulkInput');
const apiKeyContainer = document.getElementById('API_KEYS_container');
const apiKeyModal = document.getElementById('apiKeyModal');
if (!apiKeyBulkInput || !apiKeyContainer || !apiKeyModal) return;
const bulkText = apiKeyBulkInput.value;
const keyRegex = /AIzaSy\S{33}/g; // 全局匹配
const extractedKeys = bulkText.match(keyRegex) || [];
// 获取当前已有的 keys
const currentKeyInputs = apiKeyContainer.querySelectorAll('.array-input');
const currentKeys = Array.from(currentKeyInputs).map(input => input.value).filter(key => key.trim() !== '');
// 合并并去重
const combinedKeys = new Set([...currentKeys, ...extractedKeys]);
const uniqueKeys = Array.from(combinedKeys);
// 清空现有列表显示
const existingItems = apiKeyContainer.querySelectorAll('.array-item');
existingItems.forEach(item => item.remove());
// 重新填充列表
uniqueKeys.forEach(key => {
addArrayItemWithValue('API_KEYS', key);
});
// 关闭模态框
apiKeyModal.classList.remove('show');
showNotification(`添加/更新了 ${uniqueKeys.length} 个唯一密钥`, 'success');
}
// --- 新增:处理 API Key 搜索的逻辑 ---
function handleApiKeySearch() {
const apiKeySearchInput = document.getElementById('apiKeySearchInput');
const apiKeyContainer = document.getElementById('API_KEYS_container');
if (!apiKeySearchInput || !apiKeyContainer) return;
const searchTerm = apiKeySearchInput.value.toLowerCase();
const keyItems = apiKeyContainer.querySelectorAll('.array-item');
keyItems.forEach(item => {
const input = item.querySelector('.array-input');
if (input) {
const key = input.value.toLowerCase();
if (key.includes(searchTerm)) {
item.style.display = 'flex'; // 或者 'block',取决于你的布局
} else {
item.style.display = 'none';
}
}
});
}
// 切换标签
function switchTab(tabId) {
// 更新标签按钮状态
const tabButtons = document.querySelectorAll('.tab-btn');
tabButtons.forEach(button => {
if (button.getAttribute('data-tab') === tabId) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
// 更新内容区域
const sections = document.querySelectorAll('.config-section');
sections.forEach(section => {
if (section.id === `${tabId}-section`) {
section.classList.add('active');
} else {
section.classList.remove('active');
}
});
}
// 切换上传提供商配置
function toggleProviderConfig(provider) {
const providerConfigs = document.querySelectorAll('.provider-config');
providerConfigs.forEach(config => {
if (config.getAttribute('data-provider') === provider) {
config.classList.add('active');
} else {
config.classList.remove('active');
}
});
}
// 添加数组项
function addArrayItem(key) {
const container = document.getElementById(`${key}_container`);
if (!container) return;
addArrayItemWithValue(key, '');
}
// 添加带值的数组项
function addArrayItemWithValue(key, value) {
const container = document.getElementById(`${key}_container`);
if (!container) return;
const arrayItem = document.createElement('div');
arrayItem.className = 'array-item';
const input = document.createElement('input');
input.type = 'text';
input.name = `${key}[]`;
input.value = value;
input.className = 'array-input';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '<i class="fas fa-times"></i>';
removeBtn.addEventListener('click', function() {
arrayItem.remove();
});
arrayItem.appendChild(input);
arrayItem.appendChild(removeBtn);
// 插入到添加按钮之前
const controls = container.querySelector('.array-controls');
container.insertBefore(arrayItem, controls);
}
// 收集表单数据
function collectFormData() {
const formData = {};
// 处理普通输入
const inputs = document.querySelectorAll('input[type="text"], input[type="number"], select');
inputs.forEach(input => {
if (!input.name.includes('[]')) {
if (input.type === 'number') {
formData[input.name] = parseFloat(input.value);
} else {
formData[input.name] = input.value;
}
}
});
// 处理复选框
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(checkbox => {
formData[checkbox.name] = checkbox.checked;
});
// 处理数组
const arrayContainers = document.querySelectorAll('.array-container');
arrayContainers.forEach(container => {
const key = container.id.replace('_container', '');
const arrayInputs = container.querySelectorAll('.array-input');
formData[key] = Array.from(arrayInputs).map(input => input.value).filter(value => value.trim() !== '');
});
return formData;
}
// 保存配置
async function saveConfig() {
try {
const formData = collectFormData();
showNotification('正在保存配置...', 'info');
const response = await fetch('/api/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
// 显示保存状态
const saveStatus = document.getElementById('saveStatus');
saveStatus.classList.add('show');
setTimeout(() => {
saveStatus.classList.remove('show');
}, 3000);
showNotification('配置保存成功', 'success');
} catch (error) {
console.error('保存配置失败:', error);
// 显示错误状态
const saveStatus = document.getElementById('saveStatus');
saveStatus.classList.add('show', 'error');
saveStatus.querySelector('.status-icon i').className = 'fas fa-times-circle';
saveStatus.querySelector('.status-text').textContent = '配置保存失败';
setTimeout(() => {
saveStatus.classList.remove('show', 'error');
}, 3000);
showNotification('保存配置失败: ' + error.message, 'error');
}
}
// 重置配置 (现在只负责打开模态框)
function resetConfig(event) {
// 阻止事件冒泡和默认行为
if (event) {
event.preventDefault();
event.stopPropagation();
}
console.log('resetConfig called. Event target:', event ? event.target.id : 'No event');
// 确保只有当事件来自重置按钮时才显示模态框
if (!event || event.target.id === 'resetBtn' || event.currentTarget.id === 'resetBtn') {
const resetConfirmModal = document.getElementById('resetConfirmModal');
if (resetConfirmModal) {
resetConfirmModal.classList.add('show');
} else {
// Fallback if modal doesn't exist for some reason
console.error("Reset confirmation modal not found! Falling back to default confirm.");
// Fallback to original confirm behavior
if (!confirm('确定要重置所有配置吗?这将恢复到默认值。')) {
return;
}
// If confirmed, proceed with the reset logic directly (less ideal)
executeReset();
}
}
}
// --- 新增:将实际重置逻辑提取到一个单独的函数 ---
async function executeReset() {
try {
showNotification('正在重置配置...', 'info');
const response = await fetch('/api/config/reset', { method: 'POST' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const config = await response.json();
populateForm(config);
showNotification('配置已重置为默认值', 'success');
} catch (error) {
console.error('重置配置失败:', error);
showNotification('重置配置失败: ' + error.message, 'error');
}
}
// 显示通知
function showNotification(message, type = 'info') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = 'notification show';
if (type) {
notification.classList.add(type);
}
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// 刷新页面
function refreshPage(button) {
button.classList.add('loading');
location.reload();
}
// 滚动到顶部
function scrollToTop() {
const container = document.querySelector('.container');
container.scrollTo({
top: 0,
behavior: 'smooth'
});
}
// 滚动到底部
function scrollToBottom() {
const container = document.querySelector('.container');
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
}
// 切换滚动按钮显示
function toggleScrollButtons() {
const scrollButtons = document.querySelector('.scroll-buttons');
if (window.scrollY > 200) {
scrollButtons.style.display = 'flex';
} else {
scrollButtons.style.display = 'none';
}
}