From 06da23fbbceac037d6a83228dc986cc538195023 Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Fri, 17 Apr 2026 10:46:15 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=90=8E=E7=AB=AFAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/admin/app.js | 175 ++++++++++++++++++++++++++++++++- internal/handler/common.go | 33 +++++++ internal/handler/plugin_api.go | 133 ++++++++++++++++++------- 3 files changed, 302 insertions(+), 39 deletions(-) diff --git a/frontend/admin/app.js b/frontend/admin/app.js index dc30440..889d977 100644 --- a/frontend/admin/app.js +++ b/frontend/admin/app.js @@ -28,7 +28,8 @@ currentTicket: null, dashboard: null, busy: false, - modal: null // { type, data } + modal: null, // { type, data } + configTab: "site" }; boot(); @@ -112,6 +113,8 @@ } else if (state.route === "dashboard-node") { state.dashboard = unwrap(await request(api.dashboardSummary)); state.nodes = toArray(unwrap(await request(api.serverNodes))); + } else if (state.route === "system-config") { + state.config = unwrap(await request(api.adminConfig)); } else if (state.route === "overview") { state.dashboard = unwrap(await request(api.dashboardSummary)); } @@ -133,6 +136,7 @@ if (action === "nav") { window.location.hash = actionEl.getAttribute("data-route") || "overview"; return; } if (action === "refresh") { refreshAll(); return; } if (action === "modal-close") { state.modal = null; render(); return; } + if (action === "config-tab") { state.configTab = actionEl.getAttribute("data-tab"); render(); return; } // Handlers if (action === "plan-add") { state.modal = { type: "plan", data: {} }; render(); return; } @@ -179,6 +183,43 @@ return; } + if (action === "node-add") { state.modal = { type: "node", data: {} }; render(); return; } + if (action === "node-edit") { + var node = state.nodes.find(n => n.id == actionEl.getAttribute("data-id")); + state.modal = { type: "node", data: node }; render(); return; + } + if (action === "node-copy") { + adminPost(api.adminBase + "/server/manage/copy", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute); + return; + } + if (action === "node-delete") { + if (!confirm("Are you sure?")) return; + adminPost(api.adminBase + "/server/manage/drop", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute); + return; + } + + if (action === "group-add") { state.modal = { type: "group", data: {} }; render(); return; } + if (action === "group-edit") { + var group = state.groups.find(g => g.id == actionEl.getAttribute("data-id")); + state.modal = { type: "group", data: group }; render(); return; + } + if (action === "group-delete") { + if (!confirm("Are you sure?")) return; + adminPost(api.adminBase + "/server/group/drop", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute); + return; + } + + if (action === "route-add") { state.modal = { type: "route", data: {} }; render(); return; } + if (action === "route-edit") { + var route = state.routes.find(r => r.id == actionEl.getAttribute("data-id")); + state.modal = { type: "route", data: route }; render(); return; + } + if (action === "route-delete") { + if (!confirm("Are you sure?")) return; + adminPost(api.adminBase + "/server/route/drop", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute); + return; + } + // Previous handlers if (action === "approve-all") { adminPost(api.realnameBase + "/approve-all", {}).then(hydrateRoute); return; } if (action === "sync-all") { adminPost(api.realnameBase + "/sync-all", {}).then(hydrateRoute); return; } @@ -218,6 +259,22 @@ adminPost(api.adminBase + "/user/update", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); }); return; } + if (action === "config-save") { + adminPost(api.adminBase + "/config/save", serializeForm(form)).then(() => { hydrateRoute(); }); + return; + } + if (action === "node-save") { + adminPost(api.adminBase + "/server/manage/save", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); }); + return; + } + if (action === "group-save") { + adminPost(api.adminBase + "/server/group/save", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); }); + return; + } + if (action === "route-save") { + adminPost(api.adminBase + "/server/route/save", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); }); + return; + } } function render() { @@ -241,6 +298,9 @@ if (m.type === "plan") html += renderPlanForm(m.data); if (m.type === "user") html += renderUserForm(m.data); if (m.type === "coupon") html += renderCouponForm(m.data); + if (m.type === "node") html += renderNodeForm(m.data); + if (m.type === "group") html += renderGroupForm(m.data); + if (m.type === "route") html += renderRouteForm(m.data); html += ''; return html; } @@ -405,6 +465,7 @@ if (route === "coupon-manage") return renderCouponManage(); if (route === "user-manage") return renderUserManage(); if (route === "ticket-manage") return renderTicketManage(); + if (route === "system-config") return renderSystemConfig(); if (route === "realname") return renderRealName(); if (route === "user-online-devices") return renderOnlineDevices(); return renderOverview(); @@ -536,7 +597,7 @@ function renderNodeManage() { var rows = state.nodes || []; return [ - '
', + '
', '
', renderTable(["ID", "Name/Host", "Type", "Rate", "Visibility", "Actions"], rows.map(function(row) { return [ @@ -545,7 +606,7 @@ escapeHtml(row.type), '' + row.rate + 'x', renderStatus(row.show ? "visible" : "hidden"), - '
' + '
' ]; })), '
' @@ -587,6 +648,72 @@ ].join(""); } + function renderNodeGroup() { + var rows = state.groups || []; + return [ + '
', + '
', + renderTable(["ID", "Group Name", "Node Count", "User Count", "Actions"], rows.map(function(row) { + return [ + '' + row.id + '', + '' + escapeHtml(row.name) + '', + '' + (row.server_count || 0) + '', + '' + (row.user_count || 0) + '', + '
' + ]; + })), + '
' + ].join(""); + } + + function renderNodeRoute() { + var rows = state.routes || []; + return [ + '
', + '
', + renderTable(["ID", "Remarks", "Match", "Action", "Actions"], rows.map(function(row) { + return [ + '' + row.id + '', + '' + escapeHtml(row.remarks) + '', + '
' + (row.match || []).join(", ") + '
', + '' + row.action + '', + '
' + ]; + })), + '
' + ].join(""); + } + + function renderSystemConfig() { + var cfg = state.config || {}; + var tab = state.configTab || "site"; + var sections = ["site", "subscribe", "server", "email", "telegram", "safe"]; + + var tabData = cfg[tab] || {}; + + return [ + '
', + sections.map(s => '').join(""), + '
', + '
', + '
', + Object.keys(tabData).map(key => { + var val = tabData[key]; + var label = key.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase()); + var input = ''; + if (typeof val === "boolean") { + input = ''; + } + return '
' + input + '
'; + }).join(""), + '
', + '
', + '', + '
', + '
' + ].join(""); + } + // Helpers function statCard(title, value, hint) { return [ @@ -693,6 +820,48 @@ }); return r; } + function renderNodeForm(d) { + return [ + '

