diff --git a/vpngate_manager.py b/vpngate_manager.py
index 61eba5e..a5a88e6 100644
--- a/vpngate_manager.py
+++ b/vpngate_manager.py
@@ -2150,19 +2150,23 @@ INDEX_HTML = r"""
更新节点
+
+
+
+
+
+ 出站 IP 路由设置
+
+
+
+
+
+
+
+
@@ -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)})