From abc726b5a7f1b2b50e719f87b02791545b7dbaf1 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 1 Jun 2026 20:01:50 +0500 Subject: [PATCH] ui: bulk-edit edits cells in place instead of a stacked per-row form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Изменить" 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) --- VERSION | 2 +- frontend/settings.html | 2 +- frontend/static/app.js | 60 ++++++++++++++++++++++++--------------- frontend/static/style.css | 11 +++++++ 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/VERSION b/VERSION index ee358a3..1ad6b81 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.130 +0.0.131 diff --git a/frontend/settings.html b/frontend/settings.html index cfc7b34..8b510d9 100644 --- a/frontend/settings.html +++ b/frontend/settings.html @@ -56,7 +56,7 @@ - +
IDКамераIPПотокиВклЗаписьХранилищеСтатус
IDКамераIPПотокиВклЗаписьХранилищеСтатус
diff --git a/frontend/static/app.js b/frontend/static/app.js index f81f416..ae55709 100644 --- a/frontend/static/app.js +++ b/frontend/static/app.js @@ -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 ( - `` + - `
#${slotNo} · ID ${v(cam.id)}
` + - `
` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `` + - `
` + - `` + `${slotNo}` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` + + `` ); }, + // подмена заголовков колонок на время массового редактирования (Статус → Логин/Пароль) + 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"); diff --git a/frontend/static/style.css b/frontend/static/style.css index f197a7f..28730ec 100644 --- a/frontend/static/style.css +++ b/frontend/static/style.css @@ -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);