修:选了「自定义」却看不到码率框(焦点还在下拉上,列表不重建)

页面为了让定时刷新不冲掉用户正在填的内容,列表在「焦点还在列表里」时不重建。
但原生 select 选完一项后焦点会留在下拉上,于是选了「自定义」以后列表一直不重建,
那个码率框要等用户点到别处才出现 —— 看起来就是「选了自定义根本没地方填」。

- 选到「自定义」时当场把码率框建出来并聚焦(不用等服务端回来和下一次重建),
  切走时当场收掉。
- 顺带把「不重建」的条件收紧成「只有正在列表里的文本框里打字时才跳过」:
  下拉和勾选都是改完立即提交的,之前它们也会一直卡住重建,
  连带「录播中/弹幕中」的徽标也要点到别处才刷新。
- 框里的初始值:填过自定义就用上次那个数(切走再切回来不会被改成当前档位),
  没填过才用现在实际在用的那一档。

验证方式也换了:回归脚本里改用「点开下拉 + 键盘选最后一项」这种真实操作
(Playwright 的 select_option 不保留焦点,正是它让这个 bug 漏了过去),
新增「选完立刻就有框」「焦点直接落在框里」「切走即收掉」「切回带着上次的值」四条断言;
verify_streamer_br.py 33 项、verify_qualities.py 全过,verify_settings.py /
verify_danmu_match.py 无回归,go test ./...、gofmt、go vet 干净。
This commit is contained in:
tamakyi
2026-09-25 21:14:59 +08:00
parent 237fffa8fe
commit aa47dd342d
+43 -7
View File
@@ -746,8 +746,11 @@ async function refresh(){
}).join("");
var list = document.getElementById("st");
var sig = JSON.stringify(st.streamers);
// 数据没变化不重建;正在列表里操作时也跳过
if(sig !== stSig && !list.contains(document.activeElement)){
// 只有「正在列表里的文本框里打字」才跳过重建:下拉和勾选都是改完立即提交的,
// 一直卡着不重建会让「选了自定义该出现的码率框」「录播中的徽标」迟迟不刷新
var ae = document.activeElement;
var typing = !!(ae && list.contains(ae) && ae.tagName === "INPUT" && ae.type !== "checkbox");
if(sig !== stSig && !typing){
stSig = sig;
// 重建前记下用户改过、还没提交的自定义码率,建完抄回去(填错的不用留)
var keepNum = {};
@@ -1289,6 +1292,36 @@ async function delUid(uid){
if(!confirm("确定删除 uid " + uid + " 并停止其录制?")) return;
post("/api/streamer/remove", {uid: uid});
}
// 选到「自定义」时当场把码率框建出来:列表在「正在文本框里打字」时不会重建,
// 光等服务端回来是看不到输入框的,用户会以为没地方填。顺带把焦点放进去,直接就能敲数字
function showBrNum(row, sel, focus){
var el = row.querySelector("input.brnum");
if(el){
if(focus){ el.focus(); el.select(); }
return el;
}
var uid = parseInt(sel.dataset.uid, 10);
var cur = (lastSt && lastSt.streamers ? lastSt.streamers : []).filter(function(s){ return s.uid === uid; })[0];
el = document.createElement("input");
el.type = "text";
el.className = "brnum";
el.dataset.uid = String(uid);
el.inputMode = "numeric";
el.placeholder = "Kbps";
el.title = "自定义:取不超过这个码率的最高档";
// 初始值:填过自定义就用上次那个数(切走再切回来不该被改掉),没填过就用现在实际在用的那一档,
// 免得切过去的一瞬间变成最低档
var v = cur ? (cur.bitrate || 0) : 0;
if(!(v > 0) && cur) v = cur.qualityBitrate || 0;
el.value = v;
sel.parentNode.insertBefore(el, sel.nextSibling);
if(focus){ el.focus(); el.select(); }
return el;
}
function hideBrNum(row){
var el = row.querySelector("input.brnum");
if(el && el.parentNode) el.parentNode.removeChild(el);
}
// 读一行里选的档位:pref 是下拉的值;自定义时把输入框里的码率一起带上,
// 输入框不在(刚切到自定义、或不是自定义)就不带 bitrate,服务端会保留原来填的值
function rowQuality(row){
@@ -1302,11 +1335,11 @@ function rowQuality(row){
v = parseInt(num.value.trim(), 10);
if(isNaN(v) || v < 0) return null;
}else{
// 刚切到自定义、输入框还没建出来:用「现在实际用到的档位码率」当起点,
// 免得切过去的一瞬间掉到最低档(uid 可能还没设过自定义值)
var uid = parseInt(sel.dataset.uid, 10);
var cur = (lastSt && lastSt.streamers ? lastSt.streamers : []).filter(function(s){ return s.uid === uid; })[0];
v = cur ? (cur.qualityBitrate || cur.bitrate || 0) : 0;
// 兜底:填过自定义就用上次那个数,没填过就用现在实际在用的那一档
var uid0 = parseInt(sel.dataset.uid, 10);
var cur0 = (lastSt && lastSt.streamers ? lastSt.streamers : []).filter(function(s){ return s.uid === uid0; })[0];
v = cur0 ? (cur0.bitrate || 0) : 0;
if(!(v > 0) && cur0) v = cur0.qualityBitrate || 0;
}
out.bitrate = v;
}
@@ -1328,6 +1361,9 @@ document.addEventListener("change", async function(e){
// 档位下拉是选完立即生效的(正在录制时会自动换档重录)
var uid3 = parseInt(t.dataset.uid, 10);
var row3 = t.closest(".row");
// 选到「自定义」当场把码率框建出来并聚焦,切走就收掉(不依赖列表重建,见 showBrNum)
if(t.value === "custom") showBrNum(row3, t, true);
else hideBrNum(row3);
var q3 = rowQuality(row3);
if(!q3){ showMsg(false, "自定义档位的码率必须是大于等于 0 的数字"); return; }
q3.uid = uid3;