mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-10-15 12:15:32 +00:00
chore: UI 图标替换 Font Awesome + 登录安全限制 + 配置加载路径优化
This commit is contained in:
@ -23,18 +23,18 @@ if os.path.exists(prompt_tmpl_dir):
|
|||||||
datas.append((os.path.join(root, f), 'core/prompts/templates'))
|
datas.append((os.path.join(root, f), 'core/prompts/templates'))
|
||||||
|
|
||||||
# Web 静态文件
|
# Web 静态文件
|
||||||
static_dir = os.path.join(project_root, 'static')
|
static_dir = os.path.join(project_root, 'ui', 'static')
|
||||||
if os.path.exists(static_dir):
|
if os.path.exists(static_dir):
|
||||||
for root, dirs, files in os.walk(static_dir):
|
for root, dirs, files in os.walk(static_dir):
|
||||||
for f in files:
|
for f in files:
|
||||||
datas.append((os.path.join(root, f), 'static'))
|
datas.append((os.path.join(root, f), 'ui/static'))
|
||||||
|
|
||||||
# Web 模板
|
# Web 模板
|
||||||
templates_dir = os.path.join(project_root, 'templates')
|
templates_dir = os.path.join(project_root, 'ui', 'templates')
|
||||||
if os.path.exists(templates_dir):
|
if os.path.exists(templates_dir):
|
||||||
for root, dirs, files in os.walk(templates_dir):
|
for root, dirs, files in os.walk(templates_dir):
|
||||||
for f in files:
|
for f in files:
|
||||||
datas.append((os.path.join(root, f), 'templates'))
|
datas.append((os.path.join(root, f), 'ui/templates'))
|
||||||
|
|
||||||
# Web API 脚本(以便子进程模式回退使用)
|
# Web API 脚本(以便子进程模式回退使用)
|
||||||
web_api_src = os.path.join(project_root, 'web_api.py')
|
web_api_src = os.path.join(project_root, 'web_api.py')
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import argparse
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
|
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
@ -20,16 +21,85 @@ from core.activity_recorder import get_recorder
|
|||||||
from core.embedded_db import EmbeddedGraphDB
|
from core.embedded_db import EmbeddedGraphDB
|
||||||
|
|
||||||
|
|
||||||
|
# 登录安全限制
|
||||||
|
LOGIN_MAX_ATTEMPTS = 5 # 最大尝试次数
|
||||||
|
LOGIN_WAIT_MINUTES = 5 # 超过次数后等待分钟数
|
||||||
|
LOGIN_BAN_THRESHOLD = 3 # 超过此轮次后 ban IP
|
||||||
|
LOGIN_BAN_HOURS = 24 # IP ban 时长(小时)
|
||||||
|
|
||||||
|
# 内存记录:{ip: {"attempts": 0, "first_fail": 0, "ban_until": 0, "rounds": 0}}
|
||||||
|
_login_attempts: dict = {}
|
||||||
|
|
||||||
|
def _check_login_limit(ip: str) -> dict:
|
||||||
|
"""检查 IP 的登录限制。返回 {"blocked": bool, "reason": str, "wait_seconds": int}"""
|
||||||
|
now = time.time()
|
||||||
|
record = _login_attempts.get(ip)
|
||||||
|
|
||||||
|
if record:
|
||||||
|
# 检查是否在 ban 中
|
||||||
|
if record["ban_until"] > now:
|
||||||
|
remaining = int(record["ban_until"] - now)
|
||||||
|
return {"blocked": True, "reason": f"IP 已被临时封禁,剩余 {remaining//60} 分钟", "wait_seconds": remaining}
|
||||||
|
|
||||||
|
# 检查是否需要等待(连续失败超过阈值)
|
||||||
|
if record["attempts"] >= LOGIN_MAX_ATTEMPTS:
|
||||||
|
wait_end = record["first_fail"] + LOGIN_WAIT_MINUTES * 60
|
||||||
|
if wait_end > now:
|
||||||
|
remaining = int(wait_end - now)
|
||||||
|
return {"blocked": True, "reason": f"登录尝试过多,请等待 {remaining} 秒后再试", "wait_seconds": remaining}
|
||||||
|
else:
|
||||||
|
# 等待时间已过,重置计数但记录轮次
|
||||||
|
record["rounds"] += 1
|
||||||
|
record["attempts"] = 0
|
||||||
|
record["first_fail"] = 0
|
||||||
|
|
||||||
|
# 如果轮次超过阈值则 ban IP
|
||||||
|
if record["rounds"] >= LOGIN_BAN_THRESHOLD:
|
||||||
|
record["ban_until"] = now + LOGIN_BAN_HOURS * 3600
|
||||||
|
record["rounds"] = 0
|
||||||
|
return {"blocked": True, "reason": f"多次登录失败,IP 已被封禁 {LOGIN_BAN_HOURS} 小时", "wait_seconds": LOGIN_BAN_HOURS * 3600}
|
||||||
|
|
||||||
|
return {"blocked": False, "reason": "", "wait_seconds": 0}
|
||||||
|
|
||||||
|
def _record_login_fail(ip: str):
|
||||||
|
"""记录一次登录失败"""
|
||||||
|
now = time.time()
|
||||||
|
record = _login_attempts.get(ip)
|
||||||
|
if not record:
|
||||||
|
_login_attempts[ip] = {"attempts": 1, "first_fail": now, "ban_until": 0, "rounds": 0}
|
||||||
|
else:
|
||||||
|
if record["first_fail"] == 0:
|
||||||
|
record["first_fail"] = now
|
||||||
|
record["attempts"] += 1
|
||||||
|
|
||||||
|
def _record_login_success(ip: str):
|
||||||
|
"""登录成功后清除该 IP 的记录"""
|
||||||
|
_login_attempts.pop(ip, None)
|
||||||
|
|
||||||
|
# 定期清理过期记录(防止内存泄漏)
|
||||||
|
_cleanup_interval = 3600 # 1小时
|
||||||
|
_last_cleanup = time.time()
|
||||||
|
|
||||||
|
|
||||||
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
|
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
|
||||||
def load_secret_key():
|
def load_secret_key():
|
||||||
import json
|
import json
|
||||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json')
|
|
||||||
defaults = {"SECRET_KEY": "trulymem-secret-key-2026"}
|
defaults = {"SECRET_KEY": "trulymem-secret-key-2026"}
|
||||||
if os.path.exists(config_path):
|
search_paths = [
|
||||||
with open(config_path, 'r', encoding='utf-8') as f:
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json'),
|
||||||
file_config = json.load(f)
|
os.path.join(os.getcwd(), 'web_config.json'),
|
||||||
if "SECRET_KEY" in file_config:
|
os.path.join(os.path.expanduser("~"), ".trulymem", 'web_config.json'),
|
||||||
defaults["SECRET_KEY"] = file_config["SECRET_KEY"]
|
]
|
||||||
|
for config_path in search_paths:
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r', encoding='utf-8') as f:
|
||||||
|
file_config = json.load(f)
|
||||||
|
if "SECRET_KEY" in file_config:
|
||||||
|
defaults["SECRET_KEY"] = file_config["SECRET_KEY"]
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
return defaults
|
return defaults
|
||||||
|
|
||||||
WEB_CONFIG = load_secret_key()
|
WEB_CONFIG = load_secret_key()
|
||||||
@ -177,6 +247,13 @@ def api_login():
|
|||||||
username = data.get('username', '')
|
username = data.get('username', '')
|
||||||
password = data.get('password', '')
|
password = data.get('password', '')
|
||||||
|
|
||||||
|
client_ip = request.remote_addr or "unknown"
|
||||||
|
|
||||||
|
# 登录安全限制检查
|
||||||
|
limit_check = _check_login_limit(client_ip)
|
||||||
|
if limit_check["blocked"]:
|
||||||
|
return jsonify({"success": False, "error": limit_check["reason"]}), 429
|
||||||
|
|
||||||
# 从全局数据库验证
|
# 从全局数据库验证
|
||||||
g_db = get_global_db()
|
g_db = get_global_db()
|
||||||
if g_db and g_db.verify_web_user(username, password):
|
if g_db and g_db.verify_web_user(username, password):
|
||||||
@ -187,8 +264,10 @@ def api_login():
|
|||||||
# 重新加载服务器使用该用户的数据库
|
# 重新加载服务器使用该用户的数据库
|
||||||
reload_server_for_user(username)
|
reload_server_for_user(username)
|
||||||
|
|
||||||
|
_record_login_success(client_ip)
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
_record_login_fail(client_ip)
|
||||||
return jsonify({"success": False, "error": "用户名或密码错误"})
|
return jsonify({"success": False, "error": "用户名或密码错误"})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -438,9 +438,9 @@
|
|||||||
<input type="text" id="chatInput" placeholder="输入消息..." autocomplete="off">
|
<input type="text" id="chatInput" placeholder="输入消息..." autocomplete="off">
|
||||||
<button id="chatSendBtn">发送</button>
|
<button id="chatSendBtn">发送</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-status" id="chatStatus">🟢 已连接</div>
|
<div class="chat-status" id="chatStatus"><span class="status-dot" style="color:#22c55e">●</span> 已连接</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="toggle-chat">📌</button>
|
<button id="toggle-chat"><i class="fas fa-thumbtack"></i></button>
|
||||||
</div> <!-- /app-container -->
|
</div> <!-- /app-container -->
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||||
@ -1780,13 +1780,13 @@ function smoothReposition() {
|
|||||||
toggleChat.addEventListener('click', () => {
|
toggleChat.addEventListener('click', () => {
|
||||||
chatCollapsed = !chatCollapsed;
|
chatCollapsed = !chatCollapsed;
|
||||||
chatPanel.classList.toggle('collapsed', chatCollapsed);
|
chatPanel.classList.toggle('collapsed', chatCollapsed);
|
||||||
toggleChat.textContent = chatCollapsed ? '📎' : '📌';
|
toggleChat.innerHTML = chatCollapsed ? "<i class=\"fas fa-paperclip\"></i>" : "<i class=\"fas fa-thumbtack\"></i>";
|
||||||
setTimeout(onWindowResize, 300);
|
setTimeout(onWindowResize, 300);
|
||||||
});
|
});
|
||||||
chatCloseBtn.addEventListener('click', () => {
|
chatCloseBtn.addEventListener('click', () => {
|
||||||
chatCollapsed = true;
|
chatCollapsed = true;
|
||||||
chatPanel.classList.add('collapsed');
|
chatPanel.classList.add('collapsed');
|
||||||
toggleChat.textContent = '📎';
|
toggleChat.innerHTML = '<i class="fas fa-paperclip"></i>';
|
||||||
setTimeout(onWindowResize, 300);
|
setTimeout(onWindowResize, 300);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1856,16 +1856,16 @@ function smoothReposition() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
const reply = data.content || data.data?.content || '(无回复)';
|
const reply = data.content || data.data?.content || '(无回复)';
|
||||||
addChatMessage('assistant', reply);
|
addChatMessage('assistant', reply);
|
||||||
chatStatus.textContent = '🟢 已连接';
|
chatStatus.innerHTML = '<span class="status-dot" style="color:#22c55e">●</span> 已连接';
|
||||||
chatStatus.className = 'chat-status';
|
chatStatus.className = 'chat-status';
|
||||||
} else {
|
} else {
|
||||||
addChatMessage('assistant', '❌ ' + (data.error || '请求失败'));
|
addChatMessage('assistant', '❌ ' + (data.error || '请求失败'));
|
||||||
chatStatus.textContent = '🔴 错误: ' + (data.error || 'unknown');
|
chatStatus.innerHTML = '<span class="status-dot" style="color:#ef4444">●</span> 错误: ' + (data.error || 'unknown');
|
||||||
chatStatus.className = 'chat-status error';
|
chatStatus.className = 'chat-status error';
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
addChatMessage('assistant', '❌ 网络错误: ' + e.message);
|
addChatMessage('assistant', '❌ 网络错误: ' + e.message);
|
||||||
chatStatus.textContent = '🔴 网络错误';
|
chatStatus.innerHTML = '<span class="status-dot" style="color:#ef4444">●</span> 网络错误';
|
||||||
chatStatus.className = 'chat-status error';
|
chatStatus.className = 'chat-status error';
|
||||||
}
|
}
|
||||||
chatInput.disabled = false;
|
chatInput.disabled = false;
|
||||||
|
|||||||
@ -101,10 +101,15 @@
|
|||||||
}
|
}
|
||||||
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||||
.back-link {
|
.back-link {
|
||||||
display: block; text-align: center; margin-top: 20px; color: #6666aa;
|
display: block; text-align: center; margin-top: 20px; color: #4488ff;
|
||||||
text-decoration: none; font-size: 13px; transition: color 0.3s;
|
text-decoration: none; cursor: pointer; font-size: 13px; transition: color 0.3s;
|
||||||
}
|
}
|
||||||
.back-link:hover { color: #4488ff; }
|
.back-link:hover { color: #66aaff; }
|
||||||
|
.logout-link {
|
||||||
|
display: block; text-align: center; margin-top: 8px; color: #ff4488;
|
||||||
|
text-decoration: none; cursor: pointer; font-size: 13px; transition: color 0.3s;
|
||||||
|
}
|
||||||
|
.logout-link:hover { color: #ff66aa; }
|
||||||
.success-msg, .error-msg {
|
.success-msg, .error-msg {
|
||||||
display: none; margin-top: 12px; padding: 10px;
|
display: none; margin-top: 12px; padding: 10px;
|
||||||
border-radius: 6px; font-size: 13px; text-align: center;
|
border-radius: 6px; font-size: 13px; text-align: center;
|
||||||
@ -259,7 +264,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- TUI 服务控制 -->
|
<!-- TUI 服务控制 -->
|
||||||
<div class="section-title">🖥 TUI 终端服务</div>
|
<div class="section-title"><i class="fas fa-terminal"></i> TUI 终端服务</div>
|
||||||
<div class="toggle-row">
|
<div class="toggle-row">
|
||||||
<div>
|
<div>
|
||||||
<div class="toggle-label">启用终端 TUI 服务</div>
|
<div class="toggle-label">启用终端 TUI 服务</div>
|
||||||
@ -278,7 +283,7 @@
|
|||||||
|
|
||||||
<!-- 用户管理(仅管理员可见) -->
|
<!-- 用户管理(仅管理员可见) -->
|
||||||
<div id="adminSection" style="display:none;">
|
<div id="adminSection" style="display:none;">
|
||||||
<div class="section-title">👥 用户管理</div>
|
<div class="section-title"><i class="fas fa-users"></i> 用户管理</div>
|
||||||
<div class="user-section">
|
<div class="user-section">
|
||||||
<table class="user-table" id="userTable">
|
<table class="user-table" id="userTable">
|
||||||
<thead>
|
<thead>
|
||||||
@ -297,7 +302,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href="/graph.html" class="back-link">← 返回星图</a>
|
<a href="/graph.html" class="back-link"><i class="fas fa-arrow-left"></i> 返回星图</a>
|
||||||
|
<a href="#" class="logout-link" onclick="event.preventDefault(); fetch('/api/logout', {method: 'POST'}).then(() => window.location.href='/login');"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加用户弹窗 -->
|
<!-- 添加用户弹窗 -->
|
||||||
@ -356,11 +362,11 @@
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
document.getElementById('enableTuiToggle').checked = data.enable_tui !== false;
|
document.getElementById('enableTuiToggle').checked = data.enable_tui !== false;
|
||||||
document.getElementById('tuiStatus').textContent =
|
document.getElementById('tuiStatus').innerHTML =
|
||||||
data.enable_tui ? '✅ TUI 服务已启用' : '⏹️ TUI 服务已禁用';
|
data.enable_tui ? '<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> TUI 服务已启用' : '<i class="fas fa-stop-circle" style="color:var(--warning-color,#f59e0b)"></i> TUI 服务已禁用';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('tuiStatus').textContent = '⚠️ 无法加载配置';
|
document.getElementById('tuiStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 无法加载配置';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,7 +470,7 @@
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAddUserSuccess('✅ 用户添加成功');
|
showAddUserSuccess('<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> 用户添加成功');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.getElementById('addUserModal').classList.remove('show');
|
document.getElementById('addUserModal').classList.remove('show');
|
||||||
document.getElementById('addUserForm').reset();
|
document.getElementById('addUserForm').reset();
|
||||||
@ -483,7 +489,7 @@
|
|||||||
|
|
||||||
function showAddUserSuccess(msg) {
|
function showAddUserSuccess(msg) {
|
||||||
const el = document.getElementById('addUserSuccess');
|
const el = document.getElementById('addUserSuccess');
|
||||||
el.textContent = msg;
|
el.innerHTML = msg;
|
||||||
el.classList.add('show');
|
el.classList.add('show');
|
||||||
document.getElementById('addUserError').classList.remove('show');
|
document.getElementById('addUserError').classList.remove('show');
|
||||||
}
|
}
|
||||||
@ -521,10 +527,10 @@
|
|||||||
document.getElementById('settings_task_query').value = limits.task_query_max || 30;
|
document.getElementById('settings_task_query').value = limits.task_query_max || 30;
|
||||||
document.getElementById('settings_memory_query').value = limits.memory_query_max || 30;
|
document.getElementById('settings_memory_query').value = limits.memory_query_max || 30;
|
||||||
document.getElementById('settings_memory_update').value = limits.memory_update_max || 15;
|
document.getElementById('settings_memory_update').value = limits.memory_update_max || 15;
|
||||||
document.getElementById('apiConfigStatus').textContent = '✅ 配置已加载';
|
document.getElementById('apiConfigStatus').innerHTML = '<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> 配置已加载';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('apiConfigStatus').textContent = '⚠️ 无法加载配置';
|
document.getElementById('apiConfigStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 无法加载配置';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -535,7 +541,7 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '保存中...';
|
btn.textContent = '保存中...';
|
||||||
const statusEl = document.getElementById('apiConfigStatus');
|
const statusEl = document.getElementById('apiConfigStatus');
|
||||||
statusEl.textContent = '🔄 保存中...';
|
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin" style="color:var(--primary-color,#4488ff)"></i> 保存中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/settings', {
|
const resp = await fetch('/api/settings', {
|
||||||
@ -552,13 +558,13 @@
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
statusEl.textContent = '✅ API 配置已保存并生效';
|
statusEl.innerHTML = '<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> API 配置已保存并生效';
|
||||||
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = '⚠️ 保存失败: ' + (data.error || '未知错误');
|
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 保存失败: ' + (data.error || '未知错误');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = '⚠️ 网络错误';
|
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 网络错误';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '保 存 配 置';
|
btn.textContent = '保 存 配 置';
|
||||||
@ -572,7 +578,7 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '保存中...';
|
btn.textContent = '保存中...';
|
||||||
const statusEl = document.getElementById('limitsStatus');
|
const statusEl = document.getElementById('limitsStatus');
|
||||||
statusEl.textContent = '🔄 保存中...';
|
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin" style="color:var(--primary-color,#4488ff)"></i> 保存中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/settings', {
|
const resp = await fetch('/api/settings', {
|
||||||
@ -590,13 +596,13 @@
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
statusEl.textContent = '✅ 工具限制已保存并生效';
|
statusEl.innerHTML = '<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> 工具限制已保存并生效';
|
||||||
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
setTimeout(() => { statusEl.textContent = ''; }, 3000);
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = '⚠️ 保存失败: ' + (data.error || '未知错误');
|
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 保存失败: ' + (data.error || '未知错误');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
statusEl.textContent = '⚠️ 网络错误';
|
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 网络错误';
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '保 存 限 制';
|
btn.textContent = '保 存 限 制';
|
||||||
@ -613,7 +619,7 @@
|
|||||||
// TUI 开关
|
// TUI 开关
|
||||||
document.getElementById('enableTuiToggle').addEventListener('change', async function() {
|
document.getElementById('enableTuiToggle').addEventListener('change', async function() {
|
||||||
const enable = this.checked;
|
const enable = this.checked;
|
||||||
document.getElementById('tuiStatus').textContent = '🔄 更新中...';
|
document.getElementById('tuiStatus').innerHTML = '<i class="fas fa-spinner fa-spin" style="color:var(--primary-color,#4488ff)"></i> 更新中...';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/settings/config', {
|
const resp = await fetch('/api/settings/config', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@ -622,15 +628,15 @@
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
document.getElementById('tuiStatus').textContent =
|
document.getElementById('tuiStatus').innerHTML =
|
||||||
enable ? '✅ TUI 服务已启用' : '⏹️ TUI 服务已禁用';
|
enable ? '<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> TUI 服务已启用' : '<i class="fas fa-stop-circle" style="color:var(--warning-color,#f59e0b)"></i> TUI 服务已禁用';
|
||||||
showSuccess('设置已保存');
|
showSuccess('设置已保存');
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('tuiStatus').textContent = '⚠️ 更新失败';
|
document.getElementById('tuiStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 更新失败';
|
||||||
this.checked = !enable;
|
this.checked = !enable;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('tuiStatus').textContent = '⚠️ 网络错误';
|
document.getElementById('tuiStatus').innerHTML = '<i class="fas fa-exclamation-triangle" style="color:var(--warning-color,#f59e0b)"></i> 网络错误';
|
||||||
this.checked = !enable;
|
this.checked = !enable;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -665,7 +671,7 @@
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showSuccess('✅ 密码已更新');
|
showSuccess('<i class="fas fa-check-circle" style="color:var(--success-color,#22c55e)"></i> 密码已更新');
|
||||||
document.getElementById('current_password').value = '';
|
document.getElementById('current_password').value = '';
|
||||||
document.getElementById('new_password').value = '';
|
document.getElementById('new_password').value = '';
|
||||||
document.getElementById('confirm_password').value = '';
|
document.getElementById('confirm_password').value = '';
|
||||||
@ -682,7 +688,7 @@
|
|||||||
|
|
||||||
function showSuccess(msg) {
|
function showSuccess(msg) {
|
||||||
const el = document.getElementById('successMsg');
|
const el = document.getElementById('successMsg');
|
||||||
el.textContent = msg;
|
el.innerHTML = msg;
|
||||||
el.classList.add('show');
|
el.classList.add('show');
|
||||||
document.getElementById('errorMsg').classList.remove('show');
|
document.getElementById('errorMsg').classList.remove('show');
|
||||||
setTimeout(() => el.classList.remove('show'), 3000);
|
setTimeout(() => el.classList.remove('show'), 3000);
|
||||||
|
|||||||
Reference in New Issue
Block a user