ui: bulk-edit edits cells in place instead of a stacked per-row form

"Изменить" previously replaced each camera row with a vertical .cam-edit-grid
form inside one colspan cell — effectively a different table. Now bulk edit
keeps the same columns and turns the cells into compact inline inputs:
Камера→name, IP→ip, Потоки→main/sub, Вкл/Запись toggles, Хранилище select,
and the Статус column becomes Логин/Пароль (header swaps via data-bulk-label).
Live per-row onchange is suppressed while bulk is active (changes collected
until Сохранить). Inputs are compact (22px) so row height stays dense.

VERSION 0.0.131.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:01:50 +05:00
parent 421f873160
commit abc726b5a7
4 changed files with 50 additions and 25 deletions
+37 -23
View File
@@ -1168,12 +1168,14 @@ const CamSettings = {
if (!this.cams.length) { alert("Нет камер для редактирования"); return; }
this.bulk = true; this.editing = true;
this.setBulkButtons();
this.setBulkHeaders(true);
this.load();
},
async exitBulk() {
this.bulk = false; this.editing = false;
this.setBulkButtons();
this.setBulkHeaders(false);
await this.load();
},
@@ -1194,34 +1196,44 @@ const CamSettings = {
alert(`Удалено камер: ${d.removed != null ? d.removed : n}`);
},
// ── строка камеры в режиме массового редактирования ──
// ── строка камеры в режиме массового редактирования: те же колонки, но ячейки —
// редактируемые инпуты (НЕ отдельная форма). Статус-колонка на время правки = логин/пароль.
bulkRowHtml(slotNo, cam) {
const v = (x) => this.esc(x);
return (
`<td colspan="9" class="cam-edit">` +
`<div class="cam-bulk-head">#${slotNo} · ID ${v(cam.id)}</div>` +
`<div class="cam-edit-grid" data-cam="${v(cam.id)}">` +
`<label>Имя<input type="text" data-f="name" value="${v(cam.name)}"></label>` +
`<label>IP<input type="text" data-f="ip" value="${v(cam.ip)}"></label>` +
`<label>Логин<input type="text" data-f="user" value="${v(cam.user)}"></label>` +
`<label>Пароль<input type="password" data-f="password" value="${v(cam.password)}"></label>` +
`<label>Осн. поток<input type="text" data-f="main_path" value="${v(cam.main_path)}"></label>` +
`<label>Субпоток<input type="text" data-f="sub_path" value="${v(cam.sub_path)}" placeholder="можно пусто"></label>` +
`<label>Хранилище<select data-f="storage_id">${this.storageOptions(cam.storage_id)}</select></label>` +
`<label class="sw" title="Камера включена (выкл — не работает совсем)">Вкл<input type="checkbox" data-f="enabled" ${cam.enabled ? "checked" : ""}></label>` +
`<label class="sw" title="Запись на диск (выкл — только просмотр)">REC<input type="checkbox" data-f="record" ${cam.record ? "checked" : ""}></label>` +
`</div>` +
`</td>`
`<td class="muted">${slotNo}</td>` +
`<td><input class="cell-in" type="text" data-f="name" value="${v(cam.name)}"></td>` +
`<td><input class="cell-in" type="text" data-f="ip" value="${v(cam.ip)}"></td>` +
`<td class="cell-dual">` +
`<input class="cell-in" type="text" data-f="main_path" value="${v(cam.main_path)}" title="Основной поток">` +
`<input class="cell-in" type="text" data-f="sub_path" value="${v(cam.sub_path)}" title="Субпоток" placeholder="суб.">` +
`</td>` +
`<td><input type="checkbox" data-f="enabled" ${cam.enabled ? "checked" : ""}></td>` +
`<td><input type="checkbox" data-f="record" ${cam.record ? "checked" : ""}></td>` +
`<td><select class="stor-sel" data-f="storage_id">${this.storageOptions(cam.storage_id)}</select></td>` +
`<td class="cell-dual">` +
`<input class="cell-in" type="text" data-f="user" value="${v(cam.user)}" title="Логин">` +
`<input class="cell-in" type="password" data-f="password" value="${v(cam.password)}" title="Пароль" placeholder="пароль">` +
`</td>` +
`<td></td>`
);
},
// подмена заголовков колонок на время массового редактирования (Статус → Логин/Пароль)
setBulkHeaders(on) {
document.querySelectorAll("table.cams th[data-bulk-label]").forEach((th) => {
if (on) { if (th.dataset.orig == null) th.dataset.orig = th.textContent; th.textContent = th.dataset.bulkLabel; }
else if (th.dataset.orig != null) th.textContent = th.dataset.orig;
});
},
async saveBulk() {
const grids = document.querySelectorAll(".cam-edit-grid[data-cam]");
const rows = document.querySelectorAll("#cam-rows tr[data-id]");
const errors = [];
for (const g of grids) {
const id = g.dataset.cam;
for (const tr of rows) {
const id = tr.dataset.id;
const orig = this.byId[id] || {};
const get = (f) => { const el = g.querySelector(`[data-f="${f}"]`); return el ? el.value : ""; };
const get = (f) => { const el = tr.querySelector(`[data-f="${f}"]`); return el ? el.value : ""; };
const payload = {
name: get("name").trim(),
ip: get("ip").trim(),
@@ -1230,8 +1242,8 @@ const CamSettings = {
main_path: get("main_path").trim(),
sub_path: get("sub_path").trim(),
storage_id: get("storage_id"),
enabled: g.querySelector('[data-f="enabled"]').checked,
record: g.querySelector('[data-f="record"]').checked,
enabled: tr.querySelector('[data-f="enabled"]').checked,
record: tr.querySelector('[data-f="record"]').checked,
};
const changed =
payload.name !== orig.name || payload.ip !== orig.ip ||
@@ -1250,6 +1262,7 @@ const CamSettings = {
}
this.bulk = false; this.editing = false;
this.setBulkButtons();
this.setBulkHeaders(false);
await this.load();
if (errors.length) alert("Не сохранено:\n" + errors.join("\n"));
},
@@ -1352,10 +1365,10 @@ const CamSettings = {
);
tb.innerHTML = "";
if (this.bulk) {
// режим массового редактирования: только существующие камеры, форма на каждую
// массовое редактирование: те же строки/колонки, но ячейки — редактируемые инпуты
cams.forEach((c, i) => {
const tr = document.createElement("tr");
tr.className = "cam-edit-row";
tr.className = "bulk-row";
tr.dataset.id = c.id;
tr.innerHTML = this.bulkRowHtml(i + 1, c);
tb.appendChild(tr);
@@ -1433,6 +1446,7 @@ const CamSettings = {
};
tb.onchange = async (e) => { // в строке: «Вкл», «REC» (тумблеры) и «Хранилище» (select)
if (this.bulk) return; // в массовом режиме изменения копятся до «Сохранить»
const en = e.target.closest(".en-toggle");
const rec = e.target.closest(".rec-toggle");
const stor = e.target.closest(".stor-sel");
+11
View File
@@ -443,6 +443,17 @@ table.cams td select.stor-sel {
border-radius: 5px; width: 100%; max-width: 150px; box-sizing: border-box;
vertical-align: middle;
}
/* массовое редактирование: инпуты прямо в ячейках, компактные (высота строки не растёт) */
table.cams td input.cell-in {
height: 22px; padding: 1px 6px; font-size: 12px; line-height: 1;
border-radius: 5px; width: 100%; box-sizing: border-box; vertical-align: middle;
background: var(--panel-2); border: 1px solid var(--border); color: var(--text);
}
table.cams td input.cell-in:focus { outline: none; border-color: #c0392b; }
table.cams td.cell-dual { white-space: nowrap; }
table.cams td.cell-dual input.cell-in { display: inline-block; width: calc(50% - 3px); }
table.cams td.cell-dual input.cell-in + input.cell-in { margin-left: 4px; }
table.cams tr.bulk-row td { background: rgba(192, 57, 43, .05); }
.badge {
display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px;
border-radius: 11px; font-size: 12px; background: var(--panel-2);