您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Фильтр кейсов с индивидуальным выбором сочетаний параметров для каждого кейса, поиском, названиями и перетаскиваемой кнопкой
// ==UserScript== // @name TMS Case Per-Case Smart Filter // @namespace http://tampermonkey.net/ // @version 1.9.7 // @description Фильтр кейсов с индивидуальным выбором сочетаний параметров для каждого кейса, поиском, названиями и перетаскиваемой кнопкой // @match https://ingr.firetms.ru/p/*/runs/* // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; // --- Drag & Drop для кнопки --- function makeDraggable(btn, storageKey = 'tms-case-filter-btn-pos') { let offsetX, offsetY, isDragging = false, moved = false; // Восстановить позицию const saved = localStorage.getItem(storageKey); if (saved) { const {left, top} = JSON.parse(saved); btn.style.left = left; btn.style.top = top; btn.style.right = ''; btn.style.bottom = ''; } else { btn.style.right = '24px'; btn.style.bottom = '24px'; } btn.style.position = 'fixed'; btn.style.userSelect = 'none'; btn.style.width = '180px'; btn.style.height = '40px'; btn.style.fontSize = '16px'; btn.style.background = '#1976d2'; btn.style.color = '#fff'; btn.style.border = 'none'; btn.style.borderRadius = '6px'; btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; btn.style.cursor = 'pointer'; btn.style.whiteSpace = 'nowrap'; btn.style.textAlign = 'center'; btn.style.lineHeight = '40px'; btn.style.padding = '0'; btn.style.resize = 'none'; btn.style.display = 'block'; btn.style.zIndex = '2147483647'; function clampToViewport() { const vw = window.innerWidth || document.documentElement.clientWidth || 1920; const vh = window.innerHeight || document.documentElement.clientHeight || 1080; const btnWidth = btn.offsetWidth || 180; const btnHeight = btn.offsetHeight || 40; // Клэмпим только если используются left/top if (btn.style.left) { let l = parseFloat(btn.style.left); if (isFinite(l)) { l = Math.max(0, Math.min(l, vw - btnWidth)); btn.style.left = l + 'px'; } } if (btn.style.top) { let t = parseFloat(btn.style.top); if (isFinite(t)) { t = Math.max(0, Math.min(t, vh - btnHeight)); btn.style.top = t + 'px'; } } // Если после клэмпа кнопка всё ещё практически вне видимой области, сбрасываем в правый нижний угол const rect = btn.getBoundingClientRect(); const isOffscreen = rect.right < 16 || rect.bottom < 16 || rect.left > vw - 16 || rect.top > vh - 16; if (isOffscreen) { btn.style.left = ''; btn.style.top = ''; btn.style.right = '24px'; btn.style.bottom = '24px'; } } // Клэмп сразу после применения стилей try { clampToViewport(); } catch (e) {} btn.addEventListener('mousedown', function(e) { if (e.button !== 0) return; // Только ЛКМ isDragging = true; moved = false; offsetX = e.clientX - btn.getBoundingClientRect().left; offsetY = e.clientY - btn.getBoundingClientRect().top; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', function(e) { if (!isDragging) return; moved = true; btn.style.left = (e.clientX - offsetX) + 'px'; btn.style.top = (e.clientY - offsetY) + 'px'; btn.style.right = ''; btn.style.bottom = ''; }); document.addEventListener('mouseup', function(e) { if (isDragging) { isDragging = false; document.body.style.userSelect = ''; try { clampToViewport(); } catch (e) {} localStorage.setItem(storageKey, JSON.stringify({ left: btn.style.left, top: btn.style.top })); } }); // Адаптация при ресайзе окна window.addEventListener('resize', function() { try { clampToViewport(); } catch (e) {} }); // Возвращаем функцию, чтобы узнать был ли drag, и функцию сброса moved return { wasMoved: () => moved, resetMoved: () => { moved = false; } }; } // --- Сбор кейсов --- function getCases() { const allItems = document.querySelectorAll('.run-case__item'); console.log('Всего кейсов найдено:', allItems.length); return Array.from(allItems).map((item, idx) => { const checkbox = item.querySelector('input[type="checkbox"].form-check-input.checkbox-title'); if (!checkbox) return null; const paramsDiv = item.querySelector('.run-case__params'); const paramsText = paramsDiv ? paramsDiv.textContent.trim().replace(/^Параметры:\s*/i, '') : ''; const link = item.querySelector('a[href]'); const name = link ? link.textContent.trim() : `Кейс #${idx+1}`; // Новый способ: ищем название в .run-case__title-text > .section-visible-tooltip-toggler:first-child > div let title = ''; const titleBlock = item.querySelector('.run-case__title-text .section-visible-tooltip-toggler'); if (titleBlock && titleBlock.getAttribute('data-tooltip-text')) { title = titleBlock.getAttribute('data-tooltip-text').trim(); } else if (titleBlock) { // fallback: текст внутри div const innerDiv = titleBlock.querySelector('div'); if (innerDiv) title = innerDiv.textContent.trim(); } // Если не нашли, title остаётся пустым! const paramsObj = {}; paramsText.split(';').forEach(pair => { const [k, v] = pair.split(':').map(s => s && s.trim()); if (k && v) paramsObj[k] = v; }); const caseId = getCaseId({name, link: link ? link.href : ''}); const cachedVariations = globalCaseParamCache.get(caseId)?.size || 0; console.log(`Кейс ${idx}:`, { visible: item.offsetParent !== null, hasParamsDiv: !!paramsDiv, paramsText: paramsDiv ? paramsDiv.textContent : 'НЕТ', paramsCount: Object.keys(paramsObj).length, caseId: caseId, cachedVariations: cachedVariations }); return {item, paramsText, paramsObj, name, title, link: link ? link.href : '', checkbox}; }).filter(Boolean); } // --- Поиск скроллируемого контейнера для виртуализованных списков --- function getScrollableContainer() { const firstItem = document.querySelector('.run-case__item'); let node = firstItem ? firstItem.parentElement : null; while (node) { try { const style = getComputedStyle(node); const overflowY = style ? style.overflowY : ''; const isScrollable = /(auto|scroll)/i.test(overflowY) && node.scrollHeight > node.clientHeight + 8; if (isScrollable) return node; } catch (e) {} node = node.parentElement; } // Фолбэк: основной документ return document.scrollingElement || document.documentElement; } // --- Поиск внутренних прокручиваемых контейнеров внутри конкретного кейса --- function getInnerScrollablesWithinCase(caseItem) { if (!caseItem) return []; const result = []; // Ищем все потенциально прокручиваемые элементы const potentialScrollables = caseItem.querySelectorAll('*'); for (const el of potentialScrollables) { try { // Проверяем computed styles const style = getComputedStyle(el); if (!style) continue; const overflowY = style.overflowY; const overflowX = style.overflowX; // Проверяем различные условия прокручиваемости let isScrollable = false; // 1. Явно заданный overflow-y: auto/scroll if (/(auto|scroll)/i.test(overflowY) && el.scrollHeight > el.clientHeight + 8) { isScrollable = true; } // 2. Проверяем элементы с классом .run-case__params (где могут быть параметры) if (el.classList.contains('run-case__params') && el.scrollHeight > el.clientHeight + 8) { isScrollable = true; } // 3. Проверяем элементы с data-attributes, указывающими на прокручиваемость if (el.hasAttribute('data-scrollable') || el.hasAttribute('data-virtualized')) { isScrollable = true; } // 4. Проверяем элементы с определенными классами, которые обычно прокручиваются const scrollableClasses = ['scrollable', 'scroll', 'virtual-list', 'virtualized', 'params-list']; if (scrollableClasses.some(cls => el.classList.contains(cls))) { isScrollable = true; } // 5. Проверяем элементы, которые имеют scrollHeight > clientHeight (даже без overflow) if (el.scrollHeight > el.clientHeight + 8 && el.scrollHeight > 0) { // Дополнительная проверка: элемент должен быть видимым и иметь размеры const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { isScrollable = true; } } if (isScrollable) { console.log('Найден прокручиваемый элемент:', { tagName: el.tagName, className: el.className, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight, overflowY: overflowY, overflowX: overflowX }); result.push(el); } } catch (e) { console.warn('Ошибка при проверке элемента на прокручиваемость:', e); } } return result; } // --- Построить объект кейса по DOM-элементу кейса --- function buildCaseFromItem(item, idx = 0) { if (!item) return null; const checkbox = item.querySelector('input[type="checkbox"].form-check-input.checkbox-title'); const paramsDiv = item.querySelector('.run-case__params'); const paramsText = paramsDiv ? paramsDiv.textContent.trim().replace(/^Параметры:\s*/i, '') : ''; const link = item.querySelector('a[href]'); const name = link ? link.textContent.trim() : `Кейс #${idx+1}`; let title = ''; const titleBlock = item.querySelector('.run-case__title-text .section-visible-tooltip-toggler'); if (titleBlock && titleBlock.getAttribute('data-tooltip-text')) { title = titleBlock.getAttribute('data-tooltip-text').trim(); } else if (titleBlock) { const innerDiv = titleBlock.querySelector('div'); if (innerDiv) title = innerDiv.textContent.trim(); } const paramsObj = {}; paramsText.split(';').forEach(pair => { const [k, v] = pair.split(':').map(s => s && s.trim()); if (k && v) paramsObj[k] = v; }); return { item, paramsText, paramsObj, name, title, link: link ? link.href : '', checkbox }; } // --- ID кейса (стабилен между разными состояниями params) --- function getCaseId(c) { return (c && (c.link || c.name)) || ''; } // --- Прокрутка элемента целиком (вниз) с шагами --- async function scrollElementFully(el, stepPx = 200, waitMs = 120, maxSteps = 200) { if (!el) return; try { el.scrollTop = 0; await new Promise(r => setTimeout(r, waitMs)); } catch (e) { console.warn('Не удалось установить scrollTop = 0:', e); } let steps = 0; let lastScrollTop = -1; let noChangeCount = 0; while (steps++ < maxSteps) { try { const top = el.scrollTop; const height = el.clientHeight || 0; const scrollHeight = el.scrollHeight || 0; // Проверяем, достигли ли мы конца const atBottom = top + height >= scrollHeight - 4; if (atBottom) { console.log('Достигнут конец прокрутки элемента:', { steps, finalTop: top, height, scrollHeight }); break; } // Проверяем, не застряли ли мы if (top === lastScrollTop) { noChangeCount++; if (noChangeCount > 3) { console.log('Прокрутка застряла, пробуем принудительно:', { steps, currentTop: top, noChangeCount }); // Пробуем принудительно прокрутить дальше el.scrollTop = top + Math.max(100, stepPx); await new Promise(r => setTimeout(r, waitMs * 2)); if (el.scrollTop === top) { console.log('Принудительная прокрутка не помогла, выходим'); break; } noChangeCount = 0; } } else { noChangeCount = 0; } lastScrollTop = top; // Прокручиваем на шаг const newTop = top + Math.max(60, stepPx); el.scrollTop = newTop; // Ждем загрузки контента await new Promise(r => setTimeout(r, waitMs)); // Дополнительная проверка: если scrollHeight изменился, значит загрузился новый контент const newScrollHeight = el.scrollHeight || 0; if (newScrollHeight > scrollHeight) { console.log('Обнаружен новый контент, ждем дольше:', { oldScrollHeight: scrollHeight, newScrollHeight: newScrollHeight }); await new Promise(r => setTimeout(r, waitMs * 2)); } } catch (e) { console.warn('Ошибка при прокрутке элемента:', e); break; } } console.log('Завершена прокрутка элемента:', { totalSteps: steps, finalScrollTop: el.scrollTop, finalScrollHeight: el.scrollHeight }); } // --- Сбор всех состояний конкретного кейса путём прокрутки его внутренних скроллов --- async function harvestCaseByInnerScroll(caseItem, processedKeys, collected) { const caseId = getCaseId(buildCaseFromItem(caseItem) || {}); console.log('Начинаем сбор параметров кейса через внутренние скроллы:', { caseName: caseItem.querySelector('a[href]')?.textContent?.trim() || 'Unknown', caseElement: caseItem, caseId: caseId }); // Стартовый снимок try { const snap = buildCaseFromItem(caseItem); if (snap) { const key = computeCaseKey(snap); if (!processedKeys.has(key)) { processedKeys.add(key); collected.set(key, snap); // Добавляем в глобальный кэш updateCaseParamCache(caseId, snap.paramsObj); console.log('Добавлен стартовый снимок кейса:', { key, params: snap.paramsObj, paramsText: snap.paramsText }); } } } catch (e) { console.warn('Ошибка при создании стартового снимка:', e); } const innerScrolls = getInnerScrollablesWithinCase(caseItem); console.log('Найдено внутренних прокручиваемых элементов:', innerScrolls.length); // Если есть прокручиваемые элементы, работаем с ними if (innerScrolls.length > 0) { for (let i = 0; i < innerScrolls.length; i++) { const sc = innerScrolls[i]; console.log(`Прокручиваем внутренний элемент ${i + 1}/${innerScrolls.length}:`, { element: sc, className: sc.className, scrollHeight: sc.scrollHeight, clientHeight: sc.clientHeight }); // Прокручиваем элемент полностью await scrollElementFully(sc, 240, 120, 120); // Делаем несколько снимков во время прокрутки для лучшего покрытия const scrollSteps = [0.25, 0.5, 0.75, 1.0]; for (const stepRatio of scrollSteps) { try { const targetScrollTop = Math.floor(sc.scrollHeight * stepRatio); sc.scrollTop = targetScrollTop; await new Promise(r => setTimeout(r, 150)); const snap = buildCaseFromItem(caseItem); if (snap) { const key = computeCaseKey(snap); if (!processedKeys.has(key)) { processedKeys.add(key); collected.set(key, snap); // Добавляем в глобальный кэш updateCaseParamCache(caseId, snap.paramsObj); console.log('Добавлен промежуточный снимок кейса:', { stepRatio, key, params: snap.paramsObj, paramsText: snap.paramsText }); } } } catch (e) { console.warn('Ошибка при создании промежуточного снимка:', e); } } // Финальный снимок после прокрутки try { const snap2 = buildCaseFromItem(caseItem); if (snap2) { const key2 = computeCaseKey(snap2); if (!processedKeys.has(key2)) { processedKeys.add(key2); collected.set(key2, snap2); // Добавляем в глобальный кэш updateCaseParamCache(caseId, snap2.paramsObj); console.log('Добавлен финальный снимок кейса после прокрутки:', { key: key2, params: snap2.paramsObj, paramsText: snap2.paramsText }); } } } catch (e) { console.warn('Ошибка при создании финального снимка:', e); } } } else { // Если прокручиваемых элементов нет, пробуем раскрывающиеся списки console.log('Прокручиваемых элементов не найдено, пробуем раскрывающиеся списки'); await handleExpandableCaseContent(caseItem, processedKeys, collected); // Если и это не помогло, пробуем TMS-специфичную обработку if (collected.size <= 1) { console.log('Раскрывающиеся списки не помогли, пробуем TMS-специфичную обработку'); await handleTMSSpecificCaseStructure(caseItem, processedKeys, collected); } } console.log('Завершен сбор параметров кейса через внутренние скроллы. Всего уникальных комбинаций:', collected.size); console.log('В глобальном кэше для кейса', caseId, ':', globalCaseParamCache.get(caseId)?.size || 0, 'вариантов'); } // --- Специальная обработка для кейсов с раскрывающимися списками параметров --- async function handleExpandableCaseContent(caseItem, processedKeys, collected) { console.log('Проверяем кейс на наличие раскрывающихся элементов:', { caseName: caseItem.querySelector('a[href]')?.textContent?.trim() || 'Unknown' }); // Ищем элементы, которые могут раскрываться const expandableSelectors = [ '.run-case__params[data-expanded]', '.run-case__params[aria-expanded]', '.run-case__params .expandable', '.run-case__params .collapsible', '.run-case__params .toggle-content', '.run-case__params[style*="max-height"]', '.run-case__params[style*="overflow: hidden"]' ]; let foundExpandable = false; for (const selector of expandableSelectors) { const elements = caseItem.querySelectorAll(selector); if (elements.length > 0) { foundExpandable = true; console.log('Найдены потенциально раскрывающиеся элементы:', { selector, count: elements.length }); for (const el of elements) { // Пробуем различные способы раскрытия await tryExpandElement(el, caseItem, processedKeys, collected); } } } // Если не нашли стандартные раскрывающиеся элементы, ищем по структуре if (!foundExpandable) { const paramsDiv = caseItem.querySelector('.run-case__params'); if (paramsDiv) { // Проверяем, есть ли скрытый контент const allParams = paramsDiv.querySelectorAll('*'); let hasHiddenContent = false; for (const param of allParams) { try { const style = getComputedStyle(param); if (style.display === 'none' || style.visibility === 'hidden' || style.maxHeight === '0px' || style.height === '0px') { hasHiddenContent = true; break; } } catch (e) {} } if (hasHiddenContent) { console.log('Обнаружен скрытый контент в параметрах, пробуем раскрыть'); await tryExpandElement(paramsDiv, caseItem, processedKeys, collected); } } } return foundExpandable; } // --- Попытка раскрыть элемент различными способами --- async function tryExpandElement(element, caseItem, processedKeys, collected) { console.log('Пробуем раскрыть элемент:', { element, className: element.className, attributes: Array.from(element.attributes).map(attr => `${attr.name}="${attr.value}"`) }); // Способ 1: клик по элементу try { element.click(); await new Promise(r => setTimeout(r, 200)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после клика'); } catch (e) { console.warn('Клик по элементу не удался:', e); } // Способ 2: поиск кнопок раскрытия const expandButtons = element.querySelectorAll('button, .btn, .expand, .toggle, [aria-expanded]'); for (const btn of expandButtons) { try { btn.click(); await new Promise(r => setTimeout(r, 200)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после клика по кнопке'); } catch (e) { console.warn('Клик по кнопке раскрытия не удался:', e); } } // Способ 3: программное изменение стилей try { const originalStyle = element.style.cssText; element.style.maxHeight = 'none'; element.style.overflow = 'visible'; element.style.height = 'auto'; await new Promise(r => setTimeout(r, 200)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после изменения стилей'); element.style.cssText = originalStyle; // восстанавливаем } catch (e) { console.warn('Изменение стилей не удалось:', e); } // Способ 4: поиск и активация событий try { element.dispatchEvent(new Event('click', { bubbles: true })); element.dispatchEvent(new Event('mouseenter', { bubbles: true })); element.dispatchEvent(new Event('focus', { bubbles: true })); await new Promise(r => setTimeout(r, 200)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после событий'); } catch (e) { console.warn('Отправка событий не удалась:', e); } } // --- Захват снимка кейса --- async function captureCaseSnapshot(caseItem, processedKeys, collected, context = '') { try { const snap = buildCaseFromItem(caseItem); if (snap) { const key = computeCaseKey(snap); if (!processedKeys.has(key)) { processedKeys.add(key); collected.set(key, snap); // Добавляем в глобальный кэш const caseId = getCaseId(snap); updateCaseParamCache(caseId, snap.paramsObj); console.log(`Добавлен снимок кейса ${context}:`, { key, params: snap.paramsObj, paramsText: snap.paramsText }); } } } catch (e) { console.warn(`Ошибка при создании снимка кейса ${context}:`, e); } } // --- Проверка матчинга кейса по всем его внутренним состояниям (прокручивая внутренние скроллы) --- async function anyMatchAcrossInnerScroll(caseItem, combinationsForCase) { console.log('Проверяем матчинг кейса по внутренним скроллам:', { caseName: caseItem.querySelector('a[href]')?.textContent?.trim() || 'Unknown', combinationsCount: combinationsForCase.length }); // Проверяем текущее состояние try { const base = buildCaseFromItem(caseItem); if (base) { const isMatch = combinationsForCase.some(comb => Object.entries(comb).every(([k, v]) => !v || base.paramsObj[k] === v) ); if (isMatch) { console.log('Найден матч в текущем состоянии кейса:', { params: base.paramsObj, matchingCombination: combinationsForCase.find(comb => Object.entries(comb).every(([k, v]) => !v || base.paramsObj[k] === v) ) }); return true; } } } catch (e) { console.warn('Ошибка при проверке текущего состояния кейса:', e); } const innerScrolls = getInnerScrollablesWithinCase(caseItem); console.log('Проверяем матчинг по внутренним скроллам:', innerScrolls.length); // Если есть прокручиваемые элементы, проверяем их if (innerScrolls.length > 0) { for (let i = 0; i < innerScrolls.length; i++) { const sc = innerScrolls[i]; console.log(`Проверяем матчинг в скролле ${i + 1}/${innerScrolls.length}`); // Прокручиваем элемент полностью await scrollElementFully(sc, 240, 120, 120); // Проверяем матчинг в нескольких позициях прокрутки const checkPositions = [0.25, 0.5, 0.75, 1.0]; for (const posRatio of checkPositions) { try { const targetScrollTop = Math.floor(sc.scrollHeight * posRatio); sc.scrollTop = targetScrollTop; await new Promise(r => setTimeout(r, 150)); const snap = buildCaseFromItem(caseItem); if (snap) { const isMatch = combinationsForCase.some(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ); if (isMatch) { console.log('Найден матч в промежуточной позиции прокрутки:', { position: posRatio, params: snap.paramsObj, matchingCombination: combinationsForCase.find(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ) }); return true; } } } catch (e) { console.warn('Ошибка при проверке матчинга в промежуточной позиции:', e); } } // Финальная проверка после полной прокрутки try { const snap = buildCaseFromItem(caseItem); if (snap) { const isMatch = combinationsForCase.some(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ); if (isMatch) { console.log('Найден матч после полной прокрутки скролла:', { params: snap.paramsObj, matchingCombination: combinationsForCase.find(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ) }); return true; } } } catch (e) { console.warn('Ошибка при финальной проверке матчинга:', e); } } } else { // Если прокручиваемых элементов нет, пробуем раскрывающиеся списки console.log('Прокручиваемых элементов не найдено, пробуем раскрывающиеся списки для матчинга'); const tempProcessedKeys = new Set(); const tempCollected = new Map(); await handleExpandableCaseContent(caseItem, tempProcessedKeys, tempCollected); // Проверяем все собранные состояния на матчинг for (const [key, snap] of tempCollected) { const isMatch = combinationsForCase.some(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ); if (isMatch) { console.log('Найден матч в раскрывающемся контенте:', { params: snap.paramsObj, matchingCombination: combinationsForCase.find(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ) }); return true; } } // Если и это не помогло, пробуем TMS-специфичную обработку if (tempCollected.size <= 1) { console.log('Раскрывающиеся списки не помогли, пробуем TMS-специфичную обработку для матчинга'); const tmsTempProcessedKeys = new Set(); const tmsTempCollected = new Map(); await handleTMSSpecificCaseStructure(caseItem, tmsTempProcessedKeys, tmsTempCollected); // Проверяем все собранные состояния на матчинг for (const [key, snap] of tmsTempCollected) { const isMatch = combinationsForCase.some(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ); if (isMatch) { console.log('Найден матч в TMS-специфичном контенте:', { params: snap.paramsObj, matchingCombination: combinationsForCase.find(comb => Object.entries(comb).every(([k, v]) => !v || snap.paramsObj[k] === v) ) }); return true; } } } } console.log('Матчинг не найден ни в одном состоянии кейса'); return false; } // --- Ключ для уникальной идентификации кейса на странице --- function computeCaseKey(c) { const linkPart = c.link || c.name || ''; return linkPart + '|' + (c.paramsText || ''); } // --- Полный сбор кейсов с автопрокруткой (для получения всех вариантов параметров) --- async function collectAllCasesAcrossVirtualizedList() { const scrollContainer = getScrollableContainer(); const originalTop = scrollContainer.scrollTop; const processedKeys = new Set(); const collected = new Map(); const processedCaseIds = new Set(); // Стартуем с начала списка scrollContainer.scrollTop = 0; await new Promise(r => setTimeout(r, 200)); let safetyCounter = 0; while (safetyCounter++ < 1000) { const visibleCases = getCases(); for (const c of visibleCases) { const key = computeCaseKey(c); if (!processedKeys.has(key)) { processedKeys.add(key); collected.set(key, c); } const caseId = getCaseId(c); if (!processedCaseIds.has(caseId)) { processedCaseIds.add(caseId); await harvestCaseByInnerScroll(c.item, processedKeys, collected); } } const el = scrollContainer; const top = el.scrollTop; const height = el.clientHeight || window.innerHeight; const scrollHeight = el.scrollHeight || document.body.scrollHeight; const atBottom = top + height >= scrollHeight - 4; if (atBottom) { await new Promise(r => setTimeout(r, 200)); const afterWaitCases = getCases(); const newFound = afterWaitCases.some(c => !processedKeys.has(computeCaseKey(c))); if (!newFound) break; } const step = Math.max(100, Math.floor(height * 0.85)); el.scrollTop = top + step; await new Promise(r => setTimeout(r, 200)); } // Вернём скролл пользователя try { scrollContainer.scrollTop = originalTop; } catch (e) {} return Array.from(collected.values()); } // --- Применение фильтра ко всем кейсам с автопрокруткой --- async function applyFilterAcrossVirtualizedList(caseCombinations) { const processedCaseIds = new Set(); const scrollContainer = getScrollableContainer(); function getViewportState() { const el = scrollContainer; const top = el.scrollTop; const height = el.clientHeight || window.innerHeight; const scrollHeight = el.scrollHeight || document.body.scrollHeight; return { el, top, height, scrollHeight }; } let safetyCounter = 0; while (safetyCounter++ < 1000) { const visibleCases = getCases(); for (const c of visibleCases) { const caseId = getCaseId(c); if (processedCaseIds.has(caseId)) continue; processedCaseIds.add(caseId); // Убеждаемся, что кейс полностью готов к взаимодействию if (c.item) { await waitForElementReadiness(c.item, 2000); } const combs = caseCombinations[c.name] || []; if (!combs.length) { c.item.style.background = ''; if (c.checkbox && c.checkbox.checked) await safeCheckboxClick(c.checkbox); continue; } // Сначала пробуем проверить по кэшу (быстрее) let isMatchSomewhere = checkCaseMatchUsingCache(caseId, combs); // Если в кэше нет данных или матчинг не найден, пробуем динамическую проверку if (!isMatchSomewhere) { console.log('Матчинг не найден в кэше, пробуем динамическую проверку для кейса:', caseId); // Если в кэше вообще нет данных для этого кейса, принудительно обновляем if (!globalCaseParamCache.has(caseId)) { console.log('В кэше нет данных для кейса, принудительно обновляем:', caseId); await forceRefreshCaseParams(c.item); // Проверяем снова по обновленному кэшу isMatchSomewhere = checkCaseMatchUsingCache(caseId, combs); } // Если все еще нет матчинга, пробуем динамическую проверку if (!isMatchSomewhere) { isMatchSomewhere = await anyMatchAcrossInnerScroll(c.item, combs); } } // Применяем фильтр if (!isMatchSomewhere && c.checkbox && !c.checkbox.checked) { console.log('Снимаем галочку с кейса (не подходит под фильтр):', caseId); await safeCheckboxClick(c.checkbox); } if (isMatchSomewhere && c.checkbox && c.checkbox.checked) { console.log('Ставим галочку на кейс (подходит под фильтр):', caseId); await safeCheckboxClick(c.checkbox); } console.log('Результат фильтрации для кейса:', { caseId, caseName: c.name, isMatch: isMatchSomewhere, checkboxChecked: c.checkbox?.checked, combinations: combs, cachedVariations: globalCaseParamCache.get(caseId)?.size || 0 }); c.item.style.background = !isMatchSomewhere ? '#ffe0e0' : ''; } const { el, top, height, scrollHeight } = getViewportState(); const atBottom = top + height >= scrollHeight - 4; if (atBottom) { // Небольшая пауза, чтобы догрузились элементы (если есть) await new Promise(r => setTimeout(r, 200)); // Если новых кейсов не появилось, выходим const afterWait = getCases(); const unseen = afterWait.some(c => !processedCaseIds.has(getCaseId(c))); if (!unseen) break; } const step = Math.max(100, Math.floor(height * 0.85)); el.scrollTop = top + step; await new Promise(r => setTimeout(r, 250)); // Дополнительное ожидание для загрузки контента await new Promise(r => setTimeout(r, 100)); // Проверяем, изменился ли scrollHeight (значит загрузился новый контент) const newScrollHeight = el.scrollHeight || 0; if (newScrollHeight > scrollHeight) { console.log('Обнаружен новый контент, ждем дольше:', { oldScrollHeight: scrollHeight, newScrollHeight: newScrollHeight }); await new Promise(r => setTimeout(r, 300)); } } // Вернуться в начало try { scrollContainer.scrollTop = 0; } catch (e) {} // Показываем финальную статистику console.log('=== ФИНАЛЬНАЯ СТАТИСТИКА ПРИМЕНЕНИЯ ФИЛЬТРА ==='); showCacheStats(); console.log('================================================'); } // --- Уникальные параметры и значения для каждого кейса --- function getCaseParamValues(cases) { const caseParams = {}; cases.forEach(c => { if (!caseParams[c.name]) caseParams[c.name] = {}; Object.entries(c.paramsObj).forEach(([k, v]) => { if (!caseParams[c.name][k]) caseParams[c.name][k] = new Set(); caseParams[c.name][k].add(v); }); }); // Преобразуем Set в массив Object.keys(caseParams).forEach(caseName => { Object.keys(caseParams[caseName]).forEach(k => { caseParams[caseName][k] = Array.from(caseParams[caseName][k]); }); }); return caseParams; } // --- UI: Overlay с индивидуальным выбором сочетаний для каждого кейса --- function showOverlay(cases, caseParamValues, caseCombinations, onSave, caseTitles) { // Стили const style = document.createElement('style'); style.textContent = ` #tms-case-filter-modal { background: #fff; padding: 24px; border-radius: 8px; min-width: 60vw; max-width: 60vw; max-height: 80vh; overflow: hidden; box-shadow: 0 2px 16px rgba(0,0,0,0.2); margin: 40px auto 0 auto; position: relative; display: flex; flex-direction: column; align-items: stretch; } #tms-case-filter-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 99999; display: flex; align-items: flex-start; justify-content: center; } #tms-case-filter-close { position: absolute; top: 8px; right: 12px; font-size: 32px; color: #888; cursor: pointer; font-weight: bold; background: none; border: none; line-height: 1; } #tms-case-filter-close:hover { color: #d33; } #tms-case-filter-cases-scroll { flex: 1 1 auto; overflow-y: auto; max-height: 60vh; margin-bottom: 16px; } .case-block { border: 1px solid #eee; border-radius: 6px; margin-bottom: 16px; padding: 10px; } .case-title { font-weight: bold; margin-bottom: 6px; } .comb-block { border: 1px solid #f0f0f0; border-radius: 6px; margin-bottom: 8px; padding: 8px; position: relative; } .comb-params-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 6px; position: relative; } .comb-param-select { flex: 0 1 auto; min-width: 180px; margin-bottom: 4px; } .select-default { background: #ffeaea !important; color: #b22222 !important; } .comb-remove-btn { font-size: 20px !important; font-weight: bold; padding: 0 6px; line-height: 1; background: none; border: none; color: #888; cursor: pointer; margin-left: auto; align-self: center; position: relative; z-index: 1; } .comb-remove-btn:hover { color: #d33; } .add-comb-btn { margin-bottom: 8px; } #tms-case-filter-apply { margin-top: 12px; } #tms-case-filter-search { width: 60%; font-size: 16px; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 16px; display: block; margin-left: auto; margin-right: auto; } `; document.head.appendChild(style); // Overlay const overlay = document.createElement('div'); overlay.id = 'tms-case-filter-overlay'; // Модалка const modal = document.createElement('div'); modal.id = 'tms-case-filter-modal'; // Крестик для закрытия const closeBtn = document.createElement('button'); closeBtn.id = 'tms-case-filter-close'; closeBtn.innerHTML = '×'; closeBtn.onclick = () => { overlay.remove(); style.remove(); onSave(caseCombinations); // Сохраняем при закрытии }; modal.appendChild(closeBtn); // Закрытие по клику вне модалки overlay.addEventListener('mousedown', function(e) { if (!modal.contains(e.target)) { overlay.remove(); style.remove(); onSave(caseCombinations); } }); // Поиск const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.id = 'tms-case-filter-search'; searchInput.placeholder = 'Поиск по коду или названию кейса...'; modal.appendChild(searchInput); // Контейнер для всех кейсов с прокруткой const allCasesDivScroll = document.createElement('div'); allCasesDivScroll.id = 'tms-case-filter-cases-scroll'; modal.appendChild(allCasesDivScroll); // Список уникальных кейсов const uniqueCases = Object.keys(caseParamValues); // Функция создания пустого сочетания function createEmptyCombination(caseName) { const comb = {}; Object.keys(caseParamValues[caseName]).forEach(param => { comb[param] = ''; }); return comb; } // Рендер блоков для каждого кейса function renderAllCases() { allCasesDivScroll.innerHTML = ''; const filter = searchInput.value.trim().toLowerCase(); uniqueCases.forEach(caseName => { const title = caseTitles && caseTitles[caseName] ? caseTitles[caseName] : ''; if ( !filter || caseName.toLowerCase().includes(filter) || title.toLowerCase().includes(filter) ) { const block = document.createElement('div'); block.className = 'case-block'; block.innerHTML = `<div class="case-title">${caseName}${(title && title !== caseName) ? ' — ' + title : ''}</div>`; const combsContainer = document.createElement('div'); // Рендер сочетаний (caseCombinations[caseName] || []).forEach((comb, idx) => { const combBlock = document.createElement('div'); combBlock.className = 'comb-block'; const paramRow = document.createElement('div'); paramRow.className = 'comb-params-row'; Object.keys(caseParamValues[caseName]).forEach(param => { const sel = document.createElement('select'); sel.className = 'comb-param-select'; sel.innerHTML = `<option value="">${param}</option>` + caseParamValues[caseName][param].map(v => `<option value="${v}">${v}</option>`).join(''); sel.value = comb[param] || ''; // Подсветка дефолта function updateSelectStyle() { if (sel.value === '') sel.classList.add('select-default'); else sel.classList.remove('select-default'); } sel.onchange = () => { comb[param] = sel.value; updateSelectStyle(); }; updateSelectStyle(); paramRow.appendChild(sel); }); // Крестик всегда справа const removeBtn = document.createElement('button'); removeBtn.className = 'comb-remove-btn'; removeBtn.innerHTML = '×'; removeBtn.title = 'Удалить сочетание'; removeBtn.onclick = () => { caseCombinations[caseName].splice(idx, 1); renderAllCases(); }; paramRow.appendChild(removeBtn); combBlock.appendChild(paramRow); combsContainer.appendChild(combBlock); }); if (Object.keys(caseParamValues[caseName]).length === 0) { // Нет параметров — показываем некликабельную кнопку const noParamsBtn = document.createElement('button'); noParamsBtn.className = 'add-comb-btn'; noParamsBtn.textContent = 'Нет параметров'; noParamsBtn.disabled = true; noParamsBtn.style.opacity = '0.6'; block.appendChild(combsContainer); block.appendChild(noParamsBtn); } else { // Обычная кнопка "Добавить сочетание" const addCombBtn = document.createElement('button'); addCombBtn.className = 'add-comb-btn'; addCombBtn.textContent = 'Добавить сочетание'; addCombBtn.onclick = function() { caseCombinations[caseName].push(createEmptyCombination(caseName)); renderAllCases(); }; block.appendChild(combsContainer); block.appendChild(addCombBtn); } allCasesDivScroll.appendChild(block); } }); } renderAllCases(); searchInput.addEventListener('input', renderAllCases); // Кнопка применить const applyBtn = document.createElement('button'); applyBtn.id = 'tms-case-filter-apply'; applyBtn.textContent = 'Применить'; applyBtn.onclick = async function() { overlay.remove(); style.remove(); onSave(caseCombinations); // Сохраняем при применении await applyFilterAcrossVirtualizedList(caseCombinations); }; modal.appendChild(applyBtn); overlay.appendChild(modal); document.body.appendChild(overlay); } // --- Подготовка и показ фильтра по требованию --- async function openFilter() { // Очищаем кэш параметров для свежего сбора clearCaseParamCache(); // Собираем ВСЕ кейсы, чтобы в селектах были все варианты параметров const allCases = await collectAllCasesAcrossVirtualizedList(); const caseParamValues = getCaseParamValues(allCases); const runKey = 'tms-case-filter-combs-' + location.pathname; let saved = localStorage.getItem(runKey); let caseCombinations = {}; if (saved) { try { caseCombinations = JSON.parse(saved); } catch (e) {} } Object.keys(caseParamValues).forEach(name => { if (!caseCombinations[name]) caseCombinations[name] = []; }); const caseTitles = {}; allCases.forEach(c => { caseTitles[c.name] = c.title; }); console.log('Собрано кейсов:', allCases.length); console.log('Размер глобального кэша параметров:', globalCaseParamCache.size); showCacheStats(); showOverlay(allCases, caseParamValues, caseCombinations, (newCombs) => { caseCombinations = newCombs; localStorage.setItem(runKey, JSON.stringify(caseCombinations)); }, caseTitles); } // --- Гарантированная вставка кнопки и самовосстановление --- function ensureFilterButton() { if (document.getElementById('tms-case-filter-btn')) return; const btn = document.createElement('button'); btn.id = 'tms-case-filter-btn'; btn.textContent = 'Фильтр кейсов'; const dragState = makeDraggable(btn, 'tms-case-filter-btn-pos-' + location.pathname); btn.addEventListener('click', function() { if (!dragState.wasMoved()) openFilter(); dragState.resetMoved(); }); document.body.appendChild(btn); } // --- Инициализация без ожидания кейсов + keep-alive --- ensureFilterButton(); if (!window.__tmsCaseFilterBtnInterval) { window.__tmsCaseFilterBtnInterval = setInterval(() => { try { ensureFilterButton(); } catch (e) {} }, 1000); } // --- Отслеживание смены маршрута (SPA) и пересоздание кнопки под новый run --- (function setupRouteChangeWatchers() { let lastPath = location.pathname; function onRouteChanged() { if (lastPath === location.pathname) return; lastPath = location.pathname; // Очищаем кэш параметров при смене страницы clearCaseParamCache(); // Пересоздаём кнопку, чтобы использовать корректный storageKey для позиции try { const old = document.getElementById('tms-case-filter-btn'); if (old) old.remove(); } catch (e) {} // Немного отложим, чтобы DOM успел стабилизироваться после навигации setTimeout(() => { try { ensureFilterButton(); } catch (e) {} }, 0); } try { const origPush = history.pushState; history.pushState = function() { const r = origPush.apply(this, arguments); window.dispatchEvent(new Event('tms-routechange')); return r; }; } catch (e) {} try { const origReplace = history.replaceState; history.replaceState = function() { const r = origReplace.apply(this, arguments); window.dispatchEvent(new Event('tms-routechange')); return r; }; } catch (e) {} window.addEventListener('popstate', () => window.dispatchEvent(new Event('tms-routechange'))); window.addEventListener('hashchange', () => window.dispatchEvent(new Event('tms-routechange'))); window.addEventListener('tms-routechange', onRouteChanged); })(); // --- Проверка и ожидание готовности чекбокса к клику --- async function ensureCheckboxReady(checkbox, maxWaitMs = 2000) { if (!checkbox) return false; const startTime = Date.now(); while (Date.now() - startTime < maxWaitMs) { try { // Проверяем, что чекбокс видим и доступен if (checkbox.offsetParent !== null && !checkbox.disabled && checkbox.style.display !== 'none' && checkbox.style.visibility !== 'hidden') { // Проверяем, что чекбокс находится в DOM if (document.contains(checkbox)) { // Пробуем получить computed styles const style = getComputedStyle(checkbox); if (style && style.pointerEvents !== 'none') { console.log('Чекбокс готов к клику:', { checked: checkbox.checked, disabled: checkbox.disabled, visible: checkbox.offsetParent !== null }); return true; } } } // Ждем немного и проверяем снова await new Promise(r => setTimeout(r, 100)); } catch (e) { console.warn('Ошибка при проверке готовности чекбокса:', e); await new Promise(r => setTimeout(r, 100)); } } console.warn('Чекбокс не готов к клику после ожидания:', { maxWaitMs, checkbox: checkbox }); return false; } // --- Безопасный клик по чекбоксу --- async function safeCheckboxClick(checkbox) { if (!checkbox) return false; try { // Убеждаемся, что чекбокс готов if (!(await ensureCheckboxReady(checkbox))) { return false; } // Сохраняем текущее состояние const wasChecked = checkbox.checked; // Пробуем кликнуть checkbox.click(); // Ждем изменения состояния await new Promise(r => setTimeout(r, 100)); // Проверяем, что состояние изменилось if (checkbox.checked !== wasChecked) { console.log('Чекбокс успешно изменен:', { wasChecked, nowChecked: checkbox.checked }); return true; } else { // Если состояние не изменилось, пробуем альтернативные способы console.log('Обычный клик не сработал, пробуем альтернативные способы'); // Способ 1: программное изменение checkbox.checked = !wasChecked; checkbox.dispatchEvent(new Event('change', { bubbles: true })); // Способ 2: если есть label, кликаем по нему const label = checkbox.closest('label') || document.querySelector(`label[for="${checkbox.id}"]`); if (label) { label.click(); } await new Promise(r => setTimeout(r, 100)); if (checkbox.checked !== wasChecked) { console.log('Чекбокс изменен альтернативным способом'); return true; } } return false; } catch (e) { console.error('Ошибка при клике по чекбоксу:', e); return false; } } // --- Специальная обработка для конкретной структуры TMS кейсов --- async function handleTMSSpecificCaseStructure(caseItem, processedKeys, collected) { console.log('Проверяем TMS-специфичную структуру кейса:', { caseName: caseItem.querySelector('a[href]')?.textContent?.trim() || 'Unknown' }); // Ищем специфичные элементы TMS const tmsSpecificSelectors = [ '.run-case__params .section-visible-tooltip-toggler', '.run-case__params .tooltip-content', '.run-case__params .expanded-content', '.run-case__params .collapsed-content', '.run-case__params[data-state]', '.run-case__params[data-expanded]' ]; let foundTMSStructure = false; for (const selector of tmsSpecificSelectors) { const elements = caseItem.querySelectorAll(selector); if (elements.length > 0) { foundTMSStructure = true; console.log('Найдены TMS-специфичные элементы:', { selector, count: elements.length }); for (const el of elements) { await tryExpandTMSElement(el, caseItem, processedKeys, collected); } } } // Если не нашли специфичные элементы, пробуем общие подходы if (!foundTMSStructure) { console.log('TMS-специфичные элементы не найдены, пробуем общие подходы'); // Пробуем найти все скрытые параметры const allParams = caseItem.querySelectorAll('.run-case__params *'); for (const param of allParams) { try { const style = getComputedStyle(param); const isHidden = style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || style.maxHeight === '0px' || style.height === '0px' || style.width === '0px'; if (isHidden) { console.log('Найден скрытый параметр, пробуем показать:', param); await tryShowHiddenElement(param, caseItem, processedKeys, collected); } } catch (e) { console.warn('Ошибка при проверке параметра:', e); } } } return foundTMSStructure; } // --- Попытка раскрыть TMS-специфичный элемент --- async function tryExpandTMSElement(element, caseItem, processedKeys, collected) { console.log('Пробуем раскрыть TMS-элемент:', { element, className: element.className, tagName: element.tagName }); // Способ 1: клик по элементу try { element.click(); await new Promise(r => setTimeout(r, 300)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после клика по TMS-элементу'); } catch (e) { console.warn('Клик по TMS-элементу не удался:', e); } // Способ 2: поиск и активация событий try { element.dispatchEvent(new Event('click', { bubbles: true })); element.dispatchEvent(new Event('mouseenter', { bubbles: true })); element.dispatchEvent(new Event('focus', { bubbles: true })); element.dispatchEvent(new Event('mousedown', { bubbles: true })); element.dispatchEvent(new Event('mouseup', { bubbles: true })); await new Promise(r => setTimeout(r, 300)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после событий TMS-элемента'); } catch (e) { console.warn('Отправка событий TMS-элемента не удалась:', e); } // Способ 3: изменение атрибутов try { if (element.hasAttribute('data-expanded')) { element.setAttribute('data-expanded', 'true'); } if (element.hasAttribute('aria-expanded')) { element.setAttribute('aria-expanded', 'true'); } if (element.hasAttribute('data-state')) { element.setAttribute('data-state', 'expanded'); } await new Promise(r => setTimeout(r, 200)); await captureCaseSnapshot(caseItem, processedKeys, collected, 'после изменения атрибутов TMS-элемента'); } catch (e) { console.warn('Изменение атрибутов TMS-элемента не удалось:', e); } } // --- Попытка показать скрытый элемент --- async function tryShowHiddenElement(element, caseItem, processedKeys, collected) { console.log('Пробуем показать скрытый элемент:', { element, className: element.className, tagName: element.tagName }); try { const originalStyle = element.style.cssText; // Пробуем различные комбинации стилей для показа const styleCombinations = [ { display: 'block', visibility: 'visible', opacity: '1', maxHeight: 'none', height: 'auto' }, { display: 'inline', visibility: 'visible', opacity: '1' }, { display: 'flex', visibility: 'visible', opacity: '1' }, { display: 'grid', visibility: 'visible', opacity: '1' } ]; for (const styleCombo of styleCombinations) { try { Object.assign(element.style, styleCombo); await new Promise(r => setTimeout(r, 200)); await captureCaseSnapshot(caseItem, processedKeys, collected, `после применения стилей ${JSON.stringify(styleCombo)}`); } catch (e) { console.warn('Применение стилей не удалось:', e); } } // Восстанавливаем оригинальные стили element.style.cssText = originalStyle; } catch (e) { console.warn('Изменение стилей скрытого элемента не удалось:', e); } } // --- Ожидание полной готовности элемента после прокрутки --- async function waitForElementReadiness(element, maxWaitMs = 3000) { if (!element) return false; const startTime = Date.now(); while (Date.now() - startTime < maxWaitMs) { try { // Проверяем, что элемент полностью готов if (element.offsetParent !== null && !element.disabled && element.style.display !== 'none' && element.style.visibility !== 'hidden' && element.style.opacity !== '0') { // Проверяем, что элемент находится в DOM и имеет размеры if (document.contains(element)) { const rect = element.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { // Проверяем computed styles const style = getComputedStyle(element); if (style && style.pointerEvents !== 'none') { console.log('Элемент полностью готов:', { element, rect: rect, pointerEvents: style.pointerEvents }); return true; } } } } // Ждем немного и проверяем снова await new Promise(r => setTimeout(r, 100)); } catch (e) { console.warn('Ошибка при проверке готовности элемента:', e); await new Promise(r => setTimeout(r, 100)); } } console.warn('Элемент не готов после ожидания:', { maxWaitMs, element: element }); return false; } // --- Глобальный кэш всех вариантов параметров для каждого кейса --- let globalCaseParamCache = new Map(); // caseId -> Set of parameter combinations // --- Обновление кэша параметров для конкретного кейса --- function updateCaseParamCache(caseId, paramsObj) { if (!globalCaseParamCache.has(caseId)) { globalCaseParamCache.set(caseId, new Set()); } const cache = globalCaseParamCache.get(caseId); const paramKey = JSON.stringify(paramsObj); cache.add(paramKey); } // --- Получение всех вариантов параметров для кейса из кэша --- function getCaseParamVariations(caseId) { const cache = globalCaseParamCache.get(caseId); if (!cache) return []; return Array.from(cache).map(paramKey => { try { return JSON.parse(paramKey); } catch (e) { console.warn('Ошибка при парсинге параметров из кэша:', e); return {}; } }); } // --- Очистка кэша при смене страницы --- function clearCaseParamCache() { globalCaseParamCache.clear(); console.log('Кэш параметров кейсов очищен'); } // --- Принудительное обновление кэша параметров для конкретного кейса --- async function forceRefreshCaseParams(caseItem) { const caseId = getCaseId(buildCaseFromItem(caseItem) || {}); if (!caseId) return; console.log('Принудительно обновляем кэш параметров для кейса:', caseId); // Очищаем существующий кэш для этого кейса globalCaseParamCache.delete(caseId); // Собираем параметры заново const tempProcessedKeys = new Set(); const tempCollected = new Map(); await harvestCaseByInnerScroll(caseItem, tempProcessedKeys, tempCollected); console.log('Кэш обновлен для кейса', caseId, ':', globalCaseParamCache.get(caseId)?.size || 0, 'вариантов'); } // --- Проверка матчинга кейса по всем его вариантам параметров из кэша --- function checkCaseMatchUsingCache(caseId, combinationsForCase) { if (!caseId || !combinationsForCase || combinationsForCase.length === 0) { return false; } const paramVariations = getCaseParamVariations(caseId); console.log('Проверяем матчинг кейса по кэшу:', { caseId, combinationsCount: combinationsForCase.length, cachedVariationsCount: paramVariations.length }); if (paramVariations.length === 0) { console.log('В кэше нет вариантов параметров для кейса:', caseId); return false; } // Проверяем каждую комбинацию фильтра против всех вариантов параметров из кэша for (const comb of combinationsForCase) { for (const params of paramVariations) { const isMatch = Object.entries(comb).every(([k, v]) => !v || params[k] === v); if (isMatch) { console.log('Найден матч в кэше:', { caseId, matchingCombination: comb, matchingParams: params }); return true; } } } console.log('Матчинг не найден в кэше для кейса:', caseId); return false; } // --- Отображение статистики кэша параметров --- function showCacheStats() { console.log('=== СТАТИСТИКА КЭША ПАРАМЕТРОВ ==='); console.log('Общее количество кейсов в кэше:', globalCaseParamCache.size); let totalVariations = 0; for (const [caseId, variations] of globalCaseParamCache) { totalVariations += variations.size; console.log(`Кейс ${caseId}: ${variations.size} вариантов параметров`); } console.log('Общее количество вариантов параметров:', totalVariations); console.log('====================================='); } })();