Дата: Вторник, 28.10.2025, 15:06 | Сообщение # 1 |
|
Написал: Начинающий
Автор темы
Мурчанн
не в сети
Сообщений: 101
Скрытая проверка и рендер кэша Старый скрипт: 1. При старте кэш проверялся прямо в DOM через renderCards(cached.cards, false). 2. Это приводило к мерцанию и схлопыванию контейнера на несколько сотых секунды, потому что карточки обновлялись сразу в видимой карусели.Новая версия: 1. Создаётся временный скрытый контейнер (hiddenTrack), куда сначала рендерятся карточки. 2. Только после полной подготовки и установки размеров карточки вставляются в видимый трек. 3. Эффект: визуально нет схлопывания, мерцания или сдвига контейнера при старте. 2. Гибкая ширина и количество видимых карточекСтарый скрипт: 1. Все карточки занимали 100% ширины контейнера. 2. Количество одновременно видимых карточек жёстко не задавалось. 3. Ширина карточки зависела от размера контейнера (container.clientWidth).Новая версия: Введены константы: Код
const VISIBLE_CARDS = 1; // сколько карточек видно одновременно const CARD_WIDTH = 785; // ширина одной карточки в px
Карточки теперь имеют фиксированную ширину, а трек сдвигается по CARD_WIDTH. Эффект: карусель более предсказуемая, можно легко изменить количество видимых карточек и ширину для адаптивного дизайна. 3. Обновлённая логика слайдов и прокруткиСтарый скрипт: 1. updateCarouselPosition() сдвигал трек по всей ширине контейнера. 2. Ограничение индекса было приблизительным (index > total - 1). 3. Стрелки работали, но свайп и клавиши обрабатывались по общему числу карточек.Новая версия: Добавлена логика максимального индекса: Код
const maxIndex = Math.max(0, total - VISIBLE_CARDS); if (index > maxIndex) index = maxIndex;
Учитывается именно количество видимых карточек при прокрутке. Свайп, клавиши и стрелки работают корректно с учётом фиксированной ширины карточек и видимого окна.4. Скрытый рендер при подгрузке новых данных Старый скрипт: При loadData() карточки рендерились напрямую в track. Если подгрузка данных заняла хоть 100–200 мс, происходило мерцание и схлопывание контейнера.Новая версия: Добавлено создание hiddenTrack и рендер всех карточек скрытно. После полного формирования карточек они вставляются в видимую карусель.Эффект: плавный рендер, без визуальных дерганий, проверка кэша также проходит скрытно. 5. Улучшенный рендер карточек Старый скрипт: renderCards() использовал flex: 0 0 100% для всех карточек. Анимация прозрачности была довольно грубая (opacity 220ms) и в некоторых случаях контейнер схлопывался.Новая версия: renderCards() использует флекс с фиксированной шириной:Код
a.style.flex = `0 0 ${CARD_WIDTH}px`;
Добавлена плавная анимация: Код
track.style.transition = 'opacity 0.25s ease'; track.style.opacity = '0.5';
После вставки карточек minHeight убирается, предотвращая схлопывание. 6. Обновлённое автообновление Основная логика интервала setInterval(() => loadData(false), 10 * 60 * 1000) осталась,но теперь новые данные подгружаются скрытно, что предотвращает мерцание при обновлении. 7. Общее улучшение UX Визуальные баги, связанные со схлопыванием трека и миганием карточек, полностью устранены. Карточки всегда готовы к рендеру, индексация и стрелки работают корректно даже при изменении количества видимых карточек. Возможность легко масштабировать карусель (адаптивная ширина, количество карточек).Новая версия скрипта делает карусель: Плавной (нет мерцания, схлопывания и дергания контейнера) Гибкой (фиксированная ширина и видимое количество карточек) Скрытой (кэш и подгрузка новых данных проходят полностью в hiddenTrack) Более предсказуемой (корректная прокрутка стрелками, свайпом и клавишами)В целом это полная переработка UX и внутренней логики рендеринга, при сохранении всей функциональности старого парсера. Код
<!-- <body> --> <section class="sect flex-grow-1"> <div class="sect__header d-flex"> <h2 class="sect__title flex-grow-1">Горячая тема</h2> <style> /* Неоновая сиреневая обёртка */ .nf-dark-border { position: relative; border: 1px solid #9D4EDD; padding: 30px; background-color: #0a0710; color: #fff; width: 850px; margin: 40px auto; border-radius: 12px; box-shadow: 0 0 25px rgba(157, 78, 221, 0.7), 0 0 50px rgba(157, 78, 221, 0.5), inset 0 0 20px rgba(157, 78, 221, 0.25); animation: nf-neonGlow 3s ease-in-out infinite alternate; overflow: visible; } /* Анимация сияния */ @keyframes nf-neonGlow { from { box-shadow: 0 0 15px rgba(157, 78, 221, 0.5), 0 0 30px rgba(157, 78, 221, 0.3), inset 0 0 12px rgba(157, 78, 221, 0.15); } to { box-shadow: 0 0 35px rgba(157, 78, 221, 0.9), 0 0 70px rgba(157, 78, 221, 0.7), inset 0 0 25px rgba(157, 78, 221, 0.3); } } /* Контейнер карусели */ .nf-carousel-container { position: relative; width: 100%; overflow: hidden; } /* Трек карточек */ .nf-carousel-track { display: flex; transition: transform 0.5s ease; } /* Карточки */ .nf-card { flex: 0 0 100%; background: linear-gradient(145deg, #1c1122, #2a1735); border-radius: 14px; overflow: hidden; display: flex; text-decoration: none; color: #fff; cursor: pointer; position: relative; transition: box-shadow 0.3s ease, filter 0.3s ease; min-height: 250px; aspect-ratio: 16 / 9; } /* Карточка при наведении */ .nf-card:hover { box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4); filter: brightness(1.45); /* мягче подсветка */ } /* Изображение */ .nf-card-img { width: 55%; height: 100%; overflow: hidden; } .nf-card-img img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s ease; } .nf-card:hover .nf-card-img img { transform: scale(1.03); } /* Контент карточки */ .nf-card-content { padding: 25px 30px; display: flex; flex-direction: column; justify-content: flex-start; /* заголовок сверху */ width: 45%; } .nf-card-title { font-weight: bold; font-size: 26px; margin-bottom: 20px; /* немного больше пространства */ color: #e0b3ff; text-shadow: 0 0 10px rgba(157, 78, 221, 0.6); } .nf-card-desc { font-size: 16px; color: #d8c3f0; line-height: 1.7; text-align: justify; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 12; /* длиннее описание */ -webkit-box-orient: vertical; flex-grow: 1; /* описание заполняет пространство */ } /* Автор */ .nf-card-meta { position: absolute; bottom: 15px; right: 18px; display: flex; flex-direction: column; align-items: center; gap: 6px; z-index: 5; } .nf-card-meta img { width: 55px; height: 55px; border-radius: 50%; border: 2px solid #b367ff; object-fit: cover; background: #1a0e25; box-shadow: 0 0 10px rgba(157, 78, 221, 0.6); } .nf-card-meta .author-nick { font-size: 14px; font-weight: bold; color: #d7a7ff; background: rgba(0, 0, 0, 0.45); padding: 3px 10px; border-radius: 8px; backdrop-filter: blur(3px); text-shadow: 0 0 6px rgba(157, 78, 221, 0.8); } /* Бейдж */ .nf-card-badge { position: absolute; top: 10px; left: 10px; background: linear-gradient(135deg, #b367ff, #7a3dbd); padding: 5px 9px; border-radius: 8px; font-size: 11px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.6px; box-shadow: 0 0 12px rgba(157, 78, 221, 0.6); } /* Стрелки */ .nf-carousel-arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 52px; height: 52px; background: radial-gradient(circle at center, rgba(157, 78, 221, 0.85), rgba(80, 30, 140, 0.8)); border-radius: 50%; color: #fff; font-size: 30px; text-align: center; line-height: 52px; cursor: pointer; user-select: none; z-index: 20; box-shadow: 0 0 20px rgba(157, 78, 221, 0.8), 0 0 35px rgba(157, 78, 221, 0.6); transition: all 0.3s ease; } .nf-carousel-arrow:hover { background: radial-gradient(circle at center, rgba(180, 100, 255, 0.9), rgba(100, 40, 160, 0.85)); transform: translateY(-50%) scale(1.1); } .nf-carousel-arrow:active { background: rgba(157, 78, 221, 0.7); } .nf-carousel-arrow-left { left: -25px; } .nf-carousel-arrow-right { right: -25px; } /* Нижняя панель телевизора */ .nf-tv-bottom { position: relative; width: 100%; height: 40px; /* высота панели */ background-color: #000; /* черная панель */ display: flex; justify-content: center; align-items: center; font-family: 'Arial', sans-serif; font-weight: bold; font-size: 18px; color: #fff; letter-spacing: 2px; text-shadow: 0 0 6px rgba(255,255,255,0.5); border-top: 0px solid rgba(157,78,221,0.5); /* тонкая светящаяся линия сверху */ margin-top: 10px; border-radius: 0 0 12px 12px; /* скругление как у рамки */ margin-top: 1px; /* или отрицательное значение, например -5px, чтобы поднять выше */ } .tablet-stylus { position: absolute; /* Горизонтальное положение стилиуса: left или right */ left: -20px; ; /* если хочешь слева */ right: 100px; /* если хочешь справа, регулируй значение */ /* Вертикальное положение */ top: 42%; /* центр по высоте */ transform: translateY(-50%) rotate(0deg) scale(1); /* центровка + угол + масштаб */ /* Размеры */ width: 14px; /* толщина стилуса */ height: 400px; /* длина стилуса */ /* Дизайн */ background: linear-gradient(180deg, #ffffff, #e6e6e6); border-radius: 10px; box-shadow: inset 0 0 4px rgba(255,255,255,0.7), 0 0 8px rgba(255,255,255,0.8), 0 0 12px rgba(255,255,255,0.5); z-index: 10; /* Дополнительно для регулировки */ /* margin-left или margin-right можно использовать для точной подстройки */ } /* Кончик стилуса */ .tablet-stylus::after { content: ''; position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%); width: 8px; height: 8px; background: radial-gradient(circle at center, #ccc 20%, #999 80%); border-radius: 50%; box-shadow: 0 0 3px rgba(0,0,0,0.4); } </style> <div class="nf-dark-border"> <div class="nf-carousel-container"> <div class="nf-carousel-track" id="nf-forum-cards"></div> <!-- Нижняя панель телевизора --> <div class="nf-tv-bottom"> <span>SAMSUNG</span> </div> </div> <div class="nf-carousel-arrow nf-carousel-arrow-left">❮</div> <div class="nf-carousel-arrow nf-carousel-arrow-right">❯</div> <!-- Белый стилус сбоку планшета --> <div class="tablet-stylus"></div> </div> <script> (async function nfForumCarouselHot3_fixedCarousel() { const track = document.querySelector('.nf-carousel-track'); if (!track) return; const arrowLeft = document.querySelector('.nf-carousel-arrow-left'); const arrowRight = document.querySelector('.nf-carousel-arrow-right'); const container = track.closest('.nf-carousel-container') || track.parentElement; const BASE_FORUM_URL = '/forum/0-0-1-34'; const CACHE_KEY = 'nf_forum_hot_3_clean'; const CACHE_TIME = 30 * 60 * 60 * 1000; // 1 час const MIN_REFRESH_MS = 5000; const MAX_CARDS = 20; const VISIBLE_CARDS = 1; // сколько карточек видно одновременно const CARD_WIDTH = 785; // ширина одной карточки в px let lastFetch = 0; let inProgress = false; let lastRenderedHrefs = ''; let index = 0; let arrowsBound = false; function toAbs(url) { if (!url) return ''; if (url.startsWith('http')) return url; if (url.startsWith('//')) return window.location.protocol + url; return window.location.origin + (url.startsWith('/') ? url : '/' + url); } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } function getCleanText(el, maxLength = 220) { if (!el) return ''; let text = ''; const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false); let node; while (node = walker.nextNode()) { if(node.parentNode.closest('script, style, iframe')) continue; let t = node.nodeValue; if(!t) continue; t = t.replace(/[\t\n\r\u00A0]+/g, ' ').replace(/\s+/g, ' ').trim(); text += t + ' '; } text = text.replace(/\s+([.,!?;:])/g, '$1').replace(/<[^>]+>/g, ''); text = text.replace(/[^\wа-яА-Я0-9.,!?;: \-()'"«»]/g, ''); return text.trim().slice(0, maxLength); } async function fetchHTML(url) { const res = await fetch(url, { credentials: 'same-origin', cache: 'no-cache' }); return await res.text(); } function findHotInDoc(doc) { const rows = Array.from(doc.querySelectorAll('td.threadNametd, .threadNametd, tr')); const result = []; for (let td of rows) { const badge = td.querySelector('.ipsBadge'); const pages = td.querySelector('.postpSwithces, .postPSwithcesLink'); if (!((badge && /горяч/i.test(badge.textContent || '')) || pages)) continue; const linkEl = td.querySelector('a.threadLink'); if (!linkEl) continue; const href = toAbs(linkEl.getAttribute('href') || linkEl.href || ''); const title = (linkEl.textContent || '').trim() || '(без названия)'; const row = td.closest('tr'); let icon = row?.querySelector('img')?.getAttribute('src') || ''; icon = toAbs(icon) || '/forumimages/Newsletter-3.png'; const author = (row?.querySelector('.threadAuthTd')?.textContent || '').trim() || 'Аноним'; const views = parseInt((row?.querySelector('.threadViewTd, .views')?.textContent || '').replace(/\D/g, '')) || 0; result.push({ href, title, icon, author, views }); if (result.length >= MAX_CARDS) break; } return result; } function uniqueThreads(arr) { const seen = new Set(); return arr.filter(t => { if (seen.has(t.href)) return false; seen.add(t.href); return true; }); } async function enrichThread(t) { try { const txt = await fetchHTML(t.href); const doc = new DOMParser().parseFromString(txt, 'text/html'); const postEl = doc.querySelector('.post_content, .post_body, .post, .message, .ipsType_richText'); const text = getCleanText(postEl); let img = ''; if (postEl) { for (let im of Array.from(postEl.querySelectorAll('img'))) { const src = im.getAttribute('data-src') || im.src || ''; if (!src || /avatar|forumimages|template/i.test(src)) continue; img = toAbs(src); break; } } if (!img) img = t.icon; const authorEl = doc.querySelector('.postUser, .author, .post-author-name'); const author = (authorEl?.textContent?.trim()) || t.author || 'Аноним'; const avatarEl = doc.querySelector('.avatar img, .ipsUserPhoto'); const avatar = toAbs(avatarEl?.src || '/forumimages/default-avatar.png'); return { ...t, text, img, author, avatar }; } catch (e) { return { ...t, text: '', img: t.icon, avatar: '/forumimages/default-avatar.png' }; } } function setSlideSizes() { const slides = Array.from(track.children); slides.forEach(s => s.style.flex = `0 0 ${CARD_WIDTH}px`); track.style.display = 'flex'; track.style.transition = 'transform 0.45s ease'; } function updateCarouselPosition() { const total = track.children.length; const maxIndex = Math.max(0, total - VISIBLE_CARDS); if (index < 0) index = 0; if (index > maxIndex) index = maxIndex; const width = CARD_WIDTH; track.style.transform = `translateX(${-index * width}px)`; if (arrowLeft) arrowLeft.style.opacity = index === 0 ? '0.4' : '1'; if (arrowRight) arrowRight.style.opacity = index >= maxIndex ? '0.4' : '1'; } function bindArrowsOnce() { if (arrowsBound) return; arrowsBound = true; arrowLeft?.addEventListener('click', () => { index--; updateCarouselPosition(); }); arrowRight?.addEventListener('click', () => { index++; updateCarouselPosition(); }); window.addEventListener('keydown', e => { if (e.key === 'ArrowLeft') index--, updateCarouselPosition(); if (e.key === 'ArrowRight') index++, updateCarouselPosition(); }); let startX = null; track.addEventListener('touchstart', e => startX = e.touches[0].clientX, { passive:true }); track.addEventListener('touchend', e => { if (startX === null) return; const dx = e.changedTouches[0].clientX - startX; if (Math.abs(dx) > 50) index = dx < 0 ? index + 1 : index - 1; updateCarouselPosition(); startX = null; }, { passive:true }); } function renderCards(cards, smooth = true) { if (!cards?.length) return; const hrefs = cards.map(c => c.href).join('|'); if (hrefs === lastRenderedHrefs) { setSlideSizes(); updateCarouselPosition(); return; } lastRenderedHrefs = hrefs; const temp = document.createElement('div'); temp.style.display = 'flex'; temp.style.flexWrap = 'nowrap'; for (const c of cards) { const a = document.createElement('a'); a.className = 'nf-card'; a.href = c.href; a.target = '_self'; a.style.flex = `0 0 ${CARD_WIDTH}px`; a.innerHTML = ` <div class="nf-card-img"><img src="${c.img}" alt="${escapeHtml(c.title)}"><div class="nf-card-badge">🔥 Горячая тема</div></div> <div class="nf-card-content"> <div class="nf-card-title">${escapeHtml(c.title)}</div> <div class="nf-card-desc">${escapeHtml(c.text || '')}</div> <div class="nf-card-meta" style="display:flex;align-items:center;gap:8px;"> <img src="${c.avatar}" style="width:48px;height:48px;border-radius:50%;"> <span class="author-nick">${escapeHtml(c.author)}</span> <span class="views-count">👁️ ${c.views}</span> </div> </div>`; temp.appendChild(a); } const currentHeight = track.offsetHeight; track.style.minHeight = currentHeight + 'px'; track.style.transition = 'opacity 0.25s ease'; track.style.opacity = '0.5'; setTimeout(() => { track.innerHTML = temp.innerHTML; setSlideSizes(); updateCarouselPosition(); track.style.opacity = '1'; setTimeout(() => track.style.minHeight = '', 300); }, 200); bindArrowsOnce(); } async function loadData(force = false) { const now = Date.now(); if (inProgress || (!force && now - lastFetch < MIN_REFRESH_MS)) return; inProgress = true; lastFetch = now; try { const hiddenTrack = document.createElement('div'); hiddenTrack.style.display = 'none'; document.body.appendChild(hiddenTrack); let threads = findHotInDoc(document); if (!threads.length) { const html = await fetchHTML(BASE_FORUM_URL); const doc = new DOMParser().parseFromString(html, 'text/html'); threads = findHotInDoc(doc); } threads = uniqueThreads(threads).slice(0, MAX_CARDS); if (!threads.length) { hiddenTrack.remove(); inProgress = false; return; } const cards = await Promise.all(threads.map(enrichThread)); try { localStorage.setItem(CACHE_KEY, JSON.stringify({ time: Date.now(), cards })); } catch(e) {} renderCards(cards, true); hiddenTrack.remove(); } catch(err) { console.error('nfCarousel loadData error', err); } finally { inProgress = false; } } // Проверяем кэш при старте, делаем полностью скрыто (async () => { const hiddenTrack = document.createElement('div'); hiddenTrack.style.display = 'none'; document.body.appendChild(hiddenTrack); try { const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || '{}'); let cards = []; if (cached.cards && Date.now() - (cached.time || 0) < CACHE_TIME) { // подготавливаем скрытую версию карточек cards = cached.cards; for (const c of cards) { const a = document.createElement('a'); a.className = 'nf-card'; a.href = c.href; a.target = '_self'; a.style.flex = `0 0 ${CARD_WIDTH}px`; a.innerHTML = ` <div class="nf-card-img"><img src="${c.img}" alt="${escapeHtml(c.title)}"><div class="nf-card-badge">🔥 Горячая тема</div></div> <div class="nf-card-content"> <div class="nf-card-title">${escapeHtml(c.title)}</div> <div class="nf-card-desc">${escapeHtml(c.text || '')}</div> <div class="nf-card-meta" style="display:flex;align-items:center;gap:8px;"> <img src="${c.avatar}" style="width:48px;height:48px;border-radius:50%;"> <span class="author-nick">${escapeHtml(c.author)}</span> <span class="views-count">👁️ ${c.views}</span> </div> </div>`; hiddenTrack.appendChild(a); } } else { // подгружаем новые данные, тоже в скрытом контейнере await loadData(true); // loadData теперь может принимать hiddenTrack hiddenTrack.remove(); return; } // когда скрытые карточки готовы, вставляем в видимую карусель track.innerHTML = hiddenTrack.innerHTML; setSlideSizes(); updateCarouselPosition(); bindArrowsOnce(); } catch(e) { await loadData(true); } finally { hiddenTrack.remove(); } })(); // Авто-обновление каждые 10 минут setInterval(() => loadData(false), 10 * 60 * 1000); window.addEventListener('resize', () => { setSlideSizes(); updateCarouselPosition(); }, { passive: true }); })(); </script> </div>
Старый скрипт vs Новый скрипт. Битва продолжается. Победит сильнеший ..
Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.