fix(cablemanager): 修复线缆分组详情编辑时成员被自动刷新打回且保存无效的问题

根因:
- cablemanager 每 900ms scheduleAutoReload() 通过 WS state_update 触发 reloadAll({silent:true}),之前 reloadAll 在 detail-view 场景下每次都会无条件把 detailTempMembers 从服务器读回、selectedAvailable/selectedMembers 清空,所以用户刚把某站台 » 加进成员,下一次被动刷新到了就立刻还原(视觉上就是「点过去又跳回来」)
- 保存 saveDetail() PUT 本身是能成功的(server PUT /api/audio-cable-groups/:id 的 stationNames 接口契约没问题),但保存之后 loadGroups() 拉完数据又立刻触发 broadcastUpdate()→WS state_update→scheduleAutoReload→又一次 reloadAll()→把刚保存的 detailTempMembers 用服务器值覆盖掉,用户看起来就是「保存无效」
修复 (cablemanager.html reloadAll):
1. 保存 refresh 前后的 prevGroups/prevStations 快照,只有 name/color/description 或站台列表真的变了(groupListChanged/ stationListChanged)才重绘列表和快选,避免心跳、lastSeen、tokenHolder 这些纯瞬时字段反复重绘
2. 区分 silent (被动自动刷) vs 非 silent (用户手动点刷新):
   - 非 silent:保持原语义,完全用服务器值覆盖所有本地编辑(用户主动点刷新就是要回到当前后台状态)
   - silent:
     * 只在 metaChanged 时同步 name/description/color(并且不抢焦点的 input,避免正在打字被重置)
     * 只在 membersChanged(后台 stationNames 真的跟上次不一样)时才同步成员到 detailTempMembers,否则保持用户未保存的成员编辑不变;同时清理 selected 中已不存在的名字
     * renderDualLists 仅在 membersChanged || stationListChanged || metaChanged 才调用,其余时间完全不重绘详情
3. 服务器 PUT 端再验证过 stationNames 接口契约没问题:前端 saveDetail 发 {name,color,description,stationNames} 对得上 server PUT handler,保存完 /api/audio-cable-groups 读回 stationNames 正确
APP_VERSION_TEXT bump 到 v1.0.111
This commit is contained in:
2026-08-17 18:11:57 +08:00
parent a58632272e
commit 1179130656
2 changed files with 57 additions and 8 deletions
+56 -7
View File
@@ -1519,16 +1519,65 @@
async function reloadAll(options) {
const silent = !!(options && options.silent);
try {
const prevGroups = audioCableGroups.slice();
const prevStations = stations.slice();
await Promise.all([loadGroups(), loadStations()]);
renderGroupList();
renderQuickSelects();
const groupListChanged = JSON.stringify(prevGroups.map(g => ({ id: g.id, name: g.name, color: g.color, description: g.description })))
!== JSON.stringify(audioCableGroups.map(g => ({ id: g.id, name: g.name, color: g.color, description: g.description })));
const stationListChanged = prevStations.length !== stations.length
|| prevStations.some((s, i) => stations[i] && (s.name !== stations[i].name || s.code !== stations[i].code));
if (groupListChanged || stationListChanged) {
renderGroupList();
renderQuickSelects();
}
if (currentGroupId) {
const g = audioCableGroups.find(g => g.id === currentGroupId);
if (g) {
detailTempMembers = [...(g.stationNames || [])];
selectedAvailable.clear();
selectedMembers.clear();
renderDualLists();
if (!g) {
currentGroupId = null;
backToList();
} else {
const pg = prevGroups.find(x => x.id === currentGroupId) || null;
const membersChanged = !pg || JSON.stringify(pg.stationNames || []) !== JSON.stringify(g.stationNames || []);
const metaChanged = !pg || pg.name !== g.name || pg.color !== g.color || pg.description !== g.description;
if (silent) {
// 被动自动刷新:同步名字/描述/颜色的标题区,只在后台成员真的变了才同步成员(否则保留用户未保存的编辑)
const detailNameEl = document.getElementById('detail-name');
const detailDescEl = document.getElementById('detail-description');
const detailTitleEl = document.getElementById('detail-title');
if (metaChanged) {
if (detailTitleEl && detailNameEl && !detailNameEl.matches(':focus')) {
detailNameEl.value = g.name || '';
detailTitleEl.textContent = g.name || '';
}
if (detailDescEl && !detailDescEl.matches(':focus')) {
detailDescEl.value = g.description || '';
}
if (detailSelectedColor !== (g.color || PRESET_COLORS[0])) {
detailSelectedColor = g.color || PRESET_COLORS[0];
document.getElementById('detail-color').style.background = detailSelectedColor;
_rebuildDetailColorPicker();
}
}
if (membersChanged) {
// 后台分组成员已经变了(例如被别的端删除/改了分组)—— 同步掉成员,避免用户编辑的是已经不存在的状态
detailTempMembers = [...(g.stationNames || [])];
// 清理选择集中已经不存在的名字,保留仍然存在的
for (const n of Array.from(selectedAvailable)) {
if (!stations.some(s => s.name === n)) selectedAvailable.delete(n);
}
for (const n of Array.from(selectedMembers)) {
if (!detailTempMembers.includes(n)) selectedMembers.delete(n);
}
}
if (membersChanged || stationListChanged || metaChanged) renderDualLists();
} else {
// 用户主动点刷新:全同步(包括成员)
detailTempMembers = [...(g.stationNames || [])];
selectedAvailable.clear();
selectedMembers.clear();
renderDualLists();
}
}
}
if (!silent) showToast('成功', '数据已刷新', 'success');
+1 -1
View File
@@ -1,6 +1,6 @@
-- 1. Configuration & Constants
local APP_VERSION_TEXT = "v1.0.110"
local APP_VERSION_TEXT = "v1.0.111"
local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group",