fix(web): keep Reality scanner results table inside its modal (item 16 follow-up)

真机暴露:扫描器浮层沿用 .modal.wide(820px) 且结果表借用
table-wrap visible(overflow:visible),8 列 nowrap 自然宽超弹窗,
「使用」按钮被挤到弹窗边框外;后端失败原因最长约 128 字在
nowrap 下更会单行撑爆。

- public/app.js:浮层 section 改用扫描器专属 .scan-modal;
  结果包装 table-wrap visible -> .scan-wrap;目标/证书单元格
  加 scan-target/scan-cert 类与 title 全文。
- public/styles.css:新增 .scan- 专属规则——.modal.scan-modal
  宽 1140px、.scan-wrap overflow-x:auto 兜底、目标/证书列
  max-width:230px 截断省略、原因列 white-space:normal +
  max-width:280px 换行。不动 .table-wrap/.modal 既有语义,
  节点编辑弹窗本体与其它弹窗布局不受影响。

验证:以真实 styles.css + 同构 DOM 灌真机候选池 14 行 + 长
原因(近 120 字上限)/超长证书名压力行,playwright-core 驱动
Chromium 在 1280/1600 两视口断言 table 与全部「使用」按钮
右缘均在弹窗右缘内、wrap scrollWidth==clientWidth(无需滚动);
旧类名对照页同法测得表宽 1958px 冲出弹窗(负对照成立)。
This commit is contained in:
aaaamoooo
2026-10-21 12:33:32 +00:00
parent 08bf2d08a2
commit 18074daf50
2 changed files with 12 additions and 5 deletions
+5 -5
View File
@@ -1060,7 +1060,7 @@ function openRealityScanner() {
overlay.id = 'scan-overlay'; overlay.id = 'scan-overlay';
overlay.className = 'modal-overlay'; overlay.className = 'modal-overlay';
overlay.style.zIndex = '95'; overlay.style.zIndex = '95';
overlay.innerHTML = `<section class="modal panel wide" role="dialog" aria-modal="true"> overlay.innerHTML = `<section class="modal panel scan-modal" role="dialog" aria-modal="true">
<header class="modal-head"><h2>查找 Reality 目标</h2><button type="button" class="icon-btn" data-scan-close aria-label="关闭"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M18 6L6 18M6 6l12 12"/></svg></button></header> <header class="modal-head"><h2>查找 Reality 目标</h2><button type="button" class="icon-btn" data-scan-close aria-label="关闭"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M18 6L6 18M6 6l12 12"/></svg></button></header>
<div class="modal-body"> <div class="modal-body">
<p class="links-note">从主控所在网络发起探测:可达性与延迟是主控视角,节点所在网络可能不同。</p> <p class="links-note">从主控所在网络发起探测:可达性与延迟是主控视角,节点所在网络可能不同。</p>
@@ -1092,13 +1092,13 @@ async function runRealityScan() {
const input = $('#scan-input') ? $('#scan-input').value.trim() : ''; const input = $('#scan-input') ? $('#scan-input').value.trim() : '';
const data = await api('/api/reality/scan', { method: 'POST', body: JSON.stringify({ input }) }); const data = await api('/api/reality/scan', { method: 'POST', body: JSON.stringify({ input }) });
const rows = (data.results || []).map((r) => `<tr> const rows = (data.results || []).map((r) => `<tr>
<td>${esc(r.target)}${Number(r.port) !== 443 ? `:${esc(r.port)}` : ''}</td> <td class="scan-target" title="${esc(r.target)}">${esc(r.target)}${Number(r.port) !== 443 ? `:${esc(r.port)}` : ''}</td>
<td>${r.ok ? '<span class="badge green">可用</span>' : '<span class="badge gray">不可用</span>'}</td> <td>${r.ok ? '<span class="badge green">可用</span>' : '<span class="badge gray">不可用</span>'}</td>
<td>${esc(r.tlsVersion || '—')}</td><td>${esc(r.alpn || '—')}</td> <td>${esc(r.tlsVersion || '—')}</td><td>${esc(r.alpn || '—')}</td>
<td>${esc(r.keyExchange || '—')}</td><td>${esc(r.certName || '—')}</td> <td>${esc(r.keyExchange || '—')}</td><td class="scan-cert" title="${esc(r.certName || '')}">${esc(r.certName || '—')}</td>
<td>${r.ok ? `${esc(r.latencyMs)} ms` : esc(r.reason || '')}</td> <td class="scan-reason">${r.ok ? `${esc(r.latencyMs)} ms` : esc(r.reason || '')}</td>
<td>${r.ok ? `<button type="button" class="btn small" data-scan-use data-host="${esc(r.host)}" data-port="${esc(r.port)}">使用</button>` : ''}</td></tr>`).join(''); <td>${r.ok ? `<button type="button" class="btn small" data-scan-use data-host="${esc(r.host)}" data-port="${esc(r.port)}">使用</button>` : ''}</td></tr>`).join('');
box.innerHTML = `<div class="table-wrap visible"><table class="scan-table"><thead><tr><th>目标</th><th>状态</th><th>TLS</th><th>ALPN</th><th>密钥交换</th><th>证书域名</th><th>延迟 / 原因</th><th></th></tr></thead><tbody>${rows || '<tr><td colspan="8">无结果</td></tr>'}</tbody></table></div>`; box.innerHTML = `<div class="scan-wrap"><table class="scan-table"><thead><tr><th>目标</th><th>状态</th><th>TLS</th><th>ALPN</th><th>密钥交换</th><th>证书域名</th><th>延迟 / 原因</th><th></th></tr></thead><tbody>${rows || '<tr><td colspan="8">无结果</td></tr>'}</tbody></table></div>`;
} catch (error) { box.innerHTML = `<div class="empty"><b>${esc(error.message)}</b></div>`; } } catch (error) { box.innerHTML = `<div class="empty"><b>${esc(error.message)}</b></div>`; }
} }
function nodeClientsModal(node) { function nodeClientsModal(node) {
+7
View File
@@ -408,3 +408,10 @@ label>input:focus,label>select:focus,label>textarea:focus{border-color:var(--pri
.scan-table{width:100%;border-collapse:collapse;font-size:13px} .scan-table{width:100%;border-collapse:collapse;font-size:13px}
.scan-table th,.scan-table td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border-soft);white-space:nowrap} .scan-table th,.scan-table td{padding:6px 8px;text-align:left;border-bottom:1px solid var(--border-soft);white-space:nowrap}
.scan-table th{color:var(--mut);font-weight:600} .scan-table th{color:var(--mut);font-weight:600}
/* 扫描器浮层专属收口(真机修复:结果表曾冲出弹窗边框):弹窗加宽、结果容器
横向滚动兜底、目标/证书列截断、失败原因列允许换行。全部 .scan- 专属,
不影响节点编辑弹窗本体与其它弹窗/主页面表格的既有布局。 */
.modal.scan-modal{width:1140px}
.scan-wrap{overflow-x:auto}
.scan-table td.scan-target,.scan-table td.scan-cert{max-width:230px;overflow:hidden;text-overflow:ellipsis}
.scan-table td.scan-reason{white-space:normal;max-width:280px;overflow-wrap:anywhere}