mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
fix(billing-ui): 规则表列宽锁定,消除表头/按钮重叠
全部靠截图 + 几何测量发现,DOM 断言当时全绿: - table-layout:fixed 把 954px 均分成 9×106px,写在 <td> 上的每列宽度全部失效 → 相邻表头叠在一起,源 URL 输入框被压成 "https:"。 - 把宽度改到 <th> 并去掉 fixed → 浏览器改按内容算,<th> 宽度又被忽略,币种列 仍塌到 48px(输入框只剩 24px,装不下 "USD")。 - 恢复 fixed + 宽度移到 <colgroup>:fixed 下只有首行宽度生效,而「按模型价格」 格宽度随哪条规则最宽而变(实测 352px vs 206px),整行布局取决于恰好哪条规则 最宽。colgroup 一次性锁死九列,所有行一致。 - 表头双语长文案(峰段星期 UTC 1=周一)超出列宽 → nowrap + ellipsis,完整文案 留在 title。 - 删除列 58px 装不下「删除」按钮(折成两行、按钮高过整行)→ 74px,从峰段倍数 列匀出。 - 价目行 80+96+96+38≈330px 挤在 262px 格子里,× 按钮越界 78px 压在隔壁「删除」 按钮上 → 输入框改 flex:1 1 …/min-width:0 可收缩,× 保持 flex:0 0 auto。 - 顺带修上轮引入的回归:给 modelBlock 的 flex 换行后模型名不再被截断。 判据 TestRuleTableLocksColumnWidthsInAColgroup + 3 个变异(去掉 min-width:0 / 改列宽数组 / 去掉 table-layout:fixed)全部被捕获。第三个变异第一版漏放——判据用 strings.Contains(js, "table-layout:fixed") 匹配到了上方三行的解释性注释,改成匹配 style='…' 字面量后才真正失败。
This commit is contained in:
@ -1039,15 +1039,21 @@ plugin.ui = {
|
||||
// ---- row rendering -------------------------------------------------------
|
||||
function modelBlock(models, T) {
|
||||
var keys = Object.keys(models || {});
|
||||
// Every child must be allowed to SHRINK: the price cell is 262px wide and
|
||||
// the four parts (name + prompt + completion + delete) used to be
|
||||
// 80+96+96+38 plus gaps = ~330px, so the × button overflowed the cell by
|
||||
// 78px and landed on top of the neighbouring column's Delete button.
|
||||
// flex:1 1 0 with min-width:0 lets the inputs share the cell instead of
|
||||
// pushing past it; the row wraps rather than overlapping.
|
||||
var rows = keys.map(function (m) {
|
||||
var t = models[m] || {};
|
||||
return "<div class='row' style='gap:6px;margin:3px 0'>" +
|
||||
"<input class='m-name' value='" + esc(m) + "' style='flex:1;min-width:80px'>" +
|
||||
return "<div class='row' style='gap:4px;margin:3px 0;flex-wrap:wrap'>" +
|
||||
"<input class='m-name' value='" + esc(m) + "' style='flex:1 1 90px;min-width:0'>" +
|
||||
"<input class='m-p' value='" + esc(t.prompt == null ? "" : t.prompt) +
|
||||
"' placeholder='" + esc(T.prompt) + "' style='width:96px'>" +
|
||||
"' placeholder='" + esc(T.prompt) + "' style='flex:0 1 62px;min-width:0'>" +
|
||||
"<input class='m-c' value='" + esc(t.completion == null ? "" : t.completion) +
|
||||
"' placeholder='" + esc(T.completion) + "' style='width:96px'>" +
|
||||
"<button class='ghost small m-del' title='" + esc(T.delRule) + "'>×</button></div>";
|
||||
"' placeholder='" + esc(T.completion) + "' style='flex:0 1 62px;min-width:0'>" +
|
||||
"<button class='ghost small m-del' title='" + esc(T.delRule) + "' style='flex:0 0 auto'>×</button></div>";
|
||||
}).join("");
|
||||
return "<div class='m-rows'>" + rows + "</div>" +
|
||||
"<button class='ghost small m-add'>" + esc(T.addModel) + "</button>";
|
||||
@ -1068,14 +1074,14 @@ plugin.ui = {
|
||||
"<td style='min-width:200px'><input class='r-url' value='" + esc(rule.url || "") + "' list='br-urls' style='width:100%;box-sizing:border-box'>" +
|
||||
(unknown ? "<div style='color:#e0a33e;font-size:11px'>⚠ " + esc(T.warnNoSource) + "</div>" : "") +
|
||||
"</td>" +
|
||||
"<td style='width:78px'><select class='r-mode' style='width:100%;box-sizing:border-box'>" + opts + "</select></td>" +
|
||||
"<td style='width:64px'><input class='r-cur' value='" + esc(rule.currency || "") + "' placeholder='USD' style='width:100%;box-sizing:border-box'></td>" +
|
||||
"<td style='width:76px'><input class='r-sub' type='number' step='0.01' value='" +
|
||||
(rule.subscription ? esc(rule.subscription) : "") + "' placeholder='" + esc(T.monthly) + "' style='width:100%;box-sizing:border-box'></td>" +
|
||||
"<td style='width:58px'><input class='r-mult' type='number' step='0.1' value='" +
|
||||
(p && p.multiplier ? esc(p.multiplier) : "") + "' placeholder='" + esc(T.peakMult) + "' style='width:100%;box-sizing:border-box'></td>" +
|
||||
"<td style='width:88px'><input class='r-hours' value='" + esc(hoursText(p)) + "' placeholder='1-4,6-10' style='width:100%;box-sizing:border-box'></td>" +
|
||||
"<td style='width:62px'><input class='r-days' value='" + esc(daysText(p)) + "' placeholder='1,2,3' style='width:100%;box-sizing:border-box'></td>" +
|
||||
"<td style='width:88px'><select class='r-mode' style='width:100%;box-sizing:border-box;padding:2px;font-size:12px'>" + opts + "</select></td>" +
|
||||
"<td style='width:70px'><input class='r-cur' value='" + esc(rule.currency || "") + "' placeholder='USD' style='width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px'></td>" +
|
||||
"<td style='width:80px'><input class='r-sub' type='number' step='0.01' value='" +
|
||||
(rule.subscription ? esc(rule.subscription) : "") + "' placeholder='" + esc(T.monthly) + "' style='width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px'></td>" +
|
||||
"<td style='width:68px'><input class='r-mult' type='number' step='0.1' value='" +
|
||||
(p && p.multiplier ? esc(p.multiplier) : "") + "' placeholder='" + esc(T.peakMult) + "' style='width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px'></td>" +
|
||||
"<td style='width:96px'><input class='r-hours' value='" + esc(hoursText(p)) + "' placeholder='1-4,6-10' style='width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px'></td>" +
|
||||
"<td style='width:78px'><input class='r-days' value='" + esc(daysText(p)) + "' placeholder='1,2,3' style='width:100%;box-sizing:border-box;padding:2px 4px;font-size:12px'></td>" +
|
||||
"<td style='min-width:180px'>" + modelBlock(rule.models, T) + "</td>" +
|
||||
"<td style='width:56px'><button class='ghost small r-del'>" + esc(T.delRule) + "</button></td></tr>";
|
||||
}
|
||||
@ -1247,11 +1253,43 @@ plugin.ui = {
|
||||
"<span class='grow'></span>" +
|
||||
"<button class='ghost small p-add' data-pid='" + esc(p.id) + "'>" + esc(T.addRule) + "</button>" +
|
||||
"</div>" +
|
||||
// Widths live on <th> AND the layout is fixed — the two go together.
|
||||
//
|
||||
// Measured: with table-layout:fixed and widths on the <td>s, the 954px
|
||||
// container was divided into 9 equal 106px columns and every per-column
|
||||
// width was discarded, so two adjacent headers sat on top of each other
|
||||
// and the URL input was squeezed to "https:". With auto layout the
|
||||
// browser ignores the <th> widths entirely and sizes from content, so
|
||||
// the 70px currency column still collapsed to 48px (24px of input —
|
||||
// too narrow to show "USD"). Under fixed layout the FIRST ROW's widths
|
||||
// win, so the <th> widths below are the ones that take effect, and the
|
||||
// total (1046px) exceeds the container on purpose: the wrapper scrolls.
|
||||
"<div style='overflow-x:auto'><table style='width:100%;border-collapse:collapse;font-size:12px;table-layout:fixed'>" +
|
||||
// colgroup, not th widths: under table-layout:fixed the first row's
|
||||
// widths win, and the model-price cell holds a variable-width list of
|
||||
// price rows whose content width varies per rule. Measuring that cell
|
||||
// gave 352px on one rule and 206px on another — the whole row's
|
||||
// layout then depended on WHICH RULE happened to be widest. A colgroup
|
||||
// fixes the nine columns once and keeps every row identical.
|
||||
"<colgroup>" +
|
||||
// The last column holds a 「删除 / Delete」 button: 58px wrapped it
|
||||
// to two lines and the button grew taller than the row. 74px fits it
|
||||
// on one line; the peak-multiplier column gives up the difference
|
||||
// (it only ever holds a single digit such as "2").
|
||||
[214, 80, 62, 70, 66, 74, 62, 262, 74]
|
||||
.map(function (w) { return "<col style='width:" + w + "px'>"; }).join("") +
|
||||
"</colgroup>" +
|
||||
"<tr style='text-align:left;opacity:.65'>" +
|
||||
"<th>" + esc(T.url) + "</th><th>" + esc(T.mode) + "</th><th>" + esc(T.currency) + "</th>" +
|
||||
"<th>" + esc(T.monthly) + "</th><th>" + esc(T.peakMult) + "</th><th>" + esc(T.peakHours) + "</th>" +
|
||||
"<th>" + esc(T.peakDays) + "</th><th>" + esc(T.models) + "</th><th></th></tr>" +
|
||||
[[T.url], [T.mode], [T.currency], [T.monthly], [T.peakMult],
|
||||
[T.peakHours], [T.peakDays], [T.models], [""]]
|
||||
.map(function (c) {
|
||||
// nowrap + ellipsis: the bilingual headers ("峰段星期 (UTC 1=周一)")
|
||||
// are far wider than their column, and without this they spilled
|
||||
// into the neighbouring header. The full text stays in title.
|
||||
return "<th style='white-space:nowrap;overflow:hidden;text-overflow:ellipsis' title='" +
|
||||
esc(c[0]) + "'>" + esc(c[0]) + "</th>";
|
||||
}).join("") +
|
||||
"</tr>" +
|
||||
(rules.length
|
||||
? rules.map(function (r) { return ruleRow(r, known, T); }).join("")
|
||||
: "<tr><td colspan='9' class='muted' style='padding:8px 0'>" + esc(T.noRules) + "</td></tr>") +
|
||||
|
||||
Reference in New Issue
Block a user