ui: edit all 64 slots inline in bulk mode; drop per-row "+ Добавить"
"Изменить" now renders all 64 slots as editable rows — filled ones for editing, empty ones with blank fields (user prefilled "admin", channel 1) for adding. Saving PUTs changed cameras and POSTs filled empty slots (auto id camN, lowest free). Removed the per-row "+ Добавить" buttons; empty slots in normal view just show "пусто". Bulk edit can now be entered with zero cameras. VERSION 0.0.134. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+65
-35
@@ -1180,13 +1180,21 @@ const CamSettings = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
enterBulk() {
|
enterBulk() {
|
||||||
if (!this.cams.length) { alert("Нет камер для редактирования"); return; }
|
this.bulk = true; this.editing = true; // пустые слоты тоже редактируемы → можно с 0 камер
|
||||||
this.bulk = true; this.editing = true;
|
|
||||||
this.setBulkButtons();
|
this.setBulkButtons();
|
||||||
this.setBulkHeaders(true);
|
this.setBulkHeaders(true);
|
||||||
this.load();
|
this.load();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// наименьший свободный id вида camN (id скрыт от пользователя, нужен как ключ записи/потока)
|
||||||
|
nextCamId(used) {
|
||||||
|
for (let n = 1; n <= 999; n++) {
|
||||||
|
const id = "cam" + n;
|
||||||
|
if (!used.has(id)) { used.add(id); return id; }
|
||||||
|
}
|
||||||
|
return "cam" + (used.size + 1);
|
||||||
|
},
|
||||||
|
|
||||||
async exitBulk() {
|
async exitBulk() {
|
||||||
this.bulk = false; this.editing = false;
|
this.bulk = false; this.editing = false;
|
||||||
this.setBulkButtons();
|
this.setBulkButtons();
|
||||||
@@ -1212,20 +1220,29 @@ const CamSettings = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// ── строка камеры в режиме массового редактирования: те же колонки, но ячейки —
|
// ── строка камеры в режиме массового редактирования: те же колонки, но ячейки —
|
||||||
// редактируемые инпуты (НЕ отдельная форма). Статус-колонка на время правки = логин/пароль.
|
// редактируемые инпуты (НЕ отдельная форма). cam=null → пустой слот для новой камеры.
|
||||||
|
// Статус-колонка на время правки = логин/пароль.
|
||||||
bulkRowHtml(slotNo, cam) {
|
bulkRowHtml(slotNo, cam) {
|
||||||
const v = (x) => this.esc(x);
|
const v = (x) => this.esc(x);
|
||||||
|
const name = cam ? v(cam.name) : "";
|
||||||
|
const ip = cam ? v(cam.ip) : "";
|
||||||
|
const user = cam ? v(cam.user) : "admin";
|
||||||
|
const password = cam ? v(cam.password) : "";
|
||||||
|
const ch = cam ? channelFromPath(cam.main_path) : 1;
|
||||||
|
const enabled = cam ? cam.enabled : true;
|
||||||
|
const record = cam ? cam.record : true;
|
||||||
|
const storageId = cam ? cam.storage_id : "";
|
||||||
return (
|
return (
|
||||||
`<td class="muted">${slotNo}</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="name" value="${name}"${cam ? "" : ' placeholder="новая камера"'}></td>` +
|
||||||
`<td><input class="cell-in" type="text" data-f="ip" value="${v(cam.ip)}"></td>` +
|
`<td><input class="cell-in" type="text" data-f="ip" value="${ip}"${cam ? "" : ' placeholder="IP"'}></td>` +
|
||||||
`<td><select class="cell-sel" data-f="channel" title="Канал (поток)">${channelOptions(channelFromPath(cam.main_path))}</select></td>` +
|
`<td><select class="cell-sel" data-f="channel" title="Канал (поток)">${channelOptions(ch)}</select></td>` +
|
||||||
`<td><input type="checkbox" data-f="enabled" ${cam.enabled ? "checked" : ""}></td>` +
|
`<td><input type="checkbox" data-f="enabled" ${enabled ? "checked" : ""}></td>` +
|
||||||
`<td><input type="checkbox" data-f="record" ${cam.record ? "checked" : ""}></td>` +
|
`<td><input type="checkbox" data-f="record" ${record ? "checked" : ""}></td>` +
|
||||||
`<td><select class="stor-sel" data-f="storage_id">${this.storageOptions(cam.storage_id)}</select></td>` +
|
`<td><select class="stor-sel" data-f="storage_id">${this.storageOptions(storageId)}</select></td>` +
|
||||||
`<td class="cell-dual">` +
|
`<td class="cell-dual">` +
|
||||||
`<input class="cell-in" type="text" data-f="user" value="${v(cam.user)}" title="Логин">` +
|
`<input class="cell-in" type="text" data-f="user" value="${user}" title="Логин">` +
|
||||||
`<input class="cell-in" type="password" data-f="password" value="${v(cam.password)}" title="Пароль" placeholder="пароль">` +
|
`<input class="cell-in" type="password" data-f="password" value="${password}" title="Пароль" placeholder="пароль">` +
|
||||||
`</td>` +
|
`</td>` +
|
||||||
`<td></td>`
|
`<td></td>`
|
||||||
);
|
);
|
||||||
@@ -1240,37 +1257,49 @@ const CamSettings = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveBulk() {
|
async saveBulk() {
|
||||||
const rows = document.querySelectorAll("#cam-rows tr[data-id]");
|
const rows = document.querySelectorAll("#cam-rows tr.bulk-row");
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
const used = new Set(this.cams.map((c) => c.id)); // занятые id (для генерации новых)
|
||||||
for (const tr of rows) {
|
for (const tr of rows) {
|
||||||
const id = tr.dataset.id;
|
|
||||||
const orig = this.byId[id] || {};
|
|
||||||
const get = (f) => { const el = tr.querySelector(`[data-f="${f}"]`); return el ? el.value : ""; };
|
const get = (f) => { const el = tr.querySelector(`[data-f="${f}"]`); return el ? el.value : ""; };
|
||||||
|
const ch = +get("channel") || 1;
|
||||||
const payload = {
|
const payload = {
|
||||||
name: get("name").trim(),
|
name: get("name").trim(),
|
||||||
ip: get("ip").trim(),
|
ip: get("ip").trim(),
|
||||||
user: get("user").trim(),
|
user: get("user").trim(),
|
||||||
password: get("password"),
|
password: get("password"),
|
||||||
main_path: mainPathForChannel(+get("channel") || 1),
|
main_path: mainPathForChannel(ch),
|
||||||
sub_path: subPathForChannel(+get("channel") || 1),
|
sub_path: subPathForChannel(ch),
|
||||||
storage_id: get("storage_id"),
|
storage_id: get("storage_id"),
|
||||||
enabled: tr.querySelector('[data-f="enabled"]').checked,
|
enabled: tr.querySelector('[data-f="enabled"]').checked,
|
||||||
record: tr.querySelector('[data-f="record"]').checked,
|
record: tr.querySelector('[data-f="record"]').checked,
|
||||||
};
|
};
|
||||||
const changed =
|
if (tr.dataset.id) { // существующая камера → PUT только при изменении
|
||||||
payload.name !== orig.name || payload.ip !== orig.ip ||
|
const id = tr.dataset.id;
|
||||||
payload.user !== orig.user || payload.password !== (orig.password || "") ||
|
const orig = this.byId[id] || {};
|
||||||
payload.main_path !== orig.main_path ||
|
const changed =
|
||||||
(payload.sub_path || "") !== (orig.sub_path || "") ||
|
payload.name !== orig.name || payload.ip !== orig.ip ||
|
||||||
(payload.storage_id || "") !== (orig.storage_id || "") ||
|
payload.user !== orig.user || payload.password !== (orig.password || "") ||
|
||||||
payload.enabled !== orig.enabled ||
|
payload.main_path !== orig.main_path ||
|
||||||
payload.record !== orig.record;
|
(payload.sub_path || "") !== (orig.sub_path || "") ||
|
||||||
if (!changed) continue;
|
(payload.storage_id || "") !== (orig.storage_id || "") ||
|
||||||
const r = await fetch(`/api/cameras/${id}`, {
|
payload.enabled !== orig.enabled ||
|
||||||
method: "PUT", headers: { "Content-Type": "application/json" },
|
payload.record !== orig.record;
|
||||||
body: JSON.stringify(payload),
|
if (!changed) continue;
|
||||||
});
|
const r = await fetch(`/api/cameras/${id}`, {
|
||||||
if (!r.ok) { const d = await r.json().catch(() => ({})); errors.push(`${id}: ${d.detail || r.status}`); }
|
method: "PUT", headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
if (!r.ok) { const d = await r.json().catch(() => ({})); errors.push(`${id}: ${d.detail || r.status}`); }
|
||||||
|
} else { // пустой слот → создаём камеру, если заполнено имя/IP
|
||||||
|
if (!payload.name && !payload.ip) continue;
|
||||||
|
const id = this.nextCamId(used);
|
||||||
|
const r = await fetch("/api/cameras", {
|
||||||
|
method: "POST", headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id, ...payload }),
|
||||||
|
});
|
||||||
|
if (!r.ok) { const d = await r.json().catch(() => ({})); errors.push(`слот ${tr.dataset.slot}: ${d.detail || r.status}`); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.bulk = false; this.editing = false;
|
this.bulk = false; this.editing = false;
|
||||||
this.setBulkButtons();
|
this.setBulkButtons();
|
||||||
@@ -1376,14 +1405,15 @@ const CamSettings = {
|
|||||||
);
|
);
|
||||||
tb.innerHTML = "";
|
tb.innerHTML = "";
|
||||||
if (this.bulk) {
|
if (this.bulk) {
|
||||||
// массовое редактирование: те же строки/колонки, но ячейки — редактируемые инпуты
|
// массовое редактирование: ВСЕ 64 слота сразу редактируемые (пустые — для добавления)
|
||||||
cams.forEach((c, i) => {
|
for (let i = 0; i < this.SLOTS; i++) {
|
||||||
|
const c = cams[i] || null;
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.className = "bulk-row";
|
tr.className = "bulk-row";
|
||||||
tr.dataset.id = c.id;
|
if (c) tr.dataset.id = c.id; else tr.dataset.slot = i + 1;
|
||||||
tr.innerHTML = this.bulkRowHtml(i + 1, c);
|
tr.innerHTML = this.bulkRowHtml(i + 1, c);
|
||||||
tb.appendChild(tr);
|
tb.appendChild(tr);
|
||||||
});
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (let i = 0; i < this.SLOTS; i++) {
|
for (let i = 0; i < this.SLOTS; i++) {
|
||||||
@@ -1423,7 +1453,7 @@ const CamSettings = {
|
|||||||
`<td class="muted">${i + 1}</td>` +
|
`<td class="muted">${i + 1}</td>` +
|
||||||
`<td class="muted">—</td><td class="muted">—</td><td class="muted">—</td><td class="muted">—</td><td class="muted">—</td><td class="muted">—</td>` +
|
`<td class="muted">—</td><td class="muted">—</td><td class="muted">—</td><td class="muted">—</td><td class="muted">—</td><td class="muted">—</td>` +
|
||||||
`<td class="muted">пусто</td>` +
|
`<td class="muted">пусто</td>` +
|
||||||
`<td class="actions"><button data-act="add">+ Добавить</button></td>`;
|
`<td></td>`;
|
||||||
tb.appendChild(tr);
|
tb.appendChild(tr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user