346 lines
12 KiB
JavaScript
346 lines
12 KiB
JavaScript
import { announce, apiJson } from "../core.js";
|
||
import { setControlBusy, setControlReady, setControlUnavailable } from "../interaction-feedback.js";
|
||
import { createLatchedDraftState } from "../wifi-draft-state.js";
|
||
|
||
const ssidInput = document.getElementById("wifi-ssid");
|
||
const passwordInput = document.getElementById("wifi-password");
|
||
const dhcpToggle = document.getElementById("wifi-dhcp-enabled");
|
||
const dhcpState = document.getElementById("wifi-dhcp-state");
|
||
const dhcpRow = dhcpToggle.closest(".wifi-dhcp-row");
|
||
const manualFields = document.getElementById("wifi-manual-fields");
|
||
const addressInput = document.getElementById("wifi-address");
|
||
const gatewayInput = document.getElementById("wifi-gateway");
|
||
const prefixInput = document.getElementById("wifi-prefix");
|
||
const dnsInput = document.getElementById("wifi-dns");
|
||
const immediateButton = document.getElementById("wifi-apply-immediate");
|
||
const nextBootButton = document.getElementById("wifi-apply-next-boot");
|
||
const activeStatus = document.getElementById("wifi-active-status");
|
||
const feedback = document.getElementById("wifi-feedback");
|
||
const delayInput = document.getElementById("wifi-prompt-delay");
|
||
const delaySaveButton = document.getElementById("wifi-prompt-delay-save");
|
||
const delayFeedback = document.getElementById("wifi-prompt-feedback");
|
||
|
||
const networkFields = new Map([
|
||
["ssid", { control: ssidInput, marker: document.getElementById("wifi-ssid-modified"), highlight: ssidInput }],
|
||
["password", { control: passwordInput, marker: document.getElementById("wifi-password-modified"), highlight: passwordInput }],
|
||
["dhcp", { control: dhcpToggle, marker: document.getElementById("wifi-dhcp-modified"), highlight: dhcpRow }],
|
||
["address", { control: addressInput, marker: document.getElementById("wifi-address-modified"), highlight: addressInput }],
|
||
["gateway", { control: gatewayInput, marker: document.getElementById("wifi-gateway-modified"), highlight: gatewayInput }],
|
||
["prefix", { control: prefixInput, marker: document.getElementById("wifi-prefix-modified"), highlight: prefixInput }],
|
||
["dns", { control: dnsInput, marker: document.getElementById("wifi-dns-modified"), highlight: dnsInput }],
|
||
]);
|
||
const networkDraft = createLatchedDraftState(networkFields.keys());
|
||
const delayDraft = createLatchedDraftState(["prompt_delay_seconds"]);
|
||
|
||
let wifiPending = false;
|
||
let delayPending = false;
|
||
let settingsLoaded = false;
|
||
let loadGeneration = 0;
|
||
let baseline = null;
|
||
|
||
function setFeedback(message, state = "idle") {
|
||
feedback.textContent = message;
|
||
feedback.dataset.state = state;
|
||
feedback.hidden = !message;
|
||
}
|
||
|
||
function setDelayFeedback(message, state = "idle") {
|
||
delayFeedback.textContent = message;
|
||
delayFeedback.dataset.state = state;
|
||
delayFeedback.hidden = !message;
|
||
}
|
||
|
||
function showDhcpMode() {
|
||
const enabled = dhcpToggle.checked;
|
||
dhcpState.textContent = enabled ? "开启" : "关闭";
|
||
manualFields.hidden = enabled;
|
||
addressInput.required = !enabled;
|
||
gatewayInput.required = !enabled;
|
||
}
|
||
|
||
function splitDns(value) {
|
||
return value.split(/[,,\s]+/u).map((item) => item.trim()).filter(Boolean);
|
||
}
|
||
|
||
function showStatus(data) {
|
||
const active = data.active || {};
|
||
if (active.connected) {
|
||
activeStatus.textContent = `当前已连接:${active.ssid || "未知 WiFi"} · ${active.ipv4_address || "等待 IPv4"}`;
|
||
} else if (data.available === false) {
|
||
activeStatus.textContent = `网络状态不可用:${data.error || "NetworkManager 无响应"}`;
|
||
} else {
|
||
activeStatus.textContent = "当前未连接 WiFi";
|
||
}
|
||
}
|
||
|
||
function formStateFromResponse(data) {
|
||
const saved = data.saved || {};
|
||
return {
|
||
ssid: saved.ssid || "",
|
||
password: saved.password || "",
|
||
passwordConfigured: Boolean(saved.password_configured),
|
||
dhcp: (saved.ipv4_mode || "dhcp") === "dhcp",
|
||
address: saved.address || "",
|
||
gateway: saved.gateway || "",
|
||
prefix: saved.prefix ?? "",
|
||
dns: (saved.dns_servers || []).join(", "),
|
||
promptDelay: data.prompt_delay_seconds ?? 30,
|
||
};
|
||
}
|
||
|
||
function applyFormState(state) {
|
||
if (!state) return;
|
||
ssidInput.value = state.ssid;
|
||
passwordInput.value = state.password;
|
||
passwordInput.placeholder = state.passwordConfigured ? "" : "请输入 8..63 字节密码";
|
||
dhcpToggle.checked = state.dhcp;
|
||
addressInput.value = state.address;
|
||
gatewayInput.value = state.gateway;
|
||
prefixInput.value = state.prefix;
|
||
dnsInput.value = state.dns;
|
||
delayInput.value = String(state.promptDelay);
|
||
showDhcpMode();
|
||
}
|
||
|
||
function setNetworkMarker(key, modified) {
|
||
const field = networkFields.get(key);
|
||
field.marker.hidden = !modified;
|
||
field.highlight.classList.toggle("wifi-field-modified", modified);
|
||
}
|
||
|
||
function clearNetworkDraft() {
|
||
networkDraft.clear();
|
||
networkFields.forEach((_field, key) => setNetworkMarker(key, false));
|
||
updateNetworkButtons();
|
||
}
|
||
|
||
function clearDelayDraft() {
|
||
delayDraft.clear();
|
||
updateDelayButton();
|
||
}
|
||
|
||
function updateNetworkButtons() {
|
||
if (wifiPending) return;
|
||
const ready = settingsLoaded && networkDraft.dirty;
|
||
immediateButton.classList.toggle("wifi-action-ready", ready);
|
||
nextBootButton.classList.toggle("wifi-action-ready", ready);
|
||
if (ready) {
|
||
setControlReady(immediateButton);
|
||
setControlReady(nextBootButton);
|
||
return;
|
||
}
|
||
const reason = settingsLoaded
|
||
? "请先修改 WiFi 或 DHCP 设置,再选择生效方式。"
|
||
: "请等待当前 WiFi 设置读取完成。";
|
||
setControlUnavailable(immediateButton, reason);
|
||
setControlUnavailable(nextBootButton, reason);
|
||
}
|
||
|
||
function updateDelayButton() {
|
||
if (delayPending) return;
|
||
const ready = settingsLoaded && delayDraft.dirty;
|
||
delaySaveButton.classList.toggle("wifi-action-ready", ready);
|
||
if (ready) {
|
||
setControlReady(delaySaveButton);
|
||
return;
|
||
}
|
||
setControlUnavailable(
|
||
delaySaveButton,
|
||
settingsLoaded ? "请先修改网络提示等待时间。" : "请等待当前网络提示设置读取完成。",
|
||
);
|
||
}
|
||
|
||
function markNetworkField(key) {
|
||
if (!settingsLoaded) return;
|
||
networkDraft.mark(key);
|
||
setNetworkMarker(key, true);
|
||
updateNetworkButtons();
|
||
}
|
||
|
||
function markDelayField() {
|
||
if (!settingsLoaded) return;
|
||
delayDraft.mark("prompt_delay_seconds");
|
||
updateDelayButton();
|
||
}
|
||
|
||
function syncForm(data) {
|
||
baseline = formStateFromResponse(data);
|
||
applyFormState(baseline);
|
||
settingsLoaded = true;
|
||
clearNetworkDraft();
|
||
clearDelayDraft();
|
||
showStatus(data);
|
||
}
|
||
|
||
export async function loadWifiSettings() {
|
||
const generation = ++loadGeneration;
|
||
settingsLoaded = false;
|
||
clearNetworkDraft();
|
||
clearDelayDraft();
|
||
try {
|
||
const data = await apiJson("/api/network/wifi");
|
||
if (generation !== loadGeneration) return;
|
||
syncForm(data);
|
||
setFeedback("");
|
||
setDelayFeedback("");
|
||
} catch (error) {
|
||
if (generation !== loadGeneration) return;
|
||
setFeedback(`WiFi 设置读取失败:${error.message}`, "error");
|
||
announce(`WiFi 设置读取失败:${error.message}`, true);
|
||
updateNetworkButtons();
|
||
updateDelayButton();
|
||
}
|
||
}
|
||
|
||
function buildRequest(activation) {
|
||
const ssid = ssidInput.value.trim();
|
||
if (!ssid) throw new Error("请输入 WiFi 名称(SSID)");
|
||
const password = passwordInput.value;
|
||
if (ssid !== baseline.ssid && !password) throw new Error("修改 WiFi 名称时必须填写新密码");
|
||
if (!password && !baseline.passwordConfigured) throw new Error("当前没有已保存密码,请填写 WiFi 密码");
|
||
|
||
const manual = !dhcpToggle.checked;
|
||
if (manual && !addressInput.value.trim()) throw new Error("关闭 DHCP 后必须填写静态 IPv4 地址");
|
||
if (manual && !gatewayInput.value.trim()) throw new Error("关闭 DHCP 后必须填写网关");
|
||
const rawPrefix = prefixInput.value.trim();
|
||
const prefix = rawPrefix ? Number(rawPrefix) : null;
|
||
if (rawPrefix && (!Number.isInteger(prefix) || prefix < 1 || prefix > 32)) {
|
||
throw new Error("子网前缀必须是 1..32 的整数");
|
||
}
|
||
return {
|
||
ssid,
|
||
password: password || null,
|
||
ipv4_mode: manual ? "manual" : "dhcp",
|
||
address: manual ? addressInput.value.trim() : null,
|
||
gateway: manual ? gatewayInput.value.trim() : null,
|
||
prefix,
|
||
dns_servers: manual ? splitDns(dnsInput.value) : [],
|
||
activation,
|
||
};
|
||
}
|
||
|
||
function captureNetworkBaseline(request) {
|
||
baseline = {
|
||
...baseline,
|
||
ssid: request.ssid,
|
||
password: request.password || baseline.password,
|
||
passwordConfigured: Boolean(request.password || baseline.passwordConfigured),
|
||
dhcp: request.ipv4_mode === "dhcp",
|
||
address: request.address || "",
|
||
gateway: request.gateway || "",
|
||
prefix: request.prefix ?? "",
|
||
dns: (request.dns_servers || []).join(", "),
|
||
};
|
||
}
|
||
|
||
async function applyWifi(activation) {
|
||
if (wifiPending || !networkDraft.dirty) return;
|
||
let request;
|
||
try {
|
||
request = buildRequest(activation);
|
||
} catch (error) {
|
||
setFeedback(error.message, "error");
|
||
announce(error.message, true);
|
||
return;
|
||
}
|
||
wifiPending = true;
|
||
setControlBusy(immediateButton, {
|
||
label: activation === "immediate" ? "正在保存…" : undefined,
|
||
message: "正在安全保存 WiFi 与 IPv4 配置…",
|
||
});
|
||
setControlBusy(nextBootButton, {
|
||
label: activation === "next_boot" ? "正在保存…" : undefined,
|
||
});
|
||
setFeedback(
|
||
activation === "immediate"
|
||
? "正在保存;设备随后会切换网络,当前页面可能断开。"
|
||
: "正在保存下次断电开机使用的网络配置…",
|
||
"pending",
|
||
);
|
||
try {
|
||
const result = await apiJson("/api/network/wifi", {
|
||
method: "PUT",
|
||
body: JSON.stringify(request),
|
||
});
|
||
captureNetworkBaseline(request);
|
||
clearNetworkDraft();
|
||
setFeedback(result.message, "success");
|
||
announce(result.message);
|
||
if (result.network) showStatus(result.network);
|
||
} catch (error) {
|
||
setFeedback(`WiFi 设置保存失败:${error.message}`, "error");
|
||
announce(`WiFi 设置保存失败:${error.message}`, true);
|
||
} finally {
|
||
wifiPending = false;
|
||
updateNetworkButtons();
|
||
}
|
||
}
|
||
|
||
function validatedPromptDelay() {
|
||
const delay = Number(delayInput.value);
|
||
if (!Number.isInteger(delay) || delay < 1 || delay > 3600) {
|
||
throw new Error("网络提示等待时间必须是 1..3600 秒的整数");
|
||
}
|
||
return delay;
|
||
}
|
||
|
||
async function savePromptDelay() {
|
||
if (delayPending || !delayDraft.dirty) return;
|
||
let delay;
|
||
try {
|
||
delay = validatedPromptDelay();
|
||
} catch (error) {
|
||
setDelayFeedback(error.message, "error");
|
||
announce(error.message, true);
|
||
return;
|
||
}
|
||
delayPending = true;
|
||
setControlBusy(delaySaveButton, {
|
||
label: "正在保存…",
|
||
message: "正在保存网络提示等待时间…",
|
||
});
|
||
setDelayFeedback("正在保存网络提示等待时间…", "pending");
|
||
try {
|
||
const result = await apiJson("/api/network/wifi/prompt-delay", {
|
||
method: "PUT",
|
||
body: JSON.stringify({ prompt_delay_seconds: delay }),
|
||
});
|
||
baseline = { ...baseline, promptDelay: result.prompt_delay_seconds };
|
||
delayInput.value = String(result.prompt_delay_seconds);
|
||
clearDelayDraft();
|
||
setDelayFeedback(result.message, "success");
|
||
announce(result.message);
|
||
} catch (error) {
|
||
setDelayFeedback(`网络提示等待时间保存失败:${error.message}`, "error");
|
||
announce(`网络提示等待时间保存失败:${error.message}`, true);
|
||
} finally {
|
||
delayPending = false;
|
||
updateDelayButton();
|
||
}
|
||
}
|
||
|
||
export function discardWifiSettingsDrafts() {
|
||
loadGeneration += 1;
|
||
applyFormState(baseline);
|
||
settingsLoaded = false;
|
||
clearNetworkDraft();
|
||
clearDelayDraft();
|
||
setFeedback("");
|
||
setDelayFeedback("");
|
||
}
|
||
|
||
for (const [key, field] of networkFields) {
|
||
if (key === "dhcp") continue;
|
||
field.control.addEventListener("input", () => markNetworkField(key));
|
||
}
|
||
dhcpToggle.addEventListener("change", () => {
|
||
showDhcpMode();
|
||
markNetworkField("dhcp");
|
||
});
|
||
delayInput.addEventListener("input", markDelayField);
|
||
immediateButton.addEventListener("click", () => applyWifi("immediate"));
|
||
nextBootButton.addEventListener("click", () => applyWifi("next_boot"));
|
||
delaySaveButton.addEventListener("click", savePromptDelay);
|
||
updateNetworkButtons();
|
||
updateDelayButton();
|