规范化UI JS CONFIG
Some checks failed
build / build (api, amd64, linux) (push) Failing after -51s
build / build (api, arm64, linux) (push) Failing after -52s
build / build (api.exe, amd64, windows) (push) Failing after -51s

This commit is contained in:
CN-JS-HuiBai
2026-04-18 21:55:54 +08:00
parent 9d95a7c650
commit 8cca428d89
5 changed files with 518 additions and 518 deletions

View File

@@ -45,20 +45,20 @@
};
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: "编辑站点、订阅和安全参数。" }
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());
@@ -66,14 +66,14 @@
boot();
async function boot() {
window.addEventListener("hashchange", async function () {
window.addEventListener('hashchange', async function () {
state.route = normalizeRoute(readRoute());
state.modal = null;
await hydrateRoute();
});
root.addEventListener("click", onClick);
root.addEventListener("submit", onSubmit);
root.addEventListener('click', onClick);
root.addEventListener('submit', onSubmit);
// Initialize shell and modal containers
root.innerHTML = '<div id="admin-shell-container"></div><div id="admin-modal-container"></div><div id="admin-busy-container"></div>';
@@ -94,7 +94,7 @@
async function loadBootstrap() {
try {
setBusy(true);
const loginCheck = unwrap(await request("/api/v1/user/checkLogin", { method: "GET" }));
const loginCheck = unwrap(await request('/api/v1/user/checkLogin', { method: 'GET' }));
if (!loginCheck || !loginCheck.is_admin) {
clearSession();
return;
@@ -102,15 +102,15 @@
state.user = loginCheck;
const [config, system] = await Promise.all([
request(cfg.api.adminConfig, { method: "GET" }),
request(cfg.api.systemStatus, { method: "GET" })
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);
console.error('bootstrap failed', error);
clearSession();
show(error.message || "管理端初始化失败", "error");
show(error.message || '管理端初始化失败', 'error');
} finally {
setBusy(false);
render();
@@ -127,23 +127,23 @@
setBusy(true);
const page = getCurrentPage();
if (state.route === "overview") {
if (state.route === 'overview') {
state.dashboard = unwrap(await request(cfg.api.dashboardSummary));
} else if (state.route === "dashboard-node") {
} 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") {
} 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") {
} 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)));

View File

@@ -1,11 +1,11 @@
const settings = window.settings || {};
const assetNonce = window.__ADMIN_ASSET_NONCE__ || String(Date.now());
const securePath = String(settings.secure_path || "admin").replace(/^\/+/, "");
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",
title: settings.title || 'XBoard Admin',
version: settings.version || '1.0.0',
securePath,
baseUrl: settings.base_url || window.location.origin,
api: {
@@ -27,28 +27,28 @@ window.ADMIN_APP_CONFIG = {
},
};
document.documentElement.dataset.adminExecutionMode = "main-app";
document.documentElement.dataset.adminExecutionMode = 'main-app';
function showBootError(error) {
console.error("Failed to boot admin app", error);
const root = document.getElementById("admin-app");
console.error('Failed to boot admin app', error);
const root = document.getElementById('admin-app');
if (root) {
root.innerHTML =
`<div style="padding:24px;font-family:system-ui,sans-serif;color:#b91c1c;">Admin app failed to load.<br>${String(
error && error.message ? error.message : error || "Unknown error",
error && error.message ? error.message : error || 'Unknown error',
)}</div>`;
}
}
window.addEventListener("error", (event) => {
window.addEventListener('error', (event) => {
if (!event || !event.error) {
return;
}
showBootError(event.error);
});
const script = document.createElement("script");
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"));
script.onerror = () => showBootError(new Error('Failed to load /admin-assets/app.js'));
document.body.appendChild(script);

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
(function () {
"use strict";
var theme = window.NEBULA_THEME || {};
var loader = document.getElementById("nebula-loader");
var appRoot = document.getElementById("app");
const theme = window.NEBULA_THEME || {};
const loader = document.getElementById('nebula-loader');
const appRoot = document.getElementById('app');
applyCustomBackground();
mountShell();
@@ -15,12 +15,12 @@
return;
}
var shell = document.createElement("div");
shell.className = "app-shell nebula-app-shell";
shell.id = "nebula-app-shell";
const shell = document.createElement('div');
shell.className = 'app-shell nebula-app-shell';
shell.id = 'nebula-app-shell';
shell.innerHTML = renderShellChrome();
var parent = appRoot.parentNode;
const parent = appRoot.parentNode;
parent.insertBefore(shell, appRoot);
var appStage = shell.querySelector(".nebula-app-stage");
@@ -30,10 +30,10 @@
}
function mountMonitor() {
var rail = document.getElementById("nebula-side-rail");
var panel = document.createElement("aside");
panel.className = "nebula-monitor";
panel.id = "nebula-monitor";
const rail = document.getElementById('nebula-side-rail');
const panel = document.createElement('aside');
panel.className = 'nebula-monitor';
panel.id = 'nebula-monitor';
panel.innerHTML = renderLoadingPanel();
if (rail) {
@@ -42,18 +42,18 @@
document.body.appendChild(panel);
}
panel.addEventListener("click", function (event) {
var actionEl = event.target.closest("[data-nebula-action]");
panel.addEventListener('click', (event) => {
const actionEl = event.target.closest('[data-nebula-action]');
if (!actionEl) {
return;
}
var action = actionEl.getAttribute("data-nebula-action");
if (action === "refresh") {
const action = actionEl.getAttribute('data-nebula-action');
if (action === 'refresh') {
loadDeviceOverview(panel);
}
if (action === "toggle") {
panel.classList.toggle("is-collapsed");
if (action === 'toggle') {
panel.classList.toggle('is-collapsed');
}
});
@@ -69,10 +69,10 @@
}
try {
var response = await fetch("/api/v1/user/user-online-devices/get-ip", {
method: "GET",
const response = await fetch('/api/v1/user/user-online-devices/get-ip', {
method: 'GET',
headers: getRequestHeaders(),
credentials: "same-origin"
credentials: 'same-origin'
});
if (response.status === 401 || response.status === 403) {
@@ -81,17 +81,17 @@
}
if (!response.ok) {
throw new Error("Unable to load device overview");
throw new Error('Unable to load device overview');
}
var resJson = await response.json();
var data = resJson && resJson.data ? resJson.data : {};
console.log("[Nebula] Device overview data:", data); // Diagnostic log
var overview = data.session_overview || data;
const resJson = await response.json();
const data = resJson && resJson.data ? resJson.data : {};
console.log('[Nebula] Device overview data:', data); // Diagnostic log
const overview = data.session_overview || data;
panel.innerHTML = renderPanel(overview);
} catch (error) {
panel.innerHTML = renderErrorPanel(error.message || "Unable to load device overview");
panel.innerHTML = renderErrorPanel(error.message || 'Unable to load device overview');
}
}
@@ -174,43 +174,43 @@
}
function renderPanel(data) {
var ips = Array.isArray(data.online_ips) ? data.online_ips : [];
var sessions = Array.isArray(data.sessions) ? data.sessions.slice(0, 4) : [];
const ips = Array.isArray(data.online_ips) ? data.online_ips : [];
const sessions = Array.isArray(data.sessions) ? data.sessions.slice(0, 4) : [];
return [
'<div class="nebula-monitor__head">',
'<div><h2 class="nebula-monitor__title">Live Access Monitor</h2><div class="nebula-monitor__copy">' + escapeHtml((theme.config && theme.config.slogan) || "Current IP and session visibility") + '</div></div>',
'<div><h2 class="nebula-monitor__title">Live Access Monitor</h2><div class="nebula-monitor__copy">' + escapeHtml((theme.config && theme.config.slogan) || 'Current IP and session visibility') + '</div></div>',
'<div class="nebula-monitor__actions"><button data-nebula-action="refresh">Refresh</button><button data-nebula-action="toggle">Collapse</button></div>',
'</div>',
'<div class="nebula-monitor__body">',
'<div class="nebula-monitor__section">',
'<div class="nebula-monitor__grid">',
metric(String(data.online_ip_count || 0), "Current IPs"),
metric(String(data.online_device_count || 0), "Online devices"),
metric(String(data.active_session_count || 0), "Stored sessions"),
metric(formatLimit(data.device_limit), "Device limit"),
metric(String(data.online_ip_count || 0), 'Current IPs'),
metric(String(data.online_device_count || 0), 'Online devices'),
metric(String(data.active_session_count || 0), 'Stored sessions'),
metric(formatLimit(data.device_limit), 'Device limit'),
'</div>',
'<div class="nebula-monitor__meta">Last online: ' + formatDate(data.last_online_at) + '</div>',
'</div>',
'<div class="nebula-monitor__section">',
'<div class="nebula-monitor__meta">Online IP addresses</div>',
ips.length ? '<div class="nebula-monitor__chips">' + ips.map(function (ip) {
ips.length ? '<div class="nebula-monitor__chips">' + ips.map((ip) => {
return '<div class="nebula-monitor__chip"><code>' + escapeHtml(ip) + '</code></div>';
}).join("") + '</div>' : '<div class="nebula-monitor__empty">No IP data reported yet.</div>',
}).join('') : '<div class="nebula-monitor__empty">No IP data reported yet.</div>',
'</div>',
'<div class="nebula-monitor__section">',
'<div class="nebula-monitor__meta">Recent sessions</div>',
sessions.length ? '<div class="nebula-monitor__sessions">' + sessions.map(function (session) {
sessions.length ? '<div class="nebula-monitor__sessions">' + sessions.map((session) => {
return [
'<div class="nebula-monitor__session">',
'<strong>' + escapeHtml(session.name || ("Session #" + session.id)) + (session.is_current ? " · current" : "") + '</strong>',
'<strong>' + escapeHtml(session.name || ('Session #' + session.id)) + (session.is_current ? ' · current' : '') + '</strong>',
'<div class="nebula-monitor__meta">Last used: ' + formatDate(session.last_used_at) + '</div>',
'</div>'
].join("");
}).join("") + '</div>' : '<div class="nebula-monitor__empty">No session records available.</div>',
].join('');
}).join('') : '<div class="nebula-monitor__empty">No session records available.</div>',
'</div>',
'</div>'
].join("");
].join('');
}
function metric(value, label) {
@@ -218,11 +218,11 @@
}
function getRequestHeaders() {
var headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
const headers = {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
};
var token = getStoredToken();
const token = getStoredToken();
if (token) {
headers.Authorization = token;
}
@@ -275,21 +275,21 @@
}
function getStoredToken() {
var candidates = [];
var directKeys = ["access_token", "auth_data", "__nebula_auth_data__", "token", "auth_token"];
const candidates = [];
const directKeys = ['access_token', 'auth_data', '__nebula_auth_data__', 'token', 'auth_token'];
collectStorageValues(window.localStorage, directKeys, candidates);
collectStorageValues(window.sessionStorage, directKeys, candidates);
collectCookieValues(candidates);
collectGlobalValues(candidates);
for (var i = 0; i < candidates.length; i += 1) {
var token = extractToken(candidates[i]);
for (let i = 0; i < candidates.length; i += 1) {
const token = extractToken(candidates[i]);
if (token) {
return token;
}
}
return "";
return '';
}
function collectStorageValues(storage, keys, target) {
@@ -306,15 +306,15 @@
if (!document.cookie) {
return;
}
var cookies = document.cookie.split(";");
for (var i = 0; i < cookies.length; i += 1) {
var part = cookies[i].split("=");
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i += 1) {
const part = cookies[i].split('=');
if (part.length < 2) {
continue;
}
var key = String(part[0] || "").trim();
if (key.indexOf("token") !== -1 || key.indexOf("auth") !== -1) {
pushCandidate(decodeURIComponent(part.slice(1).join("=")), target);
const key = String(part[0] || '').trim();
if (key.indexOf('token') !== -1 || key.indexOf('auth') !== -1) {
pushCandidate(decodeURIComponent(part.slice(1).join('=')), target);
}
}
}
@@ -337,29 +337,29 @@
return "";
}
if (typeof value === "string") {
var trimmed = value.trim();
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) {
return "";
return '';
}
if (trimmed.indexOf("Bearer ") === 0) {
if (trimmed.indexOf('Bearer ') === 0) {
return trimmed;
}
if (/^[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_=]+(\.[A-Za-z0-9\-_.+/=]+)?$/.test(trimmed)) {
return "Bearer " + trimmed;
return 'Bearer ' + trimmed;
}
if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf("{") === -1) {
return "Bearer " + trimmed;
if (/^[A-Za-z0-9\-_.+/=]{24,}$/.test(trimmed) && trimmed.indexOf('{') === -1) {
return 'Bearer ' + trimmed;
}
if ((trimmed.charAt(0) === "{" && trimmed.charAt(trimmed.length - 1) === "}") ||
(trimmed.charAt(0) === "[" && trimmed.charAt(trimmed.length - 1) === "]")) {
if ((trimmed.charAt(0) === '{' && trimmed.charAt(trimmed.length - 1) === '}') ||
(trimmed.charAt(0) === '[' && trimmed.charAt(trimmed.length - 1) === ']')) {
try {
return extractToken(JSON.parse(trimmed), (depth || 0) + 1);
} catch (error) {
return "";
return '';
}
}
return "";
return '';
}
if (Array.isArray(value)) {
@@ -372,22 +372,22 @@
return "";
}
if (typeof value === "object") {
var keys = ["access_token", "auth_data", "token", "Authorization", "authorization"];
for (var j = 0; j < keys.length; j += 1) {
if (typeof value === 'object') {
const keys = ['access_token', 'auth_data', 'token', 'Authorization', 'authorization'];
for (let j = 0; j < keys.length; j += 1) {
if (Object.prototype.hasOwnProperty.call(value, keys[j])) {
var directToken = extractToken(value[keys[j]], (depth || 0) + 1);
const directToken = extractToken(value[keys[j]], (depth || 0) + 1);
if (directToken) {
return directToken;
}
}
}
for (var key in value) {
for (const key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
var token = extractToken(value[key], (depth || 0) + 1);
const token = extractToken(value[key], (depth || 0) + 1);
if (token) {
return token;
}

View File

@@ -270,14 +270,14 @@ func formatTimeValue(value *time.Time) string {
func getFetchParams(c *gin.Context) map[string]string {
params := make(map[string]string)
// 1. Get from Query parameters
for k, v := range c.Request.URL.Query() {
if len(v) > 0 {
params[k] = v[0]
}
}
// 2. Override with JSON body if applicable (for POST)
if c.Request.Method == http.MethodPost {
var body map[string]any
@@ -288,6 +288,6 @@ func getFetchParams(c *gin.Context) map[string]string {
}
}
}
return params
}