Журнал обмена: автопоявление новых заявок; карточка не прыгает наверх при автообновлении

Журнал: новый эндпоинт /admin/journal/rows (строки новее since, рендер тем же шаблоном
journalrow) + JS раз в 8 с подставляет новые заявки сверху с мягкой подсветкой (row-new),
без перезагрузки и без дёрганья; счётчик обновляется, плашка «нет переводов» убирается.
Таблица теперь рендерится всегда (tbody#journal-body), строка вынесена в {{define journalrow}}.

Карточка заявки: автообновление по смене подписи раньше делало location.reload() и сбрасывало
прокрутку наверх — мешало читать. Теперь bjReloadKeepScroll() сохраняет позицию прокрутки
(sessionStorage) и восстанавливает после перезагрузки; автообновление сохранено.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fontvielle
2026-10-13 12:50:20 +03:00
parent fc067a3437
commit 8f44d6fcb9
3 changed files with 160 additions and 60 deletions
+65 -1
View File
@@ -175,6 +175,10 @@ tbody tr:hover td { background:var(--accent-weak); }
.live-meta.changed { color:var(--warn,#8a5a00); }
.live-meta.lost { color:var(--err,#b3261e); }
@keyframes livedot { 50% { opacity:.3 } }
@keyframes bjRowIn { 0% { background:#fff3bf; } 100% { background:transparent; } }
#journal-body tr.row-new > td { animation: bjRowIn 3.8s ease-out; }
#journal-live-hint .dot { background:var(--ok,#2e7d32); animation:livedot 2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce){ #journal-body tr.row-new > td { animation:none; } #journal-live-hint .dot{ animation:none; } }
@media (prefers-reduced-motion: reduce) { .live-meta .dot { animation:none } }
.badge.draft, .badge.validated, .badge.submitted_to_nsd { background:var(--accent-weak); color:var(--accent); }
.badge.awaiting_decision, .badge.manual_approval { background:var(--warn-weak); color:var(--warn); }
@@ -836,6 +840,21 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd
document.addEventListener('input', function(e){ if (e.target.closest('main form')) dirty = true; }, true);
document.addEventListener('change', function(e){ if (e.target.closest('main form')) dirty = true; }, true);
try { var u = new URL(location.href); if (u.searchParams.has('flash')) { u.searchParams.delete('flash'); history.replaceState(null, '', u.pathname + u.search + u.hash); } } catch(e){}
// Автообновление карточки перерисовывает страницу через перезагрузку. Сохраняем позицию
// прокрутки, чтобы при чтении заявку не отбрасывало наверх (автообновление остаётся).
var scrollKey = 'bj-scroll:' + location.pathname;
function bjReloadKeepScroll(){ try { sessionStorage.setItem(scrollKey, String(window.scrollY||window.pageYOffset||0)); } catch(e){} location.reload(); }
try {
var sy = sessionStorage.getItem(scrollKey);
if (sy !== null) {
sessionStorage.removeItem(scrollKey);
var syY = parseInt(sy, 10) || 0;
if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e){} }
window.scrollTo(0, syY);
setTimeout(function(){ window.scrollTo(0, syY); }, 0);
window.addEventListener('load', function(){ window.scrollTo(0, syY); });
}
} catch(e){}
function hhmmss(){ var d = new Date(); return ('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2)+':'+('0'+d.getSeconds()).slice(-2); }
function meta(e, text, cls){
var m = e.nextElementSibling;
@@ -857,7 +876,7 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd
if (!st) return;
var big = e.classList.contains('live-big'), was = e.getAttribute('data-sig');
if (big && was && st.sig && st.sig !== was) {
if (!dirty) { location.reload(); return; }
if (!dirty) { bjReloadKeepScroll(); return; }
meta(e, 'Заявка изменилась. <a href="' + location.pathname + location.search + '">Обновить страницу</a> (отмеченное в форме не сохранится)', 'changed');
e.setAttribute('data-sig', st.sig);
}
@@ -876,6 +895,51 @@ button.dq-link, .dq-link { background:none !important; border:0 !important; padd
setInterval(tick, 8000);
if (document.querySelector('.live[data-live][data-active]')) setTimeout(tick, 1500);
})();
// Журнал обмена: новые заявки подгружаются и всплывают сверху без перезагрузки страницы.
(function(){
var body = document.getElementById('journal-body');
if (!body) return;
var countEl = document.getElementById('journal-count');
var empty = document.getElementById('journal-empty');
function maxTime(){
var mx = '0';
body.querySelectorAll('tr[data-time]').forEach(function(tr){
var t = tr.getAttribute('data-time') || '0';
if (t.length > mx.length || (t.length === mx.length && t > mx)) mx = t;
});
return mx;
}
function seenKeys(){
var s = {};
body.querySelectorAll('tr[data-key]').forEach(function(tr){ s[tr.getAttribute('data-key')] = true; });
return s;
}
function tick(){
fetch('/admin/journal/rows?since=' + encodeURIComponent(maxTime()), {cache:'no-store'})
.then(function(r){ return r.json(); })
.then(function(d){
if (d && typeof d.count === 'number' && countEl) countEl.textContent = d.count;
if (!d || !d.rows || !d.rows.length) return;
var seen = seenKeys();
d.rows.sort(function(a,b){ return a.t < b.t ? -1 : a.t > b.t ? 1 : 0; }); // старые первыми — новые всплывают выше
var added = 0;
d.rows.forEach(function(row){
if (!row.key || seen[row.key]) return;
var tmp = document.createElement('tbody');
tmp.innerHTML = (row.html || '').trim();
var tr = tmp.firstElementChild;
if (!tr) return;
tr.classList.add('row-new');
body.insertBefore(tr, body.firstChild);
seen[row.key] = true; added++;
setTimeout((function(el){ return function(){ el.classList.remove('row-new'); }; })(tr), 4000);
});
if (added && empty) empty.hidden = true;
})
.catch(function(){});
}
setInterval(tick, 8000);
})();
})();
})();
</script>