From 1b32b57377367be612a37d604b4d4c67bd75d406 Mon Sep 17 00:00:00 2001 From: aaaamoooo Date: Tue, 20 Oct 2026 20:52:38 +0000 Subject: [PATCH] =?UTF-8?q?feat(web):=20UI=20batch=20=E2=80=94=20content?= =?UTF-8?q?=20width,=20latency=20removal,=20icon=20tooltips,=20avatar=20re?= =?UTF-8?q?moval=20(round-2=20#3/#5/#6/#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3 .content: drop max-width:1460px centering, padding 24px full width (3x-ui content-area parity), 8px under the 760px breakpoint. #5 servers page: remove latency column + average-latency stat cell (stats grid 4->3, table 15->14 cols incl. detail colspan/empty row). #6 icon buttons: native title replaced by data-tip with a 3x-ui style bubble (black/white, rounded, arrow, body-level fixed layer so table scroll containers cannot clip it); customer QR tip renamed to 二维码; covers customers/servers/nodes rows, credentials pane, links pane, address eye toggle, drag grip, filter chip clear, logout, modal close. #9 remove decorative topbar avatar span#avatar, its showApp assignment and the dead .avatar CSS rule. Refresh button untouched. Golden re-captured: sole diff was errors.badSubscription body (the SPA index.html served there) reflecting the avatar/data-tip changes. --- panel-go/golden/golden.json | 4 +-- public/app.js | 63 +++++++++++++++++++++++++++---------- public/index.html | 5 ++- public/styles.css | 11 ++++--- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/panel-go/golden/golden.json b/panel-go/golden/golden.json index b2683e3..cc07fa5 100644 --- a/panel-go/golden/golden.json +++ b/panel-go/golden/golden.json @@ -1,6 +1,6 @@ { "meta": { - "capturedAt": "2026-10-20T20:48:49.288Z", + "capturedAt": "2026-10-20T20:52:29.987Z", "note": "Go panel golden baseline; see README.md in this directory." }, "sections": { @@ -3462,7 +3462,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\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\n", "status": 200 }, "loginFailure": { diff --git a/public/app.js b/public/app.js index f097084..7ee0ec3 100644 --- a/public/app.js +++ b/public/app.js @@ -144,7 +144,6 @@ function showLogin() { function showApp() { $('#login').classList.add('hidden'); $('#app').classList.remove('hidden'); $('#user-chip').textContent = state.session.user.username; - $('#avatar').textContent = (state.session.user.username || '管').slice(0, 1).toUpperCase(); $('#version-label').textContent = `v${state.version}`; } @@ -197,6 +196,38 @@ function showPop(anchor, html) { } function hidePop() { const pop = $('#float-pop'); pop.classList.add('hidden'); pop.innerHTML = ''; pop._anchor = null; } +/* ---------- 图标悬停提示(data-tip,3x-ui 式气泡,挂 body 防裁剪) ---------- */ +let tipBubble = null; +let tipTarget = null; +function showTip(el) { + const text = el.getAttribute('data-tip'); + if (!text) return; + if (!tipBubble) { + tipBubble = document.createElement('div'); + tipBubble.className = 'tip-bubble'; + document.body.appendChild(tipBubble); + } + tipTarget = el; + tipBubble.textContent = text; + tipBubble.classList.add('show'); + const rect = el.getBoundingClientRect(); + const bw = tipBubble.offsetWidth; + const bh = tipBubble.offsetHeight; + let left = rect.left + rect.width / 2 - bw / 2; + left = Math.max(6, Math.min(left, window.innerWidth - bw - 6)); + tipBubble.style.left = `${left}px`; + tipBubble.style.top = `${Math.max(4, rect.top - bh - 9)}px`; + tipBubble.style.setProperty('--tip-arrow-left', `${rect.left + rect.width / 2 - left}px`); +} +function hideTip() { tipTarget = null; if (tipBubble) tipBubble.classList.remove('show'); } +document.addEventListener('mouseover', (event) => { + const el = event.target && event.target.closest ? event.target.closest('[data-tip]') : null; + if (el === tipTarget) return; + if (el) showTip(el); else hideTip(); +}); +document.addEventListener('mousedown', hideTip); +window.addEventListener('scroll', hideTip, true); + /* ---------- 分页 ---------- */ function pageSlice(items, key) { const totalPages = Math.max(1, Math.ceil(items.length / PAGE_SIZE)); @@ -401,7 +432,7 @@ function serverDetailRow(server) { const tls = server.tlsDomain ? `${esc(server.tlsDomain)} · ${server.engine && (server.engine.certificates || []).includes(server.tlsDomain) ? '已确认' : '待确认'}` : '未设置'; const engine = server.role === 'exit' ? '—' : (server.engine ? (server.engine.singBoxInstalled ? '已安装' : '按需安装(ng-agent engine install)') : '未上报'); const usage = server.usage ? `${fmtDate(server.usage.lastReportAt)} · ${server.usage.sampleCount} 项${server.usage.error ? ` · 异常:${esc(server.usage.error)}` : ''}` : '尚无上报'; - return `
+ return `
资源余量${resources}
TLS 证书(${esc(server.tlsDomain || '—')})${tls}
引擎状态${engine}
@@ -424,7 +455,6 @@ function renderServers() {
设备总数
${state.servers.length}
在线
${onlineCount}
离线
${state.servers.length - onlineCount}
-
平均延迟
—
`; const filtered = state.servers.filter((item) => [item.name, item.region, item.publicAddress, item.publicAddressV6, ...(item.labels || [])].join(' ').toLowerCase().includes(q)); const { pageItems } = pageSlice(filtered, 'servers'); @@ -438,7 +468,7 @@ function renderServers() { const row = ` -
+
${esc(server.name)} ${addr} @@ -449,14 +479,13 @@ function renderServers() { — ${system.uptimeSeconds ? fmtUptime(system.uptimeSeconds) : '—'} ${ICONS.users}${clients.total}${clients.online} - — ${server.lastSeenAt ? relTime(server.lastSeenAt) : '等待注册'}`; return expanded ? row + serverDetailRow(server) : row; }).join(''); return `${stats}
-
- ${rows || emptyRow(15, '没有匹配设备')}
操作已启用名称地址状态CPU内存Agent 版本核心版本运行时长客户端延迟上次心跳
${pagerHTML('servers', filtered.length)}
`; +
+ ${rows || emptyRow(14, '没有匹配设备')}
操作已启用名称地址状态CPU内存Agent 版本核心版本运行时长客户端上次心跳
${pagerHTML('servers', filtered.length)}
`; } /* ---------- 客户与订阅 ---------- */ @@ -513,7 +542,7 @@ function renderCustomers() { const speedTotal = (item.speedUpBps || 0) + (item.speedDownBps || 0); const manualOff = item.status === 'suspended' && item.suspendReason === 'manual'; return ` -
+
${item.online ? '在线' : '离线'}${item.status === 'suspended' ? `已暂停 · ${esc(suspendReasonText[item.suspendReason] || '待恢复')}` : ''} ${esc(item.name)}${item.notes ? `${esc(item.notes)}` : ''} @@ -526,7 +555,7 @@ function renderCustomers() { return `
- ${filterNode ? `节点:${esc(filterNode.name)}` : ''} + ${filterNode ? `节点:${esc(filterNode.name)}` : ''} ${filtered.length} 位客户
${rows || emptyRow(9, '还没有客户')}
操作已启用在线客户端关联入站流量⇅速度剩余⇅时长⇅
${pagerHTML('customers', filtered.length)}
`; @@ -589,7 +618,7 @@ function refreshPicker() { function credRowHTML(label, value) { return `
${esc(label)}${esc(value || '—')} - ${value ? `` : ''}
`; + ${value ? `` : ''}
`; } function renderCredentialsPane(customer) { const pane = $('#tab-credentials'); @@ -599,7 +628,7 @@ function renderCredentialsPane(customer) { if (!data) { pane.innerHTML = '
加载中…
'; return; } const groups = data.items.map((entry) => `
${esc(entry.nodeName)}${esc(statusText[entry.status] || entry.status)}${esc(entry.protocol)} -
+
${credRowHTML('UUID', entry.clientId)} ${credRowHTML('密码', entry.password)} ${entry.shortId ? credRowHTML('Reality Short ID', entry.shortId) : ''} @@ -607,8 +636,8 @@ function renderCredentialsPane(customer) {
`).join(''); pane.innerHTML = `
订阅
订阅令牌${esc(data.subscriptionToken || '—')} - -
+ + ${groups || '
尚未关联节点
'}`; } @@ -628,12 +657,12 @@ function renderLinksPane(customer) {
-
`).join(''); + `).join(''); const sources = customerFormState.externalSources.map((source, index) => `
${linkStatusLine(customer, source)} ${source.id ? `` : ''}
`).join(''); @@ -837,10 +866,10 @@ function renderNodes() { const enabled = !['draft', 'removing'].includes(node.status); const busy = ['deploying', 'removing'].includes(node.status); return ` - ${ICONS.grip} + ${ICONS.grip} ${(node.sortIndex != null ? node.sortIndex : index) + 1} - + ${esc(node.name)}${node.error ? `
${esc(String(node.error).slice(0, 40))}
` : ''} ${esc(server ? server.name : '已删除')} diff --git a/public/index.html b/public/index.html index cc82f63..9797592 100644 --- a/public/index.html +++ b/public/index.html @@ -34,7 +34,7 @@ 安装与命令 设置与运维 -
+
@@ -44,7 +44,6 @@ - 管
@@ -52,7 +51,7 @@ diff --git a/public/styles.css b/public/styles.css index dc8e3fd..d928602 100644 --- a/public/styles.css +++ b/public/styles.css @@ -68,8 +68,7 @@ input,select,textarea{font-family:var(--font);color:var(--text)} .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)} -.avatar{width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#1668dc,#13c2c2);display:inline-flex;align-items:center;justify-content:center;font-size:12px;color:#fff;font-weight:600;flex:none} -.content{padding:16px 20px 28px;max-width:1460px;width:100%;margin:0 auto} +.content{padding:24px;width:100%} .panel{background:var(--card);border:1px solid var(--border-soft);border-radius:var(--radius);margin-bottom:16px} .panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border-soft)} .panel-head h2{font-size:14.5px;font-weight:600} @@ -181,6 +180,10 @@ tbody tr.drag-over td{border-top:2px solid var(--primary)} .icon-btn:hover{color:var(--primary-text);background:var(--hover-bg)} .icon-btn.del{color:var(--red-text)} .icon-btn.del:hover{background:var(--red-soft);color:var(--red-text)} +/* 图标悬停提示气泡(3x-ui 式:黑底白字圆角 + 小尖角,挂 body 避免被表格滚动容器裁剪) */ +.tip-bubble{position:fixed;z-index:200;pointer-events:none;background:#141414;color:#fff;font-size:12px;line-height:1.25;padding:6px 9px;border-radius:6px;white-space:nowrap;opacity:0;transition:opacity .12s;box-shadow:0 4px 14px rgba(0,0,0,.25)} +.tip-bubble.show{opacity:1} +.tip-bubble::after{content:"";position:absolute;top:100%;left:var(--tip-arrow-left,50%);transform:translateX(-50%);border:5px solid transparent;border-top-color:#141414;border-bottom:0} .tgl{position:relative;display:inline-block;width:38px;height:21px;vertical-align:middle;flex:none} .tgl input{opacity:0;width:0;height:0;position:absolute} .tgl .tr{position:absolute;inset:0;background:var(--tgl-off);border-radius:99px;transition:background .2s;cursor:pointer} @@ -206,7 +209,7 @@ tbody tr.drag-over td{border-top:2px solid var(--primary)} .empty b{display:block;color:var(--text-2);font-size:14px;margin-bottom:2px} /* ---------- 服务器页 ---------- */ -.stats{display:grid;grid-template-columns:repeat(4,1fr);background:var(--card);border:1px solid var(--border-soft);border-radius:var(--radius);margin-bottom:14px} +.stats{display:grid;grid-template-columns:repeat(3,1fr);background:var(--card);border:1px solid var(--border-soft);border-radius:var(--radius);margin-bottom:14px} .stats .stat{padding:14px 22px;border:none;background:transparent} .stats .stat .lab{color:var(--text-2);font-size:12.5px;margin-bottom:3px} .stats .stat .val{display:flex;align-items:center;gap:8px;font-size:20px;font-weight:600;font-variant-numeric:tabular-nums} @@ -399,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)}} -@media (max-width:760px){.sider{display:none}.form-grid{grid-template-columns:1fr}.top-search{display:none}} +@media (max-width:760px){.sider{display:none}.form-grid{grid-template-columns:1fr}.top-search{display:none}.content{padding:8px}}