feat: add outbound routing configuration modal and dedicated management button

This commit is contained in:
baoweise
2026-05-29 14:54:58 +08:00
parent 8ec467d668
commit 88d241f595
+143 -45
View File
@@ -2150,19 +2150,23 @@ INDEX_HTML = r"""<!doctype html>
更新节点
</button>
<button id="check" class="btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" style="width:16px; height:16px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 1121.21 8H18.5" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" style="width:16px; height:16px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
立即检测补齐
</button>
<button onclick="openRoutingModal()" class="btn-primary" style="background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);">
<svg xmlns="http://www.w3.org/2000/svg" style="width:16px; height:16px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg>
出站路由
</button>
<div class="dropdown">
<button id="admin_btn" class="btn-primary" style="background: rgba(255, 255, 255, 0.08); border: 1px solid var(--border-color); color: var(--text-primary);">
<svg xmlns="http://www.w3.org/2000/svg" style="width:16px; height:16px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
管理员
<svg xmlns="http://www.w3.org/2000/svg" style="width:12px; height:12px; margin-left: 2px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
<svg xmlns="http://www.w3.org/2000/svg; width:12px; height:12px; margin-left: 2px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
</button>
<div id="admin_dropdown" class="dropdown-content">
<a href="javascript:void(0)" onclick="openSettingsModal()">
<svg xmlns="http://www.w3.org/2000/svg" style="width:14px; height:14px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
设置
管理员设置
</a>
<a href="javascript:void(0)" onclick="logoutAdmin()" style="color: var(--danger); border-top: 1px solid rgba(255,255,255,0.05);">
<svg xmlns="http://www.w3.org/2000/svg" style="width:14px; height:14px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" 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" /></svg>
@@ -2354,10 +2358,47 @@ INDEX_HTML = r"""<!doctype html>
<input type="password" id="settings_new_password" class="input-field" placeholder="留空则不修改">
</div>
</div>
<div style="border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 16px; margin-bottom: 16px;">
<div style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); font-weight: 600; margin-bottom: 12px;">出站IP路由设置</div>
<div style="margin-bottom: 24px;">
<div style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); font-weight: 600; margin-bottom: 12px;">安全验证 (必须输入当前账号密码)</div>
<div class="form-group" style="margin-bottom: 12px;">
<label class="form-label" for="settings_curr_username">当前管理账号</label>
<input type="text" id="settings_curr_username" class="input-field" required placeholder="请输入当前管理账号">
</div>
<div class="form-group">
<label class="form-label" for="settings_curr_password">当前安全密码</label>
<input type="password" id="settings_curr_password" class="input-field" required placeholder="请输入当前安全密码">
</div>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button type="button" onclick="closeSettingsModal()" style="height: 40px; padding: 0 16px; font-weight: 600; border-radius: 8px; border: 1px solid var(--border-color); background: transparent; color: var(--text-secondary); cursor: pointer;">取消</button>
<button type="submit" id="settings_submit_btn" class="btn-primary" style="height: 40px; padding: 0 20px; font-weight: 600; border-radius: 8px;">保存修改</button>
</div>
</form>
</div>
</div>
<!-- Routing Modal -->
<div id="routing_modal" class="modal">
<div class="modal-content" style="max-width: 460px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h3 style="margin: 0; font-size: 18px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 8px;">
<svg xmlns="http://www.w3.org/2000/svg" style="width:20px; height:20px; color: #a855f7;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" /></svg>
出站 IP 路由设置
</h3>
<button type="button" onclick="closeRoutingModal()" style="background: transparent; border: none; padding: 4px; cursor: pointer; color: var(--text-secondary); width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 50%;" onmouseover="this.style.background='rgba(255,255,255,0.05)'" onmouseout="this.style.background='transparent'">
<svg xmlns="http://www.w3.org/2000/svg" style="width:18px; height:18px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
<div id="routing_error" style="color: var(--danger); font-size: 13px; margin-bottom: 16px; padding: 8px 12px; background: rgba(244,63,94,0.1); border: 1px solid rgba(244,63,94,0.2); border-radius: 6px; display: none;"></div>
<div id="routing_success" style="color: var(--success); font-size: 13px; margin-bottom: 16px; padding: 8px 12px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.2); border-radius: 6px; display: none;"></div>
<form id="routing_form" onsubmit="saveRoutingSettings(event)">
<div style="margin-bottom: 20px;">
<div class="form-group" style="margin-bottom: 12px;">
<label class="form-label" for="settings_routing_mode">IP 路由模式</label>
<select id="settings_routing_mode" class="input-field" style="background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border-color); color: var(--text-primary); outline: none; cursor: pointer; width: 100%; height: 40px; border-radius: 8px; padding: 0 12px;" onchange="handleRoutingModeChange(this.value)">
@@ -2379,23 +2420,9 @@ INDEX_HTML = r"""<!doctype html>
</div>
</div>
<div style="margin-bottom: 24px;">
<div style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); font-weight: 600; margin-bottom: 12px;">安全验证 (必须输入当前账号密码)</div>
<div class="form-group" style="margin-bottom: 12px;">
<label class="form-label" for="settings_curr_username">当前管理账号</label>
<input type="text" id="settings_curr_username" class="input-field" required placeholder="请输入当前管理账号">
</div>
<div class="form-group">
<label class="form-label" for="settings_curr_password">当前安全密码</label>
<input type="password" id="settings_curr_password" class="input-field" required placeholder="请输入当前安全密码">
</div>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button type="button" onclick="closeSettingsModal()" style="height: 40px; padding: 0 16px; font-weight: 600; border-radius: 8px; border: 1px solid var(--border-color); background: transparent; color: var(--text-secondary); cursor: pointer;">取消</button>
<button type="submit" id="settings_submit_btn" class="btn-primary" style="height: 40px; padding: 0 20px; font-weight: 600; border-radius: 8px;">保存修改</button>
<button type="button" onclick="closeRoutingModal()" style="height: 40px; padding: 0 16px; font-weight: 600; border-radius: 8px; border: 1px solid var(--border-color); background: transparent; color: var(--text-secondary); cursor: pointer;">取消</button>
<button type="submit" id="routing_submit_btn" class="btn-primary" style="height: 40px; padding: 0 20px; font-weight: 600; border-radius: 8px; background: linear-gradient(135deg, #a855f7 0%, #7c3aed 100%);">保存路由配置</button>
</div>
</form>
</div>
@@ -3039,7 +3066,7 @@ function handleRoutingModeChange(mode) {
}
}
function populateSettingsCountries() {
function populateRoutingCountries() {
const select = $("settings_force_country");
const countMap = {};
nodes.forEach(n => {
@@ -3063,6 +3090,72 @@ function populateSettingsCountries() {
}
}
function openRoutingModal() {
$("routing_error").style.display = "none";
$("routing_success").style.display = "none";
$("routing_form").reset();
populateRoutingCountries();
$("routing_modal").style.display = "flex";
$("admin_dropdown").style.display = "none";
}
function closeRoutingModal() {
$("routing_modal").style.display = "none";
}
async function saveRoutingSettings(e) {
e.preventDefault();
const errorDivEl = $("routing_error");
const successDiv = $("routing_success");
const submitBtn = $("routing_submit_btn");
errorDivEl.style.display = "none";
successDiv.style.display = "none";
const routingMode = $("settings_routing_mode").value;
const forceCountry = $("settings_force_country").value;
if (routingMode === "fixed_region" && !forceCountry) {
errorDivEl.textContent = "请选择一个要锁定的目标国家";
errorDivEl.style.display = "block";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "正在保存...";
try {
const res = await fetch("./api/update_routing", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
routing_mode: routingMode,
force_country: forceCountry
})
});
const data = await res.json();
if (res.ok && data.ok) {
successDiv.textContent = "路由配置保存成功,已即时生效!";
successDiv.style.display = "block";
setTimeout(() => {
closeRoutingModal();
load();
}, 1500);
} else {
errorDivEl.textContent = data.error || "保存失败,请检查输入";
errorDivEl.style.display = "block";
submitBtn.disabled = false;
submitBtn.textContent = "保存路由配置";
}
} catch (err) {
errorDivEl.textContent = "保存失败,请重试";
errorDivEl.style.display = "block";
submitBtn.disabled = false;
submitBtn.textContent = "保存路由配置";
}
}
function openSettingsModal() {
$("settings_error").style.display = "none";
$("settings_success").style.display = "none";
@@ -3073,8 +3166,6 @@ function openSettingsModal() {
$("settings_suffix").value = state.secret_path || "EJsW2EeBo9lY";
}
populateSettingsCountries();
$("settings_modal").style.display = "flex";
$("admin_dropdown").style.display = "none";
}
@@ -3098,8 +3189,6 @@ async function saveSettings(e) {
const newPassword = $("settings_new_password").value.trim();
const currUsername = $("settings_curr_username").value.trim();
const currPassword = $("settings_curr_password").value.trim();
const routingMode = $("settings_routing_mode").value;
const forceCountry = $("settings_force_country").value;
if (isNaN(port) || port < 1 || port > 65535) {
errorDivEl.textContent = "端口范围必须在 1 至 65535 之间";
@@ -3113,12 +3202,6 @@ async function saveSettings(e) {
return;
}
if (routingMode === "fixed_region" && !forceCountry) {
errorDivEl.textContent = "请选择一个要锁定的目标国家";
errorDivEl.style.display = "block";
return;
}
submitBtn.disabled = true;
submitBtn.textContent = "正在保存...";
@@ -3132,9 +3215,7 @@ async function saveSettings(e) {
new_username: newUsername,
new_password: newPassword,
curr_username: currUsername,
curr_password: currPassword,
routing_mode: routingMode,
force_country: forceCountry
curr_password: currPassword
})
});
@@ -3553,12 +3634,6 @@ class Handler(BaseHTTPRequestHandler):
new_suffix = str(payload.get("secret_path") or "").strip()
new_username = str(payload.get("new_username") or "").strip()
new_password = str(payload.get("new_password") or "").strip()
routing_mode = str(payload.get("routing_mode") or "auto").strip()
force_country = str(payload.get("force_country") or "").strip()
if routing_mode not in ("auto", "fixed_ip", "fixed_region"):
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
return
if not curr_username or not curr_password:
self.send_json({"ok": False, "error": "请输入当前账号和密码进行安全验证"}, HTTPStatus.FORBIDDEN)
@@ -3588,9 +3663,6 @@ class Handler(BaseHTTPRequestHandler):
ui_cfg["port"] = new_port_int
ui_cfg["secret_path"] = new_suffix
ui_cfg["routing_mode"] = routing_mode
ui_cfg["force_country"] = force_country
ui_cfg.pop("enable_force_country", None)
if new_username:
ui_cfg["username"] = new_username
if new_password:
@@ -3617,6 +3689,32 @@ class Handler(BaseHTTPRequestHandler):
self.send_json({"ok": False, "error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
return
elif effective_path == "/api/update_routing":
try:
length = parse_int(self.headers.get("Content-Length"))
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
routing_mode = str(payload.get("routing_mode") or "auto").strip()
force_country = str(payload.get("force_country") or "").strip()
if routing_mode not in ("auto", "fixed_ip", "fixed_region"):
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
return
ui_cfg = load_ui_config()
ui_cfg["routing_mode"] = routing_mode
ui_cfg["force_country"] = force_country
ui_cfg.pop("enable_force_country", None)
auth_file = DATA_DIR / "ui_auth.json"
with lock:
DATA_DIR.mkdir(exist_ok=True, parents=True)
auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8")
self.send_json({"ok": True, "message": "出站路由配置更新成功,已即时生效!"})
except Exception as exc:
self.send_json({"ok": False, "error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
return
if effective_path == "/api/check":
try:
self.send_json({"ok": True, "message": maintain_valid_nodes(force=True)})