From 756aadb4d31daff92c0b477a40fb8c8e346e0602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 5 Dec 2025 13:03:57 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B1=D0=B5=D1=81=D0=BA=D0=BE=D0=BD=D0=B5?= =?UTF-8?q?=D1=87=D0=BD=D0=B0=D1=8F=20=D0=BF=D1=80=D0=BE=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Заменена пагинация на Intersection Observer - Документы подгружаются автоматически при прокрутке вниз - Добавлено отслеживание nextUrl и hasMore из API - При append рендерятся только новые документы, без дублирования - Убраны кнопки Назад/Вперёд - Добавлено логирование для отладки - Сохранена фильтрация по scope и ack_status --- .../js/components/documentListHandler.js | 277 +++++++++++++----- 1 file changed, 206 insertions(+), 71 deletions(-) diff --git a/backend/static/js/components/documentListHandler.js b/backend/static/js/components/documentListHandler.js index 1e16b684..c575c63a 100644 --- a/backend/static/js/components/documentListHandler.js +++ b/backend/static/js/components/documentListHandler.js @@ -32,28 +32,159 @@ export function initDocumentListHandler(options) { // Текущее состояние let currentScope = 'mine'; // 'mine' | 'all' - let currentPage = 1; let currentAckStatus = ''; // '' | 'acked' | 'not_acked' + let allDocuments = []; // Все загруженные документы + let loading = false; + let hasMore = true; + let nextUrl = null; + let totalCount = 0; + + // Intersection Observer для бесконечной прокрутки + let observerTarget = null; + let observer = null; /** * Загружает документы с API + * @param {boolean} append - Добавить к существующим или заменить + * @returns {Promise} Массив новых документов */ - async function loadDocuments() { + async function loadDocuments(append = false) { + if (loading || (append && !hasMore)) { + console.log('loadDocuments: skipped', { loading, append, hasMore }); + return []; + } + + loading = true; + showLoadingSpinner(); + try { - const params = new URLSearchParams({ page: currentPage }); - const response = await fetch(`${apiListUrl}?${params}`, { headers }); + let url; + if (append && nextUrl) { + url = nextUrl; + console.log('loadDocuments: using nextUrl', url); + } else if (append && !nextUrl) { + console.warn('loadDocuments: append=true but no nextUrl, stopping'); + loading = false; + hideLoadingSpinner(); + return []; + } else { + url = apiListUrl; + console.log('loadDocuments: initial load', url); + } + + const response = await fetch(url, { headers }); if (!response.ok) { throw new Error('HTTP ' + response.status); } const data = await response.json(); - renderDocuments(data); + const newDocuments = data.results || []; + + console.log('loadDocuments: received', { + newCount: newDocuments.length, + totalCount: data.count, + hasNext: !!data.next, + currentDocuments: allDocuments.length + }); + + // Сохраняем метаданные + totalCount = data.count || newDocuments.length; + nextUrl = data.next || null; + hasMore = !!nextUrl; + + if (append) { + allDocuments = [...allDocuments, ...newDocuments]; + console.log('loadDocuments: appended, total now:', allDocuments.length); + } else { + allDocuments = newDocuments; + console.log('loadDocuments: replaced, total now:', allDocuments.length); + } + + loading = false; + hideLoadingSpinner(); + + return newDocuments; } catch (error) { console.error('Ошибка загрузки документов:', error); + loading = false; + hideLoadingSpinner(); listElement.innerHTML = '
Не удалось загрузить документы
'; + return []; } } + + /** + * Показать спиннер загрузки + */ + function showLoadingSpinner() { + const existing = listElement.querySelector('.loading-spinner'); + if (existing) return; + + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner text-center py-4'; + spinner.innerHTML = ` +
+ Загрузка... +
+ `; + listElement.appendChild(spinner); + } + + /** + * Скрыть спиннер загрузки + */ + function hideLoadingSpinner() { + const spinner = listElement.querySelector('.loading-spinner'); + if (spinner) { + spinner.remove(); + } + } + + /** + * Загрузка следующей порции документов + */ + async function loadMore() { + console.log('loadMore: triggered', { + hasMore, + loading, + nextUrl, + currentCount: allDocuments.length + }); + + try { + const newDocuments = await loadDocuments(true); // append = true + renderDocuments(newDocuments, true); // append = true + } catch (error) { + console.error('Failed to load more documents:', error); + } + } + + /** + * Настройка Intersection Observer для бесконечной прокрутки + */ + function setupInfiniteScroll() { + if (!observerTarget) return; + + // Создаем observer + observer = new IntersectionObserver( + (entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && hasMore && !loading) { + loadMore(); + } + }); + }, + { + root: null, + rootMargin: '100px', + threshold: 0.01 + } + ); + + // Наблюдаем за observer target + observer.observe(observerTarget); + } /** * Фильтрует документы для текущего пользователя @@ -101,25 +232,62 @@ export function initDocumentListHandler(options) { /** * Отображает список документов - * @param {Object} data - Данные от API + * @param {Array} documents - Документы для рендеринга + * @param {boolean} append - Добавить к существующим или заменить */ - function renderDocuments(data) { - const allItems = data.results || []; - let items = filterForUser(allItems); - items = filterByAckStatus(items); + function renderDocuments(documents, append = false) { + if (!documents || documents.length === 0) { + if (!append) { + listElement.innerHTML = '
Нет доступных документов.
'; + if (paginationElement) paginationElement.innerHTML = ''; + } + return; + } - if (items.length === 0) { + let items = filterForUser(documents); + items = filterByAckStatus(items); + + if (items.length === 0 && !append) { listElement.innerHTML = '
Нет доступных документов.
'; if (paginationElement) paginationElement.innerHTML = ''; return; } - // Рендерим документы - listElement.innerHTML = items.map(doc => renderDocumentRow(doc)).join(''); + console.log('renderDocuments: rendering', { + count: items.length, + append, + totalInMemory: allDocuments.length + }); - // Рендерим пагинацию + const itemsHtml = items.map(doc => renderDocumentRow(doc)).join(''); + + if (append) { + // Удаляем observer target перед добавлением + if (observerTarget && observerTarget.parentNode) { + observerTarget.remove(); + } + // Добавляем новые карточки + listElement.insertAdjacentHTML('beforeend', itemsHtml); + console.log('renderDocuments: appended to DOM'); + // Возвращаем observer target обратно + if (observerTarget) { + listElement.appendChild(observerTarget); + } + } else { + listElement.innerHTML = itemsHtml; + console.log('renderDocuments: replaced DOM'); + // Создаем observer target при первой загрузке + if (hasMore && !observerTarget) { + observerTarget = document.createElement('div'); + observerTarget.className = 'load-more-trigger'; + observerTarget.style.height = '1px'; + listElement.appendChild(observerTarget); + } + } + + // Убираем старую пагинацию if (paginationElement) { - renderPagination(data); + paginationElement.innerHTML = ''; } } @@ -204,48 +372,6 @@ export function initDocumentListHandler(options) { `; } - /** - * Рендерит пагинацию - * @param {Object} data - Данные от API с next/previous - */ - function renderPagination(data) { - if (!data.next && !data.previous && currentPage === 1) { - paginationElement.innerHTML = ''; - return; - } - - paginationElement.innerHTML = ` -
- - -
- `; - } - - /** - * Обработчик кликов по пагинации - */ - function handlePaginationClick(e) { - const button = e.target.closest('button[data-action]'); - if (!button || button.classList.contains('disabled')) return; - - const action = button.getAttribute('data-action'); - - if (action === 'prev-page') { - currentPage = Math.max(1, currentPage - 1); - loadDocuments(); - } else if (action === 'next-page') { - currentPage += 1; - loadDocuments(); - } - } - /** * Форматирует дату * @param {string} dateStr - ISO дата @@ -273,37 +399,46 @@ export function initDocumentListHandler(options) { return div.innerHTML; } - // Установка обработчиков - if (paginationElement) { - paginationElement.addEventListener('click', handlePaginationClick); - } - /** * Публичный API для изменения фильтров */ - function setScope(scope) { + async function setScope(scope) { currentScope = scope; - currentPage = 1; - loadDocuments(); + hasMore = true; + nextUrl = null; + allDocuments = []; + const docs = await loadDocuments(false); + renderDocuments(docs, false); + setupInfiniteScroll(); } - function setAckStatus(status) { + async function setAckStatus(status) { currentAckStatus = status; - currentPage = 1; - loadDocuments(); + hasMore = true; + nextUrl = null; + allDocuments = []; + const docs = await loadDocuments(false); + renderDocuments(docs, false); + setupInfiniteScroll(); } /** * Очистка обработчиков */ function destroy() { - if (paginationElement) { - paginationElement.removeEventListener('click', handlePaginationClick); + if (observer) { + observer.disconnect(); + observer = null; } } // Начальная загрузка - loadDocuments(); + (async () => { + hasMore = true; + const docs = await loadDocuments(false); + renderDocuments(docs, false); + setupInfiniteScroll(); + })(); return { load: loadDocuments,