/
diz
/
search
Обзор
Документация
Войти
/
diz
/
search
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/script.js
480 строк
17 KB
Dima
ui: дизайн по design.md, текстовые кнопки, блокировка очистки при обновлении; fix: детект отмены диалога выбора папки, XSS-экранирование меток
10 авг 2026, 21:55
10 авг 2026, 21:55
caaf899
Код
Авторство
О чём код?
// ==================== КОНФИГУРАЦИЯ ==================== const CONFIG = { order: [ "Маркировщик", "Принтер", "SPI", "Установщик 1", "Установщик 2", "Установщик 3", "Установщик 4", "Установщик 5", "ПромАОИ", "Печь", "АОИ", "Трафареты", "Оснастки SMT", "Оснастки THT", "Gam 320", "Gam 330L", ], labels: { "mark.txt": "Маркировщик", "print.txt": "Принтер", "spi.txt": "SPI", "ust1.txt": "Установщик 1", "ust2.txt": "Установщик 2", "ust3.txt": "Установщик 3", "ust4.txt": "Установщик 4", "ust5.txt": "Установщик 5", "aoi1.txt": "ПромАОИ", "pech.txt": "Печь", "aoi2.txt": "АОИ", "traf.txt": "Трафареты", "osmt.txt": "Оснастки SMT", "otht.txt": "Оснастки THT", "g320.txt": "Gam 320", "g330.txt": "Gam 330L", }, folders: { Line1: "Линия 1", Line2: "Линия 2", Line3: "Линия 3", TrOs: "Трафареты, оснастки, Gam", }, }; // ==================== IndexedDB ==================== const DB_NAME = 'TextSearchDB'; const DB_VERSION = 1; const STORE_NAME = 'files'; let db = null; let fileList = []; let searchTimeout = null; let currentSearchId = 0; // Инициализация IndexedDB function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { const store = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true }); store.createIndex('name', 'name', { unique: false }); store.createIndex('category', 'cat', { unique: false }); } }; request.onsuccess = (event) => { db = event.target.result; console.log('IndexedDB подключена'); resolve(db); }; request.onerror = () => reject(request.error); }); } // Сохранение в IndexedDB function saveToIndexedDB(files) { return new Promise((resolve, reject) => { if (!db) { reject('База не инициализирована'); return; } const tx = db.transaction([STORE_NAME], 'readwrite'); const store = tx.objectStore(STORE_NAME); // Очищаем старое store.clear(); // Добавляем новое files.forEach((file, index) => { store.add({ name: file.name, cat: file.cat, lines: file.lines, savedAt: Date.now(), index: index }); }); tx.oncomplete = () => { console.log('Сохранено файлов:', files.length); resolve(); }; tx.onerror = () => reject(tx.error); }); } // Чтение из IndexedDB function loadFromIndexedDB() { return new Promise((resolve, reject) => { if (!db) { reject('База не инициализирована'); return; } const tx = db.transaction([STORE_NAME], 'readonly'); const store = tx.objectStore(STORE_NAME); const request = store.getAll(); request.onsuccess = () => { const items = request.result; // Сортируем по индексу для сохранения порядка items.sort((a, b) => (a.index || 0) - (b.index || 0)); const files = items.map(item => ({ name: item.name, cat: item.cat, lines: item.lines })); console.log('Загружено из IndexedDB:', files.length, 'файлов'); resolve(files); }; request.onerror = () => reject(request.error); }); } // Очистка IndexedDB function clearIndexedDB() { return new Promise((resolve, reject) => { if (!db) { reject('База не инициализирована'); return; } const tx = db.transaction([STORE_NAME], 'readwrite'); const store = tx.objectStore(STORE_NAME); const request = store.clear(); request.onsuccess = () => { console.log('База очищена'); resolve(); }; request.onerror = () => reject(request.error); }); } // Получение статистики function getDBStats() { return new Promise((resolve) => { if (!db) { resolve({ count: 0, size: 0 }); return; } const tx = db.transaction([STORE_NAME], 'readonly'); const store = tx.objectStore(STORE_NAME); const request = store.count(); request.onsuccess = () => { resolve({ count: request.result }); }; request.onerror = () => resolve({ count: 0 }); }); } // ==================== UI функции ==================== const grid = document.getElementById("grid"); Object.keys(CONFIG.folders).forEach((id) => { grid.innerHTML += `<div class="card"><div class="card-header">${CONFIG.folders[id]}</div><div id="res-${id}" class="results-area"></div></div>`; }); const updateStatus = (txt) => { document.getElementById("statusBar").innerText = txt; }; const updateDBInfo = async () => { const stats = await getDBStats(); document.getElementById("dbInfo").innerText = stats.count > 0 ? `В базе: ${stats.count} файл.(ов)` : ''; document.getElementById("clearBtn").disabled = stats.count === 0; }; // ==================== Инициализация ==================== window.onload = async () => { try { await initDB(); // Пытаемся загрузить базу из IndexedDB const savedFiles = await loadFromIndexedDB(); if (savedFiles.length > 0) { fileList = savedFiles; const sInput = document.getElementById("searchInput"); sInput.disabled = false; sInput.placeholder = "Поиск..."; updateStatus(`База загружена из IndexedDB. Файлов: ${fileList.length}`); } else { updateStatus("База не найдена. Нажмите 'Обновить базу' для выбора папки"); } } catch (e) { console.error("Ошибка инициализации:", e); updateStatus("Ошибка инициализации базы данных"); } updateDBInfo(); }; // ==================== Обновление базы ==================== async function updateDatabase() { const btn = document.getElementById("updateBtn"); btn.disabled = true; // Не даём очистить базу во время обновления (гонка clear/save) document.getElementById("clearBtn").disabled = true; btn.innerText = "Обновление…"; btn.title = "Выбор папки..."; const input = document.getElementById("folderPicker"); try { // Сбрасываем, чтобы change сработал даже при повторном выборе той же папки input.value = ''; // Ожидаем выбора папки пользователем const selectedFiles = await new Promise((resolve, reject) => { let settled = false; let timer = null; const settle = (fn, val) => { if (settled) return; settled = true; clearTimeout(timer); input.removeEventListener('change', onFileChange); window.removeEventListener('focus', onWindowFocus); fn(val); }; // Подстраховка: если браузер не даёт событие фокуса при закрытии диалога timer = setTimeout(() => { settle(reject, new Error('Выбор папки отменён (таймаут)')); }, 120000); const onFileChange = () => { if (input.files && input.files.length > 0) { settle(resolve, input.files); } else { settle(reject, new Error('Файлы не выбраны')); } }; // Диалог выбора папки закрыт (папка выбрана или нажата "Отмена"). // Важно: событие change может прийти ПОСЛЕ focus — ждём, пока браузер // доставит выбранные файлы, иначе возникает ложная "отмена" при выборе папки const onWindowFocus = () => { if (input.files && input.files.length > 0) { settle(resolve, input.files); return; } setTimeout(() => { if (settled) return; if (input.files && input.files.length > 0) { settle(resolve, input.files); } else { settle(reject, new Error('Выбор папки отменён')); } }, 3000); }; input.addEventListener('change', onFileChange, { once: true }); window.addEventListener('focus', onWindowFocus); input.click(); }); fileList = []; updateStatus("Сканирование файлов..."); btn.innerText = "Сканирование…"; btn.title = "Сканирование..."; for (const file of selectedFiles) { const relPath = file.webkitRelativePath; const parts = relPath.split(/[/\\]/); if (parts.length < 2) continue; const fileName = parts[parts.length - 1]; if (!fileName.toLowerCase().endsWith('.txt')) continue; // Ищем известную категорию (Line1/Line2/Line3/TrOs) в любой части пути let cat = parts.slice(0, -1).find(p => CONFIG.folders[p]); if (!cat) { cat = parts[0]; // fallback на первую часть } if (!CONFIG.folders[cat]) continue; // незнакомая папка — пропускаем const content = await readFile(file); const lines = content.split(/\r?\n/); fileList.push({ name: fileName, cat, lines }); } if (fileList.length === 0) { throw new Error('Не найдено .txt файлов в папках ' + Object.keys(CONFIG.folders).join(', ')); } updateStatus("Сохранение в базу данных..."); await saveToIndexedDB(fileList); const sInput = document.getElementById("searchInput"); sInput.disabled = false; sInput.placeholder = "Поиск..."; const msg = `Готово. Файлов: ${fileList.length}. База сохранена в IndexedDB.`; updateStatus(msg); updateDBInfo(); } catch (e) { console.error("Ошибка updateDatabase:", e); if (e && e.message && e.message.indexOf('отменён') !== -1) { updateStatus("Обновление отменено."); } else { updateStatus("Ошибка: " + e.message); } } finally { btn.disabled = false; btn.innerText = "Обновить базу"; btn.title = "Обновить базу"; updateDBInfo(); // восстановит состояние clearBtn по актуальной базе } } // ==================== Очистка базы ==================== async function clearDatabase() { if (!confirm('Вы уверены, что хотите очистить базу данных?\n\nЭто действие нельзя отменить.')) { return; } try { await clearIndexedDB(); fileList = []; document.getElementById("searchInput").disabled = true; document.getElementById("searchInput").value = ''; document.getElementById("searchInput").placeholder = "Нажмите 'Обновить базу' для выбора папки"; // Очищаем результаты Object.keys(CONFIG.folders).forEach((id) => { document.getElementById(`res-${id}`).innerText = ''; }); updateStatus("База очищена. Нажмите 'Обновить базу' для загрузки новой."); updateDBInfo(); } catch (e) { console.error("Ошибка clearDatabase:", e); updateStatus("Ошибка при очистке: " + e.message); } } // ==================== Поиск ==================== async function performSearch() { const searchId = ++currentSearchId; const query = document .getElementById("searchInput") .value.trim() .toLowerCase(); if (!fileList.length) return; if (!query) { Object.keys(CONFIG.folders).forEach( (id) => (document.getElementById(`res-${id}`).innerText = "Введите текст..."), ); updateStatus(`База загружена. Файлов: ${fileList.length}`); return; } updateStatus("Поиск..."); const results = {}; Object.keys(CONFIG.folders).forEach((id) => { results[id] = {}; }); for (const f of fileList) { if (searchId !== currentSearchId) return; try { const matches = f.lines.filter((l) => l.toLowerCase().includes(query), ); if (matches.length) { const label = CONFIG.labels[f.name.toLowerCase()] || f.name; (results[f.cat][label] = results[f.cat][label] || []).push(...matches); } } catch (e) { console.error("Ошибка поиска в файле:", f.name, e); } } if (searchId !== currentSearchId) return; render(results); updateStatus(`Поиск завершен. Найдено совпадений в ${Object.keys(results).filter(k => Object.keys(results[k]).length).length} категориях.`); } // ==================== Рендеринг ==================== const esc = (t) => t.replace( /[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[m], ); function render(res) { Object.keys(CONFIG.folders).forEach((id) => { const catRes = res[id], container = document.getElementById(`res-${id}`); const labels = [ ...CONFIG.order.filter((l) => catRes[l]), ...Object.keys(catRes) .filter((l) => !CONFIG.order.includes(l)) .sort(), ]; container.innerHTML = labels .map( (l) => `<span class="label-heading">${esc(l)}:</span>${catRes[l].map(line => `<div class="result-line" onclick="selectText(this)">${esc(line)}</div>`).join("")}`, ) .join("") || "Ничего не найдено."; }); } // ==================== Выделение текста ==================== function selectText(el) { const range = document.createRange(); range.selectNodeContents(el); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } // ==================== Чтение файла ==================== async function readFile(file) { const buf = await file.arrayBuffer(); try { return new TextDecoder("utf-8", { fatal: true }).decode( buf, ); } catch (e) { console.warn("UTF-8 декодирование не удалось, пробуем windows-1251:", file.name); return new TextDecoder("windows-1251").decode(buf); } } // ==================== Debounce ==================== const debounceSearch = () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(performSearch, 150); };