' + (d.id ? "Edit Node" : "Create Node") + '

', + '
', + '', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '
', + '', + '
' + ].join(""); + } + + function renderGroupForm(d) { + return [ + '

' + (d.id ? "Edit Group" : "Create Group") + '

', + '
', + '', + '
', + '', + '
' + ].join(""); + } + + function renderRouteForm(d) { + return [ + '

' + (d.id ? "Edit Route" : "Create Route") + '

', + '
', + '', + '
', + '
', + '', + '
' + ].join(""); + } + function getRouteTitle(r) { return r.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()); } function getRouteDescription(r) { return "SingBox Gopanel integrated module management."; } diff --git a/internal/handler/common.go b/internal/handler/common.go index efccceb..b0cfa22 100644 --- a/internal/handler/common.go +++ b/internal/handler/common.go @@ -234,3 +234,36 @@ func loadTicketMessageCountMap(ticketIDs []int) map[int]int64 { } return result } + +func parsePositiveInt(raw string, defaultValue int) int { + value, err := strconv.Atoi(strings.TrimSpace(raw)) + if err != nil || value <= 0 { + return defaultValue + } + return value +} + +func calculateLastPage(total int64, perPage int) int { + if perPage <= 0 { + return 1 + } + last := int((total + int64(perPage) - 1) / int64(perPage)) + if last == 0 { + return 1 + } + return last +} + +func formatUnixValue(value int64) string { + if value <= 0 { + return "-" + } + return time.Unix(value, 0).Format("2006-01-02 15:04:05") +} + +func formatTimeValue(value *time.Time) string { + if value == nil { + return "-" + } + return value.Format("2006-01-02 15:04:05") +} diff --git a/internal/handler/plugin_api.go b/internal/handler/plugin_api.go index a066375..407a2ae 100644 --- a/internal/handler/plugin_api.go +++ b/internal/handler/plugin_api.go @@ -1,7 +1,6 @@ package handler import ( - "strconv" "strings" "time" "xboard-go/internal/database" @@ -198,11 +197,101 @@ func PluginUserAddIPv6SyncPassword(c *gin.Context) { func AdminConfigFetch(c *gin.Context) { Success(c, gin.H{ - "app_name": service.MustGetString("app_name", "XBoard"), - "app_url": service.GetAppURL(), - "secure_path": service.GetAdminSecurePath(), - "server_pull_interval": service.MustGetInt("server_pull_interval", 60), - "server_push_interval": service.MustGetInt("server_push_interval", 60), + "invite": gin.H{ + "invite_force": service.MustGetBool("invite_force", false), + "invite_commission": service.MustGetInt("invite_commission", 10), + "invite_gen_limit": service.MustGetInt("invite_gen_limit", 5), + "invite_never_expire": service.MustGetBool("invite_never_expire", false), + "commission_first_time_enable": service.MustGetBool("commission_first_time_enable", true), + "commission_auto_check_enable": service.MustGetBool("commission_auto_check_enable", true), + "commission_withdraw_limit": service.MustGetInt("commission_withdraw_limit", 100), + "withdraw_close_enable": service.MustGetBool("withdraw_close_enable", false), + "commission_distribution_enable": service.MustGetBool("commission_distribution_enable", false), + "commission_distribution_l1": service.MustGetInt("commission_distribution_l1", 0), + "commission_distribution_l2": service.MustGetInt("commission_distribution_l2", 0), + "commission_distribution_l3": service.MustGetInt("commission_distribution_l3", 0), + }, + "site": gin.H{ + "logo": service.MustGetString("logo", ""), + "force_https": service.MustGetBool("force_https", false), + "stop_register": service.MustGetBool("stop_register", false), + "app_name": service.MustGetString("app_name", "XBoard"), + "app_description": service.MustGetString("app_description", "XBoard is best!"), + "app_url": service.GetAppURL(), + "subscribe_url": service.MustGetString("subscribe_url", ""), + "try_out_enable": service.MustGetBool("try_out_enable", false), + "try_out_plan_id": service.MustGetInt("try_out_plan_id", 0), + "try_out_hour": service.MustGetInt("try_out_hour", 1), + "tos_url": service.MustGetString("tos_url", ""), + "currency": service.MustGetString("currency", "CNY"), + "currency_symbol": service.MustGetString("currency_symbol", "¥"), + "ticket_must_wait_reply": service.MustGetBool("ticket_must_wait_reply", false), + }, + "subscribe": gin.H{ + "plan_change_enable": service.MustGetBool("plan_change_enable", true), + "reset_traffic_method": service.MustGetInt("reset_traffic_method", 0), + "surplus_enable": service.MustGetBool("surplus_enable", true), + "new_order_event_id": service.MustGetInt("new_order_event_id", 0), + "renew_order_event_id": service.MustGetInt("renew_order_event_id", 0), + "change_order_event_id": service.MustGetInt("change_order_event_id", 0), + "show_info_to_server_enable": service.MustGetBool("show_info_to_server_enable", false), + "show_protocol_to_server_enable": service.MustGetBool("show_protocol_to_server_enable", false), + "subscribe_path": service.MustGetString("subscribe_path", "s"), + }, + "frontend": gin.H{ + "frontend_theme": service.MustGetString("frontend_theme", "Xboard"), + "frontend_theme_sidebar": service.MustGetString("frontend_theme_sidebar", "light"), + "frontend_theme_header": service.MustGetString("frontend_theme_header", "dark"), + "frontend_theme_color": service.MustGetString("frontend_theme_color", "default"), + "frontend_background_url": service.MustGetString("frontend_background_url", ""), + }, + "server": gin.H{ + "server_token": service.MustGetString("server_token", ""), + "server_pull_interval": service.MustGetInt("server_pull_interval", 60), + "server_push_interval": service.MustGetInt("server_push_interval", 60), + "device_limit_mode": service.MustGetInt("device_limit_mode", 0), + "server_ws_enable": service.MustGetBool("server_ws_enable", true), + "server_ws_url": service.MustGetString("server_ws_url", ""), + }, + "email": gin.H{ + "email_template": service.MustGetString("email_template", "default"), + "email_host": service.MustGetString("email_host", ""), + "email_port": service.MustGetString("email_port", ""), + "email_username": service.MustGetString("email_username", ""), + "email_password": service.MustGetString("email_password", ""), + "email_encryption": service.MustGetString("email_encryption", ""), + "email_from_address": service.MustGetString("email_from_address", ""), + "remind_mail_enable": service.MustGetBool("remind_mail_enable", false), + }, + "telegram": gin.H{ + "telegram_bot_enable": service.MustGetBool("telegram_bot_enable", false), + "telegram_bot_token": service.MustGetString("telegram_bot_token", ""), + "telegram_webhook_url": service.MustGetString("telegram_webhook_url", ""), + "telegram_discuss_link": service.MustGetString("telegram_discuss_link", ""), + }, + "app": gin.H{ + "windows_version": service.MustGetString("windows_version", ""), + "windows_download_url": service.MustGetString("windows_download_url", ""), + "macos_version": service.MustGetString("macos_version", ""), + "macos_download_url": service.MustGetString("macos_download_url", ""), + "android_version": service.MustGetString("android_version", ""), + "android_download_url": service.MustGetString("android_download_url", ""), + }, + "safe": gin.H{ + "email_verify": service.MustGetBool("email_verify", false), + "safe_mode_enable": service.MustGetBool("safe_mode_enable", false), + "secure_path": service.GetAdminSecurePath(), + "email_whitelist_enable": service.MustGetBool("email_whitelist_enable", false), + "email_whitelist_suffix": service.MustGetString("email_whitelist_suffix", ""), + "email_gmail_limit_enable": service.MustGetBool("email_gmail_limit_enable", false), + "captcha_enable": service.MustGetBool("captcha_enable", false), + "register_limit_by_ip_enable": service.MustGetBool("register_limit_by_ip_enable", false), + "register_limit_count": service.MustGetInt("register_limit_count", 3), + "register_limit_expire": service.MustGetInt("register_limit_expire", 60), + "password_limit_enable": service.MustGetBool("password_limit_enable", true), + "password_limit_count": service.MustGetInt("password_limit_count", 5), + "password_limit_expire": service.MustGetInt("password_limit_expire", 60), + }, }) } @@ -211,39 +300,11 @@ func AdminSystemStatus(c *gin.Context) { "server_time": time.Now().Unix(), "app_name": service.MustGetString("app_name", "XBoard"), "app_url": service.GetAppURL(), + "version": "1.0.0", + "go_version": "1.21+", }) } -func parsePositiveInt(raw string, defaultValue int) int { - value, err := strconv.Atoi(strings.TrimSpace(raw)) - if err != nil || value <= 0 { - return defaultValue - } - return value -} -func calculateLastPage(total int64, perPage int) int { - if perPage <= 0 { - return 1 - } - last := int((total + int64(perPage) - 1) / int64(perPage)) - if last == 0 { - return 1 - } - return last -} -func formatUnixValue(value int64) string { - if value <= 0 { - return "-" - } - return time.Unix(value, 0).Format("2006-01-02 15:04:05") -} - -func formatTimeValue(value *time.Time) string { - if value == nil { - return "-" - } - return value.Format("2006-01-02 15:04:05") -}