feat(web): topbar/nav cleanup batch (items 10-13)
Item 10: remove the disabled decorative topbar search input (each page has its own working search); CSS rule and media-query reference removed. Item 11: menu/title renames — 服务器 -> 节点管理, 客户与订阅 -> 用户订阅, 转发与节点 -> 入站&出站, 设置与运维 -> 设置 (nav, TITLES/breadcrumb and cross-references synced); the chains page add button is now 添加入站 and its count reads N 个入站; the 安装与命令 page is merged into 设置 (commandsHTML block between the settings grid and the jobs table) and the page itself is removed from nav, PAGES, TITLES, load() and the view map — an old #commands hash now lands on the SPA's standard overview fallback. Item 12: remove the system page's 最近活动 panel (rendering and its audit label map plus orphaned CSS); backend audit records untouched. Item 13: the settings jobs table gets the w-jobs treatment — ID, status and time cells never wrap, compact row padding, and the error column is capped at 22% width so an empty error column no longer balloons. Also fixes a class collision from the item-8 batch: the exit-mode segmented control is renamed .seg -> .segctl so it no longer draws a border box around the system page info-bar segments. Golden re-captured: the only diff is the served index HTML inside errors.badSubscription, audited character by character as exactly these renames and removals.
This commit is contained in:
File diff suppressed because one or more lines are too long
+17
-27
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* amo 控制台前端。六个页面:系统状态 / 服务器 / 客户与订阅 / 转发与节点 /
|
/* amo 控制台前端。五个页面:系统状态 / 节点管理 / 用户订阅 / 入站&出站 /
|
||||||
安装与命令 / 设置与运维。hash 路由,45 秒自动刷新(弹窗打开时暂停)。 */
|
设置。hash 路由,45 秒自动刷新(弹窗打开时暂停)。 */
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
session: null, page: 'overview', overview: null, system: null,
|
session: null, page: 'overview', overview: null, system: null,
|
||||||
@@ -62,14 +62,7 @@ const statusText = {
|
|||||||
};
|
};
|
||||||
const roleText = { relay: '入口 / 转发', exit: '出口 / 落地', hybrid: '综合节点' };
|
const roleText = { relay: '入口 / 转发', exit: '出口 / 落地', hybrid: '综合节点' };
|
||||||
const topologyText = { forward: '转发线路', direct: '单机直连' };
|
const topologyText = { forward: '转发线路', direct: '单机直连' };
|
||||||
const auditText = {
|
|
||||||
create_server: '添加设备', update_server: '编辑设备', delete_server: '删除设备', create_enrollment: '生成注册令牌', enroll_agent: 'Agent 注册',
|
|
||||||
reconcile_server: '设备配置对账', set_tls_domain: '节点证书同步', create_customer: '添加客户', update_customer: '编辑客户', reset_customer_usage: '重置客户流量', delete_customer: '删除客户',
|
|
||||||
create_node: '创建节点', update_node: '编辑节点', deploy_node: '部署节点', repair_node: '修复节点', remove_node: '停用节点', reorder_nodes: '节点排序',
|
|
||||||
delete_node: '删除节点', assign_customer: '分配客户', unassign_customer: '移除分配', suspend_customer_resources: '暂停客户资源', resume_customer_resources: '恢复客户资源',
|
|
||||||
retry_cleanup: '重试遗留清理', force_forget_server: '强制遗忘离线设备', update_account: '修改管理员账号', suspend_ip_limit: 'IP 超限停用', rotate_subscription: '重置订阅链接',
|
|
||||||
regenerate_credentials: '重新生成凭据', monthly_reset: '按月重置流量'
|
|
||||||
};
|
|
||||||
const suspendReasonText = { traffic_limit: '额度耗尽', expired: '已到期', ip_limit: '节点 IP 超限', manual: '手动停用' };
|
const suspendReasonText = { traffic_limit: '额度耗尽', expired: '已到期', ip_limit: '节点 IP 超限', manual: '手动停用' };
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -264,8 +257,7 @@ async function load(page = state.page) {
|
|||||||
const [servers, customers, routes, protocols] = await Promise.all([api('/api/servers'), api('/api/customers'), api('/api/nodes'), api('/api/protocols')]);
|
const [servers, customers, routes, protocols] = await Promise.all([api('/api/servers'), api('/api/customers'), api('/api/nodes'), api('/api/protocols')]);
|
||||||
state.servers = servers.servers; state.customers = customers.customers; state.customerSummary = customers.summary || null; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients;
|
state.servers = servers.servers; state.customers = customers.customers; state.customerSummary = customers.summary || null; state.nodes = routes.nodes; state.nodeClients = routes.nodeClients;
|
||||||
state.protocols = protocols.profiles; state.realityPresets = protocols.realityPresets || [];
|
state.protocols = protocols.profiles; state.realityPresets = protocols.realityPresets || [];
|
||||||
} else if (page === 'commands') { /* 静态页 */ }
|
} else if (page === 'operations') {
|
||||||
else if (page === 'operations') {
|
|
||||||
const [jobs, overview] = await Promise.all([api('/api/jobs'), api('/api/overview')]);
|
const [jobs, overview] = await Promise.all([api('/api/jobs'), api('/api/overview')]);
|
||||||
state.jobs = jobs.jobs; state.overview = overview; state.version = overview.version || state.version;
|
state.jobs = jobs.jobs; state.overview = overview; state.version = overview.version || state.version;
|
||||||
$('#version-label').textContent = `v${state.version}`;
|
$('#version-label').textContent = `v${state.version}`;
|
||||||
@@ -274,8 +266,8 @@ async function load(page = state.page) {
|
|||||||
} catch (error) { toast(error.message, true); }
|
} catch (error) { toast(error.message, true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGES = ['overview', 'servers', 'customers', 'chains', 'commands', 'operations'];
|
const PAGES = ['overview', 'servers', 'customers', 'chains', 'operations'];
|
||||||
const TITLES = { overview: '系统状态', servers: '服务器', customers: '客户与订阅', chains: '转发与节点', commands: '安装与命令', operations: '设置与运维' };
|
const TITLES = { overview: '系统状态', servers: '节点管理', customers: '用户订阅', chains: '入站&出站', operations: '设置' };
|
||||||
function pageFromHash() { const page = location.hash.replace(/^#/, '').split('?')[0]; return PAGES.includes(page) ? page : 'overview'; }
|
function pageFromHash() { const page = location.hash.replace(/^#/, '').split('?')[0]; return PAGES.includes(page) ? page : 'overview'; }
|
||||||
function setPage(page) {
|
function setPage(page) {
|
||||||
if (!PAGES.includes(page)) page = 'overview';
|
if (!PAGES.includes(page)) page = 'overview';
|
||||||
@@ -370,7 +362,6 @@ function renderOverview() {
|
|||||||
const meanUp = history.length ? history.reduce((a, p) => a + (p.upBps || 0), 0) / history.length : 0;
|
const meanUp = history.length ? history.reduce((a, p) => a + (p.upBps || 0), 0) / history.length : 0;
|
||||||
const meanDown = history.length ? history.reduce((a, p) => a + (p.downBps || 0), 0) / history.length : 0;
|
const meanDown = history.length ? history.reduce((a, p) => a + (p.downBps || 0), 0) / history.length : 0;
|
||||||
const degraded = (state.overview && state.overview.degraded) || [];
|
const degraded = (state.overview && state.overview.degraded) || [];
|
||||||
const events = ((state.overview && state.overview.activity) || []).map((event) => `<div class="event"><i class="event-dot"></i><div><b>${esc(auditText[event.action] || event.action)}</b> <small>${esc(event.actor)} · ${esc(event.target)}</small></div><time>${fmtDate(event.at)}</time></div>`).join('');
|
|
||||||
return `<section class="stat-grid">${cards}</section>
|
return `<section class="stat-grid">${cards}</section>
|
||||||
<section class="mid-grid">
|
<section class="mid-grid">
|
||||||
<article class="panel chart-card">
|
<article class="panel chart-card">
|
||||||
@@ -401,8 +392,7 @@ function renderOverview() {
|
|||||||
<div class="seg"><div class="cap"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 3.8 5.7 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.7-3.8-9S9.5 5.6 12 3z"/></svg>IP 地址</div>
|
<div class="seg"><div class="cap"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 3.8 5.7 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.7-3.8-9S9.5 5.6 12 3z"/></svg>IP 地址</div>
|
||||||
<div class="kv"><div class="item"><span class="k">IPv4</span><span class="v mono">${esc((sys.ips && sys.ips.ipv4) || '—')}</span></div><div class="item"><span class="k">IPv6</span><span class="v mono">${esc((sys.ips && sys.ips.ipv6) || '—')}</span></div></div></div>
|
<div class="kv"><div class="item"><span class="k">IPv4</span><span class="v mono">${esc((sys.ips && sys.ips.ipv4) || '—')}</span></div><div class="item"><span class="k">IPv6</span><span class="v mono">${esc((sys.ips && sys.ips.ipv6) || '—')}</span></div></div></div>
|
||||||
</section>
|
</section>
|
||||||
${degraded.length ? `<div class="notice">${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「转发与节点」查看并修复。</div>` : ''}
|
${degraded.length ? `<div class="notice">${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「入站&出站」查看并修复。</div>` : ''}`;
|
||||||
<section class="panel"><div class="panel-head"><h2>最近活动</h2></div><div class="activity-list">${events || '<div class="empty"><b>暂无活动</b></div>'}</div></section>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 服务器 ---------- */
|
/* ---------- 服务器 ---------- */
|
||||||
@@ -488,7 +478,7 @@ function renderServers() {
|
|||||||
<tbody>${rows || emptyRow(14, '没有匹配设备')}</tbody></table></div>${pagerHTML('servers', filtered.length)}</section>`;
|
<tbody>${rows || emptyRow(14, '没有匹配设备')}</tbody></table></div>${pagerHTML('servers', filtered.length)}</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 客户与订阅 ---------- */
|
/* ---------- 用户订阅 ---------- */
|
||||||
function protocolFamily(protocol) {
|
function protocolFamily(protocol) {
|
||||||
const id = String(protocol || '');
|
const id = String(protocol || '');
|
||||||
if (id.startsWith('vless')) return { cls: '', label: 'VLESS' };
|
if (id.startsWith('vless')) return { cls: '', label: 'VLESS' };
|
||||||
@@ -841,7 +831,7 @@ function uninstallModal() {
|
|||||||
<div class="form-actions"><button type="button" class="btn" data-close>关闭</button></div></div>`);
|
<div class="form-actions"><button type="button" class="btn" data-close>关闭</button></div></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 转发与节点 ---------- */
|
/* ---------- 入站&出站 ---------- */
|
||||||
function protoBadges(node) {
|
function protoBadges(node) {
|
||||||
const fam = (id) => {
|
const fam = (id) => {
|
||||||
const v = String(id || '');
|
const v = String(id || '');
|
||||||
@@ -893,9 +883,9 @@ function renderNodes() {
|
|||||||
<td>${speed > 0 ? `<span class="pill">↑ ${fmtSpeed(node.speedUpBps)} / ↓ ${fmtSpeed(node.speedDownBps)}</span>` : '<span class="pill mut">—</span>'}</td>
|
<td>${speed > 0 ? `<span class="pill">↑ ${fmtSpeed(node.speedUpBps)} / ↓ ${fmtSpeed(node.speedDownBps)}</span>` : '<span class="pill mut">—</span>'}</td>
|
||||||
<td><span class="pill">∞</span></td></tr>`;
|
<td><span class="pill">∞</span></td></tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
return `<div class="page-tools"><button class="btn primary" data-action="add-node">+ 新建节点</button>
|
return `<div class="page-tools"><button class="btn primary" data-action="add-node">+ 添加入站</button>
|
||||||
<div class="toolbar"><input class="search" data-search placeholder="搜索节点、服务器或协议" value="${esc(state.search)}">
|
<div class="toolbar"><input class="search" data-search placeholder="搜索节点、服务器或协议" value="${esc(state.search)}">
|
||||||
<span class="badge blue">${state.nodes.length} 个节点</span><span class="badge green">在线 ${active}</span>${degraded ? `<span class="badge orange">降级 ${degraded}</span>` : ''}</div></div>
|
<span class="badge blue">${state.nodes.length} 个入站</span><span class="badge green">在线 ${active}</span>${degraded ? `<span class="badge orange">降级 ${degraded}</span>` : ''}</div></div>
|
||||||
<section class="panel"><div class="table-wrap visible"><table class="w-nodes"><thead><tr><th></th><th><input type="checkbox" data-check-all></th><th>ID<span class="sarr">⇅</span></th><th>菜单</th><th>启用</th><th>备注</th><th>节点</th><th>端口<span class="sarr">⇅</span></th><th>协议</th><th>客户端</th><th>流量<span class="sarr">⇅</span></th><th>速度</th><th>到期时间</th></tr></thead>
|
<section class="panel"><div class="table-wrap visible"><table class="w-nodes"><thead><tr><th></th><th><input type="checkbox" data-check-all></th><th>ID<span class="sarr">⇅</span></th><th>菜单</th><th>启用</th><th>备注</th><th>节点</th><th>端口<span class="sarr">⇅</span></th><th>协议</th><th>客户端</th><th>流量<span class="sarr">⇅</span></th><th>速度</th><th>到期时间</th></tr></thead>
|
||||||
<tbody>${rows || emptyRow(13, '还没有节点')}</tbody></table></div>${pagerHTML('nodes', filtered.length)}</section>`;
|
<tbody>${rows || emptyRow(13, '还没有节点')}</tbody></table></div>${pagerHTML('nodes', filtered.length)}</section>`;
|
||||||
}
|
}
|
||||||
@@ -916,7 +906,7 @@ function nodeForm(item = null) {
|
|||||||
<label data-reality-only>Reality SNI / 目标端口<div class="inline-fields"><input name="realityServerName" value="${esc(node.realityServerName || 'www.tesla.com')}" placeholder="www.tesla.com"><input name="realityDestPort" type="number" value="${Number(node.realityDestPort || 443)}" min="1" max="65535"></div></label>
|
<label data-reality-only>Reality SNI / 目标端口<div class="inline-fields"><input name="realityServerName" value="${esc(node.realityServerName || 'www.tesla.com')}" placeholder="www.tesla.com"><input name="realityDestPort" type="number" value="${Number(node.realityDestPort || 443)}" min="1" max="65535"></div></label>
|
||||||
<label>拓扑<select name="topology" data-node-sync><option value="forward"${selected('forward', node.topology || 'forward')}>转发(入口 → 出口)</option><option value="direct"${selected('direct', node.topology)}>单机直连(无需出口)</option></select></label>
|
<label>拓扑<select name="topology" data-node-sync><option value="forward"${selected('forward', node.topology || 'forward')}>转发(入口 → 出口)</option><option value="direct"${selected('direct', node.topology)}>单机直连(无需出口)</option></select></label>
|
||||||
<div class="section-title" data-forward-only>出口与转发传输</div>
|
<div class="section-title" data-forward-only>出口与转发传输</div>
|
||||||
<div class="wide" data-forward-only><div class="seg" id="exit-mode-seg"><button type="button" data-action="exit-mode" data-mode="managed">受管设备</button><button type="button" data-action="exit-mode" data-mode="manual">手动落地端点</button></div></div>
|
<div class="wide" data-forward-only><div class="segctl" id="exit-mode-seg"><button type="button" data-action="exit-mode" data-mode="managed">受管设备</button><button type="button" data-action="exit-mode" data-mode="manual">手动落地端点</button></div></div>
|
||||||
<label class="wide" data-forward-only data-manual-only>从链接导入<input data-exit-link placeholder="粘贴 vless:// / ss:// / socks5:// 链接,自动填入下方字段"></label>
|
<label class="wide" data-forward-only data-manual-only>从链接导入<input data-exit-link placeholder="粘贴 vless:// / ss:// / socks5:// 链接,自动填入下方字段"></label>
|
||||||
<label data-forward-only data-managed-only>出口 / 落地设备<select name="exitServerId">${state.servers.map((server) => `<option value="${esc(server.id)}"${selected(server.id, node.exitServerId)}>${esc(server.name)} · ${esc(roleText[server.role])}</option>`).join('')}</select></label>
|
<label data-forward-only data-managed-only>出口 / 落地设备<select name="exitServerId">${state.servers.map((server) => `<option value="${esc(server.id)}"${selected(server.id, node.exitServerId)}>${esc(server.name)} · ${esc(roleText[server.role])}</option>`).join('')}</select></label>
|
||||||
<label data-forward-only data-manual-only>落地地址<input name="exitAddress" value="${esc(node.exitAddress || '')}" placeholder="IP 或域名"></label>
|
<label data-forward-only data-manual-only>落地地址<input name="exitAddress" value="${esc(node.exitAddress || '')}" placeholder="IP 或域名"></label>
|
||||||
@@ -1036,8 +1026,8 @@ function nodeMenuHTML(node) {
|
|||||||
return items.join('');
|
return items.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 安装与命令 ---------- */
|
/* ---------- 设置(命令速查已合并至此页) ---------- */
|
||||||
function renderCommands() {
|
function commandsHTML() {
|
||||||
const groups = [
|
const groups = [
|
||||||
['面板机', [['更新控制面', 'ng update'], ['检查面板 HTTPS', 'ng cert'], ['备份面板', 'ng backup']]],
|
['面板机', [['更新控制面', 'ng update'], ['检查面板 HTTPS', 'ng cert'], ['备份面板', 'ng backup']]],
|
||||||
['入口机 / 中转机', [['更新 Agent', 'ng-agent update'], ['申请或续签节点证书', 'ng-agent cert'], ['安装 sing-box 引擎', 'ng-agent engine install'], ['检查证书、引擎与双向计数', 'ng-agent doctor']]],
|
['入口机 / 中转机', [['更新 Agent', 'ng-agent update'], ['申请或续签节点证书', 'ng-agent cert'], ['安装 sing-box 引擎', 'ng-agent engine install'], ['检查证书、引擎与双向计数', 'ng-agent doctor']]],
|
||||||
@@ -1046,7 +1036,6 @@ function renderCommands() {
|
|||||||
return `<div class="command-grid">${groups.map(([title, commands]) => `<section class="panel"><div class="panel-head"><h2>${esc(title)}</h2></div><div class="panel-body">${commands.map(([label, command]) => `<div class="command-item"><div><b>${esc(label)}</b><code>${esc(command)}</code></div><button type="button" class="btn small" data-action="copy-text" data-value="${esc(command)}">复制命令</button></div>`).join('')}</div></section>`).join('')}</div>`;
|
return `<div class="command-grid">${groups.map(([title, commands]) => `<section class="panel"><div class="panel-head"><h2>${esc(title)}</h2></div><div class="panel-body">${commands.map(([label, command]) => `<div class="command-item"><div><b>${esc(label)}</b><code>${esc(command)}</code></div><button type="button" class="btn small" data-action="copy-text" data-value="${esc(command)}">复制命令</button></div>`).join('')}</div></section>`).join('')}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 设置与运维 ---------- */
|
|
||||||
function renderOperations() {
|
function renderOperations() {
|
||||||
const rows = state.jobs.slice(0, 100).map((job) => `<tr><td><b>${esc(job.action === 'apply_resource' ? '应用资源' : '移除资源')}</b><div class="mut" style="font-size:12px">${esc(job.id)}</div></td><td>${esc((serverById(job.serverId) || {}).name || job.serverId)}</td><td>${esc(statusText[job.status] || job.status)}</td><td>${job.attempts}</td><td>${fmtDate(job.updatedAt)}</td><td><span class="mut">${esc(job.error || '')}</span></td></tr>`).join('');
|
const rows = state.jobs.slice(0, 100).map((job) => `<tr><td><b>${esc(job.action === 'apply_resource' ? '应用资源' : '移除资源')}</b><div class="mut" style="font-size:12px">${esc(job.id)}</div></td><td>${esc((serverById(job.serverId) || {}).name || job.serverId)}</td><td>${esc(statusText[job.status] || job.status)}</td><td>${job.attempts}</td><td>${fmtDate(job.updatedAt)}</td><td><span class="mut">${esc(job.error || '')}</span></td></tr>`).join('');
|
||||||
return `<section class="grid-3">
|
return `<section class="grid-3">
|
||||||
@@ -1065,11 +1054,12 @@ function renderOperations() {
|
|||||||
<div class="health-row"><div><b>Caddy HTTPS</b><span>证书自动申请与续签</span></div><span class="badge green">运行中</span></div>
|
<div class="health-row"><div><b>Caddy HTTPS</b><span>证书自动申请与续签</span></div><span class="badge green">运行中</span></div>
|
||||||
<div class="codebox" style="margin-top:10px">ng update\nng domain new.example.com\nng cert</div></div></article>
|
<div class="codebox" style="margin-top:10px">ng update\nng domain new.example.com\nng cert</div></div></article>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel"><div class="panel-head"><h2>任务队列</h2></div><div class="table-wrap"><table><thead><tr><th>任务</th><th>设备</th><th>状态</th><th>尝试</th><th>更新时间</th><th>错误</th></tr></thead><tbody>${rows || emptyRow(6, '任务队列为空')}</tbody></table></div></section>`;
|
${commandsHTML()}
|
||||||
|
<section class="panel"><div class="panel-head"><h2>任务队列</h2></div><div class="table-wrap"><table class="w-jobs"><thead><tr><th>任务</th><th>设备</th><th>状态</th><th>尝试</th><th>更新时间</th><th>错误</th></tr></thead><tbody>${rows || emptyRow(6, '任务队列为空')}</tbody></table></div></section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const views = { overview: renderOverview, servers: renderServers, customers: renderCustomers, chains: renderNodes, commands: renderCommands, operations: renderOperations };
|
const views = { overview: renderOverview, servers: renderServers, customers: renderCustomers, chains: renderNodes, operations: renderOperations };
|
||||||
$('#content').innerHTML = views[state.page]();
|
$('#content').innerHTML = views[state.page]();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-6
@@ -28,11 +28,10 @@
|
|||||||
<div class="brand">amo</div>
|
<div class="brand">amo</div>
|
||||||
<nav class="menu" id="nav">
|
<nav class="menu" id="nav">
|
||||||
<a class="mi" data-page="overview" href="#overview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12l9-9 9 9"/><path d="M5 10v10h14V10"/></svg>系统状态</a>
|
<a class="mi" data-page="overview" href="#overview"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12l9-9 9 9"/><path d="M5 10v10h14V10"/></svg>系统状态</a>
|
||||||
<a class="mi" data-page="servers" href="#servers"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/></svg>服务器</a>
|
<a class="mi" data-page="servers" href="#servers"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="7" rx="2"/><rect x="3" y="13" width="18" height="7" rx="2"/><path d="M7 7.5h.01M7 16.5h.01"/></svg>节点管理</a>
|
||||||
<a class="mi" data-page="customers" href="#customers"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.2 3.4-5 6.5-5s5.7 1.8 6.5 5"/><path d="M16 4.6a3.5 3.5 0 010 6.8M21.5 20c-.6-2.6-2.2-4.2-4.5-4.8"/></svg>客户与订阅</a>
|
<a class="mi" data-page="customers" href="#customers"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.2 3.4-5 6.5-5s5.7 1.8 6.5 5"/><path d="M16 4.6a3.5 3.5 0 010 6.8M21.5 20c-.6-2.6-2.2-4.2-4.5-4.8"/></svg>用户订阅</a>
|
||||||
<a class="mi" data-page="chains" href="#chains"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="2.5"/><circle cx="19" cy="6" r="2.5"/><circle cx="19" cy="18" r="2.5"/><path d="M7.3 10.8l9.4-3.6M7.3 13.2l9.4 3.6"/></svg>转发与节点</a>
|
<a class="mi" data-page="chains" href="#chains"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="5" cy="12" r="2.5"/><circle cx="19" cy="6" r="2.5"/><circle cx="19" cy="18" r="2.5"/><path d="M7.3 10.8l9.4-3.6M7.3 13.2l9.4 3.6"/></svg>入站&出站</a>
|
||||||
<a class="mi" data-page="commands" href="#commands"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-5-6-5M12 19h8"/></svg>安装与命令</a>
|
<a class="mi" data-page="operations" href="#operations"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 00-.1-1.2l2-1.5-2-3.4-2.3 1a7 7 0 00-2-1.2L14.2 3h-4l-.4 2.5a7 7 0 00-2 1.2l-2.3-1-2 3.4 2 1.5A7 7 0 005 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.4 2.3-1a7 7 0 002 1.2l.4 2.7h4l.4-2.5a7 7 0 002-1.2l2.3 1 2-3.4-2-1.5c.1-.4.1-.8.1-1.2z"/></svg>设置</a>
|
||||||
<a class="mi" data-page="operations" href="#operations"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 00-.1-1.2l2-1.5-2-3.4-2.3 1a7 7 0 00-2-1.2L14.2 3h-4l-.4 2.5a7 7 0 00-2 1.2l-2.3-1-2 3.4 2 1.5A7 7 0 005 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.4 2.3-1a7 7 0 002 1.2l.4 2.7h4l.4-2.5a7 7 0 002-1.2l2.3 1 2-3.4-2-1.5c.1-.4.1-.8.1-1.2z"/></svg>设置与运维</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sider-foot"><span id="user-chip"></span><span class="foot-right"><span id="version-label"></span><button id="logout" class="icon-btn" type="button" data-tip="退出登录"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></svg></button></span></div>
|
<div class="sider-foot"><span id="user-chip"></span><span class="foot-right"><span id="version-label"></span><button id="logout" class="icon-btn" type="button" data-tip="退出登录"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><path d="M16 17l5-5-5-5M21 12H9"/></svg></button></span></div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -41,7 +40,6 @@
|
|||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<span class="crumb">amo / <b id="page-title">系统状态</b></span>
|
<span class="crumb">amo / <b id="page-title">系统状态</b></span>
|
||||||
<span class="sp"></span>
|
<span class="sp"></span>
|
||||||
<input class="top-search" placeholder="搜索服务器、客户、节点…" disabled>
|
|
||||||
<button class="theme-btn" id="refresh" type="button" title="刷新数据">刷新</button>
|
<button class="theme-btn" id="refresh" type="button" title="刷新数据">刷新</button>
|
||||||
<button class="theme-btn" type="button" data-theme-toggle>🌙 深色</button>
|
<button class="theme-btn" type="button" data-theme-toggle>🌙 深色</button>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
+9
-13
@@ -65,7 +65,6 @@ input,select,textarea{font-family:var(--font);color:var(--text)}
|
|||||||
.crumb{color:var(--text-3);font-size:13px;white-space:nowrap}
|
.crumb{color:var(--text-3);font-size:13px;white-space:nowrap}
|
||||||
.crumb b{color:var(--text);font-weight:600;font-size:15px}
|
.crumb b{color:var(--text);font-weight:600;font-size:15px}
|
||||||
.topbar .sp{flex:1}
|
.topbar .sp{flex:1}
|
||||||
.top-search{background:var(--card);border:1px solid var(--border);border-radius:6px;color:var(--text-2);padding:5px 12px;font-size:13px;width:230px}
|
|
||||||
.theme-btn{border:1px solid var(--border);background:var(--card);color:var(--text-2);border-radius:6px;padding:5px 11px;font-size:12.5px;cursor:pointer;white-space:nowrap}
|
.theme-btn{border:1px solid var(--border);background:var(--card);color:var(--text-2);border-radius:6px;padding:5px 11px;font-size:12.5px;cursor:pointer;white-space:nowrap}
|
||||||
.theme-btn:hover{border-color:var(--primary);color:var(--primary-text)}
|
.theme-btn:hover{border-color:var(--primary);color:var(--primary-text)}
|
||||||
.content{padding:24px;width:100%}
|
.content{padding:24px;width:100%}
|
||||||
@@ -137,13 +136,6 @@ select.search{width:auto}
|
|||||||
.stop-up-a{stop-color:var(--primary);stop-opacity:.26}
|
.stop-up-a{stop-color:var(--primary);stop-opacity:.26}
|
||||||
.stop-up-b{stop-color:var(--primary);stop-opacity:0}
|
.stop-up-b{stop-color:var(--primary);stop-opacity:0}
|
||||||
.notice{border:1px solid var(--orange);background:var(--orange-soft);color:var(--orange-text);border-radius:6px;padding:8px 12px;font-size:13px;margin-bottom:16px}
|
.notice{border:1px solid var(--orange);background:var(--orange-soft);color:var(--orange-text);border-radius:6px;padding:8px 12px;font-size:13px;margin-bottom:16px}
|
||||||
.activity-list{display:flex;flex-direction:column}
|
|
||||||
.activity-list .event{display:flex;align-items:center;gap:10px;padding:7px 16px;border-bottom:1px solid var(--border-soft);font-size:13px}
|
|
||||||
.activity-list .event:last-child{border-bottom:none}
|
|
||||||
.activity-list .event b{font-weight:500}
|
|
||||||
.activity-list .event small{color:var(--text-3)}
|
|
||||||
.activity-list .event time{margin-left:auto;color:var(--text-3);font-size:12px;white-space:nowrap}
|
|
||||||
.event-dot{width:7px;height:7px;border-radius:50%;background:var(--primary);flex:none}
|
|
||||||
|
|
||||||
/* ---------- 表格通用 ---------- */
|
/* ---------- 表格通用 ---------- */
|
||||||
.table-wrap{overflow-x:auto}
|
.table-wrap{overflow-x:auto}
|
||||||
@@ -152,6 +144,9 @@ table{width:100%;border-collapse:collapse}
|
|||||||
table.w-servers{min-width:1390px}
|
table.w-servers{min-width:1390px}
|
||||||
table.w-customers{min-width:1120px}
|
table.w-customers{min-width:1120px}
|
||||||
table.w-nodes{min-width:1180px}
|
table.w-nodes{min-width:1180px}
|
||||||
|
table.w-jobs td{white-space:nowrap;padding-top:5px;padding-bottom:5px}
|
||||||
|
table.w-jobs th:last-child{width:22%}
|
||||||
|
table.w-jobs td:last-child{white-space:normal;min-width:120px;word-break:break-word}
|
||||||
thead th{text-align:left;font-size:12.5px;font-weight:600;color:var(--text-2);letter-spacing:.3px;padding:7px 12px;border-bottom:1px solid var(--border-soft);background:var(--th-bg);white-space:nowrap}
|
thead th{text-align:left;font-size:12.5px;font-weight:600;color:var(--text-2);letter-spacing:.3px;padding:7px 12px;border-bottom:1px solid var(--border-soft);background:var(--th-bg);white-space:nowrap}
|
||||||
thead th .sarr{color:var(--text-3);font-size:10px;margin-left:3px}
|
thead th .sarr{color:var(--text-3);font-size:10px;margin-left:3px}
|
||||||
thead th.ctr,tbody td.ctr{text-align:center}
|
thead th.ctr,tbody td.ctr{text-align:center}
|
||||||
@@ -322,9 +317,9 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri
|
|||||||
.mono{font-family:var(--mono);font-size:12.5px;word-break:break-all}
|
.mono{font-family:var(--mono);font-size:12.5px;word-break:break-all}
|
||||||
|
|
||||||
/* 页签 */
|
/* 页签 */
|
||||||
.seg{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden}
|
.segctl{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden}
|
||||||
.seg button{padding:6px 16px;background:var(--card);border:none;color:var(--text-2);cursor:pointer;font:inherit;font-size:13px}
|
.segctl button{padding:6px 16px;background:var(--card);border:none;color:var(--text-2);cursor:pointer;font:inherit;font-size:13px}
|
||||||
.seg button.active{background:var(--primary);color:#fff}
|
.segctl button.active{background:var(--primary);color:#fff}
|
||||||
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--border-soft);margin-bottom:14px}
|
.tabs{display:flex;gap:2px;border-bottom:1px solid var(--border-soft);margin-bottom:14px}
|
||||||
.tabs button{border:none;background:transparent;padding:7px 14px;font-size:13.5px;color:var(--text-2);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
|
.tabs button{border:none;background:transparent;padding:7px 14px;font-size:13.5px;color:var(--text-2);cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}
|
||||||
.tabs button.active{color:var(--primary-text);border-bottom-color:var(--primary);font-weight:500}
|
.tabs button.active{color:var(--primary-text);border-bottom-color:var(--primary);font-weight:500}
|
||||||
@@ -379,7 +374,8 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri
|
|||||||
.warn-note{border:1px solid var(--orange);background:var(--orange-soft);color:var(--orange-text);border-radius:6px;padding:8px 12px;font-size:13px}
|
.warn-note{border:1px solid var(--orange);background:var(--orange-soft);color:var(--orange-text);border-radius:6px;padding:8px 12px;font-size:13px}
|
||||||
|
|
||||||
/* 命令页 / 运维页 */
|
/* 命令页 / 运维页 */
|
||||||
.command-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}
|
.command-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;margin-bottom:16px}
|
||||||
|
.command-grid .panel{margin-bottom:0}
|
||||||
.command-item{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border-soft)}
|
.command-item{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border-soft)}
|
||||||
.command-item:last-child{border-bottom:none}
|
.command-item:last-child{border-bottom:none}
|
||||||
.command-item>div{flex:1;min-width:0}
|
.command-item>div{flex:1;min-width:0}
|
||||||
@@ -406,4 +402,4 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri
|
|||||||
.toast.error{border-left-color:var(--red)}
|
.toast.error{border-left-color:var(--red)}
|
||||||
|
|
||||||
@media (max-width:1100px){.stat-grid{grid-template-columns:repeat(2,1fr)}.mid-grid{grid-template-columns:1fr}.info-bar{flex-direction:column;gap:14px}.info-bar .seg{padding:0}.info-bar .seg+.seg{border-left:none}.stats{grid-template-columns:repeat(2,1fr)}.stats.six{grid-template-columns:repeat(3,1fr)}}
|
@media (max-width:1100px){.stat-grid{grid-template-columns:repeat(2,1fr)}.mid-grid{grid-template-columns:1fr}.info-bar{flex-direction:column;gap:14px}.info-bar .seg{padding:0}.info-bar .seg+.seg{border-left:none}.stats{grid-template-columns:repeat(2,1fr)}.stats.six{grid-template-columns:repeat(3,1fr)}}
|
||||||
@media (max-width:760px){.sider{display:none}.form-grid{grid-template-columns:1fr}.top-search{display:none}.content{padding:8px}}
|
@media (max-width:760px){.sider{display:none}.form-grid{grid-template-columns:1fr}.content{padding:8px}}
|
||||||
|
|||||||
Reference in New Issue
Block a user