您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Фильтр кейсов с индивидуальным выбором сочетаний параметров для каждого кейса, поиском, названиями и перетаскиваемой кнопкой
// ==UserScript== // @name TMS Case Per-Case Smart Filter // @namespace http://tampermonkey.net/ // @version 1.9.3 // @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'; 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 = ''; localStorage.setItem(storageKey, JSON.stringify({ left: btn.style.left, top: btn.style.top })); } }); // Возвращаем функцию, чтобы узнать был ли drag, и функцию сброса moved return { wasMoved: () => moved, resetMoved: () => { moved = false; } }; } // --- Сбор кейсов --- function getCases() { return Array.from(document.querySelectorAll('.run-case__item')).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; }); return {item, paramsText, paramsObj, name, title, link: link ? link.href : '', checkbox}; }).filter(Boolean); } // --- Уникальные параметры и значения для каждого кейса --- 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 = function() { overlay.remove(); style.remove(); onSave(caseCombinations); // Сохраняем при применении // Для каждого кейса на странице ищем его name, сравниваем параметры с сочетаниями для этого name cases.forEach(c => { const combs = caseCombinations[c.name] || []; // Если сочетаний нет — кейс остаётся if (!combs.length) { c.item.style.background = ''; if (c.checkbox.checked) c.checkbox.click(); return; } // Кейс подходит, если совпадает хотя бы с одним сочетанием const isMatch = combs.some(comb => Object.entries(comb).every(([k, v]) => !v || c.paramsObj[k] === v) ); if (!isMatch && !c.checkbox.checked) c.checkbox.click(); if (isMatch && c.checkbox.checked) c.checkbox.click(); if (!isMatch) c.item.style.background = '#ffe0e0'; else c.item.style.background = ''; }); }; modal.appendChild(applyBtn); overlay.appendChild(modal); document.body.appendChild(overlay); } // --- Основная логика --- function main() { const cases = getCases(); const caseParamValues = getCaseParamValues(cases); // Ключ для localStorage — уникальный для каждого рана 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 = {}; cases.forEach(c => { caseTitles[c.name] = c.title; }); // Добавляем кнопку для открытия фильтра if (!document.getElementById('tms-case-filter-btn')) { 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(e) { if (!dragState.wasMoved()) { showOverlay(cases, caseParamValues, caseCombinations, (newCombs) => { caseCombinations = newCombs; localStorage.setItem(runKey, JSON.stringify(caseCombinations)); }, caseTitles); } dragState.resetMoved(); }); document.body.appendChild(btn); } } // Ждём появления кейсов function waitForCases() { const interval = setInterval(() => { if (document.querySelectorAll('.run-case__item').length > 0) { clearInterval(interval); main(); } }, 500); setTimeout(() => clearInterval(interval), 10000); } waitForCases(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址