From 18bbd388ea3ef71bb9c4dc6dcd8a1468488d7779 Mon Sep 17 00:00:00 2001 From: aaaamoooo Date: Tue, 20 Oct 2026 21:47:16 +0000 Subject: [PATCH] feat(web): topbar/nav cleanup batch (items 10-13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- panel-go/golden/golden.json | 4 ++-- public/app.js | 44 ++++++++++++++----------------------- public/index.html | 10 ++++----- public/styles.css | 22 ++++++++----------- 4 files changed, 32 insertions(+), 48 deletions(-) diff --git a/panel-go/golden/golden.json b/panel-go/golden/golden.json index 8346b00..0244ca6 100644 --- a/panel-go/golden/golden.json +++ b/panel-go/golden/golden.json @@ -1,6 +1,6 @@ { "meta": { - "capturedAt": "2026-10-20T21:35:58.550Z", + "capturedAt": "2026-10-20T21:46:16.902Z", "note": "Go panel golden baseline; see README.md in this directory." }, "sections": { @@ -3584,7 +3584,7 @@ }, "errors": { "badSubscription": { - "body": "\n\n\n \n \n \n \n amo\n \n \n\n\n
\n
\n
amo
\n
\n \n \n \n
\n

\n \n
\n
\n\n
\n \n\n
\n
\n amo / 系统状态\n \n \n \n \n
\n
\n
\n
\n\n
\n
\n

\n
\n
\n
\n
\n
\n \n \n\n\n", + "body": "\n\n\n \n \n \n \n amo\n \n \n\n\n
\n
\n
amo
\n
\n \n \n \n
\n

\n \n
\n
\n\n
\n \n\n
\n
\n amo / 系统状态\n \n \n \n
\n
\n
\n
\n\n
\n
\n

\n
\n
\n
\n
\n
\n \n \n\n\n", "status": 200 }, "loginFailure": { diff --git a/public/app.js b/public/app.js index 99eb2f9..2341f46 100644 --- a/public/app.js +++ b/public/app.js @@ -1,7 +1,7 @@ 'use strict'; -/* amo 控制台前端。六个页面:系统状态 / 服务器 / 客户与订阅 / 转发与节点 / - 安装与命令 / 设置与运维。hash 路由,45 秒自动刷新(弹窗打开时暂停)。 */ +/* amo 控制台前端。五个页面:系统状态 / 节点管理 / 用户订阅 / 入站&出站 / + 设置。hash 路由,45 秒自动刷新(弹窗打开时暂停)。 */ const state = { session: null, page: 'overview', overview: null, system: null, @@ -62,14 +62,7 @@ const statusText = { }; const roleText = { relay: '入口 / 转发', exit: '出口 / 落地', hybrid: '综合节点' }; 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 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')]); 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 || []; - } else if (page === 'commands') { /* 静态页 */ } - else if (page === 'operations') { + } else if (page === 'operations') { 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; $('#version-label').textContent = `v${state.version}`; @@ -274,8 +266,8 @@ async function load(page = state.page) { } catch (error) { toast(error.message, true); } } -const PAGES = ['overview', 'servers', 'customers', 'chains', 'commands', 'operations']; -const TITLES = { overview: '系统状态', servers: '服务器', customers: '客户与订阅', chains: '转发与节点', commands: '安装与命令', operations: '设置与运维' }; +const PAGES = ['overview', 'servers', 'customers', 'chains', 'operations']; +const TITLES = { overview: '系统状态', servers: '节点管理', customers: '用户订阅', chains: '入站&出站', operations: '设置' }; function pageFromHash() { const page = location.hash.replace(/^#/, '').split('?')[0]; return PAGES.includes(page) ? page : 'overview'; } function setPage(page) { 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 meanDown = history.length ? history.reduce((a, p) => a + (p.downBps || 0), 0) / history.length : 0; const degraded = (state.overview && state.overview.degraded) || []; - const events = ((state.overview && state.overview.activity) || []).map((event) => `
${esc(auditText[event.action] || event.action)} ${esc(event.actor)} · ${esc(event.target)}
`).join(''); return `
${cards}
@@ -401,8 +392,7 @@ function renderOverview() {
IP 地址
IPv4${esc((sys.ips && sys.ips.ipv4) || '—')}
IPv6${esc((sys.ips && sys.ips.ipv6) || '—')}
- ${degraded.length ? `
${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「转发与节点」查看并修复。
` : ''} -

最近活动

${events || '
暂无活动
'}
`; + ${degraded.length ? `
${degraded.length} 个节点异常:${degraded.map((n) => esc(n.name)).join('、')}。到「入站&出站」查看并修复。
` : ''}`; } /* ---------- 服务器 ---------- */ @@ -488,7 +478,7 @@ function renderServers() { ${rows || emptyRow(14, '没有匹配设备')}${pagerHTML('servers', filtered.length)}`; } -/* ---------- 客户与订阅 ---------- */ +/* ---------- 用户订阅 ---------- */ function protocolFamily(protocol) { const id = String(protocol || ''); if (id.startsWith('vless')) return { cls: '', label: 'VLESS' }; @@ -841,7 +831,7 @@ function uninstallModal() {
`); } -/* ---------- 转发与节点 ---------- */ +/* ---------- 入站&出站 ---------- */ function protoBadges(node) { const fam = (id) => { const v = String(id || ''); @@ -893,9 +883,9 @@ function renderNodes() { ${speed > 0 ? `↑ ${fmtSpeed(node.speedUpBps)} / ↓ ${fmtSpeed(node.speedDownBps)}` : '—'} ∞`; }).join(''); - return `
+ return `
- ${state.nodes.length} 个节点在线 ${active}${degraded ? `降级 ${degraded}` : ''}
+ ${state.nodes.length} 个入站在线 ${active}${degraded ? `降级 ${degraded}` : ''}
${rows || emptyRow(13, '还没有节点')}
ID⇅菜单启用备注节点端口⇅协议客户端流量⇅速度到期时间
${pagerHTML('nodes', filtered.length)}
`; } @@ -916,7 +906,7 @@ function nodeForm(item = null) {
出口与转发传输
-
+
@@ -1036,8 +1026,8 @@ function nodeMenuHTML(node) { return items.join(''); } -/* ---------- 安装与命令 ---------- */ -function renderCommands() { +/* ---------- 设置(命令速查已合并至此页) ---------- */ +function commandsHTML() { const groups = [ ['面板机', [['更新控制面', 'ng update'], ['检查面板 HTTPS', 'ng cert'], ['备份面板', 'ng backup']]], ['入口机 / 中转机', [['更新 Agent', 'ng-agent update'], ['申请或续签节点证书', 'ng-agent cert'], ['安装 sing-box 引擎', 'ng-agent engine install'], ['检查证书、引擎与双向计数', 'ng-agent doctor']]], @@ -1046,7 +1036,6 @@ function renderCommands() { return `
${groups.map(([title, commands]) => `

${esc(title)}

${commands.map(([label, command]) => `
${esc(label)}${esc(command)}
`).join('')}
`).join('')}
`; } -/* ---------- 设置与运维 ---------- */ function renderOperations() { const rows = state.jobs.slice(0, 100).map((job) => `${esc(job.action === 'apply_resource' ? '应用资源' : '移除资源')}
${esc(job.id)}
${esc((serverById(job.serverId) || {}).name || job.serverId)}${esc(statusText[job.status] || job.status)}${job.attempts}${fmtDate(job.updatedAt)}${esc(job.error || '')}`).join(''); return `
@@ -1065,11 +1054,12 @@ function renderOperations() {
Caddy HTTPS证书自动申请与续签
运行中
ng update\nng domain new.example.com\nng cert
-

任务队列

${rows || emptyRow(6, '任务队列为空')}
任务设备状态尝试更新时间错误
`; + ${commandsHTML()} +

任务队列

${rows || emptyRow(6, '任务队列为空')}
任务设备状态尝试更新时间错误
`; } 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](); } diff --git a/public/index.html b/public/index.html index 9797592..2bb09a0 100644 --- a/public/index.html +++ b/public/index.html @@ -28,11 +28,10 @@
amo
@@ -41,7 +40,6 @@
amo / 系统状态 -
diff --git a/public/styles.css b/public/styles.css index 49f1304..7ba8715 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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 b{color:var(--text);font-weight:600;font-size:15px} .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:hover{border-color:var(--primary);color:var(--primary-text)} .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-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} -.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} @@ -152,6 +144,9 @@ table{width:100%;border-collapse:collapse} table.w-servers{min-width:1390px} table.w-customers{min-width:1120px} 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 .sarr{color:var(--text-3);font-size:10px;margin-left:3px} 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} /* 页签 */ -.seg{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} -.seg button.active{background:var(--primary);color:#fff} +.segctl{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden} +.segctl button{padding:6px 16px;background:var(--card);border:none;color:var(--text-2);cursor:pointer;font:inherit;font-size:13px} +.segctl button.active{background:var(--primary);color:#fff} .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.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} /* 命令页 / 运维页 */ -.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:last-child{border-bottom:none} .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)} @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}}