ui: pick Hikvision channel (1-32) instead of typing full stream paths
Cameras (esp. an NVR at one IP with many channels) are configured by a channel
number now: channel N → main /Streaming/Channels/{N*100+1}, sub {N*100+2}
(101/102 = ch1, 1101/1102 = ch11, 3201/3202 = ch32). The add/edit form and the
bulk-edit row use a 1-32 dropdown; the camera list shows "канал N". Frontend
derives main_path/sub_path from the channel, so the backend is unchanged.
VERSION 0.0.132.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-11
@@ -106,6 +106,21 @@ function fmtUptime(s) {
|
|||||||
const STATE_RU = { online: "запись", retrying: "переподключение", offline: "выключена", starting: "запуск" };
|
const STATE_RU = { online: "запись", retrying: "переподключение", offline: "выключена", starting: "запуск" };
|
||||||
function dotClass(state) { return "dot " + (state || "offline"); }
|
function dotClass(state) { return "dot " + (state || "offline"); }
|
||||||
|
|
||||||
|
// Канал Hikvision (1-32) ↔ пути RTSP: канал N → осн. /Streaming/Channels/{N*100+1},
|
||||||
|
// суб /Streaming/Channels/{N*100+2} (101/102 = канал 1, 1101/1102 = канал 11, 3201/3202 = 32).
|
||||||
|
function channelFromPath(p) {
|
||||||
|
const m = /(\d+)\s*$/.exec(p || "");
|
||||||
|
const ch = m ? Math.floor(parseInt(m[1], 10) / 100) : 1;
|
||||||
|
return ch >= 1 ? ch : 1;
|
||||||
|
}
|
||||||
|
function mainPathForChannel(ch) { return `/Streaming/Channels/${ch * 100 + 1}`; }
|
||||||
|
function subPathForChannel(ch) { return `/Streaming/Channels/${ch * 100 + 2}`; }
|
||||||
|
function channelOptions(sel) {
|
||||||
|
let h = "";
|
||||||
|
for (let i = 1; i <= 32; i++) h += `<option value="${i}"${i === sel ? " selected" : ""}>${i}</option>`;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
// ── шапка: часы + статистика + выход ────────────────────────────
|
// ── шапка: часы + статистика + выход ────────────────────────────
|
||||||
function startClock() {
|
function startClock() {
|
||||||
const el = document.getElementById("clock");
|
const el = document.getElementById("clock");
|
||||||
@@ -1204,10 +1219,7 @@ const CamSettings = {
|
|||||||
`<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="${v(cam.name)}"></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="${v(cam.ip)}"></td>` +
|
||||||
`<td class="cell-dual">` +
|
`<td><select class="cell-sel" data-f="channel" title="Канал (поток)">${channelOptions(channelFromPath(cam.main_path))}</select></td>` +
|
||||||
`<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="enabled" ${cam.enabled ? "checked" : ""}></td>` +
|
||||||
`<td><input type="checkbox" data-f="record" ${cam.record ? "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><select class="stor-sel" data-f="storage_id">${this.storageOptions(cam.storage_id)}</select></td>` +
|
||||||
@@ -1239,8 +1251,8 @@ const CamSettings = {
|
|||||||
ip: get("ip").trim(),
|
ip: get("ip").trim(),
|
||||||
user: get("user").trim(),
|
user: get("user").trim(),
|
||||||
password: get("password"),
|
password: get("password"),
|
||||||
main_path: get("main_path").trim(),
|
main_path: mainPathForChannel(+get("channel") || 1),
|
||||||
sub_path: get("sub_path").trim(),
|
sub_path: subPathForChannel(+get("channel") || 1),
|
||||||
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,
|
||||||
@@ -1281,8 +1293,7 @@ const CamSettings = {
|
|||||||
`<label>IP<input type="text" id="e-ip" value="${cam ? v(cam.ip) : ""}" placeholder="192.168.63.x"></label>` +
|
`<label>IP<input type="text" id="e-ip" value="${cam ? v(cam.ip) : ""}" placeholder="192.168.63.x"></label>` +
|
||||||
`<label>Логин<input type="text" id="e-user" value="${cam ? v(cam.user) : "admin"}"></label>` +
|
`<label>Логин<input type="text" id="e-user" value="${cam ? v(cam.user) : "admin"}"></label>` +
|
||||||
`<label>Пароль<input type="password" id="e-pass" value="${cam ? v(cam.password) : ""}"></label>` +
|
`<label>Пароль<input type="password" id="e-pass" value="${cam ? v(cam.password) : ""}"></label>` +
|
||||||
`<label>Осн. поток<input type="text" id="e-main" value="${cam ? v(cam.main_path) : "/Streaming/Channels/101"}"></label>` +
|
`<label>Канал (поток)<select id="e-channel">${channelOptions(cam ? channelFromPath(cam.main_path) : 1)}</select></label>` +
|
||||||
`<label>Субпоток<input type="text" id="e-sub" value="${cam ? v(cam.sub_path) : "/Streaming/Channels/102"}" placeholder="можно пусто"></label>` +
|
|
||||||
`<label>Хранилище<select id="e-storage">${this.storageOptions(cam ? cam.storage_id : "")}</select></label>` +
|
`<label>Хранилище<select id="e-storage">${this.storageOptions(cam ? cam.storage_id : "")}</select></label>` +
|
||||||
`<label class="sw" title="Камера включена (выкл — не работает совсем)">Вкл<input type="checkbox" id="e-enabled" ${(!cam || cam.enabled) ? "checked" : ""}></label>` +
|
`<label class="sw" title="Камера включена (выкл — не работает совсем)">Вкл<input type="checkbox" id="e-enabled" ${(!cam || cam.enabled) ? "checked" : ""}></label>` +
|
||||||
`<label class="sw" title="Запись на диск (выкл — только просмотр)">REC<input type="checkbox" id="e-record" ${(!cam || cam.record) ? "checked" : ""}></label>` +
|
`<label class="sw" title="Запись на диск (выкл — только просмотр)">REC<input type="checkbox" id="e-record" ${(!cam || cam.record) ? "checked" : ""}></label>` +
|
||||||
@@ -1325,8 +1336,8 @@ const CamSettings = {
|
|||||||
ip: val("e-ip").trim(),
|
ip: val("e-ip").trim(),
|
||||||
user: val("e-user").trim(),
|
user: val("e-user").trim(),
|
||||||
password: val("e-pass"),
|
password: val("e-pass"),
|
||||||
main_path: val("e-main").trim(),
|
main_path: mainPathForChannel(+val("e-channel") || 1),
|
||||||
sub_path: val("e-sub").trim(),
|
sub_path: subPathForChannel(+val("e-channel") || 1),
|
||||||
storage_id: val("e-storage"),
|
storage_id: val("e-storage"),
|
||||||
enabled: document.getElementById("e-enabled").checked,
|
enabled: document.getElementById("e-enabled").checked,
|
||||||
record: document.getElementById("e-record").checked,
|
record: document.getElementById("e-record").checked,
|
||||||
@@ -1380,7 +1391,7 @@ const CamSettings = {
|
|||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
if (c) {
|
if (c) {
|
||||||
const st = (c.status && c.status.state) || "offline";
|
const st = (c.status && c.status.state) || "offline";
|
||||||
const streams = "осн. 101" + (c.has_sub ? " + суб. 102" : "");
|
const streams = "канал " + channelFromPath(c.main_path);
|
||||||
const statusCell = c.enabled
|
const statusCell = c.enabled
|
||||||
? `<span class="badge"><span class="${dotClass(st)}"></span>${STATE_RU[st] || st}</span>`
|
? `<span class="badge"><span class="${dotClass(st)}"></span>${STATE_RU[st] || st}</span>`
|
||||||
: `<span class="muted">выключена</span>`;
|
: `<span class="muted">выключена</span>`;
|
||||||
|
|||||||
@@ -437,8 +437,8 @@ button.primary { background: var(--accent); border-color: #c0392b; color: #fff;
|
|||||||
button.danger { border-color: #c0392b; color: #e74c3c; }
|
button.danger { border-color: #c0392b; color: #e74c3c; }
|
||||||
button.danger:hover { background: #c0392b; border-color: #c0392b; color: #fff; }
|
button.danger:hover { background: #c0392b; border-color: #c0392b; color: #fff; }
|
||||||
table.cams td.actions button { padding: 2px 8px; font-size: 12px; }
|
table.cams td.actions button { padding: 2px 8px; font-size: 12px; }
|
||||||
/* инлайн-выбор хранилища в строке — компактный, не раздувает высоту строки (как тумблеры) */
|
/* инлайн-селекты в строке (хранилище, канал) — компактные, не раздувают высоту строки */
|
||||||
table.cams td select.stor-sel {
|
table.cams td select.stor-sel, table.cams td select.cell-sel {
|
||||||
height: 22px; padding: 1px 4px; font-size: 12px; line-height: 1;
|
height: 22px; padding: 1px 4px; font-size: 12px; line-height: 1;
|
||||||
border-radius: 5px; width: 100%; max-width: 150px; box-sizing: border-box;
|
border-radius: 5px; width: 100%; max-width: 150px; box-sizing: border-box;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|||||||
Reference in New Issue
Block a user