🦄 Sync 2026-10-02 14:52:05

This commit is contained in:
github-actions[bot] committed 2026-10-02 14:52:05 +08:00
1 parent 4c85793342
commit 0bfe84f1e8
133 files changed
+7983 -3225

No files matched your search

@@ -2497,36 +2497,53 @@ select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select) option:
background: var(--glass-bg-hover);
}
/* 用量表格(内存/存储等):表格转 block、行用 flex 强制同行
(与移动端同一方案;标题列 20ch 允许内部换行、值列占剩余宽度。
16ch 会把最长标签"CPU 使用率(%)"挤成两行(实测 16ch 折行、
18ch 起单行),20ch 留出中文字体度量的跨平台余量) */
/* 标签/值两列表格(.table 内含 td[width="33%"]:概览"系统"表、存储/内存
用量表等多个页面共用同一标记 → 统一处理、不按页面特化;参考 Argon 让
列宽随内容自适应,再叠一层"放不下就换行"的弹性):
① 宽度足够:标签列 20ch 对齐、值同行(16ch 会把最长标签"CPU 使用率(%)"
挤成两行——实测 16ch 折行、18ch 起单行,20ch 留中文字体度量余量);
② 宽度不足:值整块换到下一行、占满行宽;
③ 极窄:标签列可压缩,标题与值的文字都换行(不再横向滚动/被裁)。
(原方案:标签锁死 20ch + 值 nowrap,只能靠横向滚动,窄屏很挤) */
.table:has(td[width="33%"]) {
display: block;
overflow-x: auto;
overflow-x: auto; /* 仅兜底:单个不可断的超长串仍可横滚 */
}
.table:has(td[width="33%"]) tr {
display: flex;
/* 放不下的列换到下一行,而不是挤压/横向滚动 */
flex-wrap: wrap;
align-items: center;
white-space: nowrap;
/* 标题与值都允许换行 */
white-space: normal;
}
.table:has(td[width="33%"]) tr > td {
flex: 0 1 auto;
min-width: 0;
}
.table:has(td[width="33%"]) tr > td[width="33%"] {
flex: 0 0 20ch;
max-width: 20ch;
white-space: normal;
/* 首选 20ch 保持各行列对齐。
!important 必须:单行规则(r13)给所有格设了 flex/width !important,
而 important 无视特异性 → 不加会把桌面固定列压成"文字宽"、
值列逐行错位(实测踩过) */
flex: 0 0 20ch !important;
width: 20ch !important;
max-width: 20ch !important;
white-space: normal !important;
overflow-wrap: break-word;
word-break: break-word;
}
.table:has(td[width="33%"]) tr > td:last-child {
flex: 1 1 100%;
white-space: nowrap;
/* 以内容宽为基准:同行放不下 → 整块换行并撑满该行 */
flex: 1 1 auto;
min-width: 0;
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
}
/* ==========================================================================
@@ -4420,7 +4437,10 @@ pre {
}
#maincontent {
padding: 0 16px 64px;
/* 外层卡片到屏幕边缘的留白:16px → 4px(用户反馈移动端概览页
padding 偏大,大幅压缩)。内层卡片自身仍有 2~6px padding,
文字距边缘合计约 14px,输入框聚焦 ring(3px) 也不至于贴边 */
padding: 0 4px 64px;
margin-left: 0;
/* r19 起移动端页面纵向滚动由文档承担(见下方「文档级滚动」块,
为了让安卓浏览器地址栏随滚动收缩)。此处 flex-shrink:0 保证
@@ -4872,3 +4892,359 @@ table.liquid-actions-overflow th:not(.cbi-section-actions) {
align-items: center;
}
}
/* ==========================================================================
多列宽表响应式:一行三列 + 单元格自带表头(.liquid-grid,由 main.js 打标)
-------------------------------------------------------------------------
适用:DHCP 租约 / DDNS / WiFi / UPnP / 进程 等多列表(GridSection 与概览
include 表)。断点 1100px(实测:1150px 时"状态"列尚有 6.5 字/行,
1100px 掉到 4.3;移动端则整表按内容宽撑开只能横滚)。
断点判定放在 main.js 的 matchMedia(单一来源),这里只按 .liquid-grid 类
生效;关键声明用 !important —— ≤854px 的既有规则给 tr/td 设过
display:flex(同特异性会靠"后写"抢胜负,实测会输),必须显式压过。
排除两类已有专属布局的表:
- 接口页表(td[data-name="_ifacebox"]):图标行有独立两行布局;
- 标签/值两列表(td[width="33%"]):已有"20ch 标签列 + 值换行"方案。
单元格自带表头用 td::before 显示 data-title(Argon 同款机制,文案由
main.js 从同列 th 回填);原表头行隐藏,带筛选输入的 thead 保留。 */
.cbi-section-table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])),
.table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])) {
display: block;
width: 100% !important;
min-width: 100% !important;
overflow-x: auto;
}
.cbi-section-table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])) tr:not(.table-titles):not(.cbi-section-table-titles),
.table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])) tr:not(.table-titles):not(.cbi-section-table-titles) {
display: flex !important;
/* 放不下的列连同表头换到下一行,而不是挤压成一字一行/横向滚动 */
flex-wrap: wrap !important;
align-items: stretch;
white-space: normal !important;
/* 行与行之间的呼吸感:操作列换行到下一行时不至于贴着上一行 */
row-gap: 10px;
}
.cbi-section-table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])) tr > td:not(.cbi-section-actions),
.table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])) tr > td:not(.cbi-section-actions) {
flex: 1 1 33.33% !important; /* 一行三列,列边界落在 0/33.3/66.6 */
display: block !important;
min-width: 0 !important;
white-space: normal !important;
overflow-wrap: break-word;
word-break: normal; /* 按词换行,不再一字一断 */
border-bottom: 0 !important;
padding: 6px 10px;
}
/* 操作列前的内容格:跨满前两列(66.6%),使按钮正好从第三列起点开始
—— 与上一行第三列"同列对齐"(用户要求);右 padding 24px 拉开间距。
:has(td.cbi-section-actions) 限定"有操作列的表",避免误伤两列内容表。 */
/* 选择器必须带与通用规则相同的 :not(:has(...)) 链:通用规则元素更多,
同特异性会输(实测 66.6% 被 33.33% 盖掉),补齐链路 + .td + :nth-last-child 才赢 */
.cbi-section-table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])):has(td.cbi-section-actions) tr > td.td:nth-last-child(2),
.table.liquid-grid:not(:has(td[data-name="_ifacebox"])):not(:has(td[width="33%"])):has(td.cbi-section-actions) tr > td.td:nth-last-child(2) {
flex: 2 1 66.6% !important;
padding-right: 24px;
}
/* 操作列占第三列:按钮起点 = 上一行第三列内容起点(严格同列对齐);
顶对齐(align-self/items: flex-start);放不下时 flex-wrap 自动换到下一行 */
.cbi-section-table.liquid-grid tr > td.cbi-section-actions,
.table.liquid-grid tr > td.cbi-section-actions {
flex: 1 1 33.4% !important;
min-width: 0 !important;
align-self: flex-start;
align-items: flex-start;
margin-left: 0;
padding: 6px 10px;
white-space: normal !important;
}
/* 单元格自带表头(Argon 同款;文案由 main.js 从同列 th 回填 data-title) */
.cbi-section-table.liquid-grid td[data-title]::before,
.table.liquid-grid td[data-title]::before {
content: attr(data-title);
display: block;
font-size: 11px;
font-weight: 600;
line-height: 1.45;
letter-spacing: .2px;
color: var(--text-color-medium);
opacity: .9;
margin-bottom: 1px;
}
/* 原表头行隐藏(每格已自带表头);带筛选输入的 thead 保留。
!important 必须:≤854px 的既有规则给 tr 设了 display:flex !important */
.cbi-section-table.liquid-grid tr.table-titles,
.cbi-section-table.liquid-grid tr.cbi-section-table-titles,
.table.liquid-grid tr.table-titles,
.cbi-section-table.liquid-grid thead:not(:has(input)),
.table.liquid-grid thead:not(:has(input)) {
display: none !important;
}
/* 三列网格下操作列仅占 1/3 宽:按钮相邻间距 8→4px,尽量保持一行
(1000px 实测:4 按钮约 290px,格内可用 284px,8px 间距会挤成两行) */
.cbi-section-table.liquid-grid td.cbi-section-actions > div > * + *,
.table.liquid-grid td.cbi-section-actions > div > * + * {
margin-left: 4px;
}
/* 折叠态(按钮优先折叠)在此模式下的适配:操作列已占整行 → 按钮横排折行;
文字格的"一字一断"兜底改回按词换行 */
/* 仅 grid 态(视口≤1100、操作列已独占整行)才把折叠按钮改横排;
桌面宽屏折叠态维持 r17 的"每个按钮一行"纵排 */
table.liquid-actions-overflow.liquid-grid td.cbi-section-actions > div {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
}
table.liquid-actions-overflow.liquid-grid td:not(.cbi-section-actions),
table.liquid-actions-overflow.liquid-grid th:not(.cbi-section-actions) {
word-break: normal;
overflow-wrap: break-word;
}
/* ===== 移动端(≤854px):标签/值两列表的标签列改为按自身文字宽度收缩 =====
原来固定 20ch≈145px(为了让最长标签"CPU 使用率(%)"单行),但短标签
(主机名/型号/温度…只需 40~60px)右侧空出 ~100px,把值列挤到换行——
实测移动端值列因此少 ~90px。改为 flex:0 1 auto + max-width:20ch:
改为固定 5 个字的内容宽(5em):既让值列拿回空间(比 20ch≈145px 窄),
又保证全表标签列等宽、值列起点对齐(用户反馈:按文字收缩会让值列
逐行错位)。超过 5 字的标签(如 CPU 使用率(%))在本格内折行。
桌面(>854px)保持 20ch 固定列。 */
@media screen and (max-width: 854px) {
.table:has(td[width="33%"]) tr > td[width="33%"],
.cbi-section-table tr > td[width="33%"],
.table tr > td[width="33%"] {
flex: 0 0 5em !important;
box-sizing: content-box !important; /* 5em=5 个汉字的内容宽,外加自身 padding */
width: 5em !important;
max-width: none !important;
min-width: 0 !important;
/* 固定 5 字宽后,超过 5 字的标签(CPU 使用率(%))在本格内折行,
值列起点全表对齐(用户反馈:按文字收缩会导致值列不齐) */
white-space: normal !important;
overflow-wrap: break-word;
word-break: break-word;
}
}
/* ===== 启动项页(admin/system/startup):操作列 5 个按钮排 3+2 =====
该页移动端按钮容器为整行宽,5 个按钮自然折行会排成 4+1(最后孤零零
一个,不美观)。改为 3 列网格:第一行 3 个、第二行 2 个。
仅按该页 data-page 生效(用户要求"单独优化这里"),其它表格不受影响。 */
@media screen and (max-width: 854px) {
/* data-page 两种写法都匹配(连字符式 admin-system-startup /
路径式 admin/system/startup,主题与 openclash 用前者) */
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div {
display: grid;
grid-template-columns: repeat(3, max-content);
justify-content: start;
align-items: start;
gap: 6px 8px;
}
/* 网格自带 gap,清掉相邻按钮的 margin-left(否则间距叠加不均) */
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > * + * {
margin-left: 0;
}
}
/* ===== 有标题的操作格:标题在上、按钮在下、两者左对齐 =====
ui.Table(cbi_update_table)会把表头文案复制成单元格 data-title、并把
表头的 cbi-section-actions 类复制过来——如概览 DHCP 租约表的最后一列
(表头"静态租约" / 内容"设为静态"按钮)。该格在 ≤854px 被通用规则
变成 flex 行 → 标题与按钮排成左右两列,看起来像"两列东西"。
实际语义:标题=这列的表头,按钮=这列的内容 → 上下堆叠;
对齐方式经用户确认为左对齐(先居中后改回左对齐更合适)。 */
.cbi-section-table.liquid-grid td.cbi-section-actions[data-title],
.table.liquid-grid td.cbi-section-actions[data-title] {
display: block !important; /* ::before 标题与按钮上下排 */
text-align: left; /* 标题与按钮左对齐 */
}
/* 若格内是按钮容器(折叠/按钮组),同样左对齐 */
.cbi-section-table.liquid-grid td.cbi-section-actions[data-title] > div,
.table.liquid-grid td.cbi-section-actions[data-title] > div {
text-align: left;
justify-content: flex-start;
}
/* ===== 概览"系统"卡片(标签/值两列表 td[width="33%"]):每行强制单行 =====
用户要求:每行只占一行不换行;固件版本这类超长值不折行,改为整卡
左右滑动查看右侧内容(.table 本就是 overflow-x:auto 的滚动容器)。
注意这是对 r4(放不下就换行)与 r10(标签按文字收缩)的本卡覆盖:
- 行 flex-wrap: nowrap + 单元格 white-space: nowrap → 行=内容宽;
- 单元格 flex:0 0 auto + width:max-content → 行宽=标签+值的自然宽;
- 表宽超出容器时由 .table:has(td[width="33%"]){overflow-x:auto} 横滚。
!important 必须:要压过 r10 移动端的 width:max-content/20ch 封顶以及
桌面 20ch 固定列规则(同特异性会靠顺序,这里统一显式压过)。 */
.table:has(td[width="33%"]) tr {
flex-wrap: nowrap !important;
white-space: nowrap !important;
}
/* 值列(最后一格)必须占满剩余宽度:内存/存储的进度条要拉满到右侧。
r13 把所有格子改成 flex:0 0 auto + max-content 后,值列缩成内容宽 →
进度条看起来"等于文字长度"(用户反馈)。important 对 important 由
特异性决定(本规则带 :last-child 更高),故这里同样加 important。 */
.table:has(td[width="33%"]) tr > td:last-child {
flex: 1 1 auto !important;
width: auto !important;
}
.table:has(td[width="33%"]) tr > td {
flex: 0 0 auto !important;
width: max-content !important;
max-width: none !important;
white-space: nowrap !important;
overflow-wrap: normal !important;
word-break: normal !important;
}
/* ===== 启动项页(admin/system/startup):操作按钮按语义上色 =====
该页操作格内 5 个子元素顺序固定(startup.js rows.push):
① 启用开关(cbi-button-positive=已启用 / negative=已禁用)
② 启动 Start ③ 重启 Restart ④ 重新加载 Reload ⑤ 停止 Stop
需求:启用/启动=绿、重启/重新加载=橙、停止=红;仅本页生效。
主题现有 .cbi-button-positive 等走 accent 蓝渐变,这里按语义覆盖,
含 :hover 态(否则 hover 会被主题的 accent-grad 规则抢回蓝色)。 */
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(2),
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > .cbi-button-positive {
/* 启动 + 启用(已启用态) → 绿 */
color: var(--on-success-color);
background: linear-gradient(135deg, #34d399, var(--success-color-medium));
box-shadow: 0 6px 16px rgba(var(--success-color-high-rgb), 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(2):hover,
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > .cbi-button-positive:hover {
filter: brightness(1.08);
}
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(3),
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(4) {
/* 重启 + 重新加载 → 橙(复用主题 amber 强调色渐变) */
color: #ffffff;
background: linear-gradient(135deg, #ffc04a 0%, #e8940f 48%, #c47100 100%);
box-shadow: 0 6px 16px rgba(232, 148, 15, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(3):hover,
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(4):hover {
filter: brightness(1.08);
}
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(5) {
/* 停止 → 红(与主题 negative 同配方,保证禁用态/其它页一致观感) */
color: var(--on-error-color);
background: linear-gradient(135deg, #f06767, var(--error-color-high));
box-shadow: 0 6px 16px rgba(var(--error-color-high-rgb), 0.35);
}
body:is([data-page="admin-system-startup"], [data-page="admin/system/startup"])
td.cbi-section-actions > div > :nth-child(5):hover {
filter: brightness(1.08);
}
/* ===== 小体积弹窗:含文件选择控件的对话框按内容收窄 =====
(用户反馈:"选择要上传的文件…取消/上传"这类弹窗异常宽,右侧大片空白)
根因:#modal_overlay > .modal 固定 width:80%(max 80vw)——1920 屏上
一个只有标题+文件框+两个按钮的对话框也占 1536px;再叠加 .button-row
的 flex-end + 首按钮 margin-right:auto(取消在最左、上传在最右),
空白被进一步拉开。
方案:只对含 input[type=file] 的弹窗收窄到 520px(按内容包裹、文字
正常换行),按钮行随之收拢;其它表单弹窗保持 80% 不变。 */
#modal_overlay > .modal:has(input[type="file"]) {
/* 按内容自适应(fit-content):内容已全部左对齐,固定 520px 会在右侧
留出大片空白(实测344px)。弹窗宽=最宽一行内容+内边距;超长说明文字
仍会被 max-width 限制并正常换行 */
/* 宽度策略(用户定量要求:约当前的 180% ≈545px,且按钮/内容不许换行):
- fit-content:内容长时随内容展开(上限 80vw),绝不逼元素换行;
- min-width 545:短内容也保底 545px —— 之前纯 fit-content 只有303px,
弹窗过小、按钮行(.button-row flex-wrap:wrap)被挤到换行。 */
width: fit-content;
min-width: min(545px, 92vw);
max-width: 80vw;
}
/* 文件框随容器宽度走,别用浏览器默认 intrinsic 宽撑破 */
#modal_overlay > .modal:has(input[type="file"]) input[type="file"] {
max-width: 100%;
}
/* ui.js 的上传弹窗真实结构(hideModal 里的 uploadFile):
.modal > p(说明) + div[display:flex] > (.left[flex:1] = 选择文件按钮
.right[flex:1] = 取消 + 上传)
默认左右各占一半、右侧组贴中线,两组之间只有 129px 且上传按钮离右缘 56px。
用户要求拉开"选择文件"与"取消/上传"的间距 → 右侧整组靠右并加左间距。 */
/* 按钮行左组(选择文件)必须贴左:标题/说明的 text-align:center(方案2)
会被 .left 继承 → 按钮被推到 .left 中央,左侧出现空白间隔(用户反馈)。 */
#modal_overlay > .modal:has(input[type="file"]) > div[style*="display:flex"] .left {
text-align: left;
}
#modal_overlay > .modal:has(input[type="file"]) > div[style*="display:flex"] .right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
margin-left: 32px;
}
/* 按钮收拢到右侧一组:.button-row 首按钮的 margin-right:auto 会把
"取消"甩到最左、"上传"顶到最右(间距 300+px 的空白带)。小弹窗里取消。 */
#modal_overlay > .modal:has(input[type="file"]) .button-row > button:first-of-type {
margin-right: 0;
}
/* 方案2(用户选定):标题与说明文字**居中**——左对齐时短文本行右侧会留
128~195px 空白,居中后空白分到两侧更平衡;文件框仍左对齐、按钮仍右对齐 */
#modal_overlay > .modal:has(input[type="file"]) > :not(input):not(.button-row) {
text-align: center;
}
/* 对齐方式(用户最终指定):标题、说明文字、选择文件按钮一律**左对齐**,
只有"取消/上传"两个按钮右对齐(.button-row 本就是 flex-end)。
(曾试过内容整体右对齐,用户反馈说明文字也要左 → 回到默认左对齐,
仅保留下方文件框的显式左推,防止被带 text-align 的包装层甩到右边。) */
#modal_overlay > .modal:has(input[type="file"]) input[type="file"] {
display: block;
/* 选择文件按钮保持左对齐(用户指定):块级 + margin-right:auto
推到左侧——只靠 text-align:right 会把它甩到右边 */
margin-right: auto;
margin-left: 0;
}
/* ===== 会话过期/重新登录弹窗(luci.js notifySessionExpiry)收窄 =====
结构(源码确认):.modal > h4 + div.alert-message.warning + div.right(登录按钮)。
它走通用规则 width:80% → 1920 屏上占 1536px,只有一句话却拉满整屏(用户反馈)。
按内容收窄:fit-content + 保底 420px(窄屏 92vw),上限 80vw;只命中"同时带
直接子级 .alert-message 与 .right"的弹窗,与上传弹窗(.right 在 flex 行内)不冲突。 */
#modal_overlay > .modal:has(> .alert-message):has(> .right) {
width: fit-content;
/* 窄屏上原来 min(420px,92vw) 等于 92% 屏宽 → 看着像贴边大卡片;
按用户要求改为"屏幕宽度的 75%":窄屏下 min 与 max 都落到 75vw,
长文本也被限在 75% 内换行;宽屏仍保底 420px 不会缩水 */
min-width: min(420px, 75vw);
max-width: 75vw;
}
@@ -2232,6 +2232,7 @@
维持原有排版与横向滚动。 */
var TBL_CLS = 'liquid-actions-overflow';
var tblTimer = null;
var tblRaf = 0;
/* 元素内容盒宽度:clientWidth 减掉左右 padding(表格比的是父级内容区) */
function contentBoxWidth(el) {
@@ -2256,50 +2257,156 @@
return w;
}
function tableOverflows(t) {
var doc = document.documentElement;
var parent = t.parentElement;
/* 1) 表格内容已超出表格盒(自身裁剪 / 内部横滚) */
if (t.scrollWidth > t.clientWidth + 1)
return true;
/* 2) 核心:需求宽(按钮横排放得下)> 可用宽 → 从按钮开始折 */
var need = needWidth(t);
var avail = parent ? contentBoxWidth(parent) : Infinity;
if (need > avail + 1 || need > doc.clientWidth + 1)
return true;
/* 3) 表格盒右缘已越出屏幕、且文档无法横向滚过去(被祖先裁切) */
var rect = t.getBoundingClientRect();
return rect.right > doc.clientWidth + 1 && doc.scrollWidth <= doc.clientWidth + 1;
/* 在“自然状态”(未折叠 + 操作列内联列宽为空)下执行 fn,随后原样还原。
所有样式改动与读数都发生在同一任务内,浏览器不会在中途绘制 ——
所以这套测量不会像“120ms 后再改样式”那样被肉眼看到(闪一下)。
内联列宽只在测量期间临时清空:量的才是“按钮横排放得下”的真实
需求宽(form.js stabilizeActionColumnWidth 写的值在折叠态下是
纵排后的 ~63px,会把需求宽算小)。 */
function inNaturalState(t, fn) {
var had = t.classList.contains(TBL_CLS);
if (had) t.classList.remove(TBL_CLS);
var acts = t.querySelectorAll('th.cbi-section-actions, td.cbi-section-actions');
var saved = [];
for (var i = 0; i < acts.length; i++) {
saved.push([acts[i], acts[i].style.width, acts[i].style.minWidth]);
acts[i].style.width = '';
acts[i].style.minWidth = '';
}
var ret = fn();
for (var k = 0; k < saved.length; k++) {
saved[k][0].style.width = saved[k][1];
saved[k][0].style.minWidth = saved[k][2];
}
if (had) t.classList.add(TBL_CLS);
return ret;
}
function updateOverflowTables() {
function tableOverflows(t) {
/* 全程在“自然状态”(未折叠 + 操作列内联宽为空)下判定:
类的摘挂、内联宽的清还都在同一任务内完成,浏览器中途不绘制
→ 测量本身不产生可见闪动 */
return inNaturalState(t, function () {
var doc = document.documentElement;
var parent = t.parentElement;
/* 1) 表格内容已超出表格盒(自身裁剪 / 内部横滚) */
if (t.scrollWidth > t.clientWidth + 1)
return true;
/* 2) 核心:需求宽(按钮横排放得下)> 可用宽 → 从按钮开始折 */
var need = needWidth(t);
var avail = parent ? contentBoxWidth(parent) : Infinity;
if (need > avail + 1 || need > doc.clientWidth + 1)
return true;
/* 3) 表格盒右缘已越出屏幕、且文档无法横向滚过去(被祖先裁切) */
var rect = t.getBoundingClientRect();
return rect.right > doc.clientWidth + 1 && doc.scrollWidth <= doc.clientWidth + 1;
});
}
/* form.js stabilizeActionColumnWidth 把操作列锁成内联 width/min-width;
折叠态下它量到的是“按钮纵排”宽(~63px),解除折叠后这个残留值会
卡住按钮 —— 只在“取消折叠”这一刻清掉,平时不动它(否则每次判定
都会改动列宽 → 页面闪一下) */
function clearActionInlineWidths(t) {
var acts = t.querySelectorAll('th.cbi-section-actions, td.cbi-section-actions');
for (var i = 0; i < acts.length; i++) {
if (acts[i].style.width || acts[i].style.minWidth) {
acts[i].style.width = '';
acts[i].style.minWidth = '';
}
}
}
/* 给多列表的单元格回填 data-title(文案取自同列表头 th)——窄屏"单元格
自带表头"(td::before { content: attr(data-title) },Argon 同款)依赖它。
概览 include 表(DHCP 租约 / DDNS / WiFi / UPnP …)与 DDNS 服务页的
自绘单元格都没有 data-title,这里统一补齐:只补缺失、不覆盖已有、
跳过操作列;轮询重绘出的新行下次判定时会补上。attribute 写入不触发
我们只监听 childList 的观察者 → 不会造成回调循环。 */
function backfillCellTitles() {
var tables = document.querySelectorAll('table.cbi-section-table, table.table');
for (var i = 0; i < tables.length; i++) {
var t = tables[i];
var head = t.querySelector(':scope > thead > tr, :scope > tbody > tr.table-titles, :scope > tbody > tr.cbi-section-table-titles, :scope > tr.table-titles');
if (!head) continue;
var ths = head.children;
var rows = t.querySelectorAll(':scope > tbody > tr');
for (var r = 0; r < rows.length; r++) {
var cells = rows[r].children;
for (var c = 0; c < ths.length && c < cells.length; c++) {
var cell = cells[c];
if (cell.tagName !== 'TD' || cell.hasAttribute('data-title') ||
cell.classList.contains('cbi-section-actions'))
continue;
var txt = (ths[c].innerText || ths[c].textContent || '')
.replace(/\s+/g, ' ').trim();
if (txt) cell.setAttribute('data-title', txt);
}
}
}
}
/* 多列表响应式打标:视口 ≤1100px 时给表格加 .liquid-grid(CSS 端据此
切到"一行三列 + 单元格自带表头")。断点放在这里(matchMedia 单一来源),
CSS 只认类、用 !important 压过 ≤854px 既有的 display:flex 规则。
1100px 是实测值:1150px 时中间列尚有 6.5 字/行,1100px 掉到 4.3;
移动端则是整表按内容宽撑开、只能横向滚动。 */
function syncGridView() {
var narrow = window.matchMedia
? window.matchMedia('(max-width: 1100px)').matches
: window.innerWidth <= 1100;
var list = document.querySelectorAll('table.cbi-section-table, table.table');
for (var i = 0; i < list.length; i++) {
var t = list[i];
t.classList.remove(TBL_CLS);
/* 清掉 form.js stabilizeActionColumnWidth 写死的内联列宽:
类还在时它量到的是“按钮纵排”的宽度(最宽按钮 ~63px),
窗口变宽、类移除后这个残留值会让 needWidth 少算一截、
判定漏折;清掉后列宽回归 auto 布局(= 按钮横排的
max-content,与 stabilize 结果一致),量到的才是
“按钮横排放得下”所需的真实宽度 */
var acts = t.querySelectorAll('th.cbi-section-actions, td.cbi-section-actions');
for (var j = 0; j < acts.length; j++) {
if (acts[j].style.width || acts[j].style.minWidth) {
acts[j].style.width = '';
acts[j].style.minWidth = '';
}
/* 两类有专属布局的表不打标(CSS 端同样排除,这里保持标记语义一致):
接口页表(td[data-name="_ifacebox"])、标签/值两列表(td[width="33%"]) */
var skip = t.querySelector('td[data-name="_ifacebox"], td[width="33%"]');
if (narrow && !skip && t.querySelector('td'))
t.classList.add('liquid-grid');
else
t.classList.remove('liquid-grid');
}
}
function updateOverflowTables() {
syncGridView();
backfillCellTitles();
var list = document.querySelectorAll('table.cbi-section-table, table.table');
for (var i = 0; i < list.length; i++) {
var t = list[i];
/* 无操作列的表(概览"系统/内存/存储/网络"等标签-值两列表)与
折叠逻辑无关:不判定、不写任何样式 —— 与标签-值换行新规则
零交集(那套规则只认 td[width="33%"]),稳态下零 DOM 写 */
if (!t.querySelector('.cbi-section-actions')) {
if (t.classList.contains(TBL_CLS))
t.classList.remove(TBL_CLS);
continue;
}
if (t.querySelector('.cbi-section-actions') && tableOverflows(t))
var want = tableOverflows(t);
var has = t.classList.contains(TBL_CLS);
if (want && !has) {
t.classList.add(TBL_CLS);
} else if (!want && has) {
t.classList.remove(TBL_CLS);
clearActionInlineWidths(t);
}
/* 其余两种情况(要折且已折 / 不折且未折)净变化为零,不动 DOM */
}
}
function scheduleOverflowTables() {
/* rAF:在下一帧绘制前完成判定 → 首帧即最终状态,不再看到
“先按一行绘制、再瞬间堆叠”的折叠过程(用户反馈的闪一下) */
if (!tblRaf) {
tblRaf = window.requestAnimationFrame(function () {
tblRaf = 0;
updateOverflowTables();
});
}
/* 仍在 form.js 的 setTimeout(stabilize 列宽) 与其 resize 处理之后
补一次校正,拿到的才是内联列宽写完后的稳定状态 */
if (tblTimer)
clearTimeout(tblTimer);
/* 延后 120ms:排在 form.js 的 setTimeout(stabilize 列宽) 与其
resize 处理之后量,拿到的才是内联列宽写完后的稳定状态 */
tblTimer = setTimeout(function () {
tblTimer = null;
updateOverflowTables();