Список пользователей

1
Админ
Постов: 101
2
Элита
Постов: 34
3
Элита
Постов: 28
4
VIP
Постов: 26
5
Дизайнер
Постов: 25
6
Пользователи
Постов: 25
7
Пользователи
Постов: 24
8
Проверенные
Постов: 21

  • Страница 1 из 1
  • 1
Модернизация Парсера - планшет Samsung для UCOZ V4
Дата: Вторник, 28.10.2025, 15:06 | Сообщение # 1 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:101
         Регистрация:20.10.2016

Скрытая проверка и рендер кэша

Старый скрипт:

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 Новый скрипт.

Битва продолжается.

Победит сильнеший ..

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: