diff --git a/cmd/api/main_entry.go b/cmd/api/main_entry.go index 3d31f47..1b249da 100644 --- a/cmd/api/main_entry.go +++ b/cmd/api/main_entry.go @@ -139,8 +139,7 @@ func registerUserRoutes(v1 *gin.RouterGroup) { user.GET("/real-name-verification/status", handler.PluginRealNameStatus) user.POST("/real-name-verification/submit", handler.PluginRealNameSubmit) user.GET("/user-online-devices/get-ip", handler.PluginUserOnlineDevicesGetIP) - user.POST("/user-add-ipv6-subscription/enable", handler.PluginUserAddIPv6Enable) - user.POST("/user-add-ipv6-subscription/sync-password", handler.PluginUserAddIPv6SyncPassword) + // User IPv6 subscription - read-only status check (enable/disable is admin-only) user.GET("/user-add-ipv6-subscription/check", handler.PluginUserAddIPv6Check) } @@ -295,6 +294,7 @@ func registerAdminRoutesV2(v2 *gin.RouterGroup) { admin.GET("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigFetch) admin.POST("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigSave) admin.POST("/user-add-ipv6-subscription/enable/:userId", handler.AdminIPv6SubscriptionEnable) + admin.POST("/user-add-ipv6-subscription/disable/:userId", handler.AdminIPv6SubscriptionDisable) admin.POST("/user-add-ipv6-subscription/sync-password/:userId", handler.AdminIPv6SubscriptionSyncPassword) } diff --git a/frontend/admin/app.js b/frontend/admin/app.js index e056ef8..1e6342b 100644 --- a/frontend/admin/app.js +++ b/frontend/admin/app.js @@ -173,8 +173,12 @@ const payload = await request(`${cfg.api.onlineDevices}?page=${page}&per_page=20`); state.devices = normalizeListPayload(payload); } else if (state.route === "user-ipv6-subscription") { - const payload = await request(`${cfg.api.ipv6Base}/users?page=${page}&per_page=20`); + const [payload, plans] = await Promise.all([ + request(`${cfg.api.ipv6Base}/users?page=${page}&per_page=20`), + request(cfg.api.plans) + ]); state.ipv6 = normalizeListPayload(payload); + state.plans = toArray(unwrap(plans)); } else if (state.route === "system-config") { state.config = unwrap(await request(cfg.api.adminConfig)); } @@ -337,18 +341,20 @@ } if (action === "node-copy") { - await adminPost(`${cfg.api.adminBase}/server/manage/copy`, { - id: Number(actionEl.getAttribute("data-id")) - }); + const nodeId = Number(actionEl.getAttribute("data-id")); + try { + await adminPost(`${cfg.api.adminBase}/server/manage/copy`, { id: nodeId }); + } catch (e) { /* adminPost already shows error */ } await hydrateRoute(); return; } if (action === "node-delete") { if (confirm("确认删除该节点吗?")) { - await adminPost(`${cfg.api.adminBase}/server/manage/drop`, { - id: Number(actionEl.getAttribute("data-id")) - }); + const nodeId = Number(actionEl.getAttribute("data-id")); + try { + await adminPost(`${cfg.api.adminBase}/server/manage/drop`, { id: nodeId }); + } catch (e) { /* adminPost already shows error */ } await hydrateRoute(); } return; @@ -356,15 +362,20 @@ if (action === "node-toggle-visible") { event.preventDefault(); + event.stopPropagation(); + const nodeId = Number(actionEl.getAttribute("data-id")); const input = actionEl.querySelector("input"); if (!input) { return; } - input.checked = !input.checked; - await adminPost(`${cfg.api.adminBase}/server/manage/update`, { - id: Number(actionEl.getAttribute("data-id")), - show: input.checked ? 1 : 0 - }); + const newShow = !input.checked; + input.checked = newShow; + try { + await adminPost(`${cfg.api.adminBase}/server/manage/update`, { + id: nodeId, + show: newShow ? 1 : 0 + }); + } catch (e) { /* adminPost already shows error */ } await hydrateRoute(); return; } @@ -436,10 +447,20 @@ return; } - if (action === "ipv6-enable") { + if (action === "ipv6-enable-modal") { const userId = actionEl.getAttribute("data-user-id"); - await adminPost(`${cfg.api.ipv6Base}/enable/${userId}`, {}); - await hydrateRoute(); + const userEmail = actionEl.getAttribute("data-email") || ""; + state.modal = { type: "ipv6-enable", data: { userId, email: userEmail } }; + render(); + return; + } + + if (action === "ipv6-disable") { + const userId = actionEl.getAttribute("data-user-id"); + if (confirm("确认关闭该用户的 IPv6 子账号?(软禁用,可恢复)")) { + await adminPost(`${cfg.api.ipv6Base}/disable/${userId}`, {}); + await hydrateRoute(); + } return; } @@ -505,6 +526,14 @@ } if (!target) { + if (action === "ipv6-enable-save") { + const userId = body.user_id; + const planId = Number(body.plan_id) || 0; + await adminPost(`${cfg.api.ipv6Base}/enable/${userId}`, { plan_id: planId }); + state.modal = null; + await hydrateRoute(); + return; + } return; } @@ -953,23 +982,34 @@ } function renderIPv6Manage() { - const rows = state.ipv6.list.map((item) => [ - renderRelationChip(item.id, item.shadow_user_id || "-"), - [ - `${escapeHtml(item.email || "-")}`, - `
${escapeHtml(item.ipv6_email || "-")}
` - ].join(""), - escapeHtml(item.plan_name || "-"), - renderStatus(item.status_label || item.status || "-"), - escapeHtml(formatDate(item.updated_at)), - renderActionRow([ - buttonAction("开通/同步", "ipv6-enable", null, `data-user-id="${item.id}"`), - buttonAction("同步密码", "ipv6-sync-password", null, `data-user-id="${item.id}"`) - ]) - ]); + const rows = state.ipv6.list.map((item) => { + const isActive = item.is_active || item.status === "active"; + const isDisabled = item.status === "disabled"; + const actions = []; + if (isActive) { + actions.push(buttonAction("关闭", "ipv6-disable", null, `data-user-id="${item.id}"`)); + actions.push(buttonAction("同步密码", "ipv6-sync-password", null, `data-user-id="${item.id}"`)); + } else { + actions.push(buttonAction("开通", "ipv6-enable-modal", null, `data-user-id="${item.id}" data-email="${escapeHtml(item.email || "")}"`)); + } + if (isDisabled) { + actions.unshift(buttonAction("重新开通", "ipv6-enable-modal", null, `data-user-id="${item.id}" data-email="${escapeHtml(item.email || "")}"`)); + } + return [ + `${item.id}`, + [ + `${escapeHtml(item.email || "-")}`, + `
${escapeHtml(item.ipv6_email || "-")}
` + ].join(""), + escapeHtml(item.plan_name || "-"), + renderStatus(item.status_label || item.status || "-"), + escapeHtml(formatDate(item.updated_at)), + renderActionRow(actions) + ]; + }); return [ - wrapTable(["主从关系", "账号", "套餐", "状态", "更新时间", "操作"], rows), + wrapTable(["用户ID", "账号", "IPv6 套餐", "状态", "更新时间", "操作"], rows), renderPagination(state.ipv6.pagination) ].join(""); } @@ -1034,7 +1074,9 @@ ? renderGroupForm(modal.data) : modal.type === "route" ? renderRouteForm(modal.data) - : ""; + : modal.type === "ipv6-enable" + ? renderIPv6EnableForm(modal.data) + : ""; return [ '