Files
matrix-screen-controller/核桃派软件源代码/app/static/views/frp-settings.js
T

323 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { announce, apiJson } from "../core.js";
import { setControlBusy, setControlReady, setControlUnavailable } from "../interaction-feedback.js";
const selectedName = document.getElementById("frp-selected-name");
const serviceState = document.getElementById("frp-service-state");
const feedback = document.getElementById("frp-feedback");
const serviceToggle = document.getElementById("frp-service-toggle");
const editButton = document.getElementById("frp-edit-config");
const diagnosticsButton = document.getElementById("network-diagnostics-run");
const dialog = document.getElementById("frp-config-dialog");
const configSelect = document.getElementById("frp-config-select");
const configName = document.getElementById("frp-config-name");
const configFormat = document.getElementById("frp-config-format");
const configContent = document.getElementById("frp-config-content");
const configFile = document.getElementById("frp-config-file");
const configFeedback = document.getElementById("frp-config-feedback");
const newButton = document.getElementById("frp-config-new");
const uploadButton = document.getElementById("frp-config-upload");
const deleteButton = document.getElementById("frp-config-delete");
const closeButton = document.getElementById("frp-config-close");
const saveButton = document.getElementById("frp-config-save");
const diagnosticsDialog = document.getElementById("network-diagnostics-dialog");
const diagnosticsSummary = document.getElementById("network-diagnostics-summary");
const diagnosticsChecks = document.getElementById("network-diagnostics-checks");
const diagnosticsClose = document.getElementById("network-diagnostics-close");
const SUPPORTED_SUFFIXES = new Set([".toml", ".yaml", ".yml", ".json", ".ini"]);
const MAX_CONFIG_BYTES = 1024 * 1024;
let state = null;
let editingId = null;
let generation = 0;
function setMessage(element, text, kind = "idle") {
element.textContent = text;
element.dataset.state = kind;
element.hidden = !text;
}
function profileById(id) {
return state?.profiles?.find((profile) => profile.id === id) || null;
}
function serviceLabel(service) {
if (!service?.installed) return "frpc 未安装";
if (service.running) return service.enabled ? "运行中(开机自动启动)" : "运行中";
if (service.state === "failed") return "启动失败";
return service.enabled ? "等待启动" : "关闭";
}
function renderState(next) {
state = next;
const selected = profileById(next?.selected_id);
selectedName.textContent = selected?.name || "无";
serviceState.textContent = serviceLabel(next?.service);
const running = next?.service?.running === true;
serviceToggle.textContent = running || next?.service?.enabled ? "关闭frp" : "启动frp";
if (!next?.available) {
setControlUnavailable(serviceToggle, next?.error || "FRP 配置目录不可用。");
setMessage(feedback, next?.error || "FRP 配置目录不可用。", "error");
} else if (!next?.service?.installed) {
setControlUnavailable(serviceToggle, "frpc 系统软件尚未安装。");
setMessage(feedback, "frpc 系统软件尚未安装。", "error");
} else if (!selected && !running) {
setControlUnavailable(serviceToggle, "请先导入或新建 FRP 配置。");
setMessage(feedback, "尚未保存 FRP 配置。", "idle");
} else {
setControlReady(serviceToggle);
setMessage(feedback, next?.service?.last_error || "", next?.service?.last_error ? "error" : "idle");
}
}
function renderProfileOptions() {
configSelect.replaceChildren();
if (!state?.profiles?.length) {
const option = document.createElement("option");
option.value = "";
option.textContent = "暂无配置";
configSelect.append(option);
configSelect.disabled = true;
} else {
configSelect.disabled = false;
state.profiles.forEach((profile) => {
const option = document.createElement("option");
option.value = profile.id;
option.textContent = profile.name;
configSelect.append(option);
});
configSelect.value = editingId || state.selected_id || state.profiles[0].id;
}
deleteButton.disabled = !editingId;
}
async function requestText(path, options = {}) {
const response = await fetch(path, { ...options, cache: "no-store" });
if (!response.ok) {
let message = response.statusText;
try { message = (await response.json()).detail || message; } catch {}
throw new Error(message);
}
return response.text();
}
async function loadProfile(id) {
const currentGeneration = generation;
const profile = profileById(id);
if (!profile) {
editingId = null;
configName.value = "";
configContent.value = "";
renderProfileOptions();
return;
}
setMessage(configFeedback, "正在读取配置…");
const content = await requestText(`/api/frp/configs/${encodeURIComponent(id)}`);
if (generation !== currentGeneration) return;
editingId = id;
configName.value = profile.name;
const suffix = profile.filename.slice(profile.filename.lastIndexOf(".")).toLowerCase();
configFormat.value = suffix === ".yml" ? ".yaml" : suffix;
configContent.value = content;
renderProfileOptions();
setMessage(configFeedback, "");
}
export async function loadFrpSettings() {
const currentGeneration = ++generation;
try {
const next = await apiJson("/api/frp");
if (generation !== currentGeneration) return;
renderState(next);
} catch (error) {
setMessage(feedback, `FRP 状态读取失败:${error.message}`, "error");
}
}
export function discardFrpDrafts() {
generation += 1;
editingId = null;
configName.value = "";
configContent.value = "";
configFile.value = "";
setMessage(configFeedback, "");
if (dialog.open) dialog.close();
if (diagnosticsDialog.open) diagnosticsDialog.close();
}
editButton.addEventListener("click", async () => {
try {
await loadFrpSettings();
editingId = state?.selected_id || state?.profiles?.[0]?.id || null;
renderProfileOptions();
if (editingId) await loadProfile(editingId);
else {
configName.value = "";
configFormat.value = ".toml";
configContent.value = "";
}
dialog.showModal();
(editingId ? configContent : configName).focus();
} catch (error) {
announce(`打开 FRP 配置失败:${error.message}`, true);
}
});
closeButton.addEventListener("click", () => dialog.close());
dialog.addEventListener("close", () => {
configFile.value = "";
setMessage(configFeedback, "");
});
newButton.addEventListener("click", () => {
editingId = null;
configSelect.value = "";
configName.value = "";
configFormat.value = ".toml";
configContent.value = "";
deleteButton.disabled = true;
setMessage(configFeedback, "请输入完整配置;保存前会使用 frpc 官方校验。", "idle");
configName.focus();
});
uploadButton.addEventListener("click", () => configFile.click());
configFile.addEventListener("change", async () => {
const file = configFile.files?.[0];
if (!file) return;
const suffix = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
if (!SUPPORTED_SUFFIXES.has(suffix)) {
setMessage(configFeedback, "请选择 TOML、YAML、JSON 或 INI 配置文件。", "error");
return;
}
if (file.size > MAX_CONFIG_BYTES) {
setMessage(configFeedback, "配置文件不能超过 1 MiB。", "error");
return;
}
try {
const content = await file.text();
editingId = null;
configName.value = file.name.slice(0, -suffix.length);
configFormat.value = suffix === ".yml" ? ".yaml" : suffix;
configContent.value = content;
deleteButton.disabled = true;
setMessage(configFeedback, "文件已读取,点击保存后执行官方校验。", "idle");
} catch (error) {
setMessage(configFeedback, `读取文件失败:${error.message}`, "error");
} finally {
configFile.value = "";
}
});
configSelect.addEventListener("change", async () => {
const id = configSelect.value;
if (!id) return;
try {
if (id !== state?.selected_id) {
const result = await apiJson(`/api/frp/configs/${encodeURIComponent(id)}/select`, { method: "POST" });
renderState(result.frp);
}
await loadProfile(id);
} catch (error) {
setMessage(configFeedback, `切换配置失败:${error.message}`, "error");
renderProfileOptions();
}
});
saveButton.addEventListener("click", async () => {
const name = configName.value.trim();
const content = configContent.value;
if (!name || !content.trim()) {
setMessage(configFeedback, "请输入配置名称和完整配置内容。", "error");
return;
}
setControlBusy(saveButton, { label: "正在校验…", message: "正在使用 frpc 校验配置" });
try {
const suffix = configFormat.value;
const path = editingId
? `/api/frp/configs/${encodeURIComponent(editingId)}?name=${encodeURIComponent(name)}`
: `/api/frp/configs?filename=${encodeURIComponent(`frpc${suffix}`)}&name=${encodeURIComponent(name)}`;
const response = await fetch(path, {
method: editingId ? "PUT" : "POST",
headers: { "Content-Type": "text/plain; charset=utf-8" },
body: content,
cache: "no-store",
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || response.statusText);
editingId = result.profile.id;
renderState(result.frp);
renderProfileOptions();
setMessage(configFeedback, result.frp.service.running ? "配置已保存并重启 FRP。" : "配置已保存并通过校验。", "success");
announce("FRP 配置已保存");
} catch (error) {
setMessage(configFeedback, `配置保存失败:${error.message}`, "error");
} finally {
setControlReady(saveButton);
}
});
deleteButton.addEventListener("click", async () => {
if (!editingId) return;
const profile = profileById(editingId);
if (!window.confirm(`确认删除 FRP 配置“${profile?.name || "未命名"}”?`)) return;
setControlBusy(deleteButton, { label: "正在删除…" });
try {
const result = await apiJson(`/api/frp/configs/${encodeURIComponent(editingId)}`, { method: "DELETE" });
renderState(result.frp);
editingId = result.frp.selected_id || result.frp.profiles?.[0]?.id || null;
renderProfileOptions();
if (editingId) await loadProfile(editingId);
else {
configName.value = "";
configContent.value = "";
}
announce("FRP 配置已删除");
} catch (error) {
setMessage(configFeedback, `删除失败:${error.message}`, "error");
} finally {
setControlReady(deleteButton);
}
});
serviceToggle.addEventListener("click", async () => {
const stopping = state?.service?.running || state?.service?.enabled;
setControlBusy(serviceToggle, { label: stopping ? "正在关闭…" : "正在启动…" });
try {
const result = await apiJson(`/api/frp/service/${stopping ? "stop" : "start"}`, { method: "POST" });
renderState(result.frp);
announce(stopping ? "FRP 已关闭" : "FRP 已启动");
} catch (error) {
setMessage(feedback, `${stopping ? "关闭" : "启动"} FRP 失败:${error.message}`, "error");
await loadFrpSettings();
}
});
diagnosticsButton.addEventListener("click", async () => {
diagnosticsSummary.textContent = "正在检测网络,请稍候…";
diagnosticsSummary.dataset.state = "idle";
diagnosticsChecks.replaceChildren();
diagnosticsDialog.showModal();
setControlBusy(diagnosticsButton, { label: "正在检测…" });
try {
const result = await apiJson("/api/network/diagnostics", { method: "POST" });
diagnosticsSummary.textContent = result.diagnosis;
diagnosticsSummary.dataset.state = result.overall === "ok" ? "success" : "error";
result.checks.forEach((check) => {
const item = document.createElement("li");
item.dataset.state = check.ok ? "success" : (check.warning ? "warning" : "error");
item.textContent = `${check.ok ? "通过" : (check.warning ? "提示" : "失败")}:${check.message}`;
diagnosticsChecks.append(item);
});
} catch (error) {
diagnosticsSummary.textContent = `网络检测失败:${error.message}`;
diagnosticsSummary.dataset.state = "error";
} finally {
setControlReady(diagnosticsButton);
}
});
diagnosticsClose.addEventListener("click", () => diagnosticsDialog.close());
document.addEventListener("matrix:status", (event) => {
if (event.detail?.frp) renderState(event.detail.frp);
});