fix(ui): 优先级页的保存/重置/添加按钮每次打开都被画布渲染冲掉

renderSortEditor 会整体替换 mount 的 innerHTML。per-key AUTO 弹窗的 mount
只有画布,覆写无害;但全局优先级页把 .card(连同里面装着工具栏的 h2)当
mount 传了进去,于是每次打开页面,画布一渲染就把工具栏删了。

症状很隐蔽:泳道和积木照常渲染、拖拽也正常,只是「保存排序」「重置」
「+ 添加档位」和聊天/生图切换全部消失 —— 优先级变得无从调整,也无从保存。
DOM 里数画布元素一切正常,只有量按钮的几何尺寸才看得见它没了。

修法:工具栏留在 .card 里,另给一个 #sort-mount 专供画布。per-key 弹窗
本来就是这个结构,无需改动。

判据 1 条,断言工具栏必须落在 #sort-mount 之外,且 renderSortEditor 必须
接收 #sort-mount 而不是 .card。变异(mount 改回 .card)判红。

生产 CDP 实测(真实点击,非仅 DOM 断言):
  5 个按钮全部可见,坐标尺寸正常(保存排序 72x29)
  画布 4 泳道 / 10 块
  点击「保存排序」→ toast「排序已保存并热重载 · 链上冷却已复位」
  切走再回来,按钮仍在位

Co-Authored-By: ModelRouter <noreply@modelrouter.dev>
This commit is contained in:
JianFeeeee
2026-10-04 07:05:11 +08:00
parent 48e7e6b2a5
commit 0bf0653d00
2 changed files with 53 additions and 3 deletions

View File

@ -3648,13 +3648,20 @@ recsState.keyNames = keyNames || {};
sortState.origin = JSON.stringify(sortState.lanes); sortState.origin = JSON.stringify(sortState.lanes);
const kindToggle = const kindToggle =
`<div class="kind-toggle"><button class="${sortState.kind === "chat" ? "" : "ghost"} small" onclick="switchSortKind('chat')">${t("kChat") || "聊天"}</button><button class="${sortState.kind === "image" ? "" : "ghost"} small" onclick="switchSortKind('image')">${t("kImage") || "生图"}</button></div>`; `<div class="kind-toggle"><button class="${sortState.kind === "chat" ? "" : "ghost"} small" onclick="switchSortKind('chat')">${t("kChat") || "聊天"}</button><button class="${sortState.kind === "image" ? "" : "ghost"} small" onclick="switchSortKind('image')">${t("kImage") || "生图"}</button></div>`;
// renderSortEditor replaces its mount wholesale, so the mount must be
// the canvas slot ALONE. Passing the .card (which holds the h2 with the
// save/reset/add buttons) used to wipe the toolbar on every open — the
// lane canvas survived, the buttons did not. The per-key modal never hit
// this because its mount only ever contained the canvas.
$("#tab-sort").innerHTML = ` $("#tab-sort").innerHTML = `
<div class="card"><h2><span>${t("sortTitle")}</span><span class="grow"></span>${kindToggle} <div class="card">
<h2><span>${t("sortTitle")}</span><span class="grow"></span>${kindToggle}
<button class="ghost small" onclick="scrAddModal()">+ ${t("sortAdd")}</button> <button class="ghost small" onclick="scrAddModal()">+ ${t("sortAdd")}</button>
<button class="ghost small" onclick="sortReset()">${t("sortReset")}</button> <button class="ghost small" onclick="sortReset()">${t("sortReset")}</button>
<button class="small" onclick="saveSort()">${t("sortSave")}</button></h2> <button class="small" onclick="saveSort()">${t("sortSave")}</button></h2>
<div id="sort-mount"></div>
</div>`; </div>`;
renderSortEditor($("#tab-sort").querySelector(".card"), { renderSortEditor($("#sort-mount"), {
canvasId: "scr-canvas", canvasId: "scr-canvas",
}); });
} }

View File

@ -393,3 +393,46 @@ func TestAutoScopeConflictMessageComesFromCheckQuota(t *testing.T) {
t.Fatal("checkQuota's AUTO branch must name the scope as the cause") t.Fatal("checkQuota's AUTO branch must name the scope as the cause")
} }
} }
// renderSortEditor replaces its mount's innerHTML wholesale. The global
// priority page used to hand it the .card that also holds the h2 toolbar, so
// every open deleted the save/reset/add buttons: the lanes still rendered and
// the drag worked, but there was no way to persist a change. The per-key modal
// never showed this because its mount held only the canvas.
//
// The regression is invisible in the DOM when you only count lane elements, so
// this asserts on the structure: the toolbar lives in a sibling of the mount,
// not inside it.
func TestGlobalSortToolbarSurvivesCanvasRender(t *testing.T) {
src := uiSource(t)
// The global editor must mount into a dedicated host, not the .card.
i := strings.Index(src, `$("#tab-sort").innerHTML = `)
if i < 0 {
t.Fatal("global sort markup not found")
}
chunk := src[i:min(len(src), i+1400)]
if !strings.Contains(chunk, `id="sort-mount"`) {
t.Fatal("global sort page must provide a dedicated #sort-mount for the canvas")
}
// The mount handed to renderSortEditor must be that host.
if !strings.Contains(src, `renderSortEditor($("#sort-mount")`) {
t.Fatal("renderSortEditor must be given #sort-mount; passing the .card " +
"wipes the h2 toolbar that holds the save button")
}
// The toolbar (save in particular) must be OUTSIDE #sort-mount.
close := strings.Index(chunk, "</div>`;")
if close < 0 {
t.Fatal("malformed #tab-sort template")
}
head := chunk[:close]
mountAt := strings.Index(head, `id="sort-mount"`)
saveAt := strings.Index(head, `onclick="saveSort()"`)
if saveAt < 0 {
t.Fatal("the global sort page has no save button at all")
}
if mountAt >= 0 && saveAt > mountAt {
t.Fatal("the save button sits inside the canvas mount and will be " +
"erased the moment the canvas renders")
}
}