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}
}
+
+
+
+ );
+}
+
+export default QKt;
diff --git a/frontend/templates/admin_app.html b/frontend/templates/admin_app.html
index 596ea4c..bd42a53 100644
--- a/frontend/templates/admin_app.html
+++ b/frontend/templates/admin_app.html
@@ -7,14 +7,14 @@
-
-
-
-
-
+
+
+
+
+
-
+