// Конфигурация API - OpenRouter DeepSeek const API_CONFIG = { OPENROUTER_URL: "https://openrouter.ai/api/v1/chat/completions", OPENROUTER_KEY: "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", MODEL: "deepseek/deepseek-r1:free" }; // Глобальные переменные let currentFile = null; let currentFileId = null; let accessToken = null; let jsonData = null; let isEditMode = false; let originalTableData = null; let selectedRows = new Set(); let selectedColumns = new Set(); let tokenCache = { value: null, expires: 0 }; let rawResponse = null; // Элементы DOM const elements = { uploadArea: document.getElementById('uploadArea'), pdfFileInput: document.getElementById('pdfFile'), selectFileBtn: document.getElementById('selectFileBtn'), fileInfo: document.getElementById('fileInfo'), fileName: document.getElementById('fileName'), fileSize: document.getElementById('fileSize'), removeFileBtn: document.getElementById('removeFileBtn'), validationAlert: document.getElementById('validationAlert'), apiKeyInput: document.getElementById('apiKey'), accessTokenInput: document.getElementById('accessToken'), getTokenBtn: document.getElementById('getTokenBtn'), expectedRowsInput: document.getElementById('expectedRows'), processBtn: document.getElementById('processBtn'), downloadBtn: document.getElementById('downloadBtn'), loadingOverlay: document.getElementById('loadingOverlay'), progressBar: document.getElementById('progressBar'), statusText: document.getElementById('statusText'), jsonOutput: document.getElementById('jsonOutput'), tableView: document.getElementById('tableView'), jsonView: document.getElementById('jsonView'), tableHeader: document.getElementById('tableHeader'), tableBody: document.getElementById('tableBody'), tableInfo: document.getElementById('tableInfo'), alertTitle: document.getElementById('alertTitle'), alertMessage: document.getElementById('alertMessage'), copyJsonBtn: document.getElementById('copyJsonBtn'), toggleViewBtn: document.getElementById('toggleViewBtn'), clearResultsBtn: document.getElementById('clearResultsBtn'), tokenModal: document.getElementById('tokenModal'), modalOverlay: document.getElementById('modalOverlay'), closeTokenModal: document.getElementById('closeTokenModal'), closeModalBtn: document.getElementById('closeModalBtn'), tokenStatusContent: document.getElementById('tokenStatusContent'), tokenResult: document.getElementById('tokenResult'), tokenError: document.getElementById('tokenError'), tokenOutput: document.getElementById('tokenOutput'), copyTokenBtn: document.getElementById('copyTokenBtn'), errorText: document.getElementById('errorText'), toggleApiKey: document.getElementById('toggleApiKey') }; function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupEventListeners); } else { setupEventListeners(); } loadSavedSettings(); // Инициализируем dropdown экспорта setTimeout(setupExportDropdown, 100); } function setupEventListeners() { // Загрузка файла elements.selectFileBtn.addEventListener('click', () => elements.pdfFileInput.click()); elements.pdfFileInput.addEventListener('change', handleFileSelect); elements.uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); elements.uploadArea.classList.add('dragover'); }); elements.uploadArea.addEventListener('dragleave', () => { elements.uploadArea.classList.remove('dragover'); }); elements.uploadArea.addEventListener('drop', (e) => { e.preventDefault(); elements.uploadArea.classList.remove('dragover'); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); if (document.getElementById('exportTableBtn')) { document.getElementById('exportTableBtn').addEventListener('click', function() { // Показываем dropdown экспорта const dropdown = document.getElementById('exportDropdown'); if (dropdown) { dropdown.classList.toggle('show'); } }); } elements.uploadArea.addEventListener('click', () => elements.pdfFileInput.click()); elements.removeFileBtn.addEventListener('click', removeFile); elements.getTokenBtn.addEventListener('click', getAccessToken); elements.processBtn.addEventListener('click', processPDF); elements.downloadBtn.addEventListener('click', downloadJSON); elements.copyJsonBtn.addEventListener('click', copyJSON); elements.toggleViewBtn.addEventListener('click', toggleView); elements.clearResultsBtn.addEventListener('click', clearResults); // Модальное окно токена elements.closeTokenModal.addEventListener('click', () => { elements.tokenModal.style.display = 'none'; }); elements.copyTokenBtn.addEventListener('click', copyToken); // Сохранение настроек elements.apiKeyInput.addEventListener('input', saveSettings); elements.accessTokenInput.addEventListener('input', saveSettings); elements.expectedRowsInput.addEventListener('input', saveSettings); // Кнопка редактирования if (document.getElementById('editTableBtn')) { document.getElementById('editTableBtn').addEventListener('click', toggleEditMode); } // Кнопка исправления ассимиляции if (document.getElementById('forceAssimilationBtn')) { document.getElementById('forceAssimilationBtn').addEventListener('click', function() { try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data) { alert('Нет табличных данных'); return; } const originalCount = data.table_data.length; data.table_data = forceFixAssimilationImproved(data.table_data); let changes = 0; data.table_data.forEach(row => { Object.keys(row).forEach(key => { if (key !== 'characteristic' && row[key] === '+') { const originalRow = jsonData ? JSON.parse(jsonData).table_data.find(r => r.characteristic === row.characteristic ) : null; if (originalRow && originalRow[key] !== '+') { changes++; } } }); }); elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } jsonData = JSON.stringify(data); if (changes > 0) { showNotification(`Исправлено ${changes} значений ассимиляции`, 'success'); } else { alert('Не найдено значений "?" для исправления ассимиляции.'); } } catch (error) { console.error('Ошибка при исправлении ассимиляции:', error); showNotification('Ошибка при исправлении ассимиляции', 'error'); } }); } // Экспорт таблицы if (document.getElementById('exportTableBtn')) { document.getElementById('exportTableBtn').addEventListener('click', exportTable); } // Показать сырой ответ if (document.getElementById('showRawBtn')) { document.getElementById('showRawBtn').addEventListener('click', showRawResponse); } // Закрытие модального окна при клике вне его window.addEventListener('click', (e) => { if (e.target === elements.tokenModal) { elements.tokenModal.style.display = 'none'; } }); } function handleFileSelect(e) { if (e.target.files.length > 0) { handleFile(e.target.files[0]); } } function handleFile(file) { if (file.type !== 'application/pdf') { alert('Пожалуйста, выберите файл в формате PDF'); return; } if (file.size > 50 * 1024 * 1024) { alert('Размер файла не должен превышать 50 МБ'); return; } currentFile = file; updateFileInfo(); checkProcessButton(); } function updateFileInfo() { elements.fileName.textContent = currentFile.name; elements.fileSize.textContent = formatFileSize(currentFile.size); elements.fileInfo.style.display = 'block'; } function checkRateLimits() { const lastRequestTime = localStorage.getItem('lastGigaChatRequest'); const now = Date.now(); if (lastRequestTime) { const timeSinceLastRequest = now - parseInt(lastRequestTime); // Если прошло меньше 1 секунды с последнего запроса if (timeSinceLastRequest < 1000) { showNotification('Слишком частые запросы. Подождите секунду.', 'warning'); return false; } } localStorage.setItem('lastGigaChatRequest', now.toString()); return true; } function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function removeFile() { currentFile = null; currentFileId = null; elements.pdfFileInput.value = ''; elements.fileInfo.style.display = 'none'; checkProcessButton(); } function checkProcessButton() { const hasFile = currentFile !== null; const hasApiKey = elements.apiKeyInput.value.trim() !== ''; const hasToken = elements.accessTokenInput.value.trim() !== ''; elements.processBtn.disabled = !hasFile || (!hasApiKey && !hasToken); } async function getAccessToken() { const apiKey = elements.apiKeyInput.value.trim(); if (!apiKey) { alert('Пожалуйста, введите API ключ'); return; } // Проверяем кеш const now = Date.now(); if (tokenCache.value && tokenCache.expires > now) { console.log('Используем кешированный токен'); accessToken = tokenCache.value; elements.accessTokenInput.value = accessToken; // Показываем уведомление showNotification('Используется кешированный токен', 'info'); checkProcessButton(); return; } // Показываем модальное окно if (elements.tokenModal) { elements.tokenModal.style.display = 'flex'; } if (elements.tokenStatusContent) { elements.tokenStatusContent.style.display = 'block'; } if (elements.tokenResult) { elements.tokenResult.style.display = 'none'; } if (elements.tokenError) { elements.tokenError.style.display = 'none'; } try { const response = await fetch('/api/oauth', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ api_key: apiKey }) }); console.log('Response status:', response.status); if (!response.ok) { const errorText = await response.text(); console.error('Error response:', errorText); // Проверяем специфичные ошибки if (response.status === 429) { throw new Error('Превышен лимит запросов. Подождите 1 минуту и попробуйте снова.'); } throw new Error(`HTTP error! status: ${response.status}, details: ${errorText}`); } const result = await response.json(); console.log('Token response:', result); if (result.access_token) { // Сохраняем токен в кеш (действителен 1 час = 3600000 мс) tokenCache.value = result.access_token; tokenCache.expires = Date.now() + 3500000; // 58 минут для запаса // Сохраняем токен accessToken = result.access_token; elements.accessTokenInput.value = result.access_token; saveSettings(); // Показываем результат if (elements.tokenStatusContent) { elements.tokenStatusContent.style.display = 'none'; } if (elements.tokenResult) { elements.tokenResult.style.display = 'block'; } if (elements.tokenOutput) { elements.tokenOutput.value = result.access_token; } // Обновляем статус токена if (elements.tokenStatus) { elements.tokenStatus.innerHTML = ' Получен'; elements.tokenStatus.className = 'status-value status-active'; } checkProcessButton(); } else { throw new Error('Токен не найден в ответе'); } } catch (error) { console.error('Ошибка при получении токена:', error); if (elements.tokenStatusContent) { elements.tokenStatusContent.style.display = 'none'; } if (elements.tokenError) { elements.tokenError.style.display = 'block'; } if (elements.errorText) { elements.errorText.textContent = `Ошибка: ${error.message}`; } // Показываем уведомление showNotification(error.message, 'error'); } } function copyToken() { navigator.clipboard.writeText(elements.tokenOutput.value) .then(() => { const originalText = elements.copyTokenBtn.innerHTML; elements.copyTokenBtn.innerHTML = ' Скопировано!'; setTimeout(() => { elements.copyTokenBtn.innerHTML = originalText; }, 2000); }) .catch(err => { console.error('Ошибка при копировании: ', err); }); } async function processPDF() { if (!currentFile) { alert('Пожалуйста, выберите PDF файл'); return; } // Проверяем лимиты запросов if (!checkRateLimits()) { return; } // Проверяем токен if (elements.accessTokenInput.value.trim()) { accessToken = elements.accessTokenInput.value.trim(); } else if (elements.apiKeyInput.value.trim()) { // Пытаемся получить токен из API ключа await getAccessToken(); if (!accessToken) { return; } } else { alert('Пожалуйста, введите API ключ или токен доступа'); return; } // Показываем индикатор загрузки showLoading(true); updateProgress(10, 'Проверка данных...'); try { // Загружаем файл updateProgress(30, 'Отправка файла на сервер...'); const fileId = await uploadPDF(accessToken, currentFile); currentFileId = fileId; // Запрашиваем данные из GigaChat updateProgress(60, 'Извлечение данных из таблицы...'); const jsonResult = await askGigaChat(accessToken, fileId); // Парсим и отображаем результат updateProgress(90, 'Обработка результатов...'); jsonData = jsonResult; // Отображаем JSON displayJSON(jsonResult); // Проверяем данные checkMissingData(jsonResult); // Включаем кнопки elements.downloadBtn.disabled = false; updateExportButtons(); updateProgress(100, 'Готово!'); // Переключаемся на JSON вид showView('json'); // Через секунду скрываем загрузку setTimeout(() => { showLoading(false); }, 1000); } catch (error) { console.error('Ошибка при обработке PDF:', error); showLoading(false); // Более информативное сообщение об ошибке if (error.message.includes('429') || error.message.includes('лимит')) { alert(`Ошибка: ${error.message}\n\nРекомендации:\n1. Подождите 1-2 минуты\n2. Проверьте ваш тарифный план GigaChat\n3. Попробуйте позже`); } else if (error.message.includes('401') || error.message.includes('токен')) { alert(`Ошибка: ${error.message}\n\nПолучите новый токен через кнопку "Получить токен"`); } else { alert(`Ошибка: ${error.message}`); } } } if (document.getElementById('clearCacheBtn')) { document.getElementById('clearCacheBtn').addEventListener('click', function() { tokenCache.value = null; tokenCache.expires = 0; localStorage.removeItem('lastGigaChatRequest'); showNotification('Кеш очищен', 'info'); }); } async function uploadPDF(token, file) { const formData = new FormData(); formData.append('file', file); formData.append('purpose', 'general'); console.log("DEBUG: Загрузка файла в GigaChat"); console.log("DEBUG: Имя файла:", file.name); console.log("DEBUG: Размер файла:", file.size); try { const response = await fetch('/api/files', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: formData }); console.log("DEBUG: Статус ответа загрузки:", response.status); if (!response.ok) { const errorText = await response.text(); console.error("DEBUG: Ошибка загрузки файла:", errorText); let errorMessage = `Ошибка загрузки файла: ${response.status}`; if (response.status === 429) { errorMessage = 'Превышен лимит запросов к GigaChat API. Подождите 1 минуту и попробуйте снова.'; // Предлагаем пользователю подождать showNotification('Превышен лимит запросов. Подождите 1 минуту.', 'warning'); // Можно автоматически попробовать через 60 секунд // setTimeout(() => { // showNotification('Можно попробовать снова', 'info'); // }, 60000); } else if (response.status === 401) { errorMessage = 'Токен устарел или недействителен. Получите новый токен.'; // Сбрасываем кеш токена tokenCache.value = null; tokenCache.expires = 0; } throw new Error(errorMessage); } const result = await response.json(); console.log("DEBUG: Файл успешно загружен. ID:", result.id); return result.id; } catch (error) { console.error('Ошибка при загрузке файла:', error); throw error; } } async function askGigaChat(token, fileId) { console.log("DEBUG: Запрос к GigaChat"); console.log("DEBUG: File ID:", fileId); const expectedRows = parseInt(elements.expectedRowsInput?.value) || 30; const expectedColumns = 24; // Как в Python коде const prompt = `ВНИМАТЕЛЬНО ПРОСМОТРИ таблицу из PDF и верни её строго в формате JSON. ВАЖНО ЧТОБЫ ТЫ ЕЕ ПРОСМОТРЕЛ, ПРОАНАЛИЗИРОВАЛ КАК КАРТИНКУ. СЧИТЫВАНИЕ PDF МОЖЕТ БЫТЬ НЕ КОРРЕКТНЫМ. ВАЖНО: В таблице должно быть ${expectedColumns} колонок (столбцов)! Проверь внимательно - если видишь меньше колонок, значит ты пропустил часть данных. СТРУКТУРА JSON: { "table_data": [ { "characteristic": "Название характеристики", "column_1": "значение", "column_2": "значение", ... "column_${expectedColumns}": "значение" } ] } КРИТИЧЕСКИ ВАЖНО: 1. Должно быть РОВНО ${expectedColumns} колонок. Если какая-то колонка пустая - оставь пустую строку "". 2. Имена колонок должны быть: column_1, column_2, ..., column_${expectedColumns} 3. Сохрани ВСЕ символы как есть: +, -, W, ND, числа, буквы. 4. Если колонок больше, чем ${expectedColumns} - включи все! 5. Верни ТОЛЬКО JSON, без пояснений. 6. Включи все строки таблицы, включая подзаголовки. Пример для строки с ${expectedColumns} колонками: { "characteristic": "Название характеристики", "column_1": "+", "column_2": "-", ... "column_${expectedColumns}": "значение" } Извлеки ВСЕ данные и убедись, что колонок ровно ${expectedColumns}!`; const payload = { "model": "GigaChat", "messages": [ { "role": "user", "content": prompt, "attachments": [fileId] } ], "temperature": 0.1, "max_tokens": 8000 // Увеличиваем для больших таблиц }; console.log("DEBUG: Отправляемый payload"); try { const response = await fetch('/api/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); console.log("DEBUG: Статус ответа:", response.status); if (!response.ok) { const errorText = await response.text(); console.error("DEBUG: Текст ошибки:", errorText); throw new Error(`Ошибка GigaChat API: ${response.status}, details: ${errorText}`); } const result = await response.json(); console.log("DEBUG: Получен ответ от GigaChat"); // Сохраняем сырой ответ для отладки rawResponse = result.choices?.[0]?.message?.content || '{"table_data": []}'; // В setupEventListeners() замените обработчики: if (document.getElementById('showRawBtn')) { document.getElementById('showRawBtn').addEventListener('click', function(e) { e.stopPropagation(); safeClick(showRawResponse); }); } // Очищаем JSON const cleanedContent = cleanJSON(rawResponse); console.log("DEBUG: Очищенный контент (первые 500 символов):", cleanedContent.substring(0, 500)); // Анализируем данные const analysis = analyzeExtractedData(cleanedContent, expectedColumns); console.log("Анализ данных:", analysis); // Проверяем, достаточно ли колонок if (analysis.columns_missing > 10) { console.warn(`ВНИМАНИЕ: Найдено только ${analysis.total_columns_found} из ${expectedColumns} колонок!`); // Можно показать уведомление пользователю showNotification(`Найдено только ${analysis.total_columns_found} из ${expectedColumns} колонок. Возможно, данные неполные.`, 'warning'); } return cleanedContent; } catch (error) { console.error('Ошибка при запросе к GigaChat:', error); throw error; } } function setupTableControls() { console.log('Настройка обработчиков для кнопок управления таблицей'); // Маппинг ID кнопок и их обработчиков const buttonHandlers = { 'showAllColumnsBtn': showAllColumns, 'showDataColumnsBtn': showDataColumns, 'toggleEmptyColumnsBtn': toggleEmptyColumns, 'deleteEmptyColumnsBtn': deleteEmptyColumnsSimple, // Убедитесь, что используется правильная функция 'showAllRowsBtn': showAllRows, 'showDataRowsBtn': showDataRows, 'toggleEmptyRowsBtn': toggleEmptyRows, 'deleteEmptyRowsBtn': deleteEmptyRows }; // Назначаем обработчики для каждой кнопки Object.entries(buttonHandlers).forEach(([id, handler]) => { const button = document.getElementById(id); if (button) { // Удаляем старые обработчики, если есть const newButton = button.cloneNode(true); button.parentNode.replaceChild(newButton, button); // Добавляем новый обработчик newButton.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); // Добавляем визуальную обратную связь this.classList.add('active'); setTimeout(() => this.classList.remove('active'), 200); // Вызываем обработчик try { handler(); } catch (error) { console.error(`Ошибка в обработчике ${id}:`, error); showNotification(`Ошибка: ${error.message}`, 'error'); } }); console.log(`Обработчик добавлен для кнопки: ${id}`); } else { console.warn(`Кнопка с ID "${id}" не найдена`); } }); } function displayJSON(jsonStr) { try { // Очищаем предыдущие ошибки const existingError = elements.jsonOutput.parentNode.querySelector('.error-message'); if (existingError) { existingError.remove(); } console.log('Начало отображения JSON'); console.log('Длина исходных данных:', jsonStr.length); // Сначала пробуем очистить JSON от возможных проблем let cleanedJsonStr = cleanJSON(jsonStr); console.log('Длина после очистки:', cleanedJsonStr.length); // Если очищенная строка слишком короткая, возможно проблема if (cleanedJsonStr.length < 50) { console.warn('Очищенный JSON слишком короткий, используем альтернативный метод'); cleanedJsonStr = extractJSONFromText(jsonStr); } // Парсим JSON let jsonObj; try { jsonObj = JSON.parse(cleanedJsonStr); console.log('JSON успешно спарсен'); } catch (parseError) { console.error('Ошибка при парсинге JSON после очистки:', parseError.message); // Пробуем альтернативные методы jsonObj = tryAlternativeParsing(jsonStr); } // Исправляем символы в таблице if (jsonObj.table_data && Array.isArray(jsonObj.table_data)) { console.log('Применяем исправления символов...'); jsonObj.table_data = fixTableSymbols(jsonObj.table_data); // Дополнительная проверка и исправление jsonObj.table_data = postProcessTableData(jsonObj.table_data); // Автоматическое исправление ассимиляции console.log('Автоматическое исправление ассимиляции...'); jsonObj.table_data = forceFixAssimilationImproved(jsonObj.table_data); } // Показываем очищенный JSON elements.jsonOutput.textContent = JSON.stringify(jsonObj, null, 2); // Синтаксическая подсветка highlightJSON(); // Отображаем таблицу, если есть данные console.log('JSON для таблицы:', jsonObj); if (jsonObj.table_data && Array.isArray(jsonObj.table_data) && jsonObj.table_data.length > 0) { console.log('Вызываем displayTable с данными:', jsonObj.table_data.length, 'строк'); displayTable(jsonObj.table_data); elements.toggleViewBtn.disabled = false; elements.toggleViewBtn.setAttribute('data-view', 'json'); elements.toggleViewBtn.innerHTML = ' Показать таблицу'; } else { console.warn('Нет данных table_data или массив пустой'); elements.tableInfo.textContent = 'Нет табличных данных для отображения'; elements.toggleViewBtn.disabled = true; } // Включаем кнопки elements.copyJsonBtn.disabled = false; elements.clearResultsBtn.disabled = false; elements.downloadBtn.disabled = false; // Сохраняем данные для скачивания jsonData = JSON.stringify(jsonObj); } catch (error) { console.error('Критическая ошибка при отображении JSON:', error); console.error('Исходный текст (первые 500 символов):', jsonStr.substring(0, 500)); // Показываем исходный текст elements.jsonOutput.textContent = jsonStr; // Показываем сообщение об ошибке showJSONError(error, jsonStr); // Отключаем кнопки, связанные с таблицей elements.toggleViewBtn.disabled = true; elements.copyJsonBtn.disabled = true; elements.downloadBtn.disabled = false; // все равно можно скачать сырой текст updateExportButtons(); // <-- Добавьте эту строку } } function analyzeExtractedData(jsonStr, expectedColumns = 24) { try { const data = JSON.parse(jsonStr); const tableData = data.table_data || []; if (!tableData.length) { return { error: "Таблица пуста", columns_found: 0 }; } // Собираем все уникальные колонки const allColumns = new Set(); tableData.forEach(row => { Object.keys(row).forEach(key => allColumns.add(key)); }); // Убираем characteristic из подсчета колонок с данными const dataColumns = Array.from(allColumns).filter(col => col !== "characteristic"); // Определяем числовые колонки const numericColumns = []; const nonNumericColumns = []; dataColumns.forEach(col => { // Проверяем, является ли колонка числовой (column_1, column_2, ...) if (col.startsWith("column_")) { try { const num = parseInt(col.replace("column_", "")); numericColumns.push({ col, num }); } catch { nonNumericColumns.push(col); } } // Или просто число "1", "2", ... else if (/^\d+$/.test(col)) { numericColumns.push({ col, num: parseInt(col) }); } else { nonNumericColumns.push(col); } }); // Сортируем числовые колонки numericColumns.sort((a, b) => a.num - b.num); const sortedColumns = ["characteristic", ...numericColumns.map(c => c.col), ...nonNumericColumns]; return { total_rows: tableData.length, total_columns_found: dataColumns.length, expected_columns: expectedColumns, columns_missing: Math.max(0, expectedColumns - dataColumns.length), sorted_columns: sortedColumns, numeric_columns_count: numericColumns.length, non_numeric_columns: nonNumericColumns, sample_columns: dataColumns.slice(0, 10) }; } catch (error) { console.error('Ошибка при анализе данных:', error); return { error: error.message, columns_found: 0 }; } } function extractJSONFromText(text) { console.log('Извлечение JSON из текста'); // Ищем JSON структуру const jsonPattern = /\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*\}/gs; const matches = text.match(jsonPattern); if (matches && matches.length > 0) { // Берем самый длинный match (скорее всего это наш JSON) const longestMatch = matches.reduce((a, b) => a.length > b.length ? a : b); console.log('Найден JSON длиной:', longestMatch.length); return longestMatch; } // Если не нашли, создаем минимальный JSON return '{"table_data": []}'; } function tryAlternativeParsing(jsonStr) { console.log('Пробуем альтернативные методы парсинга'); // Метод 1: Пробуем найти и исправить конкретные ошибки let fixed = jsonStr; // Исправляем распространенные ошибки const fixes = [ // Некорректные escape [/\\([^"\\\/bfnrtu])/g, ''], // Двойные обратные слеши [/\\\\/g, '\\'], // Незакрытые кавычки [/: ([^",\[\]\{\}\s][^,\]\}]*?)(?=\s*[,}\]])/g, ': "$1"'], // Ключи без кавычек [/(\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*):/g, '$1"$2"$3:'] ]; fixes.forEach(([pattern, replacement]) => { fixed = fixed.replace(pattern, replacement); }); try { return JSON.parse(fixed); } catch (e) { console.warn('Альтернативный метод 1 не сработал:', e.message); } // Метод 2: Пробуем извлечь данные построчно try { const lines = jsonStr.split('\n'); const tableData = []; let currentRow = null; for (const line of lines) { if (line.includes('"characteristic"')) { if (currentRow) { tableData.push(currentRow); } currentRow = {}; // Извлекаем characteristic const charMatch = line.match(/"characteristic"\s*:\s*"([^"]*)"/); if (charMatch) { currentRow.characteristic = charMatch[1]; } } else if (line.includes('"column_')) { // Извлекаем column данные const colMatch = line.match(/"column_(\d+)"\s*:\s*"([^"]*)"/); if (colMatch && currentRow) { currentRow[`column_${colMatch[1]}`] = colMatch[2]; } } } if (currentRow) { tableData.push(currentRow); } return { table_data: tableData }; } catch (e) { console.warn('Альтернативный метод 2 не сработал:', e.message); } // Метод 3: Возвращаем пустые данные return { table_data: [] }; } function showJSONError(error, jsonStr) { const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.style.cssText = ` background-color: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; padding: 16px; margin-bottom: 16px; display: flex; align-items: flex-start; gap: 12px; `; // Создаем более информативное сообщение let errorDetails = error.message; // Пытаемся найти позицию ошибки const positionMatch = error.message.match(/position (\d+)/); if (positionMatch) { const position = parseInt(positionMatch[1]); errorDetails += `\n\nКонтекст ошибки:\n`; errorDetails += jsonStr.substring(Math.max(0, position - 50), Math.min(jsonStr.length, position + 50)); } errorDiv.innerHTML = `

