@@ -3160,7 +3214,7 @@ function getFilteredNodes() {
if (selectedStatus === "unavailable" && (n.probe_status !== "unavailable" || n.active)) {
return false;
}
- if (selectedStatus === "favorites" && (!state.favorite_node_ids || !state.favorite_node_ids.includes(n.id))) {
+ if (showFavoritesOnly && (!state.favorite_node_ids || !state.favorite_node_ids.includes(n.id))) {
return false;
}
const searchStr = [
@@ -3322,9 +3376,7 @@ function render(){
}
}
- const isFavoritesFilter = $("status_filter").value === "favorites";
- const banner = $("favorites_banner");
- if (banner) banner.style.display = isFavoritesFilter ? "block" : "none";
+ updateFavPanelUI();
// Pagination calculation
const totalPages = Math.ceil(shown.length / pageSize) || 1;
@@ -3642,6 +3694,161 @@ document.addEventListener("click", () => {
if (githubDropdown) githubDropdown.style.display = "none";
});
+let showFavoritesOnly = false;
+
+function toggleFavoritesView() {
+ showFavoritesOnly = !showFavoritesOnly;
+ currentPage = 1;
+ render();
+}
+
+function updateFavPanelUI() {
+ const panel = $("favorites_panel");
+ if (!panel) return;
+ panel.style.display = showFavoritesOnly ? "block" : "none";
+
+ const btn = $("btn_favorites");
+ if (btn) {
+ if (showFavoritesOnly) {
+ btn.classList.add("active");
+ } else {
+ btn.classList.remove("active");
+ }
+ }
+
+ if (showFavoritesOnly && state) {
+ const fallbackCheckbox = $("fav_fail_fallback_checkbox");
+ if (fallbackCheckbox) {
+ fallbackCheckbox.checked = !!state.fav_fail_fallback;
+ }
+
+ const warningDiv = $("fav_fallback_warning");
+ if (warningDiv) {
+ warningDiv.style.display = state.fav_fail_fallback ? "none" : "block";
+ }
+
+ const favRoutingBtn = $("btn_toggle_fav_routing");
+ if (favRoutingBtn) {
+ if (state.routing_mode === "favorites") {
+ favRoutingBtn.textContent = "禁用仅用收藏出站";
+ favRoutingBtn.style.background = "var(--danger-gradient)";
+ favRoutingBtn.style.borderColor = "transparent";
+ favRoutingBtn.style.color = "#ffffff";
+ favRoutingBtn.style.boxShadow = "0 0 12px rgba(244, 63, 94, 0.3)";
+ } else {
+ favRoutingBtn.textContent = "启用仅用收藏出站";
+ favRoutingBtn.style.background = "rgba(255,255,255,0.03)";
+ favRoutingBtn.style.borderColor = "var(--border-color)";
+ favRoutingBtn.style.color = "var(--text-primary)";
+ favRoutingBtn.style.boxShadow = "none";
+ }
+ }
+ }
+}
+
+async function toggleFavRouting() {
+ if (!state) return;
+ const newMode = state.routing_mode === "favorites" ? "auto" : "favorites";
+
+ state.routing_mode = newMode;
+ updateFavPanelUI();
+
+ try {
+ const res = await fetch("./api/update_routing", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ routing_mode: newMode,
+ force_country: state.force_country || "",
+ routing_ip_type: state.routing_ip_type || "all",
+ fav_fail_fallback: state.fav_fail_fallback !== false
+ })
+ });
+ const data = await res.json();
+ if (res.ok && data.ok) {
+ load();
+ } else {
+ alert("更新出站路由设置失败: " + (data.error || "未知错误"));
+ load();
+ }
+ } catch (err) {
+ alert("连接服务器失败,请稍后重试");
+ load();
+ }
+}
+
+async function handleFavFallbackChange(checked) {
+ if (!state) return;
+
+ if (!checked) {
+ alert("⚠️ 警告:不勾选此项可能在所有收藏节点失效时造成网络彻底断开连接,无法自动切换到其他非收藏的可用节点!");
+ }
+
+ state.fav_fail_fallback = checked;
+ updateFavPanelUI();
+
+ try {
+ const res = await fetch("./api/update_routing", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ routing_mode: state.routing_mode || "auto",
+ force_country: state.force_country || "",
+ routing_ip_type: state.routing_ip_type || "all",
+ fav_fail_fallback: checked
+ })
+ });
+ const data = await res.json();
+ if (res.ok && data.ok) {
+ load();
+ } else {
+ alert("更新失败: " + (data.error || "未知错误"));
+ load();
+ }
+ } catch (err) {
+ alert("连接服务器失败,请稍后重试");
+ load();
+ }
+}
+
+function selectOptionCard(groupName, value) {
+ if (groupName === 'routing_mode') {
+ const input = $("net_routing_mode");
+ if (input) input.value = value;
+
+ const cards = document.querySelectorAll("#routing_mode_group .option-card");
+ cards.forEach(card => {
+ if (card.getAttribute("data-value") === value) {
+ card.classList.add("active");
+ } else {
+ card.classList.remove("active");
+ }
+ });
+
+ handleRoutingModeChange(value);
+ } else if (groupName === 'routing_ip_type') {
+ const input = $("net_routing_ip_type");
+ if (input) input.value = value;
+
+ const cards = document.querySelectorAll("#routing_ip_type_group .option-card");
+ cards.forEach(card => {
+ if (card.getAttribute("data-value") === value) {
+ card.classList.add("active");
+ } else {
+ card.classList.remove("active");
+ }
+ });
+ }
+}
+
+function setRoutingMode(value) {
+ selectOptionCard('routing_mode', value);
+}
+
+function setRoutingIpType(value) {
+ selectOptionCard('routing_ip_type', value);
+}
+
function handleRoutingModeChange(mode) {
const countryGroup = $("net_force_country_group");
const warningDiv = $("net_routing_warning");
@@ -3692,11 +3899,7 @@ function populateRoutingCountries() {
select.innerHTML = html;
if (state) {
- const mode = state.routing_mode || "auto";
- const modeSelect = $("net_routing_mode");
- if (modeSelect) modeSelect.value = mode;
select.value = translateCountry(state.force_country) || "";
- handleRoutingModeChange(mode);
}
}
@@ -3706,6 +3909,9 @@ function openCredentialsModal() {
$("credentials_form").reset();
if (state) {
$("cred_username").value = state.username || "";
+ $("cred_password").value = state.password || "";
+ $("cred_port").value = state.port || 8787;
+ $("cred_suffix").value = state.secret_path || "";
}
$("credentials_modal").style.display = "flex";
$("admin_dropdown").style.display = "none";
@@ -3726,6 +3932,8 @@ async function saveCredentials(e) {
const username = $("cred_username").value.trim();
const password = $("cred_password").value.trim();
+ const port = parseInt($("cred_port").value);
+ const suffix = $("cred_suffix").value.trim();
if (!username || !password) {
errorDivEl.textContent = "用户名和密码不能为空";
@@ -3733,6 +3941,24 @@ async function saveCredentials(e) {
return;
}
+ if (isNaN(port) || port < 1 || port > 65535) {
+ errorDivEl.textContent = "网页管理端口范围必须在 1 至 65535 之间";
+ errorDivEl.style.display = "block";
+ return;
+ }
+
+ if (!/^[A-Za-z0-9]+$/.test(suffix)) {
+ errorDivEl.textContent = "登录安全后缀仅能由英文字母和数字组成";
+ errorDivEl.style.display = "block";
+ return;
+ }
+
+ if (state && port === state.proxy_port) {
+ errorDivEl.textContent = "网页管理端口不能与代理出站端口相同";
+ errorDivEl.style.display = "block";
+ return;
+ }
+
submitBtn.disabled = true;
submitBtn.textContent = "正在保存...";
@@ -3740,17 +3966,36 @@ async function saveCredentials(e) {
const res = await fetch("./api/update_credentials", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ username, password })
+ body: JSON.stringify({
+ username: username,
+ password: password,
+ port: port,
+ secret_path: suffix
+ })
});
const data = await res.json();
if (res.ok && data.ok) {
- successDiv.textContent = "账号密码保存成功,已即时生效!";
- successDiv.style.display = "block";
- setTimeout(() => {
- closeCredentialsModal();
- load();
- }, 1500);
+ if (data.restart_needed) {
+ successDiv.textContent = "保存成功!网页管理端口或路径已变更,页面将在 4 秒内自动跳转...";
+ successDiv.style.display = "block";
+
+ const inputs = $("credentials_form").querySelectorAll("input, button");
+ inputs.forEach(el => el.disabled = true);
+
+ setTimeout(() => {
+ const protocol = window.location.protocol;
+ const host = window.location.hostname;
+ window.location.href = `${protocol}//${host}:${port}/${suffix}/`;
+ }, 4000);
+ } else {
+ successDiv.textContent = "账号密码保存成功,已即时生效!";
+ successDiv.style.display = "block";
+ setTimeout(() => {
+ closeCredentialsModal();
+ load();
+ }, 1500);
+ }
} else {
errorDivEl.textContent = data.error || "保存失败,请检查输入";
errorDivEl.style.display = "block";
@@ -3771,10 +4016,12 @@ function openNetworkModal() {
$("network_form").reset();
if (state) {
- $("net_port").value = state.port || 8787;
- $("net_suffix").value = state.secret_path || "";
$("net_proxy_port").value = state.proxy_port || 7928;
- $("net_routing_ip_type").value = state.routing_ip_type || "all";
+ const mode = state.routing_mode || "auto";
+ const ipType = state.routing_ip_type || "all";
+
+ selectOptionCard('routing_mode', mode);
+ selectOptionCard('routing_ip_type', ipType);
}
populateRoutingCountries();
@@ -3795,37 +4042,23 @@ async function saveNetwork(e) {
errorDivEl.style.display = "none";
successDiv.style.display = "none";
- const port = parseInt($("net_port").value);
- const suffix = $("net_suffix").value.trim();
const proxyPort = parseInt($("net_proxy_port").value);
const routingMode = $("net_routing_mode").value;
const forceCountry = $("net_force_country").value;
const routingIpType = $("net_routing_ip_type").value;
- if (isNaN(port) || port < 1 || port > 65535) {
- errorDivEl.textContent = "网页管理端口范围必须在 1 至 65535 之间";
- errorDivEl.style.display = "block";
- return;
- }
-
if (isNaN(proxyPort) || proxyPort < 1024 || proxyPort > 65535) {
errorDivEl.textContent = "代理出站端口范围必须在 1024 至 65535 之间";
errorDivEl.style.display = "block";
return;
}
- if (proxyPort === port) {
+ if (state && proxyPort === state.port) {
errorDivEl.textContent = "代理出站端口不能与网页管理端口相同";
errorDivEl.style.display = "block";
return;
}
- if (!/^[A-Za-z0-9]+$/.test(suffix)) {
- errorDivEl.textContent = "登录安全后缀仅能由英文字母和数字组成";
- errorDivEl.style.display = "block";
- return;
- }
-
if (routingMode === "fixed_region" && !forceCountry) {
errorDivEl.textContent = "请选择一个要锁定的目标国家";
errorDivEl.style.display = "block";
@@ -3840,8 +4073,6 @@ async function saveNetwork(e) {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- port: port,
- secret_path: suffix,
proxy_port: proxyPort,
routing_mode: routingMode,
force_country: forceCountry,
@@ -3852,16 +4083,14 @@ async function saveNetwork(e) {
const data = await res.json();
if (res.ok && data.ok) {
if (data.restart_needed) {
- successDiv.textContent = "保存成功!网页端口或路径已变更,页面将在 4 秒内自动跳转...";
+ successDiv.textContent = "保存成功!代理出站端口已变更,页面将在 4 秒内自动刷新...";
successDiv.style.display = "block";
- const inputs = $("network_form").querySelectorAll("input, button, select");
+ const inputs = $("network_form").querySelectorAll("input, button");
inputs.forEach(el => el.disabled = true);
setTimeout(() => {
- const protocol = window.location.protocol;
- const host = window.location.hostname;
- window.location.href = `${protocol}//${host}:${port}/${suffix}/`;
+ window.location.reload();
}, 4000);
} else {
successDiv.textContent = "配置保存成功,已即时生效!";
@@ -3885,6 +4114,8 @@ async function saveNetwork(e) {
}
}
+
+
function openAdModal() {
$("ad_modal").style.display = "flex";
}