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: "запуск" };
|
||||
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() {
|
||||
const el = document.getElementById("clock");
|
||||
@@ -1204,10 +1219,7 @@ const CamSettings = {
|
||||
`<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><select class="cell-sel" data-f="channel" title="Канал (поток)">${channelOptions(channelFromPath(cam.main_path))}</select></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>` +
|
||||
@@ -1239,8 +1251,8 @@ const CamSettings = {
|
||||
ip: get("ip").trim(),
|
||||
user: get("user").trim(),
|
||||
password: get("password"),
|
||||
main_path: get("main_path").trim(),
|
||||
sub_path: get("sub_path").trim(),
|
||||
main_path: mainPathForChannel(+get("channel") || 1),
|
||||
sub_path: subPathForChannel(+get("channel") || 1),
|
||||
storage_id: get("storage_id"),
|
||||
enabled: tr.querySelector('[data-f="enabled"]').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>Логин<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="text" id="e-main" value="${cam ? v(cam.main_path) : "/Streaming/Channels/101"}"></label>` +
|
||||
`<label>Субпоток<input type="text" id="e-sub" value="${cam ? v(cam.sub_path) : "/Streaming/Channels/102"}" placeholder="можно пусто"></label>` +
|
||||
`<label>Канал (поток)<select id="e-channel">${channelOptions(cam ? channelFromPath(cam.main_path) : 1)}</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="Запись на диск (выкл — только просмотр)">REC<input type="checkbox" id="e-record" ${(!cam || cam.record) ? "checked" : ""}></label>` +
|
||||
@@ -1325,8 +1336,8 @@ const CamSettings = {
|
||||
ip: val("e-ip").trim(),
|
||||
user: val("e-user").trim(),
|
||||
password: val("e-pass"),
|
||||
main_path: val("e-main").trim(),
|
||||
sub_path: val("e-sub").trim(),
|
||||
main_path: mainPathForChannel(+val("e-channel") || 1),
|
||||
sub_path: subPathForChannel(+val("e-channel") || 1),
|
||||
storage_id: val("e-storage"),
|
||||
enabled: document.getElementById("e-enabled").checked,
|
||||
record: document.getElementById("e-record").checked,
|
||||
@@ -1380,7 +1391,7 @@ const CamSettings = {
|
||||
const tr = document.createElement("tr");
|
||||
if (c) {
|
||||
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
|
||||
? `<span class="badge"><span class="${dotClass(st)}"></span>${STATE_RU[st] || st}</span>`
|
||||
: `<span class="muted">выключена</span>`;
|
||||
|
||||
Reference in New Issue
Block a user