(function () { "use strict"; const cfg = window.ADMIN_APP_CONFIG || {}; const root = document.getElementById("admin-app"); if (!root) { return; } const state = { token: readToken(), user: null, route: "overview", busy: false, message: "", messageType: "info", modal: null, configTab: "site", sidebarGroups: { dashboard: true, infrastructure: true, business: true, operations: true, system: true }, pagination: {}, dashboard: null, system: null, config: null, nodes: [], groups: [], routes: [], plans: [], orders: { list: [], pagination: null }, coupons: [], users: { list: [], pagination: null }, tickets: { list: [], pagination: null }, realname: { list: [], pagination: null }, devices: { list: [], pagination: null }, ipv6: { list: [], pagination: null }, expandedNodes: new Set() }; const ROUTE_META = { overview: { title: "总览", description: "查看收入、用户和流量概况。" }, "dashboard-node": { title: "节点状态", description: "查看节点在线状态、负载和推送情况。" }, "node-manage": { title: "节点管理", description: "管理服务节点、可见性以及父子节点关系。" }, "node-group": { title: "权限组", description: "管理节点权限组和用户分组映射。" }, "node-route": { title: "路由规则", description: "维护节点路由匹配规则。" }, "plan-manage": { title: "套餐管理", description: "维护套餐、流量和价格配置。" }, "order-manage": { title: "订单管理", description: "处理待支付和已支付订单。" }, "coupon-manage": { title: "优惠券", description: "创建和维护优惠券信息。" }, "user-manage": { title: "用户管理", description: "查看用户订阅、流量和封禁状态。" }, "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() { window.addEventListener("hashchange", async function () { state.route = normalizeRoute(readRoute()); state.modal = null; await hydrateRoute(); }); root.addEventListener("click", onClick); root.addEventListener("submit", onSubmit); if (state.token) { await loadBootstrap(); } else { render(); } if (state.user) { await hydrateRoute(); } else { render(); } } async function loadBootstrap() { try { setBusy(true); const loginCheck = unwrap(await request("/api/v1/user/checkLogin", { method: "GET" })); if (!loginCheck || !loginCheck.is_admin) { clearSession(); return; } state.user = loginCheck; const [config, system] = await Promise.all([ request(cfg.api.adminConfig, { method: "GET" }), request(cfg.api.systemStatus, { method: "GET" }) ]); state.config = unwrap(config) || {}; state.system = unwrap(system) || {}; } catch (error) { console.error("bootstrap failed", error); clearSession(); show(error.message || "管理端初始化失败", "error"); } finally { setBusy(false); render(); } } async function hydrateRoute() { if (!state.user) { render(); return; } try { setBusy(true); const page = getCurrentPage(); if (state.route === "overview") { state.dashboard = unwrap(await request(cfg.api.dashboardSummary)); } else if (state.route === "dashboard-node") { const [dashboard, nodes] = await Promise.all([ request(cfg.api.dashboardSummary), request(cfg.api.serverNodes) ]); state.dashboard = unwrap(dashboard) || {}; state.nodes = toArray(unwrap(nodes)); } else if (state.route === "node-manage") { const [nodes, groups] = await Promise.all([ request(cfg.api.serverNodes), request(cfg.api.serverGroups) ]); state.nodes = toArray(unwrap(nodes)); state.groups = toArray(unwrap(groups)); } else if (state.route === "node-group") { state.groups = toArray(unwrap(await request(cfg.api.serverGroups))); } else if (state.route === "node-route") { state.routes = toArray(unwrap(await request(cfg.api.serverRoutes))); } else if (state.route === "plan-manage") { const [plans, groups] = await Promise.all([ request(cfg.api.plans), request(cfg.api.serverGroups) ]); state.plans = toArray(unwrap(plans)); state.groups = toArray(unwrap(groups)); } else if (state.route === "order-manage") { 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))); } else if (state.route === "user-manage") { const [users, plans, groups] = await Promise.all([ request(`${cfg.api.users}?page=${page}&per_page=20`), request(cfg.api.plans), request(cfg.api.serverGroups) ]); state.users = normalizeListPayload(users); state.plans = toArray(unwrap(plans)); state.groups = toArray(unwrap(groups)); } else if (state.route === "ticket-manage") { const payload = await request(`${cfg.api.tickets}?page=${page}&per_page=20`); state.tickets = normalizeListPayload(payload); } else if (state.route === "realname") { 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 = 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)); } } catch (error) { console.error("route hydrate failed", error); show(error.message || "页面数据加载失败", "error"); } finally { setBusy(false); render(); } } async function onClick(event) { const actionEl = event.target.closest("[data-action]"); if (!actionEl || state.busy) { return; } const action = actionEl.getAttribute("data-action"); if (action === "nav") { window.location.hash = actionEl.getAttribute("data-route") || "overview"; return; } if (action === "logout") { clearSession(); render(); return; } if (action === "refresh") { await loadBootstrap(); await hydrateRoute(); return; } if (action === "modal-close") { state.modal = null; render(); return; } if (action === "sidebar-toggle") { const group = actionEl.getAttribute("data-id"); state.sidebarGroups[group] = state.sidebarGroups[group] === false; render(); return; } if (action === "config-tab") { state.configTab = actionEl.getAttribute("data-tab") || "site"; render(); return; } if (action === "page") { setCurrentPage(Number(actionEl.getAttribute("data-page")) || 1); await hydrateRoute(); return; } if (action === "node-expand") { const id = String(actionEl.getAttribute("data-id")); if (state.expandedNodes.has(id)) { state.expandedNodes.delete(id); } else { state.expandedNodes.add(id); } render(); return; } if (action === "plan-add") { state.modal = { type: "plan", data: {} }; render(); return; } if (action === "plan-edit") { const item = state.plans.find((row) => String(row.id) === actionEl.getAttribute("data-id")); state.modal = { type: "plan", data: item || {} }; render(); return; } if (action === "plan-delete") { if (confirm("确认删除该套餐吗?")) { await adminPost(`${cfg.api.adminBase}/plan/drop`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); } return; } if (action === "coupon-add") { state.modal = { type: "coupon", data: {} }; render(); return; } if (action === "coupon-delete") { if (confirm("确认删除该优惠券吗?")) { await adminPost(`${cfg.api.adminBase}/coupon/drop`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); } return; } if (action === "user-edit") { const item = state.users.list.find((row) => String(row.id) === actionEl.getAttribute("data-id")); state.modal = { type: "user", data: item || {} }; render(); return; } if (action === "user-ban" || action === "user-unban") { await adminPost(`${cfg.api.adminBase}/user/ban`, { id: Number(actionEl.getAttribute("data-id")), banned: action === "user-ban" }); await hydrateRoute(); return; } if (action === "user-reset-traffic") { await adminPost(`${cfg.api.adminBase}/user/resetTraffic`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); return; } if (action === "order-paid") { await adminPost(`${cfg.api.adminBase}/order/paid`, { trade_no: actionEl.getAttribute("data-trade") }); await hydrateRoute(); return; } if (action === "order-cancel") { await adminPost(`${cfg.api.adminBase}/order/cancel`, { trade_no: actionEl.getAttribute("data-trade") }); await hydrateRoute(); return; } if (action === "node-add") { state.modal = { type: "node", data: {} }; render(); return; } if (action === "node-edit") { const item = state.nodes.find((row) => String(row.id) === actionEl.getAttribute("data-id")); state.modal = { type: "node", data: item || {} }; render(); return; } if (action === "node-copy") { await adminPost(`${cfg.api.adminBase}/server/manage/copy`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); return; } if (action === "node-delete") { if (confirm("确认删除该节点吗?")) { await adminPost(`${cfg.api.adminBase}/server/manage/drop`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); } return; } if (action === "node-toggle-visible") { event.preventDefault(); const input = actionEl.querySelector("input"); if (!input) { return; } input.checked = !input.checked; await adminPost(`${cfg.api.adminBase}/server/manage/update`, { id: Number(actionEl.getAttribute("data-id")), show: input.checked ? 1 : 0 }); await hydrateRoute(); return; } if (action === "group-add") { state.modal = { type: "group", data: {} }; render(); return; } if (action === "group-edit") { const item = state.groups.find((row) => String(row.id) === actionEl.getAttribute("data-id")); state.modal = { type: "group", data: item || {} }; render(); return; } if (action === "group-delete") { if (confirm("确认删除该权限组吗?")) { await adminPost(`${cfg.api.adminBase}/server/group/drop`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); } return; } if (action === "route-add") { state.modal = { type: "route", data: {} }; render(); return; } if (action === "route-edit") { const item = state.routes.find((row) => String(row.id) === actionEl.getAttribute("data-id")); state.modal = { type: "route", data: item || {} }; render(); return; } if (action === "route-delete") { if (confirm("确认删除该路由规则吗?")) { await adminPost(`${cfg.api.adminBase}/server/route/drop`, { id: Number(actionEl.getAttribute("data-id")) }); await hydrateRoute(); } return; } if (action === "realname-review") { const userId = actionEl.getAttribute("data-user-id"); const status = actionEl.getAttribute("data-status"); const reason = status === "rejected" ? prompt("请输入驳回原因,可留空:", "") || "" : ""; await adminPost(`${cfg.api.realnameBase}/review/${userId}`, { status, reason }); await hydrateRoute(); return; } if (action === "approve-all") { await adminPost(`${cfg.api.realnameBase}/approve-all`, {}); await hydrateRoute(); return; } 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; } } async function onSubmit(event) { const form = event.target; const action = form.getAttribute("data-form"); if (!action) { return; } event.preventDefault(); if (action === "login") { try { setBusy(true); const payload = unwrap(await request("/api/v2/passport/auth/login", { method: "POST", auth: false, body: serializeForm(form) })); if (!payload || !payload.auth_data || !payload.is_admin) { throw new Error("当前账号没有管理员权限"); } saveToken(payload.auth_data); state.token = readToken(); await loadBootstrap(); await hydrateRoute(); show("登录成功", "success"); } catch (error) { show(error.message || "登录失败", "error"); render(); } finally { setBusy(false); } return; } const body = serializeForm(form); let target = ""; if (action === "plan-save") { target = `${cfg.api.adminBase}/plan/save`; } else if (action === "coupon-save") { target = `${cfg.api.adminBase}/coupon/save`; } else if (action === "user-save") { target = `${cfg.api.adminBase}/user/update`; } else if (action === "node-save") { target = `${cfg.api.adminBase}/server/manage/save`; } else if (action === "group-save") { target = `${cfg.api.adminBase}/server/group/save`; } else if (action === "route-save") { target = `${cfg.api.adminBase}/server/route/save`; } else if (action === "config-save") { target = `${cfg.api.adminBase}/config/save`; } if (!target) { return; } await adminPost(target, body); if (action !== "config-save") { state.modal = null; } await hydrateRoute(); } function render() { root.innerHTML = state.user ? renderDashboard() : renderLogin(); if (state.modal) { const modalShell = document.createElement("div"); modalShell.className = "modal-overlay"; modalShell.innerHTML = renderModalContent(); root.appendChild(modalShell); } if (state.busy) { const overlay = document.createElement("div"); overlay.className = "busy-overlay"; overlay.innerHTML = '
正在处理...
'; root.appendChild(overlay); } } function renderLogin() { return [ '
', '
', '", state.message ? renderNotice() : "", '", "
", "
" ].join(""); } function renderDashboard() { return [ '
', renderSidebar(), '
', renderTopbar(), '
', state.message ? renderNotice() : "", renderMainContent(), "
", "
", "
" ].join(""); } function renderSidebar() { return [ '" ].join(""); } function renderSidebarGroup(id, label, items) { const expanded = state.sidebarGroups[id] !== false; return [ '" ].join(""); } function navItem(route, title, desc) { const active = state.route === route ? "active" : ""; return [ ``, `${escapeHtml(title)}`, `${escapeHtml(desc)}`, "" ].join(""); } function renderTopbar() { const meta = ROUTE_META[state.route] || { title: getRouteTitle(state.route), description: "管理后台页面" }; return [ '
', '
', `

${escapeHtml(meta.title)}

`, `

${escapeHtml(meta.description)}

`, '
', `路径 /${escapeHtml(getSecurePath())}`, `${escapeHtml(state.user.email || "")}`, `${escapeHtml(formatDate(state.system && state.system.server_time))}`, "
", "
", '
', "
" ].join(""); } function renderMainContent() { if (state.route === "overview") return renderOverview(); if (state.route === "dashboard-node") return renderDashboardNodes(); if (state.route === "node-manage") return renderNodeManage(); if (state.route === "node-group") return renderNodeGroup(); if (state.route === "node-route") return renderNodeRoute(); if (state.route === "plan-manage") return renderPlanManage(); if (state.route === "order-manage") return renderOrderManage(); if (state.route === "coupon-manage") return renderCouponManage(); if (state.route === "user-manage") return renderUserManage(); 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(); } function renderOverview() { const dash = state.dashboard || {}; return [ '
', statCard("今日收入", formatMoney(dash.todayIncome), `${formatSignedPercent(dash.dayIncomeGrowth)} 较昨日`), statCard("本月收入", formatMoney(dash.currentMonthIncome), `${formatSignedPercent(dash.monthIncomeGrowth)} 较上月`), statCard("总用户", String(dash.totalUsers || 0), `本月新增 ${dash.currentMonthNewUsers || 0}`), statCard("在线用户", String(dash.onlineUsers || 0), `在线设备 ${dash.onlineDevices || 0}`), statCard("待处理工单", String(dash.ticketPendingTotal || 0), `在线节点 ${dash.onlineNodes || 0}`), statCard("总流量", formatTraffic(dash.totalTraffic && dash.totalTraffic.total), `今日 ${formatTraffic(dash.todayTraffic && dash.todayTraffic.total)}`), "
", '
', "

运行概况

", '
', summaryItem("应用名称", dash.app_name || cfg.title || "XBoard"), summaryItem("管理路径", dash.secure_path || getSecurePath()), summaryItem("服务器时间", formatDate(dash.server_time)), summaryItem("活跃用户", String(dash.activeUsers || 0)), summaryItem("月流量", formatTraffic(dash.monthTraffic && dash.monthTraffic.total)), summaryItem("用户增长", formatSignedPercent(dash.userGrowth)), "
", "
" ].join(""); } function renderDashboardNodes() { const rows = (state.nodes || []).map((node) => [ `${node.id}`, escapeHtml(node.name || "-"), escapeHtml(node.type || "-"), renderStatus(node.available_status || "offline"), escapeHtml(`${formatTraffic(node.u)} / ${formatTraffic(node.d)}`), escapeHtml(formatDate(node.last_push_at || node.last_check_at)) ]); return wrapTable( ["ID", "节点", "类型", "状态", "上下行", "最后心跳"], rows ); } function renderNodeManage() { const roots = state.nodes.filter((node) => !node.parent_id); const childrenByParent = {}; state.nodes.forEach((node) => { if (node.parent_id) { childrenByParent[node.parent_id] = childrenByParent[node.parent_id] || []; childrenByParent[node.parent_id].push(node); } }); const rows = []; roots.forEach((node) => { const children = childrenByParent[node.id] || []; const expanded = state.expandedNodes.has(String(node.id)); rows.push(renderNodeRow(node, false, children.length > 0, expanded)); if (expanded) { children.forEach((child) => rows.push(renderNodeRow(child, true, false, false))); } }); return [ '
', '', "
", wrapTable(["ID", "名称 / 主机", "类型", "倍率", "显示", "操作"], rows) ].join(""); } function renderNodeRow(node, isChild, hasChildren, expanded) { const toggle = hasChildren ? `` : ''; const relationCell = isChild && node.parent_id ? `${escapeHtml(node.id)}${escapeHtml(node.parent_id)}` : `${node.id}`; return { className: isChild ? "node-child" : "", cells: [ relationCell, [ '
', toggle, `
${escapeHtml(node.name || "-")}
${escapeHtml(node.host || "-")}
`, "
" ].join(""), escapeHtml(node.type || "-"), escapeHtml(`${node.rate || 1}x`), [ '
', renderStatus(node.show ? "visible" : "hidden"), ``, "
" ].join(""), renderActionRow([ buttonAction("编辑", "node-edit", node.id), buttonAction("复制", "node-copy", node.id), buttonAction("删除", "node-delete", node.id) ]) ] }; } function renderNodeGroup() { const rows = state.groups.map((group) => [ `${group.id}`, escapeHtml(group.name || "-"), `${group.server_count || 0}`, `${group.user_count || 0}`, renderActionRow([ buttonAction("编辑", "group-edit", group.id), buttonAction("删除", "group-delete", group.id) ]) ]); return [ '
', wrapTable(["ID", "名称", "节点数", "用户数", "操作"], rows) ].join(""); } function renderNodeRoute() { const rows = state.routes.map((route) => [ `${route.id}`, escapeHtml(route.remarks || "-"), escapeHtml((route.match || []).join(", ")), escapeHtml(route.action || "-"), renderActionRow([ buttonAction("编辑", "route-edit", route.id), buttonAction("删除", "route-delete", route.id) ]) ]); return [ '
', wrapTable(["ID", "备注", "匹配规则", "动作", "操作"], rows) ].join(""); } function renderPlanManage() { const rows = state.plans.map((plan) => [ `${plan.id}`, escapeHtml(plan.name || "-"), escapeHtml(plan.group_name || "-"), escapeHtml(formatTraffic(plan.transfer_enable)), `${escapeHtml(plan.prices || "{}")}`, renderStatus(plan.show ? "visible" : "hidden"), renderActionRow([ buttonAction("编辑", "plan-edit", plan.id), buttonAction("删除", "plan-delete", plan.id) ]) ]); return [ '
', wrapTable(["ID", "套餐名称", "权限组", "流量", "价格", "状态", "操作"], rows) ].join(""); } function renderOrderManage() { const rows = state.orders.list.map((order) => [ `${escapeHtml(order.trade_no)}`, escapeHtml(order.user_email || `#${order.user_id}`), escapeHtml((order.plan && order.plan.name) || "-"), formatMoney(order.total_amount), renderStatus(orderStatusText(order.status)), renderActionRow( order.status === 0 ? [ buttonAction("标记已支付", "order-paid", null, `data-trade="${escapeHtml(order.trade_no)}"`), buttonAction("取消", "order-cancel", null, `data-trade="${escapeHtml(order.trade_no)}"`) ] : ["-"] ) ]); return [ wrapTable(["订单号", "用户", "套餐", "金额", "状态", "操作"], rows), renderPagination(state.orders.pagination) ].join(""); } function renderCouponManage() { const rows = state.coupons.map((coupon) => [ `${coupon.id}`, escapeHtml(coupon.name || "-"), `${escapeHtml(coupon.code || "-")}`, escapeHtml(coupon.type === 1 ? "固定金额" : "比例折扣"), escapeHtml(coupon.type === 1 ? formatMoney(coupon.value) : `${coupon.value || 0}%`), renderActionRow([buttonAction("删除", "coupon-delete", coupon.id)]) ]); return [ '
', wrapTable(["ID", "名称", "编码", "类型", "面额", "操作"], rows) ].join(""); } function renderUserManage() { const rows = state.users.list.map((user) => [ user.parent_id ? renderRelationChip(user.parent_id, user.id) : `${user.id}`, [ `${escapeHtml(user.email || "-")}`, 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)}`), escapeHtml(formatDate(user.expired_at)), renderActionRow([ buttonAction("编辑", "user-edit", user.id), buttonAction("重置流量", "user-reset-traffic", user.id), buttonAction(user.banned ? "解封" : "封禁", user.banned ? "user-unban" : "user-ban", user.id) ]) ]); return [ wrapTable(["ID", "邮箱", "状态", "流量", "到期时间", "操作"], rows), renderPagination(state.users.pagination) ].join(""); } function renderTicketManage() { const rows = state.tickets.list.map((ticket) => [ `${ticket.id}`, escapeHtml(ticket.user_email || `#${ticket.user_id}`), escapeHtml(ticket.subject || "-"), renderStatus(ticketStatusText(ticket.status)), escapeHtml(formatDate(ticket.updated_at || ticket.created_at)), `${ticket.message_count || 0}` ]); return [ wrapTable(["ID", "用户", "主题", "状态", "更新时间", "消息数"], rows), renderPagination(state.tickets.pagination) ].join(""); } function renderRealnameManage() { const rows = state.realname.list.map((record) => [ `${record.id}`, escapeHtml(record.email || "-"), escapeHtml(record.real_name || "-"), `${escapeHtml(record.identity_no_masked || "-")}`, renderStatus(record.status_label || record.status || "unverified"), escapeHtml(formatDate(record.submitted_at)), renderActionRow([ buttonAction("通过", "realname-review", null, `data-user-id="${record.id}" data-status="approved"`), buttonAction("驳回", "realname-review", null, `data-user-id="${record.id}" data-status="rejected"`) ]) ]); return [ '
', '
', '', '', "
", "
", wrapTable(["用户ID", "邮箱", "姓名", "证件号", "状态", "提交时间", "操作"], rows), renderPagination(state.realname.pagination) ].join(""); } function renderOnlineDevices() { const rows = state.devices.list.map((item) => [ `${item.id}`, escapeHtml(item.email || "-"), escapeHtml(item.subscription_name || "-"), escapeHtml((item.online_devices || []).join(", ") || "-"), `${item.online_count || 0}`, escapeHtml(item.last_online_text || "-") ]); return [ wrapTable(["用户ID", "邮箱", "套餐", "在线 IP", "设备数", "最后在线"], rows), renderPagination(state.devices.pagination) ].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"; const values = (state.config && state.config[activeTab]) || {}; return [ '
', sections .map((section) => { const active = section === activeTab ? "btn-primary" : "btn-ghost"; return ``; }) .join(""), "
", '
', ``, '
', Object.keys(values) .map((key) => renderConfigField(key, values[key])) .join(""), "
", '
', "
" ].join(""); } function renderConfigField(key, value) { const label = key.replace(/_/g, " "); if (typeof value === "boolean") { return [ '
', ``, ``, "
" ].join(""); } return [ '
', ``, ``, "
" ].join(""); } function renderModalContent() { const modal = state.modal; const body = modal.type === "plan" ? renderPlanForm(modal.data) : modal.type === "coupon" ? renderCouponForm(modal.data) : modal.type === "user" ? renderUserForm(modal.data) : modal.type === "node" ? renderNodeForm(modal.data) : modal.type === "group" ? renderGroupForm(modal.data) : modal.type === "route" ? renderRouteForm(modal.data) : ""; return [ '" ].join(""); } function renderPlanForm(plan) { return [ `

${plan.id ? "编辑套餐" : "新建套餐"}

`, '
', hiddenField("id", plan.id || ""), inputField("套餐名称", "name", plan.name || "", "text", true), selectField("权限组", "group_id", state.groups, plan.group_id, false), inputField("流量额度", "transfer_enable", plan.transfer_enable || 0, "number", false), inputField("速度限制 Mbps", "speed_limit", plan.speed_limit || 0, "number", false), textareaField("价格 JSON", "prices", plan.prices || "{}"), booleanField("是否展示", "show", plan.show !== false), submitRow(plan.id ? "保存套餐" : "创建套餐"), "
" ].join(""); } function renderCouponForm(coupon) { return [ `

${coupon.id ? "编辑优惠券" : "新建优惠券"}

`, '
', hiddenField("id", coupon.id || ""), inputField("名称", "name", coupon.name || "", "text", true), inputField("编码", "code", coupon.code || "", "text", true), selectSimpleField("类型", "type", [ { value: 1, label: "固定金额" }, { value: 2, label: "比例折扣" } ], coupon.type || 1), inputField("数值", "value", coupon.value || 0, "number", true), booleanField("是否展示", "show", coupon.show !== false), submitRow(coupon.id ? "保存优惠券" : "创建优惠券"), "
" ].join(""); } function renderUserForm(user) { return [ `

编辑用户 #${escapeHtml(user.id || "")}

`, '
', hiddenField("id", user.id || ""), inputField("邮箱", "email", user.email || "", "email", true), inputField("新密码", "password", "", "password", false), inputField("余额", "balance", user.balance || 0, "number", false), selectField("套餐", "plan_id", state.plans, user.plan_id, true), inputField("设备限制", "device_limit", user.device_limit || 0, "number", false), inputField("速度限制 Mbps", "speed_limit", user.speed_limit || 0, "number", false), textareaField("备注", "remarks", user.remarks || ""), datetimeField("到期时间", "expired_at", user.expired_at), submitRow("保存用户"), "
" ].join(""); } function renderNodeForm(node) { return [ `

${node.id ? "编辑节点" : "新增节点"}

`, '
', hiddenField("id", node.id || ""), inputField("节点名称", "name", node.name || "", "text", true), inputField("节点类型", "type", node.type || "shadowsocks", "text", true), inputField("主机地址", "host", node.host || "", "text", true), inputField("对外端口", "port", node.port || "", "text", true), inputField("服务端口", "server_port", node.server_port || 443, "number", true), inputField("倍率", "rate", node.rate || 1, "number", true, 'step="0.1"'), booleanField("是否展示", "show", node.show !== false), submitRow(node.id ? "保存节点" : "创建节点"), "
" ].join(""); } function renderGroupForm(group) { return [ `

${group.id ? "编辑权限组" : "新增权限组"}

`, '
', hiddenField("id", group.id || ""), inputField("权限组名称", "name", group.name || "", "text", true), submitRow(group.id ? "保存权限组" : "创建权限组"), "
" ].join(""); } function renderRouteForm(route) { return [ `

${route.id ? "编辑路由规则" : "新增路由规则"}

`, '
', hiddenField("id", route.id || ""), inputField("备注", "remarks", route.remarks || "", "text", true), textareaField("匹配规则", "match", Array.isArray(route.match) ? route.match.join("\n") : ""), selectSimpleField("动作", "action", [ { value: "direct", label: "direct" }, { value: "proxy", label: "proxy" }, { value: "block", label: "block" }, { value: "dns", label: "dns" } ], route.action || "direct"), inputField("动作值", "action_value", route.action_value || "", "text", false), submitRow(route.id ? "保存规则" : "创建规则"), "
" ].join(""); } function renderNotice() { return `
${escapeHtml(state.message)}
`; } function wrapTable(headers, rows) { return `
${renderTable(headers, rows)}
`; } function renderTable(headers, rows) { if (!rows || !rows.length) { return '
暂无数据
'; } return [ "", "", headers.map((header) => ``).join(""), "", "", rows .map((row) => { const isObjectRow = !Array.isArray(row); const cells = isObjectRow ? row.cells : row; const className = isObjectRow ? row.className || "" : ""; return `${cells.map((cell) => ``).join("")}`; }) .join(""), "", "
${escapeHtml(header)}
${cell}
" ].join(""); } function renderPagination(pagination) { if (!pagination || pagination.last_page <= 1) { return ""; } const buttons = []; for (let page = 1; page <= pagination.last_page; page += 1) { buttons.push( `` ); } return `
${buttons.join("")}
`; } function statCard(title, value, hint) { return [ '
', `${escapeHtml(title)}`, `${escapeHtml(value)}`, `

${escapeHtml(hint || "")}

`, "
" ].join(""); } function summaryItem(label, value) { return `
${escapeHtml(label)}${escapeHtml(value)}
`; } function buttonAction(label, action, id, extraAttrs) { const idAttr = id == null ? "" : `data-id="${id}"`; return ``; } function renderActionRow(items) { return `
${items.join("")}
`; } function renderRelationChip(fromId, toId) { return `${escapeHtml(fromId)}${escapeHtml(toId)}`; } function hiddenField(name, value) { return ``; } function inputField(label, name, value, type, required, extraAttrs) { return [ '
', ``, ``, "
" ].join(""); } function textareaField(label, name, value) { return [ '
', ``, ``, "
" ].join(""); } function booleanField(label, name, value) { return selectSimpleField(label, name, [ { value: 1, label: "开启" }, { value: 0, label: "关闭" } ], value ? 1 : 0); } function datetimeField(label, name, value) { const formatted = value ? new Date(value * 1000).toISOString().slice(0, 16) : ""; return inputField(label, name, formatted, "datetime-local", false); } function selectField(label, name, items, currentValue, allowEmpty) { const options = []; if (allowEmpty) { options.push(''); } items.forEach((item) => { options.push( `` ); }); return `
`; } function selectSimpleField(label, name, items, currentValue) { return `
`; } function submitRow(label) { return `
`; } function setBusy(value) { state.busy = value; render(); } function show(message, type) { state.message = message; state.messageType = type || "info"; render(); window.clearTimeout(show._timer); show._timer = window.setTimeout(function () { state.message = ""; render(); }, 3000); } async function adminPost(url, body) { const response = await request(url, { method: "POST", body }); show("操作成功", "success"); return response; } async function request(url, options) { const opt = options || {}; const headers = { "Content-Type": "application/json" }; if (opt.auth !== false && state.token) { headers.Authorization = state.token; } const response = await fetch(url, { method: opt.method || "GET", headers, body: opt.body ? JSON.stringify(opt.body) : undefined }); const payload = await response.json().catch(() => null); if (!response.ok) { if (response.status === 401) { clearSession(); render(); } throw new Error((payload && (payload.message || payload.msg)) || "请求失败"); } return payload; } function normalizeListPayload(payload, listKey) { const key = listKey || "list"; return { list: toArray(payload, key), pagination: payload && payload.pagination ? payload.pagination : null }; } function unwrap(payload) { return payload && typeof payload.data !== "undefined" ? payload.data : payload; } function toArray(value, key) { if (Array.isArray(value)) { return value; } if (!value || typeof value !== "object") { return []; } if (Array.isArray(value[key || "data"])) { return value[key || "data"]; } if (Array.isArray(value.list)) { return value.list; } if (Array.isArray(value.data)) { return value.data; } return []; } function serializeForm(form) { const values = {}; const section = form.querySelector('input[name="__section"]'); const formData = new window.FormData(form); formData.forEach((rawValue, key) => { if (key === "__section") { return; } if (key === "show" || key === "renew" || key === "sell") { values[key] = String(rawValue) === "1"; return; } if (key === "expired_at") { values[key] = rawValue ? Math.floor(new Date(rawValue).getTime() / 1000) : null; return; } if (key === "match") { values[key] = String(rawValue) .split(/\r?\n/) .map((item) => item.trim()) .filter(Boolean); return; } if (["id", "group_id", "transfer_enable", "speed_limit", "balance", "plan_id", "device_limit", "server_port", "value"].includes(key)) { values[key] = rawValue === "" ? null : Number(rawValue); return; } values[key] = rawValue; }); if (section) { return values; } return values; } function getCurrentPage() { return state.pagination[state.route] || 1; } function setCurrentPage(page) { state.pagination[state.route] = page > 0 ? page : 1; } function readToken() { return window.localStorage.getItem("__gopanel_admin_auth__") || ""; } function saveToken(token) { window.localStorage.setItem("__gopanel_admin_auth__", /^Bearer /.test(token) ? token : `Bearer ${token}`); } function clearSession() { state.token = ""; state.user = null; state.modal = null; window.localStorage.removeItem("__gopanel_admin_auth__"); } function readRoute() { return (window.location.hash || "#overview").slice(1); } function normalizeRoute(route) { return ROUTE_META[route] ? route : "overview"; } function getSecurePath() { if (state.config && state.config.safe && state.config.safe.secure_path) { return String(state.config.safe.secure_path).replace(/^\//, ""); } return String(cfg.securePath || "admin").replace(/^\//, ""); } function formatTraffic(bytes) { const value = Number(bytes || 0); if (!value) { return "0 B"; } const units = ["B", "KB", "MB", "GB", "TB"]; let index = 0; let size = value; while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; } return `${size.toFixed(size >= 100 ? 0 : 2)} ${units[index]}`; } function formatMoney(amount) { return `¥${((Number(amount) || 0) / 100).toFixed(2)}`; } function formatDate(value) { if (!value) { return "-"; } const date = new Date(typeof value === "number" ? value * 1000 : value); if (Number.isNaN(date.getTime())) { return String(value); } return date.toLocaleString(); } function formatSignedPercent(value) { const num = Number(value || 0); return `${num >= 0 ? "+" : ""}${num.toFixed(2)}%`; } function orderStatusText(status) { if (status === 0) return "pending"; if (status === 3) return "paid"; if (status === 2) return "cancelled"; return String(status); } function ticketStatusText(status) { if (status === 0) return "pending"; if (status === 1) return "answered"; if (status === 2) return "closed"; return String(status); } function renderStatus(status) { const text = String(status || "-"); const normalized = text.toLowerCase(); let type = "danger"; if (/(ok|active|visible|approved|paid|answered|online|enabled)/.test(normalized)) { type = "ok"; } else if (/(pending|warn|unverified|eligible|ready)/.test(normalized)) { type = "warn"; } return `${escapeHtml(text)}`; } function escapeHtml(value) { return String(value == null ? "" : value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function getRouteTitle(route) { return route.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()); } })();