diff --git a/frontend/admin/app.css b/frontend/admin/app.css
index a3c8370..a1d57e6 100644
--- a/frontend/admin/app.css
+++ b/frontend/admin/app.css
@@ -199,7 +199,7 @@ html, body {
background: #fff;
border-radius: var(--radius-lg);
border: 1px solid var(--border-light);
- overflow: hidden;
+ overflow: auto;
}
table {
diff --git a/frontend/admin/app.js b/frontend/admin/app.js
index 1e6342b..c325c15 100644
--- a/frontend/admin/app.js
+++ b/frontend/admin/app.js
@@ -39,7 +39,9 @@
realname: { list: [], pagination: null },
devices: { list: [], pagination: null },
ipv6: { list: [], pagination: null },
- expandedNodes: new Set()
+ expandedNodes: new Set(),
+ prevUser: null,
+ prevRoute: null
};
const ROUTE_META = {
@@ -73,6 +75,9 @@
root.addEventListener("click", onClick);
root.addEventListener("submit", onSubmit);
+ // Initialize shell and modal containers
+ root.innerHTML = '
';
+
if (state.token) {
await loadBootstrap();
} else {
@@ -266,6 +271,8 @@
}
if (action === "plan-delete") {
+ event.preventDefault();
+ event.stopPropagation();
if (confirm("确认删除该套餐吗?")) {
await adminPost(`${cfg.api.adminBase}/plan/drop`, { id: Number(actionEl.getAttribute("data-id")) });
await hydrateRoute();
@@ -280,6 +287,8 @@
}
if (action === "coupon-delete") {
+ event.preventDefault();
+ event.stopPropagation();
if (confirm("确认删除该优惠券吗?")) {
await adminPost(`${cfg.api.adminBase}/coupon/drop`, { id: Number(actionEl.getAttribute("data-id")) });
await hydrateRoute();
@@ -312,6 +321,8 @@
}
if (action === "order-paid") {
+ event.preventDefault();
+ event.stopPropagation();
await adminPost(`${cfg.api.adminBase}/order/paid`, {
trade_no: actionEl.getAttribute("data-trade")
});
@@ -320,6 +331,8 @@
}
if (action === "order-cancel") {
+ event.preventDefault();
+ event.stopPropagation();
await adminPost(`${cfg.api.adminBase}/order/cancel`, {
trade_no: actionEl.getAttribute("data-trade")
});
@@ -334,13 +347,18 @@
}
if (action === "node-edit") {
- const item = state.nodes.find((row) => String(row.id) === actionEl.getAttribute("data-id"));
+ event.preventDefault();
+ event.stopPropagation();
+ const nodeId = String(actionEl.getAttribute("data-id"));
+ const item = state.nodes.find((row) => String(row.id) === nodeId);
state.modal = { type: "node", data: item || {} };
render();
return;
}
if (action === "node-copy") {
+ event.preventDefault();
+ event.stopPropagation();
const nodeId = Number(actionEl.getAttribute("data-id"));
try {
await adminPost(`${cfg.api.adminBase}/server/manage/copy`, { id: nodeId });
@@ -350,6 +368,8 @@
}
if (action === "node-delete") {
+ event.preventDefault();
+ event.stopPropagation();
if (confirm("确认删除该节点吗?")) {
const nodeId = Number(actionEl.getAttribute("data-id"));
try {
@@ -544,19 +564,32 @@
await hydrateRoute();
}
- function render() {
- root.innerHTML = state.user ? renderDashboard() : renderLogin();
- if (state.modal) {
- const modalShell = document.createElement("div");
- modalShell.className = "modal-overlay";
- modalShell.innerHTML = renderModalContent();
- root.appendChild(modalShell);
+ function render(force) {
+ const shellContainer = document.getElementById("admin-shell-container");
+ const modalContainer = document.getElementById("admin-modal-container");
+ const busyContainer = document.getElementById("admin-busy-container");
+
+ if (!shellContainer || !modalContainer || !busyContainer) return;
+
+ // Re-render the shell if user or route changed, or if forced (e.g., data updated)
+ if (force || state.user !== state.prevUser || state.route !== state.prevRoute) {
+ shellContainer.innerHTML = state.user ? renderDashboard() : renderLogin();
+ state.prevUser = state.user;
+ state.prevRoute = state.route;
}
+
+ // Handle Modal independently
+ if (state.modal) {
+ modalContainer.innerHTML = `${renderModalContent()}
`;
+ } else {
+ modalContainer.innerHTML = "";
+ }
+
+ // Handle Busy overlay independently
if (state.busy) {
- const overlay = document.createElement("div");
- overlay.className = "busy-overlay";
- overlay.innerHTML = '正在处理...
';
- root.appendChild(overlay);
+ busyContainer.innerHTML = '';
+ } else {
+ busyContainer.innerHTML = "";
}
}
@@ -741,7 +774,8 @@
}
function renderNodeManage() {
- const roots = state.nodes.filter((node) => !node.parent_id);
+ const isRoot = (node) => !node.parent_id || String(node.parent_id) === "0";
+ const roots = state.nodes.filter(isRoot);
const childrenByParent = {};
state.nodes.forEach((node) => {
if (node.parent_id) {
@@ -1332,17 +1366,17 @@
function setBusy(value) {
state.busy = value;
- render();
+ render(true);
}
function show(message, type) {
state.message = message;
state.messageType = type || "info";
- render();
+ render(true);
window.clearTimeout(show._timer);
show._timer = window.setTimeout(function () {
state.message = "";
- render();
+ render(true);
}, 3000);
}
diff --git a/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js b/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js
index 213ea08..4e59ffa 100644
--- a/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js
+++ b/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js
@@ -232827,9 +232827,7 @@ function qst({ title: e, icon: t, label: n, sub: i, closeNav: r }) {
children: [
Q.jsx(Fst, {
asChild: !0,
- children: Q.jsx(Lot, {
- asChild: !0,
- children: Q.jsx(Nm, {
+ children: Q.jsx(Lot, { asChild: !0, onClick: (e) => (e.stopPropagation(), e.preventDefault()), children: Q.jsx(Nm, {
variant: a ? "secondary" : "ghost",
size: "icon",
className: "h-12 w-12",
@@ -235896,9 +235894,7 @@ function vut() {
o = r.substring(0, 2).toUpperCase();
return Q.jsxs(Not, {
children: [
- Q.jsx(Lot, {
- asChild: !0,
- children: Q.jsx(Nm, {
+ Q.jsx(Lot, { asChild: !0, onClick: (e) => (e.stopPropagation(), e.preventDefault()), children: Q.jsx(Nm, {
variant: "ghost",
className: "relative h-8 w-8 rounded-full",
children: Q.jsxs(fut, {
@@ -292882,6 +292878,7 @@ function c5t() {
v = kv({ resolver: Ov(n), defaultValues: i, mode: "onChange" }),
b = T_({ control: v.control, name: "protocol_settings" }),
y = H.useRef(new Map()),
+ T = H.useRef(0),
x = H.useCallback(async () => {
if (!r) return;
const [e, t, n] = await Promise.all([eD(), rD(), UL()]);
@@ -292899,6 +292896,9 @@ function c5t() {
(H.useEffect(() => {
x();
}, [x]),
+ H.useEffect(() => {
+ r && s && (T.current = Date.now());
+ }, [r, s]),
H.useEffect(() => {
if (s) {
const e =
@@ -292939,9 +292939,11 @@ function c5t() {
),
E = (e) => {
if (e) {
+ s && (T.current = Date.now());
o(!0);
return;
}
+ if (s && Date.now() - T.current < 250) return;
(o(!1), a(null), v.reset(i));
};
return Q.jsxs(Jet, {
@@ -293992,9 +293994,7 @@ function h5t({ table: e, refetch: t, saveOrder: n, isSortMode: i, groups: r }) {
Q.jsxs(Not, {
modal: !1,
children: [
- Q.jsx(Lot, {
- asChild: !0,
- children: Q.jsx(Nm, {
+ Q.jsx(Lot, { asChild: !0, onClick: (e) => (e.stopPropagation(), e.preventDefault()), children: Q.jsx(Nm, {
variant: "outline",
size: "sm",
className: "h-8 border-dashed",
@@ -294097,9 +294097,7 @@ function h5t({ table: e, refetch: t, saveOrder: n, isSortMode: i, groups: r }) {
Q.jsxs(Not, {
modal: !1,
children: [
- Q.jsx(Lot, {
- asChild: !0,
- children: Q.jsx(Nm, {
+ Q.jsx(Lot, { asChild: !0, onClick: (e) => (e.stopPropagation(), e.preventDefault()), children: Q.jsx(Nm, {
variant: "outline",
size: "sm",
className: "h-8 border-dashed",
@@ -294399,14 +294397,26 @@ function D5t({ node: e, refetch: t }) {
});
}
function T5t({ node: e, refetch: t, t: n }) {
- const { setIsOpen: i, setEditingServer: r, setServerType: o } = p4t();
+ const { setIsOpen: i, setEditingServer: r, setServerType: o } = p4t(),
+ [s, a] = H.useState(!1),
+ l = H.useCallback(() => {
+ (a(!1),
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ (o(e.type), r(e), i(!0));
+ });
+ }));
+ }, [e, i, r, o]);
return Q.jsx("div", {
className: "flex justify-center",
children: Q.jsxs(Not, {
modal: !1,
+ open: s,
+ onOpenChange: a,
children: [
Q.jsx(Lot, {
asChild: !0,
+ onClick: (e) => (e.stopPropagation(), e.preventDefault()),
children: Q.jsx(Nm, {
variant: "ghost",
className: "h-8 w-8 p-0 hover:bg-muted",
@@ -294420,9 +294430,8 @@ function T5t({ node: e, refetch: t, t: n }) {
children: [
Q.jsx(Rot, {
className: "cursor-pointer",
- onSelect: (t) => {
- t.preventDefault();
- (o(e.type), r(e), i(!0));
+ onSelect: (e) => {
+ (e.preventDefault(), e.stopPropagation(), l());
},
children: Q.jsxs("div", {
className: "flex w-full items-center",
@@ -302584,9 +302593,7 @@ function P6t({ table: e, refetch: t, subscriptionPlans: n = [] }) {
Q.jsxs(Not, {
modal: !1,
children: [
- Q.jsx(Lot, {
- asChild: !0,
- children: Q.jsx(Nm, {
+ Q.jsx(Lot, { asChild: !0, onClick: (e) => (e.stopPropagation(), e.preventDefault()), children: Q.jsx(Nm, {
variant: "outline",
size: "sm",
className: "h-8 border-dashed",
diff --git a/scratch/patch_bundle.ps1 b/scratch/patch_bundle.ps1
new file mode 100644
index 0000000..9ed33fb
--- /dev/null
+++ b/scratch/patch_bundle.ps1
@@ -0,0 +1,19 @@
+$filePath = "d:\Development\SingBox-Gopanel\frontend\admin\reverse\output\index-CO3BwsT2.pretty.js"
+
+# Read file
+$content = [System.IO.File]::ReadAllText($filePath)
+
+# Regex patterns (escaping literals to handle parens and braces correctly)
+$t1 = [regex]::Escape("Q.jsx(Lot, {") + "\s+" + [regex]::Escape("asChild: !0,") + "\s+" + [regex]::Escape("children: Q.jsx(Nm, {")
+$r1 = "Q.jsx(Lot, { asChild: !0, onClick: (e) => (e.stopPropagation(), e.preventDefault()), children: Q.jsx(Nm, {"
+
+$t2 = [regex]::Escape("onSelect: (e) => {") + "\s+" + [regex]::Escape("(e.preventDefault(), l());")
+$r2 = "onSelect: (e) => { (e.preventDefault(), e.stopPropagation(), l());"
+
+# Perform replacements
+$content = [System.Text.RegularExpressions.Regex]::Replace($content, $t1, $r1)
+$content = [System.Text.RegularExpressions.Regex]::Replace($content, $t2, $r2)
+
+# Write back
+[System.IO.File]::WriteAllText($filePath, $content)
+Write-Output "Patch applied successfully (Regex Escaped)"