archive: replace toolbar with a 220px split sidebar (camera list + calendar)
The player-frame toolbar (camera <select> + date input) is removed. Archive main is now a row: a 220px sidebar split in half by height — the top half is a scrollable clickable camera list, the bottom half holds the date input plus a new inline month calendar (prev/next month, today + selected highlight, Monday-first). Picking a camera or a day loads that day's recordings. The player and DVR timeline move into an .arch-content column on the right. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+12
-9
@@ -16,16 +16,19 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="archive-main">
|
<main class="archive-main">
|
||||||
<div class="toolbar">
|
<aside class="arch-side">
|
||||||
<label class="muted">Камера:</label>
|
<div class="arch-cams" id="arch-cams"></div>
|
||||||
<select id="cam-select"></select>
|
<div class="arch-date">
|
||||||
<label class="muted">Дата:</label>
|
<input type="date" id="date" class="arch-dateinput">
|
||||||
<input type="date" id="date">
|
<div class="arch-cal" id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class="arch-content">
|
||||||
|
<div class="player-wrap" id="player">
|
||||||
|
<div class="hint">Кликните по таймлайну ниже, чтобы смотреть запись</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline" id="timeline"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="player-wrap" id="player">
|
|
||||||
<div class="hint">Кликните по таймлайну ниже, чтобы смотреть запись</div>
|
|
||||||
</div>
|
|
||||||
<div class="timeline" id="timeline"></div>
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+79
-9
@@ -1025,7 +1025,7 @@ class ArchivePlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Archive = {
|
const Archive = {
|
||||||
cameras: [], cam: null, date: null, segments: [], transcode: false,
|
cameras: [], cam: null, date: null, segments: [], transcode: false, calYear: 0, calMonth: 0,
|
||||||
centerTime: 0, dayStart: 0, dragging: false, following: false, track: null, _curSeg: null, _playTok: 0,
|
centerTime: 0, dayStart: 0, dragging: false, following: false, track: null, _curSeg: null, _playTok: 0,
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -1033,16 +1033,19 @@ const Archive = {
|
|||||||
this.cameras = data.cameras;
|
this.cameras = data.cameras;
|
||||||
this.transcode = profileMode("a") === "transcode"; // профиль архива (этот браузер)
|
this.transcode = profileMode("a") === "transcode"; // профиль архива (этот браузер)
|
||||||
this.cam = this.cameras[0] ? this.cameras[0].id : null;
|
this.cam = this.cameras[0] ? this.cameras[0].id : null;
|
||||||
|
this.buildCamList();
|
||||||
|
|
||||||
const sel = document.getElementById("cam-select");
|
|
||||||
sel.innerHTML = this.cameras.map((c) => `<option value="${c.id}">${c.name}</option>`).join("");
|
|
||||||
sel.onchange = () => { this.cam = sel.value; this.load(); };
|
|
||||||
|
|
||||||
const dp = document.getElementById("date");
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
dp.value = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
this.date = `${today.getFullYear()}-${pad(today.getMonth() + 1)}-${pad(today.getDate())}`;
|
||||||
this.date = dp.value;
|
this.calYear = today.getFullYear();
|
||||||
dp.onchange = () => { this.date = dp.value; this.load(); };
|
this.calMonth = today.getMonth();
|
||||||
|
const dp = document.getElementById("date");
|
||||||
|
if (dp) {
|
||||||
|
dp.value = this.date;
|
||||||
|
dp.onchange = () => { this.date = dp.value; this.syncCalToDate(); this.renderCalendar(); this.load(); };
|
||||||
|
}
|
||||||
|
this.bindCalendar();
|
||||||
|
this.renderCalendar();
|
||||||
|
|
||||||
// при ресайзе окна пересчитать смещение ленты (центр зависит от ширины вьюпорта)
|
// при ресайзе окна пересчитать смещение ленты (центр зависит от ширины вьюпорта)
|
||||||
window.addEventListener("resize", () => { if (this.track) this._applyTl(); });
|
window.addEventListener("resize", () => { if (this.track) this._applyTl(); });
|
||||||
@@ -1050,6 +1053,73 @@ const Archive = {
|
|||||||
this.load();
|
this.load();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// верхняя половина сайдбара: список камер (клик — выбор)
|
||||||
|
buildCamList() {
|
||||||
|
const box = document.getElementById("arch-cams");
|
||||||
|
if (!box) return;
|
||||||
|
box.innerHTML = this.cameras.map((c) =>
|
||||||
|
`<div class="arch-cam${c.id === this.cam ? " active" : ""}" data-cam="${c.id}">${c.name}</div>`
|
||||||
|
).join("");
|
||||||
|
box.onclick = (e) => {
|
||||||
|
const it = e.target.closest(".arch-cam[data-cam]");
|
||||||
|
if (!it) return;
|
||||||
|
this.cam = it.dataset.cam;
|
||||||
|
box.querySelectorAll(".arch-cam").forEach((x) => x.classList.toggle("active", x.dataset.cam === this.cam));
|
||||||
|
this.load();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
syncCalToDate() {
|
||||||
|
const [y, m] = (this.date || "").split("-").map(Number);
|
||||||
|
if (y && m) { this.calYear = y; this.calMonth = m - 1; }
|
||||||
|
},
|
||||||
|
|
||||||
|
bindCalendar() {
|
||||||
|
const box = document.getElementById("calendar");
|
||||||
|
if (!box) return;
|
||||||
|
box.onclick = (e) => {
|
||||||
|
const nav = e.target.closest("[data-cal]");
|
||||||
|
if (nav) {
|
||||||
|
if (nav.dataset.cal === "prev") { if (--this.calMonth < 0) { this.calMonth = 11; this.calYear--; } }
|
||||||
|
else { if (++this.calMonth > 11) { this.calMonth = 0; this.calYear++; } }
|
||||||
|
this.renderCalendar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const day = e.target.closest(".cal-day[data-day]");
|
||||||
|
if (!day) return;
|
||||||
|
this.date = `${this.calYear}-${pad(this.calMonth + 1)}-${pad(+day.dataset.day)}`;
|
||||||
|
const dp = document.getElementById("date");
|
||||||
|
if (dp) dp.value = this.date;
|
||||||
|
this.renderCalendar();
|
||||||
|
this.load();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// нижняя половина сайдбара: месячный календарь
|
||||||
|
renderCalendar() {
|
||||||
|
const box = document.getElementById("calendar");
|
||||||
|
if (!box) return;
|
||||||
|
const MN = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"];
|
||||||
|
const WD = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||||||
|
const y = this.calYear, m = this.calMonth;
|
||||||
|
const [sy, sm, sd] = (this.date || "").split("-").map(Number);
|
||||||
|
const startDow = (new Date(y, m, 1).getDay() + 6) % 7; // 0 = понедельник
|
||||||
|
const days = new Date(y, m + 1, 0).getDate();
|
||||||
|
const t = new Date();
|
||||||
|
let html = `<div class="cal-head">` +
|
||||||
|
`<button class="cal-nav" data-cal="prev" title="Предыдущий месяц">‹</button>` +
|
||||||
|
`<span class="cal-title">${MN[m]} ${y}</span>` +
|
||||||
|
`<button class="cal-nav" data-cal="next" title="Следующий месяц">›</button></div>`;
|
||||||
|
html += `<div class="cal-grid">` + WD.map((w) => `<div class="cal-wd">${w}</div>`).join("");
|
||||||
|
for (let i = 0; i < startDow; i++) html += `<div class="cal-day empty"></div>`;
|
||||||
|
for (let dd = 1; dd <= days; dd++) {
|
||||||
|
const sel = (y === sy && m === sm - 1 && dd === sd) ? " sel" : "";
|
||||||
|
const today = (y === t.getFullYear() && m === t.getMonth() && dd === t.getDate()) ? " today" : "";
|
||||||
|
html += `<div class="cal-day${sel}${today}" data-day="${dd}">${dd}</div>`;
|
||||||
|
}
|
||||||
|
box.innerHTML = html + `</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
if (!this.cam || !this.date) return;
|
if (!this.cam || !this.date) return;
|
||||||
const [y, m, d] = this.date.split("-").map(Number);
|
const [y, m, d] = this.date.split("-").map(Number);
|
||||||
|
|||||||
@@ -247,7 +247,47 @@ footer.grid-bar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Архив ── */
|
/* ── Архив ── */
|
||||||
.archive-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
.archive-main { flex: 1; display: flex; overflow: hidden; } /* сайдбар + контент */
|
||||||
|
/* левый сайдбар архива: 220px, поделён по высоте пополам */
|
||||||
|
.arch-side {
|
||||||
|
width: 220px; flex: none; display: flex; flex-direction: column;
|
||||||
|
background: var(--panel); border-right: 1px solid var(--border); overflow: hidden;
|
||||||
|
}
|
||||||
|
.arch-cams { flex: 1 1 50%; min-height: 0; overflow-y: auto; border-bottom: 1px solid var(--border); }
|
||||||
|
.arch-cam {
|
||||||
|
padding: 6px 12px; font-size: 13px; color: var(--text); cursor: pointer;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.arch-cam:hover { background: var(--panel-2); }
|
||||||
|
.arch-cam.active { background: #c0392b; color: #fff; font-weight: 600; }
|
||||||
|
.arch-date {
|
||||||
|
flex: 1 1 50%; min-height: 0; overflow-y: auto;
|
||||||
|
display: flex; flex-direction: column; gap: 8px; padding: 10px;
|
||||||
|
}
|
||||||
|
.arch-dateinput {
|
||||||
|
width: 100%; background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; padding: 5px 8px; font-size: 13px;
|
||||||
|
}
|
||||||
|
.arch-cal { user-select: none; }
|
||||||
|
.cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
|
||||||
|
.cal-title { font-size: 13px; font-weight: 600; color: var(--text); }
|
||||||
|
.cal-nav {
|
||||||
|
background: var(--panel-2); border: 1px solid var(--border); color: var(--text);
|
||||||
|
width: 24px; height: 24px; border-radius: 6px; cursor: pointer; padding: 0; line-height: 1; font-size: 14px;
|
||||||
|
}
|
||||||
|
.cal-nav:hover { border-color: #c0392b; color: #fff; }
|
||||||
|
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
|
||||||
|
.cal-wd { font-size: 10px; color: var(--text-dim); text-align: center; padding: 2px 0; }
|
||||||
|
.cal-day {
|
||||||
|
text-align: center; font-size: 12px; padding: 4px 0; border-radius: 5px; cursor: pointer;
|
||||||
|
color: var(--text); font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.cal-day.empty { visibility: hidden; cursor: default; }
|
||||||
|
.cal-day:hover:not(.empty) { background: var(--panel-2); }
|
||||||
|
.cal-day.today { box-shadow: inset 0 0 0 1px var(--text-dim); }
|
||||||
|
.cal-day.sel { background: #c0392b; color: #fff; font-weight: 600; }
|
||||||
|
/* контент архива (плеер + таймлайн) */
|
||||||
|
.arch-content { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
|
||||||
.player-wrap { flex: 1; background: #000; display: flex; align-items: center; justify-content: center; min-height: 0; }
|
.player-wrap { flex: 1; background: #000; display: flex; align-items: center; justify-content: center; min-height: 0; }
|
||||||
.player-wrap video { max-width: 100%; max-height: 100%; }
|
.player-wrap video { max-width: 100%; max-height: 100%; }
|
||||||
.player-wrap .hint { color: var(--text-dim); }
|
.player-wrap .hint { color: var(--text-dim); }
|
||||||
|
|||||||
Reference in New Issue
Block a user