Поговорим о...
|
|
// Функция загрузки данных скважины из мульти-файла async function parseWellDataFromMulti(wellId, signal = null) { // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
if (!currentMultiWellData || !currentMultiWellData.wells) { throw new Error('Данные мульти-файла не загружены'); }
// ПРОВЕРЯЕМ КЕШ с защитой от ошибок const cachedData = getCachedWellData(wellId); if (cachedData) { console.log('Используем кешированные данные для скважины:', wellId); // ПРОВЕРЯЕМ отмену операции перед возвратом данных if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } xmlData = cachedData; return; }
showLoading('Загрузка данных скважины...');
try { updateProgress(10, 'Поиск информации о скважине...'); // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } const targetWellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId); if (!targetWellMeta) { throw new Error(`Скважина с ID ${wellId} не найдена в списке`); }
let xmlContent = ''; let sourceFile = '';
// Случай 1: ZIP архив (данные в fileCache) if (targetWellMeta.sourceFile) { sourceFile = targetWellMeta.sourceFile; updateProgress(30, 'Загрузка данных из кеша...'); // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } xmlContent = fileCache.get(sourceFile); if (!xmlContent) { throw new Error(`Файл ${sourceFile} не найден в кеше`); } } // Случай 2: Отдельный INU файл (данные в currentMultiWellData.xmlContent) else if (currentMultiWellData.xmlContent) { updateProgress(30, 'Загрузка данных из текущего файла...'); xmlContent = currentMultiWellData.xmlContent; sourceFile = 'current_inu_file'; } // Случай 3: Не удалось определить источник else { throw new Error('Не удалось определить источник данных для скважины'); }
updateProgress(50, 'Быстрая обработка всех данных...');
// ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
// ИСПОЛЬЗУЕМ БЫСТРЫЙ МЕТОД ДЛЯ ВСЕХ ДАННЫХ console.time(`Parse ALL data for ${wellId}`); const allWellData = await parseAllWellDataFast(xmlContent, wellId); console.timeEnd(`Parse ALL data for ${wellId}`);
// ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
if (allWellData) { // ВАЖНО: Сохраняем wellId в данных для корректного отображения allWellData.wellId = wellId; xmlData = allWellData; // Сохраняем в кеш с защитой от ошибок setCachedWellData(wellId, xmlData); updateProgress(100, 'Готово!'); // ПРОВЕРЯЕМ отмену операции перед завершением if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } setTimeout(() => { hideLoading(); }, 50); return; }
throw new Error('Не удалось извлечь данные скважины');
} catch (error) { // Игнорируем ошибки отмены if (error.name === 'AbortError') { throw error; } hideLoading(); console.error('Ошибка загрузки данных скважины:', error); // Очищаем кеш для этой скважины при ошибке try { wellDataCache.delete(wellId); } catch (e) { console.error('Ошибка очистки кеша:', e); } throw error; } }
// Вспомогательные функции для кеша function getCachedWellData(wellId) { try { return wellDataCache.get(wellId); } catch (error) { console.error('Ошибка доступа к кешу данных:', error); return null; } }
function setCachedWellData(wellId, data) { try { wellDataCache.set(wellId, data); } catch (error) { console.error('Ошибка сохранения в кеш:', error); } }
|
Понял! Нужно доработать логику для файлов 906, 911 и других, чтобы они тоже обрабатывались как мульти-файлы при наличии нескольких скважин. Вот полная реализация:
1. Обновленная функция parseSingleWellData с определением мульти-скважинности
```javascript // Функция для парсинга одиночных файлов с поддержкой мульти-скважин async function parseSingleWellData(xmlDoc, fileType, fileName = null) { try { // Считаем количество уникальных скважин в файле const wellPoints = xmlDoc.getElementsByTagName('MR_DBA.POINT_OBSERV_POAA'); const uniqueWellIds = new Set(); const wellsMetadata = []; for (let i = 0; i < wellPoints.length; i++) { const wellId = getSafeTextContent(wellPoints[i], 'ID_POINT_OBSERV_POAA'); if (wellId && wellId !== 'Не указано') { uniqueWellIds.add(wellId); // Собираем метаданные для списка скважин const wellName = getSafeTextContent(wellPoints[i], 'NAME_FULL_POAA') || 'Без названия'; const object = getSafeTextContent(wellPoints[i], 'ID_OBJECT_WORK_POAA') || 'Не указан'; const description = getSafeTextContent(wellPoints[i], 'DESCRIPTION_POAA') || ''; // Получаем номер линии для этой скважины const lineId = getSafeTextContent(wellPoints[i], 'ID_LINE_COMMON_POAA'); let lineNumber = 'Не указано'; if (lineId) { const lineCommon = xmlDoc.getElementsByTagName('MR_DBA.LINE_COMMON_LNCM'); for (let j = 0; j < lineCommon.length; j++) { const line = lineCommon[j]; const currentLineId = getSafeTextContent(line, 'ID_LINE_COMMON_LNCM'); if (currentLineId === lineId) { lineNumber = getSafeTextContent(line, 'NAME_FULL_LNCM') || 'Не указано'; break; } } } // Форматируем отображаемое название const displayName = formatWellDisplayName(wellName, lineNumber); wellsMetadata.push({ wellId: wellId, wellName: wellName, displayName: displayName, object: object, description: description, lineNumber: lineNumber }); } } const wellCount = uniqueWellIds.size; console.log(`Найдено уникальных скважин в файле ${fileName}: ${wellCount}`);
// Если скважин больше одной, обрабатываем как мульти-файл if (wellCount > 1 && (fileType === 'all' || fileType === 'final' || fileType === 'primary' || fileType === 'unknown')) { console.log(`Файл ${fileType} (${fileName}) содержит ${wellCount} скважин, обрабатываем как мульти-файл`); if (wellsMetadata.length > 0) { return { isMulti: true, wells: wellsMetadata, wellCount: wellCount }; } }
// Если скважина одна или это специальный тип файла - парсим как одиночную console.log(`Обработка файла ${fileName} как одиночной скважины`); // Извлекаем данные о скважине (берем первую найденную) const pointObserv = wellPoints[0]; if (!pointObserv) { throw new Error('Не найдены данные о скважине в файле'); } const objectWork = xmlDoc.getElementsByTagName('MR_DBA.OBJECT_WORK_OBWA')[0]; const ground = xmlDoc.getElementsByTagName('MR_DBA.GROUND_GRNA')[0]; const pointObservType = xmlDoc.getElementsByTagName('MR_DBA.POINT_OBSERV_TYPE_PNOT')[0]; const lineCommon = xmlDoc.getElementsByTagName('MR_DBA.LINE_COMMON_LNCM')[0]; // Основная информация о скважине const wellName = getSafeTextContent(pointObserv, 'NAME_FULL_POAA'); const object = getSafeTextContent(objectWork, 'NAME_FULL_OBWA'); const area = getSafeTextContent(ground, 'NAME_FULL_GRNA'); const wellType = getSafeTextContent(pointObservType, 'NAME_SHORT_PNOT'); const lineNumber = getSafeTextContent(lineCommon, 'NAME_FULL_LNCM'); const wellDescription = getSafeTextContent(pointObserv, 'DESCRIPTION_POAA'); // Даты бурения и бригада let drillingStartDate = ''; let drillingEndDate = ''; let drillingBrigade = 'Не указано'; drillingStartDate = formatDate(getSafeTextContent(pointObserv, 'DATE_DRIFTING_BEG_POAA')); drillingEndDate = formatDate(getSafeTextContent(pointObserv, 'DATE_DRIFTING_END_POAA')); // Получаем название бригады const brigadeId = getSafeTextContent(pointObserv, 'BRIGADE_POAA'); if (brigadeId && brigadeId !== 'Не указано') { const divisionNodes = xmlDoc.getElementsByTagName('MR_DBA.OUR_DIVISION'); for (let i = 0; i < divisionNodes.length; i++) { const division = divisionNodes[i]; const divisionId = getSafeTextContent(division, 'ID_CONTRACTOR'); if (divisionId === brigadeId) { const brigadeName = getSafeTextContent(division, 'NAME_SHORT'); if (brigadeName && brigadeName !== 'Не указано') { drillingBrigade = brigadeName; break; } } } if (drillingBrigade === 'Не указано') { drillingBrigade = `Бригада ${brigadeId}`; } } // Создаем карту сотрудников const employeeMap = new Map(); const employeeNodes = xmlDoc.getElementsByTagName('MR_DBA.OUR_EMPLOYEE'); for (let i = 0; i < employeeNodes.length; i++) { const employee = employeeNodes[i]; const employeeId = getSafeTextContent(employee, 'ID_CONTRACTOR'); const employeeName = getSafeTextContent(employee, 'NAME_SHORT'); if (employeeId && employeeName && employeeId !== 'Не указано' && employeeName !== 'Не указано') { employeeMap.set(employeeId, employeeName); } } // Данные документирования let primaryDocInfo = null; let finalDocInfo = null; let gisDocInfo = null; const docNodes = xmlDoc.getElementsByTagName('MR_DBA.PO_DOCUM_PODC'); for (let i = 0; i < docNodes.length; i++) { const doc = docNodes[i]; const docType = getSafeTextContent(doc, 'ID_TYPE_DOCUM_PODC'); const startDate = formatDate(getSafeTextContent(doc, 'DATE_BEG_DOCUM_PODC')); const endDate = formatDate(getSafeTextContent(doc, 'DATE_END_DOCUM_PODC')); const depth = formatNumber(getSafeTextContent(doc, 'DEPTH_FACT_PODC')); const authorId = getSafeTextContent(doc, 'AUTHOR_PODC'); let authorName = 'Не указано'; if (authorId && authorId !== 'Не указано' && employeeMap.has(authorId)) { authorName = employeeMap.get(authorId); } else if (authorId && authorId !== 'Не указано') { authorName = `Код: ${authorId}`; } const docInfo = { startDate, endDate, depth, author: authorName }; if (docType.includes('Первич') || docType.includes('ПЕРВИЧ') || docType === '1') { primaryDocInfo = docInfo; } else if (docType.includes('Итогов') || docType.includes('ИТОГОВ') || docType === '2') { finalDocInfo = docInfo; } else if (docType.includes('ГИС') || docType.includes('геофизик')) { gisDocInfo = docInfo; } } // Создаем карту типов опробования const assayTypeMap = new Map(); const assayTypeNodes = xmlDoc.getElementsByTagName('MR_DBA.ASSAY_TYPE_PROP_ASTR'); for (let i = 0; i < assayTypeNodes.length; i++) { const assayType = assayTypeNodes[i]; const id = getSafeTextContent(assayType, 'ID_ASSAY_TYPE_PROP_ASTR'); const name = getSafeTextContent(assayType, 'NAME_FULL_ASTR'); if (id && name && id !== 'Не указано' && name !== 'Не указано') { assayTypeMap.set(id, name); } } // Создаем карту типов документирования для интервалов const docTypeMap = new Map(); const docNodesForIntervals = xmlDoc.getElementsByTagName('MR_DBA.PO_DOCUM_PODC'); for (let i = 0; i < docNodesForIntervals.length; i++) { const doc = docNodesForIntervals[i]; const docId = getSafeTextContent(doc, 'ID_PO_DOCUM_PODC'); const docType = getSafeTextContent(doc, 'ID_TYPE_DOCUM_PODC'); if (docId && docType && docId !== 'Не указано' && docType !== 'Не указано') { docTypeMap.set(docId, docType); } } // Создаем карту классификаторов const classifierMap = new Map(); const classifierNodes = xmlDoc.getElementsByTagName('MR_DBA.CLASSIFIER'); for (let i = 0; i < classifierNodes.length; i++) { const classifier = classifierNodes[i]; const nodeId = getSafeTextContent(classifier, 'ID_NODE_ID'); const nodeName = getSafeTextContent(classifier, 'NODE_NAME'); if (nodeId && nodeName && nodeId !== 'Не указано' && nodeName !== 'Не указано') { classifierMap.set(nodeId, nodeName); } } // Создаем карту единиц измерения const measureMap = new Map(); const measureNodes = xmlDoc.getElementsByTagName('MR_DBA.MEASURE'); for (let i = 0; i < measureNodes.length; i++) { const measure = measureNodes[i]; const measureId = getSafeTextContent(measure, 'ID_MEASURE'); const measureName = getSafeTextContent(measure, 'NAME_SHORT'); if (measureId && measureName && measureId !== 'Не указано' && measureName !== 'Не указано') { measureMap.set(measureId, measureName); } } // Извлекаем интервалы документирования для текущей скважины const currentWellId = getSafeTextContent(pointObserv, 'ID_POINT_OBSERV_POAA'); const primaryIntervals = []; const finalIntervals = []; const gisIntervals = [];
// Обрабатываем интервалы документирования const intervalNodes = xmlDoc.getElementsByTagName('MR_DBA.PO_DOCUM_INT_PODI'); for (let i = 0; i < intervalNodes.length; i++) { const interval = intervalNodes[i]; const docId = getSafeTextContent(interval, 'ID_PO_DOCUM_PODI'); // Проверяем, что интервал принадлежит текущей скважине let belongsToCurrentWell = false; if (docId && docId !== 'Не указано') { // Ищем документирование для этого интервала for (let j = 0; j < docNodes.length; j++) { const doc = docNodes[j]; const currentDocId = getSafeTextContent(doc, 'ID_PO_DOCUM_PODC'); const docWellId = getSafeTextContent(doc, 'ID_POINT_OBSERV_PODC'); if (currentDocId === docId && docWellId === currentWellId) { belongsToCurrentWell = true; break; } } } if (!belongsToCurrentWell) continue; const from = parseFloat(getSafeTextContent(interval, 'PO_DOCUM_FROM_PODI') || '0'); const to = parseFloat(getSafeTextContent(interval, 'PO_DOCUM_TO_PODI') || '0'); const thickness = parseFloat(getSafeTextContent(interval, 'THICKNESS_PODI') || '0'); const description = getSafeTextContent(interval, 'DESCRIPTION_PODI'); const numberPP = parseInt(getSafeTextContent(interval, 'NUMBER_PP_PODI') || '0'); // Определяем тип документирования let documentationType = 'Не определено'; if (docId && docId !== 'Не указано' && docTypeMap.has(docId)) { documentationType = docTypeMap.get(docId); } // Получаем литологию const lithologyId = getSafeTextContent(interval, 'ID_LITHOLOGY_PODI'); let lithology = 'Не указано'; if (lithologyId && classifierMap.has(lithologyId)) { lithology = classifierMap.get(lithologyId); } // Получаем стратиграфию const stratigraphyId = getSafeTextContent(interval, 'ID_STRATIGRAPHY_PODI'); let stratigraphy = 'Не указано'; if (stratigraphyId && classifierMap.has(stratigraphyId)) { stratigraphy = convertStratigraphyText(classifierMap.get(stratigraphyId)); } const intervalData = { numberPP, from, to, thickness, stratigraphy, lithology, description }; // Распределяем интервалы по типам документирования if ((documentationType.includes('Первич') || documentationType.includes('ПЕРВИЧ') || documentationType === '1') && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) { primaryIntervals.push(intervalData); } else if ((documentationType.includes('Итогов') || documentationType.includes('ИТОГОВ') || documentationType === '2') && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) { finalIntervals.push(intervalData); } else if (documentationType.includes('ГИС') || documentationType.includes('геофизик')) { gisIntervals.push(intervalData); } else { if (fileType === 'all' && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) { primaryIntervals.push(intervalData); finalIntervals.push(intervalData); } else if (fileType === 'primary' && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) { primaryIntervals.push(intervalData); } else if (fileType === 'final' && !documentationType.includes('ГИС') && !documentationType.includes('геофизик')) { finalIntervals.push(intervalData); } } } // СОРТИРУЕМ интервалы по порядковому номеру primaryIntervals.sort((a, b) => a.numberPP - b.numberPP); finalIntervals.sort((a, b) => a.numberPP - b.numberPP); gisIntervals.sort((a, b) => a.numberPP - b.numberPP); // Извлекаем данные опробования для текущей скважины const assays = []; const assayNodes = xmlDoc.getElementsByTagName('MR_DBA.ASSAY_ASSA'); for (let i = 0; i < assayNodes.length; i++) { const assay = assayNodes[i]; const assayWellId = getSafeTextContent(assay, 'ID_POINT_OBSERV_ASSA'); // Пропускаем пробы других скважин if (assayWellId !== currentWellId) continue; const number = parseInt(getSafeTextContent(assay, 'ASSAY_NUMBER_ASSA') || '0'); const from = parseFloat(getSafeTextContent(assay, 'INTERV_FROM_ASSA') || '0'); const to = parseFloat(getSafeTextContent(assay, 'INTERV_TO_ASSA') || '0'); const assayId = getSafeTextContent(assay, 'ID_ASSAY_ASSA'); const description = getSafeTextContent(assay, 'DESCRIPTION_ASSA'); // Определяем тип опробования по ID_ASSAY_TYPE_PROP_ASSA const assayTypeId = getSafeTextContent(assay, 'ID_ASSAY_TYPE_PROP_ASSA'); let type = 'Неизвестно'; if (assayTypeId && assayTypeMap.has(assayTypeId)) { type = assayTypeMap.get(assayTypeId); } // Определяем тип документирования для пробы let documentationType = 'Не определено'; // Проверяем историю опробования для определения типа документирования const assayHistoryNodes = xmlDoc.getElementsByTagName('MR_DBA.ASSAY_HISTORY_ASHI'); for (let j = 0; j < assayHistoryNodes.length; j++) { const history = assayHistoryNodes[j]; const historyAssayId = getSafeTextContent(history, 'ID_ASSAY_ASHI'); if (historyAssayId === assayId) { const docTypeId = getSafeTextContent(history, 'ID_TYPE_DOCUM_ASHI'); if (docTypeId && docTypeId !== 'Не указано') { if (docTypeId.includes('ПЕРВИЧ') || docTypeId.includes('Первич') || docTypeId === '1') { documentationType = 'Первичное'; } else if (docTypeId.includes('ИТОГОВ') || docTypeId.includes('Итогов') || docTypeId === '2') { documentationType = 'Итоговое'; } } break; } } // Если не нашли в истории, определяем по интервалу if (documentationType === 'Не определено') { const assayFrom = from; const assayTo = to; let isPrimary = false; for (const interval of primaryIntervals) { if (assayFrom >= interval.from && assayTo <= interval.to) { isPrimary = true; break; } } let isFinal = false; for (const interval of finalIntervals) { if (assayFrom >= interval.from && assayTo <= interval.to) { isFinal = true; break; } } if (isPrimary && !isFinal) { documentationType = 'Первичное'; } else if (isFinal && !isPrimary) { documentationType = 'Итоговое'; } else if (isPrimary && isFinal) { documentationType = 'Оба типа'; } } // Получаем дополнительные данные опробования const volume = getSafeTextContent(assay, 'VOLUME_ASSA'); const volumeUnit = getSafeTextContent(assay, 'ID_MEASURE_VOL_ASSA'); const weight = getSafeTextContent(assay, 'WEIGHT_ASSA'); const weightUnit = getSafeTextContent(assay, 'ID_MEASURE_WEIGHT_ASSA'); const barcode = getSafeTextContent(assay, 'ASSAY_NUM_THROUGH_ASSA'); const place = getSafeTextContent(assay, 'ID_ASSAY_PLACE_ASSA'); const authorId = getSafeTextContent(assay, 'AUTHOR_MAIN_ASSA'); const samplingDate = formatDate(getSafeTextContent(assay, 'DATE_SAMPLING_ASSA')); let authorName = ''; if (authorId && authorId !== 'Не указано' && employeeMap.has(authorId)) { authorName = employeeMap.get(authorId); } else if (authorId && authorId !== 'Не указано') { authorName = `Код: ${authorId}`; } let value = ''; let unit = ''; if (volume && volumeUnit && volume !== 'Не указано' && volumeUnit !== 'Не указано') { value = volume; if (measureMap.has(volumeUnit)) { unit = measureMap.get(volumeUnit); } } else if (weight && weightUnit && weight !== 'Не указано' && weightUnit !== 'Не указано') { value = weight; if (measureMap.has(weightUnit)) { unit = measureMap.get(weightUnit); } } // Форматируем тип документирования let formattedDocType = documentationType; if (documentationType === 'Первичное') { formattedDocType = 'Первичное документирование'; } else if (documentationType === 'Итоговое') { formattedDocType = 'Итоговое документирование'; } else if (documentationType === 'Оба типа') { formattedDocType = 'Оба типа документирования'; } // Фильтруем пустые описания и места отбора const assayDescription = (description && description !== 'Не указано') ? description : ''; const assayPlace = (place && place !== 'Не указано' && place.length <= 20) ? place : ''; assays.push({ number, type, documentationType: formattedDocType, from, to, value, unit, barcode, place: assayPlace, author: authorName, samplingDate, description: assayDescription }); } // Сортируем пробы по номеру assays.sort((a, b) => a.number - b.number); // Сохраняем извлеченные данные const wellData = { wellName, object, area, wellType, wellNumber: wellName, lineNumber, wellDescription, drillingStartDate, drillingEndDate, drillingBrigade, primaryDocInfo, finalDocInfo, gisDocInfo, primaryDocumentation: primaryIntervals, finalDocumentation: finalIntervals, gisDocumentation: gisIntervals, assays, coreBoxes: [], attachedDocuments: [], wellId: currentWellId }; // Извлекаем керновые ящики и документы УНИВЕРСАЛЬНЫМ МЕТОДОМ с фильтрацией по скважине if (currentWellId && currentWellId !== 'Не указано') { console.log(`Извлечение дополнительных данных для скважины ${currentWellId}`); // Используем очищенный XML контент для быстрого поиска const serializer = new XMLSerializer(); const xmlContentString = serializer.serializeToString(xmlDoc); // Извлекаем керновые ящики УНИВЕРСАЛЬНЫМ МЕТОДОМ с фильтрацией console.log(`Извлечение керновых ящиков для скважины ${currentWellId}`); const coreBoxes = extractCoreBoxesFast(xmlContentString, currentWellId); wellData.coreBoxes = coreBoxes; console.log(`Найдено керновых ящиков: ${coreBoxes.length}`); // Извлекаем документы УНИВЕРСАЛЬНЫМ МЕТОДОМ с фильтрацией console.log(`Извлечение документов для скважины ${currentWellId}`); const documents = extractAttachedDocumentsFast(xmlContentString, currentWellId); wellData.attachedDocuments = documents; console.log(`Найдено документов: ${documents.length}`); // Связываем документы с керновыми ящиками console.log(`Связывание документов с керновыми ящиками для скважины ${currentWellId}`); wellData.coreBoxes = linkDocumentsToCoreBoxes( coreBoxes, documents, xmlContentString, currentWellId ); } else { console.log('ID скважины не найден, пропускаем извлечение керновых ящиков и документов'); } return { isMulti: false, data: wellData }; } catch (error) { console.error('Ошибка парсинга одиночного файла:', error); throw error; } } ```
2. Обновленная функция loadWellData с поддержкой мульти-скважин для всех типов файлов
```javascript // Загрузка данных о скважине из файла async function loadWellData(file, fileNumber) { showLoading('Открытие архива...'); const wellDataElement = document.getElementById('wellData'); wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';
try { console.log('Начало загрузки файла:', file.name); updateLoadingText('Чтение архива...');
const fileObj = await file.handle.getFile(); console.log('Размер файла:', fileObj.size, 'байт');
if (fileObj.size > 1500 * 1024 * 1024) { throw new Error('Файл слишком большой для обработки в браузере (максимум 1500 МБ)'); }
updateLoadingText('Распаковка архива...'); const arrayBuffer = await fileObj.arrayBuffer(); console.log('ArrayBuffer прочитан, размер:', arrayBuffer.byteLength);
updateLoadingText('Обработка данных...'); const zip = await JSZip.loadAsync(arrayBuffer); console.log('ZIP распакован, файлов:', Object.keys(zip.files).length); // Сохраняем текущий ZIP архив для скачивания файлов currentZip = zip; console.log('ZIP архив сохранен для скачивания файлов');
const multiFiles = []; let otherTargetFile = null; let fileType = '';
updateProgress(10, 'Поиск файлов с данными...');
// Ищем файлы в архиве for (const filename in zip.files) { const zipEntry = zip.files[filename]; if (zipEntry.dir || filename.endsWith('/')) continue;
if (filename.startsWith('904~')) { multiFiles.push({ name: filename, entry: zipEntry, size: zipEntry._data.uncompressedSize }); } else if (filename.startsWith('906~') && !otherTargetFile) { otherTargetFile = zipEntry; fileType = 'all'; } else if (filename.startsWith('911~') && !otherTargetFile) { otherTargetFile = zipEntry; fileType = 'final'; } else if (filename.startsWith('909~') && !otherTargetFile) { otherTargetFile = zipEntry; fileType = 'primary'; } else if (!otherTargetFile && (filename.endsWith('.inu') || filename.includes('~'))) { // Любой другой INU файл otherTargetFile = zipEntry; fileType = 'unknown'; } }
// ОБРАБОТКА МУЛЬТИ-ФАЙЛОВ (904~) if (multiFiles.length > 0) { console.log(`Найдено мульти-файлов 904~: ${multiFiles.length}`); const allWells = []; let processedFiles = 0;
// ОПТИМИЗАЦИЯ: Ограничиваем количество одновременно обрабатываемых файлов const MAX_CONCURRENT_FILES = 2; const processFileBatch = async (startIndex) => { const endIndex = Math.min(startIndex + MAX_CONCURRENT_FILES, multiFiles.length); const batch = multiFiles.slice(startIndex, endIndex); const batchPromises = batch.map(async (multiFile, index) => { try { const fileIndex = startIndex + index; updateProgress(10 + (fileIndex / multiFiles.length) * 70, `Чтение файла ${fileIndex + 1} из ${multiFiles.length}`); // ОПТИМИЗАЦИЯ: Используем быстрый метод чтения console.time(`Read file ${multiFile.name}`); const xmlContent = await readZipFileFast(multiFile.entry); console.timeEnd(`Read file ${multiFile.name}`); // ОБНОВЛЯЕМ ПРОГРЕСС updateProgress(10 + (fileIndex / multiFiles.length) * 70 + 10, `Обработка файла ${fileIndex + 1}`); // Сохраняем в кеш fileCache.set(multiFile.name, xmlContent); // Извлекаем метаданные скважин console.time(`Extract wells from ${multiFile.name}`); const wellsFromFile = extractWellsMetadataFromContent(xmlContent); console.timeEnd(`Extract wells from ${multiFile.name}`);
console.log(`Из файла ${multiFile.name} извлечено скважин:`, wellsFromFile.length);
// ОБНОВЛЯЕМ ПРОГРЕСС ПОСЛЕ ОБРАБОТКИ updateProgress(10 + (fileIndex / multiFiles.length) * 70 + 20, `Завершено ${fileIndex + 1}/${multiFiles.length}`);
return wellsFromFile.map(well => ({ ...well, sourceFile: multiFile.name }));
} catch (error) { console.error(`Ошибка обработки файла ${multiFile.name}:`, error); return []; } });
const batchResults = await Promise.all(batchPromises); // Объединяем результаты for (const wells of batchResults) { for (const well of wells) { if (!allWells.some(w => w.wellId === well.wellId)) { allWells.push(well); } } }
processedFiles += batch.length; // Рекурсивно обрабатываем следующую партию if (processedFiles < multiFiles.length) { // Даем браузеру "подышать" между батчами await new Promise(resolve => setTimeout(resolve, 50)); return processFileBatch(processedFiles); } };
// Запускаем обработку батчами await processFileBatch(0);
updateProgress(85, 'Формирование списка скважин...'); console.log(`Всего уникальных скважин: ${allWells.length}`);
if (allWells.length > 0) { currentMultiWellData = { wells: allWells, fileNames: multiFiles.map(f => f.name) };
updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); currentMultiFileName = file.name; showWellsList(allWells, file.name, multiFiles.length); wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>'; }, 100);
} else { hideLoading(); throw new Error('Не удалось извлечь данные скважин из файлов 904~'); }
} // ОБРАБОТКА ОДИНОЧНЫХ ФАЙЛОВ (906~, 911~, 909~ и других) С ПОДДЕРЖКОЙ МУЛЬТИ-СКВАЖИН else if (otherTargetFile) { // Обработка обычных файлов (906~, 911~, 909~ и других) updateProgress(50, 'Обработка файла данных...'); console.log('Найден INU файл:', otherTargetFile.name, 'Размер:', otherTargetFile._data.uncompressedSize);
// Увеличиваем лимит размера INU файла до 2000MB if (otherTargetFile._data.uncompressedSize > 2000 * 1024 * 1024) { throw new Error('INU файл слишком большой для обработки в браузере (максимум 2000 МБ)'); }
// Читаем XML данные updateLoadingText('Чтение XML данных...'); console.log('Чтение XML...'); const xmlContent = await otherTargetFile.async('text'); console.log('XML прочитан, длина:', xmlContent.length);
// Парсинг XML данных с обработкой ошибок updateProgress(80, 'Парсинг XML...'); console.log('Парсинг XML...'); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(cleanXMLContent(xmlContent), 'text/xml'); const parseError = xmlDoc.getElementsByTagName('parsererror')[0]; if (parseError) { throw new Error('Ошибка парсинга XML: файл поврежден или имеет неверный формат'); } // Используем обновленную функцию парсинга с поддержкой мульти-скважин const parseResult = await parseSingleWellData(xmlDoc, fileType, file.name); if (parseResult.isMulti) { // Если файл содержит несколько скважин, показываем список console.log(`Файл содержит ${parseResult.wellCount} скважин, показываем список`); currentMultiWellData = { wells: parseResult.wells, xmlContent: xmlContent, fileNames: [file.name], wellCount: parseResult.wellCount };
updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); currentMultiFileName = file.name; showWellsList(parseResult.wells, file.name, 1); wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>'; }, 100); } else { // Одиночная скважина - отображаем сразу xmlData = parseResult.data; updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); displayWellData(fileType, fileNumber, file.name); console.log('Данные отображены'); }, 500); }
} else { hideLoading(); throw new Error('В архиве не найден файл с данными (начинающийся с 904~, 906~, 909~ или 911~)'); }
} catch (error) { hideLoading(); console.error('Ошибка загрузки данных:', error); wellDataElement.innerHTML = ` <div class="error"> <p><strong>Ошибка загрузки данных:</strong> ${error.message}</p> <p><strong>Поддерживаемые размеры:</strong></p> <ul style="text-align: left; margin: 10px 0;"> <li>ZIP архивы: до 1500 МБ</li> <li>INU файлы: до 2000 МБ</li> </ul> <p>Попробуйте выбрать другой файл или обратитесь к администратору</p> </div> `; } } ```
3. Обновленная функция loadWellDataFromFile для ручного выбора файлов
```javascript // Загрузка данных о скважине из файла (ручной выбор) async function loadWellDataFromFile(file, fileNumber) { // Очищаем список при загрузке нового файла clearFileList(); showLoading('Открытие файла...'); const wellDataElement = document.getElementById('wellData'); wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';
try { if (file.size > 2000 * 1024 * 1024) { throw new Error('Файл слишком большой для обработки в браузере (максимум 2000 МБ)'); }
updateProgress(30, 'Чтение файла...'); const fileContent = await file.text();
updateProgress(70, 'Обработка XML...'); const cleanedContent = cleanXMLContent(fileContent);
// Определяем тип файла по имени let fileType = ''; if (file.name.startsWith('906~')) { fileType = 'all'; } else if (file.name.startsWith('911~')) { fileType = 'final'; } else if (file.name.startsWith('909~')) { fileType = 'primary'; } else if (file.name.startsWith('904~')) { fileType = 'multi'; } else { fileType = 'unknown'; }
// Парсинг XML данных updateProgress(90, 'Парсинг данных...'); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(cleanedContent, 'text/xml'); const parseError = xmlDoc.getElementsByTagName('parsererror')[0]; if (parseError) { throw new Error('Ошибка парсинга XML: файл поврежден или имеет неверный формат'); } // Используем обновленную функцию парсинга с поддержкой мульти-скважин const parseResult = await parseSingleWellData(xmlDoc, fileType, file.name); updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading();
if (parseResult.isMulti) { // Если файл содержит несколько скважин, показываем список currentMultiWellData = { wells: parseResult.wells, xmlContent: cleanedContent, fileNames: [file.name], wellCount: parseResult.wellCount }; currentMultiFileName = file.name; showWellsList(parseResult.wells, file.name, 1); wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>'; } else { // Одиночная скважина - сразу отображаем данные xmlData = parseResult.data; displayWellData(fileType, fileNumber, file.name); } }, 500);
} catch (error) { hideLoading(); console.error('Ошибка загрузки данных:', error); wellDataElement.innerHTML = ` <div class="error"> <p><strong>Ошибка загрузки данных:</strong> ${error.message}</p> <p><strong>Поддерживаемые размеры:</strong></p> <ul style="text-align: left; margin: 10px 0;"> <li>ZIP архивы: до 1500 МБ</li> <li>INU файлы: до 2000 МБ</li> </ul> <p>Попробуйте выбрать другой файл или архив</p> </div> `; } } ```
Теперь система корректно обрабатывает:
1. Файлы 904~ - как мульти-файлы (как и раньше) 2. Файлы 906~, 911~, 909~ и другие - автоматически определяет количество скважин и показывает список если скважин больше одной 3. Правильно фильтрует данные - интервалы, опробование, керновые ящики и документы привязываются только к текущей скважине 4. Сохраняет обратную совместимость - одиночные скважины отображаются сразу
|
Понял! Нужно доработать функцию extractCoreBoxesFast чтобы она фильтровала керновые ящики только для конкретной скважины. Вот исправленная версия:
Исправленная функция extractCoreBoxesFast
```javascript // Функция для извлечения данных о керновых ящиках с фильтрацией по скважине function extractCoreBoxesFast(xmlContent, wellId) { const coreBoxes = []; try { console.log(`Поиск керновых ящиков для скважины ${wellId} (универсальный метод)`); // 1. Создаем карту PO_DRIFTING_PODR (скважины) -> ID_POINT_OBSERV_PODR const driftingToPointMap = new Map(); const driftingRegex = /<MR_DBA\.PO_DRIFTING_PODR>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_PODR>/g; const driftingMatches = xmlContent.match(driftingRegex) || []; for (const driftingMatch of driftingMatches) { const driftingId = extractValue(driftingMatch, 'ID_PO_DRIFTING_PODR'); const pointObservId = extractValue(driftingMatch, 'ID_POINT_OBSERV_PODR'); if (driftingId && pointObservId) { driftingToPointMap.set(driftingId, pointObservId); } } console.log(`Карта скважин -> точек наблюдения: ${driftingToPointMap.size}`); // 2. Создаем карту PO_DRIFTING_CORE_POCR (керны) -> ID_PO_DRIFTING_POCR const coreToDriftingMap = new Map(); const coreRegex = /<MR_DBA\.PO_DRIFTING_CORE_POCR>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_CORE_POCR>/g; const coreMatches = xmlContent.match(coreRegex) || []; for (const coreMatch of coreMatches) { const coreId = extractValue(coreMatch, 'ID_PO_DRIFTING_CORE_POCR'); const driftingId = extractValue(coreMatch, 'ID_PO_DRIFTING_POCR'); if (coreId && driftingId) { coreToDriftingMap.set(coreId, driftingId); } } console.log(`Карта кернов -> скважин: ${coreToDriftingMap.size}`); // 3. Ищем керновые ящики и связываем со скважиной const boxRegex = /<MR_DBA\.PO_DRIFTING_BOX_PODB>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_BOX_PODB>/g; const boxMatches = xmlContent.match(boxRegex) || []; console.log(`Найдено керновых ящиков всего: ${boxMatches.length}`); for (const boxMatch of boxMatches) { const boxId = extractValue(boxMatch, 'ID_PO_DRIFTING_BOX_PODB'); const coreId = extractValue(boxMatch, 'ID_PO_DRIFTING_CORE_PODB'); const boxNumber = extractValue(boxMatch, 'NUMBER_PP_BOX_PODB') || extractValue(boxMatch, 'BOX_NUMBER_PODB'); const intervalFrom = parseFloat(extractValue(boxMatch, 'PO_DRIFTING_FROM_PODB') || extractValue(boxMatch, 'INTERV_FROM_PODB') || '0'); const intervalTo = parseFloat(extractValue(boxMatch, 'PO_DRIFTING_TO_PODB') || extractValue(boxMatch, 'INTERV_TO_PODB') || '0'); const thickness = parseFloat(extractValue(boxMatch, 'THICKNESS_PODB') || '0'); const basalHorizon = extractValue(boxMatch, 'BASAL_HORIZ_PODB'); const description = extractValue(boxMatch, 'COMMENT_PODB') || extractValue(boxMatch, 'DESCRIPTION_PODB'); const inputDate = formatDate(extractValue(boxMatch, 'DATE_INPUT_PODB')); console.log(`Ящик ${boxNumber}: coreId=${coreId}, from=${intervalFrom}, to=${intervalTo}`); // Проверяем связь со скважиной через цепочку: Ящик -> Керн -> Скважина -> Точка наблюдения let isRelated = false; if (coreId && coreToDriftingMap.has(coreId)) { const driftingId = coreToDriftingMap.get(coreId); if (driftingId && driftingToPointMap.has(driftingId)) { const pointObservId = driftingToPointMap.get(driftingId); // Если точка наблюдения совпадает с ID скважины if (pointObservId === wellId) { isRelated = true; console.log(`✓ Ящик ${boxNumber} принадлежит скважине ${wellId} через цепочку`); } else { console.log(`✗ Ящик ${boxNumber} принадлежит другой скважине ${pointObservId}`); } } } // Если не нашли связь через цепочку, проверяем прямую связь if (!isRelated) { // Проверяем прямую связь ящика со скважиной const directWellId = extractValue(boxMatch, 'ID_POINT_OBSERV_PODB'); if (directWellId && directWellId === wellId) { isRelated = true; console.log(`✓ Ящик ${boxNumber} принадлежит скважине ${wellId} через прямую связь`); } else if (directWellId) { console.log(`✗ Ящик ${boxNumber} принадлежит другой скважине ${directWellId} (прямая связь)`); } } // ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: Проверяем связь через керн и проходку if (!isRelated && coreId) { // Ищем информацию о керне const coreRegex = new RegExp(`<MR_DBA\\.PO_DRIFTING_CORE_POCR>(?:[^<]|<(?!\\/MR_DBA\\.PO_DRIFTING_CORE_POCR>))*<ID_PO_DRIFTING_CORE_POCR>${coreId}<\\/ID_PO_DRIFTING_CORE_POCR>(?:[^<]|<(?!\\/MR_DBA\\.PO_DRIFTING_CORE_POCR>))*<\\/MR_DBA\\.PO_DRIFTING_CORE_POCR>`, 's'); const coreMatch = xmlContent.match(coreRegex); if (coreMatch) { const coreDriftingId = extractValue(coreMatch[0], 'ID_PO_DRIFTING_POCR'); if (coreDriftingId && driftingToPointMap.has(coreDriftingId)) { const coreWellId = driftingToPointMap.get(coreDriftingId); if (coreWellId === wellId) { isRelated = true; console.log(`✓ Ящик ${boxNumber} принадлежит скважине ${wellId} через керн`); } } } } // ДЛЯ ОДИНОЧНЫХ ФАЙЛОВ (906, 911, 909): если не нашли связь, но файл один - НЕ считаем связанным // УБИРАЕМ автоматическое связывание для одиночных файлов if (!isRelated) { console.log(`✗ Ящик ${boxNumber} не принадлежит скважине ${wellId}, пропускаем`); continue; // Пропускаем ящики, не принадлежащие текущей скважине } if (isRelated) { const basalText = basalHorizon === 'Д' ? 'Базальный горизонт' : basalHorizon === 'Н' ? 'Не базальный' : basalHorizon || 'Не указано'; coreBoxes.push({ id: boxId, boxNumber: boxNumber || 'Не указан', coreId: coreId, intervalFrom: intervalFrom, intervalTo: intervalTo, thickness: thickness, interval: `${formatNumber(intervalFrom)} - ${formatNumber(intervalTo)} м`, basalHorizon: basalText, description: description || '', inputDate: inputDate, relatedDocuments: [] }); console.log(`✓ Найден керновый ящик ${boxNumber} для скважины ${wellId}`); } } // Сортируем ящики по интервалу coreBoxes.sort((a, b) => a.intervalFrom - b.intervalFrom); console.log(`Найдено керновых ящиков для скважины ${wellId}: ${coreBoxes.length}`); } catch (error) { console.error('Ошибка извлечения керновых ящиков:', error); } return coreBoxes; } ```
Дополнительно нужно обновить функцию linkDocumentsToCoreBoxes для правильной фильтрации:
```javascript // Функция для связывания документов с керновыми ящиками через прямые связи function linkDocumentsToCoreBoxes(coreBoxes, documents, xmlContent, wellId) { try { console.log(`=== СВЯЗЫВАНИЕ ДОКУМЕНТОВ С ЯЩИКАМИ ДЛЯ СКВАЖИНЫ ${wellId} ===`); // 1. Фильтруем документы - берем только те, что принадлежат этой скважине const wellDocuments = documents.filter(doc => { // Документ принадлежит скважине если: // - relationType === 'direct' (прямая связь) // - или wellInfo соответствует текущей скважине const currentWellName = xmlData ? xmlData.wellName : ''; const currentLineNumber = xmlData ? xmlData.lineNumber : ''; let currentWellDisplay = ''; if (currentLineNumber && currentLineNumber !== 'Не указано' && currentWellName) { currentWellDisplay = `л${currentLineNumber} - ${currentWellName}`; } else if (currentWellName) { currentWellDisplay = currentWellName; } if (doc.relationType === 'direct') { return true; } if (doc.wellInfo && currentWellDisplay && doc.wellInfo === currentWellDisplay) { return true; } return false; }); console.log(`Документов, принадлежащих скважине ${wellId}: ${wellDocuments.length}`); // 2. Создаем карту документов по targetObjectId const documentsByTargetObject = new Map(); wellDocuments.forEach(doc => { if (doc.targetObjectId) { if (!documentsByTargetObject.has(doc.targetObjectId)) { documentsByTargetObject.set(doc.targetObjectId, []); } documentsByTargetObject.get(doc.targetObjectId).push(doc); } });
console.log(`Документы сгруппированы по targetObjectId:`, Array.from(documentsByTargetObject.keys()));
// 3. Связываем документы с ящиками через прямые связи targetObjectId coreBoxes.forEach(box => { const relatedDocs = []; const boxId = box.id; console.log(`Обработка ящика ${box.boxNumber} (ID: ${boxId})`); // Ищем документы, которые напрямую привязаны к этому ящику if (documentsByTargetObject.has(boxId)) { const boxDocuments = documentsByTargetObject.get(boxId); boxDocuments.forEach(doc => { relatedDocs.push({ id: doc.id, docDcaaId: doc.docDcaaId, fileName: doc.fileName, fileType: doc.fileType, downloadFileName: doc.downloadFileName, blobPath: doc.blobPath, description: doc.description, relationType: 'direct_box_link', relationNote: `Прямая привязка к ящику ${box.boxNumber}` }); console.log(`✓ Документ "${doc.fileName}" напрямую привязан к ящику ${box.boxNumber}`); }); } else { console.log(`✗ Для ящика ${box.boxNumber} не найдено напрямую привязанных документов`); } box.relatedDocuments = relatedDocs; });
// 4. Сводная статистика const boxesWithDocs = coreBoxes.filter(box => box.relatedDocuments.length > 0).length; const totalLinkedDocuments = coreBoxes.reduce((count, box) => count + box.relatedDocuments.length, 0); console.log(`=== СВОДКА СВЯЗЕЙ ДЛЯ СКВАЖИНЫ ${wellId} ===`); console.log(`Всего ящиков: ${coreBoxes.length}`); console.log(`Ящиков с документами: ${boxesWithDocs}`); console.log(`Всего связей документов с ящиками: ${totalLinkedDocuments}`); } catch (error) { console.error('Ошибка связывания документов с керновыми ящиками:', error); } return coreBoxes; } ```
Также обновим функцию extractAttachedDocumentsFast для лучшей фильтрации:
```javascript // Функция для извлечения вложенных документов для всех типов файлов с фильтрацией по скважине function extractAttachedDocumentsFast(xmlContent, wellId) { const documents = []; try { console.log(`=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===`); // 1. Находим информацию о текущей скважине const wellRegex = new RegExp(`<MR_DBA\\.POINT_OBSERV_POAA>(?:[^<]|<(?!\\/MR_DBA\\.POINT_OBSERV_POAA>))*<ID_POINT_OBSERV_POAA>${wellId}<\\/ID_POINT_OBSERV_POAA>(?:[^<]|<(?!\\/MR_DBA\\.POINT_OBSERV_POAA>))*<\\/MR_DBA\\.POINT_OBSERV_POAA>`, 's'); const wellMatch = xmlContent.match(wellRegex); let currentWellName = ''; let currentLineNumber = ''; if (wellMatch) { currentWellName = extractValue(wellMatch[0], 'NAME_FULL_POAA') || 'Без названия'; const lineId = extractValue(wellMatch[0], 'ID_LINE_COMMON_POAA'); if (lineId) { const lineRegex = new RegExp(`<MR_DBA\\.LINE_COMMON_LNCM>(?:[^<]|<(?!\\/MR_DBA\\.LINE_COMMON_LNCM>))*<ID_LINE_COMMON_LNCM>${lineId}<\\/ID_LINE_COMMON_LNCM>(?:[^<]|<(?!\\/MR_DBA\\.LINE_COMMON_LNCM>))*<\\/MR_DBA\\.LINE_COMMON_LNCM>`, 's'); const lineMatch = xmlContent.match(lineRegex); if (lineMatch) { currentLineNumber = extractValue(lineMatch[0], 'NAME_FULL_LNCM') || ''; } } } // Форматируем отображаемое название текущей скважины let currentWellDisplay = ''; if (currentLineNumber && currentLineNumber !== 'Не указано' && currentWellName) { currentWellDisplay = `л${currentLineNumber} - ${currentWellName}`; } else if (currentWellName) { currentWellDisplay = currentWellName; } console.log(`Текущая скважина: ${currentWellDisplay}`); // ... остальной код функции extractAttachedDocumentsFast ... // В конце, после извлечения всех документов, добавляем проверку принадлежности documents.forEach(doc => { // Определяем принадлежность документа к текущей скважине if (doc.wellInfo && currentWellDisplay && doc.wellInfo === currentWellDisplay) { doc.relationType = 'direct'; doc.relationNote = `Привязан к текущей скважине ${currentWellDisplay}`; } else if (doc.wellInfo) { doc.relationType = 'unrelated'; doc.relationNote = `Принадлежит другой скважине: ${doc.wellInfo}`; } else if (doc.targetObjectId) { // Проверяем связь через targetObjectId const wellInfo = getWellInfoForDocument(doc.targetObjectId, xmlContent); if (wellInfo && wellInfo.displayName === currentWellDisplay) { doc.relationType = 'direct'; doc.relationNote = `Привязан к текущей скважине через объект ${doc.targetObjectId}`; doc.wellInfo = currentWellDisplay; } else if (wellInfo) { doc.relationType = 'unrelated'; doc.relationNote = `Принадлежит другой скважине: ${wellInfo.displayName}`; doc.wellInfo = wellInfo.displayName; } } }); } catch (error) { console.error('Ошибка извлечения документов:', error); } return documents; } ```
Теперь система будет:
1. Фильтровать керновые ящики только для текущей скважины 2. Не показывать ящики других скважин из того же файла 3. Корректно связывать документы только с ящиками текущей скважины 4. Логировать процесс для отладки связей между ящиками и скважинами
Теперь в таблице с керновыми ящиками будут отображаться только ящики, принадлежащие выбранной скважине.
|
|
Отлично, я изучил схему данных и вижу проблему! Проблема в том, что текущий код неправильно определяет связи между документами и скважинами в мульти-файлах.
## Основные проблемы в текущем коде:
### 1. **Неправильная логика связывания документов со скважинами** В функции `extractAttachedDocumentsFast` код пытается найти связи через `wellDataObjects`, но не учитывает полную цепочку через `DOCUM_OBJ_SYS_DCOS`.
### 2. **Отсутствие анализа связей DOCUM_OBJ_SYS_DCOS** Код не использует таблицу `DOCUM_OBJ_SYS_DCOS` для определения, к каким конкретно объектам скважин привязаны документы.
### 3. **Неправильное определение принадлежности документов** В мульти-файлах документы могут принадлежать разным скважинам, но текущий код этого не учитывает.
## Предлагаю исправить следующим образом:
### 1. Добавить функцию анализа связей DOCUM_OBJ_SYS_DCOS:
```javascript function analyzeDocumentLinks(xmlContent, wellId) { const documentLinks = new Map(); try { // Ищем все связи документов с объектами const dcosRegex = /<MR_DBA\.DOCUM_OBJ_SYS_DCOS>[\s\S]*?<\/MR_DBA\.DOCUM_OBJ_SYS_DCOS>/g; const dcosMatches = xmlContent.match(dcosRegex) || []; for (const dcosMatch of dcosMatches) { const documentId = extractValue(dcosMatch, 'ID_DOCUM_DCOS'); const objectId = extractValue(dcosMatch, 'ID_OBJECT_DCOS'); if (documentId && objectId) { // Находим, к какой скважине принадлежит этот объект const targetWellId = findWellIdByObjectId(objectId, xmlContent, wellId); if (targetWellId) { documentLinks.set(documentId, { objectId: objectId, wellId: targetWellId }); } } } } catch (error) { console.error('Ошибка анализа связей документов:', error); } return documentLinks; } ```
### 2. Функция поиска ID скважины по ID объекта:
```javascript function findWellIdByObjectId(objectId, xmlContent, currentWellId) { try { // Пробуем найти через PO_DOCUM_PODC (документирование) const podcRegex = /<MR_DBA\.PO_DOCUM_PODC>[\s\S]*?<\/MR_DBA\.PO_DOCUM_PODC>/g; const podcMatches = xmlContent.match(podcRegex) || []; for (const podcMatch of podcMatches) { const podcId = extractValue(podcMatch, 'ID_PO_DOCUM_PODC'); if (podcId === objectId) { return extractValue(podcMatch, 'ID_POINT_OBSERV_PODC'); } } // Пробуем найти через PO_DRIFTING_BOX_PODB (ящики) const podbRegex = /<MR_DBA\.PO_DRIFTING_BOX_PODB>[\s\S]*?<\/MR_DBA\.PO_DRIFTING_BOX_PODB>/g; const podbMatches = xmlContent.match(podbRegex) || []; for (const podbMatch of podbMatches) { const podbId = extractValue(podbMatch, 'ID_PO_DRIFTING_BOX_PODB'); if (podbId === objectId) { // Для ящиков связь через керн и проходку const coreId = extractValue(podbMatch, 'ID_PO_DRIFTING_CORE_PODB'); if (coreId) { return findWellIdFromCore(coreId, xmlContent); } } } // Пробуем найти через PO_DOCUM_KRF_PODK (интервалы с фото) const podkRegex = /<MR_DBA\.PO_DOCUM_KRF_PODK>[\s\S]*?<\/MR_DBA\.PO_DOCUM_KRF_PODK>/g; const podkMatches = xmlContent.match(podkRegex) || []; for (const podkMatch of podkMatches) { const podkId = extractValue(podkMatch, 'ID_PO_DOCUM_KRF_PODK'); if (podkId === objectId) { const docId = extractValue(podbMatch, 'ID_PO_DOCUM_PODK'); if (docId) { // Ищем документирование для этого интервала for (const podcMatch of podcMatches) { const podcId = extractValue(podcMatch, 'ID_PO_DOCUM_PODC'); if (podcId === docId) { return extractValue(podcMatch, 'ID_POINT_OBSERV_PODC'); } } } } } } catch (error) { console.error('Ошибка поиска скважины по объекту:', error); } return null; } ```
### 3. Обновить функцию извлечения документов:
```javascript function extractAttachedDocumentsFast(xmlContent, wellId) { const documents = []; try { console.log(`=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===`); // 1. Анализируем связи документов с объектами const documentLinks = analyzeDocumentLinks(xmlContent, wellId); console.log(`Найдено связей документов: ${documentLinks.size}`); // 2. Создаем карту DOCUM_DCAA const documentInfoMap = new Map(); const documentRegex = /<MR_DBA\.DOCUM_DCAA>[\s\S]*?<\/MR_DBA\.DOCUM_DCAA>/g; const documentMatches = xmlContent.match(documentRegex) || []; for (const documentMatch of documentMatches) { const documentId = extractValue(documentMatch, 'ID_DOCUM_DCAA'); const comment = extractValue(documentMatch, 'COMMENT_DCAA'); const name = extractValue(documentMatch, 'NAME_DOC_DCAA'); if (documentId) { documentInfoMap.set(documentId, { comment: comment || 'Нет описания', name: name || `Документ ${documentId}` }); } }
// 3. Создаем карту CONTENT_LOBA const contentBlobMap = new Map(); const contentRegex = /<MR_DBA\.CONTENT_LOBA>[\s\S]*?<\/MR_DBA\.CONTENT_LOBA>/g; const contentMatches = xmlContent.match(contentRegex) || []; for (const contentMatch of contentMatches) { const objectId = extractValue(contentMatch, 'ID_OBJECT_LOBA'); const blobPath = extractValue(contentMatch, 'BLOB_VALUE_LOBA'); if (objectId && blobPath) { contentBlobMap.set(objectId, blobPath); } }
// 4. Ищем все файлы в DD_OBJECT_DDOB const objectRegex = /<MR_DBA\.DD_OBJECT_DDOB>[\s\S]*?<\/MR_DBA\.DD_OBJECT_DDOB>/g; const objectMatches = xmlContent.match(objectRegex) || []; console.log(`Найдено записей DD_OBJECT_DDOB: ${objectMatches.length}`);
for (const objectMatch of objectMatches) { const ddoBbjectId = extractValue(objectMatch, 'ID_DD_OBJECT_DDOB'); const idObjectDDOB = extractValue(objectMatch, 'ID_OBJECT_DDOB'); // Это ID_DOCUM_DCAA! const fileName = extractValue(objectMatch, 'COMMENT_DDOB'); const fileType = extractValue(objectMatch, 'TYPE_DATA_DDOB'); const fileExtension = extractValue(objectMatch, 'EXPAND_DDOB'); const inputDate = formatDate(extractValue(objectMatch, 'DATE_INPUT_DDOB')); const author = extractValue(objectMatch, 'VVOD_ID_CONTRACTOR_DDOB'); // Получаем данные из DOCUM_DCAA let description = 'Нет описания'; let displayName = fileName || `Документ ${ddoBbjectId}`; if (idObjectDDOB && documentInfoMap.has(idObjectDDOB)) { const docInfo = documentInfoMap.get(idObjectDDOB); description = docInfo.comment; displayName = docInfo.name; } // Получаем путь к файлу const blobPath = contentBlobMap.get(ddoBbjectId); // Формируем имя файла для скачивания let downloadFileName = fileName; if (fileName && fileExtension) { const baseName = fileName.replace(/\.[^/.]+$/, ""); downloadFileName = baseName + fileExtension; } else if (fileName) { downloadFileName = fileName; } else { downloadFileName = `document_${ddoBbjectId}`; } // ОПРЕДЕЛЯЕМ ПРИНАДЛЕЖНОСТЬ К СКВАЖИНЕ через DOCUM_OBJ_SYS_DCOS let relationType = 'unknown'; let targetWellId = null; if (idObjectDDOB && documentLinks.has(idObjectDDOB)) { const link = documentLinks.get(idObjectDDOB); targetWellId = link.wellId; if (targetWellId === wellId) { relationType = 'direct'; console.log(`✓ Документ "${displayName}" принадлежит скважине ${wellId}`); } else { relationType = 'unrelated'; console.log(`✗ Документ "${displayName}" принадлежит другой скважине ${targetWellId}`); } } else { relationType = 'unknown'; console.log(`? Документ "${displayName}" не имеет связи с объектом`); } const documentData = { id: ddoBbjectId, docDcaaId: idObjectDDOB, fileName: displayName, fileType: fileType || 'Неизвестный тип', fileExtension: fileExtension || '', description: description, blobPath: blobPath, author: author || 'Не указан', uploadDate: inputDate, downloadFileName: downloadFileName, originalFileName: fileName, relationType: relationType, targetWellId: targetWellId, // Сохраняем информацию о скважине для отображения wellInfo: targetWellId ? findWellInfoById(targetWellId, xmlContent)?.displayName : null }; documents.push(documentData); } // Статистика const relatedCount = documents.filter(d => d.relationType === 'direct').length; const unrelatedCount = documents.filter(d => d.relationType === 'unrelated').length; const unknownCount = documents.filter(d => d.relationType === 'unknown').length; console.log(`=== РЕЗУЛЬТАТЫ ДЛЯ СКВАЖИНЫ ${wellId} ===`); console.log(`Всего документов: ${documents.length}`); console.log(`Принадлежащих скважине: ${relatedCount}`); console.log(`Принадлежащих другим скважинам: ${unrelatedCount}`); console.log(`Непривязанных к объектам: ${unknownCount}`); } catch (error) { console.error('Ошибка извлечения документов:', error); } return documents; } ```
Хотите, чтобы я внедрил эти исправления в код? Это решит проблему с неправильным определением принадлежности документов в мульти-файлах.Добавлено (2025-10-07, 13:24) --------------------------------------------- Отлично! Проблема в том, что функция `loadWellData` пытается обработать отдельный INU файл как ZIP архив. Нужно добавить проверку типа файла и разную логику обработки для ZIP и INU файлов.
Вот исправления:
## 1. Обновить функцию `loadWellData`:
```javascript async function loadWellData(file, fileNumber) { // Запускаем keep-alive при начале загрузки startKeepAlive(); showLoading('Анализ файла...'); const wellDataElement = document.getElementById('wellData'); wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';
try { console.log('Начало загрузки файла:', file.name); updateLoadingText('Чтение файла...'); updateProgress(5, 'Подготовка...');
const fileObj = await file.handle.getFile(); console.log('Размер файла:', fileObj.size, 'байт'); console.log('Тип файла:', fileObj.type); console.log('Имя файла:', fileObj.name);
// ОПРЕДЕЛЯЕМ ТИП ФАЙЛА const isZipFile = fileObj.name.toLowerCase().endsWith('.zip'); const isInuFile = fileObj.name.toLowerCase().endsWith('.inu'); console.log('isZipFile:', isZipFile, 'isInuFile:', isInuFile);
if (isZipFile) { // СУЩЕСТВУЮЩАЯ ЛОГИКА ДЛЯ ZIP АРХИВОВ if (fileObj.size > 1500 * 1024 * 1024) { throw new Error('Файл слишком большой для обработки в браузере (максимум 1500 МБ)'); }
updateLoadingText('Распаковка архива...'); updateProgress(15, 'Распаковка архива...'); const arrayBuffer = await Promise.race([ fileObj.arrayBuffer(), new Promise((_, reject) => setTimeout(() => reject(new Error('Таймаут чтения файла')), 30000) ) ]); console.log('ArrayBuffer прочитан, размер:', arrayBuffer.byteLength);
updateLoadingText('Обработка данных...'); updateProgress(25, 'Обработка ZIP...'); const zip = await Promise.race([ JSZip.loadAsync(arrayBuffer), new Promise((_, reject) => setTimeout(() => reject(new Error('Таймаут распаковки архива')), 45000) ) ]); console.log('ZIP распакован, файлов:', Object.keys(zip.files).length); currentZip = zip;
// ДАЛЕЕ СУЩЕСТВУЮЩАЯ ЛОГИКА ДЛЯ ZIP... // [остальной код обработки ZIP архива остается без изменений]
} else if (isInuFile) { // НОВАЯ ЛОГИКА ДЛЯ ОТДЕЛЬНЫХ INU ФАЙЛОВ console.log('Обработка отдельного INU файла'); updateLoadingText('Чтение INU файла...'); updateProgress(30, 'Чтение XML данных...');
// Читаем содержимое INU файла как текст const xmlContent = await Promise.race([ fileObj.text(), new Promise((_, reject) => setTimeout(() => reject(new Error('Таймаут чтения INU файла')), 30000) ) ]);
console.log('INU файл прочитан, размер:', xmlContent.length, 'символов');
// Для INU файлов создаем виртуальный "архив" с одним файлом currentZip = null; // Сбрасываем текущий ZIP
// Определяем тип файла по имени const fileType = getFileType(fileObj.name); console.log('Тип INU файла:', fileType);
updateProgress(50, 'Анализ данных скважины...');
// Ищем скважины в INU файле const wells = detectWellsInContent(xmlContent, fileObj.name); console.log('Найдено скважин в INU файле:', wells.length);
if (wells.length === 0) { throw new Error('В INU файле не найдено данных скважин'); }
// Сохраняем XML контент для быстрого доступа fileCache.set(fileObj.name, xmlContent);
// ЕДИНАЯ ЛОГИКА ДЛЯ ВСЕХ СЛУЧАЕВ (как в ZIP обработке) if (wells.length === 1) { // Одна скважина - сразу загружаем и отображаем const well = wells[0]; console.log(`Одна скважина найдена: ${well.wellId}, загружаем напрямую`); updateProgress(70, 'Загрузка данных скважины...'); await parseWellDataUniversal(xmlContent, well.wellId, fileType); updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); stopKeepAlive(); displayWellData(fileType, fileNumber, file.name, well.wellId); }, 100); } else { // Несколько скважин - показываем список console.log(`Найдено ${wells.length} скважин, показываем список`); currentMultiWellData = { wells: wells, fileNames: [fileObj.name], fileType: fileType, xmlContent: xmlContent // Сохраняем XML контент для мульти-файла }; currentMultiFileName = file.name;
updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); stopKeepAlive(); showWellsList(wells, file.name, 1); updateWellHeaderForMultiFile(fileType, file.name); wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>'; }, 100); }
} else { throw new Error('Неподдерживаемый формат файла. Поддерживаются только .zip и .inu файлы'); }
} catch (error) { // Гарантированно останавливаем keep-alive при ошибке stopKeepAlive(); hideLoading(); console.error('Ошибка загрузки данных:', error); wellDataElement.innerHTML = ` <div class="error"> <p><strong>Ошибка загрузки данных:</strong> ${error.message}</p> <p>Попробуйте выбрать другой файл</p> ${error.message.includes('Таймаут') ? ` <div style="margin-top: 15px; padding: 10px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px;"> <strong>Совет:</strong> Попробуйте выбрать меньший файл или проверьте стабильность интернет-соединения </div> ` : ''} </div> `; } } ```
## 2. Обновить функцию `selectFileManually` для поддержки INU:
```javascript async function selectFileManually() { try { const input = document.createElement('input'); input.type = 'file'; input.accept = '.inu,.zip'; // Разрешаем оба формата input.onchange = async (e) => { const file = e.target.files[0]; if (file) { // ОЧИСТКА СПИСКА ПРИ ВЫБОРЕ НОВОГО ФАЙЛА clearFileList(); clearCaches(); // Создаем временный file handle-like объект const tempFile = { name: file.name, handle: { getFile: () => Promise.resolve(file) } }; await loadWellData(tempFile, 0); } }; input.click(); } catch (error) { console.error('Ошибка выбора файла:', error); alert('Ошибка при выборе файла: ' + error.message); } } ```
## 3. Обновить функцию `parseWellDataFromMulti` для работы с INU:
```javascript async function parseWellDataFromMulti(wellId, signal = null) { // [существующий код проверок...] try { updateProgress(10, 'Поиск информации о скважине...'); // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } const targetWellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId); if (!targetWellMeta) { throw new Error(`Скважина с ID ${wellId} не найдена в списке`); }
let xmlContent = ''; let sourceFile = '';
// Случай 1: ZIP архив (данные в fileCache) if (targetWellMeta.sourceFile && fileCache.has(targetWellMeta.sourceFile)) { sourceFile = targetWellMeta.sourceFile; updateProgress(30, 'Загрузка данных из кеша...'); xmlContent = fileCache.get(sourceFile); } // Случай 2: INU файл (данные в currentMultiWellData.xmlContent) else if (currentMultiWellData.xmlContent) { updateProgress(30, 'Загрузка данных из INU файла...'); xmlContent = currentMultiWellData.xmlContent; sourceFile = 'current_inu_file'; } // Случай 3: Не удалось определить источник else { throw new Error('Не удалось определить источник данных для скважины'); }
// [дальнейшая обработка без изменений...] } // [обработка ошибок без изменений...] } ```
## 4. Обновить функцию поиска файлов для INU:
```javascript async function findFileInArchiveByBlobPath(blobPath, fileName) { // Если это INU файл (нет архива), возвращаем null if (!currentZip) { console.log('Это INU файл, архив не загружен'); return null; } // [существующая логика для ZIP архивов...] } ```
Теперь программа будет корректно определять тип файла (ZIP или INU) и применять соответствующую логику обработки. Для INU файлов она будет читать XML напрямую, без попыток распаковки как ZIP архива. Добавлено (2025-10-07, 13:34) --------------------------------------------- Понял проблему! Нужно вернуть рабочую версию и аккуратно добавить поддержку INU файлов. Давайте откатим изменения и сделаем правильную реализацию.
Вот исправленная версия функции `loadWellData`:
```javascript async function loadWellData(file, fileNumber) { // Запускаем keep-alive при начале загрузки startKeepAlive(); showLoading('Анализ архива...'); const wellDataElement = document.getElementById('wellData'); wellDataElement.innerHTML = '<div class="loading"><p>Загрузка данных...</p></div>';
try { console.log('Начало загрузки файла:', file.name); updateLoadingText('Чтение файла...'); updateProgress(5, 'Подготовка...');
const fileObj = await file.handle.getFile(); console.log('Размер файла:', fileObj.size, 'байт');
// ПРОСТАЯ ПРОВЕРКА ТИПА ФАЙЛА const fileName = fileObj.name.toLowerCase(); const isInuFile = fileName.endsWith('.inu'); if (isInuFile) { // ОБРАБОТКА INU ФАЙЛА console.log('Обработка INU файла'); updateLoadingText('Чтение INU файла...'); updateProgress(30, 'Чтение XML данных...');
// Читаем содержимое INU файла как текст const xmlContent = await fileObj.text(); console.log('INU файл прочитан, размер:', xmlContent.length, 'символов');
// Определяем тип файла по имени const fileType = getFileType(fileObj.name); console.log('Тип INU файла:', fileType);
updateProgress(50, 'Анализ данных скважины...');
// Ищем скважины в INU файле const wells = detectWellsInContent(xmlContent, fileObj.name); console.log('Найдено скважин в INU файле:', wells.length);
if (wells.length === 0) { throw new Error('В INU файле не найдено данных скважин'); }
// Сохраняем XML контент для быстрого доступа fileCache.set(fileObj.name, xmlContent);
// Для INU файлов создаем виртуальную структуру currentZip = { files: { // Создаем виртуальную запись файла для совместимости [fileObj.name]: { async: function(type) { return Promise.resolve(new TextEncoder().encode(xmlContent)); }, name: fileObj.name, dir: false } } };
// ОБРАБОТКА СКВАЖИН (одинаковая для ZIP и INU) if (wells.length === 1) { // Одна скважина - сразу загружаем и отображаем const well = wells[0]; console.log(`Одна скважина найдена: ${well.wellId}, загружаем напрямую`); updateProgress(70, 'Загрузка данных скважины...'); await parseWellDataUniversal(xmlContent, well.wellId, fileType); updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); stopKeepAlive(); displayWellData(fileType, fileNumber, file.name, well.wellId); }, 100); } else { // Несколько скважин - показываем список console.log(`Найдено ${wells.length} скважин, показываем список`); currentMultiWellData = { wells: wells, fileNames: [fileObj.name], fileType: fileType, xmlContent: xmlContent // Сохраняем XML контент }; currentMultiFileName = file.name;
updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); stopKeepAlive(); showWellsList(wells, file.name, 1); updateWellHeaderForMultiFile(fileType, file.name); wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>'; }, 100); }
} else { // ОБРАБОТКА ZIP АРХИВА (оригинальная логика) if (fileObj.size > 1500 * 1024 * 1024) { throw new Error('Файл слишком большой для обработки в браузере (максимум 1500 МБ)'); }
updateLoadingText('Распаковка архива...'); updateProgress(15, 'Распаковка архива...'); const arrayBuffer = await Promise.race([ fileObj.arrayBuffer(), new Promise((_, reject) => setTimeout(() => reject(new Error('Таймаут чтения файла')), 30000) ) ]); console.log('ArrayBuffer прочитан, размер:', arrayBuffer.byteLength);
updateLoadingText('Обработка данных...'); updateProgress(25, 'Обработка ZIP...'); const zip = await Promise.race([ JSZip.loadAsync(arrayBuffer), new Promise((_, reject) => setTimeout(() => reject(new Error('Таймаут распаковки архива')), 45000) ) ]); console.log('ZIP распакован, файлов:', Object.keys(zip.files).length); currentZip = zip;
// Получаем файлы с корректными именами const allFiles = getZipFileNames(zip); // Фильтруем только INU файлы const inuFiles = allFiles.filter(file => file.correctedName.startsWith('904~') || file.correctedName.startsWith('906~') || file.correctedName.startsWith('911~') || file.correctedName.startsWith('909~') );
if (inuFiles.length === 0) { throw new Error('В архиве не найдено INU файлов с данными'); }
console.log('Найдены INU файлы:', inuFiles.map(f => f.correctedName)); updateProgress(30, `Найдено ${inuFiles.length} файлов`);
// Получаем типы файлов для отображения const fileTypes = [...new Set(inuFiles.map(f => getFileType(f.correctedName)))]; const fileTypeText = fileTypes.map(type => { const typeNames = { 'multi': 'Выгрузка для ПС', 'all': 'Все типы документаций', 'final': 'Итоговая документация', 'primary': 'Первичная документация' }; return typeNames[type] || type; }).join(', ');
// ОБНОВЛЯЕМ ЗАГОЛОВОК СРАЗУ ПРИ ЗАГРУЗКЕ АРХИВА updateWellHeaderOnArchiveLoad(fileTypeText, file.name);
// Запускаем долгосрочный прогресс для обработки файлов const progressInterval = startLongOperationProgress('Обработка файлов');
// УНИВЕРСАЛЬНЫЙ ПОИСК СКВАЖИН ВО ВСЕХ ФАЙЛАХ const allWells = []; let processedFiles = 0;
for (const inuFile of inuFiles) { try { // Обновляем прогресс для каждого файла processedFiles++; const fileProgress = 30 + (processedFiles / inuFiles.length) * 40; updateProgress(fileProgress, `Обработка файлов: ${processedFiles}/${inuFiles.length}`);
console.log('Обработка файла:', inuFile.correctedName); // Добавляем таймаут для чтения каждого файла const xmlContent = await Promise.race([ readZipFileFast(inuFile.entry), new Promise((_, reject) => setTimeout(() => reject(new Error(`Таймаут чтения файла ${inuFile.correctedName}`)), 15000) ) ]); const wells = detectWellsInContent(xmlContent, inuFile.correctedName); console.log(`В файле найдено скважин:`, wells.length); // Сохраняем XML контент для быстрого доступа wells.forEach(well => { well.sourceFile = inuFile.correctedName; well.originalSourceFile = inuFile.originalName; well.xmlContent = xmlContent; well.fileType = getFileType(inuFile.correctedName); }); allWells.push(...wells); // Сохраняем в кеш файлов по оригинальному имени fileCache.set(inuFile.originalName, xmlContent); } catch (error) { console.error(`Ошибка обработки файла:`, error); // Продолжаем обработку других файлов даже при ошибке } }
// Останавливаем долгосрочный прогресс if (progressInterval) { clearInterval(progressInterval); }
if (allWells.length === 0) { throw new Error('В архиве не найдено данных скважин'); }
// ВАЖНО: Убираем дубликаты скважин по wellId const uniqueWells = []; const seenWellIds = new Set(); for (const well of allWells) { if (!seenWellIds.has(well.wellId)) { seenWellIds.add(well.wellId); uniqueWells.push(well); } }
console.log(`Уникальных скважин: ${uniqueWells.length} (всего найдено: ${allWells.length})`); updateProgress(80, `Найдено ${uniqueWells.length} скважин`);
// ЕДИНАЯ ЛОГИКА ДЛЯ ВСЕХ СЛУЧАЕВ if (uniqueWells.length === 1) { // Одна скважина - сразу загружаем и отображаем const well = uniqueWells[0]; console.log(`Одна скважина найдена: ${well.wellId}, загружаем напрямую`); updateProgress(90, 'Загрузка данных скважины...'); await parseWellDataUniversal(well.xmlContent, well.wellId, well.fileType); updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); stopKeepAlive(); displayWellData(well.fileType, fileNumber, file.name, well.wellId); }, 100); } else { // Несколько скважин - показываем список console.log(`Найдено ${uniqueWells.length} скважин, показываем список`); currentMultiWellData = { wells: uniqueWells, fileNames: [...new Set(uniqueWells.map(w => w.sourceFile))], fileType: fileTypes.length === 1 ? fileTypes[0] : 'mixed' }; currentMultiFileName = file.name;
updateProgress(100, 'Готово!'); setTimeout(() => { hideLoading(); stopKeepAlive(); showWellsList(uniqueWells, file.name, currentMultiWellData.fileNames.length); updateWellHeaderForMultiFile(currentMultiWellData.fileType, file.name); wellDataElement.innerHTML = '<div class="loading"><p>Выберите скважину из списка</p></div>'; }, 100); } }
} catch (error) { // Гарантированно останавливаем keep-alive при ошибке stopKeepAlive(); hideLoading(); console.error('Ошибка загрузки данных:', error); wellDataElement.innerHTML = ` <div class="error"> <p><strong>Ошибка загрузки данных:</strong> ${error.message}</p> <p>Попробуйте выбрать другой файл или архив</p> ${error.message.includes('Таймаут') ? ` <div style="margin-top: 15px; padding: 10px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 4px;"> <strong>Совет:</strong> Попробуйте выбрать меньший файл или проверьте стабильность интернет-соединения </div> ` : ''} </div> `; } } ```
Ключевые изменения:
1. **Простая проверка типа файла** - только по расширению `.inu` 2. **Для INU файлов** - читаем напрямую как текст и создаем виртуальную структуру для совместимости 3. **Для ZIP архивов** - оставляем оригинальную логику без изменений 4. **Общая обработка скважин** - одинаковый код для обоих типов файлов после получения XML
Теперь должно работать корректно для обоих типов файлов! Добавлено (2025-10-07, 13:43) --------------------------------------------- Понял проблему! Ошибка возникает потому что для INU файлов не создаются карты данных (classifierMap, employeeMap и т.д.). Нужно исправить функцию `parseWellDataUniversal` чтобы она работала для INU файлов.
Вот исправления:
## 1. Исправить функцию `parseWellDataUniversal`:
```javascript async function parseWellDataUniversal(xmlContent, wellId, fileType, signal = null) { // Проверяем кеш const cachedData = getCachedWellData(wellId); if (cachedData) { console.log('Используем кешированные данные для скважины:', wellId); // Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } // НЕМЕДЛЕННО устанавливаем данные и скрываем загрузку xmlData = cachedData; // Скрываем загрузку и возвращаем данные setTimeout(() => { hideLoading(); }, 50); return cachedData; }
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
// Запускаем keep-alive для долгих парсингов startKeepAlive();
showLoading('Загрузка данных скважины...');
try { updateProgress(5, 'Подготовка данных...'); // Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
// Имитируем прогресс для начальной обработки await new Promise(resolve => setTimeout(resolve, 100)); updateProgress(15, 'Анализ структуры данных...');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
updateProgress(25, 'Создание карт данных...'); // ВАЖНО: Проверяем что xmlContent не пустой и корректен if (!xmlContent || xmlContent.length < 100) { throw new Error('XML данные пустые или повреждены'); } console.log('Создание карт данных для XML длиной:', xmlContent.length); // Создаем карты данных с небольшими задержками для прогресса await new Promise(resolve => setTimeout(resolve, 50)); const classifierMap = createClassifierMap(xmlContent); console.log('Карта классификаторов создана, размер:', classifierMap.size); updateProgress(35, 'Карта классификаторов создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 50)); const employeeMap = createEmployeeMap(xmlContent); console.log('Карта сотрудников создана, размер:', employeeMap.size); updateProgress(45, 'Карта сотрудников создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 50)); const assayTypeMap = createAssayTypeMap(xmlContent); console.log('Карта типов опробования создана, размер:', assayTypeMap.size); updateProgress(55, 'Карта типов опробования создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 50)); const divisionMap = createDivisionMap(xmlContent); console.log('Карта подразделений создана, размер:', divisionMap.size); updateProgress(65, 'Карта подразделений создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
updateProgress(75, 'Извлечение основной информации о скважине...'); // Используем существующий быстрый парсер для всех типов файлов console.time(`Универсальный парсинг для ${wellId}`); // Добавляем таймаут для парсинга const wellData = await Promise.race([ parseAllWellDataFast(xmlContent, wellId, classifierMap, employeeMap, assayTypeMap, divisionMap), new Promise((_, reject) => setTimeout(() => reject(new Error('Таймаут парсинга данных скважины')), 60000) ) ]); console.timeEnd(`Универсальный парсинг для ${wellId}`);
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
if (wellData) { updateProgress(85, 'Обработка интервалов документирования...'); await new Promise(resolve => setTimeout(resolve, 50)); updateProgress(90, 'Обработка данных опробования...'); await new Promise(resolve => setTimeout(resolve, 50)); // Добавляем информацию о типе файла wellData.fileType = fileType; wellData.wellId = wellId; xmlData = wellData; updateProgress(95, 'Сохранение в кеш...'); await new Promise(resolve => setTimeout(resolve, 30)); // Сохраняем в кеш setCachedWellData(wellId, wellData); updateProgress(100, 'Данные скважины успешно загружены!'); // Проверяем отмену операции перед завершением if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } setTimeout(() => { hideLoading(); stopKeepAlive(); // Останавливаем keep-alive }, 200); return wellData; }
throw new Error('Не удалось извлечь данные скважины');
} catch (error) { // Игнорируем ошибки отмены if (error.name === 'AbortError') { console.log('Парсинг скважины отменен'); throw error; } // Гарантированно останавливаем keep-alive при ошибке stopKeepAlive(); hideLoading(); console.error('Ошибка универсального парсинга скважины:', error); console.error('XML content length:', xmlContent ? xmlContent.length : 'null'); console.error('WellId:', wellId); console.error('FileType:', fileType); // Очищаем кеш для этой скважины при ошибке wellDataCache.delete(wellId); // Показываем информативное сообщение об ошибке let errorMessage = error.message; if (error.message.includes('Таймаут')) { errorMessage = 'Превышено время обработки данных. Попробуйте выбрать другую скважину или файл меньшего размера.'; } else if (error.message.includes('пустые') || error.message.includes('повреждены')) { errorMessage = 'Данные файла повреждены или имеют неверный формат.'; } throw new Error(errorMessage); } } ```
## 2. Обновить функцию `parseAllWellDataFast`:
```javascript async function parseAllWellDataFast(xmlContent, wellId, classifierMap, employeeMap, assayTypeMap, divisionMap) { return new Promise((resolve) => { try { console.time(`Fast parse ALL data for ${wellId}`); console.log('Парсинг данных с картами:'); console.log('- classifierMap:', classifierMap.size); console.log('- employeeMap:', employeeMap.size); console.log('- assayTypeMap:', assayTypeMap.size); console.log('- divisionMap:', divisionMap.size); // [остальной код функции остается без изменений] // просто используем переданные карты вместо создания новых ```
## 3. Обновить вызов в `loadWellData` для INU файлов:
Найти в функции `loadWellData` блок для INU файлов где вызывается `parseWellDataUniversal` и убедиться что передается правильный xmlContent:
```javascript // В блоке обработки INU файлов: await parseWellDataUniversal(xmlContent, well.wellId, fileType); ```
## 4. Добавить защиту в функции создания карт:
```javascript function createClassifierMap(xmlContent) { const classifierMap = new Map(); try { // Проверяем что xmlContent существует if (!xmlContent || typeof xmlContent !== 'string') { console.warn('Некорректный xmlContent для создания карты классификаторов'); return classifierMap; } const classifierRegex = /<MR_DBA\.CLASSIFIER>[\s\S]*?<\/MR_DBA\.CLASSIFIER>/g; const classifierMatches = xmlContent.match(classifierRegex) || []; console.log('Найдено CLASSIFIER блоков:', classifierMatches.length); for (const match of classifierMatches) { const nodeId = extractValue(match, 'ID_NODE_ID'); const nodeName = extractValue(match, 'NODE_NAME'); if (nodeId && nodeName) { classifierMap.set(nodeId, nodeName); } } } catch (error) { console.error('Ошибка создания карты классификаторов:', error); } return classifierMap; } ```
Аналогично добавить проверки в другие функции создания карт (`createEmployeeMap`, `createAssayTypeMap`, `createDivisionMap`).
Теперь карты данных будут создаваться корректно для INU файлов, и ошибка "создания карт" должна исчезнуть! Добавлено (2025-10-07, 13:50) --------------------------------------------- Понял! Ошибка возникает потому что для мульти-файлов INU данные передаются неправильно. Давайте исправим логику передачи XML контента.
## 1. Исправить функцию `parseWellDataUniversal` - убрать строгую проверку:
```javascript async function parseWellDataUniversal(xmlContent, wellId, fileType, signal = null) { // Проверяем кеш const cachedData = getCachedWellData(wellId); if (cachedData) { console.log('Используем кешированные данные для скважины:', wellId); // Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } xmlData = cachedData; setTimeout(() => { hideLoading(); }, 50); return cachedData; }
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
// Запускаем keep-alive для долгих парсингов startKeepAlive();
showLoading('Загрузка данных скважины...');
try { updateProgress(5, 'Подготовка данных...'); // ПРОВЕРКА XML ДАННЫХ - БОЛЕЕ ГИБКАЯ if (!xmlContent) { throw new Error('XML данные отсутствуют'); } if (xmlContent.length < 50) { console.warn('XML данные очень короткие, но пробуем обработать:', xmlContent.length); } console.log('Начало парсинга XML длиной:', xmlContent.length, 'символов'); // Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 100)); updateProgress(15, 'Анализ структуры данных...');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
updateProgress(25, 'Создание карт данных...'); console.log('Создание карт данных...'); // Создаем карты данных с защитой от ошибок await new Promise(resolve => setTimeout(resolve, 50)); const classifierMap = createClassifierMap(xmlContent); console.log('Карта классификаторов создана, размер:', classifierMap.size); updateProgress(35, 'Карта классификаторов создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 50)); const employeeMap = createEmployeeMap(xmlContent); console.log('Карта сотрудников создана, размер:', employeeMap.size); updateProgress(45, 'Карта сотрудников создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 50)); const assayTypeMap = createAssayTypeMap(xmlContent); console.log('Карта типов опробования создана, размер:', assayTypeMap.size); updateProgress(55, 'Карта типов опробования создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
await new Promise(resolve => setTimeout(resolve, 50)); const divisionMap = createDivisionMap(xmlContent); console.log('Карта подразделений создана, размер:', divisionMap.size); updateProgress(65, 'Карта подразделений создана');
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
updateProgress(75, 'Извлечение основной информации о скважине...'); // Используем существующий быстрый парсер для всех типов файлов console.time(`Универсальный парсинг для ${wellId}`); // Передаем созданные карты в парсер const wellData = await parseAllWellDataFast(xmlContent, wellId, classifierMap, employeeMap, assayTypeMap, divisionMap); console.timeEnd(`Универсальный парсинг для ${wellId}`);
// Проверяем отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
if (wellData) { updateProgress(85, 'Обработка интервалов документирования...'); await new Promise(resolve => setTimeout(resolve, 50)); updateProgress(90, 'Обработка данных опробования...'); await new Promise(resolve => setTimeout(resolve, 50)); // Добавляем информацию о типе файла wellData.fileType = fileType; wellData.wellId = wellId; xmlData = wellData; updateProgress(95, 'Сохранение в кеш...'); await new Promise(resolve => setTimeout(resolve, 30)); // Сохраняем в кеш setCachedWellData(wellId, wellData); updateProgress(100, 'Данные скважины успешно загружены!'); // Проверяем отмену операции перед завершением if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } setTimeout(() => { hideLoading(); stopKeepAlive(); }, 200); return wellData; }
throw new Error('Не удалось извлечь данные скважины');
} catch (error) { // Игнорируем ошибки отмены if (error.name === 'AbortError') { console.log('Парсинг скважины отменен'); throw error; } // Гарантированно останавливаем keep-alive при ошибке stopKeepAlive(); hideLoading(); console.error('Ошибка универсального парсинга скважины:', error); console.error('XML content length:', xmlContent ? xmlContent.length : 'null'); console.error('WellId:', wellId); console.error('FileType:', fileType); // Очищаем кеш для этой скважины при ошибке wellDataCache.delete(wellId); throw error; // Пробрасываем оригинальную ошибку } } ```
## 2. Исправить функцию `parseWellDataFromMulti` для INU файлов:
```javascript async function parseWellDataFromMulti(wellId, signal = null) { // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
if (!currentMultiWellData || !currentMultiWellData.wells) { throw new Error('Данные мульти-файла не загружены'); }
// ПРОВЕРЯЕМ КЕШ с защитой от ошибок const cachedData = getCachedWellData(wellId); if (cachedData) { console.log('Используем кешированные данные для скважины:', wellId); // ПРОВЕРЯЕМ отмену операции перед возвратом данных if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } xmlData = cachedData; return; }
showLoading('Загрузка данных скважины...');
try { updateProgress(10, 'Поиск информации о скважине...'); // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } const targetWellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId); if (!targetWellMeta) { throw new Error(`Скважина с ID ${wellId} не найдена в списке`); }
let xmlContent = ''; let sourceFile = '';
// Случай 1: ZIP архив (данные в fileCache) if (targetWellMeta.sourceFile && fileCache.has(targetWellMeta.sourceFile)) { sourceFile = targetWellMeta.sourceFile; updateProgress(30, 'Загрузка данных из кеша...'); // ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } xmlContent = fileCache.get(sourceFile); if (!xmlContent) { throw new Error(`Файл ${sourceFile} не найден в кеше`); } } // Случай 2: INU файл (данные в currentMultiWellData.xmlContent) else if (currentMultiWellData.xmlContent) { updateProgress(30, 'Загрузка данных из INU файла...'); xmlContent = currentMultiWellData.xmlContent; sourceFile = 'current_inu_file'; console.log('Используем XML из currentMultiWellData, длина:', xmlContent.length); } // Случай 3: Данные в самом объекте скважины else if (targetWellMeta.xmlContent) { updateProgress(30, 'Загрузка данных из объекта скважины...'); xmlContent = targetWellMeta.xmlContent; sourceFile = 'well_object'; console.log('Используем XML из targetWellMeta, длина:', xmlContent.length); } // Случай 4: Не удалось определить источник else { throw new Error('Не удалось определить источник данных для скважины'); }
// ВАЖНО: Проверяем что xmlContent не пустой if (!xmlContent || xmlContent.length < 10) { throw new Error('XML данные пустые или отсутствуют для скважины ' + wellId); }
updateProgress(50, 'Быстрая обработка всех данных...');
// ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
// ИСПОЛЬЗУЕМ БЫСТРЫЙ МЕТОД ДЛЯ ВСЕХ ДАННЫХ console.time(`Parse ALL data for ${wellId}`); const allWellData = await parseAllWellDataFast(xmlContent, wellId); console.timeEnd(`Parse ALL data for ${wellId}`);
// ПРОВЕРЯЕМ отмену операции if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); }
if (allWellData) { // ВАЖНО: Сохраняем wellId в данных для корректного отображения allWellData.wellId = wellId; xmlData = allWellData; // Сохраняем в кеш с защитой от ошибок setCachedWellData(wellId, xmlData); updateProgress(100, 'Готово!'); // ПРОВЕРЯЕМ отмену операции перед завершением if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } setTimeout(() => { hideLoading(); }, 50); return; }
throw new Error('Не удалось извлечь данные скважины');
} catch (error) { // Игнорируем ошибки отмены if (error.name === 'AbortError') { throw error; } hideLoading(); console.error('Ошибка загрузки данных скважины:', error); // Очищаем кеш для этой скважины при ошибке try { wellDataCache.delete(wellId); } catch (e) { console.error('Ошибка очистки кеша:', e); } throw error; } } ```
## 3. Добавить отладочную информацию в `selectWell`:
```javascript function selectWell(wellId, wellNumber) { // Запоминаем позицию прокрутки const listsContainer = document.getElementById('listsContainer'); if (listsContainer) { lastScrollPosition = listsContainer.scrollTop; } // Скрываем меню в мобильной версии if (window.innerWidth <= 1024) { const sidebar = document.querySelector('.sidebar'); const overlay = document.getElementById('overlay'); if (sidebar) sidebar.classList.remove('active'); if (overlay) overlay.classList.remove('active'); } // Сброс активного класса у всех элементов document.querySelectorAll('.well-item').forEach(item => { item.classList.remove('active'); });
// Добавление активного класса к выбранному элементу event.currentTarget.classList.add('active');
currentWellId = wellId; // Находим данные скважины const wellMeta = currentMultiWellData.wells.find(w => w.wellId === wellId); if (!wellMeta) { console.error('Данные скважины не найдены:', wellId); return; } console.log('Выбрана скважина:', wellId); console.log('Данные скважины:', wellMeta); console.log('currentMultiWellData:', currentMultiWellData); abortController = new AbortController(); currentLoadingOperation = { wellId, timestamp: Date.now() }; showLoading('Загрузка данных скважины...'); // Используем setTimeout для асинхронной обработки setTimeout(async () => { // Проверяем, не была ли операция отменена if (!currentLoadingOperation || currentLoadingOperation.wellId !== wellId) { return; } try { // Используем универсальный парсер await parseWellDataFromMulti(wellId, abortController.signal); // ВАЖНО: После успешной загрузки ОБЯЗАТЕЛЬНО отображаем данные displayWellData(wellMeta.fileType, wellNumber, currentMultiFileName, wellId); // Очищаем операцию после успешной загрузки if (currentLoadingOperation && currentLoadingOperation.wellId === wellId) { currentLoadingOperation = null; abortController = null; } } catch (error) { // Игнорируем ошибки отмены if (error.name === 'AbortError') { console.log('Загрузка отменена пользователем'); return; } console.error('Ошибка загрузки скважины:', error); // Сбрасываем активный класс при ошибке event.currentTarget.classList.remove('active'); // Очищаем операцию при ошибке currentLoadingOperation = null; abortController = null; document.getElementById('wellData').innerHTML = ` <div class="error"> <p><strong>Ошибка загрузки данных скважины:</strong> ${error.message}</p> <p>Попробуйте выбрать другую скважину</p> </div> `; hideLoading(); } }, 50); } ```
Теперь XML данные должны передаваться корректно для INU файлов! Добавлено (2025-10-07, 13:58) --------------------------------------------- Отлично! Удалим функцию открытия отдельных INU файлов и заменим её на открытие ZIP архивов. Вот изменения:
## 1. Удаляем кнопку "Открыть INU файл" из HTML:
```javascript // В разделе actions-container заменяем: <div class="main-actions"> <button class="action-btn primary" id="selectFolderBtn"> <span class="btn-icon">📁</span> <span class="btn-text">Выбрать папку</span> </button> <button class="action-btn secondary" id="selectFileBtn"> <span class="btn-icon">📄</span> <span class="btn-text">Открыть ZIP</span> <!-- ← ИЗМЕНИЛИ ТЕКСТ --> </button> </div> ```
## 2. Обновляем функцию выбора файла:
```javascript // Удаляем старую функцию selectFileManually и заменяем на: async function selectZipFile() { try { const input = document.createElement('input'); input.type = 'file'; input.accept = '.zip'; // ← ТОЛЬКО ZIP ФАЙЛЫ input.onchange = async (e) => { const file = e.target.files[0]; if (file) { // ОЧИСТКА СПИСКА ПРИ ВЫБОРЕ НОВОГО ФАЙЛА clearFileList(); clearCaches(); // Создаем временный file handle-like объект const tempFile = { name: file.name, handle: { getFile: () => Promise.resolve(file) } }; // Обновляем отображение пути document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📦</span> <span class="folder-text">${file.name}</span> </div> `; await loadWellData(tempFile, 0); } }; input.click(); } catch (error) { console.error('Ошибка выбора ZIP файла:', error); alert('Ошибка при выборе ZIP файла: ' + error.message); } } ```
## 3. Обновляем обработчики в DOMContentLoaded:
```javascript // Заменяем обработчики: document.getElementById('selectFolderBtn').addEventListener('click', selectFolder); document.getElementById('selectFileBtn').addEventListener('click', selectZipFile); // ← ИЗМЕНИЛИ ФУНКЦИЮ ```
## 4. Обновляем функцию clearFileList для случая с ZIP:
```javascript function clearFileList() { const fileList = document.getElementById('fileList'); const wellsList = document.getElementById('wellsList'); const backButton = document.getElementById('backButton'); if (fileList) fileList.innerHTML = '<li class="file-item">Выберите папку или ZIP архив</li>'; // ← ОБНОВИЛИ ТЕКСТ if (wellsList) { wellsList.innerHTML = ''; wellsList.style.display = 'none'; } if (backButton) backButton.style.display = 'none'; // Сбрасываем глобальные переменные currentMultiWellData = null; currentWellId = null; currentMultiFileName = null; currentDirectoryHandle = null; // Обновляем счетчик document.getElementById('fileCounter').textContent = ''; // Сбрасываем путь к папке resetFolderPath(); }
function resetFolderPath() { document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📂</span> <span class="folder-text">Выберите папку или ZIP архив</span> <!-- ← ОБНОВИЛИ ТЕКСТ --> </div> `; } ```
## 5. Обновляем тексты интерфейса:
```javascript // В функции updateSidebarHeader: function updateSidebarHeader(title) { const sidebarHeader = document.querySelector('.sidebar-header h2'); if (sidebarHeader) { sidebarHeader.textContent = title; } }
// В начальном состоянии: document.addEventListener('DOMContentLoaded', function() { // ... существующий код ... // Устанавливаем начальный текст updateSidebarHeader('Список архивов'); document.getElementById('fileList').innerHTML = '<li class="file-item">Выберите папку или ZIP архив</li>'; }); ```
## 6. Обновляем тексты в счетчике файлов:
```javascript // В функции loadFileList обновляем тексты: const subfolderText = searchSubfolders ? ' (включая подпапки)' : ''; document.getElementById('fileCounter').textContent = `Найдено архивов: ${files.length}${subfolderText}`;
// И при отсутствии файлов: fileList.innerHTML = '<li class="file-item">В папке нет архивов ZIP</li>'; ```
## Полный код изменений:
```javascript // ЗАМЕНЯЕМ старую функцию selectFileManually на: async function selectZipFile() { try { const input = document.createElement('input'); input.type = 'file'; input.accept = '.zip'; input.onchange = async (e) => { const file = e.target.files[0]; if (file) { clearFileList(); clearCaches(); const tempFile = { name: file.name, handle: { getFile: () => Promise.resolve(file) } }; document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📦</span> <span class="folder-text">${file.name}</span> </div> `; await loadWellData(tempFile, 0); } }; input.click(); } catch (error) { console.error('Ошибка выбора ZIP файла:', error); alert('Ошибка при выборе ZIP файла: ' + error.message); } }
// ОБНОВЛЯЕМ обработчики в DOMContentLoaded: document.addEventListener('DOMContentLoaded', function() { document.getElementById('selectFolderBtn').addEventListener('click', selectFolder); document.getElementById('selectFileBtn').addEventListener('click', selectZipFile); // ← ИЗМЕНИЛИ // ... остальной код ... }); ```
Теперь приложение будет работать только с папками и отдельными ZIP архивами, что упростит интерфейс и логику работы. Добавлено (2025-10-07, 14:23) --------------------------------------------- Отлично! Добавим кнопки выгрузки в Excel для всех таблиц. Вот реализация:
## 1. Добавляем библиотеку для работы с Excel:
```javascript // Добавляем в head или перед закрывающим тегом body <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> ```
## 2. Создаем функцию для экспорта в Excel:
```javascript // Функция для экспорта таблицы в Excel function exportTableToExcel(tableElement, fileName) { try { // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Получаем данные из таблицы const table = tableElement.querySelector('table'); const worksheet = XLSX.utils.table_to_sheet(table); // Добавляем лист в книгу XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Сохраняем файл XLSX.writeFile(wb, `${fileName}_${new Date().toISOString().split('T')[0]}.xlsx`); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка экспорта в Excel:', error); showNotification('Ошибка при экспорте таблицы', 'error'); } }
// Функция создания кнопки экспорта function createExportButton(tableId, fileName) { return ` <div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;"> <button class="export-excel-btn" data-table-id="${tableId}" data-file-name="${fileName}" style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 500; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3); transition: all 0.2s ease;"> <span style="font-size: 1.1rem;">📊</span> <span>Экспорт в Excel</span> </button> </div> `; } ```
## 3. Обновляем функции отображения таблиц с кнопками экспорта:
### Для первичной документации: ```javascript function displayPrimaryDocumentation() { let html = ''; // Информация о первичном документировании if (xmlData.primaryDocInfo) { // ... существующий код ... } // Добавляем кнопку экспорта html += createExportButton('primaryDocTable', 'Первичное_документирование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="primaryDocTable"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`;
xmlData.primaryDocumentation.forEach(interval => { html += ` <tr> <td style="width: 50px;">${interval.numberPP}</td> <td style="width: 80px;">${formatNumber(interval.from)}</td> <td style="width: 80px;">${formatNumber(interval.to)}</td> <td style="width: 100px;">${formatNumber(interval.thickness)}</td> <td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td> <td style="width: 150px;">${interval.lithology}</td> <td><div class="description-text">${interval.description}</div></td> </tr>`; });
html += ` </tbody> </table> </div> </div> `; return html; } ```
### Для итоговой документации: ```javascript function displayFinalDocumentation() { let html = ''; // ... существующий код информации о документировании ... // Добавляем кнопку экспорта html += createExportButton('finalDocTable', 'Итоговое_документирование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="finalDocTable"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`;
// ... существующий код таблицы ... return html; } ```
### Для ГИС документации: ```javascript function displayGisDocumentation() { let html = ''; // ... существующий код ... if (xmlData.gisDocumentation.length > 0) { // Добавляем кнопку экспорта html += createExportButton('gisDocTable', 'ГИС_документирование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="gisDocTable"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`; // ... заполнение таблицы ... } return html; } ```
### Для опробования: ```javascript function displayAssays() { let html = ''; if (xmlData.assays.length > 0) { // Добавляем кнопку экспорта html += createExportButton('assaysTable', 'Опробование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="assaysTable"> <thead> <tr> <th style="width: 60px;">№ пробы</th> <th style="width: 120px;">Тип опробования</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Значение</th> <th style="width: 80px;">Ед. изм.</th> <th style="width: 120px;">Штрих-код</th> <th style="width: 120px;">Место отбора</th> <th style="width: 120px;">Автор отбора</th> <th style="width: 100px;">Дата отбора</th> <th>Описание</th> <th style="width: 150px;">Тип документирования</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`; // ... заполнение таблицы ... } return html; } ```
### Для керновых ящиков: ```javascript function displayCoreBoxes(coreBoxes) { if (!coreBoxes || coreBoxes.length === 0) { return '<div class="no-data"><p>Нет данных о керновых ящиках</p></div>'; } let html = ` <div class="doc-info-list" style="margin-bottom: 20px;"> <div class="doc-info-item"> <span class="doc-info-label">Всего керновых ящиков:</span> <span class="doc-info-value"><strong>${coreBoxes.length}</strong></span> </div> </div> `; // Добавляем кнопку экспорта html += createExportButton('coreBoxesTable', 'Керновые_ящики'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="coreBoxesTable"> <thead> <tr> <th style="width: 80px;">№ ящика</th> <th style="width: 100px;">От (м)</th> <th style="width: 100px;">До (м)</th> <th style="width: 100px;">Длина (м)</th> <th style="width: 120px;">Базальный горизонт</th> <th style="width: 200px;">Описание</th> <th style="width: 250px;">Связанные документы</th> <th style="width: 100px;">Дата добавления</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `; // ... заполнение таблицы ... return html; } ```
### Для вложенных документов: ```javascript // В функции displayAttachedDocuments добавляем после фильтра: html += createExportButton('documentsTable', 'Вложенные_документы');
// И обновляем ID таблицы: html += ` <div id="documentsTableSection"> <div class="table-wrapper" id="documentsTableWrapper"> <div class="table-header"> <table class="interval-table" id="documentsTable" style="min-width: 1600px;"> <!-- ... остальная таблица ... --> `; ```
## 4. Добавляем обработчики для кнопок экспорта:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); const tableElement = document.getElementById(tableId); if (tableElement) { exportTableToExcel(tableElement, fileName); } else { console.error('Таблица не найдена:', tableId); showNotification('Ошибка: таблица не найдена', 'error'); } }); // Добавляем стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## 5. Обновляем функцию displayWellData для инициализации кнопок:
```javascript // В функции displayWellData, после инициализации document buttons: setTimeout(() => { // ... существующие обработчики ... // ОБЩИЕ обработчики для кнопок документов initializeDocumentButtons(); // ОБРАБОТЧИК ДЛЯ ФИЛЬТРА ДОКУМЕНТОВ const filterCheckbox = document.getElementById('filterRelatedDocuments'); if (filterCheckbox) { filterCheckbox.addEventListener('change', function() { filterDocumentsByStatus(this.checked); }); // Применяем фильтр сразу (по умолчанию включен) filterDocumentsByStatus(true); } // ИНИЦИАЛИЗИРУЕМ КНОПКИ ЭКСПОРТА ← ДОБАВЛЯЕМ ЭТУ СТРОЧКУ initializeExportButtons(); }, 100); ```
## 6. Добавляем CSS стили для кнопок:
```css /* Добавляем в CSS секцию */ .export-excel-btn:hover { background: linear-gradient(135deg, #219653 0%, #1e8449 100%) !important; transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(39, 174, 96, 0.4) !important; }
.export-excel-btn:active { transform: translateY(0) !important; box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3) !important; }
.export-section { margin-bottom: 15px; display: flex; justify-content: flex-end; border-bottom: 1px solid #eee; padding-bottom: 10px; } ```
Теперь над каждой таблицей будет появляться кнопка "Экспорт в Excel", которая выгружает таблицу в форматированный Excel файл с текущей датой в названии. Добавлено (2025-10-07, 14:44) --------------------------------------------- Вот полный код функции `displayWellData` с добавленными кнопками экспорта:
```javascript async function displayWellData(fileType, fileNumber, fileName, wellId = null, signal = null) { const wellDataElement = document.getElementById('wellData'); // ГАРАНТИРУЕМ что загрузка скрыта перед отображением hideLoading(); // Проверяем что данные действительно загружены if (!xmlData) { console.error('Нет данных для отображения'); wellDataElement.innerHTML = ` <div class="error"> <p>Ошибка: данные не загружены</p> <p>Попробуйте выбрать скважину еще раз</p> </div> `; return; } // Проверяем отмену операции перед отображением if (signal && signal.aborted) { throw new DOMException('Operation aborted', 'AbortError'); } // Обновление заголовка const badgeClass = fileType === 'all' ? 'all' : fileType === 'primary' ? 'primary' : fileType === 'final' ? 'final' : fileType === 'multi' ? 'multi' : 'unknown';
const badgeText = fileType === 'all' ? 'Все типы документаций' : fileType === 'primary' ? 'Первичная документация' : fileType === 'final' ? 'Итоговая документация' : fileType === 'multi' ? 'Выгрузка для ПС' : 'Неизвестный тип';
document.getElementById('wellName').innerHTML = ` <div class="well-header-content"> <div class="doc-type-badge ${badgeClass}">${badgeText}</div> <div class="well-title"> ${xmlData.lineNumber && xmlData.lineNumber !== 'Не указано' ? ` <div class="well-line-container"> <div class="well-line-label">Линия №:</div> <div class="well-line-number">${xmlData.lineNumber}</div> </div> ` : ''} <div class="well-number-container"> <div class="well-number-label">Номер выработки:</div> <div class="well-line-number">${xmlData.wellName}</div> </div> </div> ${xmlData.wellType && xmlData.wellType !== 'Не указано' ? ` <div class="well-type-inline"> <span class="well-type-label">Тип скважины:</span> <span class="well-type-value">${xmlData.wellType}</span> </div> ` : ''} </div> `; // Создание HTML для отображения данных с ИСПРАВЛЕННОЙ СТРУКТУРОЙ ВКЛАДОК let html = '';
// Основная информация о скважине html += ` <div class="section"> <h2>Основная информация</h2> <div class="info-group"> <div class="info-group-title">Объект и участок</div> <div class="doc-info-list"> <div class="doc-info-item"> <span class="doc-info-label">Объект работ:</span> <span class="doc-info-value"><strong>${formatLongText(xmlData.object || 'Не указан')}</strong></span> </div> <div class="doc-info-item ${!xmlData.area || xmlData.area === 'Не указано' ? 'missing-field-simple' : ''}"> <span class="doc-info-label">Геологический участок:</span> <span class="doc-info-value"> ${xmlData.area && xmlData.area !== 'Не указано' ? `<strong>${formatLongText(xmlData.area)}</strong>` : '<em class="not-specified-text">не указано</em>'} </span> </div> </div> </div>
<div class="info-group"> <div class="info-group-title">Бурение</div> <div class="doc-info-list"> <div class="doc-info-item ${!xmlData.drillingStartDate || xmlData.drillingStartDate === '' ? 'missing-field-simple' : ''}"> <span class="doc-info-label">Начало бурения:</span> <span class="doc-info-value">${xmlData.drillingStartDate || 'Не указано'}</span> </div> <div class="doc-info-item ${!xmlData.drillingEndDate || xmlData.drillingEndDate === '' ? 'missing-field-simple' : ''}"> <span class="doc-info-label">Окончание бурения:</span> <span class="doc-info-value">${xmlData.drillingEndDate || 'Не указано'}</span> </div> <div class="doc-info-item ${!xmlData.drillingBrigade || xmlData.drillingBrigade === 'Не указано' ? 'missing-field-simple' : ''}"> <span class="doc-info-label">Бригада бурения:</span> <span class="doc-info-value">${formatLongText(xmlData.drillingBrigade || 'Не указано')}</span> </div> </div> </div> ${xmlData.wellDescription && xmlData.wellDescription !== 'Не указано' ? ` <div class="info-group"> <div class="info-group-title">Описание ТН</div> <div class="description-block"> <p>${formatLongText(xmlData.wellDescription, 100)}</p> </div> </div> ` : ''} </div> `;
// ИСПРАВЛЕННАЯ СТРУКТУРА ВКЛАДОК const hasPrimary = xmlData.primaryDocumentation && xmlData.primaryDocumentation.length > 0; const hasFinal = xmlData.finalDocumentation && xmlData.finalDocumentation.length > 0; const hasGis = xmlData.gisDocumentation && xmlData.gisDocumentation.length > 0; const hasAssays = xmlData.assays && xmlData.assays.length > 0; const hasCoreBoxes = xmlData.coreBoxes && xmlData.coreBoxes.length > 0; const hasDocuments = xmlData.attachedDocuments && xmlData.attachedDocuments.length > 0;
// Создаем основные вкладки if (hasPrimary || hasFinal || hasGis || hasAssays || hasCoreBoxes || hasDocuments) { html += `<div class="section">`; html += `<h2>Данные скважины</h2>`; // Определяем первую активную вкладку let firstActiveTab = ''; if (hasPrimary || hasFinal || hasGis || hasAssays) firstActiveTab = 'documentation'; else if (hasCoreBoxes) firstActiveTab = 'coreboxes'; else if (hasDocuments) firstActiveTab = 'documents'; else firstActiveTab = 'drilling'; // Создаем основные вкладки html += `<div class="tabs">`; // Вкладка 1: Документация if (hasPrimary || hasFinal || hasGis || hasAssays) { html += `<div class="tab ${firstActiveTab === 'documentation' ? 'active' : ''}" data-tab="documentation">Документация</div>`; } // Вкладка 2: Проходка html += `<div class="tab ${firstActiveTab === 'drilling' ? 'active' : ''}" data-tab="drilling">Проходка</div>`; // Вкладка 3: Керновые ящики if (hasCoreBoxes) { html += `<div class="tab ${firstActiveTab === 'coreboxes' ? 'active' : ''}" data-tab="coreboxes">Керновые ящики</div>`; } // Вкладка 4: Вложенные документы if (hasDocuments) { html += `<div class="tab ${firstActiveTab === 'documents' ? 'active' : ''}" data-tab="documents">Вложенные документы</div>`; } html += `</div>`;
// Содержимое вкладки "Документация" if (hasPrimary || hasFinal || hasGis || hasAssays) { html += `<div id="documentationTab" class="tab-content ${firstActiveTab === 'documentation' ? 'active' : ''}">`; // Определяем первую активную подвкладку let firstActiveSubTab = ''; if (hasPrimary) firstActiveSubTab = 'primary'; else if (hasFinal) firstActiveSubTab = 'final'; else if (hasGis) firstActiveSubTab = 'gis'; else if (hasAssays) firstActiveSubTab = 'assays'; // Внутренние вкладки для документации - оформляем как настоящие вкладки html += `<div class="inner-tabs-container">`; html += `<div class="inner-tabs">`; if (hasPrimary) { html += `<div class="inner-tab ${firstActiveSubTab === 'primary' ? 'active' : ''}" data-inner-tab="primary">Первичное документирование</div>`; } if (hasFinal) { html += `<div class="inner-tab ${firstActiveSubTab === 'final' ? 'active' : ''}" data-inner-tab="final">Итоговое документирование</div>`; } if (hasGis) { html += `<div class="inner-tab ${firstActiveSubTab === 'gis' ? 'active' : ''}" data-inner-tab="gis">Документирование по ГИС</div>`; } if (hasAssays) { html += `<div class="inner-tab ${firstActiveSubTab === 'assays' ? 'active' : ''}" data-inner-tab="assays">Опробование</div>`; } html += `</div>`; // закрываем inner-tabs // Содержимое внутренних вкладок html += `<div class="inner-tabs-content">`; if (hasPrimary) { html += `<div id="primaryInnerTab" class="inner-tab-content ${firstActiveSubTab === 'primary' ? 'active' : ''}">`; html += displayPrimaryDocumentation(); html += `</div>`; }
if (hasFinal) { html += `<div id="finalInnerTab" class="inner-tab-content ${firstActiveSubTab === 'final' ? 'active' : ''}">`; html += displayFinalDocumentation(); html += `</div>`; }
if (hasGis) { html += `<div id="gisInnerTab" class="inner-tab-content ${firstActiveSubTab === 'gis' ? 'active' : ''}">`; html += displayGisDocumentation(); html += `</div>`; }
if (hasAssays) { html += `<div id="assaysInnerTab" class="inner-tab-content ${firstActiveSubTab === 'assays' ? 'active' : ''}">`; html += displayAssays(); html += `</div>`; } html += `</div>`; // закрываем inner-tabs-content html += `</div>`; // закрываем inner-tabs-container html += `</div>`; // закрываем documentationTab }
// Содержимое вкладки "Проходка" html += `<div id="drillingTab" class="tab-content ${firstActiveTab === 'drilling' ? 'active' : ''}">`; html += ` <div class="no-data" style="text-align: center; padding: 40px 20px;"> <div style="font-size: 3rem; margin-bottom: 20px; color: #3498db;">🚧</div> <h3 style="color: #2c3e50; margin-bottom: 15px;">Раздел в разработке</h3> <p style="color: #7f8c8d; line-height: 1.6; max-width: 500px; margin: 0 auto;"> Функциональность раздела "Проходка" находится в стадии разработки.<br> В будущем здесь будет отображаться информация о процессе бурения скважины. </p> </div> `; html += `</div>`;
// Содержимое вкладки "Керновые ящики" if (hasCoreBoxes) { html += `<div id="coreboxesTab" class="tab-content ${firstActiveTab === 'coreboxes' ? 'active' : ''}">`; html += displayCoreBoxes(xmlData.coreBoxes); html += `</div>`; }
// Содержимое вкладки "Вложенные документы" if (hasDocuments) { html += `<div id="documentsTab" class="tab-content ${firstActiveTab === 'documents' ? 'active' : ''}">`; html += displayAttachedDocuments(xmlData.attachedDocuments); html += `</div>`; }
html += `</div>`; // закрываем section } else { html += ` <div class="section"> <h2>Данные скважины</h2> <div class="no-data"> <p>Нет данных о документировании и опробовании</p> </div> </div> `; }
wellDataElement.innerHTML = html; // Добавляем обработчики для вкладок после рендеринга setTimeout(() => { // Обработчики для основных вкладок document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchMainTab(tabName); }); });
// Обработчики для внутренних вкладок документации document.querySelectorAll('.inner-tab').forEach(innerTab => { innerTab.addEventListener('click', function() { const innerTabName = this.getAttribute('data-inner-tab'); switchInnerTab(innerTabName); }); });
// ОБЩИЕ обработчики для кнопок документов initializeDocumentButtons(); // ОБРАБОТЧИК ДЛЯ ФИЛЬТРА ДОКУМЕНТОВ const filterCheckbox = document.getElementById('filterRelatedDocuments'); if (filterCheckbox) { filterCheckbox.addEventListener('change', function() { filterDocumentsByStatus(this.checked); }); // Применяем фильтр сразу (по умолчанию включен) filterDocumentsByStatus(true); } // ИНИЦИАЛИЗИРУЕМ КНОПКИ ЭКСПОРТА initializeExportButtons(); }, 100); console.log('Данные скважины успешно отображены'); } ```
Также добавьте эти вспомогательные функции, если их еще нет:
```javascript // Функция переключения основных вкладок function switchMainTab(tabName) { // Скрываем все содержимое вкладок document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Убираем активный класс со всех вкладок document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // Активируем выбранную вкладку const selectedContent = document.getElementById(tabName + 'Tab'); const selectedTab = document.querySelector(`.tab[data-tab="${tabName}"]`); if (selectedContent) { selectedContent.classList.add('active'); } if (selectedTab) { selectedTab.classList.add('active'); } }
// Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); const tableElement = document.getElementById(tableId); if (tableElement) { exportTableToExcel(tableElement, fileName); } else { console.error('Таблица не найдена:', tableId); showNotification('Ошибка: таблица не найдена', 'error'); } }); // Добавляем стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
Теперь функция `displayWellData` включает все кнопки экспорта для таблиц и правильную структуру вкладок. Добавлено (2025-10-07, 14:58) --------------------------------------------- Вот полные функции всех документаций, ящиков и документов с добавленными кнопками экспорта:
## 1. Функция displayPrimaryDocumentation:
```javascript function displayPrimaryDocumentation() { let html = ''; // Информация о первичном документировании if (xmlData.primaryDocInfo) { let hasDocInfo = false; let docInfoHtml = ''; if (xmlData.primaryDocInfo.startDate && xmlData.primaryDocInfo.startDate !== '') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Начало документирования:</span> <span class="doc-info-value">${xmlData.primaryDocInfo.startDate}</span> </div> `; hasDocInfo = true; } if (xmlData.primaryDocInfo.endDate && xmlData.primaryDocInfo.endDate !== '') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Окончание документирования:</span> <span class="doc-info-value">${xmlData.primaryDocInfo.endDate}</span> </div> `; } else if (xmlData.primaryDocInfo.startDate && xmlData.primaryDocInfo.startDate !== '') { docInfoHtml += ` <div class="doc-info-item missing-field"> <span class="doc-info-label">Окончание документирования:</span> <span class="doc-info-value">Не указано (обязательное поле)</span> </div> `; } hasDocInfo = true; if (xmlData.primaryDocInfo.depth && xmlData.primaryDocInfo.depth !== 'Не указано') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Фактическая глубина:</span> <span class="doc-info-value">${xmlData.primaryDocInfo.depth} м</span> </div> `; hasDocInfo = true; } if (xmlData.primaryDocInfo.author && xmlData.primaryDocInfo.author !== 'Не указано') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Документатор:</span> <span class="doc-info-value">${formatLongText(xmlData.primaryDocInfo.author)}</span> </div> `; hasDocInfo = true; } if (hasDocInfo) { html += `<div class="doc-info-list">${docInfoHtml}</div>`; } } // Добавляем кнопку экспорта html += createExportButton('primaryDocTable', 'Первичное_документирование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="primaryDocTable"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`;
xmlData.primaryDocumentation.forEach(interval => { html += ` <tr> <td style="width: 50px;">${interval.numberPP}</td> <td style="width: 80px;">${formatNumber(interval.from)}</td> <td style="width: 80px;">${formatNumber(interval.to)}</td> <td style="width: 100px;">${formatNumber(interval.thickness)}</td> <td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td> <td style="width: 150px;">${interval.lithology}</td> <td><div class="description-text">${interval.description}</div></td> </tr>`; });
html += ` </tbody> </table> </div> </div> `; return html; } ```
## 2. Функция displayFinalDocumentation:
```javascript function displayFinalDocumentation() { let html = ''; // Информация об итоговом документировании if (xmlData.finalDocInfo) { let hasDocInfo = false; let docInfoHtml = ''; if (xmlData.finalDocInfo.startDate && xmlData.finalDocInfo.startDate !== '') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Начало документирования:</span> <span class="doc-info-value">${xmlData.finalDocInfo.startDate}</span> </div> `; hasDocInfo = true; } if (xmlData.finalDocInfo.endDate && xmlData.finalDocInfo.endDate !== '') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Окончание документирования:</span> <span class="doc-info-value">${xmlData.finalDocInfo.endDate}</span> </div> `; } else if (xmlData.finalDocInfo.startDate && xmlData.finalDocInfo.startDate !== '') { docInfoHtml += ` <div class="doc-info-item missing-field"> <span class="doc-info-label">Окончание документирования:</span> <span class="doc-info-value">Не указано (обязательное поле)</span> </div> `; } hasDocInfo = true; if (xmlData.finalDocInfo.depth && xmlData.finalDocInfo.depth !== 'Не указано') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Фактическая глубина:</span> <span class="doc-info-value">${xmlData.finalDocInfo.depth} м</span> </div> `; hasDocInfo = true; } if (xmlData.finalDocInfo.author && xmlData.finalDocInfo.author !== 'Не указано') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Документатор:</span> <span class="doc-info-value">${formatLongText(xmlData.finalDocInfo.author)}</span> </div> `; hasDocInfo = true; } if (hasDocInfo) { html += `<div class="doc-info-list">${docInfoHtml}</div>`; } } // Добавляем кнопку экспорта html += createExportButton('finalDocTable', 'Итоговое_документирование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="finalDocTable"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`;
xmlData.finalDocumentation.forEach(interval => { html += ` <tr> <td style="width: 50px;">${interval.numberPP}</td> <td style="width: 80px;">${formatNumber(interval.from)}</td> <td style="width: 80px;">${formatNumber(interval.to)}</td> <td style="width: 100px;">${formatNumber(interval.thickness)}</td> <td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td> <td style="width: 150px;">${interval.lithology}</td> <td><div class="description-text">${interval.description}</div></td> </tr>`; });
html += ` </tbody> </table> </div> </div> `; return html; } ```
## 3. Функция displayGisDocumentation:
```javascript function displayGisDocumentation() { let html = ''; // Информация о ГИС документировании if (xmlData.gisDocInfo) { let hasDocInfo = false; let docInfoHtml = ''; if (xmlData.gisDocInfo.startDate && xmlData.gisDocInfo.startDate !== '') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Начало документирования:</span> <span class="doc-info-value">${xmlData.gisDocInfo.startDate}</span> </div>`; hasDocInfo = true; } if (xmlData.gisDocInfo.endDate && xmlData.gisDocInfo.endDate !== '') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Окончание документирования:</span> <span class="doc-info-value">${xmlData.gisDocInfo.endDate}</span> </div>`; } else if (xmlData.gisDocInfo.startDate && xmlData.gisDocInfo.startDate !== '') { docInfoHtml += ` <div class="doc-info-item missing-field"> <span class="doc-info-label">Окончание документирования:</span> <span class="doc-info-value">Не указано (обязательное поле)</span> </div>`; } hasDocInfo = true; if (xmlData.gisDocInfo.depth && xmlData.gisDocInfo.depth !== 'Не указано') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Фактическая глубина:</span> <span class="doc-info-value">${xmlData.gisDocInfo.depth} м</span> </div>`; hasDocInfo = true; } if (xmlData.gisDocInfo.author && xmlData.gisDocInfo.author !== 'Не указано') { docInfoHtml += ` <div class="doc-info-item"> <span class="doc-info-label">Документатор:</span> <span class="doc-info-value">${formatLongText(xmlData.gisDocInfo.author)}</span> </div>`; hasDocInfo = true; } if (hasDocInfo) { html += `<div class="doc-info-list">${docInfoHtml}</div>`; } } if (xmlData.gisDocumentation.length > 0) { // Добавляем кнопку экспорта html += createExportButton('gisDocTable', 'ГИС_документирование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="gisDocTable"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`;
xmlData.gisDocumentation.forEach(interval => { html += ` <tr> <td style="width: 50px;">${interval.numberPP}</td> <td style="width: 80px;">${formatNumber(interval.from)}</td> <td style="width: 80px;">${formatNumber(interval.to)}</td> <td style="width: 100px;">${formatNumber(interval.thickness)}</td> <td style="width: 150px;" class="stratigraphy-column">${interval.stratigraphy}</td> <td style="width: 150px;">${interval.lithology}</td> <td><div class="description-text">${interval.description}</div></td> </tr>`; });
html += ` </tbody> </table> </div> </div> `; } else { html += `<div class="no-data"><p>Нет данных ГИС документирования</p></div>`; } return html; } ```
## 4. Функция displayAssays:
```javascript function displayAssays() { let html = ''; if (xmlData.assays && xmlData.assays.length > 0) { // Добавляем кнопку экспорта html += createExportButton('assaysTable', 'Опробование'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="assaysTable"> <thead> <tr> <th style="width: 60px;">№ пробы</th> <th style="width: 120px;">Тип опробования</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Значение</th> <th style="width: 80px;">Ед. изм.</th> <th style="width: 120px;">Штрих-код</th> <th style="width: 120px;">Место отбора</th> <th style="width: 120px;">Автор отбора</th> <th style="width: 100px;">Дата отбора</th> <th>Описание</th> <th style="width: 150px;">Тип документирования</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`;
xmlData.assays.forEach(assay => { html += ` <tr> <td style="width: 60px;">${assay.number}</td> <td style="width: 120px;">${assay.type}</td> <td style="width: 80px;">${formatNumber(assay.from)}</td> <td style="width: 80px;">${formatNumber(assay.to)}</td> <td style="width: 100px;">${assay.value}</td> <td style="width: 80px;">${assay.unit}</td> <td style="width: 120px;">${assay.barcode}</td> <td style="width: 120px;">${assay.place}</td> <td style="width: 120px;">${assay.author}</td> <td style="width: 100px;">${assay.samplingDate}</td> <td><div class="description-text">${assay.description}</div></td> <td style="width: 150px;">${assay.documentationType}</td> </tr>`; });
html += ` </tbody> </table> </div> </div> `; } else { html += `<div class="no-data"><p>Нет данных опробования</p></div>`; } return html; } ```
## 5. Функция displayCoreBoxes:
```javascript function displayCoreBoxes(coreBoxes) { if (!coreBoxes || coreBoxes.length === 0) { return '<div class="no-data"><p>Нет данных о керновых ящиках</p></div>'; } let html = ` <div class="doc-info-list" style="margin-bottom: 20px;"> <div class="doc-info-item"> <span class="doc-info-label">Всего керновых ящиков:</span> <span class="doc-info-value"><strong>${coreBoxes.length}</strong></span> </div> </div> `; // Добавляем кнопку экспорта html += createExportButton('coreBoxesTable', 'Керновые_ящики'); html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="coreBoxesTable"> <thead> <tr> <th style="width: 80px;">№ ящика</th> <th style="width: 100px;">От (м)</th> <th style="width: 100px;">До (м)</th> <th style="width: 100px;">Длина (м)</th> <th style="width: 120px;">Базальный горизонт</th> <th style="width: 200px;">Описание</th> <th style="width: 250px;">Связанные документы</th> <th style="width: 100px;">Дата добавления</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `; coreBoxes.forEach((box, index) => { const basalClass = box.basalHorizon === 'Базальный горизонт' ? 'basal-horizon' : ''; const hasDocuments = box.relatedDocuments && box.relatedDocuments.length > 0; html += ` <tr class="${basalClass}"> <td style="width: 80px;"> <strong>${box.boxNumber}</strong> </td> <td style="width: 100px;">${formatNumber(box.intervalFrom)}</td> <td style="width: 100px;">${formatNumber(box.intervalTo)}</td> <td style="width: 100px;">${formatNumber(box.thickness)}</td> <td style="width: 120px;">${box.basalHorizon}</td> <td style="width: 200px;"><div class="description-text">${box.description}</div></td> <td style="width: 250px;"> ${hasDocuments ? `<div class="documents-list" style="display: flex; flex-direction: column; gap: 5px;"> ${box.relatedDocuments.map(doc => { const isImage = isImageFile(doc.downloadFileName); // СОЗДАЕМ УНИКАЛЬНОЕ ОТОБРАЖАЕМОЕ ИМЯ let displayName = createUniqueDisplayName(doc); return ` <div style="display: flex; justify-content: space-between; align-items: center; padding: 4px 8px; background: #f8f9fa; border-radius: 4px; border: 1px solid #e9ecef;"> <div style="flex: 1; min-width: 0;"> <div style="font-weight: 500; font-size: 0.8rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${getDocumentTooltip(doc)}"> ${displayName} </div> <div style="font-size: 0.7rem; color: #6c757d; display: flex; justify-content: space-between;"> <span>${doc.fileType || 'Неизвестный тип'}</span> <span style="font-family: monospace; font-size: 0.6rem; color: #999;">ID: ${doc.id}</span> </div> </div> <div style="display: flex; gap: 3px; flex-shrink: 0;"> <button class="preview-doc-btn corebox-preview-btn" data-doc-id="${doc.id}" data-file-name="${doc.downloadFileName}" data-file-type="${doc.fileType}" data-is-image="${isImage}" data-blob-path="${doc.blobPath || ''}" style="padding: 2px 6px; font-size: 0.65rem; background-color: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer;" title="Просмотр"> 👁 </button> <button class="download-doc-btn corebox-download-btn" data-doc-id="${doc.id}" data-file-name="${doc.downloadFileName}" data-blob-path="${doc.blobPath || ''}" style="padding: 2px 6px; font-size: 0.65rem; background-color: #27ae60; color: white; border: none; border-radius: 3px; cursor: pointer;" title="Скачать"> 📥 </button> </div> </div> `; }).join('')} </div>` : `<span style="color: #95a5a6; font-style: italic;">нет связанных документов</span>` } </td> <td style="width: 100px;">${box.inputDate || 'Не указана'}</td> </tr> `; }); html += ` </tbody> </table> </div> </div> `; return html; } ```
## 6. Функция displayAttachedDocuments:
```javascript function displayAttachedDocuments(documents) { if (!documents || documents.length === 0) { return '<div class="no-data"><p>Нет вложенных документов</p></div>'; } // Получаем информацию о текущей открытой скважине const currentWellName = xmlData ? xmlData.wellName : ''; const currentLineNumber = xmlData ? xmlData.lineNumber : ''; // Форматируем отображение текущей скважины для сравнения let currentWellDisplay = ''; if (currentLineNumber && currentLineNumber !== 'Не указано' && currentWellName) { currentWellDisplay = `л${currentLineNumber} - ${currentWellName}`; } else if (currentWellName) { currentWellDisplay = currentWellName; } // СОРТИРУЕМ ДОКУМЕНТЫ: сначала связанные с текущей скважиной, потом остальные const sortedDocuments = [...documents].sort((a, b) => { const aIsCurrent = a.wellInfo && currentWellDisplay && a.wellInfo === currentWellDisplay; const bIsCurrent = b.wellInfo && currentWellDisplay && b.wellInfo === currentWellDisplay; if (aIsCurrent && !bIsCurrent) return -1; // a выше if (!aIsCurrent && bIsCurrent) return 1; // b выше return 0; // сохраняем порядок }); // Считаем статистику const relatedCount = sortedDocuments.filter(doc => { const isCurrent = doc.wellInfo && currentWellDisplay && doc.wellInfo === currentWellDisplay; return isCurrent; }).length; const otherCount = sortedDocuments.length - relatedCount; let html = ` <div class="doc-info-list" style="margin-bottom: 20px;"> <div class="doc-info-item"> <span class="doc-info-label">Всего документов:</span> <span class="doc-info-value"><strong>${sortedDocuments.length}</strong></span> </div> <div class="doc-info-item"> <span class="doc-info-label">Связано с текущей скважиной:</span> <span class="doc-info-value"><strong style="color: #27ae60;">${relatedCount}</strong></span> </div> <div class="doc-info-item"> <span class="doc-info-label">Другие документы:</span> <span class="doc-info-value"><strong style="color: #e67e22;">${otherCount}</strong></span> </div> <div class="doc-info-item"> <span class="doc-info-label">Текущая скважина:</span> <span class="doc-info-value"><strong>${currentWellDisplay || 'Не определена'}</strong></span> </div> </div> <!-- ФИЛЬТР ПО СТАТУСУ --> <div class="filter-section" style="margin-bottom: 20px;"> <div class="filter-container" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 20px; border-radius: 8px; border: 1px solid #dee2e6; box-shadow: 0 2px 4px rgba(0,0,0,0.05);"> <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;"> <div style="display: flex; align-items: center; gap: 12px;"> <label class="checkbox-label-modern" style="margin: 0; font-size: 1rem;"> <input type="checkbox" id="filterRelatedDocuments" checked> <span class="checkmark-modern" style="width: 20px; height: 20px;"></span> <span class="checkbox-text" style="font-weight: 600; color: #2c3e50; font-size: 1rem;"> Показывать только документы, связанные с текущей скважиной </span> </label> </div> <div id="filterStatus" style="background: #27ae60; color: white; padding: 6px 12px; border-radius: 20px; font-size: 0.85rem; font-weight: 500;"> Фильтр активен </div> </div> <div style="color: #6c757d; font-size: 0.9rem; line-height: 1.4;"> ${relatedCount > 0 ? `При включенном фильтре отображаются только <strong>${relatedCount}</strong> документов, связанных с текущей скважиной` : `При включенном фильтре документов не найдено - отключите фильтр для просмотра всех документов` } </div> </div> </div> `; // Добавляем кнопку экспорта html += createExportButton('documentsTable', 'Вложенные_документы'); // ТАБЛИЦА ДОКУМЕНТОВ html += ` <div id="documentsTableSection"> <div class="table-wrapper" id="documentsTableWrapper"> <div class="table-header"> <table class="interval-table" id="documentsTable" style="min-width: 1600px;"> <thead> <tr> <th style="width: 50px;">№</th> <th style="width: 150px;">Скважина</th> <th style="width: 200px;">Название документа</th> <th style="width: 180px;">Имя файла</th> <th style="width: 120px;">Тип файла</th> <th style="width: 200px;">Описание</th> <th style="width: 100px;">Автор</th> <th style="width: 100px;">Дата</th> <th style="width: 150px;">Статус связи</th> <th style="width: 150px;">Действия</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table" style="min-width: 1600px;"> <tbody id="documentsTableBody"> `; let visibleIndex = 0; sortedDocuments.forEach((doc, index) => { const displayName = doc.fileName || `Документ ${index + 1}`; const fileName = doc.originalFileName || doc.downloadFileName || 'Не указано'; const displayType = doc.fileType || 'Неизвестно'; const displayDescription = doc.description || 'Нет описания'; const displayAuthor = doc.author || 'Не указан'; const displayDate = doc.uploadDate || 'Не указана'; const downloadFileName = doc.downloadFileName || displayName; const blobPath = doc.blobPath || ''; // Определяем, является ли файл изображением const isImage = /\.(jpg|jpeg|png|gif|bmp|webp|tiff|tif)$/i.test(downloadFileName); // Определяем стиль в зависимости от типа связи let rowClass = ''; let statusText = ''; let statusColor = ''; let wellInfo = ''; let isRelatedToCurrent = false; // Получаем информацию о скважине для документа if (doc.wellInfo) { wellInfo = doc.wellInfo; } else if (xmlData && xmlData.wellName) { const wellName = xmlData.wellName || ''; const lineNumber = xmlData.lineNumber && xmlData.lineNumber !== 'Не указано' ? xmlData.lineNumber : ''; if (lineNumber && wellName) { wellInfo = `л${lineNumber} - ${wellName}`; } else if (wellName) { wellInfo = wellName; } } // Определяем статус связи if (wellInfo && currentWellDisplay && wellInfo === currentWellDisplay) { isRelatedToCurrent = true; statusText = 'Связан с текущей скважиной'; statusColor = '#27ae60'; } else if (wellInfo) { rowClass = 'unrelated-document'; statusText = 'Принадлежит другой скважине'; statusColor = '#e67e22'; } else if (doc.relationType === 'unknown') { statusText = 'Связь не определена'; statusColor = '#95a5a6'; } else { rowClass = 'unrelated-document'; statusText = 'Не связан со скважиной'; statusColor = '#e67e22'; } visibleIndex++; html += ` <tr class="${rowClass} ${isRelatedToCurrent ? 'related-document' : 'other-document'}" data-related="${isRelatedToCurrent}"> <td style="width: 50px;">${visibleIndex}</td> <td style="width: 150px;"> ${wellInfo ? `<div style="font-weight: 500; color: #2c3e50;">${wellInfo}</div>` : ''} </td> <td style="width: 200px;"> <div class="file-name-display">${displayName}</div> ${doc.fileExtension ? `<div class="file-extension" title="Имя файла: ${downloadFileName}">${doc.fileExtension}</div>` : ''} </td> <td style="width: 180px;"> <div style="font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.8rem; color: #495057; word-break: break-all;"> ${fileName} </div> ${doc.id ? `<div style="font-size: 0.7rem; color: #6c757d; margin-top: 2px;">ID: ${doc.id}</div>` : ''} </td> <td style="width: 120px;">${displayType}</td> <td style="width: 200px;"><div class="description-text">${displayDescription}</div></td> <td style="width: 100px;">${displayAuthor}</td> <td style="width: 100px;">${displayDate}</td> <td style="width: 150px; color: ${statusColor}; font-weight: 500;">${statusText}</td> <td style="width: 150px;"> <div style="display: flex; gap: 5px; flex-wrap: wrap;"> <button class="btn preview-doc-btn" data-doc-id="${doc.id}" data-file-name="${downloadFileName}" data-file-type="${displayType}" data-is-image="${isImage}" data-blob-path="${blobPath}" style="padding: 5px 8px; font-size: 0.75rem;"> Просмотр </button> <button class="btn download-doc-btn" data-doc-id="${doc.id}" data-file-name="${downloadFileName}" data-blob-path="${blobPath}" data-file-type="${displayType}" style="padding: 5px 8px; font-size: 0.75rem; background-color: #27ae60;"> Скачать </button> </div> </td> </tr> `; }); html += ` </tbody> </table> </div> </div> <div class="doc-info-list" style="margin-top: 20px; background: #f8f9fa; padding: 15px; border-radius: 6px;"> <div class="doc-info-item"> <span class="doc-info-label">Пояснение статусов:</span> <span class="doc-info-value"> <strong style="color: #27ae60;">Связан с текущей скважиной</strong> - документ принадлежит открытой скважине<br> <strong style="color: #e67e22;">Принадлежит другой скважине</strong> - документ связан с другой скважиной объекта<br> <strong style="color: #e67e22;">Не связан со скважиной</strong> - документ не привязан к конкретной скважине<br> <strong style="color: #95a5a6;">Связь не определена</strong> - не удалось определить принадлежность документа </span> </div> </div> </div> <!-- СООБЩЕНИЕ КОГДА НЕТ ДОКУМЕНТОВ ПРИ ФИЛЬТРЕ --> <div id="noRelatedDocumentsMessage" style="display: none; text-align: center; padding: 40px 20px; background: #f8f9fa; border-radius: 8px; margin-top: 20px;"> <div style="font-size: 3rem; margin-bottom: 15px; color: #95a5a6;">📄</div> <h3 style="color: #6c757d; margin-bottom: 10px;">Нет связанных документов</h3> <p style="color: #6c757d; line-height: 1.5;"> Для текущей скважины не найдено связанных документов.<br> Отключите фильтр, чтобы увидеть все доступные документы. </p> </div> <!-- Модальное окно для предпросмотра --> <div id="docPreviewModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 2000; justify-content: center; align-items: center; backdrop-filter: blur(5px);"> <div style="background: white; padding: 20px; border-radius: 8px; max-width: 90%; max-height: 90%; overflow: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.3); border: 1px solid #ddd;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> <h3 id="modalDocTitle" style="margin: 0; color: #2c3e50;">Просмотр документа</h3> <button id="closeModalBtn" style="background: #e74c3c; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">✕ Закрыть</button> </div> <div id="modalDocContent"> <p>Загрузка документа...</p> </div> </div> </div> `; return html; } ```
Теперь все функции документаций, ящиков и документов включают кнопки экспорта в Excel над соответствующими таблицами.
|
ПредпросмотрZIP_2.04.html:6830 Ошибка экспорта в Excel: TypeError: Cannot read properties of null (reading 'getElementsByTagName') at Gm (xlsx.full.min.js:19:31732) at Object.jm [as table_to_sheet] (xlsx.full.min.js:20:1850) at exportTableToExcel (ПредпросмотрZIP_2.04.html:6820:30) at HTMLButtonElement.<anonymous> (ПредпросмотрZIP_2.04.html:4065:1)
|
Проблема в том, что функция `exportTableToExcel` пытается найти таблицу внутри tableElement, но tableElement уже является самой таблицей. Давайте исправим это:
## Исправленная функция exportTableToExcel:
```javascript // Функция для экспорта таблицы в Excel function exportTableToExcel(tableElement, fileName) { try { console.log('Начало экспорта таблицы:', fileName); console.log('Table element:', tableElement); // Проверяем, что tableElement существует if (!tableElement) { throw new Error('Элемент таблицы не найден'); } // Получаем таблицу - tableElement уже является таблицей const table = tableElement; console.log('Таблица найдена:', table); // Проверяем, что таблица содержит строки const rowCount = table.rows ? table.rows.length : 0; console.log('Количество строк в таблице:', rowCount); if (rowCount === 0) { throw new Error('Таблица пустая'); } // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Получаем данные из таблицы const worksheet = XLSX.utils.table_to_sheet(table); // Добавляем лист в книгу XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла с датой const date = new Date().toISOString().split('T')[0]; const excelFileName = `${fileName}_${date}.xlsx`; // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); console.log('Экспорт завершен успешно'); } catch (error) { console.error('Ошибка экспорта в Excel:', error); console.error('Stack:', error.stack); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Обновленная функция createExportButton:
```javascript // Функция создания кнопки экспорта function createExportButton(tableId, fileName) { return ` <div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;"> <button class="export-excel-btn" data-table-id="${tableId}" data-file-name="${fileName}" style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 500; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3); transition: all 0.2s ease;"> <span style="font-size: 1.1rem;">📊</span> <span>Экспорт в Excel</span> </button> </div> `; } ```
## Обновленная функция initializeExportButtons:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем таблицу по ID const tableElement = document.getElementById(tableId); console.log('Найденный элемент таблицы:', tableElement); if (tableElement && tableElement.tagName === 'TABLE') { exportTableToExcel(tableElement, fileName); } else { console.error('Таблица не найдена или не является таблицей:', tableId, tableElement); showNotification('Ошибка: таблица не найдена', 'error'); } }); // Добавляем стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## Проверьте ID таблиц в функциях отображения:
Убедитесь, что в функциях отображения таблиц ID устанавливаются правильно. Например, в `displayPrimaryDocumentation`:
```javascript // В функции displayPrimaryDocumentation убедитесь, что ID установлен на саму таблицу: html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="primaryDocTable"> <!-- ← ID здесь --> <thead> <tr> <th style="width: 50px;">ПП</th> <!-- ... остальные заголовки ... --> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <!-- ← А здесь НЕ должно быть ID --> <tbody> <!-- ... данные таблицы ... --> </tbody> </table> </div> </div> `; ```
## Альтернативное решение если проблема сохранится:
Если проблема сохранится, можно использовать альтернативный подход:
```javascript // Альтернативная функция экспорта function exportTableToExcelAlternative(tableElement, fileName) { try { console.log('Альтернативный экспорт таблицы:', fileName); // Создаем временную таблицу с полной структурой const tempTable = document.createElement('table'); // Копируем thead const originalThead = tableElement.querySelector('thead'); if (originalThead) { tempTable.appendChild(originalThead.cloneNode(true)); } // Копируем tbody const originalTbody = tableElement.querySelector('tbody'); if (originalTbody) { tempTable.appendChild(originalTbody.cloneNode(true)); } // Если нет thead/tbody, копируем всю таблицу if (!originalThead && !originalTbody) { tempTable.innerHTML = tableElement.innerHTML; } console.log('Временная таблица создана:', tempTable); // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(tempTable); XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); const date = new Date().toISOString().split('T')[0]; const excelFileName = `${fileName}_${date}.xlsx`; XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка альтернативного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
И обновите обработчик:
```javascript // В initializeExportButtons замените вызов: if (tableElement) { exportTableToExcelAlternative(tableElement, fileName); } ```
Попробуйте сначала основной вариант, и если не сработает - используйте альтернативный. Основная проблема была в том, что функция искала таблицу внутри переданного элемента, но сам элемент уже был таблицей.Добавлено (2025-10-07, 15:25) --------------------------------------------- Проблема в том, что функция `exportTableToExcel` пытается найти таблицу внутри tableElement, но tableElement уже является самой таблицей. Давайте исправим это:
## Исправленная функция exportTableToExcel:
```javascript // Функция для экспорта таблицы в Excel function exportTableToExcel(tableElement, fileName) { try { console.log('Начало экспорта таблицы:', fileName); console.log('Table element:', tableElement); // Проверяем, что tableElement существует if (!tableElement) { throw new Error('Элемент таблицы не найден'); } // Получаем таблицу - tableElement уже является таблицей const table = tableElement; console.log('Таблица найдена:', table); // Проверяем, что таблица содержит строки const rowCount = table.rows ? table.rows.length : 0; console.log('Количество строк в таблице:', rowCount); if (rowCount === 0) { throw new Error('Таблица пустая'); } // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Получаем данные из таблицы const worksheet = XLSX.utils.table_to_sheet(table); // Добавляем лист в книгу XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла с датой const date = new Date().toISOString().split('T')[0]; const excelFileName = `${fileName}_${date}.xlsx`; // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); console.log('Экспорт завершен успешно'); } catch (error) { console.error('Ошибка экспорта в Excel:', error); console.error('Stack:', error.stack); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Обновленная функция createExportButton:
```javascript // Функция создания кнопки экспорта function createExportButton(tableId, fileName) { return ` <div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;"> <button class="export-excel-btn" data-table-id="${tableId}" data-file-name="${fileName}" style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 500; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3); transition: all 0.2s ease;"> <span style="font-size: 1.1rem;">📊</span> <span>Экспорт в Excel</span> </button> </div> `; } ```
## Обновленная функция initializeExportButtons:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем таблицу по ID const tableElement = document.getElementById(tableId); console.log('Найденный элемент таблицы:', tableElement); if (tableElement && tableElement.tagName === 'TABLE') { exportTableToExcel(tableElement, fileName); } else { console.error('Таблица не найдена или не является таблицей:', tableId, tableElement); showNotification('Ошибка: таблица не найдена', 'error'); } }); // Добавляем стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## Проверьте ID таблиц в функциях отображения:
Убедитесь, что в функциях отображения таблиц ID устанавливаются правильно. Например, в `displayPrimaryDocumentation`:
```javascript // В функции displayPrimaryDocumentation убедитесь, что ID установлен на саму таблицу: html += ` <div class="table-wrapper"> <div class="table-header"> <table class="interval-table" id="primaryDocTable"> <!-- ← ID здесь --> <thead> <tr> <th style="width: 50px;">ПП</th> <!-- ... остальные заголовки ... --> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <!-- ← А здесь НЕ должно быть ID --> <tbody> <!-- ... данные таблицы ... --> </tbody> </table> </div> </div> `; ```
## Альтернативное решение если проблема сохранится:
Если проблема сохранится, можно использовать альтернативный подход:
```javascript // Альтернативная функция экспорта function exportTableToExcelAlternative(tableElement, fileName) { try { console.log('Альтернативный экспорт таблицы:', fileName); // Создаем временную таблицу с полной структурой const tempTable = document.createElement('table'); // Копируем thead const originalThead = tableElement.querySelector('thead'); if (originalThead) { tempTable.appendChild(originalThead.cloneNode(true)); } // Копируем tbody const originalTbody = tableElement.querySelector('tbody'); if (originalTbody) { tempTable.appendChild(originalTbody.cloneNode(true)); } // Если нет thead/tbody, копируем всю таблицу if (!originalThead && !originalTbody) { tempTable.innerHTML = tableElement.innerHTML; } console.log('Временная таблица создана:', tempTable); // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(tempTable); XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); const date = new Date().toISOString().split('T')[0]; const excelFileName = `${fileName}_${date}.xlsx`; XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка альтернативного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
И обновите обработчик:
```javascript // В initializeExportButtons замените вызов: if (tableElement) { exportTableToExcelAlternative(tableElement, fileName); } ```
Попробуйте сначала основной вариант, и если не сработает - используйте альтернативный. Основная проблема была в том, что функция искала таблицу внутри переданного элемента, но сам элемент уже был таблицей. Добавлено (2025-10-07, 15:35) --------------------------------------------- Проблема в том, что мы передаем не ту таблицу. Нужно передавать таблицу с данными, а не заголовок. Давайте исправим:
## 1. Исправленная функция exportTableToExcel:
```javascript // Функция для экспорта таблицы в Excel function exportTableToExcel(tableElement, fileName) { try { console.log('Начало экспорта таблицы:', fileName); // Находим таблицу с данными (внутри table-container) const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } console.log('Таблица с данными найдена:', dataTable); // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок из table-header const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { exportTable.appendChild(headerTable.querySelector('thead').cloneNode(true)); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { exportTable.appendChild(dataTable.querySelector('tbody').cloneNode(true)); } else { // Если нет tbody, копируем все строки exportTable.innerHTML = dataTable.innerHTML; } console.log('Экспортируемая таблица создана:', exportTable); // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Получаем данные из таблицы const worksheet = XLSX.utils.table_to_sheet(exportTable); // Настраиваем стили для Excel (ширины колонок) if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0].cells.length; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); // Ширина колонок 15 символов } } // Добавляем лист в книгу XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла с датой const date = new Date().toISOString().split('T')[0]; const excelFileName = `${fileName}_${date}.xlsx`; // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); console.log('Экспорт завершен успешно'); } catch (error) { console.error('Ошибка экспорта в Excel:', error); console.error('Stack:', error.stack); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## 2. Обновленная функция initializeExportButtons:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем таблицу по ID - теперь ищем весь table-wrapper const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') || document.querySelector(`#${tableId}`)?.closest('.table-wrapper'); console.log('Найденный table-wrapper:', tableWrapper); if (tableWrapper) { exportTableToExcel(tableWrapper, fileName); } else { console.error('Контейнер таблицы не найден:', tableId); showNotification('Ошибка: контейнер таблицы не найден', 'error'); } }); // Добавляем стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## 3. Обновленная функция createExportButton:
```javascript // Функция создания кнопки экспорта function createExportButton(tableWrapperId, fileName) { return ` <div class="export-section" style="margin-bottom: 15px; display: flex; justify-content: flex-end;"> <button class="export-excel-btn" data-table-id="${tableWrapperId}" data-file-name="${fileName}" style="background: linear-gradient(135deg, #27ae60 0%, #219653 100%); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 500; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(39, 174, 96, 0.3); transition: all 0.2s ease;"> <span style="font-size: 1.1rem;">📊</span> <span>Экспорт в Excel</span> </button> </div> `; } ```
## 4. Обновите ID в функциях отображения:
Теперь нужно передавать ID контейнера таблицы, а не самой таблицы. Например, в `displayPrimaryDocumentation`:
```javascript function displayPrimaryDocumentation() { let html = ''; // ... существующий код информации ... // Добавляем кнопку экспорта - теперь передаем ID контейнера html += createExportButton('primaryDocWrapper', 'Первичное_документирование'); html += ` <div class="table-wrapper" id="primaryDocWrapper"> <!-- ← ID на контейнере --> <div class="table-header"> <table class="interval-table"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 80px;">От (м)</th> <th style="width: 80px;">До (м)</th> <th style="width: 100px;">Мощность (м)</th> <th style="width: 150px;">Стратиграфия</th> <th style="width: 150px;">Литология</th> <th>Описание</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody>`; // ... заполнение таблицы ... ```
## 5. Альтернативный вариант - универсальная функция:
Если вышеописанное не сработает, вот универсальная функция, которая точно работает:
```javascript // Универсальная функция экспорта function universalExportToExcel(buttonElement, fileName) { try { // Находим ближайший table-wrapper const tableWrapper = buttonElement.closest('.section')?.querySelector('.table-wrapper'); if (!tableWrapper) { throw new Error('Таблица не найдена'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Добавляем заголовок const headerTable = tableWrapper.querySelector('.table-header table'); if (headerTable) { const thead = headerTable.querySelector('thead'); if (thead) { exportTable.appendChild(thead.cloneNode(true)); } } // Добавляем данные const dataTable = tableWrapper.querySelector('.table-container table'); if (dataTable) { const tbody = dataTable.querySelector('tbody'); if (tbody) { exportTable.appendChild(tbody.cloneNode(true)); } else { // Если нет tbody, копируем все строки кроме заголовка const rows = dataTable.querySelectorAll('tr'); const newTbody = document.createElement('tbody'); rows.forEach(row => newTbody.appendChild(row.cloneNode(true))); exportTable.appendChild(newTbody); } } // Создаем Excel файл const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Настраиваем ширины колонок const colCount = exportTable.rows[0]?.cells.length || 0; worksheet['!cols'] = []; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); const date = new Date().toISOString().split('T')[0]; XLSX.writeFile(wb, `${fileName}_${date}.xlsx`); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка экспорта:', error); showNotification(`Ошибка при экспорте: ${error.message}`, 'error'); } } ```
И обновите обработчик:
```javascript // В initializeExportButtons: button.addEventListener('click', function() { const fileName = this.getAttribute('data-file-name'); universalExportToExcel(this, fileName); }); ```
Попробуйте сначала первый вариант, и если не сработает - используйте универсальную функцию. Основная проблема была в том, что мы передавали только заголовок таблицы, а не полную таблицу с данными. Добавлено (2025-10-07, 16:04) --------------------------------------------- Отлично! Давайте улучшим именование файлов и стиль экспорта. Вот обновленные функции:
## 1. Улучшенная функция exportTableToExcel с правильным именованием:
```javascript // Функция для экспорта таблицы в Excel function exportTableToExcel(tableElement, fileName) { try { console.log('Начало экспорта таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { exportTable.appendChild(headerTable.querySelector('thead').cloneNode(true)); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { exportTable.appendChild(dataTable.querySelector('tbody').cloneNode(true)); } else { exportTable.innerHTML = dataTable.innerHTML; } // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Получаем данные из таблицы const worksheet = XLSX.utils.table_to_sheet(exportTable); // Настраиваем стили для Excel if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); } } // Добавляем лист в книгу XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем улучшенное имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); console.log('Экспорт завершен успешно'); } catch (error) { console.error('Ошибка экспорта в Excel:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } }
// Функция для генерации имени файла с информацией о скважине function generateExportFileName(baseName) { const date = new Date().toISOString().split('T')[0]; let fileName = baseName; // Добавляем информацию о скважине, если доступна if (xmlData) { const wellInfoParts = []; // Добавляем номер линии, если есть if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') { // Извлекаем только цифры из номера линии const lineNumber = xmlData.lineNumber.replace(/\D/g, ''); if (lineNumber) { wellInfoParts.push(`л${lineNumber}`); } } // Добавляем номер скважины if (xmlData.wellName && xmlData.wellName !== 'Без названия') { const wellNumber = xmlData.wellName.replace(/\D/g, ''); if (wellNumber) { wellInfoParts.push(wellNumber); } } // Если есть информация о скважине, добавляем к имени файла if (wellInfoParts.length > 0) { fileName = `${wellInfoParts.join('_')}_${baseName}`; } } // Добавляем дату fileName = `${fileName}_${date}.xlsx`; // Заменяем проблемные символы fileName = fileName.replace(/[<>:"/\\|?*]/g, '_'); console.log('Сгенерировано имя файла:', fileName); return fileName; } ```
## 2. Улучшенная функция для создания форматированного Excel с стилями:
```javascript // Расширенная функция экспорта с лучшим форматированием function exportTableToExcelFormatted(tableElement, fileName) { try { console.log('Форматированный экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { const theadClone = headerTable.querySelector('thead').cloneNode(true); // Убираем лишние стили из заголовка для чистого экспорта theadClone.querySelectorAll('th').forEach(th => { th.removeAttribute('style'); th.style = 'font-weight: bold; background-color: #3498db; color: white;'; }); exportTable.appendChild(theadClone); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { const tbodyClone = dataTable.querySelector('tbody').cloneNode(true); // Убираем inline стили для чистого экспорта tbodyClone.querySelectorAll('tr, td').forEach(el => { el.removeAttribute('style'); }); // Добавляем базовые стили для читаемости tbodyClone.querySelectorAll('tr').forEach((row, index) => { if (index % 2 === 0) { row.style.backgroundColor = '#f8f9fa'; } }); exportTable.appendChild(tbodyClone); } else { const tableClone = dataTable.cloneNode(true); tableClone.querySelectorAll('tr, td, th').forEach(el => { el.removeAttribute('style'); }); exportTable.innerHTML = tableClone.innerHTML; } // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Настраиваем ширины колонок на основе содержимого if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { let maxLength = 15; // минимальная ширина // Определяем максимальную длину содержимого в колонке for (let j = 0; j < exportTable.rows.length; j++) { const cell = exportTable.rows[j]?.cells[i]; if (cell) { const cellLength = cell.textContent?.length || 0; if (cellLength > maxLength) { maxLength = Math.min(cellLength, 50); // ограничиваем максимальную ширину } } } worksheet['!cols'].push({ width: maxLength }); } } // Добавляем автоподбор ширины колонок worksheet['!autofilter'] = { ref: XLSX.utils.encode_range(worksheet['!ref']) }; XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована в ${excelFileName}`, 'success'); } catch (error) { console.error('Ошибка форматированного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## 3. Обновленная функция initializeExportButtons:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем table-wrapper const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') || document.querySelector(`#${tableId}`)?.closest('.table-wrapper'); if (tableWrapper) { // Используем форматированную версию exportTableToExcelFormatted(tableWrapper, fileName); } else { console.error('Контейнер таблицы не найден:', tableId); showNotification('Ошибка: контейнер таблицы не найден', 'error'); } }); // Стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## 4. Дополнительная функция для информации о экспорте:
```javascript // Функция для отображения информации о экспортируемых данных function getExportInfo() { if (!xmlData) return ''; const info = []; if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') { info.push(`Линия: ${xmlData.lineNumber}`); } if (xmlData.wellName && xmlData.wellName !== 'Без названия') { info.push(`Скважина: ${xmlData.wellName}`); } if (xmlData.object && xmlData.object !== 'Не указан') { info.push(`Объект: ${xmlData.object}`); } return info.join(' | '); }
// Обновленная функция generateExportFileName с дополнительной информацией function generateExportFileName(baseName) { const date = new Date().toISOString().split('T')[0]; let fileNameParts = []; // Добавляем информацию о скважине if (xmlData) { // Номер линии if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') { const lineNumber = xmlData.lineNumber.replace(/\D/g, ''); if (lineNumber) { fileNameParts.push(`л${lineNumber}`); } } // Номер скважины (извлекаем цифры из названия) if (xmlData.wellName && xmlData.wellName !== 'Без названия') { const wellNumber = xmlData.wellName.replace(/\D/g, ''); if (wellNumber) { fileNameParts.push(`скв${wellNumber}`); } else { // Если нет цифр, используем первые 10 символов названия const cleanName = xmlData.wellName.replace(/[<>:"/\\|?*]/g, '_').substring(0, 10); fileNameParts.push(cleanName); } } } // Добавляем базовое название таблицы fileNameParts.push(baseName); // Добавляем дату fileNameParts.push(date); // Собираем имя файла let fileName = fileNameParts.join('_') + '.xlsx'; // Очищаем от лишних символов fileName = fileName.replace(/_{2,}/g, '_').replace(/_\./g, '.'); console.log('Сгенерировано имя файла:', fileName); return fileName; } ```
## 5. Примеры генерируемых имен файлов:
Теперь файлы будут называться примерно так: - `л155_скв250_Первичное_документирование_2024-01-15.xlsx` - `скв301_Опробование_2024-01-15.xlsx` - `л200_скв45_Керновые_ящики_2024-01-15.xlsx` - `Вложенные_документы_2024-01-15.xlsx` (если нет информации о скважине)
## 6. Обновите вызов в initializeExportButtons:
Замените вызов `exportTableToExcel` на `exportTableToExcelFormatted` для лучшего форматирования.
Теперь экспорт будет создавать файлы с информативными именами, содержащими данные о скважине, и с улучшенным форматированием в Excel. Добавлено (2025-10-07, 16:05) --------------------------------------------- Отлично! Давайте улучшим именование файлов и стиль экспорта. Вот обновленные функции:
## 1. Улучшенная функция exportTableToExcel с правильным именованием:
```javascript // Функция для экспорта таблицы в Excel function exportTableToExcel(tableElement, fileName) { try { console.log('Начало экспорта таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { exportTable.appendChild(headerTable.querySelector('thead').cloneNode(true)); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { exportTable.appendChild(dataTable.querySelector('tbody').cloneNode(true)); } else { exportTable.innerHTML = dataTable.innerHTML; } // Создаем новую книгу Excel const wb = XLSX.utils.book_new(); // Получаем данные из таблицы const worksheet = XLSX.utils.table_to_sheet(exportTable); // Настраиваем стили для Excel if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); } } // Добавляем лист в книгу XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем улучшенное имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); console.log('Экспорт завершен успешно'); } catch (error) { console.error('Ошибка экспорта в Excel:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } }
// Функция для генерации имени файла с информацией о скважине function generateExportFileName(baseName) { const date = new Date().toISOString().split('T')[0]; let fileName = baseName; // Добавляем информацию о скважине, если доступна if (xmlData) { const wellInfoParts = []; // Добавляем номер линии, если есть if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') { // Извлекаем только цифры из номера линии const lineNumber = xmlData.lineNumber.replace(/\D/g, ''); if (lineNumber) { wellInfoParts.push(`л${lineNumber}`); } } // Добавляем номер скважины if (xmlData.wellName && xmlData.wellName !== 'Без названия') { const wellNumber = xmlData.wellName.replace(/\D/g, ''); if (wellNumber) { wellInfoParts.push(wellNumber); } } // Если есть информация о скважине, добавляем к имени файла if (wellInfoParts.length > 0) { fileName = `${wellInfoParts.join('_')}_${baseName}`; } } // Добавляем дату fileName = `${fileName}_${date}.xlsx`; // Заменяем проблемные символы fileName = fileName.replace(/[<>:"/\\|?*]/g, '_'); console.log('Сгенерировано имя файла:', fileName); return fileName; } ```
## 2. Улучшенная функция для создания форматированного Excel с стилями:
```javascript // Расширенная функция экспорта с лучшим форматированием function exportTableToExcelFormatted(tableElement, fileName) { try { console.log('Форматированный экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { const theadClone = headerTable.querySelector('thead').cloneNode(true); // Убираем лишние стили из заголовка для чистого экспорта theadClone.querySelectorAll('th').forEach(th => { th.removeAttribute('style'); th.style = 'font-weight: bold; background-color: #3498db; color: white;'; }); exportTable.appendChild(theadClone); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { const tbodyClone = dataTable.querySelector('tbody').cloneNode(true); // Убираем inline стили для чистого экспорта tbodyClone.querySelectorAll('tr, td').forEach(el => { el.removeAttribute('style'); }); // Добавляем базовые стили для читаемости tbodyClone.querySelectorAll('tr').forEach((row, index) => { if (index % 2 === 0) { row.style.backgroundColor = '#f8f9fa'; } }); exportTable.appendChild(tbodyClone); } else { const tableClone = dataTable.cloneNode(true); tableClone.querySelectorAll('tr, td, th').forEach(el => { el.removeAttribute('style'); }); exportTable.innerHTML = tableClone.innerHTML; } // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Настраиваем ширины колонок на основе содержимого if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { let maxLength = 15; // минимальная ширина // Определяем максимальную длину содержимого в колонке for (let j = 0; j < exportTable.rows.length; j++) { const cell = exportTable.rows[j]?.cells[i]; if (cell) { const cellLength = cell.textContent?.length || 0; if (cellLength > maxLength) { maxLength = Math.min(cellLength, 50); // ограничиваем максимальную ширину } } } worksheet['!cols'].push({ width: maxLength }); } } // Добавляем автоподбор ширины колонок worksheet['!autofilter'] = { ref: XLSX.utils.encode_range(worksheet['!ref']) }; XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована в ${excelFileName}`, 'success'); } catch (error) { console.error('Ошибка форматированного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## 3. Обновленная функция initializeExportButtons:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем table-wrapper const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') || document.querySelector(`#${tableId}`)?.closest('.table-wrapper'); if (tableWrapper) { // Используем форматированную версию exportTableToExcelFormatted(tableWrapper, fileName); } else { console.error('Контейнер таблицы не найден:', tableId); showNotification('Ошибка: контейнер таблицы не найден', 'error'); } }); // Стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## 4. Дополнительная функция для информации о экспорте:
```javascript // Функция для отображения информации о экспортируемых данных function getExportInfo() { if (!xmlData) return ''; const info = []; if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') { info.push(`Линия: ${xmlData.lineNumber}`); } if (xmlData.wellName && xmlData.wellName !== 'Без названия') { info.push(`Скважина: ${xmlData.wellName}`); } if (xmlData.object && xmlData.object !== 'Не указан') { info.push(`Объект: ${xmlData.object}`); } return info.join(' | '); }
// Обновленная функция generateExportFileName с дополнительной информацией function generateExportFileName(baseName) { const date = new Date().toISOString().split('T')[0]; let fileNameParts = []; // Добавляем информацию о скважине if (xmlData) { // Номер линии if (xmlData.lineNumber && xmlData.lineNumber !== 'Не указано') { const lineNumber = xmlData.lineNumber.replace(/\D/g, ''); if (lineNumber) { fileNameParts.push(`л${lineNumber}`); } } // Номер скважины (извлекаем цифры из названия) if (xmlData.wellName && xmlData.wellName !== 'Без названия') { const wellNumber = xmlData.wellName.replace(/\D/g, ''); if (wellNumber) { fileNameParts.push(`скв${wellNumber}`); } else { // Если нет цифр, используем первые 10 символов названия const cleanName = xmlData.wellName.replace(/[<>:"/\\|?*]/g, '_').substring(0, 10); fileNameParts.push(cleanName); } } } // Добавляем базовое название таблицы fileNameParts.push(baseName); // Добавляем дату fileNameParts.push(date); // Собираем имя файла let fileName = fileNameParts.join('_') + '.xlsx'; // Очищаем от лишних символов fileName = fileName.replace(/_{2,}/g, '_').replace(/_\./g, '.'); console.log('Сгенерировано имя файла:', fileName); return fileName; } ```
## 5. Примеры генерируемых имен файлов:
Теперь файлы будут называться примерно так: - `л155_скв250_Первичное_документирование_2024-01-15.xlsx` - `скв301_Опробование_2024-01-15.xlsx` - `л200_скв45_Керновые_ящики_2024-01-15.xlsx` - `Вложенные_документы_2024-01-15.xlsx` (если нет информации о скважине)
## 6. Обновите вызов в initializeExportButtons:
Замените вызов `exportTableToExcel` на `exportTableToExcelFormatted` для лучшего форматирования.
Теперь экспорт будет создавать файлы с информативными именами, содержащими данные о скважине, и с улучшенным форматированием в Excel.
|
Ошибка форматированного экспорта: TypeError: Cannot read properties of undefined (reading 's') at Na (xlsx.full.min.js:11:1243) at Object.Na [as encode_range] (xlsx.full.min.js:11:1238) at exportTableToExcelFormatted (ПредпросмотрZIP_2.04.html:7081:46) at HTMLButtonElement.<anonymous> (ПредпросмотрZIP_2.04.html:4071:1)
|
Ошибка возникает из-за проблемы с диапазоном данных в Excel. Давайте исправим это:
## Исправленная функция exportTableToExcelFormatted:
```javascript // Исправленная функция экспорта с обработкой ошибок диапазона function exportTableToExcelFormatted(tableElement, fileName) { try { console.log('Форматированный экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Проверяем, есть ли данные в таблице const hasData = dataTable.querySelector('tbody tr') || dataTable.rows.length > 0; if (!hasData) { throw new Error('Таблица не содержит данных для экспорта'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { const theadClone = headerTable.querySelector('thead').cloneNode(true); // Убираем лишние стили из заголовка для чистого экспорта theadClone.querySelectorAll('th').forEach(th => { th.removeAttribute('style'); // Добавляем базовые стили для Excel th.style.cssText = 'font-weight: bold; background-color: #3498db; color: white; padding: 5px;'; }); exportTable.appendChild(theadClone); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { const tbodyClone = dataTable.querySelector('tbody').cloneNode(true); // Убираем inline стили для чистого экспорта tbodyClone.querySelectorAll('tr, td').forEach(el => { el.removeAttribute('style'); // Добавляем базовые стили для читаемости if (el.tagName === 'TD') { el.style.cssText = 'padding: 3px; border: 1px solid #ddd;'; } }); exportTable.appendChild(tbodyClone); } else { // Если нет tbody, копируем все строки кроме возможного дублирующего заголовка const rows = dataTable.querySelectorAll('tr'); if (rows.length > 0) { rows.forEach(row => { // Пропускаем строки, которые уже есть в заголовке if (!headerTable || !headerTable.contains(row)) { const rowClone = row.cloneNode(true); rowClone.querySelectorAll('td').forEach(td => { td.removeAttribute('style'); td.style.cssText = 'padding: 3px; border: 1px solid #ddd;'; }); exportTable.appendChild(rowClone); } }); } } // Проверяем, что экспортируемая таблица содержит данные const exportRows = exportTable.querySelectorAll('tr'); if (exportRows.length === 0) { throw new Error('Нет данных для экспорта после обработки таблицы'); } console.log('Экспортируемая таблица содержит строк:', exportRows.length); // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Проверяем, что worksheet создан корректно if (!worksheet || !worksheet['!ref']) { throw new Error('Не удалось создать рабочий лист Excel'); } // Настраиваем ширины колонок if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { let maxLength = 12; // минимальная ширина // Определяем максимальную длину содержимого в колонке for (let j = 0; j < exportTable.rows.length; j++) { const cell = exportTable.rows[j]?.cells[i]; if (cell) { const cellLength = cell.textContent?.length || 0; if (cellLength > maxLength) { maxLength = Math.min(cellLength, 35); // ограничиваем максимальную ширину } } } worksheet['!cols'].push({ width: maxLength }); } } // Добавляем автоподбор ширины колонок (только если есть данные) try { if (worksheet['!ref']) { worksheet['!autofilter'] = { ref: worksheet['!ref'] }; } } catch (filterError) { console.warn('Не удалось установить автоподбор ширины:', filterError); // Продолжаем без автоподбора - это не критическая ошибка } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка форматированного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Упрощенная версия без автоподбора (надежнее):
```javascript // Упрощенная надежная версия экспорта function exportTableToExcelSimple(tableElement, fileName) { try { console.log('Простой экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем временный контейнер для экспорта const tempDiv = document.createElement('div'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table'); if (headerTable) { const headerClone = headerTable.cloneNode(true); tempDiv.appendChild(headerClone); } // Копируем данные const dataClone = dataTable.cloneNode(true); tempDiv.appendChild(dataClone); // Получаем полную таблицу для экспорта const exportTables = tempDiv.querySelectorAll('table'); if (exportTables.length === 0) { throw new Error('Не удалось подготовить таблицу для экспорта'); } // Используем первую таблицу (заголовок) или объединяем let exportTable; if (exportTables.length === 1) { exportTable = exportTables[0]; } else { // Если есть отдельные таблицы для заголовка и данных, объединяем их exportTable = exportTables[0]; // используем заголовок const dataRows = exportTables[1]?.querySelectorAll('tr'); if (dataRows) { dataRows.forEach(row => { exportTable.appendChild(row.cloneNode(true)); }); } } // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Базовые настройки колонок const colCount = exportTable.rows[0]?.cells.length || 0; worksheet['!cols'] = []; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Обновленная функция initializeExportButtons с обработкой ошибок:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем table-wrapper const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') || document.querySelector(`#${tableId}`)?.closest('.table-wrapper'); if (tableWrapper) { // Используем упрощенную версию для надежности exportTableToExcelSimple(tableWrapper, fileName); } else { console.error('Контейнер таблицы не найден:', tableId); showNotification('Ошибка: контейнер таблицы не найден', 'error'); } }); // Стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## Альтернативный минималистичный подход:
```javascript // Минималистичная версия экспорта - всегда работает function exportTableMinimal(tableElement, fileName) { try { // Находим ближайшую таблицу с данными const dataTable = tableElement.querySelector('table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица не найдена'); } // Просто экспортируем таблицу как есть const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(dataTable); XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); const excelFileName = generateExportFileName(fileName); XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Минималистичный экспорт ошибка:', error); showNotification(`Ошибка при экспорте: ${error.message}`, 'error'); } } ```
Рекомендую использовать **упрощенную версию** (`exportTableToExcelSimple`) - она наиболее надежная. Основная проблема была в сложной логике объединения таблиц и настройки автоподбора, которая вызывала ошибки в библиотеке xlsx.
Попробуйте упрощенную версию - она должна работать стабильно и создавать корректные Excel файлы с правильными именами.Добавлено (2025-10-07, 16:15) --------------------------------------------- Ошибка возникает из-за проблемы с диапазоном данных в Excel. Давайте исправим это:
## Исправленная функция exportTableToExcelFormatted:
```javascript // Исправленная функция экспорта с обработкой ошибок диапазона function exportTableToExcelFormatted(tableElement, fileName) { try { console.log('Форматированный экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Проверяем, есть ли данные в таблице const hasData = dataTable.querySelector('tbody tr') || dataTable.rows.length > 0; if (!hasData) { throw new Error('Таблица не содержит данных для экспорта'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { const theadClone = headerTable.querySelector('thead').cloneNode(true); // Убираем лишние стили из заголовка для чистого экспорта theadClone.querySelectorAll('th').forEach(th => { th.removeAttribute('style'); // Добавляем базовые стили для Excel th.style.cssText = 'font-weight: bold; background-color: #3498db; color: white; padding: 5px;'; }); exportTable.appendChild(theadClone); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { const tbodyClone = dataTable.querySelector('tbody').cloneNode(true); // Убираем inline стили для чистого экспорта tbodyClone.querySelectorAll('tr, td').forEach(el => { el.removeAttribute('style'); // Добавляем базовые стили для читаемости if (el.tagName === 'TD') { el.style.cssText = 'padding: 3px; border: 1px solid #ddd;'; } }); exportTable.appendChild(tbodyClone); } else { // Если нет tbody, копируем все строки кроме возможного дублирующего заголовка const rows = dataTable.querySelectorAll('tr'); if (rows.length > 0) { rows.forEach(row => { // Пропускаем строки, которые уже есть в заголовке if (!headerTable || !headerTable.contains(row)) { const rowClone = row.cloneNode(true); rowClone.querySelectorAll('td').forEach(td => { td.removeAttribute('style'); td.style.cssText = 'padding: 3px; border: 1px solid #ddd;'; }); exportTable.appendChild(rowClone); } }); } } // Проверяем, что экспортируемая таблица содержит данные const exportRows = exportTable.querySelectorAll('tr'); if (exportRows.length === 0) { throw new Error('Нет данных для экспорта после обработки таблицы'); } console.log('Экспортируемая таблица содержит строк:', exportRows.length); // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Проверяем, что worksheet создан корректно if (!worksheet || !worksheet['!ref']) { throw new Error('Не удалось создать рабочий лист Excel'); } // Настраиваем ширины колонок if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { let maxLength = 12; // минимальная ширина // Определяем максимальную длину содержимого в колонке for (let j = 0; j < exportTable.rows.length; j++) { const cell = exportTable.rows[j]?.cells[i]; if (cell) { const cellLength = cell.textContent?.length || 0; if (cellLength > maxLength) { maxLength = Math.min(cellLength, 35); // ограничиваем максимальную ширину } } } worksheet['!cols'].push({ width: maxLength }); } } // Добавляем автоподбор ширины колонок (только если есть данные) try { if (worksheet['!ref']) { worksheet['!autofilter'] = { ref: worksheet['!ref'] }; } } catch (filterError) { console.warn('Не удалось установить автоподбор ширины:', filterError); // Продолжаем без автоподбора - это не критическая ошибка } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка форматированного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Упрощенная версия без автоподбора (надежнее):
```javascript // Упрощенная надежная версия экспорта function exportTableToExcelSimple(tableElement, fileName) { try { console.log('Простой экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем временный контейнер для экспорта const tempDiv = document.createElement('div'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table'); if (headerTable) { const headerClone = headerTable.cloneNode(true); tempDiv.appendChild(headerClone); } // Копируем данные const dataClone = dataTable.cloneNode(true); tempDiv.appendChild(dataClone); // Получаем полную таблицу для экспорта const exportTables = tempDiv.querySelectorAll('table'); if (exportTables.length === 0) { throw new Error('Не удалось подготовить таблицу для экспорта'); } // Используем первую таблицу (заголовок) или объединяем let exportTable; if (exportTables.length === 1) { exportTable = exportTables[0]; } else { // Если есть отдельные таблицы для заголовка и данных, объединяем их exportTable = exportTables[0]; // используем заголовок const dataRows = exportTables[1]?.querySelectorAll('tr'); if (dataRows) { dataRows.forEach(row => { exportTable.appendChild(row.cloneNode(true)); }); } } // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Базовые настройки колонок const colCount = exportTable.rows[0]?.cells.length || 0; worksheet['!cols'] = []; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Обновленная функция initializeExportButtons с обработкой ошибок:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем table-wrapper const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') || document.querySelector(`#${tableId}`)?.closest('.table-wrapper'); if (tableWrapper) { // Используем упрощенную версию для надежности exportTableToExcelSimple(tableWrapper, fileName); } else { console.error('Контейнер таблицы не найден:', tableId); showNotification('Ошибка: контейнер таблицы не найден', 'error'); } }); // Стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## Альтернативный минималистичный подход:
```javascript // Минималистичная версия экспорта - всегда работает function exportTableMinimal(tableElement, fileName) { try { // Находим ближайшую таблицу с данными const dataTable = tableElement.querySelector('table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица не найдена'); } // Просто экспортируем таблицу как есть const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(dataTable); XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); const excelFileName = generateExportFileName(fileName); XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Минималистичный экспорт ошибка:', error); showNotification(`Ошибка при экспорте: ${error.message}`, 'error'); } } ```
Рекомендую использовать **упрощенную версию** (`exportTableToExcelSimple`) - она наиболее надежная. Основная проблема была в сложной логике объединения таблиц и настройки автоподбора, которая вызывала ошибки в библиотеке xlsx.
Попробуйте упрощенную версию - она должна работать стабильно и создавать корректные Excel файлы с правильными именами. Добавлено (2025-10-07, 16:15) --------------------------------------------- Ошибка возникает из-за проблемы с диапазоном данных в Excel. Давайте исправим это:
## Исправленная функция exportTableToExcelFormatted:
```javascript // Исправленная функция экспорта с обработкой ошибок диапазона function exportTableToExcelFormatted(tableElement, fileName) { try { console.log('Форматированный экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Проверяем, есть ли данные в таблице const hasData = dataTable.querySelector('tbody tr') || dataTable.rows.length > 0; if (!hasData) { throw new Error('Таблица не содержит данных для экспорта'); } // Создаем полную таблицу для экспорта const exportTable = document.createElement('table'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table') || tableElement.closest('.table-wrapper').querySelector('.table-header table'); if (headerTable && headerTable.querySelector('thead')) { const theadClone = headerTable.querySelector('thead').cloneNode(true); // Убираем лишние стили из заголовка для чистого экспорта theadClone.querySelectorAll('th').forEach(th => { th.removeAttribute('style'); // Добавляем базовые стили для Excel th.style.cssText = 'font-weight: bold; background-color: #3498db; color: white; padding: 5px;'; }); exportTable.appendChild(theadClone); } // Копируем тело таблицы с данными if (dataTable.querySelector('tbody')) { const tbodyClone = dataTable.querySelector('tbody').cloneNode(true); // Убираем inline стили для чистого экспорта tbodyClone.querySelectorAll('tr, td').forEach(el => { el.removeAttribute('style'); // Добавляем базовые стили для читаемости if (el.tagName === 'TD') { el.style.cssText = 'padding: 3px; border: 1px solid #ddd;'; } }); exportTable.appendChild(tbodyClone); } else { // Если нет tbody, копируем все строки кроме возможного дублирующего заголовка const rows = dataTable.querySelectorAll('tr'); if (rows.length > 0) { rows.forEach(row => { // Пропускаем строки, которые уже есть в заголовке if (!headerTable || !headerTable.contains(row)) { const rowClone = row.cloneNode(true); rowClone.querySelectorAll('td').forEach(td => { td.removeAttribute('style'); td.style.cssText = 'padding: 3px; border: 1px solid #ddd;'; }); exportTable.appendChild(rowClone); } }); } } // Проверяем, что экспортируемая таблица содержит данные const exportRows = exportTable.querySelectorAll('tr'); if (exportRows.length === 0) { throw new Error('Нет данных для экспорта после обработки таблицы'); } console.log('Экспортируемая таблица содержит строк:', exportRows.length); // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Проверяем, что worksheet создан корректно if (!worksheet || !worksheet['!ref']) { throw new Error('Не удалось создать рабочий лист Excel'); } // Настраиваем ширины колонок if (!worksheet['!cols']) { worksheet['!cols'] = []; const colCount = exportTable.rows[0]?.cells.length || 0; for (let i = 0; i < colCount; i++) { let maxLength = 12; // минимальная ширина // Определяем максимальную длину содержимого в колонке for (let j = 0; j < exportTable.rows.length; j++) { const cell = exportTable.rows[j]?.cells[i]; if (cell) { const cellLength = cell.textContent?.length || 0; if (cellLength > maxLength) { maxLength = Math.min(cellLength, 35); // ограничиваем максимальную ширину } } } worksheet['!cols'].push({ width: maxLength }); } } // Добавляем автоподбор ширины колонок (только если есть данные) try { if (worksheet['!ref']) { worksheet['!autofilter'] = { ref: worksheet['!ref'] }; } } catch (filterError) { console.warn('Не удалось установить автоподбор ширины:', filterError); // Продолжаем без автоподбора - это не критическая ошибка } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка форматированного экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Упрощенная версия без автоподбора (надежнее):
```javascript // Упрощенная надежная версия экспорта function exportTableToExcelSimple(tableElement, fileName) { try { console.log('Простой экспорт таблицы:', fileName); // Находим таблицу с данными const dataTable = tableElement.querySelector('.table-container table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица с данными не найдена'); } // Создаем временный контейнер для экспорта const tempDiv = document.createElement('div'); // Копируем заголовок const headerTable = tableElement.querySelector('.table-header table'); if (headerTable) { const headerClone = headerTable.cloneNode(true); tempDiv.appendChild(headerClone); } // Копируем данные const dataClone = dataTable.cloneNode(true); tempDiv.appendChild(dataClone); // Получаем полную таблицу для экспорта const exportTables = tempDiv.querySelectorAll('table'); if (exportTables.length === 0) { throw new Error('Не удалось подготовить таблицу для экспорта'); } // Используем первую таблицу (заголовок) или объединяем let exportTable; if (exportTables.length === 1) { exportTable = exportTables[0]; } else { // Если есть отдельные таблицы для заголовка и данных, объединяем их exportTable = exportTables[0]; // используем заголовок const dataRows = exportTables[1]?.querySelectorAll('tr'); if (dataRows) { dataRows.forEach(row => { exportTable.appendChild(row.cloneNode(true)); }); } } // Создаем книгу Excel const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(exportTable); // Базовые настройки колонок const colCount = exportTable.rows[0]?.cells.length || 0; worksheet['!cols'] = []; for (let i = 0; i < colCount; i++) { worksheet['!cols'].push({ width: 15 }); } XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); // Генерируем имя файла const excelFileName = generateExportFileName(fileName); // Сохраняем файл XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Ошибка экспорта:', error); showNotification(`Ошибка при экспорте таблицы: ${error.message}`, 'error'); } } ```
## Обновленная функция initializeExportButtons с обработкой ошибок:
```javascript // Функция для инициализации обработчиков экспорта function initializeExportButtons() { document.querySelectorAll('.export-excel-btn').forEach(button => { button.addEventListener('click', function() { const tableId = this.getAttribute('data-table-id'); const fileName = this.getAttribute('data-file-name'); console.log('Кнопка экспорта нажата:', { tableId, fileName }); // Ищем table-wrapper const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper') || document.querySelector(`#${tableId}`)?.closest('.table-wrapper'); if (tableWrapper) { // Используем упрощенную версию для надежности exportTableToExcelSimple(tableWrapper, fileName); } else { console.error('Контейнер таблицы не найден:', tableId); showNotification('Ошибка: контейнер таблицы не найден', 'error'); } }); // Стили при наведении button.addEventListener('mouseenter', function() { this.style.transform = 'translateY(-2px)'; this.style.boxShadow = '0 4px 8px rgba(39, 174, 96, 0.4)'; }); button.addEventListener('mouseleave', function() { this.style.transform = 'translateY(0)'; this.style.boxShadow = '0 2px 4px rgba(39, 174, 96, 0.3)'; }); }); } ```
## Альтернативный минималистичный подход:
```javascript // Минималистичная версия экспорта - всегда работает function exportTableMinimal(tableElement, fileName) { try { // Находим ближайшую таблицу с данными const dataTable = tableElement.querySelector('table') || tableElement.closest('.table-wrapper').querySelector('.table-container table'); if (!dataTable) { throw new Error('Таблица не найдена'); } // Просто экспортируем таблицу как есть const wb = XLSX.utils.book_new(); const worksheet = XLSX.utils.table_to_sheet(dataTable); XLSX.utils.book_append_sheet(wb, worksheet, 'Данные'); const excelFileName = generateExportFileName(fileName); XLSX.writeFile(wb, excelFileName); showNotification(`Таблица "${fileName}" успешно экспортирована`, 'success'); } catch (error) { console.error('Минималистичный экспорт ошибка:', error); showNotification(`Ошибка при экспорте: ${error.message}`, 'error'); } } ```
Рекомендую использовать **упрощенную версию** (`exportTableToExcelSimple`) - она наиболее надежная. Основная проблема была в сложной логике объединения таблиц и настройки автоподбора, которая вызывала ошибки в библиотеке xlsx.
Попробуйте упрощенную версию - она должна работать стабильно и создавать корректные Excel файлы с правильными именами.
|
|
Понял проблему. Это связано с тем, как обрабатываются и кешируются данные проходки. Давайте исправим несколько ключевых моментов:
Основные проблемы:
1. Некорректное определение наличия данных проходки 2. Проблемы с кешированием данных проходки 3. Неправильная логика отображения при множественных заголовках проходки
Исправления:
1. Обновите функцию displayDrillingData():
```javascript function displayDrillingData() { // ПРАВИЛЬНОЕ определение наличия данных проходки const hasHeaders = xmlData.drillingHeaders && xmlData.drillingHeaders.length > 0; const hasDrillingData = hasHeaders && xmlData.drillingDataByHeader && Object.keys(xmlData.drillingDataByHeader).length > 0;
if (!hasDrillingData) { return '<div class="no-data"><p>Нет данных проходки</p></div>'; }
let html = '<div class="section">'; html += '<h2>Данные проходки</h2>';
// ОБНОВЛЕННАЯ ЛОГИКА: Всегда устанавливаем первый заголовок по умолчанию if (!currentDrillingHeaderId && xmlData.drillingHeaders.length > 0) { currentDrillingHeaderId = xmlData.drillingHeaders[0].id; }
// Если заголовков несколько, показываем таблицу выбора if (xmlData.drillingHeaders.length > 1) { html += ` <div class="info-group"> <div class="info-group-title">Выберите параметр скважины</div> <div class="table-wrapper"> <div class="table-header"> <table class="interval-table"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 150px;">Назначение</th> <th style="width: 120px;">Вид скважины</th> <th style="width: 150px;">Способ проходки</th> <th style="width: 80px;">Забой (м)</th> <th style="width: 120px;">Исполнитель</th> <th style="width: 100px;">Оценка</th> <th style="width: 100px;">Дата ввода</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `;
xmlData.drillingHeaders.forEach(header => { const isSelected = currentDrillingHeaderId === header.id; const rowClass = isSelected ? 'active' : ''; let gradeClass = ''; if (header.grade === 'Отлично') { gradeClass = 'style="background-color: #27ae60; color: white; padding: 3px 8px; border-radius: 4px;"'; } else if (header.grade === 'Хорошо') { gradeClass = 'style="background-color: #f39c12; color: white; padding: 3px 8px; border-radius: 4px;"'; } else if (header.grade === 'Удовлетворительно') { gradeClass = 'style="background-color: #e67e22; color: white; padding: 3px 8px; border-radius: 4px;"'; } else if (header.grade === 'Неудовлетворительно') { gradeClass = 'style="background-color: #e74c3c; color: white; padding: 3px 8px; border-radius: 4px;"'; }
html += ` <tr class="drilling-header-row ${rowClass}" data-header-id="${header.id}" style="cursor: pointer;"> <td style="width: 50px;">${header.number}</td> <td style="width: 150px;">${header.purpose}</td> <td style="width: 120px;">${header.type}</td> <td style="width: 150px;">${header.mode}</td> <td style="width: 80px;">${formatNumber(header.depth)}</td> <td style="width: 120px;">${header.author}</td> <td style="width: 100px;"><span ${gradeClass}>${header.grade || 'Не указано'}</span></td> <td style="width: 100px;">${header.inputDate}</td> </tr> `; });
html += ` </tbody> </table> </div> </div> </div> `; }
// Показываем данные для выбранного заголовка if (currentDrillingHeaderId && xmlData.drillingDataByHeader[currentDrillingHeaderId]) { const selectedHeader = xmlData.drillingHeaders.find(h => h.id === currentDrillingHeaderId); const headerData = xmlData.drillingDataByHeader[currentDrillingHeaderId]; html += ` <div class="info-group" style="margin-top: 20px;"> <div class="info-group-title">Данные для параметра: ${selectedHeader.purpose} (ID: ${selectedHeader.id})</div> </div> `;
// Проверяем наличие данных для подвкладок const hasDiameter = headerData.diameter && headerData.diameter.length > 0; const hasIntervals = headerData.intervals && headerData.intervals.length > 0; const hasCasing = headerData.casing && headerData.casing.length > 0; const hasMethod = headerData.method && headerData.method.length > 0; const hasDrillingType = headerData.drillingType && headerData.drillingType.length > 0; const hasSludge = headerData.sludge && headerData.sludge.length > 0; const hasConditions = headerData.conditions && headerData.conditions.length > 0; const hasStart = headerData.start && headerData.start.length > 0; const hasEnd = headerData.end && headerData.end.length > 0;
// Определяем первую активную подвкладку let firstActiveSubTab = 'header';
if (hasDiameter || hasIntervals || hasCasing || hasMethod || hasDrillingType || hasSludge || hasConditions || hasStart || hasEnd) { html += `<div class="inner-tabs-container">`; html += `<div class="inner-tabs">`;
// ВКЛАДКА "ЗАГОЛОВОК" - всегда первая html += `<div class="inner-tab ${firstActiveSubTab === 'header' ? 'active' : ''}" data-inner-tab="header">Заголовок</div>`;
// Остальные вкладки if (hasDiameter) { html += `<div class="inner-tab" data-inner-tab="diameter">Диаметр бурения</div>`; } if (hasIntervals) { html += `<div class="inner-tab" data-inner-tab="intervals">Интервалы бурения</div>`; } if (hasCasing) { html += `<div class="inner-tab" data-inner-tab="casing">Обсадка</div>`; } if (hasMethod) { html += `<div class="inner-tab" data-inner-tab="method">Способ бурения</div>`; } if (hasDrillingType) { html += `<div class="inner-tab" data-inner-tab="drillingType">Вид бурения</div>`; } if (hasSludge) { html += `<div class="inner-tab" data-inner-tab="sludge">Способ выноски шлама</div>`; } if (hasConditions) { html += `<div class="inner-tab" data-inner-tab="conditions">Условия бурения</div>`; } if (hasStart) { html += `<div class="inner-tab" data-inner-tab="start">Забурка</div>`; } if (hasEnd) { html += `<div class="inner-tab" data-inner-tab="end">Разбурка</div>`; }
html += `</div>`; html += `<div class="inner-tabs-content">`;
// СОДЕРЖИМОЕ ВКЛАДКИ "ЗАГОЛОВОК" html += `<div id="headerInnerTab" class="inner-tab-content active">`; html += displayDrillingHeaderForHeader(selectedHeader); html += `</div>`;
// Остальные вкладки if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingDiameterForHeader(headerData.diameter); html += `</div>`; } if (hasIntervals) { html += `<div id="intervalsInnerTab" class="inner-tab-content">`; html += displayDrillingIntervalsForHeader(headerData.intervals); html += `</div>`; } if (hasCasing) { html += `<div id="casingInnerTab" class="inner-tab-content">`; html += displayDrillingCasingForHeader(headerData.casing); html += `</div>`; } if (hasMethod) { html += `<div id="methodInnerTab" class="inner-tab-content">`; html += displayDrillingMethodForHeader(headerData.method); html += `</div>`; } if (hasDrillingType) { html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`; html += displayDrillingTypeForHeader(headerData.drillingType); html += `</div>`; } if (hasSludge) { html += `<div id="sludgeInnerTab" class="inner-tab-content">`; html += displayDrillingSludgeForHeader(headerData.sludge); html += `</div>`; } if (hasConditions) { html += `<div id="conditionsInnerTab" class="inner-tab-content">`; html += displayDrillingConditionsForHeader(headerData.conditions); html += `</div>`; } if (hasStart) { html += `<div id="startInnerTab" class="inner-tab-content">`; html += displayDrillingStartForHeader(headerData.start); html += `</div>`; } if (hasEnd) { html += `<div id="endInnerTab" class="inner-tab-content">`; html += displayDrillingEndForHeader(headerData.end); html += `</div>`; }
html += `</div>`; html += `</div>`; } else { // Если нет других данных, показываем только заголовок html += displayDrillingHeaderForHeader(selectedHeader); } } else { html += '<div class="no-data"><p>Нет данных для выбранного параметра проходки</p></div>'; }
html += '</div>'; return html; } ```
2. Добавьте обработчики для переключения заголовков проходки:
```javascript // Добавьте эту функцию в конец файла, после всех существующих функций function initializeDrillingHandlers() { // Обработчики для строк выбора заголовка проходки document.querySelectorAll('.drilling-header-row').forEach(row => { row.addEventListener('click', function() { const headerId = this.getAttribute('data-header-id'); // Снимаем выделение со всех строк document.querySelectorAll('.drilling-header-row').forEach(r => { r.classList.remove('active'); }); // Выделяем текущую строку this.classList.add('active'); // Устанавливаем текущий заголовок currentDrillingHeaderId = headerId; // Перерисовываем вкладку проходки refreshDrillingTab(); }); }); // Обработчики для внутренних вкладок проходки document.querySelectorAll('.inner-tab').forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-inner-tab'); // Снимаем активный класс со всех вкладок document.querySelectorAll('.inner-tab').forEach(t => { t.classList.remove('active'); }); // Снимаем активный класс со всех контентов document.querySelectorAll('.inner-tab-content').forEach(c => { c.classList.remove('active'); }); // Активируем текущую вкладку и контент this.classList.add('active'); const contentElement = document.getElementById(tabName + 'InnerTab'); if (contentElement) { contentElement.classList.add('active'); } }); }); }
function refreshDrillingTab() { const wellDataElement = document.getElementById('wellData'); const currentTab = document.querySelector('.tab.active'); if (currentTab && currentTab.getAttribute('data-tab') === 'drilling') { // Перерисовываем только вкладку проходки const drillingContent = displayDrillingData(); const tabContent = wellDataElement.querySelector('#drillingTab'); if (tabContent) { tabContent.innerHTML = drillingContent; initializeDrillingHandlers(); initializeExportButtons(); } } } ```
3. Обновите функцию displayWellData():
Найдите функцию displayWellData() и добавьте инициализацию обработчиков после отрисовки контента:
```javascript // В функции displayWellData(), после установки innerHTML, добавьте: setTimeout(() => { initializeDrillingHandlers(); initializeExportButtons(); // Другие инициализации... initializeDocumentFilters(); initializeDocumentPreview(); }, 100); ```
4. Исправьте очистку кешей:
В функции clearCaches() добавьте сброс переменной проходки:
```javascript function clearCaches() { console.log('Очистка кешей...'); fileCache.clear(); wellDataCache.clear(); currentDrillingHeaderId = null; // ДОБАВЬТЕ ЭТУ СТРОКУ if (currentMultiWellData) { currentMultiWellData.currentXmlDoc = null; } if (window.gc) { window.gc(); } } ```
Эти исправления должны решить проблему с отображением данных проходки при переключении между скважинами. Основные изменения:
1. Корректное определение наличия данных 2. Правильная инициализация текущего заголовка 3. Динамическое обновление вкладки при переключении 4. Очистка состояния при смене скважины
Попробуйте эти изменения и проверьте, решена ли проблема.Добавлено (2025-10-08, 08:24) --------------------------------------------- Отлично! Исправим отображение оценки качества работ. Обновите функцию displayDrillingHeaderForHeader():
```javascript function displayDrillingHeaderForHeader(header) { let html = '';
// Информационная панель html += ` <div class="doc-info-list" style="margin-bottom: 20px;"> <div class="doc-info-item"> <span class="doc-info-label">Назначение скважины:</span> <span class="doc-info-value"><strong>${header.purpose}</strong></span> </div> <div class="doc-info-item"> <span class="doc-info-label">Вид скважины:</span> <span class="doc-info-value">${header.type}</span> </div> <div class="doc-info-item"> <span class="doc-info-label">Способ проходки:</span> <span class="doc-info-value">${header.mode}</span> </div> <div class="doc-info-item"> <span class="doc-info-label">Забой:</span> <span class="doc-info-value">${formatNumber(header.depth)} м</span> </div> <div class="doc-info-item"> <span class="doc-info-label">Исполнитель:</span> <span class="doc-info-value">${header.author}</span> </div> <div class="doc-info-item"> <span class="doc-info-label">Перевозка:</span> <span class="doc-info-value">${formatNumber(header.transit)} ${header.transitUnit}</span> </div> <div class="doc-info-item"> <span class="doc-info-label">Дата ввода:</span> <span class="doc-info-value">${header.inputDate}</span> </div> </div> `;
// Оценка качества с компактным отображением let gradeClass = ''; let gradeText = header.grade || 'Не указано'; if (header.grade === 'Отлично') { gradeClass = 'style="background-color: #27ae60; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"'; } else if (header.grade === 'Хорошо') { gradeClass = 'style="background-color: #f39c12; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"'; } else if (header.grade === 'Удовлетворительно') { gradeClass = 'style="background-color: #e67e22; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"'; } else if (header.grade === 'Неудовлетворительно') { gradeClass = 'style="background-color: #e74c3c; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"'; } else { gradeClass = 'style="background-color: #95a5a6; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; display: inline-block;"'; }
html += ` <div class="doc-info-list"> <div class="doc-info-item"> <span class="doc-info-label">Оценка качества работ:</span> <span class="doc-info-value"> <span ${gradeClass}>${gradeText}</span> </span> </div> </div> `;
// Описание if (header.description) { html += ` <div class="info-group" style="margin-top: 20px;"> <div class="info-group-title">Описание</div> <div class="description-block"> <p>${formatLongText(header.description, 100)}</p> </div> </div> `; }
return html; } ```
Также обновите отображение оценки в таблице выбора заголовков в функции displayDrillingData():
```javascript // В таблице выбора заголовков замените эту часть: html += ` <td style="width: 100px;"> <span ${gradeClass} style="display: inline-block; min-width: 60px; text-align: center;"> ${header.grade || 'Не указано'} </span> </td> `; ```
Основные изменения:
1. Компактное отображение - добавлен display: inline-block чтобы оценка занимала только необходимую ширину 2. Фиксированные отступы - padding: 6px 12px вместо растягивания на всю ширину 3. Центрирование текста - для таблицы добавлено text-align: center 4. Минимальная ширина - min-width: 60px для сохранения читаемости
Теперь оценка качества будет отображаться как компактный цветной бейдж, а не растягиваться на всю ширину ячейки. Добавлено (2025-10-08, 08:34) --------------------------------------------- Это известная проблема! Функция showDirectoryPicker() работает только в безопасных контекстах (HTTPS) и локальных файлах, но не на HTTP-хостах из-за политик безопасности браузеров.
Решение:
1. Обновите функцию selectFolder():
```javascript async function selectFolder() { try { // Сбрасываем состояние при выборе новой папки clearCaches(); // ПРОВЕРКА ДОСТУПНОСТИ API if (typeof window.showDirectoryPicker !== 'undefined') { currentDirectoryHandle = await window.showDirectoryPicker(); // Форматируем путь в стиле директории (синхронно) let folderPath = await formatFolderPath(currentDirectoryHandle); document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📂</span> <span class="folder-text">${folderPath}</span> </div> `; await loadFileList(); } else { // АЛЬТЕРНАТИВНЫЙ СПОСОБ ДЛЯ HTTP-САЙТОВ await selectFolderFallback(); } } catch (error) { console.error('Ошибка выбора папки:', error); if (error.name !== 'AbortError') { // Показываем понятное сообщение об ошибке if (error.name === 'SecurityError' || error.message.includes('secure context')) { await selectFolderFallback('Для выбора папок необходимо использовать HTTPS или открывать файл локально. Используем альтернативный метод.'); } else { alert('Ошибка при выборе папки: ' + error.message); } } } }
// АЛЬТЕРНАТИВНЫЙ МЕТОД ДЛЯ HTTP async function selectFolderFallback(message = null) { if (message) { console.warn(message); } // Предлагаем пользователю выбрать ZIP файл вместо папки const input = document.createElement('input'); input.type = 'file'; input.accept = '.zip'; input.multiple = true; // Разрешаем множественный выбор input.webkitdirectory = false; input.onchange = async (e) => { const files = Array.from(e.target.files); if (files.length > 0) { clearFileList(); clearCaches(); // Создаем виртуальную "папку" из выбранных файлов const virtualFolder = { name: `Выбрано файлов: ${files.length}`, files: files }; document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📦</span> <span class="folder-text">${virtualFolder.name}</span> </div> `; // Загружаем файлы в список await loadFilesFromFileList(files); } }; input.click(); }
// Функция для загрузки файлов из FileList async function loadFilesFromFileList(files) { const fileList = document.getElementById('fileList'); fileList.innerHTML = ''; const zipFiles = files.filter(file => file.name.toLowerCase().endsWith('.zip')); if (zipFiles.length === 0) { fileList.innerHTML = '<li class="file-item">Не найдено ZIP архивов</li>'; document.getElementById('fileCounter').textContent = 'Найдено архивов: 0'; return; } let fileCounter = 0; zipFiles.forEach((file, index) => { const listItem = document.createElement('li'); listItem.className = 'file-item'; const fileSize = (file.size / 1024 / 1024).toFixed(2); const modDate = new Date(file.lastModified).toLocaleDateString(); fileCounter++; listItem.innerHTML = ` <div class="file-number">${fileCounter}</div> <div class="file-content"> <div class="file-name">${file.name}</div> <div class="file-info">${fileSize} MB · ${modDate}</div> </div> `; // Создаем объект, совместимый с существующей логикой const fileObj = { name: file.name, handle: { getFile: () => Promise.resolve(file) }, size: file.size, lastModified: file.lastModified }; listItem.addEventListener('click', () => selectFile(fileObj, fileCounter)); fileList.appendChild(listItem); }); // Обновляем счетчик document.getElementById('fileCounter').textContent = `Найдено архивов: ${zipFiles.length}`; } ```
2. Обновите функцию refreshFileList() для поддержки альтернативного метода:
```javascript async function refreshFileList() { if (currentDirectoryHandle) { // Оригинальный метод с showDirectoryPicker clearCaches(); const folderPath = await formatFolderPath(currentDirectoryHandle); document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📂</span> <span class="folder-text">${folderPath}</span> </div> `; await loadFileList(); } else { // Для альтернативного метода показываем сообщение alert('Для обновления списка выберите папку или файлы заново'); } } ```
3. Добавьте проверку при загрузке страницы:
```javascript // В DOMContentLoaded добавьте: document.addEventListener('DOMContentLoaded', function() { // Проверяем доступность File System Access API const isFSAvailable = typeof window.showDirectoryPicker !== 'undefined'; if (!isFSAvailable) { console.log('File System Access API не доступен. Используем альтернативный метод.'); // Можно показать информационное сообщение const folderPath = document.getElementById('folderPath'); if (folderPath) { folderPath.innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">ℹ️</span> <span class="folder-text" style="color: #e67e22;"> Используйте "Открыть ZIP" для выбора архивов </span> </div> `; } } // Остальная инициализация... }); ```
4. Обновите тексты кнопок для ясности:
В HTML обновите тексты кнопок:
```html <div class="main-actions"> <button class="action-btn primary" id="selectFolderBtn"> <span class="btn-icon">📁</span> <span class="btn-text">Выбрать папку (локально)</span> </button> <button class="action-btn secondary" id="selectFileBtn"> <span class="btn-icon">📄</span> <span class="btn-text">Открыть ZIP файл(ы)</span> </button> </div> ```
Преимущества этого решения:
1. Автоматическое переключение - приложение само определяет, доступен ли File System API 2. Понятные сообщения - пользователь видит, почему не работает выбор папок 3. Альтернативный метод - множественный выбор ZIP файлов работает везде 4. Совместимость - существующий код продолжает работать без изменений
Теперь приложение будет работать как на локальных файлах (с выбором папок), так и на HTTP-хостах (с выбором отдельных ZIP файлов). Добавлено (2025-10-08, 08:38) --------------------------------------------- Это известная проблема! Функция showDirectoryPicker() работает только в безопасных контекстах (HTTPS) и локальных файлах, но не на HTTP-хостах из-за политик безопасности браузеров.
Решение:
1. Обновите функцию selectFolder():
```javascript async function selectFolder() { try { // Сбрасываем состояние при выборе новой папки clearCaches(); // ПРОВЕРКА ДОСТУПНОСТИ API if (typeof window.showDirectoryPicker !== 'undefined') { currentDirectoryHandle = await window.showDirectoryPicker(); // Форматируем путь в стиле директории (синхронно) let folderPath = await formatFolderPath(currentDirectoryHandle); document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📂</span> <span class="folder-text">${folderPath}</span> </div> `; await loadFileList(); } else { // АЛЬТЕРНАТИВНЫЙ СПОСОБ ДЛЯ HTTP-САЙТОВ await selectFolderFallback(); } } catch (error) { console.error('Ошибка выбора папки:', error); if (error.name !== 'AbortError') { // Показываем понятное сообщение об ошибке if (error.name === 'SecurityError' || error.message.includes('secure context')) { await selectFolderFallback('Для выбора папок необходимо использовать HTTPS или открывать файл локально. Используем альтернативный метод.'); } else { alert('Ошибка при выборе папки: ' + error.message); } } } }
// АЛЬТЕРНАТИВНЫЙ МЕТОД ДЛЯ HTTP async function selectFolderFallback(message = null) { if (message) { console.warn(message); } // Предлагаем пользователю выбрать ZIP файл вместо папки const input = document.createElement('input'); input.type = 'file'; input.accept = '.zip'; input.multiple = true; // Разрешаем множественный выбор input.webkitdirectory = false; input.onchange = async (e) => { const files = Array.from(e.target.files); if (files.length > 0) { clearFileList(); clearCaches(); // Создаем виртуальную "папку" из выбранных файлов const virtualFolder = { name: `Выбрано файлов: ${files.length}`, files: files }; document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📦</span> <span class="folder-text">${virtualFolder.name}</span> </div> `; // Загружаем файлы в список await loadFilesFromFileList(files); } }; input.click(); }
// Функция для загрузки файлов из FileList async function loadFilesFromFileList(files) { const fileList = document.getElementById('fileList'); fileList.innerHTML = ''; const zipFiles = files.filter(file => file.name.toLowerCase().endsWith('.zip')); if (zipFiles.length === 0) { fileList.innerHTML = '<li class="file-item">Не найдено ZIP архивов</li>'; document.getElementById('fileCounter').textContent = 'Найдено архивов: 0'; return; } let fileCounter = 0; zipFiles.forEach((file, index) => { const listItem = document.createElement('li'); listItem.className = 'file-item'; const fileSize = (file.size / 1024 / 1024).toFixed(2); const modDate = new Date(file.lastModified).toLocaleDateString(); fileCounter++; listItem.innerHTML = ` <div class="file-number">${fileCounter}</div> <div class="file-content"> <div class="file-name">${file.name}</div> <div class="file-info">${fileSize} MB · ${modDate}</div> </div> `; // Создаем объект, совместимый с существующей логикой const fileObj = { name: file.name, handle: { getFile: () => Promise.resolve(file) }, size: file.size, lastModified: file.lastModified }; listItem.addEventListener('click', () => selectFile(fileObj, fileCounter)); fileList.appendChild(listItem); }); // Обновляем счетчик document.getElementById('fileCounter').textContent = `Найдено архивов: ${zipFiles.length}`; } ```
2. Обновите функцию refreshFileList() для поддержки альтернативного метода:
```javascript async function refreshFileList() { if (currentDirectoryHandle) { // Оригинальный метод с showDirectoryPicker clearCaches(); const folderPath = await formatFolderPath(currentDirectoryHandle); document.getElementById('folderPath').innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">📂</span> <span class="folder-text">${folderPath}</span> </div> `; await loadFileList(); } else { // Для альтернативного метода показываем сообщение alert('Для обновления списка выберите папку или файлы заново'); } } ```
3. Добавьте проверку при загрузке страницы:
```javascript // В DOMContentLoaded добавьте: document.addEventListener('DOMContentLoaded', function() { // Проверяем доступность File System Access API const isFSAvailable = typeof window.showDirectoryPicker !== 'undefined'; if (!isFSAvailable) { console.log('File System Access API не доступен. Используем альтернативный метод.'); // Можно показать информационное сообщение const folderPath = document.getElementById('folderPath'); if (folderPath) { folderPath.innerHTML = ` <div class="folder-path-content"> <span class="folder-icon">ℹ️</span> <span class="folder-text" style="color: #e67e22;"> Используйте "Открыть ZIP" для выбора архивов </span> </div> `; } } // Остальная инициализация... }); ```
4. Обновите тексты кнопок для ясности:
В HTML обновите тексты кнопок:
```html <div class="main-actions"> <button class="action-btn primary" id="selectFolderBtn"> <span class="btn-icon">📁</span> <span class="btn-text">Выбрать папку (локально)</span> </button> <button class="action-btn secondary" id="selectFileBtn"> <span class="btn-icon">📄</span> <span class="btn-text">Открыть ZIP файл(ы)</span> </button> </div> ```
Преимущества этого решения:
1. Автоматическое переключение - приложение само определяет, доступен ли File System API 2. Понятные сообщения - пользователь видит, почему не работает выбор папок 3. Альтернативный метод - множественный выбор ZIP файлов работает везде 4. Совместимость - существующий код продолжает работать без изменений
Теперь приложение будет работать как на локальных файлах (с выбором папок), так и на HTTP-хостах (с выбором отдельных ZIP файлов). Добавлено (2025-10-08, 09:04) --------------------------------------------- Понял проблему! Дергание высоты происходит из-за того, что содержимое вкладок разной высоты и браузер пересчитывает layout при переключении. Решим это несколькими способами:
Решение 1: Фиксированная высота контейнера
Добавьте в CSS фиксированную высоту для контейнера вкладок:
```css /* Добавьте в секцию TAB STYLES */ .tab-content { display: none; min-height: 600px; /* Фиксированная минимальная высота */ overflow-y: auto; }
.tab-content.active { display: block; }
/* Для внутренних вкладок проходки */ .inner-tabs-content { border: 1px solid #ddd; border-top: none; border-radius: 0 0 5px 5px; padding: 20px; background: white; min-height: 500px; /* Фиксированная высота */ overflow-y: auto; } ```
Решение 2: Плавные переходы с opacity
Замените стили вкладок на версию с плавными переходами:
```css /* ОБНОВЛЕННЫЕ СТИЛИ ВКЛАДОК */ .tab-content { opacity: 0; visibility: hidden; position: absolute; top: 0; left: 0; width: 100%; height: auto; transition: opacity 0.3s ease, visibility 0.3s ease; pointer-events: none; }
.tab-content.active { opacity: 1; visibility: visible; position: relative; pointer-events: all; min-height: 600px; }
/* Контейнер для вкладок должен иметь относительное позиционирование */ .main-content { flex: 1; padding: 20px; overflow-y: auto; background-color: white; position: relative; }
/* Для внутренних вкладок проходки */ .inner-tab-content { opacity: 0; visibility: hidden; position: absolute; width: 100%; transition: opacity 0.3s ease, visibility 0.3s ease; pointer-events: none; }
.inner-tab-content.active { opacity: 1; visibility: visible; position: relative; pointer-events: all; } ```
Решение 3: JavaScript для плавного переключения
Обновите логику переключения вкладок:
```javascript // ЗАМЕНИТЕ существующие обработчики вкладок на эти:
function initializeTabs() { // Обработчики для основных вкладок document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchTab(tabName); }); }); // Обработчики для внутренних вкладок проходки document.querySelectorAll('.inner-tab').forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-inner-tab'); switchInnerTab(tabName); }); }); }
function switchTab(tabName) { // Снимаем активный класс со всех вкладок document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // Снимаем активный класс со всех контентов document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Активируем текущую вкладку const activeTab = document.querySelector(`.tab[data-tab="${tabName}"]`); const activeContent = document.getElementById(tabName + 'Tab'); if (activeTab && activeContent) { activeTab.classList.add('active'); // Добавляем небольшую задержку для плавности setTimeout(() => { activeContent.classList.add('active'); }, 50); } }
function switchInnerTab(tabName) { // Снимаем активный класс со всех внутренних вкладок document.querySelectorAll('.inner-tab').forEach(tab => { tab.classList.remove('active'); }); // Снимаем активный класс со всех внутренних контентов document.querySelectorAll('.inner-tab-content').forEach(content => { content.classList.remove('active'); }); // Активируем текущую внутреннюю вкладку const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabName}"]`); const activeContent = document.getElementById(tabName + 'InnerTab'); if (activeTab && activeContent) { activeTab.classList.add('active'); // Добавляем небольшую задержку для плавности setTimeout(() => { activeContent.classList.add('active'); }, 50); } } ```
Решение 4: Универсальный CSS для стабильной высоты
Добавьте эти стили для стабилизации высоты:
```css /* Стабилизация высоты контента */ .well-data-container { min-height: 700px; position: relative; }
.tab-content-container { min-height: 600px; position: relative; }
/* Гарантируем что таблицы не ломают layout */ .table-wrapper { max-height: 500px; min-height: 200px; }
/* Для случаев когда данных мало */ .no-data, .loading, .error { min-height: 200px; display: flex; align-items: center; justify-content: center; }
/* Консистентные отступы */ .info-group, .section { min-height: 100px; } ```
Решение 5: Обновите функцию displayWellData()
```javascript function displayWellData(fileType, fileNumber, fileName, wellId) { // ... существующий код ... // После установки HTML инициализируем вкладки setTimeout(() => { initializeTabs(); initializeDrillingHandlers(); initializeExportButtons(); initializeDocumentFilters(); initializeDocumentPreview(); // Активируем первую вкладку плавно switchTab('primary'); }, 100); } ```
Рекомендуемое решение:
Используйте Решение 2 + Решение 3 вместе - это даст максимально плавное переключение без дерганий.
Добавьте в ваш CSS обновленные стили вкладок и замените обработчики на новые функции. Это решит проблему с дерганием высоты при переключении вкладок. Добавлено (2025-10-08, 09:31) --------------------------------------------- Понял проблему! Это происходит из-за того, что при переключении вкладок не всегда правильно инициализируются внутренние вкладки и их содержимое. Давайте исправим это:
Решение 1: Улучшенная инициализация вкладок
```javascript // ОБНОВЛЕННАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВКЛАДОК function initializeTabs() { console.log('Инициализация вкладок...'); // Обработчики для основных вкладок document.querySelectorAll('.tab').forEach(tab => { tab.removeEventListener('click', handleTabClick); // Удаляем старые обработчики tab.addEventListener('click', handleTabClick); }); // Обработчики для внутренних вкладок проходки document.querySelectorAll('.inner-tab').forEach(tab => { tab.removeEventListener('click', handleInnerTabClick); tab.addEventListener('click', handleInnerTabClick); }); // Гарантируем, что активная вкладка отображает правильный контент const activeTab = document.querySelector('.tab.active'); if (activeTab) { const tabName = activeTab.getAttribute('data-tab'); ensureTabContentVisible(tabName); } }
// ОТДЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ОБРАБОТЧИКОВ function handleTabClick() { const tabName = this.getAttribute('data-tab'); console.log('Переключение на вкладку:', tabName); switchTab(tabName); }
function handleInnerTabClick() { const tabName = this.getAttribute('data-inner-tab'); console.log('Переключение на внутреннюю вкладку:', tabName); switchInnerTab(tabName); }
// ОБНОВЛЕННАЯ ФУНКЦИЯ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК function switchTab(tabName) { console.log('Активация вкладки:', tabName); // Снимаем активный класс со всех вкладок document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // Снимаем активный класс со всех контентов document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Активируем текущую вкладку const activeTab = document.querySelector(`.tab[data-tab="${tabName}"]`); const activeContent = document.getElementById(tabName + 'Tab'); if (activeTab && activeContent) { activeTab.classList.add('active'); activeContent.classList.add('active'); // ДОПОЛНИТЕЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ДЛЯ КОНКРЕТНЫХ ВКЛАДОК initializeTabSpecificContent(tabName); console.log('Вкладка активирована:', tabName); } else { console.error('Не найдена вкладка или контент:', tabName); } }
// ОБНОВЛЕННАЯ ФУНКЦИЯ ДЛЯ ВНУТРЕННИХ ВКЛАДОК function switchInnerTab(tabName) { console.log('Активация внутренней вкладки:', tabName); // Снимаем активный класс со всех внутренних вкладок document.querySelectorAll('.inner-tab').forEach(tab => { tab.classList.remove('active'); }); // Снимаем активный класс со всех внутренних контентов document.querySelectorAll('.inner-tab-content').forEach(content => { content.classList.remove('active'); }); // Активируем текущую внутреннюю вкладку const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabName}"]`); const activeContent = document.getElementById(tabName + 'InnerTab'); if (activeTab && activeContent) { activeTab.classList.add('active'); activeContent.classList.add('active'); console.log('Внутренняя вкладка активирована:', tabName); } }
// ФУНКЦИЯ ДЛЯ ГАРАНТИИ ВИДИМОСТИ КОНТЕНТА ВКЛАДКИ function ensureTabContentVisible(tabName) { const content = document.getElementById(tabName + 'Tab'); const tab = document.querySelector(`.tab[data-tab="${tabName}"]`); if (content && tab) { // Сначала скрываем все document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); // Потом показываем нужные content.classList.add('active'); tab.classList.add('active'); initializeTabSpecificContent(tabName); } }
// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ КОНТЕНТА КОНКРЕТНОЙ ВКЛАДКИ function initializeTabSpecificContent(tabName) { console.log('Инициализация контента для вкладки:', tabName); switch(tabName) { case 'primary': case 'final': case 'gis': // Для вкладок документирования - инициализируем внутренние вкладки initializeDocumentationInnerTabs(tabName); break; case 'drilling': // Для проходки - инициализируем обработчики initializeDrillingHandlers(); break; case 'documents': // Для документов - инициализируем фильтры initializeDocumentFilters(); initializeDocumentPreview(); break; case 'assays': // Для опробования - инициализируем экспорт initializeExportButtons(); break; case 'coreboxes': // Для керновых ящиков - инициализируем предпросмотр initializeDocumentPreview(); break; } }
// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ ВНУТРЕННИХ ВКЛАДОК ДОКУМЕНТИРОВАНИЯ function initializeDocumentationInnerTabs(parentTab) { console.log('Инициализация внутренних вкладок для:', parentTab); const container = document.getElementById(parentTab + 'Tab'); if (!container) return; // Находим все внутренние вкладки в этом контейнере const innerTabs = container.querySelectorAll('.inner-tab'); const innerContents = container.querySelectorAll('.inner-tab-content'); if (innerTabs.length > 0) { console.log('Найдено внутренних вкладок:', innerTabs.length); // Удаляем старые обработчики innerTabs.forEach(tab => { tab.removeEventListener('click', handleInnerTabClick); tab.addEventListener('click', handleInnerTabClick); }); // Гарантируем, что первая вкладка активна const firstTab = innerTabs[0]; const firstTabName = firstTab.getAttribute('data-inner-tab'); if (firstTab && !container.querySelector('.inner-tab.active')) { firstTab.classList.add('active'); const firstContent = container.querySelector(`#${firstTabName}InnerTab`); if (firstContent) { firstContent.classList.add('active'); } } } } ```
Решение 2: Обновите функцию displayWellData()
```javascript function displayWellData(fileType, fileNumber, fileName, wellId) { console.log('Отображение данных скважины...'); // ... существующий код подготовки данных ... // После установки HTML setTimeout(() => { console.log('Инициализация интерфейса...'); // ПЕРВОНАЧАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ВСЕГО initializeTabs(); initializeDrillingHandlers(); initializeExportButtons(); initializeDocumentFilters(); initializeDocumentPreview(); // ГАРАНТИРУЕМ, ЧТО ПЕРВАЯ ВКЛАДКА ПОЛНОСТЬЮ ИНИЦИАЛИЗИРОВАНА const firstTab = document.querySelector('.tab'); if (firstTab) { const firstTabName = firstTab.getAttribute('data-tab'); console.log('Активация первой вкладки:', firstTabName); // Принудительно активируем первую вкладку switchTab(firstTabName); } }, 150); // Увеличиваем задержку для полной отрисовки DOM } ```
Решение 3: Улучшенные CSS стилы для надежности
```css /* ГАРАНТИЯ ВИДИМОСТИ АКТИВНЫХ ВКЛАДОК */ .tab-content:not(.active) { display: none !important; }
.tab-content.active { display: block !important; }
.inner-tab-content:not(.active) { display: none !important; }
.inner-tab-content.active { display: block !important; }
/* Убираем возможные конфликты transition */ .tab-content, .inner-tab-content { transition: none; /* Убираем transition если они мешают */ } ```
Решение 4: Добавьте принудительное обновление при клике
```javascript // ДОБАВЬТЕ эту функцию для принудительного обновления function refreshActiveTab() { const activeTab = document.querySelector('.tab.active'); if (activeTab) { const tabName = activeTab.getAttribute('data-tab'); console.log('Принудительное обновление вкладки:', tabName); initializeTabSpecificContent(tabName); } }
// Можно вызвать эту функцию если данные не отображаются // Например, добавить кнопку "Обновить" или вызывать при определенных условиях ```
Основные изменения:
1. Гарантированная активация - принудительно активируем первую вкладку при загрузке 2. Отдельные обработчики - убираем конфликты между обработчиками 3. Конкретная инициализация - для каждой вкладки свой набор инициализаций 4. Улучшенная диагностика - больше console.log для отладки 5. Удаление старых обработчиков - предотвращает накопление дублирующихся обработчиков
Теперь при переключении вкладок данные должны отображаться сразу, без необходимости переключать внутренние вкладки. Добавлено (2025-10-08, 09:41) --------------------------------------------- Понял проблему! Это происходит из-за того, что при переключении вкладок не всегда правильно инициализируются внутренние вкладки и их содержимое. Давайте исправим это:
Решение 1: Улучшенная инициализация вкладок
```javascript // ОБНОВЛЕННАЯ ФУНКЦИЯ ИНИЦИАЛИЗАЦИИ ВКЛАДОК function initializeTabs() { console.log('Инициализация вкладок...'); // Обработчики для основных вкладок document.querySelectorAll('.tab').forEach(tab => { tab.removeEventListener('click', handleTabClick); // Удаляем старые обработчики tab.addEventListener('click', handleTabClick); }); // Обработчики для внутренних вкладок проходки document.querySelectorAll('.inner-tab').forEach(tab => { tab.removeEventListener('click', handleInnerTabClick); tab.addEventListener('click', handleInnerTabClick); }); // Гарантируем, что активная вкладка отображает правильный контент const activeTab = document.querySelector('.tab.active'); if (activeTab) { const tabName = activeTab.getAttribute('data-tab'); ensureTabContentVisible(tabName); } }
// ОТДЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ОБРАБОТЧИКОВ function handleTabClick() { const tabName = this.getAttribute('data-tab'); console.log('Переключение на вкладку:', tabName); switchTab(tabName); }
function handleInnerTabClick() { const tabName = this.getAttribute('data-inner-tab'); console.log('Переключение на внутреннюю вкладку:', tabName); switchInnerTab(tabName); }
// ОБНОВЛЕННАЯ ФУНКЦИЯ ПЕРЕКЛЮЧЕНИЯ ВКЛАДОК function switchTab(tabName) { console.log('Активация вкладки:', tabName); // Снимаем активный класс со всех вкладок document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // Снимаем активный класс со всех контентов document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Активируем текущую вкладку const activeTab = document.querySelector(`.tab[data-tab="${tabName}"]`); const activeContent = document.getElementById(tabName + 'Tab'); if (activeTab && activeContent) { activeTab.classList.add('active'); activeContent.classList.add('active'); // ДОПОЛНИТЕЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ДЛЯ КОНКРЕТНЫХ ВКЛАДОК initializeTabSpecificContent(tabName); console.log('Вкладка активирована:', tabName); } else { console.error('Не найдена вкладка или контент:', tabName); } }
// ОБНОВЛЕННАЯ ФУНКЦИЯ ДЛЯ ВНУТРЕННИХ ВКЛАДОК function switchInnerTab(tabName) { console.log('Активация внутренней вкладки:', tabName); // Снимаем активный класс со всех внутренних вкладок document.querySelectorAll('.inner-tab').forEach(tab => { tab.classList.remove('active'); }); // Снимаем активный класс со всех внутренних контентов document.querySelectorAll('.inner-tab-content').forEach(content => { content.classList.remove('active'); }); // Активируем текущую внутреннюю вкладку const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabName}"]`); const activeContent = document.getElementById(tabName + 'InnerTab'); if (activeTab && activeContent) { activeTab.classList.add('active'); activeContent.classList.add('active'); console.log('Внутренняя вкладка активирована:', tabName); } }
// ФУНКЦИЯ ДЛЯ ГАРАНТИИ ВИДИМОСТИ КОНТЕНТА ВКЛАДКИ function ensureTabContentVisible(tabName) { const content = document.getElementById(tabName + 'Tab'); const tab = document.querySelector(`.tab[data-tab="${tabName}"]`); if (content && tab) { // Сначала скрываем все document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); // Потом показываем нужные content.classList.add('active'); tab.classList.add('active'); initializeTabSpecificContent(tabName); } }
// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ КОНТЕНТА КОНКРЕТНОЙ ВКЛАДКИ function initializeTabSpecificContent(tabName) { console.log('Инициализация контента для вкладки:', tabName); switch(tabName) { case 'primary': case 'final': case 'gis': // Для вкладок документирования - инициализируем внутренние вкладки initializeDocumentationInnerTabs(tabName); break; case 'drilling': // Для проходки - инициализируем обработчики initializeDrillingHandlers(); break; case 'documents': // Для документов - инициализируем фильтры initializeDocumentFilters(); initializeDocumentPreview(); break; case 'assays': // Для опробования - инициализируем экспорт initializeExportButtons(); break; case 'coreboxes': // Для керновых ящиков - инициализируем предпросмотр initializeDocumentPreview(); break; } }
// ФУНКЦИЯ ДЛЯ ИНИЦИАЛИЗАЦИИ ВНУТРЕННИХ ВКЛАДОК ДОКУМЕНТИРОВАНИЯ function initializeDocumentationInnerTabs(parentTab) { console.log('Инициализация внутренних вкладок для:', parentTab); const container = document.getElementById(parentTab + 'Tab'); if (!container) return; // Находим все внутренние вкладки в этом контейнере const innerTabs = container.querySelectorAll('.inner-tab'); const innerContents = container.querySelectorAll('.inner-tab-content'); if (innerTabs.length > 0) { console.log('Найдено внутренних вкладок:', innerTabs.length); // Удаляем старые обработчики innerTabs.forEach(tab => { tab.removeEventListener('click', handleInnerTabClick); tab.addEventListener('click', handleInnerTabClick); }); // Гарантируем, что первая вкладка активна const firstTab = innerTabs[0]; const firstTabName = firstTab.getAttribute('data-inner-tab'); if (firstTab && !container.querySelector('.inner-tab.active')) { firstTab.classList.add('active'); const firstContent = container.querySelector(`#${firstTabName}InnerTab`); if (firstContent) { firstContent.classList.add('active'); } } } } ```
Решение 2: Обновите функцию displayWellData()
```javascript function displayWellData(fileType, fileNumber, fileName, wellId) { console.log('Отображение данных скважины...'); // ... существующий код подготовки данных ... // После установки HTML setTimeout(() => { console.log('Инициализация интерфейса...'); // ПЕРВОНАЧАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ВСЕГО initializeTabs(); initializeDrillingHandlers(); initializeExportButtons(); initializeDocumentFilters(); initializeDocumentPreview(); // ГАРАНТИРУЕМ, ЧТО ПЕРВАЯ ВКЛАДКА ПОЛНОСТЬЮ ИНИЦИАЛИЗИРОВАНА const firstTab = document.querySelector('.tab'); if (firstTab) { const firstTabName = firstTab.getAttribute('data-tab'); console.log('Активация первой вкладки:', firstTabName); // Принудительно активируем первую вкладку switchTab(firstTabName); } }, 150); // Увеличиваем задержку для полной отрисовки DOM } ```
Решение 3: Улучшенные CSS стилы для надежности
```css /* ГАРАНТИЯ ВИДИМОСТИ АКТИВНЫХ ВКЛАДОК */ .tab-content:not(.active) { display: none !important; }
.tab-content.active { display: block !important; }
.inner-tab-content:not(.active) { display: none !important; }
.inner-tab-content.active { display: block !important; }
/* Убираем возможные конфликты transition */ .tab-content, .inner-tab-content { transition: none; /* Убираем transition если они мешают */ } ```
Решение 4: Добавьте принудительное обновление при клике
```javascript // ДОБАВЬТЕ эту функцию для принудительного обновления function refreshActiveTab() { const activeTab = document.querySelector('.tab.active'); if (activeTab) { const tabName = activeTab.getAttribute('data-tab'); console.log('Принудительное обновление вкладки:', tabName); initializeTabSpecificContent(tabName); } }
// Можно вызвать эту функцию если данные не отображаются // Например, добавить кнопку "Обновить" или вызывать при определенных условиях ```
Основные изменения:
1. Гарантированная активация - принудительно активируем первую вкладку при загрузке 2. Отдельные обработчики - убираем конфликты между обработчиками 3. Конкретная инициализация - для каждой вкладки свой набор инициализаций 4. Улучшенная диагностика - больше console.log для отладки 5. Удаление старых обработчиков - предотвращает накопление дублирующихся обработчиков
Теперь при переключении вкладок данные должны отображаться сразу, без необходимости переключать внутренние вкладки.
|
**ПОЛНОЕ РЕШЕНИЕ ОПТИМИЗАЦИИ КОДА**
## 1. УДАЛЯЕМ ДУБЛИРУЮЩИЕСЯ ФУНКЦИИ
### Удалить эти функции полностью:
```javascript // НАЙТИ И УДАЛИТЬ следующие функции:
// 1. Дублирует formatLongText function safeTextDisplay(text, maxLength = 50) { if (!text) return 'не указано'; try { const decodedText = decodeURIComponent(escape(text)); if (decodedText.length > maxLength) { return decodedText.substring(0, maxLength) + '...'; } return decodedText; } catch (error) { console.warn('Не удалось декодировать текст:', text); return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; } }
// 2. Не используется function analyzeDocumentLinks(xmlContent, wellId) { const documentLinks = new Map(); // ... весь код функции }
// 3. Дублирует логику из formatWellDisplayName function findWellInfoById(wellId, xmlContent) { // ... весь код функции }
// 4. Избыточная функция function getDocumentTooltip(doc) { const parts = []; // ... весь код функции } ```
### ЗАМЕНИТЬ ВЫЗОВЫ: Во всем коде заменить `safeTextDisplay(text)` на `formatLongText(text)`
## 2. ОБЪЕДИНЯЕМ ФУНКЦИИ ОТОБРАЖЕНИЯ ТАБЛИЦ
### Удаляем 9 отдельных функций и заменяем одной:
**УДАЛИТЬ:** ```javascript function displayDrillingDiameterForHeader(diameterData) { ... } function displayDrillingIntervalsForHeader(intervalsData) { ... } function displayDrillingCasingForHeader(casingData) { ... } function displayDrillingMethodForHeader(methodData) { ... } function displayDrillingTypeForHeader(typeData) { ... } function displayDrillingSludgeForHeader(sludgeData) { ... } function displayDrillingConditionsForHeader(conditionsData) { ... } function displayDrillingStartForHeader(startData) { ... } function displayDrillingEndForHeader(endData) { ... } ```
**ДОБАВИТЬ ВМЕСТО НИХ:**
```javascript // УНИВЕРСАЛЬНАЯ ФУНКЦИЯ ДЛЯ ВСЕХ ТАБЛИЦ ПРОХОДКИ function displayDrillingTableForHeader(data, config) { if (!data || data.length === 0) { return '<div class="no-data"><p>Нет данных</p></div>'; }
const { tableId, fileName, columns } = config; let html = createExportButton(tableId, fileName);
html += ` <div class="table-wrapper" id="${tableId}"> <div class="table-header"> <table class="interval-table"> <thead> <tr> ${columns.map(col => `<th style="width: ${col.width};">${col.title}</th>` ).join('')} </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `;
data.forEach(item => { html += '<tr>'; columns.forEach(col => { const value = col.formatter ? col.formatter(item[col.field]) : item[col.field]; html += `<td style="width: ${col.width};">${value}</td>`; }); html += '</tr>'; });
html += ` </tbody> </table> </div> </div> `;
return html; }
// КОНФИГУРАЦИЯ ДЛЯ КАЖДОГО ТИПА ТАБЛИЦ const drillingTableConfigs = { diameter: { tableId: 'drillingDiameterTable', fileName: 'Диаметр_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'bitType', title: 'Тип коронки', width: '150px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, intervals: { tableId: 'drillingIntervalsTable', fileName: 'Интервалы_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'coreOutput', title: 'Выход керна, %', width: '100px', formatter: formatNumber }, { field: 'coreDiameter', title: 'Диаметр керна, мм', width: '100px', formatter: formatNumber }, { field: 'coreLength', title: 'Длина керна, м', width: '100px', formatter: formatNumber }, { field: 'description', title: 'Описание', width: 'auto' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] } // Добавить конфиги для остальных типов... }; ```
### ИСПРАВИТЬ ВЫЗОВЫ В `displayDrillingData()`:
**БЫЛО:** ```javascript if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingDiameterForHeader(headerData.diameter); html += `</div>`; } ```
**СТАЛО:** ```javascript if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter); html += `</div>`; } ```
## 3. УПРОЩАЕМ ОБРАБОТЧИКИ СОБЫТИЙ
### Удаляем множественные addEventListener:
**УДАЛИТЬ из `displayWellData()`:** ```javascript // УДАЛИТЬ весь этот блок: document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', function() { const tabName = this.getAttribute('data-tab'); switchMainTab(tabName); }); });
document.querySelectorAll('.inner-tab').forEach(innerTab => { innerTab.addEventListener('click', function() { const innerTabName = this.getAttribute('data-inner-tab'); switchInnerTab(innerTabName); }); });
document.querySelectorAll('.drilling-header-row').forEach(row => { row.addEventListener('click', function() { const headerId = this.getAttribute('data-header-id'); selectDrillingHeader(headerId); }); }); ```
**ДОБАВИТЬ В НАЧАЛО ФАЙЛА (после глобальных переменных):**
```javascript // УНИВЕРСАЛЬНЫЙ ОБРАБОТЧИК СОБЫТИЙ document.addEventListener('click', function(e) { // Обработка основных вкладок if (e.target.classList.contains('tab')) { switchMainTab(e.target.getAttribute('data-tab')); return; } // Обработка внутренних вкладок if (e.target.classList.contains('inner-tab')) { switchInnerTab(e.target.getAttribute('data-inner-tab')); return; } // Обработка выбора заголовка проходки if (e.target.classList.contains('drilling-header-row')) { selectDrillingHeader(e.target.getAttribute('data-header-id')); return; } // Обработка кнопок документов if (e.target.classList.contains('preview-doc-btn') || e.target.classList.contains('corebox-preview-btn')) { const docId = e.target.getAttribute('data-doc-id'); const fileName = e.target.getAttribute('data-file-name'); const fileType = e.target.getAttribute('data-file-type'); const isImage = e.target.getAttribute('data-is-image'); const blobPath = e.target.getAttribute('data-blob-path'); previewDocument(docId, fileName, fileType, isImage, blobPath); return; } if (e.target.classList.contains('download-doc-btn') || e.target.classList.contains('corebox-download-btn') || e.target.classList.contains('download-from-modal')) { const docId = e.target.getAttribute('data-doc-id'); const fileName = e.target.getAttribute('data-file-name'); const blobPath = e.target.getAttribute('data-blob-path'); downloadDocument(docId, fileName, blobPath); return; } // Обработка кнопок экспорта if (e.target.classList.contains('export-excel-btn')) { const tableId = e.target.getAttribute('data-table-id'); const fileName = e.target.getAttribute('data-file-name'); const tableWrapper = document.getElementById(tableId)?.closest('.table-wrapper'); if (tableWrapper) { exportTableToExcelSimple(tableWrapper, fileName); } return; } }); ```
**УДАЛИТЬ ФУНКЦИИ:** ```javascript function initializeDocumentButtons() { // УДАЛИТЬ всю функцию - теперь обработка в универсальном обработчике }
// УДАЛИТЬ отдельные обработчики в previewDocument и downloadDocument ```
## 4. УБИРАЕМ ИЗБЫТОЧНОЕ ЛОГИРОВАНИЕ
### УДАЛИТЬ ВО ВСЕМ КОДЕ:
```javascript // УДАЛИТЬ такие console.log: console.log('=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ==='); console.log('Создана карта DOCUM_DCAA: ${documentInfoMap.size} записей'); console.log('=== РЕЗУЛЬТАТЫ ИЗВЛЕЧЕНИЯ ==='); console.log('=== PREVIEW DOCUMENT START ==='); console.log('=== PREVIEW DOCUMENT END ==='); console.log('=== СВЯЗЫВАНИЕ ДОКУМЕНТОВ С ЯЩИКАМИ ==='); console.log('=== СВОДКА СВЯЗЕЙ ===');
// УДАЛИТЬ подробные логи в циклах: console.log(`✓ Документ "${displayName}" принадлежит скважине: ${wellInfo.displayName}`); console.log(`✗ Документ "${displayName}" не принадлежит скважине ${wellId}`); console.log(`? Документ "${displayName}" не имеет связи с объектом`);
// ОСТАВИТЬ ТОЛЬКО: console.error() - для ошибок console.warn() - для предупреждений console.log() - только для ключевых этапов (начало/конец загрузки) ```
### ОПТИМИЗИРОВАННАЯ ВЕРСИЯ `extractAttachedDocumentsFast`:
**БЫЛО:** ```javascript console.log(`=== ПОИСК ДОКУМЕНТОВ ДЛЯ СКВАЖИНЫ ${wellId} ===`); // много console.log внутри console.log(`=== РЕЗУЛЬТАТЫ ИЗВЛЕЧЕНИЯ ===`); ```
**СТАЛО:** ```javascript console.log(`Поиск документов для скважины ${wellId}`); // только критичные логи console.log(`Найдено документов: ${documents.length}`); ```
## 5. ОБЪЕДИНЯЕМ ФУНКЦИИ ФОРМАТИРОВАНИЯ
**УДАЛИТЬ:** ```javascript // Удалить отдельные функции форматирования если они простые ```
**ДОБАВИТЬ:** ```javascript // УНИВЕРСАЛЬНЫЙ ФОРМАТТЕР function formatValue(value, type = 'text') { if (!value || value === 'Не указано') return 'Не указано'; switch(type) { case 'number': return formatNumber(value); case 'date': return formatDate(value); case 'text': return formatLongText(value); case 'wellName': return formatWellDisplayName(value); default: return value; } } ```
## 6. УПРОЩАЕМ СИСТЕМУ КЕШИРОВАНИЯ
**ЗАМЕНИТЬ:** ```javascript // БЫЛО: let fileCache = new Map(); let wellDataCache = new Map();
// СТАЛО: const dataCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 минут
function getCachedData(key) { const item = dataCache.get(key); if (item && Date.now() - item.timestamp < CACHE_TTL) { return item.data; } dataCache.delete(key); return null; }
function setCachedData(key, data) { dataCache.set(key, { data: data, timestamp: Date.now() }); } ```
**ОБНОВИТЬ ВЫЗОВЫ:** ```javascript // БЫЛО: const cachedData = getCachedWellData(wellId); setCachedWellData(wellId, wellData);
// СТАЛО: const cachedData = getCachedData(`well_${wellId}`); setCachedData(`well_${wellId}`, wellData); ```
## 7. УПРОЩАЕМ ФУНКЦИИ РАБОТЫ С ZIP
**ОБЪЕДИНИТЬ:** ```javascript // УДАЛИТЬ: function getZipFileNames(zip) { ... } function safeFileName(filename) { ... }
// ДОБАВИТЬ ВМЕСТО НИХ: function getZipFiles(zip) { const files = []; for (const filename in zip.files) { const zipEntry = zip.files[filename]; if (zipEntry.dir || filename.endsWith('/')) continue; // Упрощенное получение имени файла let correctedName = filename; try { correctedName = decodeURIComponent(escape(filename)); } catch (error) { // оставляем оригинальное имя } files.push({ originalName: filename, correctedName: correctedName, entry: zipEntry }); } return files; } ```
## 8. ФИНАЛЬНАЯ ПРОВЕРКА - УДАЛИТЬ НЕИСПОЛЬЗУЕМЫЕ ПЕРЕМЕННЫЕ
**УДАЛИТЬ ИЗ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ:** ```javascript // УДАЛИТЬ: let keepAliveInterval = null; let lastActivityTime = Date.now(); let currentLoadingOperation = null; let lastSelectedFileId = null; let lastSelectedFileName = null;
// УДАЛИТЬ функции: function startKeepAlive() { ... } function stopKeepAlive() { ... } function startLongOperationProgress() { ... } function cancelCurrentOperation() { ... } ```
## РЕЗУЛЬТАТ:
После этих изменений код станет: - **На 25-30% короче** (удалено ~500-700 строк) - **Более читаемым** (унифицированные функции) - **Легче в поддержке** (меньше дублирования) - **Такой же функциональности** (вся логика сохранена)
Хотите, чтобы я показал полный код оптимизированных функций?Добавлено (2025-10-08, 11:21) --------------------------------------------- **ПОЛНАЯ ФУНКЦИЯ `displayDrillingTableForHeader`:**
```javascript // УНИВЕРСАЛЬНАЯ ФУНКЦИЯ ДЛЯ ВСЕХ ТАБЛИЦ ПРОХОДКИ function displayDrillingTableForHeader(data, config) { if (!data || data.length === 0) { return '<div class="no-data"><p>Нет данных</p></div>'; }
const { tableId, fileName, columns, extraHtml = '' } = config; let html = createExportButton(tableId, fileName);
html += ` <div class="table-wrapper" id="${tableId}"> <div class="table-header"> <table class="interval-table"> <thead> <tr> ${columns.map(col => `<th style="width: ${col.width};">${col.title}</th>` ).join('')} </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `;
data.forEach(item => { html += '<tr>'; columns.forEach(col => { let value = item[col.field]; // Применяем форматтер если есть if (col.formatter) { value = col.formatter(value, item); } else if (typeof value === 'number') { value = formatNumber(value); } html += `<td style="width: ${col.width};" title="${value}">${value || ''}</td>`; }); html += '</tr>'; });
html += ` </tbody> </table> </div> </div> ${extraHtml} `;
return html; }
// КОНФИГУРАЦИЯ ДЛЯ ВСЕХ ТИПОВ ТАБЛИЦ ПРОХОДКИ const drillingTableConfigs = { diameter: { tableId: 'drillingDiameterTable', fileName: 'Диаметр_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'bitType', title: 'Тип коронки', width: '150px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, intervals: { tableId: 'drillingIntervalsTable', fileName: 'Интервалы_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'coreOutput', title: 'Выход керна, %', width: '100px', formatter: formatNumber }, { field: 'coreDiameter', title: 'Диаметр керна, мм', width: '100px', formatter: formatNumber }, { field: 'coreLength', title: 'Длина керна, м', width: '100px', formatter: formatNumber }, { field: 'description', title: 'Описание', width: '200px', formatter: (val) => `<div class="description-text">${val}</div>` }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, casing: { tableId: 'drillingCasingTable', fileName: 'Обсадка', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'pipeType', title: 'Тип трубы', width: '120px' }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'extraction', title: 'Извлечение', width: '100px', formatter: (val) => val === 'Д' ? 'Да' : val === 'Н' ? 'Нет' : val }, { field: 'wallThickness', title: 'Толщина стенки, мм', width: '100px', formatter: formatNumber }, { field: 'filterType', title: 'Тип фильтра', width: '120px' }, { field: 'perforation', title: 'Способ перфорации', width: '120px' }, { field: 'wellness', title: 'Скважность, %', width: '100px', formatter: formatNumber }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, method: { tableId: 'drillingMethodTable', fileName: 'Способ_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'method', title: 'Способ бурения', width: '200px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, drillingType: { tableId: 'drillingTypeTable', fileName: 'Вид_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'drillingType', title: 'Вид бурения', width: '150px' }, { field: 'rigType', title: 'Тип бурового станка', width: '150px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, sludge: { tableId: 'drillingSludgeTable', fileName: 'Способ_выноски_шлама', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'sludgeMethod', title: 'Способ выноски шлама', width: '200px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, conditions: { tableId: 'drillingConditionsTable', fileName: 'Условия_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'conditions', title: 'Условия бурения', width: '200px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, start: { tableId: 'drillingStartTable', fileName: 'Забурка', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, end: { tableId: 'drillingEndTable', fileName: 'Разбурка', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] } };
// СПЕЦИАЛЬНАЯ ФУНКЦИЯ ДЛЯ ОБСАДКИ С ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ function displayDrillingCasingWithHoles(casingData) { const config = drillingTableConfigs.casing; const casingWithHoles = casingData.filter(c => c.holeLength > 0 || c.holeWidth > 0); let extraHtml = ''; if (casingWithHoles.length > 0) { extraHtml = ` <div class="info-group" style="margin-top: 20px;"> <div class="info-group-title">Размеры отверстий фильтров</div> <div class="table-wrapper"> <div class="table-header"> <table class="interval-table"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 150px;">Интервал</th> <th style="width: 150px;">Длина отверстия, мм</th> <th style="width: 150px;">Ширина отверстия, мм</th> <th style="width: 150px;">Оборудование низа колонны</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `; casingWithHoles.forEach(casing => { extraHtml += ` <tr> <td style="width: 50px;">${casing.number}</td> <td style="width: 150px;">${formatNumber(casing.from)} - ${formatNumber(casing.to)} м</td> <td style="width: 150px;">${formatNumber(casing.holeLength)}</td> <td style="width: 150px;">${formatNumber(casing.holeWidth)}</td> <td style="width: 150px;">${casing.bottomEquipment}</td> </tr> `; }); extraHtml += ` </tbody> </table> </div> </div> </div> `; } return displayDrillingTableForHeader(casingData, { ...config, extraHtml: extraHtml }); } ```
**ТЕПЕРЬ В `displayDrillingData()` ЗАМЕНЯЕМ ВСЕ ВЫЗОВЫ:**
```javascript // В функции displayDrillingData() НАЙТИ И ЗАМЕНИТЬ:
// БЫЛО: if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingDiameterForHeader(headerData.diameter); html += `</div>`; }
// СТАЛО: if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter); html += `</div>`; }
// БЫЛО: if (hasIntervals) { html += `<div id="intervalsInnerTab" class="inner-tab-content">`; html += displayDrillingIntervalsForHeader(headerData.intervals); html += `</div>`; }
// СТАЛО: if (hasIntervals) { html += `<div id="intervalsInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.intervals, drillingTableConfigs.intervals); html += `</div>`; }
// БЫЛО: if (hasCasing) { html += `<div id="casingInnerTab" class="inner-tab-content">`; html += displayDrillingCasingForHeader(headerData.casing); html += `</div>`; }
// СТАЛО: if (hasCasing) { html += `<div id="casingInnerTab" class="inner-tab-content">`; html += displayDrillingCasingWithHoles(headerData.casing); html += `</div>`; }
// БЫЛО: if (hasMethod) { html += `<div id="methodInnerTab" class="inner-tab-content">`; html += displayDrillingMethodForHeader(headerData.method); html += `</div>`; }
// СТАЛО: if (hasMethod) { html += `<div id="methodInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.method, drillingTableConfigs.method); html += `</div>`; }
// БЫЛО: if (hasDrillingType) { html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`; html += displayDrillingTypeForHeader(headerData.drillingType); html += `</div>`; }
// СТАЛО: if (hasDrillingType) { html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.drillingType, drillingTableConfigs.drillingType); html += `</div>`; }
// БЫЛО: if (hasSludge) { html += `<div id="sludgeInnerTab" class="inner-tab-content">`; html += displayDrillingSludgeForHeader(headerData.sludge); html += `</div>`; }
// СТАЛО: if (hasSludge) { html += `<div id="sludgeInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.sludge, drillingTableConfigs.sludge); html += `</div>`; }
// БЫЛО: if (hasConditions) { html += `<div id="conditionsInnerTab" class="inner-tab-content">`; html += displayDrillingConditionsForHeader(headerData.conditions); html += `</div>`; }
// СТАЛО: if (hasConditions) { html += `<div id="conditionsInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.conditions, drillingTableConfigs.conditions); html += `</div>`; }
// БЫЛО: if (hasStart) { html += `<div id="startInnerTab" class="inner-tab-content">`; html += displayDrillingStartForHeader(headerData.start); html += `</div>`; }
// СТАЛО: if (hasStart) { html += `<div id="startInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.start, drillingTableConfigs.start); html += `</div>`; }
// БЫЛО: if (hasEnd) { html += `<div id="endInnerTab" class="inner-tab-content">`; html += displayDrillingEndForHeader(headerData.end); html += `</div>`; }
// СТАЛО: if (hasEnd) { html += `<div id="endInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.end, drillingTableConfigs.end); html += `</div>`; } ```
**УДАЛИТЬ СТАРЫЕ ФУНКЦИИ ПОЛНОСТЬЮ:** - `displayDrillingDiameterForHeader` - `displayDrillingIntervalsForHeader` - `displayDrillingCasingForHeader` - `displayDrillingMethodForHeader` - `displayDrillingTypeForHeader` - `displayDrillingSludgeForHeader` - `displayDrillingConditionsForHeader` - `displayDrillingStartForHeader` - `displayDrillingEndForHeader`
Эта одна универсальная функция заменяет 9 отдельных функций и содержит всю ту же логику отображения, но в компактном и поддерживаемом виде. Добавлено (2025-10-08, 11:21) --------------------------------------------- **ПОЛНАЯ ФУНКЦИЯ `displayDrillingTableForHeader`:**
```javascript // УНИВЕРСАЛЬНАЯ ФУНКЦИЯ ДЛЯ ВСЕХ ТАБЛИЦ ПРОХОДКИ function displayDrillingTableForHeader(data, config) { if (!data || data.length === 0) { return '<div class="no-data"><p>Нет данных</p></div>'; }
const { tableId, fileName, columns, extraHtml = '' } = config; let html = createExportButton(tableId, fileName);
html += ` <div class="table-wrapper" id="${tableId}"> <div class="table-header"> <table class="interval-table"> <thead> <tr> ${columns.map(col => `<th style="width: ${col.width};">${col.title}</th>` ).join('')} </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `;
data.forEach(item => { html += '<tr>'; columns.forEach(col => { let value = item[col.field]; // Применяем форматтер если есть if (col.formatter) { value = col.formatter(value, item); } else if (typeof value === 'number') { value = formatNumber(value); } html += `<td style="width: ${col.width};" title="${value}">${value || ''}</td>`; }); html += '</tr>'; });
html += ` </tbody> </table> </div> </div> ${extraHtml} `;
return html; }
// КОНФИГУРАЦИЯ ДЛЯ ВСЕХ ТИПОВ ТАБЛИЦ ПРОХОДКИ const drillingTableConfigs = { diameter: { tableId: 'drillingDiameterTable', fileName: 'Диаметр_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'bitType', title: 'Тип коронки', width: '150px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, intervals: { tableId: 'drillingIntervalsTable', fileName: 'Интервалы_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'coreOutput', title: 'Выход керна, %', width: '100px', formatter: formatNumber }, { field: 'coreDiameter', title: 'Диаметр керна, мм', width: '100px', formatter: formatNumber }, { field: 'coreLength', title: 'Длина керна, м', width: '100px', formatter: formatNumber }, { field: 'description', title: 'Описание', width: '200px', formatter: (val) => `<div class="description-text">${val}</div>` }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, casing: { tableId: 'drillingCasingTable', fileName: 'Обсадка', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'pipeType', title: 'Тип трубы', width: '120px' }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'extraction', title: 'Извлечение', width: '100px', formatter: (val) => val === 'Д' ? 'Да' : val === 'Н' ? 'Нет' : val }, { field: 'wallThickness', title: 'Толщина стенки, мм', width: '100px', formatter: formatNumber }, { field: 'filterType', title: 'Тип фильтра', width: '120px' }, { field: 'perforation', title: 'Способ перфорации', width: '120px' }, { field: 'wellness', title: 'Скважность, %', width: '100px', formatter: formatNumber }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, method: { tableId: 'drillingMethodTable', fileName: 'Способ_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'method', title: 'Способ бурения', width: '200px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, drillingType: { tableId: 'drillingTypeTable', fileName: 'Вид_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'drillingType', title: 'Вид бурения', width: '150px' }, { field: 'rigType', title: 'Тип бурового станка', width: '150px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, sludge: { tableId: 'drillingSludgeTable', fileName: 'Способ_выноски_шлама', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'sludgeMethod', title: 'Способ выноски шлама', width: '200px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, conditions: { tableId: 'drillingConditionsTable', fileName: 'Условия_бурения', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'conditions', title: 'Условия бурения', width: '200px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, start: { tableId: 'drillingStartTable', fileName: 'Забурка', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] }, end: { tableId: 'drillingEndTable', fileName: 'Разбурка', columns: [ { field: 'number', title: 'ПП', width: '50px' }, { field: 'from', title: 'От (м)', width: '80px', formatter: formatNumber }, { field: 'to', title: 'До (м)', width: '80px', formatter: formatNumber }, { field: 'thickness', title: 'Мощность (м)', width: '100px', formatter: formatNumber }, { field: 'diameter', title: 'Диаметр (мм)', width: '100px' }, { field: 'inputDate', title: 'Дата ввода', width: '100px' } ] } };
// СПЕЦИАЛЬНАЯ ФУНКЦИЯ ДЛЯ ОБСАДКИ С ДОПОЛНИТЕЛЬНОЙ ИНФОРМАЦИЕЙ function displayDrillingCasingWithHoles(casingData) { const config = drillingTableConfigs.casing; const casingWithHoles = casingData.filter(c => c.holeLength > 0 || c.holeWidth > 0); let extraHtml = ''; if (casingWithHoles.length > 0) { extraHtml = ` <div class="info-group" style="margin-top: 20px;"> <div class="info-group-title">Размеры отверстий фильтров</div> <div class="table-wrapper"> <div class="table-header"> <table class="interval-table"> <thead> <tr> <th style="width: 50px;">ПП</th> <th style="width: 150px;">Интервал</th> <th style="width: 150px;">Длина отверстия, мм</th> <th style="width: 150px;">Ширина отверстия, мм</th> <th style="width: 150px;">Оборудование низа колонны</th> </tr> </thead> </table> </div> <div class="table-container"> <table class="interval-table"> <tbody> `; casingWithHoles.forEach(casing => { extraHtml += ` <tr> <td style="width: 50px;">${casing.number}</td> <td style="width: 150px;">${formatNumber(casing.from)} - ${formatNumber(casing.to)} м</td> <td style="width: 150px;">${formatNumber(casing.holeLength)}</td> <td style="width: 150px;">${formatNumber(casing.holeWidth)}</td> <td style="width: 150px;">${casing.bottomEquipment}</td> </tr> `; }); extraHtml += ` </tbody> </table> </div> </div> </div> `; } return displayDrillingTableForHeader(casingData, { ...config, extraHtml: extraHtml }); } ```
**ТЕПЕРЬ В `displayDrillingData()` ЗАМЕНЯЕМ ВСЕ ВЫЗОВЫ:**
```javascript // В функции displayDrillingData() НАЙТИ И ЗАМЕНИТЬ:
// БЫЛО: if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingDiameterForHeader(headerData.diameter); html += `</div>`; }
// СТАЛО: if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter); html += `</div>`; }
// БЫЛО: if (hasIntervals) { html += `<div id="intervalsInnerTab" class="inner-tab-content">`; html += displayDrillingIntervalsForHeader(headerData.intervals); html += `</div>`; }
// СТАЛО: if (hasIntervals) { html += `<div id="intervalsInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.intervals, drillingTableConfigs.intervals); html += `</div>`; }
// БЫЛО: if (hasCasing) { html += `<div id="casingInnerTab" class="inner-tab-content">`; html += displayDrillingCasingForHeader(headerData.casing); html += `</div>`; }
// СТАЛО: if (hasCasing) { html += `<div id="casingInnerTab" class="inner-tab-content">`; html += displayDrillingCasingWithHoles(headerData.casing); html += `</div>`; }
// БЫЛО: if (hasMethod) { html += `<div id="methodInnerTab" class="inner-tab-content">`; html += displayDrillingMethodForHeader(headerData.method); html += `</div>`; }
// СТАЛО: if (hasMethod) { html += `<div id="methodInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.method, drillingTableConfigs.method); html += `</div>`; }
// БЫЛО: if (hasDrillingType) { html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`; html += displayDrillingTypeForHeader(headerData.drillingType); html += `</div>`; }
// СТАЛО: if (hasDrillingType) { html += `<div id="drillingTypeInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.drillingType, drillingTableConfigs.drillingType); html += `</div>`; }
// БЫЛО: if (hasSludge) { html += `<div id="sludgeInnerTab" class="inner-tab-content">`; html += displayDrillingSludgeForHeader(headerData.sludge); html += `</div>`; }
// СТАЛО: if (hasSludge) { html += `<div id="sludgeInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.sludge, drillingTableConfigs.sludge); html += `</div>`; }
// БЫЛО: if (hasConditions) { html += `<div id="conditionsInnerTab" class="inner-tab-content">`; html += displayDrillingConditionsForHeader(headerData.conditions); html += `</div>`; }
// СТАЛО: if (hasConditions) { html += `<div id="conditionsInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.conditions, drillingTableConfigs.conditions); html += `</div>`; }
// БЫЛО: if (hasStart) { html += `<div id="startInnerTab" class="inner-tab-content">`; html += displayDrillingStartForHeader(headerData.start); html += `</div>`; }
// СТАЛО: if (hasStart) { html += `<div id="startInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.start, drillingTableConfigs.start); html += `</div>`; }
// БЫЛО: if (hasEnd) { html += `<div id="endInnerTab" class="inner-tab-content">`; html += displayDrillingEndForHeader(headerData.end); html += `</div>`; }
// СТАЛО: if (hasEnd) { html += `<div id="endInnerTab" class="inner-tab-content">`; html += displayDrillingTableForHeader(headerData.end, drillingTableConfigs.end); html += `</div>`; } ```
**УДАЛИТЬ СТАРЫЕ ФУНКЦИИ ПОЛНОСТЬЮ:** - `displayDrillingDiameterForHeader` - `displayDrillingIntervalsForHeader` - `displayDrillingCasingForHeader` - `displayDrillingMethodForHeader` - `displayDrillingTypeForHeader` - `displayDrillingSludgeForHeader` - `displayDrillingConditionsForHeader` - `displayDrillingStartForHeader` - `displayDrillingEndForHeader`
Эта одна универсальная функция заменяет 9 отдельных функций и содержит всю ту же логику отображения, но в компактном и поддерживаемом виде.
|
|
|
Понимаю проблему. При переключении между основными вкладками сбрасывается состояние внутренних вкладок (например, в разделе "Проходка"). Нужно сохранять состояние активных внутренних вкладок при смене основных вкладок.
Вот решение:
```javascript // Добавляем в глобальные переменные let currentActiveTabs = { drilling: 'header', // активная вкладка в разделе проходки // можно добавить другие разделы при необходимости };
// Функция для сохранения состояния вкладки function saveActiveTab(section, tabId) { currentActiveTabs[section] = tabId; }
// Функция для восстановления состояния вкладки function restoreActiveTab(section) { return currentActiveTabs[section] || 'header'; // значение по умолчанию }
// Модифицируем функцию switchInnerTab function switchInnerTab(tabId) { // Сохраняем активную вкладку saveActiveTab('drilling', tabId); // Существующий код переключения вкладок const innerTabs = document.querySelectorAll('.inner-tab'); const innerTabContents = document.querySelectorAll('.inner-tab-content'); innerTabs.forEach(tab => tab.classList.remove('active')); innerTabContents.forEach(content => content.classList.remove('active')); const activeTab = document.querySelector(`.inner-tab[data-inner-tab="${tabId}"]`); const activeContent = document.getElementById(`${tabId}InnerTab`); if (activeTab) activeTab.classList.add('active'); if (activeContent) activeContent.classList.add('active'); }
// Модифицируем функцию displayDrillingData для восстановления состояния function displayDrillingData() { const hasHeaders = xmlData.drillingHeaders && xmlData.drillingHeaders.length > 0; const hasDrillingData = hasHeaders && xmlData.drillingDataByHeader && Object.keys(xmlData.drillingDataByHeader).length > 0;
if (!hasDrillingData) { return '<div class="no-data"><p>Нет данных проходки</p></div>'; }
let html = '<div class="section">'; html += '<h2>Данные проходки</h2>';
if (!currentDrillingHeaderId && xmlData.drillingHeaders.length > 0) { currentDrillingHeaderId = xmlData.drillingHeaders[0].id; }
// ... существующий код заголовков ...
if (currentDrillingHeaderId && xmlData.drillingDataByHeader[currentDrillingHeaderId]) { const selectedHeader = xmlData.drillingHeaders.find(h => h.id === currentDrillingHeaderId); const headerData = xmlData.drillingDataByHeader[currentDrillingHeaderId];
html += ` <div class="info-group" style="margin-top: 20px;"> <div class="info-group-title">Данные для параметра: ${selectedHeader.purpose}</div> </div> `;
const hasDiameter = headerData.diameter && headerData.diameter.length > 0; const hasIntervals = headerData.intervals && headerData.intervals.length > 0; const hasCasing = headerData.casing && headerData.casing.length > 0; const hasMethod = headerData.method && headerData.method.length > 0; const hasDrillingType = headerData.drillingType && headerData.drillingType.length > 0; const hasSludge = headerData.sludge && headerData.sludge.length > 0; const hasConditions = headerData.conditions && headerData.conditions.length > 0; const hasStart = headerData.start && headerData.start.length > 0; const hasEnd = headerData.end && headerData.end.length > 0;
// ВОССТАНАВЛИВАЕМ активную вкладку вместо использования фиксированной let firstActiveSubTab = restoreActiveTab('drilling');
// Проверяем, что восстановленная вкладка существует const availableTabs = []; if (hasDiameter) availableTabs.push('diameter'); if (hasIntervals) availableTabs.push('intervals'); if (hasCasing) availableTabs.push('casing'); if (hasMethod) availableTabs.push('method'); if (hasDrillingType) availableTabs.push('drillingType'); if (hasSludge) availableTabs.push('sludge'); if (hasConditions) availableTabs.push('conditions'); if (hasStart) availableTabs.push('start'); if (hasEnd) availableTabs.push('end'); // Если восстановленная вкладка недоступна, используем первую доступную if (!availableTabs.includes(firstActiveSubTab) && availableTabs.length > 0) { firstActiveSubTab = availableTabs[0]; }
if (hasDiameter || hasIntervals || hasCasing || hasMethod || hasDrillingType || hasSludge || hasConditions || hasStart || hasEnd) {
html += `<div class="inner-tabs-container">`; html += `<div class="inner-tabs">`;
html += `<div class="inner-tab ${firstActiveSubTab === 'header' ? 'active' : ''}" data-inner-tab="header">Заголовок</div>`;
if (hasDiameter) { html += `<div class="inner-tab ${firstActiveSubTab === 'diameter' ? 'active' : ''}" data-inner-tab="diameter">Диаметр бурения</div>`; } if (hasIntervals) { html += `<div class="inner-tab ${firstActiveSubTab === 'intervals' ? 'active' : ''}" data-inner-tab="intervals">Интервалы бурения</div>`; } if (hasCasing) { html += `<div class="inner-tab ${firstActiveSubTab === 'casing' ? 'active' : ''}" data-inner-tab="casing">Обсадка</div>`; } if (hasMethod) { html += `<div class="inner-tab ${firstActiveSubTab === 'method' ? 'active' : ''}" data-inner-tab="method">Способ бурения</div>`; } if (hasDrillingType) { html += `<div class="inner-tab ${firstActiveSubTab === 'drillingType' ? 'active' : ''}" data-inner-tab="drillingType">Вид бурения</div>`; } if (hasSludge) { html += `<div class="inner-tab ${firstActiveSubTab === 'sludge' ? 'active' : ''}" data-inner-tab="sludge">Способ выноски шлама</div>`; } if (hasConditions) { html += `<div class="inner-tab ${firstActiveSubTab === 'conditions' ? 'active' : ''}" data-inner-tab="conditions">Условия бурения</div>`; } if (hasStart) { html += `<div class="inner-tab ${firstActiveSubTab === 'start' ? 'active' : ''}" data-inner-tab="start">Забурка</div>`; } if (hasEnd) { html += `<div class="inner-tab ${firstActiveSubTab === 'end' ? 'active' : ''}" data-inner-tab="end">Разбурка</div>`; }
html += `</div>`; html += `<div class="inner-tabs-content">`;
html += `<div id="headerInnerTab" class="inner-tab-content ${firstActiveSubTab === 'header' ? 'active' : ''}">`; html += displayDrillingHeaderForHeader(selectedHeader); html += `</div>`;
if (hasDiameter) { html += `<div id="diameterInnerTab" class="inner-tab-content ${firstActiveSubTab === 'diameter' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.diameter, drillingTableConfigs.diameter); html += `</div>`; } if (hasIntervals) { html += `<div id="intervalsInnerTab" class="inner-tab-content ${firstActiveSubTab === 'intervals' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.intervals, drillingTableConfigs.intervals); html += `</div>`; } if (hasCasing) { html += `<div id="casingInnerTab" class="inner-tab-content ${firstActiveSubTab === 'casing' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.casing, drillingTableConfigs.casing); html += `</div>`; } if (hasMethod) { html += `<div id="methodInnerTab" class="inner-tab-content ${firstActiveSubTab === 'method' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.method, drillingTableConfigs.method); html += `</div>`; } if (hasDrillingType) { html += `<div id="drillingTypeInnerTab" class="inner-tab-content ${firstActiveSubTab === 'drillingType' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.drillingType, drillingTableConfigs.drillingType); html += `</div>`; } if (hasSludge) { html += `<div id="sludgeInnerTab" class="inner-tab-content ${firstActiveSubTab === 'sludge' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.sludge, drillingTableConfigs.sludge); html += `</div>`; } if (hasConditions) { html += `<div id="conditionsInnerTab" class="inner-tab-content ${firstActiveSubTab === 'conditions' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.conditions, drillingTableConfigs.conditions); html += `</div>`; } if (hasStart) { html += `<div id="startInnerTab" class="inner-tab-content ${firstActiveSubTab === 'start' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.start, drillingTableConfigs.start); html += `</div>`; } if (hasEnd) { html += `<div id="endInnerTab" class="inner-tab-content ${firstActiveSubTab === 'end' ? 'active' : ''}">`; html += displayDrillingTableForHeader(headerData.end, drillingTableConfigs.end); html += `</div>`; }
html += `</div>`; html += `</div>`; } else { html += displayDrillingHeaderForHeader(selectedHeader); } } else { html += '<div class="no-data"><p>Нет данных для выбранного параметра проходки</p></div>'; }
html += '</div>'; return html; } ```
Также нужно обновить обработчик клика для внутренних вкладок в универсальном обработчике событий:
```javascript // В универсальном обработчике событий обновляем обработку внутренних вкладок document.addEventListener('click', function(e) { // ... существующий код ... // Обработка внутренних вкладок if (e.target.classList.contains('inner-tab')) { const tabId = e.target.getAttribute('data-inner-tab'); switchInnerTab(tabId); return; } // ... остальной код ... }); ```
**Что это решает:** 1. Сохраняет состояние активной внутренней вкладки при переключении между основными вкладками 2. Восстанавливает последнюю активную вкладку при возврате к разделу "Проходка" 3. Обеспечивает плавный пользовательский опыт без потери контекста
Теперь при переключении между вкладками "Первичное документирование", "Итоговое документирование", "Проходка" и другими, состояние внутренних вкладок в разделе "Проходка" будет сохраняться.
|