diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 00babaf..915667b 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: docker.gitea.com/runner-images:ubuntu-22.04 strategy: fail-fast: false matrix: diff --git a/api.exe b/api.exe deleted file mode 100644 index 16736da..0000000 Binary files a/api.exe and /dev/null differ diff --git a/cmd/api/main.go b/cmd/api/main.go index 9690538..6041ec2 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -134,10 +134,6 @@ func main() { admin.POST("/order/assign", handler.AdminOrderAssign) admin.POST("/order/update", handler.AdminOrderUpdate) - admin.POST("/coupon/fetch", handler.AdminCouponsFetch) - admin.POST("/coupon/save", handler.AdminCouponSave) - admin.POST("/coupon/drop", handler.AdminCouponDrop) - admin.POST("/ticket/fetch", handler.AdminTicketsFetch) // Knowledge Base @@ -149,14 +145,6 @@ func main() { knowledgeGrp.POST("/sort", handler.AdminKnowledgeSort) } - // Gift Card - giftCardGrp := admin.Group("/gift-card") - { - giftCardGrp.GET("/fetch", handler.AdminGiftCardFetch) - giftCardGrp.POST("/save", handler.AdminGiftCardSave) - giftCardGrp.POST("/generate", handler.AdminGiftCardGenerate) - } - // Traffic Reset trafficResetGrp := admin.Group("/traffic-reset") { @@ -190,14 +178,6 @@ func main() { admin.POST("/server/route/save", handler.AdminServerRouteSave) admin.POST("/server/route/drop", handler.AdminServerRouteDrop) - // Payment - admin.GET("/payment/fetch", handler.AdminPaymentFetch) - admin.POST("/payment/save", handler.AdminPaymentSave) - admin.POST("/payment/drop", handler.AdminPaymentDrop) - admin.POST("/payment/show", handler.AdminPaymentShow) - admin.POST("/payment/sort", handler.AdminPaymentSort) - admin.GET("/payment/getPaymentMethods", handler.AdminGetPaymentMethods) - // Notice admin.GET("/notice/fetch", handler.AdminNoticeFetch) admin.POST("/notice/save", handler.AdminNoticeSave) diff --git a/cmd/api/main_entry.go b/cmd/api/main_entry.go index c3aaa41..841c340 100644 --- a/cmd/api/main_entry.go +++ b/cmd/api/main_entry.go @@ -4,6 +4,7 @@ import ( "log" "os" "path/filepath" + "strings" "xboard-go/internal/config" "xboard-go/internal/database" "xboard-go/internal/handler" @@ -188,12 +189,16 @@ func registerAdminRoutesV2(v2 *gin.RouterGroup) { admin.GET("/config/fetch", handler.AdminConfigFetch) admin.POST("/config/save", handler.AdminConfigSave) + admin.GET("/config/getEmailTemplate", handler.AdminGetEmailTemplate) + admin.POST("/config/testSendMail", handler.AdminTestSendMail) + admin.POST("/config/setTelegramWebhook", handler.AdminSetTelegramWebhook) admin.GET("/dashboard/summary", handler.AdminDashboardSummary) admin.GET("/stat/getStats", handler.AdminDashboardSummary) admin.GET("/stat/getOverride", handler.AdminDashboardSummary) admin.GET("/stat/getTrafficRank", handler.AdminGetTrafficRank) admin.GET("/stat/getOrder", handler.AdminGetOrderStats) + admin.POST("/stat/getStatUser", handler.AdminGetStatUser) admin.GET("/system/getSystemStatus", handler.AdminSystemStatus) admin.GET("/system/getQueueStats", handler.AdminSystemQueueStats) @@ -222,25 +227,40 @@ func registerAdminRoutesV2(v2 *gin.RouterGroup) { admin.POST("/plan/sort", handler.AdminPlanSort) admin.GET("/order/fetch", handler.AdminOrdersFetch) + admin.POST("/order/fetch", handler.AdminOrdersFetch) + admin.POST("/order/detail", handler.AdminOrderDetail) admin.POST("/order/paid", handler.AdminOrderPaid) admin.POST("/order/cancel", handler.AdminOrderCancel) - - admin.GET("/coupon/fetch", handler.AdminCouponsFetch) - admin.POST("/coupon/save", handler.AdminCouponSave) - admin.POST("/coupon/drop", handler.AdminCouponDrop) + admin.POST("/order/update", handler.AdminOrderUpdate) + admin.POST("/order/assign", handler.AdminOrderAssign) admin.GET("/user/fetch", handler.AdminUsersFetch) + admin.POST("/user/fetch", handler.AdminUsersFetch) admin.POST("/user/update", handler.AdminUserUpdate) admin.POST("/user/ban", handler.AdminUserBan) + admin.POST("/user/resetSecret", handler.AdminUserResetSecret) + admin.POST("/user/sendMail", handler.AdminUserSendMail) + admin.POST("/user/destroy", handler.AdminUserDelete) admin.POST("/user/resetTraffic", handler.AdminUserResetTraffic) admin.POST("/user/drop", handler.AdminUserDelete) admin.GET("/ticket/fetch", handler.AdminTicketsFetch) - admin.GET("/gift-card/status", handler.AdminGiftCardStatus) + admin.POST("/ticket/fetch", handler.AdminTicketsFetch) admin.GET("/traffic-reset/fetch", handler.AdminTrafficResetFetch) admin.GET("/traffic-reset/logs", handler.AdminTrafficResetFetch) admin.POST("/traffic-reset/reset-user", handler.AdminTrafficResetUser) admin.GET("/traffic-reset/user/:id/history", handler.AdminTrafficResetUserHistory) + admin.GET("/notice/fetch", handler.AdminNoticeFetch) + admin.POST("/notice/save", handler.AdminNoticeSave) + admin.POST("/notice/drop", handler.AdminNoticeDrop) + admin.POST("/notice/show", handler.AdminNoticeShow) + admin.POST("/notice/sort", handler.AdminNoticeSort) + + admin.GET("/knowledge/fetch", handler.AdminKnowledgeFetch) + admin.POST("/knowledge/save", handler.AdminKnowledgeSave) + admin.POST("/knowledge/drop", handler.AdminKnowledgeDrop) + admin.POST("/knowledge/sort", handler.AdminKnowledgeSort) + // Integrated Admin Features admin.GET("/realname/records", handler.PluginRealNameRecords) admin.POST("/realname/clear-cache", handler.PluginRealNameClearCache) @@ -249,6 +269,9 @@ func registerAdminRoutesV2(v2 *gin.RouterGroup) { admin.POST("/realname/sync-all", handler.PluginRealNameSyncAll) admin.POST("/realname/approve-all", handler.PluginRealNameApproveAll) admin.GET("/user-online-devices/users", handler.PluginUserOnlineDevicesUsers) + admin.GET("/user-add-ipv6-subscription/users", handler.AdminIPv6SubscriptionUsers) + admin.POST("/user-add-ipv6-subscription/enable/:userId", handler.AdminIPv6SubscriptionEnable) + admin.POST("/user-add-ipv6-subscription/sync-password/:userId", handler.AdminIPv6SubscriptionSyncPassword) } func registerWebRoutes(router *gin.Engine) { @@ -267,5 +290,14 @@ func registerWebRoutes(router *gin.Engine) { router.GET("/dashboard", handler.UserThemePage) router.GET(securePath, handler.AdminAppPage) router.GET(securePath+"/", handler.AdminAppPage) + router.GET(securePath+"/plugin-panel/:kind", handler.AdminPluginPanelPage) router.GET(securePath+"/plugins/:plugin", handler.AdminAppPage) + router.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + if path == securePath || strings.HasPrefix(path, securePath+"/") { + handler.AdminAppPage(c) + return + } + c.JSON(404, gin.H{"message": "not found"}) + }) } diff --git a/frontend/admin/app.css b/frontend/admin/app.css index 4d0233c..a3c8370 100644 --- a/frontend/admin/app.css +++ b/frontend/admin/app.css @@ -248,6 +248,26 @@ tbody tr:hover { border-bottom: 1px solid var(--border); } +.relation-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border: 1px solid var(--border); + border-radius: 999px; + background: #fafafa; + white-space: nowrap; +} + +.relation-chip code { + font-size: 12px; +} + +.relation-arrow { + color: var(--accent); + font-weight: 700; +} + /* Buttons */ .btn { display: inline-flex; diff --git a/frontend/admin/app.js b/frontend/admin/app.js index 13d4613..e056ef8 100644 --- a/frontend/admin/app.js +++ b/frontend/admin/app.js @@ -11,7 +11,7 @@ const state = { token: readToken(), user: null, - route: normalizeRoute(readRoute()), + route: "overview", busy: false, message: "", messageType: "info", @@ -38,6 +38,7 @@ tickets: { list: [], pagination: null }, realname: { list: [], pagination: null }, devices: { list: [], pagination: null }, + ipv6: { list: [], pagination: null }, expandedNodes: new Set() }; @@ -54,9 +55,12 @@ "ticket-manage": { title: "工单中心", description: "查看用户工单和处理状态。" }, realname: { title: "实名认证", description: "审核实名记录和同步状态。" }, "user-online-devices": { title: "在线设备", description: "查看用户在线 IP 和设备分布。" }, + "user-ipv6-subscription": { title: "IPv6 子账号", description: "管理 IPv6 阴影账号与密码同步。" }, "system-config": { title: "系统设置", description: "编辑站点、订阅和安全参数。" } }; + state.route = normalizeRoute(readRoute()); + boot(); async function boot() { @@ -146,7 +150,7 @@ state.plans = toArray(unwrap(plans)); state.groups = toArray(unwrap(groups)); } else if (state.route === "order-manage") { - const payload = unwrap(await request(`${cfg.api.orders}?page=${page}&per_page=20`)); + const payload = await request(`${cfg.api.orders}?page=${page}&per_page=20`); state.orders = normalizeListPayload(payload); } else if (state.route === "coupon-manage") { state.coupons = toArray(unwrap(await request(cfg.api.coupons))); @@ -156,18 +160,21 @@ request(cfg.api.plans), request(cfg.api.serverGroups) ]); - state.users = normalizeListPayload(unwrap(users)); + state.users = normalizeListPayload(users); state.plans = toArray(unwrap(plans)); state.groups = toArray(unwrap(groups)); } else if (state.route === "ticket-manage") { - const payload = unwrap(await request(`${cfg.api.tickets}?page=${page}&per_page=20`)); + const payload = await request(`${cfg.api.tickets}?page=${page}&per_page=20`); state.tickets = normalizeListPayload(payload); } else if (state.route === "realname") { - const payload = unwrap(await request(`${cfg.api.realnameBase}/records?page=${page}&per_page=20`)); + const payload = await request(`${cfg.api.realnameBase}/records?page=${page}&per_page=20`); state.realname = normalizeListPayload(payload, "data"); } else if (state.route === "user-online-devices") { - const payload = unwrap(await request(`${cfg.api.onlineDevices}?page=${page}&per_page=20`)); + 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`); + state.ipv6 = normalizeListPayload(payload); } else if (state.route === "system-config") { state.config = unwrap(await request(cfg.api.adminConfig)); } @@ -426,6 +433,21 @@ if (action === "sync-all") { await adminPost(`${cfg.api.realnameBase}/sync-all`, {}); await hydrateRoute(); + return; + } + + if (action === "ipv6-enable") { + const userId = actionEl.getAttribute("data-user-id"); + await adminPost(`${cfg.api.ipv6Base}/enable/${userId}`, {}); + await hydrateRoute(); + return; + } + + if (action === "ipv6-sync-password") { + const userId = actionEl.getAttribute("data-user-id"); + await adminPost(`${cfg.api.ipv6Base}/sync-password/${userId}`, {}); + await hydrateRoute(); + return; } } @@ -441,7 +463,7 @@ if (action === "login") { try { setBusy(true); - const payload = unwrap(await request("/api/v1/passport/auth/login", { + const payload = unwrap(await request("/api/v2/passport/auth/login", { method: "POST", auth: false, body: serializeForm(form) @@ -571,6 +593,7 @@ navItem("user-manage", "用户管理", "账号、订阅、流量"), navItem("realname", "实名认证", "实名审核"), navItem("user-online-devices", "在线设备", "在线 IP 与会话"), + navItem("user-ipv6-subscription", "IPv6 子账号", "开通与密码同步"), navItem("ticket-manage", "工单中心", "用户支持") ]), renderSidebarGroup("system", "系统", [ @@ -643,6 +666,7 @@ if (state.route === "ticket-manage") return renderTicketManage(); if (state.route === "realname") return renderRealnameManage(); if (state.route === "user-online-devices") return renderOnlineDevices(); + if (state.route === "user-ipv6-subscription") return renderIPv6Manage(); if (state.route === "system-config") return renderSystemConfig(); return renderOverview(); } @@ -719,10 +743,13 @@ const toggle = hasChildren ? `` : ''; + const relationCell = isChild && node.parent_id + ? `${escapeHtml(node.id)}${escapeHtml(node.parent_id)}` + : `${node.id}`; return { className: isChild ? "node-child" : "", cells: [ - `${node.id}`, + relationCell, [ '
', toggle, @@ -843,10 +870,13 @@ function renderUserManage() { const rows = state.users.list.map((user) => [ - `${user.id}`, + user.parent_id ? renderRelationChip(user.parent_id, user.id) : `${user.id}`, [ `${escapeHtml(user.email || "-")}`, - user.online_ip ? `
在线 IP: ${escapeHtml(user.online_ip)}
` : "" + user.parent_id ? `
${renderRelationChip(user.parent_id, user.id)}
` : "", + user.online_ip ? `
在线 IP: ${escapeHtml(user.online_ip)}
` : "", + `
实名: ${escapeHtml(user.realname_label || user.realname_status || "-")}
`, + user.ipv6_enabled ? `
IPv6: ${renderRelationChip(user.id, user.ipv6_shadow_id)}
` : "" ].join(""), renderStatus(user.banned ? "banned" : "active"), escapeHtml(`${formatTraffic((user.u || 0) + (user.d || 0))} / ${formatTraffic(user.transfer_enable)}`), @@ -922,6 +952,28 @@ ].join(""); } + 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}"`) + ]) + ]); + + return [ + wrapTable(["主从关系", "账号", "套餐", "状态", "更新时间", "操作"], rows), + renderPagination(state.ipv6.pagination) + ].join(""); + } + function renderSystemConfig() { const sections = ["site", "subscribe", "server", "safe", "invite", "frontend"]; const activeTab = sections.includes(state.configTab) ? state.configTab : "site"; @@ -1163,6 +1215,10 @@ return `
${items.join("")}
`; } + function renderRelationChip(fromId, toId) { + return `${escapeHtml(fromId)}${escapeHtml(toId)}`; + } + function hiddenField(name, value) { return ``; } @@ -1432,9 +1488,9 @@ const text = String(status || "-"); const normalized = text.toLowerCase(); let type = "danger"; - if (/(ok|active|visible|approved|paid|answered|online)/.test(normalized)) { + if (/(ok|active|visible|approved|paid|answered|online|enabled)/.test(normalized)) { type = "ok"; - } else if (/(pending|warn|unverified)/.test(normalized)) { + } else if (/(pending|warn|unverified|eligible|ready)/.test(normalized)) { type = "warn"; } return `${escapeHtml(text)}`; diff --git a/frontend/admin/main.js b/frontend/admin/main.js new file mode 100644 index 0000000..a38caf0 --- /dev/null +++ b/frontend/admin/main.js @@ -0,0 +1,54 @@ +const settings = window.settings || {}; +const assetNonce = window.__ADMIN_ASSET_NONCE__ || String(Date.now()); +const securePath = String(settings.secure_path || "admin").replace(/^\/+/, ""); +const adminBase = `/api/v2/${securePath}`; + +window.ADMIN_APP_CONFIG = { + title: settings.title || "XBoard Admin", + version: settings.version || "1.0.0", + securePath, + baseUrl: settings.base_url || window.location.origin, + api: { + adminBase, + adminConfig: `${adminBase}/config/fetch`, + dashboardSummary: `${adminBase}/dashboard/summary`, + systemStatus: `${adminBase}/system/getSystemStatus`, + serverNodes: `${adminBase}/server/manage/getNodes`, + serverGroups: `${adminBase}/server/group/fetch`, + serverRoutes: `${adminBase}/server/route/fetch`, + plans: `${adminBase}/plan/fetch`, + orders: `${adminBase}/order/fetch`, + coupons: `${adminBase}/coupon/fetch`, + users: `${adminBase}/user/fetch`, + tickets: `${adminBase}/ticket/fetch`, + realnameBase: `${adminBase}/realname`, + onlineDevices: `${adminBase}/user-online-devices/users`, + ipv6Base: `${adminBase}/user-add-ipv6-subscription`, + }, +}; + +document.documentElement.dataset.adminExecutionMode = "main-app"; + +function showBootError(error) { + console.error("Failed to boot admin app", error); + const root = document.getElementById("admin-app"); + if (root) { + root.innerHTML = + `
Admin app failed to load.
${String( + error && error.message ? error.message : error || "Unknown error", + )}
`; + } +} + +window.addEventListener("error", (event) => { + if (!event || !event.error) { + return; + } + showBootError(event.error); +}); + +const script = document.createElement("script"); +script.src = `/admin-assets/app.js?v=${encodeURIComponent(assetNonce)}`; +script.defer = true; +script.onerror = () => showBootError(new Error("Failed to load /admin-assets/app.js")); +document.body.appendChild(script); diff --git a/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js b/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js index 7fba4be..ba0312d 100644 --- a/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js +++ b/frontend/admin/reverse/output/index-CO3BwsT2.pretty.js @@ -195337,21 +195337,6 @@ const DQe = { path: "subscribe-template", element: Q.jsx(LQe, {}) }, ], }, - { - path: "payment", - lazy: async () => ({ - Component: ( - await xp( - async () => { - const { default: e } = await Promise.resolve().then(() => kXt); - return { default: e }; - }, - void 0, - import.meta.url, - ) - ).default, - }), - }, { path: "plugin", lazy: async () => ({ @@ -195499,36 +195484,6 @@ const DQe = ).default, }), }, - { - path: "coupon", - lazy: async () => ({ - Component: ( - await xp( - async () => { - const { default: e } = await Promise.resolve().then(() => z3t); - return { default: e }; - }, - void 0, - import.meta.url, - ) - ).default, - }), - }, - { - path: "gift-card", - lazy: async () => ({ - Component: ( - await xp( - async () => { - const { default: e } = await Promise.resolve().then(() => d6t); - return { default: e }; - }, - void 0, - import.meta.url, - ) - ).default, - }), - }, ], }, { @@ -195565,6 +195520,18 @@ const DQe = ).default, }), }, + { + path: "realname", + lazy: async () => ({ Component: codexNativeRealnamePage }), + }, + { + path: "online-devices", + lazy: async () => ({ Component: codexNativeOnlineDevicesPage }), + }, + { + path: "ipv6-subscription", + lazy: async () => ({ Component: codexNativeIPv6Page }), + }, { path: "traffic-reset-logs", lazy: async () => ({ @@ -233443,17 +233410,11 @@ var Zst = { href: "/config/system", icon: Q.jsx(Kf, { size: 18 }), }, - { - title: "nav:pluginManagement", - label: "", - href: "/config/plugin", - icon: Q.jsx(Fat, { size: 18 }), - }, { title: "nav:themeConfig", label: "", href: "/config/theme", - icon: Q.jsx(im, { size: 18 }), + icon: Q.jsx(Kf, { size: 18 }), }, { title: "nav:noticeManagement", @@ -233461,12 +233422,6 @@ var Zst = { href: "/config/notice", icon: Q.jsx(pm, { size: 18 }), }, - { - title: "nav:paymentConfig", - label: "", - href: "/config/payment", - icon: Q.jsx(tm, { size: 18 }), - }, { title: "nav:knowledgeManagement", label: "", @@ -233519,18 +233474,6 @@ var Zst = { href: "/finance/order", icon: Q.jsx(tm, { size: 18 }), }, - { - title: "nav:couponManagement", - label: "", - href: "/finance/coupon", - icon: Q.jsx(rm, { size: 18 }), - }, - { - title: "nav:giftCardManagement", - label: "", - href: "/finance/gift-card", - icon: Q.jsx(lm, { size: 18 }), - }, ], }, { @@ -233551,6 +233494,24 @@ var Zst = { href: "/user/ticket", icon: Q.jsx(xm, { size: 18 }), }, + { + title: "实名认证", + label: "", + href: "/user/realname", + icon: Q.jsx(xm, { size: 18 }), + }, + { + title: "在线 IP 统计", + label: "", + href: "/user/online-devices", + icon: Q.jsx(xm, { size: 18 }), + }, + { + title: "IPv6 子账号", + label: "", + href: "/user/ipv6-subscription", + icon: Q.jsx(xm, { size: 18 }), + }, ], }, ]; @@ -264036,26 +263997,6 @@ function x$t({ className: e }) { return Q.jsxs("div", { className: Rf("grid gap-4 md:grid-cols-2 lg:grid-cols-4", e), children: [ - Q.jsx(y$t, { - title: n("dashboard:stats.todayIncome"), - value: DS(i.todayIncome), - icon: Q.jsx(Jst, { className: "h-4 w-4 text-emerald-500" }), - trend: { - value: i.dayIncomeGrowth, - label: n("dashboard:stats.vsYesterday"), - isPositive: i.dayIncomeGrowth > 0, - }, - }), - Q.jsx(y$t, { - title: n("dashboard:stats.monthlyIncome"), - value: DS(i.currentMonthIncome), - icon: Q.jsx(rat, { className: "h-4 w-4 text-blue-500" }), - trend: { - value: i.monthIncomeGrowth, - label: n("dashboard:stats.vsLastMonth"), - isPositive: i.monthIncomeGrowth > 0, - }, - }), Q.jsx(y$t, { title: n("dashboard:stats.pendingTickets"), value: i.ticketPendingTotal, @@ -264072,38 +264013,6 @@ function x$t({ className: e }) { onClick: () => t("/user/ticket"), highlight: i.ticketPendingTotal > 0, }), - Q.jsx(y$t, { - title: n("dashboard:stats.pendingCommission"), - value: i.commissionPendingTotal, - icon: Q.jsx(oat, { - className: Rf( - "h-4 w-4", - i.commissionPendingTotal > 0 ? "text-blue-500" : "text-muted-foreground", - ), - }), - description: - i.commissionPendingTotal > 0 - ? n("dashboard:stats.hasPendingCommission") - : n("dashboard:stats.noPendingCommission"), - onClick: () => { - const e = new URLSearchParams(); - (e.set("commission_status", c$t.PENDING.toString()), - e.set("status", o$t.COMPLETED.toString()), - e.set("commission_balance", "gt:0"), - t(`/finance/order?${e.toString()}`)); - }, - highlight: i.commissionPendingTotal > 0, - }), - Q.jsx(y$t, { - title: n("dashboard:stats.monthlyNewUsers"), - value: i.currentMonthNewUsers, - icon: Q.jsx(dlt, { className: "h-4 w-4 text-blue-500" }), - trend: { - value: i.userGrowth, - label: n("dashboard:stats.vsLastMonth"), - isPositive: i.userGrowth > 0, - }, - }), Q.jsx(y$t, { title: n("dashboard:stats.totalUsers"), value: i.totalUsers, @@ -269438,7 +269347,7 @@ const tGt = Object.freeze( className: "space-y-6", children: Q.jsxs("div", { className: "grid gap-6", - children: [Q.jsx(x$t, {}), Q.jsx(t$t, {}), Q.jsx(dqt, {}), Q.jsx(eGt, {})], + children: [Q.jsx(x$t, {}), Q.jsx(dqt, {}), Q.jsx(eGt, {})], }), }), }), @@ -293836,23 +293745,27 @@ const I5t = { Q.jsxs("span", { className: "flex flex-col items-start gap-0.5 leading-tight", children: [ - Q.jsx("span", { className: "flex items-center gap-0.5", children: i ?? n }), + e.parent + ? Q.jsxs("span", { + className: "flex items-center gap-1", + children: [ + Q.jsx("span", { children: n }), + Q.jsx("span", { + className: "text-sm text-muted-foreground/30", + children: "\u2192", + }), + Q.jsx("span", { children: e.parent_id ?? e.parent?.id }), + ], + }) + : Q.jsx("span", { + className: "flex items-center gap-0.5", + children: n, + }), i && Q.jsxs("span", { className: "font-mono text-[10px] text-muted-foreground", - children: [t("columns.originalId"), ": ", n], + children: [t("columns.customId"), ": ", i], }), - e.parent - ? Q.jsxs(Q.Fragment, { - children: [ - Q.jsx("span", { - className: "text-sm text-muted-foreground/30", - children: "→", - }), - Q.jsx("span", { children: e.parent?.code || e.parent?.id }), - ], - }) - : null, ], }), ], @@ -304571,6 +304484,496 @@ function l8t() { }) ); } +function codexNativePageLayout(e, t, n) { + return Q.jsxs(Wot, { + children: [ + Q.jsxs(Hot, { + children: [ + Q.jsx(Vdt, {}), + Q.jsxs("div", { + className: "ml-auto flex items-center space-x-4", + children: [Q.jsx(Wdt, {}), Q.jsx(vut, {})], + }), + ], + }), + Q.jsxs(zot, { + className: "flex flex-col", + fixedHeight: !0, + children: [ + Q.jsx("div", { + className: "mb-2 flex items-center justify-between space-y-2", + children: Q.jsxs("div", { + children: [ + Q.jsx("h2", { className: "text-2xl font-bold tracking-tight", children: e }), + Q.jsx("p", { className: "mt-2 text-muted-foreground", children: t }), + ], + }), + }), + Q.jsx("div", { + className: "-mx-4 flex-1 overflow-auto px-4 py-1 lg:flex-row lg:space-x-12 lg:space-y-0", + children: Q.jsx("div", { className: "w-full", children: n }), + }), + ], + }), + ], + }); +} +function codexNativeStatusBadge(e) { + const t = String(e || "-").toLowerCase(), + n = /approved|active|enabled|ready/.test(t) + ? "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400" + : /pending|eligible|unverified/.test(t) + ? "bg-yellow-500/15 text-yellow-700 dark:text-yellow-400" + : "bg-destructive/10 text-destructive"; + return Q.jsx("span", { + className: Rf("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold", n), + children: e || "-", + }); +} +function codexNativeRelationChip(e, t) { + return Q.jsxs("span", { + className: + "inline-flex items-center gap-1 rounded-full border bg-muted/50 px-2.5 py-1 font-mono text-xs", + children: [ + e, + Q.jsx("span", { className: "text-muted-foreground/60", children: "\u2192" }), + t, + ], + }); +} +function codexNativeSearchToolbar({ + keyword: e, + setKeyword: t, + onSearch: n, + onReset: i, + refetch: r, + actions: o, +}) { + return Q.jsxs("div", { + className: "flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between", + children: [ + Q.jsxs("div", { + className: "flex flex-1 flex-wrap items-center gap-2 sm:flex-nowrap", + children: [ + Q.jsx(Q6e, { + placeholder: "搜索用户 ID / 邮箱", + value: e, + onChange: (e) => t(e.target.value), + onKeyDown: (e) => { + "Enter" === e.key && n(); + }, + className: "h-8 w-full min-w-[150px] sm:w-[180px] lg:w-[280px]", + }), + Q.jsx(Nm, { variant: "outline", className: "h-8 px-3", onClick: n, children: "搜索" }), + Q.jsx(Nm, { variant: "ghost", className: "h-8 px-3", onClick: i, children: "重置" }), + Q.jsx(Nm, { variant: "ghost", className: "h-8 px-3", onClick: () => r(), children: "刷新" }), + ], + }), + o ? Q.jsx("div", { className: "flex flex-wrap items-center gap-2", children: o }) : null, + ], + }); +} +function codexNativeRealnameTable() { + const [e, t] = H.useState(""), + [n, i] = H.useState(""), + [r, o] = H.useState({}), + [s, a] = H.useState({}), + [l, c] = H.useState({ pageIndex: 0, pageSize: 20 }), + { + refetch: d, + data: u, + isLoading: h, + } = gC({ + queryKey: ["codexNativeRealname", l, n], + queryFn: () => + TL(`${RL()}/realname/records`, { + params: { page: l.pageIndex + 1, per_page: l.pageSize, keyword: n }, + }), + }), + g = H.useMemo( + () => [ + { + accessorKey: "id", + header: () => "用户 ID", + cell: ({ row: e }) => + Q.jsx("div", { className: "font-mono text-sm font-medium", children: e.getValue("id") }), + }, + { + accessorKey: "email", + header: () => "邮箱", + cell: ({ row: e }) => + Q.jsx("div", { className: "max-w-[240px] truncate", children: e.getValue("email") || "-" }), + }, + { + accessorKey: "real_name", + header: () => "姓名", + cell: ({ row: e }) => e.getValue("real_name") || "-", + }, + { + accessorKey: "identity_no_masked", + header: () => "证件号", + cell: ({ row: e }) => + Q.jsx("div", { className: "font-mono text-xs", children: e.getValue("identity_no_masked") || "-" }), + }, + JKt.display({ + id: "status", + header: () => "状态", + cell: ({ row: e }) => codexNativeStatusBadge(e.original.status_label || e.original.status), + }), + JKt.display({ + id: "actions", + header: () => "操作", + cell: ({ row: e }) => + Q.jsxs("div", { + className: "flex flex-wrap items-center gap-2", + children: [ + Q.jsx(Nm, { + size: "sm", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/realname/review/${e.original.id}`, { status: "approved", reason: "" }); + hN.success("已通过实名认证"); + d(); + }, + children: "通过", + }), + Q.jsx(Nm, { + size: "sm", + variant: "outline", + className: "h-8", + onClick: async () => { + const t = window.prompt("请输入驳回原因", "") || ""; + await IL(`${RL()}/realname/review/${e.original.id}`, { + status: "rejected", + reason: t, + }); + hN.success("已驳回实名认证"); + d(); + }, + children: "驳回", + }), + Q.jsx(Nm, { + size: "sm", + variant: "ghost", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/realname/reset/${e.original.id}`, {}); + hN.success("已重置实名记录"); + d(); + }, + children: "重置", + }), + ], + }), + }), + ], + [d], + ), + p = PKt({ + data: u?.data?.data ?? [], + columns: g, + state: { columnVisibility: o, rowSelection: s, pagination: l }, + getRowId: (e) => String(e.id), + rowCount: u?.data?.pagination?.total ?? 0, + manualPagination: !0, + enableRowSelection: !0, + onRowSelectionChange: a, + onColumnVisibilityChange: o, + onPaginationChange: c, + getCoreRowModel: LKt(), + getPaginationRowModel: OKt(), + }); + return Q.jsxs("div", { + className: "space-y-4", + children: [ + Q.jsx(codexNativeSearchToolbar, { + keyword: e, + setKeyword: t, + onSearch: () => { + c((e) => ({ ...e, pageIndex: 0 })); + i(e.trim()); + }, + onReset: () => { + t(""); + i(""); + c((e) => ({ ...e, pageIndex: 0 })); + }, + refetch: d, + actions: Q.jsxs(Q.Fragment, { + children: [ + Q.jsx(Nm, { + variant: "outline", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/realname/sync-all`, {}); + hN.success("已同步全部实名状态"); + d(); + }, + children: "同步全部", + }), + Q.jsx(Nm, { + className: "h-8", + onClick: async () => { + await IL(`${RL()}/realname/approve-all`, {}); + hN.success("已全部通过"); + d(); + }, + children: "全部通过", + }), + ], + }), + }), + Q.jsx(QKt, { + table: p, + isLoading: h, + showPagination: !0, + mobilePrimaryField: "email", + mobileGridFields: ["real_name", "status", "identity_no_masked"], + }), + ], + }); +} +function codexNativeRealnamePage() { + return codexNativePageLayout("实名认证", "审核实名记录并同步实名状态。", Q.jsx(codexNativeRealnameTable, {})); +} +function codexNativeOnlineDevicesTable() { + const [e, t] = H.useState(""), + [n, i] = H.useState(""), + [r, o] = H.useState({}), + [s, a] = H.useState({}), + [l, c] = H.useState({ pageIndex: 0, pageSize: 20 }), + { + refetch: d, + data: u, + isLoading: h, + } = gC({ + queryKey: ["codexNativeOnlineDevices", l, n], + queryFn: () => + TL(`${RL()}/user-online-devices/users`, { + params: { page: l.pageIndex + 1, per_page: l.pageSize, keyword: n }, + }), + }), + g = H.useMemo( + () => [ + { + accessorKey: "id", + header: () => "用户 ID", + cell: ({ row: e }) => + Q.jsx("div", { className: "font-mono text-sm font-medium", children: e.getValue("id") }), + }, + { + accessorKey: "email", + header: () => "邮箱", + cell: ({ row: e }) => + Q.jsx("div", { className: "max-w-[240px] truncate", children: e.getValue("email") || "-" }), + }, + { + accessorKey: "subscription_name", + header: () => "套餐", + cell: ({ row: e }) => e.getValue("subscription_name") || "-", + }, + JKt.display({ + id: "online_devices", + header: () => "在线 IP", + cell: ({ row: e }) => { + const t = e.original.online_devices || []; + return t.length + ? Q.jsx("div", { + className: "flex flex-col gap-1 font-mono text-xs", + children: t.map((e, t) => Q.jsx("span", { children: e }, `${e}-${t}`)), + }) + : "-"; + }, + }), + { + accessorKey: "online_count", + header: () => "数量", + cell: ({ row: e }) => + Q.jsx("div", { className: "font-mono text-sm", children: e.getValue("online_count") || 0 }), + }, + JKt.display({ + id: "status", + header: () => "Status", + cell: ({ row: e }) => codexNativeStatusBadge(e.original.status_label || e.original.status), + }), + { + accessorKey: "last_online_text", + header: () => "最后在线", + cell: ({ row: e }) => + Q.jsx("div", { className: "text-nowrap text-sm text-muted-foreground", children: e.getValue("last_online_text") || "-" }), + }, + ], + [], + ), + p = PKt({ + data: u?.data?.list ?? [], + columns: g, + state: { columnVisibility: r, rowSelection: s, pagination: l }, + getRowId: (e) => String(e.id), + rowCount: u?.data?.pagination?.total ?? 0, + manualPagination: !0, + enableRowSelection: !0, + onRowSelectionChange: a, + onColumnVisibilityChange: o, + onPaginationChange: c, + getCoreRowModel: LKt(), + getPaginationRowModel: OKt(), + }); + return Q.jsxs("div", { + className: "space-y-4", + children: [ + Q.jsx(codexNativeSearchToolbar, { + keyword: e, + setKeyword: t, + onSearch: () => { + c((e) => ({ ...e, pageIndex: 0 })); + i(e.trim()); + }, + onReset: () => { + t(""); + i(""); + c((e) => ({ ...e, pageIndex: 0 })); + }, + refetch: d, + }), + Q.jsx(QKt, { + table: p, + isLoading: h, + showPagination: !0, + mobilePrimaryField: "email", + mobileGridFields: ["subscription_name", "online_count", "status", "last_online_text"], + }), + ], + }); +} +function codexNativeOnlineDevicesPage() { + return codexNativePageLayout("在线设备", "查看用户在线 IP、数量和最后在线时间。", Q.jsx(codexNativeOnlineDevicesTable, {})); +} +function codexNativeIPv6Table() { + const [e, t] = H.useState(""), + [n, i] = H.useState(""), + [r, o] = H.useState({}), + [s, a] = H.useState({}), + [l, c] = H.useState({ pageIndex: 0, pageSize: 20 }), + { + refetch: d, + data: u, + isLoading: h, + } = gC({ + queryKey: ["codexNativeIPv6", l, n], + queryFn: () => + TL(`${RL()}/user-add-ipv6-subscription/users`, { + params: { page: l.pageIndex + 1, per_page: l.pageSize, keyword: n }, + }), + }), + g = H.useMemo( + () => [ + JKt.display({ + id: "relation", + header: () => "主从关系", + cell: ({ row: e }) => + codexNativeRelationChip(e.original.id, e.original.shadow_user_id || "-"), + }), + { + accessorKey: "email", + header: () => "主账号", + cell: ({ row: e }) => + Q.jsx("div", { className: "max-w-[240px] truncate", children: e.getValue("email") || "-" }), + }, + { + accessorKey: "ipv6_email", + header: () => "IPv6 账号", + cell: ({ row: e }) => + Q.jsx("div", { className: "font-mono text-xs", children: e.getValue("ipv6_email") || "-" }), + }, + { + accessorKey: "plan_name", + header: () => "套餐", + cell: ({ row: e }) => e.getValue("plan_name") || "-", + }, + JKt.display({ + id: "status", + header: () => "状态", + cell: ({ row: e }) => codexNativeStatusBadge(e.original.status_label || e.original.status), + }), + JKt.display({ + id: "actions", + header: () => "操作", + cell: ({ row: e }) => + Q.jsxs("div", { + className: "flex flex-wrap items-center gap-2", + children: [ + Q.jsx(Nm, { + size: "sm", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/user-add-ipv6-subscription/enable/${e.original.id}`, {}); + hN.success("已开通并同步 IPv6 子账号"); + d(); + }, + children: "开通并同步", + }), + Q.jsx(Nm, { + size: "sm", + variant: "outline", + className: "h-8", + onClick: async () => { + await IL(`${RL()}/user-add-ipv6-subscription/sync-password/${e.original.id}`, {}); + hN.success("已同步密码"); + d(); + }, + children: "同步密码", + }), + ], + }), + }), + ], + [d], + ), + p = PKt({ + data: u?.data?.list ?? [], + columns: g, + state: { columnVisibility: r, rowSelection: s, pagination: l }, + getRowId: (e) => String(e.id), + rowCount: u?.data?.pagination?.total ?? 0, + manualPagination: !0, + enableRowSelection: !0, + onRowSelectionChange: a, + onColumnVisibilityChange: o, + onPaginationChange: c, + getCoreRowModel: LKt(), + getPaginationRowModel: OKt(), + }); + return Q.jsxs("div", { + className: "space-y-4", + children: [ + Q.jsx(codexNativeSearchToolbar, { + keyword: e, + setKeyword: t, + onSearch: () => { + c((e) => ({ ...e, pageIndex: 0 })); + i(e.trim()); + }, + onReset: () => { + t(""); + i(""); + c((e) => ({ ...e, pageIndex: 0 })); + }, + refetch: d, + }), + Q.jsx(QKt, { + table: p, + isLoading: h, + showPagination: !0, + mobilePrimaryField: "email", + mobileGridFields: ["ipv6_email", "plan_name", "status"], + }), + ], + }); +} +function codexNativeIPv6Page() { + return codexNativePageLayout("IPv6 子账号", "管理 IPv6 子账号开通、主从关系和密码同步。", Q.jsx(codexNativeIPv6Table, {})); +} function c8t() { const [e] = Hg(), [t, n] = H.useState({}), diff --git a/frontend/admin/src-reverse/config/navigation.js b/frontend/admin/src-reverse/config/navigation.js index cd9b5a9..e327582 100644 --- a/frontend/admin/src-reverse/config/navigation.js +++ b/frontend/admin/src-reverse/config/navigation.js @@ -3,10 +3,7 @@ export const navigationGroups = [ key: "config", items: [ { href: "/config/system", titleKey: "nav:systemConfig" }, - { href: "/config/plugin", titleKey: "nav:pluginManagement" }, - { href: "/config/theme", titleKey: "nav:themeConfig" }, { href: "/config/notice", titleKey: "nav:noticeManagement" }, - { href: "/config/payment", titleKey: "nav:paymentConfig" }, { href: "/config/knowledge", titleKey: "nav:knowledgeManagement" }, ], }, @@ -16,10 +13,7 @@ export const navigationGroups = [ }, { key: "finance", - items: [ - { href: "/finance/plan", titleKey: "nav:planManagement" }, - { href: "/finance/gift-card", titleKey: "nav:giftCardManagement" }, - ], + items: [{ href: "/finance/plan", titleKey: "nav:planManagement" }], }, { key: "user", @@ -37,6 +31,7 @@ export const navigationGroups = [ { href: "/config/system/invite", titleKey: "nav:inviteConfig" }, { href: "/config/system/server", titleKey: "nav:serverConfig" }, { href: "/config/system/email", titleKey: "nav:emailConfig" }, + { href: "/config/theme", titleKey: "nav:themeConfig" }, { href: "/config/system/telegram", titleKey: "nav:telegramConfig" }, { href: "/config/system/app", titleKey: "nav:appConfig" }, { href: "/config/system/subscribe-template", titleKey: "nav:subscribeTemplateConfig" }, diff --git a/frontend/admin/src-reverse/config/routes.js b/frontend/admin/src-reverse/config/routes.js index af4a463..5612214 100644 --- a/frontend/admin/src-reverse/config/routes.js +++ b/frontend/admin/src-reverse/config/routes.js @@ -10,7 +10,6 @@ import SystemEmailPage from "../pages/config/system/SystemEmailPage.js"; import SystemTelegramPage from "../pages/config/system/SystemTelegramPage.js"; import SystemAppPage from "../pages/config/system/SystemAppPage.js"; import SubscribeTemplatePage from "../pages/config/system/SubscribeTemplatePage.js"; -import PaymentConfigPage from "../pages/config/PaymentConfigPage.js"; import PluginManagementPage from "../pages/config/PluginManagementPage.js"; import ThemeConfigPage from "../pages/config/ThemeConfigPage.js"; import NoticeManagementPage from "../pages/config/NoticeManagementPage.js"; @@ -20,8 +19,6 @@ import ServerGroupPage from "../pages/server/ServerGroupPage.js"; import ServerRoutePage from "../pages/server/ServerRoutePage.js"; import FinancePlanPage from "../pages/finance/FinancePlanPage.js"; import FinanceOrderPage from "../pages/finance/FinanceOrderPage.js"; -import FinanceCouponPage from "../pages/finance/FinanceCouponPage.js"; -import FinanceGiftCardPage from "../pages/finance/FinanceGiftCardPage.js"; import UserManagePage from "../pages/user/UserManagePage.js"; import UserTicketPage from "../pages/user/UserTicketPage.js"; import TrafficResetLogsPage from "../pages/user/TrafficResetLogsPage.js"; @@ -42,7 +39,6 @@ export const reverseRoutes = [ { path: "/config/system/telegram", page: SystemTelegramPage }, { path: "/config/system/app", page: SystemAppPage }, { path: "/config/system/subscribe-template", page: SubscribeTemplatePage }, - { path: "/config/payment", page: PaymentConfigPage }, { path: "/config/plugin", page: PluginManagementPage }, { path: "/config/theme", page: ThemeConfigPage }, { path: "/config/notice", page: NoticeManagementPage }, @@ -52,8 +48,6 @@ export const reverseRoutes = [ { path: "/server/route", page: ServerRoutePage }, { path: "/finance/plan", page: FinancePlanPage }, { path: "/finance/order", page: FinanceOrderPage }, - { path: "/finance/coupon", page: FinanceCouponPage }, - { path: "/finance/gift-card", page: FinanceGiftCardPage }, { path: "/user/manage", page: UserManagePage }, { path: "/user/ticket", page: UserTicketPage }, { path: "/user/traffic-reset-logs", page: TrafficResetLogsPage }, diff --git a/frontend/admin/src-reverse/pages/config/ThemeConfigPage.js b/frontend/admin/src-reverse/pages/config/ThemeConfigPage.js index 79593cc..fe06810 100644 --- a/frontend/admin/src-reverse/pages/config/ThemeConfigPage.js +++ b/frontend/admin/src-reverse/pages/config/ThemeConfigPage.js @@ -1,9 +1,10 @@ -import { makeSkeletonPage } from "../../runtime/makeSkeletonPage.js"; +import { makeRecoveredPage } from "../../runtime/makeRecoveredPage.js"; +import ThemeConfigPageView from "./ThemeConfigPage.jsx"; -export default makeSkeletonPage({ +export default makeRecoveredPage({ title: "Theme Config", routePath: "/config/theme", moduleId: "WQt", featureKey: "config.theme", - notes: ["Bundle contains theme activation and theme config editing flows."], + component: ThemeConfigPageView, }); diff --git a/frontend/admin/src-reverse/pages/config/ThemeConfigPage.jsx b/frontend/admin/src-reverse/pages/config/ThemeConfigPage.jsx new file mode 100644 index 0000000..34f05d4 --- /dev/null +++ b/frontend/admin/src-reverse/pages/config/ThemeConfigPage.jsx @@ -0,0 +1,226 @@ +import React, { useEffect, useState } from "../../../recovery-preview/node_modules/react/index.js"; +import { + compactText, + requestJson, +} from "../../runtime/client.js"; + +// Wot: Wrapper structure for the config page +function Wot({ children }) { + return
{children}
; +} + +// Hot: Header component for sections or the page +function Hot({ title, description }) { + return ( +
+
+

+ Theme Customization +

+

{title}

+

{description}

+
+
+ ); +} + +// zot: Setting Item (Zone) component for form fields +function zot({ label, children, span = 1 }) { + return ( +
+ +
+ {children} +
+
+ ); +} + +// QKt: The main Nebula configuration component +function QKt() { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [form, setForm] = useState({ + nebula_theme_color: 'aurora', + nebula_hero_slogan: '', + nebula_welcome_target: '', + nebula_register_title: '', + nebula_background_url: '', + nebula_metrics_base_url: '', + nebula_default_theme_mode: 'system', + nebula_light_logo_url: '', + nebula_dark_logo_url: '', + nebula_custom_html: '', + nebula_static_cdn_url: '', + }); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + useEffect(() => { + (async () => { + try { + const payload = await requestJson("/config/fetch?key=nebula"); + if (payload?.nebula) { + setForm(prev => ({ ...prev, ...payload.nebula })); + } + } catch (err) { + setError("Failed to load configuration: " + err.message); + } finally { + setLoading(false); + } + })(); + }, []); + + const handleSave = async (e) => { + e.preventDefault(); + setSaving(true); + setSuccess(""); + setError(""); + try { + await requestJson("/config/save", { + method: "POST", + body: form, + }); + setSuccess("Nebula configuration updated successfully."); + window.scrollTo({ top: 0, behavior: 'smooth' }); + } catch (err) { + setError("Failed to save configuration: " + err.message); + } finally { + setSaving(false); + } + }; + + if (loading) return
Initializing Nebula settings...
; + + return ( + + + + {error &&
{error}
} + {success &&
{success}
} + +
+ + + + + + setForm({...form, nebula_hero_slogan: e.target.value})} + placeholder="Main visual headline" + /> + + + + setForm({...form, nebula_welcome_target: e.target.value})} + placeholder="Name displayed after WELCOME TO" + /> + + + + setForm({...form, nebula_register_title: e.target.value})} + placeholder="Title on registration panel" + /> + + + + + + + + setForm({...form, nebula_background_url: e.target.value})} + placeholder="Direct link to background image" + /> + + + + setForm({...form, nebula_metrics_base_url: e.target.value})} + placeholder="https://stats.example.com" + /> + + + + setForm({...form, nebula_light_logo_url: e.target.value})} + placeholder="Logo for light mode" + /> + + + + setForm({...form, nebula_dark_logo_url: e.target.value})} + placeholder="Logo for dark mode" + /> + + + + setForm({...form, nebula_static_cdn_url: e.target.value})} + placeholder="e.g. https://cdn.example.com/nebula (no trailing slash)" + /> + + + +