规范化UI JS CONFIG
This commit is contained in:
@@ -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)));
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user