Ошибка при разборе JSON

Сообщение: ${error.message}

Длина данных: ${jsonStr.length} символов

Начало данных:
${jsonStr.substring(0, 200).replace(//g, '>')}
`; // Вставляем сообщение об ошибке перед JSON elements.jsonOutput.parentNode.insertBefore(errorDiv, elements.jsonOutput); } function copyRawJSON() { const jsonText = elements.jsonOutput.textContent; navigator.clipboard.writeText(jsonText) .then(() => { showNotification('Сырые данные скопированы в буфер', 'info'); }) .catch(err => { console.error('Ошибка при копировании:', err); }); } function cleanJSON(jsonStr) { try { console.log('Очистка JSON...'); console.log('Исходная длина:', jsonStr.length); console.log('Первые 500 символов:', jsonStr.substring(0, 500)); // 1. Удаляем лишние пробелы и переносы в начале/конце let cleaned = jsonStr.trim(); // 2. Удаляем маркдаун обрамление ```json ... ``` if (cleaned.includes('```json')) { const start = cleaned.indexOf('```json') + 7; const end = cleaned.lastIndexOf('```'); cleaned = cleaned.substring(start, end).trim(); } else if (cleaned.includes('```')) { const start = cleaned.indexOf('```') + 3; const end = cleaned.lastIndexOf('```'); cleaned = cleaned.substring(start, end).trim(); } // 3. Ищем JSON объект или массив const jsonMatch = cleaned.match(/(\{[\s\S]*\}|\[[\s\S]*\])/); if (jsonMatch) { cleaned = jsonMatch[0]; } // 4. Специальная обработка для исправления некорректных escape-последовательностей cleaned = cleaned // Исправляем двойные обратные слэши .replace(/\\\\/g, '\\') // Исправляем некорректные escape-последовательности .replace(/\\([^"\\\/bfnrtu])/g, '$1') // Заменяем некорректные кавычки .replace(/[``'']/g, '"') // Убираем управляющие символы кроме табуляции и переноса строки .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '') // Убираем BOM (Byte Order Mark) .replace(/^\uFEFF/, ''); // 5. Исправляем отсутствующие кавычки в ключах cleaned = cleaned.replace(/(\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*):/g, '$1"$2"$3:'); // 6. Убираем лишние запятые в конце объектов и массивов cleaned = cleaned .replace(/,\s*}/g, '}') .replace(/,\s*\]/g, ']'); // 7. Исправляем незакрытые строки cleaned = cleaned.replace(/:\s*([^"\[\]\{\}\d,\s][^,\]\}]*?)(?=\s*[,}\]])/g, ': "$1"'); // 8. Исправляем распространенные ошибки формата let fixed = ''; let inString = false; let escapeNext = false; for (let i = 0; i < cleaned.length; i++) { const char = cleaned[i]; const nextChar = cleaned[i + 1] || ''; if (escapeNext) { // Если предыдущий символ был \, добавляем текущий как есть fixed += char; escapeNext = false; } else if (char === '\\') { // Начинаем escape-последовательность if (nextChar === 'u') { // Unicode escape - проверяем формат \uXXXX if (cleaned.substring(i + 2, i + 6).match(/[0-9a-fA-F]{4}/)) { fixed += cleaned.substring(i, i + 6); i += 5; } else { // Некорректный Unicode escape - заменяем на пустую строку fixed += ''; i += 5; } } else if ('"\\/bfnrt'.includes(nextChar)) { // Корректный escape символ fixed += char; escapeNext = true; } else { // Некорректный escape - пропускаем \ fixed += ''; } } else if (char === '"') { inString = !inString; fixed += char; } else if (!inString && char === "'") { // Заменяем одинарные кавычки на двойные вне строк fixed += '"'; } else if (char === '\n' && inString) { // Убираем переносы строк внутри строк fixed += ' '; } else { fixed += char; } } cleaned = fixed; // 9. Проверяем сбалансированность скобок const stack = []; for (let i = 0; i < cleaned.length; i++) { const char = cleaned[i]; if (char === '{' || char === '[') { stack.push(char); } else if (char === '}') { if (stack.pop() !== '{') { console.warn('Несбалансированная }'); // Добавляем недостающую { cleaned = '{' + cleaned; } } else if (char === ']') { if (stack.pop() !== '[') { console.warn('Несбалансированная ]'); // Добавляем недостающую [ cleaned = '[' + cleaned; } } } // Добавляем недостающие закрывающие скобки while (stack.length > 0) { const open = stack.pop(); cleaned += open === '{' ? '}' : ']'; } console.log('Очищенная длина:', cleaned.length); console.log('Первые 500 символов очищенного:', cleaned.substring(0, 500)); // 10. Пробуем спарсить try { const parsed = JSON.parse(cleaned); console.log('JSON успешно спарсен'); return cleaned; } catch (parseError) { console.warn('Ошибка парсинга после очистки:', parseError.message); // Попробуем более агрессивную очистку return cleanJSONAggressive(jsonStr); } } catch (error) { console.warn('Не удалось очистить JSON, возвращаем исходный с обработкой:', error.message); return cleanJSONAggressive(jsonStr); } } function cleanJSONAggressive(jsonStr) { console.log('Применяем агрессивную очистку JSON'); try { // 1. Находим первый { и последний } const firstBrace = jsonStr.indexOf('{'); const lastBrace = jsonStr.lastIndexOf('}'); if (firstBrace === -1 || lastBrace === -1) { throw new Error('Не найдены фигурные скобки'); } let cleaned = jsonStr.substring(firstBrace, lastBrace + 1); // 2. Убираем все сложные escape-последовательности cleaned = cleaned .replace(/\\\\/g, '\\') .replace(/\\"/g, '"') .replace(/\\([^"\\\/bfnrtu])/g, '') .replace(/\\u[0-9a-fA-F]{4}/g, match => { try { return JSON.parse(`"${match}"`); } catch { return ''; } }); // 3. Заменяем все нестандартные кавычки cleaned = cleaned .replace(/[`'']/g, '"') .replace(/„|"|«|»/g, '"'); // 4. Убираем управляющие символы cleaned = cleaned.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); // 5. Исправляем ключи без кавычек (ограниченный набор) cleaned = cleaned.replace(/"table_data":/g, '"table_data":'); cleaned = cleaned.replace(/"characteristic":/g, '"characteristic":'); // Ищем и исправляем ключи column_X for (let i = 1; i <= 30; i++) { const pattern1 = new RegExp(`column_${i}(\\s*):`, 'g'); const pattern2 = new RegExp(`"column_${i}"(\\s*):`, 'g'); cleaned = cleaned.replace(pattern1, `"column_${i}"$1:`); // Убедимся, что кавычки правильные if (!pattern2.test(cleaned)) { // Добавляем кавычки, если их нет const missingPattern = new RegExp(`column_${i}(\\s*):`, 'g'); cleaned = cleaned.replace(missingPattern, `"column_${i}"$1:`); } } // 6. Убираем лишние запятые cleaned = cleaned .replace(/,\s*}/g, '}') .replace(/,\s*\]/g, ']'); // 7. Убираем лишние двоеточия cleaned = cleaned.replace(/::/g, ':'); // 8. Добавляем финальную проверку структуры // Проверяем, что это похоже на наш ожидаемый формат if (!cleaned.includes('"table_data"') || !cleaned.includes('"characteristic"')) { throw new Error('Не найден ожидаемый формат данных'); } console.log('Агрессивно очищенный JSON (первые 300 символов):', cleaned.substring(0, 300)); return cleaned; } catch (error) { console.error('Агрессивная очистка не удалась:', error.message); // Возвращаем минимальный валидный JSON return '{"table_data": []}'; } } function highlightJSON() { const text = elements.jsonOutput.textContent; let highlighted = text .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function(match) { let cls = 'json-value'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'json-key'; } else { cls = 'json-string'; } } else if (/true|false/.test(match)) { cls = 'json-boolean'; } else if (/null/.test(match)) { cls = 'json-null'; } else if (/^-?\d/.test(match)) { cls = 'json-number'; } return `${match}`; }); elements.jsonOutput.innerHTML = highlighted; } function displayTable(tableData) { if (!tableData || !Array.isArray(tableData)) { console.warn('Нет данных для отображения таблицы:', tableData); elements.tableInfo.textContent = 'Нет данных для отображения'; return; } // Очищаем таблицу elements.tableHeader.innerHTML = ''; elements.tableBody.innerHTML = ''; console.log('Всего строк в данных:', tableData.length); console.log('Первые 5 строк:', tableData.slice(0, 5)); // ОПРЕДЕЛЯЕМ КОЛОНКИ ИЗ ДАННЫХ const keysFromData = new Set(); // Собираем ВСЕ ключи из всех строк tableData.forEach(row => { if (row && typeof row === 'object') { Object.keys(row).forEach(key => { if (key !== 'characteristic') { keysFromData.add(key); } }); } }); // Сортируем колонки по номеру const sortedKeys = Array.from(keysFromData).sort((a, b) => { const numA = parseInt(a.replace('column_', '')) || 0; const numB = parseInt(b.replace('column_', '')) || 0; return numA - numB; }); // Добавляем обработчики для выбора строк и колонок setupTableSelection(); console.log('Найдено колонок:', sortedKeys.length); console.log('Колонки:', sortedKeys); // Если нет колонок, создаем заглушку if (sortedKeys.length === 0) { console.warn('Нет колонок для отображения в таблице'); elements.tableInfo.textContent = 'Нет колонок для отображения'; return; } // Создаем заголовки const headerRow = document.createElement('tr'); // Первый заголовок - характеристика const thChar = document.createElement('th'); thChar.textContent = 'Characteristic'; thChar.style.backgroundColor = '#2c3e50'; thChar.style.color = 'white'; thChar.style.position = 'sticky'; thChar.style.left = '0'; thChar.style.zIndex = '3'; thChar.style.minWidth = '250px'; // Широкая колонка для длинных названий headerRow.appendChild(thChar); // Создаем заголовки для всех колонок sortedKeys.forEach(key => { const th = document.createElement('th'); // Создаем короткое имя для колонки const colNum = key.replace('column_', ''); th.textContent = `Col ${colNum}`; th.title = key; // Полное имя в tooltip th.style.backgroundColor = '#2c3e50'; th.style.color = 'white'; th.style.zIndex = '2'; th.style.minWidth = '100px'; th.style.textAlign = 'center'; headerRow.appendChild(th); }); elements.tableHeader.appendChild(headerRow); setTimeout(() => { try { if (typeof addTableControls === 'function') { addTableControls(); } else { console.error('addTableControls не определена, пропускаем'); } if (typeof updateRowCounter === 'function') updateRowCounter(); if (typeof updateColumnCounter === 'function') updateColumnCounter(); } catch (error) { console.error('Ошибка при добавлении контролов таблицы:', error); } }, 100); // Заполняем таблицу данными - ПОКАЗЫВАЕМ ВСЕ СТРОКИ let rowCount = 0; let displayedRows = 0; tableData.forEach((row, index) => { // Проверяем, есть ли строка if (!row || typeof row !== 'object') { console.log(`Пропускаем строку ${index}: нет данных или не объект`, row); return; } const tr = document.createElement('tr'); rowCount++; // Чередуем цвета строк для лучшей читаемости if (index % 2 === 0) { tr.style.backgroundColor = '#f8f9fa'; } // Ячейка с характеристикой (закрепленная слева) const tdChar = document.createElement('td'); const characteristic = row.characteristic || `Row ${index + 1}`; tdChar.textContent = characteristic; tdChar.style.fontWeight = '600'; tdChar.style.position = 'sticky'; tdChar.style.left = '0'; tdChar.style.backgroundColor = index % 2 === 0 ? '#f8f9fa' : 'white'; tdChar.style.zIndex = '1'; tdChar.style.borderRight = '2px solid #e2e8f0'; tdChar.style.minWidth = '250px'; tdChar.style.maxWidth = '300px'; tdChar.style.whiteSpace = 'normal'; // Разрешаем перенос текста tdChar.style.wordBreak = 'break-word'; tr.appendChild(tdChar); // Ячейки с значениями колонок sortedKeys.forEach(key => { const td = document.createElement('td'); const value = row[key] !== undefined ? String(row[key]) : ''; // Применяем стили к ячейке applyCellStyles(td, value); tr.appendChild(td); }); elements.tableBody.appendChild(tr); displayedRows++; }); // Добавляем легенду addTableLegend(0); // Добавляем кнопки управления addTableControls(); // Добавляем информацию о данных console.log(`Отображено ${displayedRows} из ${tableData.length} строк`); // Если отображено не все строки, показываем предупреждение if (displayedRows < tableData.length) { console.warn(`Пропущено ${tableData.length - displayedRows} строк!`); showNotification(`Показано ${displayedRows} из ${tableData.length} строк. Некоторые строки пропущены из-за некорректных данных.`, 'warning'); } } function setupTableSelection() { // Выбор строк (по клику на характеристику) const firstCells = document.querySelectorAll('#tableBody td:first-child'); firstCells.forEach(cell => { cell.addEventListener('click', function(e) { e.stopPropagation(); const row = this.parentElement; row.classList.toggle('selected'); }); }); // Выбор колонок (по клику на заголовок) const headerCells = document.querySelectorAll('#tableHeader th'); headerCells.forEach(th => { th.addEventListener('click', function(e) { e.stopPropagation(); if (this.textContent !== 'Characteristic') { this.classList.toggle('selected'); } }); }); } function addTableControls() { console.log('Добавление контролов управления таблицей'); const oldControls = document.getElementById('tableControls'); if (oldControls) oldControls.remove(); const controls = document.createElement('div'); controls.id = 'tableControls'; controls.style.cssText = ` padding: 12px; background: #f8fafc; border-bottom: 1px solid #e2e8f0; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; position: sticky; top: 0; z-index: 20; `; controls.innerHTML = `
Колонки:
Строки:
Строк: ... Колонок: ...
`; const tableWrapper = document.querySelector('.table-wrapper'); if (tableWrapper) { tableWrapper.parentNode.insertBefore(controls, tableWrapper); setTimeout(() => { try { setupTableControls(); updateRowCounter(); updateColumnCounter(); } catch (error) { console.error('Ошибка при настройке контролов таблицы:', error); } }, 100); } } function setupFallbackHandlers() { console.log('Использование фолбэк обработчиков'); const handlers = { 'showAllColumns': showAllColumns, 'showDataColumns': showDataColumns, 'toggleEmptyColumns': toggleEmptyColumns, 'showAllRows': showAllRows, 'showDataRows': showDataRows, 'toggleEmptyRows': toggleEmptyRows, 'deleteEmptyRows': deleteEmptyRows }; // Простой подход через onclick Object.entries(handlers).forEach(([name, handler]) => { const button = document.querySelector(`[onclick*="${name}"]`); if (button && !button.hasAttribute('data-handler-set')) { button.setAttribute('data-handler-set', 'true'); button.onclick = function(e) { e.preventDefault(); e.stopPropagation(); safeClick(handler); }; } }); } function updateColumnCounter() { const counter = document.getElementById('visibleColumnsCount'); if (counter) { const totalColumns = document.querySelectorAll('#tableHeader th').length - 1; // -1 для characteristic const visibleCount = countVisibleColumns(); counter.textContent = `${visibleCount}/${totalColumns}`; } } let isProcessingClick = false; const CLICK_DELAY = 300; function safeClick(callback, delay = CLICK_DELAY) { if (isProcessingClick) { console.log('Клик проигнорирован - обработка предыдущего клика еще идет'); return; } isProcessingClick = true; try { callback(); } catch (error) { console.error('Ошибка при обработке клика:', error); isProcessingClick = false; throw error; } setTimeout(() => { isProcessingClick = false; }, delay); } function showAllColumns() { const table = document.getElementById('dataTable'); if (!table) return; const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); headers.forEach(header => header.style.display = ''); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach(cell => cell.style.display = ''); }); showNotification('Показаны все колонки', 'info'); updateColumnCounter(); } function showDataColumns() { const table = document.getElementById('dataTable'); if (!table) return; const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет данных в таблице', 'warning'); return; } const columnHasData = new Array(headers.length).fill(false); columnHasData[0] = true; rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, cellIndex) => { const value = cell.textContent.trim(); if (value && value !== '—' && value !== '') { columnHasData[cellIndex] = true; } }); }); headers.forEach((header, index) => { header.style.display = columnHasData[index] ? '' : 'none'; }); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, index) => { cell.style.display = columnHasData[index] ? '' : 'none'; }); }); const visibleCount = columnHasData.filter(Boolean).length - 1; showNotification(`Показано ${visibleCount} колонок с данными`, 'info'); updateColumnCounter(); } function toggleEmptyColumns() { const table = document.getElementById('dataTable'); if (!table) return; const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет данных в таблице', 'warning'); return; } // Определяем, какие колонки полностью пустые const columnIsEmpty = new Array(headers.length).fill(true); columnIsEmpty[0] = false; // Первая колонка никогда не пустая rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, cellIndex) => { const value = cell.textContent.trim(); if (value && value !== '—' && value !== '') { columnIsEmpty[cellIndex] = false; } }); }); // Проверяем текущее состояние - скрыты ли пустые колонки const firstEmptyHeader = headers[1]; // Проверяем вторую колонку const isEmptyHidden = firstEmptyHeader && columnIsEmpty[1] && firstEmptyHeader.style.display === 'none'; // Переключаем состояние headers.forEach((header, index) => { if (index === 0) return; // characteristic не трогаем if (columnIsEmpty[index]) { if (isEmptyHidden) { // Если скрыты - показываем header.style.display = ''; } else { // Если показаны - скрываем header.style.display = 'none'; } } }); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, index) => { if (index === 0) return; // characteristic не трогаем if (columnIsEmpty[index]) { if (isEmptyHidden) { cell.style.display = ''; } else { cell.style.display = 'none'; } } }); }); const emptyCount = columnIsEmpty.filter((empty, idx) => empty && idx > 0).length; const action = isEmptyHidden ? 'показаны' : 'скрыты'; console.log(`${emptyCount} пустых колонок ${action}`); showNotification(`${emptyCount} пустых колонок ${action}`, 'info'); updateColumnCounter(); } function showAllRows() { const rows = document.querySelectorAll('#tableBody tr'); rows.forEach(row => row.style.display = ''); showNotification('Показаны все строки', 'info'); updateRowCounter(); } function showDataRows() { const rows = document.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет строк в таблице', 'warning'); return; } rows.forEach(row => { const cells = row.querySelectorAll('td'); let hasData = false; for (let i = 1; i < cells.length; i++) { const value = cells[i].textContent.trim(); if (value && value !== '—' && value !== '') { hasData = true; break; } } row.style.display = hasData ? '' : 'none'; }); const visibleCount = Array.from(rows).filter(row => row.style.display !== 'none').length; showNotification(`Показано ${visibleCount} строк с данными`, 'info'); updateRowCounter(); } function toggleEmptyRows() { const rows = document.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет строк в таблице', 'warning'); return; } const emptyRows = []; rows.forEach(row => { const cells = row.querySelectorAll('td'); let isEmpty = true; for (let i = 1; i < cells.length; i++) { const value = cells[i].textContent.trim(); if (value && value !== '—' && value !== '') { isEmpty = false; break; } } if (isEmpty) emptyRows.push(row); }); if (emptyRows.length === 0) { showNotification('Нет пустых строк', 'info'); return; } const firstEmptyRow = emptyRows[0]; const isEmptyHidden = firstEmptyRow && firstEmptyRow.style.display === 'none'; emptyRows.forEach(row => { row.style.display = isEmptyHidden ? '' : 'none'; }); const action = isEmptyHidden ? 'показаны' : 'скрыты'; showNotification(`${emptyRows.length} пустых строк ${action}`, 'info'); updateRowCounter(); } function deleteEmptyRows() { const rows = document.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет строк в таблице', 'warning'); return; } const emptyRowIndices = []; rows.forEach((row, index) => { const cells = row.querySelectorAll('td'); let isEmpty = true; for (let i = 1; i < cells.length; i++) { const value = cells[i].textContent.trim(); if (value && value !== '—' && value !== '') { isEmpty = false; break; } } if (isEmpty) emptyRowIndices.push(index); }); if (emptyRowIndices.length === 0) { showNotification('Нет пустых строк для удаления', 'info'); return; } if (!confirm(`Удалить ${emptyRowIndices.length} пустых строк? Это действие нельзя отменить!`)) { return; } try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { throw new Error('Нет табличных данных'); } emptyRowIndices.sort((a, b) => b - a).forEach(index => { data.table_data.splice(index, 1); }); elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } jsonData = JSON.stringify(data); showNotification(`Удалено ${emptyRowIndices.length} пустых строк`, 'success'); } catch (error) { console.error('Ошибка при удалении пустых строк:', error); showNotification('Ошибка при удалении пустых строк', 'error'); } } function countVisibleRows() { const rows = document.querySelectorAll('#tableBody tr'); let visibleCount = 0; rows.forEach(row => { if (row.style.display !== 'none') { visibleCount++; } }); return visibleCount; } function updateRowCounter() { const counter = document.getElementById('visibleRowsCount'); if (counter) { const totalRows = document.querySelectorAll('#tableBody tr').length; const visibleCount = countVisibleRows(); counter.textContent = `${visibleCount}/${totalRows}`; } } function countVisibleColumns() { const headers = document.querySelectorAll('#tableHeader th'); let visibleCount = 0; headers.forEach(header => { if (header.style.display !== 'none') { visibleCount++; } }); return visibleCount - 1; // Минус characteristic } function updateColumnCounter() { const counter = document.getElementById('visibleColumnsCount'); if (counter) { const totalColumns = document.querySelectorAll('#tableHeader th').length - 1; // -1 для characteristic const visibleCount = countVisibleColumns(); counter.textContent = `${visibleCount}/${totalColumns}`; } } function setupFallbackHandlers() { console.log('Использование фолбэк обработчиков'); const handlers = { 'showAllColumns': showAllColumns, 'showDataColumns': showDataColumns, 'toggleEmptyColumns': toggleEmptyColumns, 'showAllRows': showAllRows, 'showDataRows': showDataRows, 'toggleEmptyRows': toggleEmptyRows, 'deleteEmptyRows': deleteEmptyRows }; // Простой подход через onclick Object.entries(handlers).forEach(([name, handler]) => { const button = document.querySelector(`[onclick*="${name}"]`); if (button && !button.hasAttribute('data-handler-set')) { button.setAttribute('data-handler-set', 'true'); button.onclick = function(e) { e.preventDefault(); e.stopPropagation(); try { handler(); } catch (error) { console.error(`Ошибка в обработчике ${name}:`, error); showNotification(`Ошибка: ${error.message}`, 'error'); } }; } }); } function deleteEmptyRows() { const rows = document.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет строк в таблице', 'warning'); return; } // Находим пустые строки const emptyRowIndices = []; rows.forEach((row, index) => { const cells = row.querySelectorAll('td'); let isEmpty = true; // Проверяем ячейки (начиная со второй) for (let i = 1; i < cells.length; i++) { const value = cells[i].textContent.trim(); if (value && value !== '—' && value !== '') { isEmpty = false; break; } } if (isEmpty) { emptyRowIndices.push(index); } }); if (emptyRowIndices.length === 0) { showNotification('Нет пустых строк для удаления', 'info'); return; } // Подтверждение удаления if (!confirm(`Удалить ${emptyRowIndices.length} пустых строк? Это действие нельзя отменить!`)) { return; } try { // Получаем текущие данные const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { throw new Error('Нет табличных данных'); } // Удаляем строки из данных (в обратном порядке) emptyRowIndices.sort((a, b) => b - a).forEach(index => { data.table_data.splice(index, 1); }); // Обновляем JSON elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); // Обновляем таблицу if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } // Обновляем глобальные данные jsonData = JSON.stringify(data); console.log(`Удалено ${emptyRowIndices.length} пустых строк`); showNotification(`Удалено ${emptyRowIndices.length} пустых строк`, 'success'); } catch (error) { console.error('Ошибка при удалении пустых строк:', error); showNotification('Ошибка при удалении пустых строк', 'error'); } } function countVisibleRows() { const rows = document.querySelectorAll('#tableBody tr'); let visibleCount = 0; rows.forEach(row => { if (row.style.display !== 'none') { visibleCount++; } }); return visibleCount; } function updateRowCounter() { const counter = document.getElementById('visibleRowsCount'); if (counter) { const totalRows = document.querySelectorAll('#tableBody tr').length; const visibleCount = document.querySelectorAll('#tableBody tr').length - document.querySelectorAll('#tableBody tr[style*="display: none"]').length; counter.textContent = `${visibleCount}/${totalRows}`; } } function updateColumnCounter() { const counter = document.getElementById('visibleColumnsCount'); if (counter) { const totalColumns = document.querySelectorAll('#tableHeader th').length - 1; const visibleCount = document.querySelectorAll('#tableHeader th').length - 1 - document.querySelectorAll('#tableHeader th[style*="display: none"]').length; counter.textContent = `${visibleCount}/${totalColumns}`; } } function countVisibleColumns() { const headers = document.querySelectorAll('#tableHeader th'); let visibleCount = 0; headers.forEach(header => { if (header.style.display !== 'none') { visibleCount++; } }); return visibleCount - 1; // Минус characteristic } function updateColumnCounter() { const counter = document.getElementById('visibleColumnsCount'); if (counter) { const totalColumns = document.querySelectorAll('#tableHeader th').length - 1; // -1 для characteristic const visibleCount = countVisibleColumns(); counter.textContent = `${visibleCount}/${totalColumns}`; } } function setupFallbackHandlers() { console.log('Использование фолбэк обработчиков'); const handlers = { 'showAllColumns': showAllColumns, 'showDataColumns': showDataColumns, 'toggleEmptyColumns': toggleEmptyColumns, 'showAllRows': showAllRows, 'showDataRows': showDataRows, 'toggleEmptyRows': toggleEmptyRows, 'deleteEmptyRows': deleteEmptyRows }; // Простой подход через onclick Object.entries(handlers).forEach(([name, handler]) => { const button = document.querySelector(`[onclick*="${name}"]`); if (button && !button.hasAttribute('data-handler-set')) { button.setAttribute('data-handler-set', 'true'); button.onclick = function(e) { e.preventDefault(); e.stopPropagation(); try { handler(); } catch (error) { console.error(`Ошибка в обработчике ${name}:`, error); showNotification(`Ошибка: ${error.message}`, 'error'); } }; } }); } function showDataColumns() { const table = document.getElementById('dataTable'); const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет данных в таблице', 'warning'); return; } // Определяем, какие колонки имеют данные const columnHasData = new Array(headers.length).fill(false); columnHasData[0] = true; // Первая колонка (characteristic) всегда показываем rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, cellIndex) => { const value = cell.textContent.trim(); if (value && value !== '—' && value !== '') { columnHasData[cellIndex] = true; } }); }); // Показываем/скрываем колонки headers.forEach((header, index) => { if (columnHasData[index]) { header.style.display = ''; } else { header.style.display = 'none'; } }); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, index) => { if (columnHasData[index]) { cell.style.display = ''; } else { cell.style.display = 'none'; } }); }); const visibleCount = columnHasData.filter(Boolean).length - 1; // -1 для characteristic console.log(`Показано ${visibleCount} колонок с данными`); showNotification(`Показано ${visibleCount} колонок с данными`, 'info'); } function toggleEmptyColumns() { const table = document.getElementById('dataTable'); if (!table) return; const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет данных в таблице', 'warning'); return; } const columnIsEmpty = new Array(headers.length).fill(true); columnIsEmpty[0] = false; rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, cellIndex) => { const value = cell.textContent.trim(); if (value && value !== '—' && value !== '') { columnIsEmpty[cellIndex] = false; } }); }); const firstEmptyHeader = headers[1]; const isEmptyHidden = firstEmptyHeader && columnIsEmpty[1] && firstEmptyHeader.style.display === 'none'; headers.forEach((header, index) => { if (index === 0) return; if (columnIsEmpty[index]) { header.style.display = isEmptyHidden ? '' : 'none'; } }); rows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach((cell, index) => { if (index === 0) return; if (columnIsEmpty[index]) { cell.style.display = isEmptyHidden ? '' : 'none'; } }); }); const emptyCount = columnIsEmpty.filter((empty, idx) => empty && idx > 0).length; const action = isEmptyHidden ? 'показаны' : 'скрыты'; showNotification(`${emptyCount} пустых колонок ${action}`, 'info'); updateColumnCounter(); } function countVisibleColumns() { const headers = document.querySelectorAll('#tableHeader th'); let visibleCount = 0; headers.forEach(header => { if (header.style.display !== 'none') { visibleCount++; } }); return visibleCount - 1; // Минус characteristic } function showNotification(message, type = 'info') { let container = document.getElementById('notificationContainer'); if (!container) { // Создаем контейнер, если его нет const newContainer = document.createElement('div'); newContainer.id = 'notificationContainer'; newContainer.className = 'notification-container'; document.body.appendChild(newContainer); container = newContainer; } const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = `
${message}
`; container.appendChild(notification); // Автоматическое удаление через 5 секунд setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 5000); } const notificationStyle = document.createElement('style'); notificationStyle.textContent = ` .notification-container { position: fixed; top: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 10px; } .notification { background: white; border-radius: 8px; padding: 15px 20px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; align-items: center; justify-content: space-between; min-width: 300px; max-width: 400px; animation: slideIn 0.3s ease-out; border-left: 4px solid #3498db; } .notification.success { border-left-color: #10b981; } .notification.error { border-left-color: #ef4444; } .notification.warning { border-left-color: #f59e0b; } .notification-content { display: flex; align-items: center; gap: 10px; flex: 1; } .notification-content i { font-size: 20px; } .notification.success .notification-content i { color: #10b981; } .notification.error .notification-content i { color: #ef4444; } .notification.warning .notification-content i { color: #f59e0b; } .notification-close { background: none; border: none; color: #6b7280; cursor: pointer; padding: 5px; margin-left: 10px; } .notification-close:hover { color: #374151; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(notificationStyle); function debugSpecificRows() { try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { alert('Нет табличных данных'); return; } console.log('=== ОТЛАДКА КОНКРЕТНЫХ СТРОК ==='); // Показываем все строки data.table_data.forEach((row, index) => { console.log(`\n=== Строка ${index} ===`); console.log('Тип:', typeof row); console.log('Данные:', row); if (row && typeof row === 'object') { console.log('Ключи:', Object.keys(row)); console.log('characteristic:', row.characteristic || '(пусто)'); // Показываем первые 5 колонок const columns = Object.keys(row).filter(key => key.startsWith('column_')).sort(); columns.slice(0, 5).forEach(key => { console.log(` ${key}: "${row[key]}"`); }); if (columns.length > 5) { console.log(` ... и ещё ${columns.length - 5} колонок`); } } }); alert(`Проанализировано ${data.table_data.length} строк. Смотрите консоль (F12) для деталей.`); } catch (error) { console.error('Ошибка при отладке строк:', error); alert('Ошибка: ' + error.message); } } function analyzeTableData(tableData) { console.log('=== АНАЛИЗ ДАННЫХ ТАБЛИЦЫ ==='); console.log('Всего строк в массиве:', tableData.length); let validRows = 0; let emptyRows = 0; let invalidRows = 0; tableData.forEach((row, index) => { if (!row) { console.log(`Строка ${index}: null или undefined`); invalidRows++; return; } if (typeof row !== 'object') { console.log(`Строка ${index}: не объект (${typeof row})`, row); invalidRows++; return; } // Проверяем, есть ли характеристика if (!row.characteristic) { console.log(`Строка ${index}: нет characteristic`, row); emptyRows++; } else { validRows++; // Подсчитываем колонки с данными const dataColumns = Object.keys(row).filter(key => key !== 'characteristic' && row[key] !== undefined && row[key] !== '' ).length; console.log(`Строка ${index}: "${row.characteristic.substring(0, 30)}...", колонок с данными: ${dataColumns}`); } }); console.log(`Итоги анализа:`); console.log(`- Всего строк: ${tableData.length}`); console.log(`- Валидных строк: ${validRows}`); console.log(`- Пустых строк: ${emptyRows}`); console.log(`- Некорректных строк: ${invalidRows}`); return { validRows, emptyRows, invalidRows }; } function applyCellStyles(td, value) { td.style.textAlign = 'center'; td.style.verticalAlign = 'middle'; td.style.padding = '8px 4px'; td.style.minWidth = '80px'; td.style.maxWidth = '150px'; td.style.whiteSpace = 'nowrap'; td.style.overflow = 'hidden'; td.style.textOverflow = 'ellipsis'; // Очищаем HTML и показываем просто текст td.textContent = value || ''; td.title = value || ''; // Если значение пустое if (!value || value.trim() === '') { td.style.color = '#9ca3af'; td.style.fontStyle = 'italic'; td.textContent = '—'; return; } const cleanValue = value.trim(); // Простые стили БЕЗ эмодзи if (cleanValue === '+') { td.style.color = '#10b981'; td.style.fontWeight = 'bold'; } else if (cleanValue === '-') { td.style.color = '#ef4444'; td.style.fontWeight = 'bold'; } else if (cleanValue === '?' || cleanValue.includes('?')) { td.style.color = '#f59e0b'; td.style.fontWeight = 'bold'; } else if (cleanValue === 'W' || cleanValue === 'w') { td.style.color = '#8b5cf6'; td.style.fontWeight = 'bold'; } else if (cleanValue === 'ND' || cleanValue === 'nd') { td.style.color = '#9ca3af'; td.style.fontStyle = 'italic'; } else if (cleanValue.includes('/')) { // Значения типа "+/+", "+/-", "W/+" td.style.fontWeight = 'bold'; td.textContent = cleanValue; } else if (cleanValue.includes('-') && !isNaN(parseInt(cleanValue.split('-')[0]))) { // Числовые диапазоны типа "0-1", "2-4" td.style.color = '#3b82f6'; td.textContent = cleanValue; } else if (!isNaN(parseInt(cleanValue)) || !isNaN(parseFloat(cleanValue))) { // Числа td.style.color = '#3b82f6'; td.textContent = cleanValue; } else { // Остальные значения td.style.color = '#374151'; td.textContent = cleanValue; } } function addTableLegend(correctedCells) { const existingLegend = document.getElementById('tableLegend'); if (existingLegend) { existingLegend.remove(); } const legend = document.createElement('div'); legend.id = 'tableLegend'; legend.style.cssText = ` padding: 8px; background: #f8fafc; border-top: 1px solid #e2e8f0; font-size: 0.8rem; color: #4a5568; `; legend.innerHTML = ` + Положительный - Отрицательный ? Неопределенный w Слабоположительный ND Нет данных `; if (correctedCells > 0) { legend.innerHTML += ` | Исправлено: ${correctedCells}`; } elements.tableInfo.parentNode.insertBefore(legend, elements.tableInfo.nextSibling); } function checkMissingData(jsonStr) { try { const data = JSON.parse(jsonStr); const tableData = data.table_data || []; const expectedRows = parseInt(elements.expectedRowsInput?.value) || 30; // Проверяем, что элементы существуют if (elements.validationAlert && elements.alertTitle && elements.alertMessage) { elements.validationAlert.style.display = 'block'; if (tableData.length < expectedRows) { const missing = expectedRows - tableData.length; elements.alertTitle.textContent = 'ВНИМАНИЕ: Пропущены данные'; elements.alertMessage.textContent = `Пропущено ${missing} строк! Ожидалось: ${expectedRows}, получено: ${tableData.length}`; elements.validationAlert.className = 'validation-alert warning'; } else { elements.alertTitle.textContent = 'Успешно'; elements.alertMessage.textContent = `Все данные извлечены успешно. Строк: ${tableData.length}`; elements.validationAlert.className = 'validation-alert success'; } } else { console.warn('Элементы валидации не найдены'); // Просто выводим в консоль if (tableData.length < expectedRows) { const missing = expectedRows - tableData.length; console.warn(`ВНИМАНИЕ: Пропущено ${missing} строк!`); } else { console.log(`Успешно извлечено ${tableData.length} строк`); } } } catch (error) { console.error('Ошибка при проверке данных:', error); } } function toggleView() { console.log('toggleView вызвана'); // Определяем, что сейчас видно const tableViewVisible = elements.tableView.style.display === 'block'; const jsonViewVisible = elements.jsonView.style.display === 'block'; console.log('Таблица видна:', tableViewVisible); console.log('JSON виден:', jsonViewVisible); if (tableViewVisible) { // Если таблица видна, показываем JSON showView('json'); } else { // Иначе показываем таблицу showView('table'); } } function showView(view) { console.log('showView вызывается с параметром:', view); if (!elements.tableView || !elements.jsonView || !elements.toggleViewBtn) { console.error('Элементы для переключения вида не найдены'); return; } if (view === 'table') { console.log('Показываем таблицу, скрываем JSON'); // Показываем таблицу elements.tableView.style.display = 'block'; // Скрываем JSON elements.jsonView.style.display = 'none'; // Обновляем кнопку elements.toggleViewBtn.innerHTML = ' Показать JSON'; elements.toggleViewBtn.setAttribute('data-view', 'json'); // Если есть данные, обновляем таблицу try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (data.table_data && Array.isArray(data.table_data) && data.table_data.length > 0) { console.log('Обновляем таблицу с данными'); displayTable(data.table_data); } } catch (error) { console.error('Ошибка при обновлении таблицы:', error); } } else { console.log('Показываем JSON, скрываем таблицу'); // Показываем JSON elements.jsonView.style.display = 'block'; // Скрываем таблицу elements.tableView.style.display = 'none'; // Обновляем кнопку elements.toggleViewBtn.innerHTML = ' Показать таблицу'; elements.toggleViewBtn.setAttribute('data-view', 'table'); } // Логируем конечное состояние console.log('После showView:'); console.log('tableView display:', elements.tableView.style.display); console.log('jsonView display:', elements.jsonView.style.display); console.log('data-view:', elements.toggleViewBtn.getAttribute('data-view')); } function copyJSON() { try { // Пытаемся спарсить JSON, чтобы убедиться в его корректности const jsonText = elements.jsonOutput.textContent; JSON.parse(jsonText); navigator.clipboard.writeText(jsonText) .then(() => { const originalText = elements.copyJsonBtn.innerHTML; elements.copyJsonBtn.innerHTML = ' Скопировано!'; setTimeout(() => { elements.copyJsonBtn.innerHTML = originalText; }, 2000); }) .catch(err => { console.error('Ошибка при копировании: ', err); alert('Ошибка при копировании: ' + err.message); }); } catch (error) { alert('Невозможно скопировать: JSON содержит ошибки. ' + error.message); } } function downloadJSON() { if (!jsonData) return; try { // Пытаемся спарсить JSON const jsonObj = JSON.parse(jsonData); const dataStr = JSON.stringify(jsonObj, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; link.download = `table_result_${Date.now()}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (error) { console.error('Ошибка при создании файла для скачивания:', error); alert('Ошибка: Невозможно скачать файл. JSON содержит ошибки.'); } } function showLoading(show) { if (elements.loadingOverlay) { elements.loadingOverlay.style.display = show ? 'flex' : 'none'; // было block } if (!show && elements.progressBar) { updateProgress(0, ''); } } function updateProgress(percent, status) { if (elements.progressBar) { elements.progressBar.style.width = `${percent}%`; } if (elements.statusText) { elements.statusText.textContent = status; } // Обновляем шаги прогресса (добавьте эту функцию) updateProgressSteps(percent); } function updateProgressSteps(percent) { const steps = document.querySelectorAll('.step'); steps.forEach((step, index) => { const stepPercent = (index + 1) * 25; if (percent >= stepPercent) { step.classList.add('active'); } else { step.classList.remove('active'); } }); } function saveSettings() { const settings = { apiKey: elements.apiKeyInput.value, accessToken: elements.accessTokenInput.value, expectedRows: elements.expectedRowsInput.value }; localStorage.setItem('pdfExtractorSettings', JSON.stringify(settings)); checkProcessButton(); } function loadSavedSettings() { const saved = localStorage.getItem('pdfExtractorSettings'); if (saved) { try { const settings = JSON.parse(saved); if (settings.apiKey) elements.apiKeyInput.value = settings.apiKey; if (settings.accessToken) elements.accessTokenInput.value = settings.accessToken; if (settings.expectedRows) elements.expectedRowsInput.value = settings.expectedRows; checkProcessButton(); } catch (error) { console.error('Ошибка при загрузке настроек:', error); } } } document.addEventListener('DOMContentLoaded', init); const style = document.createElement('style'); style.textContent = ` .json-key { color: #005cc5; font-weight: bold; } .json-string { color: #032f62; } .json-number { color: #d73a49; } .json-boolean { color: #6f42c1; } .json-null { color: #d73a49; font-weight: bold; } .special-char { font-weight: bold; color: #d73a49; } `; document.head.appendChild(style); console.log("Проверка элементов:"); Object.keys(elements).forEach(key => { console.log(`${key}:`, elements[key] ? '✓' : '✗'); }); if (elements.clearResultsBtn) { elements.clearResultsBtn.addEventListener('click', clearResults); } function clearResults() { // Очищаем JSON elements.jsonOutput.textContent = '{\n "table_data": []\n}'; // Очищаем таблицу elements.tableHeader.innerHTML = 'characteristic'; elements.tableBody.innerHTML = ''; elements.tableInfo.textContent = 'Загрузите файл для просмотра данных'; // Скрываем таблицу, показываем JSON showView('json'); if (document.getElementById('exportDropdownBtn')) { document.getElementById('exportDropdownBtn').disabled = true; } // Очищаем валидационное сообщение if (elements.validationAlert) { elements.validationAlert.style.display = 'none'; } // Отключаем кнопки elements.copyJsonBtn.disabled = true; elements.toggleViewBtn.disabled = true; elements.clearResultsBtn.disabled = true; elements.downloadBtn.disabled = true; // Очищаем глобальные переменные jsonData = null; currentFileId = null; console.log('Результаты очищены'); } if (document.getElementById('showRawBtn')) { document.getElementById('showRawBtn').addEventListener('click', showRawResponse); } rawResponse = content; function showRawResponse() { if (rawResponse) { alert('Сырой ответ от GigaChat (первые 1000 символов):\n\n' + rawResponse.substring(0, 1000)); } } if (document.getElementById('showRawBtn')) { document.getElementById('showRawBtn').style.display = 'inline-block'; } function validateTableData(tableData) { if (!tableData || !Array.isArray(tableData)) { console.error('tableData должен быть массивом:', tableData); return false; } if (tableData.length === 0) { console.warn('Массив tableData пустой'); return false; } // Проверяем первую строку на наличие необходимых полей const firstRow = tableData[0]; if (!firstRow || typeof firstRow !== 'object') { console.error('Первая строка не является объектом:', firstRow); return false; } // Проверяем наличие хотя бы characteristic if (!firstRow.hasOwnProperty('characteristic')) { console.warn('Нет поля characteristic в данных'); } return true; } if (document.getElementById('exportTableBtn')) { document.getElementById('exportTableBtn').addEventListener('click', exportTable); } if (document.getElementById('scrollToTopBtn')) { document.getElementById('scrollToTopBtn').addEventListener('click', scrollTableToTop); } if (document.getElementById('scrollToBottomBtn')) { document.getElementById('scrollToBottomBtn').addEventListener('click', scrollTableToBottom); } function exportTable() { if (!jsonData) return; try { const data = JSON.parse(jsonData); if (!data.table_data || !Array.isArray(data.table_data)) { throw new Error('Нет табличных данных для экспорта'); } // Создаем CSV let csv = ''; const headers = ['Characteristic']; // Определяем колонки const firstRow = data.table_data[0]; if (firstRow) { Object.keys(firstRow).forEach(key => { if (key !== 'characteristic') { headers.push(key.toUpperCase()); } }); } csv += headers.join(',') + '\n'; // Добавляем данные data.table_data.forEach(row => { const rowData = [row.characteristic || '']; headers.slice(1).forEach(header => { const key = header.toLowerCase(); rowData.push(row[key] || ''); }); csv += rowData.map(cell => `"${cell}"`).join(',') + '\n'; }); // Скачиваем CSV const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `table_${Date.now()}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (error) { console.error('Ошибка при экспорте таблицы:', error); alert('Ошибка при экспорте: ' + error.message); } } function scrollTableToTop() { const tableWrapper = document.querySelector('.table-wrapper'); if (tableWrapper) { tableWrapper.scrollTop = 0; } } function scrollTableToBottom() { const tableWrapper = document.querySelector('.table-wrapper'); if (tableWrapper) { tableWrapper.scrollTop = tableWrapper.scrollHeight; } } if (elements.toggleViewBtn) { console.log('Добавляем обработчик для toggleViewBtn'); elements.toggleViewBtn.addEventListener('click', function(e) { console.log('Кнопка toggleViewBtn нажата', e); toggleView(); }); } else { console.error('Кнопка toggleViewBtn не найдена!'); } window.testTableView = function() { console.log('=== Тест переключения вида ==='); console.log('Текущее состояние:'); console.log('tableView:', elements.tableView.style.display); console.log('jsonView:', elements.jsonView.style.display); console.log('data-view:', elements.toggleViewBtn.getAttribute('data-view')); // Принудительно показываем таблицу elements.tableView.style.display = 'block'; elements.jsonView.style.display = 'none'; elements.toggleViewBtn.setAttribute('data-view', 'json'); elements.toggleViewBtn.innerHTML = ' Показать JSON'; console.log('После принудительного переключения:'); console.log('tableView:', elements.tableView.style.display); console.log('jsonView:', elements.jsonView.style.display); console.log('data-view:', elements.toggleViewBtn.getAttribute('data-view')); }; function fixTableSymbols(tableData) { if (!Array.isArray(tableData)) return tableData; console.log('Исправление символов в таблице...'); return tableData.map(row => { if (!row || typeof row !== 'object') return row; const fixedRow = { ...row }; Object.keys(fixedRow).forEach(key => { if (key === 'characteristic') return; const value = String(fixedRow[key] || ''); // Исправляем символы let fixedValue = value; // Преобразуем похожие на плюс символы if (/[?�•∗·∙⋅◦]/.test(value)) { // Если в строке есть символы, похожие на плюс fixedValue = value.replace(/[?�•∗·∙⋅◦]/g, match => { // Проверяем контекст const context = JSON.stringify(row).toLowerCase(); const isLikelyPlus = context.includes('assimilation') || context.includes('enzyme') || context.includes('test') || context.includes('фермент') || context.includes('ассимиляция'); if (isLikelyPlus) { console.log(`Исправляем символ "${match}" в "${row.characteristic}" на "+"`); return '+'; } return match; }); } // Преобразуем разные типы минусов/тире if (/[―–—~~˗‐‑‒–—―﹘﹣-]/.test(fixedValue)) { fixedValue = fixedValue.replace(/[―–—~~˗‐‑‒–—―﹘﹣-]/g, '-'); } // Убираем лишние пробелы fixedValue = fixedValue.trim(); // Заменяем множественные пробелы на один fixedValue = fixedValue.replace(/\s+/g, ' '); if (value !== fixedValue) { console.log(`Исправлено: "${value}" → "${fixedValue}" в ${row.characteristic}`); } fixedRow[key] = fixedValue; }); return fixedRow; }); } function postProcessTableData(tableData) { if (!Array.isArray(tableData)) return tableData; console.log('Дополнительная постобработка данных...'); // Определяем контекст строк const assimilationRows = new Set(); const enzymeRows = new Set(); // Расширенные ключевые слова для разных типов строк const assimilationKeywords = [ 'assimilation', 'ассимиляция', 'assimilates', 'assimilate', 'углевод', 'углеводы', 'сахар', 'сахара', 'glucose', 'глюкоза', 'glucos', 'глюкоз', 'fructose', 'фруктоза', 'fructos', 'фруктоз', 'maltose', 'мальтоза', 'maltos', 'мальтоз', 'dextrin', 'декстрин', 'dextr', 'декстри', 'cellobiose', 'целлобиоза', 'cellobios', 'целлобиоз', 'mannose', 'манноза', 'mannos', 'манноз', 'lactose', 'лактоза', 'lactos', 'лактоз', 'sucrose', 'сахароза', 'sucros', 'сахароз', 'galactose', 'галактоза', 'galactos', 'галактоз', 'sorbose', 'сорбоза', 'sorbos', 'сорбоз', 'xylose', 'ксилоза', 'xylos', 'ксилоз', 'arabinose', 'арабиноза', 'arabinos', 'арабиноз', 'ribose', 'рибоза', 'ribos', 'рибоз', 'rhamnose', 'рамноза', 'rhamnos', 'рамноз', 'trehalose', 'трегалоза', 'trehalos', 'трегалоз', 'raffinose', 'раффиноза', 'raffinos', 'раффиноз', 'melezitose', 'мелецитоза', 'melezitos', 'мелецитоз', 'starch', 'крахмал', 'starc', 'крахма', 'glycogen', 'гликоген', 'glycoge', 'гликоге' ]; const enzymeKeywords = [ 'enzyme', 'фермент', 'activity', 'активность', 'lipase', 'липаза', 'lipas', 'липаз', 'arylamidase', 'ариламидаза', 'arylamidas', 'ариламидаз', 'trypsin', 'трипсин', 'trypsi', 'трипси', 'glucosaminidase', 'глюкозаминидаза', 'glucosaminidas', 'глюкозаминидаз', 'phosphatase', 'фосфатаза', 'phosphatas', 'фосфатаз', 'protease', 'протеаза', 'proteas', 'протеаз', 'catalase', 'каталаза', 'catalas', 'каталаз', 'oxidase', 'оксидаза', 'oxidas', 'оксидаз', 'urease', 'уреаза', 'ureas', 'уреаз', 'amylase', 'амилаза', 'amylas', 'амилаз', 'cellulase', 'целлюлаза', 'cellulas', 'целлюлаз', 'hemolysin', 'гемолизин', 'hemolysi', 'гемолизи', 'coagulase', 'коагулаза', 'coagulas', 'коагулаз' ]; // Классифицируем строки tableData.forEach(row => { if (!row.characteristic) return; const characteristic = row.characteristic.toLowerCase().trim(); // Проверяем ассимиляцию const isAssimilation = assimilationKeywords.some(keyword => { // Точное совпадение или вхождение слова return characteristic === keyword || characteristic.includes(keyword) || characteristic.split(/\s+/).some(word => word === keyword); }); if (isAssimilation) { assimilationRows.add(row.characteristic); } // Проверяем ферменты const isEnzyme = enzymeKeywords.some(keyword => { return characteristic === keyword || characteristic.includes(keyword) || characteristic.split(/\s+/).some(word => word === keyword); }); if (isEnzyme) { enzymeRows.add(row.characteristic); } }); console.log('Классификация строк:'); console.log('Ассимиляция:', Array.from(assimilationRows)); console.log('Ферменты:', Array.from(enzymeRows)); // Правила преобразования в зависимости от контекста return tableData.map(row => { const newRow = { ...row }; const characteristic = row.characteristic ? row.characteristic.toLowerCase().trim() : ''; Object.keys(newRow).forEach(key => { if (key === 'characteristic') return; let value = String(newRow[key] || '').trim(); let originalValue = value; // ПРАВИЛО 1: Ассимиляция углеводов - "?" ВСЕГДА преобразуется в "+" if (assimilationRows.has(row.characteristic) && value === '?') { console.log(`Ассимиляция углеводов: "${row.characteristic}" - заменяем "?" на "+"`); value = '+'; } // ПРАВИЛО 2: Для ферментов - "?" может быть "+" или "-" в зависимости от типа else if (enzymeRows.has(row.characteristic) && value === '?') { const enzymeName = characteristic; // Ферменты, которые обычно дают положительную реакцию const positiveEnzymes = [ 'lipase', 'липаза', 'trypsin', 'трипсин', 'glucosaminidase', 'глюкозаминидаза', 'catalase', 'каталаза', 'oxidase', 'оксидаза' ]; const isPositiveEnzyme = positiveEnzymes.some(enzyme => enzymeName.includes(enzyme) ); if (isPositiveEnzyme) { console.log(`Фермент ${row.characteristic}: обычно положительный, заменяем "?" на "+"`); value = '+'; } else { console.log(`Фермент ${row.characteristic}: обычно отрицательный, заменяем "?" на "-"`); value = '-'; } } // ПРАВИЛО 3: Общее правило для остальных случаев else if (value === '?') { // Анализируем столбец const columnData = tableData .map(r => r[key]) .filter(v => v !== undefined) .map(v => String(v).trim()); const plusCount = columnData.filter(v => v === '+').length; const minusCount = columnData.filter(v => v === '-').length; const questionCount = columnData.filter(v => v === '?').length; const total = columnData.length; if (total > 5) { // Только если достаточно данных const plusRatio = plusCount / total; const minusRatio = minusCount / total; const questionRatio = questionCount / total; // Если в столбце явное большинство "+" if (plusRatio > 0.7 && questionRatio < 0.3) { console.log(`Столбец ${key}: доминируют "+" (${plusCount}/${total}), заменяем "?" на "+" в "${row.characteristic}"`); value = '+'; } // Если в столбце явное большинство "-" else if (minusRatio > 0.7 && questionRatio < 0.3) { console.log(`Столбец ${key}: доминируют "-" (${minusCount}/${total}), заменяем "?" на "-" в "${row.characteristic}"`); value = '-'; } } } // Исправляем другие похожие символы if (value && !['+', '-', '?'].includes(value)) { // Любой символ, похожий на плюс if (/[+﹢⁺₊†‡ᐩ•∗·∙⋅◦]/.test(value)) { console.log(`Исправляем похожий на плюс символ "${value}" на "+" в "${row.characteristic}"`); value = '+'; } // Любой символ, похожий на минус else if (/[−–—―‑‒–—―]/.test(value)) { console.log(`Исправляем похожий на минус символ "${value}" на "-" в "${row.characteristic}"`); value = '-'; } } // Убираем лишние пробелы value = value.trim(); // Если значение изменилось, сохраняем if (originalValue !== value) { newRow[key] = value; console.log(`Исправлено: "${row.characteristic}"[${key}] "${originalValue}" → "${value}"`); } }); return newRow; }); } function addTableLegend(correctedCells) { const existingLegend = document.getElementById('tableLegend'); if (existingLegend) { existingLegend.remove(); } const legend = document.createElement('div'); legend.id = 'tableLegend'; legend.style.cssText = ` padding: 12px; background: #f8fafc; border-top: 1px solid #e2e8f0; display: flex; flex-wrap: wrap; gap: 16px; align-items: center; font-size: 0.85rem; color: #4a5568; `; if (correctedCells > 0) { legend.innerHTML += `
Исправленный символ Автоматически исправлено: ${correctedCells}
`; } elements.tableInfo.parentNode.insertBefore(legend, elements.tableInfo.nextSibling); } if (document.getElementById('fixSymbolsBtn')) { document.getElementById('fixSymbolsBtn').addEventListener('click', manualFixSymbols); } function manualFixSymbols() { try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { alert('Нет табличных данных для исправления'); return; } console.log('Ручное исправление символов...'); // Сохраняем оригинальные данные const originalData = JSON.parse(JSON.stringify(data.table_data)); // Применяем исправления data.table_data = fixTableSymbols(data.table_data); data.table_data = postProcessTableData(data.table_data); // Считаем изменения let changes = 0; originalData.forEach((originalRow, index) => { const fixedRow = data.table_data[index]; if (!originalRow || !fixedRow) return; Object.keys(originalRow).forEach(key => { if (key === 'characteristic') return; const originalValue = String(originalRow[key] || ''); const fixedValue = String(fixedRow[key] || ''); if (originalValue !== fixedValue) { changes++; console.log(`Изменение: ${originalRow.characteristic}[${key}] "${originalValue}" → "${fixedValue}"`); } }); }); // Обновляем отображение elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } // Показываем результат if (changes > 0) { alert(`Исправлено ${changes} символов в таблице`); } else { alert('Символы не нуждаются в исправлении'); } // Сохраняем исправленные данные jsonData = JSON.stringify(data); } catch (error) { console.error('Ошибка при ручном исправлении символов:', error); alert('Ошибка: ' + error.message); } } if (document.getElementById('convertQuestionsBtn')) { document.getElementById('convertQuestionsBtn').addEventListener('click', convertQuestionsToPlus); } function convertQuestionsToPlus() { try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { alert('Нет табличных данных для обработки'); return; } let changes = 0; const assimilationKeywords = ['dextrin', 'cellobiose', 'fructose', 'glucose', 'maltose', 'mannose', 'sugar', 'углевод', 'сахар']; data.table_data = data.table_data.map(row => { const newRow = { ...row }; const characteristic = (row.characteristic || '').toLowerCase(); // Проверяем, относится ли строка к ассимиляции углеводов const isAssimilation = assimilationKeywords.some(keyword => characteristic.includes(keyword) ); if (isAssimilation) { Object.keys(newRow).forEach(key => { if (key === 'characteristic') return; const value = String(newRow[key] || ''); if (value === '?') { newRow[key] = '+'; changes++; console.log(`Преобразовано: ${row.characteristic}[${key}] "?" → "+"`); } }); } return newRow; }); // Обновляем отображение elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } alert(`Преобразовано ${changes} символов "?" в "+" для ассимиляции углеводов`); // Сохраняем исправленные данные jsonData = JSON.stringify(data); } catch (error) { console.error('Ошибка при преобразовании:', error); alert('Ошибка: ' + error.message); } } function forceFixAssimilation(tableData) { if (!Array.isArray(tableData)) return tableData; // Расширенный список углеводов для ассимиляции const carbohydrateKeywords = [ // Английские названия 'glucose', 'fructose', 'maltose', 'dextrin', 'cellobiose', 'mannose', 'lactose', 'sucrose', 'galactose', 'sorbose', 'xylose', 'arabinose', 'ribose', 'rhamnose', 'trehalose', 'raffinose', 'melezitose', 'starch', 'glycogen', // Русские названия 'глюкоза', 'фруктоза', 'мальтоза', 'декстрин', 'целлобиоза', 'манноза', 'лактоза', 'сахароза', 'галактоза', 'сорбоза', 'ксилоза', 'арабиноза', 'рибоза', 'рамноза', 'трегалоза', 'раффиноза', 'мелецитоза', 'крахмал', 'гликоген', // Общие термины 'sugar', 'углевод', 'сахар', 'carbohydrate', 'углеводы', 'assimilation', 'ассимиляция' ]; return tableData.map(row => { if (!row || typeof row !== 'object' || !row.characteristic) return row; const characteristic = row.characteristic.toLowerCase().trim(); const isCarbohydrate = carbohydrateKeywords.some(keyword => characteristic.includes(keyword.toLowerCase()) ); if (!isCarbohydrate) return row; const newRow = { ...row }; let changes = 0; Object.keys(newRow).forEach(key => { if (key === 'characteristic') return; const value = String(newRow[key] || '').trim(); if (value === '?') { newRow[key] = '+'; changes++; } }); if (changes > 0) { console.log(`Принудительно исправлено ${changes} символов "?" в "+" для: ${row.characteristic}`); } return newRow; }); } function manualFixSymbols() { try { const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { alert('Нет табличных данных для исправления'); return; } console.log('Ручное исправление символов...'); // Сохраняем оригинальные данные const originalData = JSON.parse(JSON.stringify(data.table_data)); // Применяем все исправления data.table_data = fixTableSymbols(data.table_data); data.table_data = postProcessTableData(data.table_data); data.table_data = forceFixAssimilation(data.table_data); // Добавляем принудительное исправление // Считаем изменения let changes = 0; originalData.forEach((originalRow, index) => { const fixedRow = data.table_data[index]; if (!originalRow || !fixedRow) return; Object.keys(originalRow).forEach(key => { if (key === 'characteristic') return; const originalValue = String(originalRow[key] || ''); const fixedValue = String(fixedRow[key] || ''); if (originalValue !== fixedValue) { changes++; console.log(`Изменение: ${originalRow.characteristic}[${key}] "${originalValue}" → "${fixedValue}"`); } }); }); // Обновляем отображение elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } // Показываем результат if (changes > 0) { alert(`Исправлено ${changes} символов в таблице`); } else { alert('Символы не нуждаются в исправлении'); } // Сохраняем исправленные данные jsonData = JSON.stringify(data); } catch (error) { console.error('Ошибка при ручном исправлении символов:', error); alert('Ошибка: ' + error.message); } } function forceFixAssimilationImproved(tableData) { if (!Array.isArray(tableData)) return tableData; console.log('=== УЛУЧШЕННОЕ ИСПРАВЛЕНИЕ АССИМИЛЯЦИИ ==='); // Шаблоны для распознавания углеводов const carbPatterns = [ // Основные углеводы (точные совпадения) /^dextrin$/i, /^cellobiose$/i, /^fructose$/i, /^glucose$/i, /^maltose$/i, /^mannose$/i, /^lactose$/i, /^sucrose$/i, /^galactose$/i, /^sorbose$/i, /^xylose$/i, /^arabinose$/i, /^ribose$/i, /^rhamnose$/i, /^trehalose$/i, /^raffinose$/i, /^melezitose$/i, /^starch$/i, /^glycogen$/i, // С префиксами /^d-.*glucose$/i, /^d-.*fructose$/i, /^d-.*mannose$/i, /^d-.*maltose$/i, /^d-.*cellobiose$/i, /^α-d-.*glucose$/i, /^d-glucose$/i, /^d-fructose$/i, /^d-mannose$/i, /^d-maltose$/i, // Русские варианты /^декстрин$/i, /^целлобиоза$/i, /^фруктоза$/i, /^глюкоза$/i, /^мальтоза$/i, /^манноза$/i, /^лактоза$/i, /^сахароза$/i, /^галактоза$/i, /^сорбоза$/i, /^ксилоза$/i, /^арабиноза$/i, /^рибоза$/i, /^рамноза$/i, /^трегалоза$/i, /^раффиноза$/i, /^мелецитоза$/i, /^крахмал$/i, /^гликоген$/i, // Частичные совпадения /glucos/i, /fructos/i, /maltos/i, /mannos/i, /cellobios/i, /dextr/i, /сахар/i, /углевод/i ]; let totalChanges = 0; const changedRows = []; const result = tableData.map((row, rowIndex) => { if (!row || typeof row !== 'object' || !row.characteristic) return row; const characteristic = row.characteristic.trim(); const charLower = characteristic.toLowerCase(); // Проверяем, является ли строка углеводом const isCarbohydrate = carbPatterns.some(pattern => pattern.test(characteristic) || pattern.test(charLower) ); console.log(`Строка ${rowIndex}: "${characteristic}" - углевод: ${isCarbohydrate}`); const newRow = { ...row }; let rowChanges = 0; if (isCarbohydrate) { Object.keys(newRow).forEach(key => { if (key === 'characteristic') return; const originalValue = String(newRow[key] || ''); // Очищаем значение для проверки const cleanValue = originalValue .replace(/[❓❌✅⭐—–\s↵↵]/g, '') // Убираем эмодзи и спецсимволы .trim(); // Проверяем различные варианты "?" const isQuestionMark = cleanValue === '?' || originalValue.includes('?') || originalValue.includes('❓') || cleanValue === '' && originalValue !== ''; // Пустые значения тоже меняем if (isQuestionMark) { const oldValue = originalValue; newRow[key] = '+'; rowChanges++; console.log(` Изменение [${key}]: "${oldValue}" → "+"`); } }); } if (rowChanges > 0) { totalChanges += rowChanges; changedRows.push({ index: rowIndex, characteristic: characteristic, changes: rowChanges }); } return newRow; }); // Логируем результаты console.log('\n=== ИТОГИ ИСПРАВЛЕНИЯ ==='); console.log(`Обработано строк: ${tableData.length}`); console.log(`Изменено строк: ${changedRows.length}`); console.log(`Всего изменений: ${totalChanges}`); if (changedRows.length > 0) { console.log('\nИзмененные строки:'); changedRows.forEach(item => { console.log(` ${item.index}. "${item.characteristic}": ${item.changes} ячеек`); }); } else { console.log('Нет изменений. Возможные причины:'); console.log('1. Нет строк углеводов в данных'); console.log('2. В строках углеводов нет знаков "?"'); console.log('3. Знаки "?" уже исправлены'); } return result; } function handleCellClick(e) { e.target.style.backgroundColor = '#e8f4fd'; e.target.focus(); } function handleCellBlur(e) { e.target.style.backgroundColor = ''; updateTableDataFromDOM(); } function handleCellKeydown(e) { if (e.key === 'Enter') { e.preventDefault(); e.target.blur(); } } function updateTableDataFromDOM() { try { const table = document.getElementById('dataTable'); const rows = table.querySelectorAll('#tableBody tr'); const headers = table.querySelectorAll('#tableHeader th'); if (!jsonData) return; const data = JSON.parse(jsonData); if (!data.table_data) return; // Обновляем данные из DOM rows.forEach((row, rowIndex) => { const cells = row.querySelectorAll('td'); const characteristic = cells[0].textContent; // Находим соответствующую строку в данных const dataRow = data.table_data.find(r => r.characteristic === characteristic); if (dataRow) { cells.forEach((cell, cellIndex) => { if (cellIndex > 0) { // Пропускаем характеристику const columnName = headers[cellIndex].textContent; const cleanColumnName = columnName.replace('Col ', 'column_'); dataRow[cleanColumnName] = cell.textContent.trim(); } }); } }); // Сохраняем обновленные данные jsonData = JSON.stringify(data); elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); } catch (error) { console.error('Ошибка при обновлении данных:', error); } } function deleteSelectedRows() { const selectedRows = document.querySelectorAll('#tableBody tr.selected'); if (selectedRows.length === 0) { alert('Выберите строки для удаления (кликните на номер строки)'); return; } if (!confirm(`Удалить выбранные ${selectedRows.length} строк?`)) { return; } try { const data = JSON.parse(jsonData); const rowsToDelete = []; selectedRows.forEach(row => { const characteristic = row.querySelector('td:first-child').textContent; const rowIndex = data.table_data.findIndex(r => r.characteristic === characteristic); if (rowIndex !== -1) { rowsToDelete.push(rowIndex); } }); // Удаляем строки в обратном порядке rowsToDelete.sort((a, b) => b - a).forEach(index => { data.table_data.splice(index, 1); }); // Обновляем данные jsonData = JSON.stringify(data); displayJSON(jsonData); showNotification(`Удалено ${rowsToDelete.length} строк`, 'success'); } catch (error) { console.error('Ошибка при удалении строк:', error); showNotification('Ошибка при удалении строк', 'error'); } } function deleteSelectedColumns() { const selectedColumns = document.querySelectorAll('#tableHeader th.selected'); if (selectedColumns.length === 0) { alert('Выберите колонки для удаления (кликните на заголовок колонки)'); return; } if (!confirm(`Удалить выбранные ${selectedColumns.length} колонок?`)) { return; } try { const data = JSON.parse(jsonData); const columnsToDelete = []; selectedColumns.forEach(th => { const colName = th.textContent.replace('Col ', 'column_'); columnsToDelete.push(colName); }); // Удаляем колонки из всех строк data.table_data.forEach(row => { columnsToDelete.forEach(col => { delete row[col]; }); }); // Обновляем данные jsonData = JSON.stringify(data); displayJSON(jsonData); showNotification(`Удалено ${columnsToDelete.length} колонок`, 'success'); } catch (error) { console.error('Ошибка при удалении колонок:', error); showNotification('Ошибка при удалении колонок', 'error'); } } function addNewRow() { try { const data = JSON.parse(jsonData); const headers = document.querySelectorAll('#tableHeader th'); // Создаем новую строку const newRow = { characteristic: `Новая строка ${data.table_data.length + 1}` }; // Добавляем все колонки headers.forEach((th, index) => { if (index > 0) { // Пропускаем характеристику const colName = th.textContent.replace('Col ', 'column_'); newRow[colName] = ''; } }); data.table_data.push(newRow); // Обновляем данные jsonData = JSON.stringify(data); displayJSON(jsonData); showNotification('Добавлена новая строка', 'success'); } catch (error) { console.error('Ошибка при добавлении строки:', error); showNotification('Ошибка при добавлении строки', 'error'); } } function addNewColumn() { const colName = prompt('Введите имя новой колонки (например: column_31):', 'column_31'); if (!colName) return; try { const data = JSON.parse(jsonData); // Добавляем новую колонку всем строкам data.table_data.forEach(row => { row[colName] = ''; }); // Обновляем данные jsonData = JSON.stringify(data); displayJSON(jsonData); showNotification(`Добавлена новая колонка: ${colName}`, 'success'); } catch (error) { console.error('Ошибка при добавлении колонки:', error); showNotification('Ошибка при добавлении колонки', 'error'); } } function showEditPanel() { const editPanel = document.createElement('div'); editPanel.id = 'editPanel'; editPanel.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: white; padding: 15px 20px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 1000; display: flex; gap: 10px; align-items: center; border: 2px solid #3b82f6; `; editPanel.innerHTML = ` Режим редактирования `; document.body.appendChild(editPanel); } function hideEditPanel() { const editPanel = document.getElementById('editPanel'); if (editPanel) { editPanel.remove(); } } function saveChanges() { updateTableDataFromDOM(); disableEditMode(); showNotification('Изменения сохранены', 'success'); } function cancelEdit() { // Восстанавливаем исходные данные if (originalTableData) { jsonData = JSON.stringify(originalTableData); displayJSON(jsonData); } disableEditMode(); showNotification('Изменения отменены', 'info'); } function toggleEditMode() { console.log('toggleEditMode вызвана, текущее состояние:', isEditMode); if (!jsonData) { showNotification('Нет данных для редактирования', 'warning'); return; } isEditMode = !isEditMode; if (isEditMode) { enableEditMode(); } else { disableEditMode(); } } function enableEditMode() { console.log('Включение режима редактирования...'); try { // Сохраняем оригинальные данные originalTableData = JSON.parse(jsonData); isEditMode = true; // Добавляем класс для редактируемых ячеек const cells = document.querySelectorAll('#tableBody td:not(:first-child)'); cells.forEach(cell => { cell.classList.add('editable-cell'); cell.contentEditable = true; cell.addEventListener('focus', handleCellFocus); cell.addEventListener('blur', handleCellBlur); cell.addEventListener('keydown', handleCellKeydown); // Добавляем возможность выбора ячейки cell.addEventListener('click', function(e) { if (e.ctrlKey || e.metaKey) { this.classList.toggle('selected-cell'); } }); }); // Добавляем номера строк addRowNumbers(); // Показываем панель редактирования showEditPanel(); showNotification('Режим редактирования включен', 'success'); } catch (e) { console.error('Ошибка при включении режима редактирования:', e); showNotification('Ошибка включения режима редактирования', 'error'); isEditMode = false; } } function disableEditMode() { console.log('Отключение режима редактирования...'); isEditMode = false; // Убираем классы и обработчики const cells = document.querySelectorAll('#tableBody td.editable-cell'); cells.forEach(cell => { cell.classList.remove('editable-cell', 'selected-cell'); cell.contentEditable = false; cell.removeEventListener('focus', handleCellFocus); cell.removeEventListener('blur', handleCellBlur); cell.removeEventListener('keydown', handleCellKeydown); }); // Убираем номера строк removeRowNumbers(); // Скрываем панель редактирования hideEditPanel(); // Сбрасываем выбранные строки и колонки selectedRows.clear(); selectedColumns.clear(); showNotification('Режим редактирования отключен', 'info'); } function handleCellFocus(e) { e.target.style.backgroundColor = '#e8f4fd'; e.target.style.outline = '2px solid #3b82f6'; } function handleCellBlur(e) { const cell = e.target; cell.style.backgroundColor = ''; cell.style.outline = ''; // Обновляем данные в JSON updateCellValue(cell); } function handleCellKeydown(e) { if (e.key === 'Enter') { e.preventDefault(); e.target.blur(); } // Ctrl+A для выбора всех ячеек if ((e.ctrlKey || e.metaKey) && e.key === 'a') { e.preventDefault(); selectAllCells(); } } function updateCellValue(cell) { try { const rowElement = cell.parentElement; const rowIndex = Array.from(rowElement.parentElement.children).indexOf(rowElement); const cellIndex = Array.from(rowElement.children).indexOf(cell); const data = JSON.parse(jsonData); const row = data.table_data[rowIndex]; if (!row) return; // Получаем имя колонки из заголовка const headers = document.querySelectorAll('#tableHeader th'); if (cellIndex >= headers.length) return; const header = headers[cellIndex]; let columnName = header.textContent.trim(); // Преобразуем заголовок в имя колонки if (columnName === 'Characteristic') { columnName = 'characteristic'; } else if (columnName.startsWith('Col ')) { columnName = columnName.toLowerCase().replace('col ', 'column_'); } // Обновляем значение const newValue = cell.textContent.trim(); const oldValue = row[columnName] || ''; if (newValue !== oldValue) { row[columnName] = newValue; // Обновляем JSON jsonData = JSON.stringify(data, null, 2); elements.jsonOutput.textContent = jsonData; highlightJSON(); // Обновляем стиль ячейки applyCellStyles(cell, newValue); console.log(`Обновлено: строка ${rowIndex}, колонка ${columnName}: "${oldValue}" → "${newValue}"`); // Показываем кнопку сохранения showSaveButton(); } } catch (error) { console.error('Ошибка при обновлении ячейки:', error); showNotification('Ошибка обновления ячейки', 'error'); } } function addRowNumbers() { const rows = document.querySelectorAll('#tableBody tr'); rows.forEach((row, index) => { if (!row.querySelector('.row-number')) { const firstCell = row.querySelector('td:first-child'); const rowNumber = document.createElement('span'); rowNumber.className = 'row-number'; rowNumber.textContent = `${index + 1}. `; rowNumber.style.marginRight = '5px'; rowNumber.style.fontWeight = 'bold'; rowNumber.style.color = '#6b7280'; if (firstCell && firstCell.firstChild) { firstCell.insertBefore(rowNumber, firstCell.firstChild); } } }); } function removeRowNumbers() { const rowNumbers = document.querySelectorAll('.row-number'); rowNumbers.forEach(number => { number.remove(); }); } function showEditPanel() { // Удаляем старую панель, если есть hideEditPanel(); const editPanel = document.createElement('div'); editPanel.id = 'editPanel'; editPanel.className = 'edit-panel'; editPanel.innerHTML = `
Режим редактирования
Кликните на ячейку для редактирования Ctrl+клик для выбора нескольких ячеек
`; document.body.appendChild(editPanel); } function hideEditPanel() { const editPanel = document.getElementById('editPanel'); if (editPanel) { editPanel.remove(); } } function showSaveButton() { const saveBtn = document.querySelector('#editPanel .btn-success'); if (saveBtn) { saveBtn.innerHTML = ' Сохранить*'; saveBtn.style.backgroundColor = '#f59e0b'; saveBtn.style.borderColor = '#f59e0b'; } } function saveChanges() { if (!originalTableData) return; // Просто обновляем данные - они уже обновляются при изменении ячеек originalTableData = JSON.parse(jsonData); const saveBtn = document.querySelector('#editPanel .btn-success'); if (saveBtn) { saveBtn.innerHTML = ' Сохранено'; saveBtn.style.backgroundColor = ''; saveBtn.style.borderColor = ''; setTimeout(() => { saveBtn.innerHTML = ' Сохранить'; }, 2000); } updateExportButtons(); showNotification('Изменения сохранены', 'success'); } function cancelEdit() { if (!originalTableData) return; if (!confirm('Отменить все изменения? Это действие нельзя отменить.')) { return; } // Восстанавливаем оригинальные данные jsonData = JSON.stringify(originalTableData, null, 2); // Обновляем отображение displayJSON(jsonData); // Показываем уведомление showNotification('Изменения отменены', 'info'); } function addNewRow() { try { const data = JSON.parse(jsonData); if (!data.table_data) return; // Получаем колонки из первой строки const columns = data.table_data.length > 0 ? Object.keys(data.table_data[0]) : ['characteristic']; // Создаем новую строку const newRow = {}; columns.forEach(col => { if (col === 'characteristic') { newRow[col] = `Новая строка ${data.table_data.length + 1}`; } else { newRow[col] = ''; } }); // Добавляем строку в данные data.table_data.push(newRow); // Обновляем данные jsonData = JSON.stringify(data, null, 2); displayJSON(jsonData); showNotification('Добавлена новая строка', 'success'); } catch (error) { console.error('Ошибка при добавлении строки:', error); showNotification('Ошибка при добавлении строки', 'error'); } } function deleteSelectedRows() { const selectedCells = document.querySelectorAll('#tableBody td.selected-cell'); const rowsToDelete = new Set(); selectedCells.forEach(cell => { const row = cell.parentElement; rowsToDelete.add(row); }); // Если нет выбранных ячеек, проверяем выбранные строки через клик if (rowsToDelete.size === 0) { const selectedRows = document.querySelectorAll('#tableBody tr.selected-row'); selectedRows.forEach(row => rowsToDelete.add(row)); } if (rowsToDelete.size === 0) { alert('Выберите строки для удаления (Ctrl+клик на ячейки или клик на номер строки)'); return; } if (!confirm(`Удалить выбранные ${rowsToDelete.size} строк?`)) { return; } try { const data = JSON.parse(jsonData); const rowsArray = Array.from(rowsToDelete); const indicesToDelete = []; // Получаем индексы строк для удаления rowsArray.forEach(row => { const rowIndex = Array.from(row.parentElement.children).indexOf(row); if (rowIndex !== -1) { indicesToDelete.push(rowIndex); } }); // Сортируем индексы по убыванию и удаляем indicesToDelete.sort((a, b) => b - a).forEach(index => { data.table_data.splice(index, 1); }); // Обновляем данные jsonData = JSON.stringify(data, null, 2); displayJSON(jsonData); showNotification(`Удалено ${indicesToDelete.length} строк`, 'success'); } catch (error) { console.error('Ошибка при удалении строк:', error); showNotification('Ошибка при удалении строк', 'error'); } } function addNewColumn() { const colName = prompt('Введите имя новой колонки (например: column_31):', 'column_31'); if (!colName) return; try { const data = JSON.parse(jsonData); // Добавляем новую колонку всем строкам data.table_data.forEach(row => { row[colName] = ''; }); // Обновляем данные jsonData = JSON.stringify(data, null, 2); displayJSON(jsonData); showNotification(`Добавлена новая колонка: ${colName}`, 'success'); } catch (error) { console.error('Ошибка при добавлении колонки:', error); showNotification('Ошибка при добавлении колонки', 'error'); } } function deleteSelectedColumns() { const selectedCells = document.querySelectorAll('#tableBody td.selected-cell'); const columnsToDelete = new Set(); selectedCells.forEach(cell => { const cellIndex = Array.from(cell.parentElement.children).indexOf(cell); if (cellIndex !== -1) { columnsToDelete.add(cellIndex); } }); // Получаем имена колонок для удаления const headers = document.querySelectorAll('#tableHeader th'); const columnNames = []; columnsToDelete.forEach(index => { if (index < headers.length) { let colName = headers[index].textContent.trim(); if (colName === 'Characteristic') { colName = 'characteristic'; } else if (colName.startsWith('Col ')) { colName = colName.toLowerCase().replace('col ', 'column_'); } columnNames.push(colName); } }); if (columnNames.length === 0) { alert('Выберите колонки для удаления (Ctrl+клик на ячейки в нужных колонках)'); return; } if (!confirm(`Удалить выбранные ${columnNames.length} колонок?`)) { return; } try { const data = JSON.parse(jsonData); // Удаляем колонки из всех строк data.table_data.forEach(row => { columnNames.forEach(colName => { delete row[colName]; }); }); // Обновляем данные jsonData = JSON.stringify(data, null, 2); displayJSON(jsonData); showNotification(`Удалено ${columnNames.length} колонок`, 'success'); } catch (error) { console.error('Ошибка при удалении колонок:', error); showNotification('Ошибка при удалении колонок', 'error'); } } function selectAllCells() { const cells = document.querySelectorAll('#tableBody td.editable-cell'); cells.forEach(cell => { cell.classList.add('selected-cell'); }); } function addEditButtonToTable() { // Удаляем старую кнопку, если есть const oldBtn = document.querySelector('.edit-mode-btn'); if (oldBtn) oldBtn.remove(); if (!jsonData) return; const editBtn = document.createElement('button'); editBtn.className = 'btn btn-primary edit-mode-btn'; editBtn.innerHTML = ' Режим редактирования'; editBtn.onclick = toggleEditMode; editBtn.style.position = 'absolute'; editBtn.style.top = '10px'; editBtn.style.right = '10px'; editBtn.style.zIndex = '10'; const tableHeader = document.querySelector('.table-header'); if (tableHeader) { tableHeader.style.position = 'relative'; tableHeader.appendChild(editBtn); } } const originalDisplayTable = displayTable; displayTable = function(tableData) { // Вызываем оригинальную функцию originalDisplayTable(tableData); // Добавляем кнопку редактирования setTimeout(() => { addEditButtonToTable(); }, 100); }; if (document.getElementById('editTableBtn')) { document.getElementById('editTableBtn').addEventListener('click', function() { toggleEditMode(); }); } function analyzeRows() { const rows = document.querySelectorAll('#tableBody tr'); const result = { total: rows.length, empty: 0, withData: 0, details: [] }; rows.forEach((row, index) => { const cells = row.querySelectorAll('td'); let isEmpty = true; const values = []; // Проверяем все ячейки кроме characteristic for (let i = 1; i < cells.length; i++) { const value = cells[i].textContent.trim(); values.push(value); if (value && value !== '—' && value !== '') { isEmpty = false; } } result.details.push({ index: index, characteristic: cells[0].textContent.trim(), isEmpty: isEmpty, values: values }); if (isEmpty) { result.empty++; } else { result.withData++; } }); console.log('Анализ строк:', result); return result; } function deleteEmptyColumns() { const table = document.getElementById('dataTable'); if (!table) { showNotification('Таблица не найдена', 'warning'); return; } const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Нет данных в таблице', 'warning'); return; } // Находим пустые колонки const emptyColumnIndices = []; // Проверяем каждую колонку (кроме characteristic) for (let colIndex = 1; colIndex < headers.length; colIndex++) { let isEmpty = true; // Проверяем все строки в этой колонке for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const cells = rows[rowIndex].querySelectorAll('td'); if (colIndex < cells.length) { const value = cells[colIndex].textContent.trim(); if (value && value !== '—' && value !== '') { isEmpty = false; break; } } } if (isEmpty) { emptyColumnIndices.push(colIndex); } } if (emptyColumnIndices.length === 0) { showNotification('Нет пустых колонок для удаления', 'info'); return; } // Подтверждение удаления if (!confirm(`Удалить ${emptyColumnIndices.length} пустых колонок? Это действие нельзя отменить!`)) { return; } try { // Получаем текущие данные const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { throw new Error('Нет табличных данных'); } // Получаем имена колонок для удаления const columnsToDelete = []; emptyColumnIndices.forEach(colIndex => { const header = headers[colIndex]; if (header) { let colName = header.textContent.trim(); if (colName.startsWith('Col ')) { colName = colName.toLowerCase().replace('col ', 'column_'); } columnsToDelete.push(colName); } }); // Удаляем колонки из всех строк data.table_data.forEach(row => { columnsToDelete.forEach(colName => { delete row[colName]; }); }); // Обновляем JSON elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); // Обновляем таблицу if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } // Обновляем глобальные данные jsonData = JSON.stringify(data); console.log(`Удалено ${emptyColumnIndices.length} пустых колонок:`, columnsToDelete); showNotification(`Удалено ${emptyColumnIndices.length} пустых колонок`, 'success'); } catch (error) { console.error('Ошибка при удалении пустых колонок:', error); showNotification('Ошибка при удалении пустых колонок', 'error'); } } function analyzeColumns() { const table = document.getElementById('dataTable'); if (!table) return null; const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); const columnAnalysis = []; // Проверяем каждую колонку for (let colIndex = 0; colIndex < headers.length; colIndex++) { const header = headers[colIndex]; const columnName = header.textContent.trim(); let totalCells = 0; let emptyCells = 0; let nonEmptyCells = 0; // Анализируем данные в колонке for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const cells = rows[rowIndex].querySelectorAll('td'); if (colIndex < cells.length) { totalCells++; const value = cells[colIndex].textContent.trim(); if (value && value !== '—' && value !== '') { nonEmptyCells++; } else { emptyCells++; } } } columnAnalysis.push({ index: colIndex, name: columnName, totalCells: totalCells, emptyCells: emptyCells, nonEmptyCells: nonEmptyCells, isEmpty: nonEmptyCells === 0, emptyPercentage: totalCells > 0 ? (emptyCells / totalCells * 100).toFixed(1) : 100 }); } console.log('Анализ колонок:', columnAnalysis); return columnAnalysis; } function showColumnStats() { const analysis = analyzeColumns(); if (!analysis) return; let message = '📊 Статистика колонок:\n\n'; analysis.forEach((col, index) => { if (index === 0) return; // Пропускаем characteristic const status = col.isEmpty ? '🔴 ПУСТАЯ' : '🟢 С данными'; message += `${index}. ${col.name}: ${status}\n`; message += ` Заполнено: ${col.nonEmptyCells}/${col.totalCells} (${col.emptyPercentage}% пустых)\n`; message += ` ${col.isEmpty ? '✅ Может быть удалена' : '❌ Не может быть удалена'}\n\n`; }); // Подсчитываем пустые колонки const emptyCols = analysis.filter(col => col.index > 0 && col.isEmpty); const totalCols = analysis.length - 1; // Минус characteristic message += `\n📈 Итоги:\n`; message += `Всего колонок: ${totalCols}\n`; message += `Пустых колонок: ${emptyCols.length}\n`; message += `Процент пустых: ${(emptyCols.length / totalCols * 100).toFixed(1)}%\n`; alert(message); if (emptyCols.length > 0) { const colNames = emptyCols.map(col => col.name).join(', '); console.log('Пустые колонки для удаления:', colNames); if (confirm(`Найдено ${emptyCols.length} пустых колонок. Удалить их?`)) { deleteEmptyColumns(); } } } function addStatsButton() { const controls = document.getElementById('tableControls'); if (!controls) return; const statsBtn = document.createElement('button'); statsBtn.className = 'btn btn-small btn-info'; statsBtn.id = 'showStatsBtn'; statsBtn.innerHTML = ' Статистика'; statsBtn.title = 'Показать статистику колонок и строк'; statsBtn.onclick = showColumnStats; const columnGroup = controls.querySelector('.control-group'); if (columnGroup) { columnGroup.appendChild(statsBtn); } } function deleteEmptyColumnsDOM() { const table = document.getElementById('dataTable'); if (!table) { showNotification('Таблица не найдена', 'warning'); return; } const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); if (rows.length === 0) { showNotification('Таблица пуста', 'warning'); return; } // Находим пустые колонки (кроме characteristic) const emptyColumnIndices = []; for (let colIndex = 1; colIndex < headers.length; colIndex++) { let isEmpty = true; for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { const cells = rows[rowIndex].querySelectorAll('td'); if (colIndex < cells.length) { const value = cells[colIndex].textContent.trim(); if (value && value !== '—' && value !== '') { isEmpty = false; break; } } } if (isEmpty) { emptyColumnIndices.push(colIndex); } } if (emptyColumnIndices.length === 0) { showNotification('Нет пустых колонок', 'info'); return; } if (!confirm(`Удалить ${emptyColumnIndices.length} пустых колонок?`)) { return; } try { // Удаляем из DOM // Заголовки emptyColumnIndices.reverse().forEach(colIndex => { const header = headers[colIndex]; if (header && header.parentNode) { header.parentNode.removeChild(header); } }); // Ячейки rows.forEach(row => { const cells = row.querySelectorAll('td'); emptyColumnIndices.forEach(colIndex => { if (colIndex < cells.length) { const cell = cells[colIndex]; if (cell && cell.parentNode) { cell.parentNode.removeChild(cell); } } }); }); // Обновляем данные JSON updateJSONFromTable(); showNotification(`Удалено ${emptyColumnIndices.length} пустых колонок`, 'success'); } catch (error) { console.error('Ошибка при удалении колонок:', error); showNotification('Ошибка при удалении колонок', 'error'); } } function updateJSONFromTable() { const table = document.getElementById('dataTable'); const headers = table.querySelectorAll('#tableHeader th'); const rows = table.querySelectorAll('#tableBody tr'); const tableData = []; // Собираем данные из таблицы rows.forEach(row => { const cells = row.querySelectorAll('td'); const rowData = {}; cells.forEach((cell, index) => { if (index === 0) { rowData.characteristic = cell.textContent.trim(); } else if (index < headers.length) { const headerText = headers[index].textContent; const columnName = headerText.startsWith('Col ') ? `column_${headerText.replace('Col ', '')}` : headerText.toLowerCase(); rowData[columnName] = cell.textContent.trim(); } }); tableData.push(rowData); }); // Обновляем JSON const data = { table_data: tableData }; jsonData = JSON.stringify(data, null, 2); elements.jsonOutput.textContent = jsonData; highlightJSON(); } function deleteEmptyColumnsSimple() { console.log('Функция deleteEmptyColumnsSimple вызвана'); try { // Получаем текущие данные const jsonText = elements.jsonOutput.textContent; const data = JSON.parse(jsonText); if (!data.table_data || !Array.isArray(data.table_data)) { showNotification('Нет табличных данных', 'warning'); return; } if (data.table_data.length === 0) { showNotification('Таблица пуста', 'warning'); return; } // Находим все уникальные колонки (кроме characteristic) const allColumns = new Set(); data.table_data.forEach(row => { Object.keys(row).forEach(key => { if (key !== 'characteristic') { allColumns.add(key); } }); }); // Находим пустые колонки const emptyColumns = []; allColumns.forEach(colName => { let isEmpty = true; data.table_data.forEach(row => { const value = row[colName]; if (value !== undefined && value !== null && value !== '' && String(value).trim() !== '') { isEmpty = false; } }); if (isEmpty) { emptyColumns.push(colName); } }); if (emptyColumns.length === 0) { showNotification('Нет пустых колонок для удаления', 'info'); return; } // Подтверждение if (!confirm(`Удалить ${emptyColumns.length} пустых колонок?\n\n${emptyColumns.join(', ')}`)) { return; } // Удаляем пустые колонки const cleanedData = data.table_data.map(row => { const newRow = { characteristic: row.characteristic }; // Копируем только непустые колонки Object.keys(row).forEach(key => { if (key !== 'characteristic' && !emptyColumns.includes(key)) { newRow[key] = row[key]; } }); return newRow; }); // Обновляем данные data.table_data = cleanedData; // Обновляем JSON elements.jsonOutput.textContent = JSON.stringify(data, null, 2); highlightJSON(); // Обновляем таблицу, если она видна if (elements.tableView.style.display === 'block') { displayTable(data.table_data); } // Сохраняем глобальные данные jsonData = JSON.stringify(data); showNotification(`Удалено ${emptyColumns.length} пустых колонок`, 'success'); console.log('Удалены колонки:', emptyColumns); } catch (error) { console.error('Ошибка при удалении пустых колонок:', error); showNotification('Ошибка при удалении колонок: ' + error.message, 'error'); } } const buttonHandlers = { // ... существующие обработчики 'deleteEmptyColumnsBtn': deleteEmptyColumnsSimple, // или deleteEmptyColumns // ... остальные обработчики }; console.log('Кнопка удаления колонок:', document.getElementById('deleteEmptyColumnsBtn')); console.log('Функция deleteEmptyColumnsSimple:', typeof deleteEmptyColumnsSimple); document.getElementById('deleteEmptyColumnsBtn')?.click(); document.addEventListener('DOMContentLoaded', function() { // Ждем полной загрузки страницы setTimeout(function() { const deleteBtn = document.getElementById('deleteEmptyColumnsBtn'); if (deleteBtn) { console.log('Кнопка "Удалить пустые колонки" найдена, добавляем прямой обработчик'); // Удаляем все старые обработчики const newBtn = deleteBtn.cloneNode(true); deleteBtn.parentNode.replaceChild(newBtn, deleteBtn); // Добавляем новый обработчик document.getElementById('deleteEmptyColumnsBtn').addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); console.log('Кнопка "Удалить пустые колонки" нажата (прямой обработчик)'); deleteEmptyColumnsSimple(); }); } }, 2000); }); // Запуск приложения document.addEventListener('DOMContentLoaded', init); // Функции экспорта таблицы function setupExportDropdown() { const exportBtn = document.getElementById('exportDropdownBtn'); const dropdown = document.getElementById('exportDropdown'); if (!exportBtn || !dropdown) return; // Показываем/скрываем dropdown exportBtn.addEventListener('click', function(e) { e.stopPropagation(); dropdown.classList.toggle('show'); }); // Закрываем dropdown при клике вне его document.addEventListener('click', function(e) { if (!dropdown.contains(e.target) && !exportBtn.contains(e.target)) { dropdown.classList.remove('show'); } }); // Обработчики для кнопок экспорта document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel); document.getElementById('exportPdfBtn')?.addEventListener('click', exportToPDF); document.getElementById('exportCsvBtn')?.addEventListener('click', exportToCSV); document.getElementById('exportJsonBtn')?.addEventListener('click', exportToJSON); } function exportToExcel() { try { const data = JSON.parse(jsonData); if (!data.table_data || !Array.isArray(data.table_data) || data.table_data.length === 0) { throw new Error('Нет табличных данных для экспорта'); } // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Подготавливаем данные const wsData = []; // Заголовки const headers = ['Characteristic']; const firstRow = data.table_data[0]; // Получаем все колонки кроме characteristic const columns = Object.keys(firstRow || {}).filter(key => key !== 'characteristic'); // Сортируем колонки по номеру columns.sort((a, b) => { const numA = parseInt(a.replace('column_', '')) || 0; const numB = parseInt(b.replace('column_', '')) || 0; return numA - numB; }); headers.push(...columns); wsData.push(headers); // Данные data.table_data.forEach(row => { const rowData = [row.characteristic || '']; columns.forEach(col => { rowData.push(row[col] || ''); }); wsData.push(rowData); }); // Создаем worksheet const ws = XLSX.utils.aoa_to_sheet(wsData); // Настраиваем ширину колонок const colWidths = headers.map((header, index) => { let maxLength = header.length; data.table_data.forEach(row => { const cellValue = index === 0 ? String(row.characteristic || '') : String(row[columns[index-1]] || ''); maxLength = Math.max(maxLength, cellValue.length); }); return { wch: Math.min(Math.max(maxLength, 10), 50) }; }); ws['!cols'] = colWidths; // Добавляем стили (если нужно) // Для более продвинутого форматирования можно использовать xlsx-style // Добавляем worksheet в книгу XLSX.utils.book_append_sheet(wb, ws, 'Table Data'); // Генерируем файл const fileName = `table_export_${new Date().toISOString().slice(0,10)}.xlsx`; XLSX.writeFile(wb, fileName); showNotification('Таблица экспортирована в Excel', 'success'); } catch (error) { console.error('Ошибка при экспорте в Excel:', error); showNotification('Ошибка экспорта: ' + error.message, 'error'); } } function exportToPDF() { try { const data = JSON.parse(jsonData); if (!data.table_data || !Array.isArray(data.table_data) || data.table_data.length === 0) { throw new Error('Нет данных для экспорта'); } // Импортируем jsPDF const { jsPDF } = window.jspdf; // Создаем PDF с поддержкой кириллицы const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }); // Добавляем поддержку кириллицы (стандартный шрифт поддерживает русские буквы) pdf.setFont("helvetica"); const pageWidth = pdf.internal.pageSize.width; const pageHeight = pdf.internal.pageSize.height; const margin = 10; let y = margin; // Заголовок - используем английский или простой текст pdf.setFontSize(16); pdf.setFont("helvetica", "bold"); pdf.text("TABLE EXPORT", pageWidth / 2, y, { align: "center" }); y += 8; // Информация на английском pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); const exportDate = new Date().toLocaleDateString('en-GB'); pdf.text(`Export date: ${exportDate}`, margin, y); pdf.text(`Total rows: ${data.table_data.length}`, pageWidth - margin, y, { align: "right" }); y += 10; // Получаем все колонки из данных const allColumns = new Set(); data.table_data.forEach(row => { Object.keys(row).forEach(key => { if (key !== 'characteristic') { allColumns.add(key); } }); }); // Сортируем колонки по номеру const sortedColumns = Array.from(allColumns).sort((a, b) => { const numA = parseInt(a.replace('column_', '')) || 0; const numB = parseInt(b.replace('column_', '')) || 0; return numA - numB; }); // Ограничиваем количество колонок для читаемости const maxColumns = Math.min(sortedColumns.length, 15); const displayColumns = sortedColumns.slice(0, maxColumns); // Рассчитываем ширину колонок const tableWidth = pageWidth - (2 * margin); const charColWidth = 35; // Ширина для характеристики const dataColWidth = (tableWidth - charColWidth - 10) / Math.max(displayColumns.length, 1); // Заголовки таблицы (на английском) const headers = ['#', 'Characteristic', ...displayColumns.map(col => `Col ${col.replace('column_', '')}` )]; // Подготавливаем данные const tableData = []; const maxRows = Math.min(data.table_data.length, 100); for (let i = 0; i < maxRows; i++) { const row = data.table_data[i]; const rowData = [ (i + 1).toString(), row.characteristic || '', ...displayColumns.map(col => row[col] || '') ]; tableData.push(rowData); } // Стили для заголовков pdf.setFillColor(44, 62, 80); // Темно-синий pdf.setTextColor(255, 255, 255); pdf.setFontSize(8); pdf.setFont("helvetica", "bold"); // Рисуем заголовки таблицы let x = margin; const headerHeight = 6; // Номер pdf.rect(x, y, 10, headerHeight, 'F'); pdf.text('#', x + 3, y + 4); x += 10; // Характеристика pdf.rect(x, y, charColWidth, headerHeight, 'F'); pdf.text('Characteristic', x + 2, y + 4); x += charColWidth; // Колонки displayColumns.forEach((col, index) => { pdf.rect(x, y, dataColWidth, headerHeight, 'F'); const colNum = col.replace('column_', ''); pdf.text(colNum, x + dataColWidth/2, y + 4, { align: 'center' }); x += dataColWidth; }); y += headerHeight; // Рисуем данные pdf.setFont("helvetica", "normal"); pdf.setFontSize(7); pdf.setTextColor(0, 0, 0); const rowHeight = 5; tableData.forEach((row, rowIndex) => { // Проверяем, нужна ли новая страница if (y + rowHeight > pageHeight - margin - 10) { pdf.addPage(); y = margin; // Рисуем заголовки на новой странице x = margin; pdf.setFillColor(44, 62, 80); pdf.setTextColor(255, 255, 255); pdf.setFont("helvetica", "bold"); // Номер pdf.rect(x, y, 10, headerHeight, 'F'); pdf.text('#', x + 3, y + 4); x += 10; // Характеристика pdf.rect(x, y, charColWidth, headerHeight, 'F'); pdf.text('Characteristic', x + 2, y + 4); x += charColWidth; // Колонки displayColumns.forEach((col, index) => { pdf.rect(x, y, dataColWidth, headerHeight, 'F'); const colNum = col.replace('column_', ''); pdf.text(colNum, x + dataColWidth/2, y + 4, { align: 'center' }); x += dataColWidth; }); y += headerHeight; // Возвращаем стили для данных pdf.setFont("helvetica", "normal"); pdf.setTextColor(0, 0, 0); } // Рисуем строку x = margin; // Чередующийся цвет фона if (rowIndex % 2 === 0) { pdf.setFillColor(248, 249, 250); // Светло-серый let fillX = margin; // Номер pdf.rect(fillX, y, 10, rowHeight, 'F'); fillX += 10; // Характеристика pdf.rect(fillX, y, charColWidth, rowHeight, 'F'); fillX += charColWidth; // Колонки displayColumns.forEach(() => { pdf.rect(fillX, y, dataColWidth, rowHeight, 'F'); fillX += dataColWidth; }); } // Номер строки pdf.text(row[0], x + 3, y + 3.5, { align: 'center' }); x += 10; // Характеристика (обрезаем если слишком длинная) const characteristic = row[1] || ''; const maxCharLength = 30; const displayChar = characteristic.length > maxCharLength ? characteristic.substring(0, maxCharLength - 3) + '...' : characteristic; pdf.text(displayChar, x + 2, y + 3.5); x += charColWidth; // Значения колонок row.slice(2).forEach((cell, cellIndex) => { const cellValue = String(cell || ''); // Устанавливаем цвет в зависимости от значения if (cellValue === '+') { pdf.setTextColor(16, 185, 129); // Green } else if (cellValue === '-') { pdf.setTextColor(239, 68, 68); // Red } else if (cellValue === '?') { pdf.setTextColor(245, 158, 11); // Orange } else if (cellValue.toUpperCase() === 'W') { pdf.setTextColor(139, 92, 246); // Purple } else if (cellValue.toUpperCase() === 'ND') { pdf.setTextColor(156, 163, 175); // Gray } else { pdf.setTextColor(0, 0, 0); // Black } // Центрируем текст pdf.text(cellValue, x + dataColWidth/2, y + 3.5, { align: 'center' }); x += dataColWidth; // Сбрасываем цвет pdf.setTextColor(0, 0, 0); }); y += rowHeight; }); // Легенда на английском y = pageHeight - margin - 10; pdf.setFontSize(7); pdf.setTextColor(102, 102, 102); pdf.text("Legend: + (positive) - (negative) ? (unknown) W (weak positive) ND (no data)", margin, y); // Номер страницы pdf.text("Page 1", pageWidth - margin, pageHeight - margin, { align: "right" }); // Сохраняем PDF const fileName = `table_export_${new Date().toISOString().slice(0, 10)}.pdf`; pdf.save(fileName); showNotification('PDF successfully created', 'success'); } catch (error) { console.error('Error creating PDF:', error); showNotification('Error: ' + error.message, 'error'); } } // Функция для рисования таблицы function drawTable(pdf, tableData, headers, options) { const { startX, startY, tableWidth, charColWidth, dataColWidth, headerStyle, cellStyle, margin, pageHeight } = options; const colCount = headers.length; const rowHeight = 6; let x = startX; let y = startY; let currentPage = 1; // Рисуем заголовки pdf.setFillColor(...headerStyle.fillColor); pdf.setTextColor(...headerStyle.textColor); pdf.setFont("helvetica", headerStyle.fontStyle); pdf.setFontSize(headerStyle.fontSize); headers.forEach((header, colIndex) => { // Рассчитываем ширину колонки let colWidth; if (colIndex === 0) { colWidth = 8; // Для номера } else if (colIndex === 1) { colWidth = charColWidth; // Для характеристики } else { colWidth = dataColWidth; // Для остальных колонок } // Рисуем ячейку заголовка pdf.rect(x, y, colWidth, rowHeight, 'F'); // Текст заголовка const text = colIndex === 0 ? '№' : colIndex === 1 ? 'Characteristic' : header; const maxTextWidth = colWidth - 4; // Минус отступы const textWidth = pdf.getTextWidth(text); let displayText = text; if (textWidth > maxTextWidth) { // Укорачиваем текст если не помещается for (let i = text.length - 1; i > 0; i--) { const shortText = text.substring(0, i) + '...'; if (pdf.getTextWidth(shortText) <= maxTextWidth) { displayText = shortText; break; } } } pdf.text(displayText, x + 2, y + rowHeight - 2); x += colWidth; }); y += rowHeight; // Рисуем данные pdf.setFont("helvetica", "normal"); pdf.setFontSize(cellStyle.fontSize); pdf.setTextColor(...cellStyle.textColor); tableData.forEach((row, rowIndex) => { // Проверяем, нужна ли новая страница if (y + rowHeight > pageHeight - margin - 10) { pdf.addPage(); currentPage++; y = margin; // Повторяем заголовки на новой странице x = startX; pdf.setFillColor(...headerStyle.fillColor); pdf.setTextColor(...headerStyle.textColor); pdf.setFont("helvetica", headerStyle.fontStyle); pdf.setFontSize(headerStyle.fontSize); headers.forEach((header, colIndex) => { let colWidth; if (colIndex === 0) { colWidth = 8; } else if (colIndex === 1) { colWidth = charColWidth; } else { colWidth = dataColWidth; } pdf.rect(x, y, colWidth, rowHeight, 'F'); const text = colIndex === 0 ? '№' : colIndex === 1 ? 'Characteristic' : header; const maxTextWidth = colWidth - 4; const textWidth = pdf.getTextWidth(text); let displayText = text; if (textWidth > maxTextWidth) { for (let i = text.length - 1; i > 0; i--) { const shortText = text.substring(0, i) + '...'; if (pdf.getTextWidth(shortText) <= maxTextWidth) { displayText = shortText; break; } } } pdf.text(displayText, x + 2, y + rowHeight - 2); x += colWidth; }); y += rowHeight; } // Рисуем строку данных x = startX; row.forEach((cell, colIndex) => { // Рассчитываем ширину колонки let colWidth; if (colIndex === 0) { colWidth = 8; } else if (colIndex === 1) { colWidth = charColWidth; } else { colWidth = dataColWidth; } // Чередуем цвет фона строк if (rowIndex % 2 === 0) { pdf.setFillColor(248, 249, 250); pdf.rect(x, y, colWidth, rowHeight, 'F'); } // Рисуем границу pdf.setDrawColor(200, 200, 200); pdf.rect(x, y, colWidth, rowHeight); // Устанавливаем цвет текста в зависимости от значения const cellValue = String(cell || ''); if (cellValue === '+') { pdf.setTextColor(16, 185, 129); // Зеленый } else if (cellValue === '-') { pdf.setTextColor(239, 68, 68); // Красный } else if (cellValue === '?') { pdf.setTextColor(245, 158, 11); // Оранжевый } else if (cellValue.toUpperCase() === 'W') { pdf.setTextColor(139, 92, 246); // Фиолетовый } else if (cellValue.toUpperCase() === 'ND') { pdf.setTextColor(156, 163, 175); // Серый } else { pdf.setTextColor(0, 0, 0); // Черный } // Отображаем текст с переносом const maxTextWidth = colWidth - 4; let displayText = cellValue; // Если текст слишком длинный, обрезаем его if (pdf.getTextWidth(cellValue) > maxTextWidth) { for (let i = cellValue.length - 1; i > 0; i--) { const shortText = cellValue.substring(0, i); if (pdf.getTextWidth(shortText) <= maxTextWidth) { displayText = shortText; break; } } } // Центрируем текст для числовых колонок, левый край для характеристик const textX = colIndex <= 1 ? x + 2 : x + (colWidth / 2); const align = colIndex <= 1 ? 'left' : 'center'; pdf.text(displayText, textX, y + rowHeight - 2, { align: align }); x += colWidth; }); y += rowHeight; // Сбрасываем цвет текста pdf.setTextColor(0, 0, 0); }); // Информация о страницах pdf.setFontSize(7); pdf.setTextColor(102, 102, 102); pdf.text(`Страница ${currentPage}`, margin, pageHeight - margin); } // Альтернативная упрощенная версия для больших таблиц function exportToPDFSimple() { try { const data = JSON.parse(jsonData); if (!data.table_data || !Array.isArray(data.table_data) || data.table_data.length === 0) { throw new Error('Нет данных для экспорта'); } // Используем jsPDF autotable если доступен if (typeof pdf.autoTable !== 'undefined') { exportWithAutoTable(data); return; } const { jsPDF } = window.jspdf; const pdf = new jsPDF('l', 'mm', 'a4'); const margin = 15; let y = margin; // Заголовок pdf.setFontSize(18); pdf.setFont("helvetica", "bold"); pdf.text("ЭКСПОРТ ТАБЛИЦЫ", 148.5, y, { align: "center" }); y += 8; pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); pdf.text(`Экспортировано: ${new Date().toLocaleString()}`, margin, y); pdf.text(`Строк: ${data.table_data.length}`, 280, y, { align: "right" }); y += 15; // Получаем колонки const columns = []; const sampleRow = data.table_data[0]; Object.keys(sampleRow).forEach(key => { if (key !== 'characteristic') { const colNum = parseInt(key.replace('column_', '')) || 0; columns.push({ key, num: colNum }); } }); columns.sort((a, b) => a.num - b.num); const displayColumns = columns.slice(0, 15); // Ограничиваем 15 колонками // Создаем заголовки const headers = ['№', 'Characteristic', ...displayColumns.map(col => `C${col.num}`)]; // Создаем данные const tableData = []; const maxRows = Math.min(data.table_data.length, 100); for (let i = 0; i < maxRows; i++) { const row = data.table_data[i]; const rowData = [ (i + 1).toString(), row.characteristic || '', ...displayColumns.map(col => row[col.key] || '') ]; tableData.push(rowData); } // Простая таблица без сложной логики const colWidths = [10, 40, ...Array(displayColumns.length).fill(15)]; const rowHeight = 6; // Заголовки pdf.setFillColor(44, 62, 80); pdf.setTextColor(255, 255, 255); pdf.setFontSize(9); let x = margin; headers.forEach((header, i) => { pdf.rect(x, y, colWidths[i], rowHeight, 'F'); pdf.text(header.substring(0, 8), x + 2, y + 4); x += colWidths[i]; }); y += rowHeight; // Данные pdf.setFontSize(8); pdf.setTextColor(0, 0, 0); tableData.forEach((row, rowIndex) => { if (y > 190) { // Конец страницы pdf.addPage(); y = margin; // Повторяем заголовки x = margin; pdf.setFillColor(44, 62, 80); pdf.setTextColor(255, 255, 255); headers.forEach((header, i) => { pdf.rect(x, y, colWidths[i], rowHeight, 'F'); pdf.text(header.substring(0, 8), x + 2, y + 4); x += colWidths[i]; }); y += rowHeight; pdf.setFontSize(8); pdf.setTextColor(0, 0, 0); } // Цвет фона для четных строк if (rowIndex % 2 === 0) { x = margin; pdf.setFillColor(248, 249, 250); colWidths.forEach(width => { pdf.rect(x, y, width, rowHeight, 'F'); x += width; }); } // Текст x = margin; row.forEach((cell, cellIndex) => { // Форматирование значений let displayCell = String(cell || ''); if (displayCell.length > 8) { displayCell = displayCell.substring(0, 7) + '...'; } pdf.text(displayCell, cellIndex === 1 ? x + 2 : x + colWidths[cellIndex] / 2, y + 4, { align: cellIndex === 1 ? 'left' : 'center' } ); x += colWidths[cellIndex]; }); y += rowHeight; }); // Сохраняем pdf.save(`table_${Date.now()}.pdf`); showNotification('PDF успешно создан', 'success'); } catch (error) { console.error('Ошибка при создании PDF:', error); showNotification('Ошибка: ' + error.message, 'error'); } } // Если у вас есть autotable, используйте эту функцию function exportWithAutoTable(data) { const { jsPDF } = window.jspdf; const pdf = new jsPDF('l', 'mm', 'a4'); // Получаем колонки const columns = []; const sampleRow = data.table_data[0]; Object.keys(sampleRow).forEach(key => { if (key !== 'characteristic') { const colNum = parseInt(key.replace('column_', '')) || 0; columns.push({ key, num: colNum, name: `C${colNum}` }); } }); columns.sort((a, b) => a.num - b.num); const displayColumns = columns.slice(0, 20); // Заголовки const headers = ['№', 'Characteristic', ...displayColumns.map(col => col.name)]; // Данные const tableData = []; const maxRows = Math.min(data.table_data.length, 200); for (let i = 0; i < maxRows; i++) { const row = data.table_data[i]; const rowData = [ (i + 1).toString(), row.characteristic || '', ...displayColumns.map(col => row[col.key] || '') ]; tableData.push(rowData); } // Создаем таблицу pdf.autoTable({ head: [headers], body: tableData, startY: 20, theme: 'grid', styles: { fontSize: 7, cellPadding: 2, overflow: 'linebreak', lineColor: [200, 200, 200], lineWidth: 0.1 }, headStyles: { fillColor: [44, 62, 80], textColor: [255, 255, 255], fontStyle: 'bold', fontSize: 8 }, alternateRowStyles: { fillColor: [248, 249, 250] }, columnStyles: { 0: { cellWidth: 10, halign: 'center' }, 1: { cellWidth: 40, halign: 'left' } }, margin: { top: 20 }, didDrawPage: function(data) { // Заголовок на каждой странице pdf.setFontSize(10); pdf.text(`Страница ${data.pageNumber}`, data.settings.margin.left, 10); } }); // Сохраняем pdf.save(`table_autotable_${Date.now()}.pdf`); showNotification('PDF с использованием AutoTable создан', 'success'); } // Альтернативная функция для создания простого PDF function createSimplePDF(data) { try { const pdf = new window.jspdf.jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' }); // Заголовок pdf.setFontSize(16); pdf.setTextColor(44, 62, 80); pdf.text('Экспорт таблицы', 20, 20); pdf.setFontSize(10); pdf.setTextColor(127, 140, 141); pdf.text(`Создано: ${new Date().toLocaleString()}`, 20, 30); // Получаем колонки const columns = new Set(); data.table_data.forEach(row => { Object.keys(row).forEach(key => { if (key !== 'characteristic') { columns.add(key); } }); }); const sortedColumns = Array.from(columns).sort((a, b) => { const numA = parseInt(a.replace('column_', '')) || 0; const numB = parseInt(b.replace('column_', '')) || 0; return numA - numB; }); // Ограничиваем количество колонок для читаемости const displayColumns = sortedColumns.slice(0, 15); const colWidth = 180 / (displayColumns.length + 1); // мм на колонку // Заголовки таблицы pdf.setFillColor(44, 62, 80); pdf.setTextColor(255, 255, 255); pdf.rect(20, 40, 180, 8, 'F'); pdf.setFontSize(9); pdf.text('Characteristic', 22, 46); displayColumns.forEach((col, index) => { pdf.text(`Col ${col.replace('column_', '')}`, 22 + (colWidth * (index + 1)), 46); }); // Данные таблицы (ограничиваем количество строк) const maxRows = Math.min(data.table_data.length, 50); pdf.setFontSize(8); pdf.setTextColor(0, 0, 0); for (let i = 0; i < maxRows; i++) { const row = data.table_data[i]; const yPos = 50 + (i * 5); // Рисуем строку if (i % 2 === 0) { pdf.setFillColor(248, 249, 250); pdf.rect(20, yPos - 4, 180, 5, 'F'); } // Характеристика const characteristic = row.characteristic || ''; pdf.text(characteristic.substring(0, 30), 22, yPos); // Значения колонок displayColumns.forEach((col, index) => { const value = row[col] || ''; pdf.text(value, 22 + (colWidth * (index + 1)), yPos); }); } // Информация pdf.setFontSize(8); pdf.setTextColor(102, 102, 102); pdf.text(`Показано ${maxRows} из ${data.table_data.length} строк`, 20, 50 + (maxRows * 5) + 10); // Сохраняем PDF const fileName = `table_simple_${new Date().toISOString().slice(0,10)}.pdf`; pdf.save(fileName); showNotification('Таблица экспортирована в упрощенный PDF', 'info'); } catch (error) { console.error('Ошибка при создании простого PDF:', error); showNotification('Не удалось создать PDF файл', 'error'); } } function exportToCSV() { try { const data = JSON.parse(jsonData); if (!data.table_data || !Array.isArray(data.table_data)) { throw new Error('Нет табличных данных для экспорта'); } // Создаем CSV let csv = ''; // Находим все колонки const allColumns = new Set(); data.table_data.forEach(row => { Object.keys(row).forEach(key => { if (key !== 'characteristic') { allColumns.add(key); } }); }); // Сортируем колонки const sortedColumns = Array.from(allColumns).sort((a, b) => { const numA = parseInt(a.replace('column_', '')) || 0; const numB = parseInt(b.replace('column_', '')) || 0; return numA - numB; }); // Заголовки const headers = ['Characteristic', ...sortedColumns]; csv += headers.map(h => `"${h}"`).join(',') + '\n'; // Данные data.table_data.forEach(row => { const rowData = [row.characteristic || '']; sortedColumns.forEach(col => { rowData.push(row[col] || ''); }); csv += rowData.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',') + '\n'; }); // Создаем и скачиваем файл const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `table_export_${new Date().toISOString().slice(0,10)}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification('Таблица экспортирована в CSV', 'success'); } catch (error) { console.error('Ошибка при экспорте в CSV:', error); showNotification('Ошибка экспорта: ' + error.message, 'error'); } } function exportToJSON() { try { if (!jsonData) { throw new Error('Нет данных для экспорта'); } const data = JSON.parse(jsonData); const formattedData = JSON.stringify(data, null, 2); const blob = new Blob([formattedData], { type: 'application/json' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `table_export_${new Date().toISOString().slice(0,10)}.json`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification('Таблица экспортирована в JSON', 'success'); } catch (error) { console.error('Ошибка при экспорте в JSON:', error); showNotification('Ошибка экспорта: ' + error.message, 'error'); } } // Улучшенная функция для Excel с форматированием function exportToExcelWithFormatting() { try { const data = JSON.parse(jsonData); if (!data.table_data || !Array.isArray(data.table_data)) { throw new Error('Нет табличных данных для экспорта'); } // Создаем новую книгу const wb = XLSX.utils.book_new(); // Подготавливаем данные const wsData = []; // Заголовки const headers = ['№', 'Characteristic']; const columns = []; // Находим все колонки data.table_data.forEach(row => { Object.keys(row).forEach(key => { if (key !== 'characteristic' && !columns.includes(key)) { columns.push(key); } }); }); // Сортируем колонки columns.sort((a, b) => { const numA = parseInt(a.replace('column_', '')) || 0; const numB = parseInt(b.replace('column_', '')) || 0; return numA - numB; }); headers.push(...columns); wsData.push(headers); // Данные с номерами строк data.table_data.forEach((row, index) => { const rowData = [index + 1, row.characteristic || '']; columns.forEach(col => { rowData.push(row[col] || ''); }); wsData.push(rowData); }); // Создаем worksheet const ws = XLSX.utils.aoa_to_sheet(wsData); // Автоматическая ширина колонок const colWidths = headers.map((header, idx) => { let maxLength = header.length; wsData.forEach((row, rowIdx) => { if (rowIdx > 0) { // Пропускаем заголовки const cellValue = String(row[idx] || ''); maxLength = Math.max(maxLength, cellValue.length); } }); return { wch: Math.min(Math.max(maxLength + 2, 10), 30) }; }); ws['!cols'] = colWidths; // Добавляем фильтры ws['!autofilter'] = { ref: XLSX.utils.encode_range({ s: { r: 0, c: 0 }, e: { r: wsData.length - 1, c: headers.length - 1 } })}; // Добавляем закрепление первой строки ws['!freeze'] = { xSplit: 0, ySplit: 1, topLeftCell: 'A2', activePane: 'bottomRight' }; // Добавляем worksheet в книгу XLSX.utils.book_append_sheet(wb, ws, 'Table Data'); // Добавляем информационный лист const infoData = [ ['Информация об экспорте'], [''], ['Дата экспорта:', new Date().toLocaleString()], ['Количество строк:', data.table_data.length], ['Количество колонок:', columns.length + 1], [''], ['Обозначения:'], ['+', 'Положительный результат'], ['-', 'Отрицательный результат'], ['?', 'Неопределенный результат'], ['W', 'Слабоположительный'], ['ND', 'Нет данных'] ]; const infoWs = XLSX.utils.aoa_to_sheet(infoData); infoWs['!cols'] = [{ wch: 20 }, { wch: 40 }]; XLSX.utils.book_append_sheet(wb, infoWs, 'Информация'); // Сохраняем файл const fileName = `table_${new Date().toISOString().slice(0,10)}.xlsx`; XLSX.writeFile(wb, fileName); showNotification('Файл Excel успешно создан', 'success'); } catch (error) { console.error('Ошибка при создании Excel файла:', error); showNotification('Ошибка: ' + error.message, 'error'); } } // Функция для включения/выключения кнопок экспорта function updateExportButtons() { const exportBtn = document.getElementById('exportDropdownBtn'); if (!exportBtn) return; // Проверяем, есть ли данные для экспорта const hasData = jsonData && jsonData !== '{"table_data": []}' && jsonData !== ''; console.log('updateExportButtons вызвана. hasData:', hasData, 'jsonData:', jsonData?.substring(0, 100)); // Включаем/выключаем кнопку exportBtn.disabled = !hasData; // Добавляем визуальную обратную связь if (hasData) { exportBtn.title = 'Экспорт табличных данных'; exportBtn.classList.remove('disabled'); } else { exportBtn.title = 'Нет данных для экспорта'; exportBtn.classList.add('disabled'); } } // Обновляем функцию displayJSON для включения кнопок экспорта const originalDisplayJSON = displayJSON; displayJSON = function(jsonStr) { originalDisplayJSON(jsonStr); updateExportButtons(); }; function forceEnableExport() { const exportBtn = document.getElementById('exportDropdownBtn'); if (exportBtn) { exportBtn.disabled = false; console.log('Кнопка экспорта принудительно включена'); } } // Добавьте эту функцию в обработчик клика по кнопке редактирования if (document.getElementById('editTableBtn')) { document.getElementById('editTableBtn').addEventListener('click', function() { // Включаем кнопку экспорта при входе в режим редактирования forceEnableExport(); toggleEditMode(); }); }