Отзывы и предложения к софту от AleXStam
  • Страница 5 из 5
  • «
  • 1
  • 2
  • 3
  • 4
  • 5
Поговорим о...
litology/:2630 Uncaught ReferenceError: checkDataVariable is not defined
at litology/:2630:1
---
title: "Литология"
date: 2024-01-01
---

Ваш обычный текст...

<!-- Начало компонента литологии -->
<div id="geo-lithology-app" class="geo-lithology-container">

<style>
.geo-lithology-container {
width: 100%;
max-width: 1400px;
margin: 2rem auto;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
overflow: hidden;
border: 1px solid #e2e8f0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.geo-lithology-header {
background: #fff;
padding: 24px 32px;
border-bottom: 1px solid #e2e8f0;
}

.geo-lithology-header h2 {
font-size: 1.5rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 8px;
}

.geo-lithology-header p {
color: #64748b;
font-size: 0.875rem;
}

.geo-lithology-controls {
padding: 20px 32px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: center;
}

.geo-lithology-search-container {
position: relative;
flex: 1;
min-width: 300px;
}

.geo-lithology-search-input {
width: 100%;
padding: 12px 16px 12px 44px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
background: white;
color: #1e293b;
transition: all 0.2s;
}

.geo-lithology-search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.1);
}

.geo-lithology-search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 14px;
}

.geo-lithology-action-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}

.geo-lithology-btn {
padding: 10px 20px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: white;
color: #475569;
}

.geo-lithology-btn:hover {
background: #f1f5f9;
}

.geo-lithology-tree-container {
padding: 0;
min-height: 400px;
max-height: 500px;
overflow-y: auto;
background: white;
}

.geo-lithology-tree-list {
list-style: none;
padding: 0;
margin: 0;
}

.geo-lithology-tree-node {
position: relative;
}

.geo-lithology-node-content {
display: flex;
align-items: center;
padding: 12px 32px 12px 20px;
border-bottom: 1px solid #f1f5f9;
cursor: pointer;
transition: background 0.2s;
}

.geo-lithology-node-content:hover {
background: #f8fafc;
}

.geo-lithology-node-content.level-0 { padding-left: 20px; }
.geo-lithology-node-content.level-1 { padding-left: 40px; }
.geo-lithology-node-content.level-2 { padding-left: 60px; }
.geo-lithology-node-content.level-3 { padding-left: 80px; }
.geo-lithology-node-content.level-4 { padding-left: 100px; }

.geo-lithology-toggle-btn {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid #cbd5e1;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
font-weight: 600;
color: #475569;
cursor: pointer;
}

.geo-lithology-node-name {
font-size: 14px;
color: #1e293b;
flex: 1;
}

.geo-lithology-node-level {
font-size: 11px;
font-weight: 500;
color: #64748b;
background: #f1f5f9;
padding: 4px 10px;
border-radius: 12px;
text-transform: uppercase;
margin-left: 12px;
}

.geo-lithology-node-children {
display: none;
}

.geo-lithology-node-children.visible {
display: block;
}

.geo-lithology-empty-state,
.geo-lithology-no-results {
text-align: center;
padding: 60px 32px;
color: #64748b;
}

.geo-lithology-no-results {
display: none;
}

.geo-lithology-stats-bar {
padding: 16px 32px;
background: white;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #64748b;
}

.geo-lithology-loading {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.95);
z-index: 100;
justify-content: center;
align-items: center;
flex-direction: column;
}

.geo-lithology-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f1f5f9;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: geo-lithology-spin 1s linear infinite;
margin-bottom: 16px;
}

@keyframes geo-lithology-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.geo-lithology-search-highlight {
background-color: #fef3c7;
color: #92400e;
padding: 2px 4px;
border-radius: 4px;
font-weight: 500;
}

@media (max-width: 768px) {
.geo-lithology-controls {
flex-direction: column;
align-items: stretch;
}

.geo-lithology-search-container {
min-width: unset;
}

.geo-lithology-action-buttons {
justify-content: center;
}
}
</style>

<!-- HTML структура -->
<div class="geo-lithology-header">
<h2>Классификация литологии</h2>
<p>Иерархическое дерево классификации горных пород и отложений</p>
</div>

<div class="geo-lithology-controls">
<div class="geo-lithology-search-container">
<span class="geo-lithology-search-icon">🔍</span>
<input type="text" id="geo-lithology-search" class="geo-lithology-search-input"
placeholder="Поиск по названию...">
</div>

<div class="geo-lithology-action-buttons">
<button id="geo-lithology-expand-all" class="geo-lithology-btn">
<span>📂</span>
Развернуть всё
</button>
<button id="geo-lithology-collapse-all" class="geo-lithology-btn">
<span>📁</span>
Свернуть всё
</button>
</div>
</div>

<div class="geo-lithology-tree-container">
<div id="geo-lithology-loading" class="geo-lithology-loading">
<div class="geo-lithology-loading-spinner"></div>
<div>Загрузка данных...</div>
</div>

<div id="geo-lithology-empty" class="geo-lithology-empty-state" style="display: none;">
<div style="font-size: 48px; margin-bottom: 20px;">📊</div>
<h3>Загрузка данных...</h3>
</div>

<div id="geo-lithology-no-results" class="geo-lithology-no-results">
<div style="font-size: 48px; margin-bottom: 20px;">🔍</div>
<h3>Ничего не найдено</h3>
<p>Попробуйте изменить поисковый запрос</p>
</div>

<ul id="geo-lithology-tree" class="geo-lithology-tree-list" style="display: none;"></ul>
</div>

<div class="geo-lithology-stats-bar">
<div id="geo-lithology-stats">Загрузка...</div>
<div>
<span>Элементов: <span id="geo-lithology-count">0</span></span>
</div>
</div>

<script>
// Компонент литологии
(function() {
'use strict';

const LithologyTree = {
data: null,
elements: {},
searchResults: new Set(),
allNodes: new Map(),

init: function() {
this.cacheElements();
this.bindEvents();
this.showLoading();
this.loadData();
},

cacheElements: function() {
this.elements = {
tree: document.getElementById('geo-lithology-tree'),
search: document.getElementById('geo-lithology-search'),
expandBtn: document.getElementById('geo-lithology-expand-all'),
collapseBtn: document.getElementById('geo-lithology-collapse-all'),
empty: document.getElementById('geo-lithology-empty'),
noResults: document.getElementById('geo-lithology-no-results'),
stats: document.getElementById('geo-lithology-stats'),
count: document.getElementById('geo-lithology-count'),
loading: document.getElementById('geo-lithology-loading')
};
},

bindEvents: function() {
let searchTimeout;
this.elements.search.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.handleSearch(e.target.value);
}, 300);
});

this.elements.expandBtn.addEventListener('click', () => {
this.expandAll();
});

this.elements.collapseBtn.addEventListener('click', () => {
this.collapseAll();
});
},

showLoading: function() {
this.elements.loading.style.display = 'flex';
this.elements.empty.style.display = 'none';
},

hideLoading: function() {
this.elements.loading.style.display = 'none';
},

checkDataVariable: function() {
console.log('Проверяем глобальные переменные...');

// Проверяем разные возможные имена переменных
if (window.lithologyData && Array.isArray(window.lithologyData)) {
console.log('Данные найдены в window.lithologyData');
this.data = window.lithologyData;
this.render();
return true;
}

if (window.geoLithologyData && Array.isArray(window.geoLithologyData)) {
console.log('Данные найдены в window.geoLithologyData');
this.data = window.geoLithologyData;
this.render();
return true;
}

if (window.LithologyData && Array.isArray(window.LithologyData)) {
console.log('Данные найдены в window.LithologyData');
this.data = window.LithologyData;
this.render();
return true;
}

return false;
},

loadData: function() {
console.log('Начинаем загрузку данных...');

// Сначала проверяем, может данные уже загружены
if (this.checkDataVariable()) {
return;
}

// Список всех возможных путей к JS файлу
const possiblePaths = [
// Текущая директория (рядом с HTML)
'./LithologyData.js',
'LithologyData.js',

// Поддиректории (ниже текущей)
'./data/LithologyData.js',
'./js/LithologyData.js',
'./scripts/LithologyData.js',
'./lithology/LithologyData.js',

// Родительские директории
'../LithologyData.js',
'../../LithologyData.js',

// Абсолютные пути (от корня сайта)
'/LithologyData.js',
'/data/LithologyData.js',
'/js/LithologyData.js'
];

// Пытаемся загрузить данные из всех возможных мест
this.tryLoadFromPaths(possiblePaths, 0);
},

tryLoadFromPaths: function(paths, index) {
if (index >= paths.length) {
// Все пути перебрали, данные не найдены
console.error('Данные не найдены ни по одному из путей:', paths);
this.showDataError();
return;
}

const path = paths[index];
console.log(`Пытаемся загрузить данные из: ${path} (${index + 1}/${paths.length})`);

const script = document.createElement('script');
script.src = path;

const self = this; // Сохраняем контекст для использования внутри onload

script.onload = function() {
console.log(`Скрипт ${path} загружен успешно`);

// Даем время на выполнение скрипта
setTimeout(function() {
if (self.checkDataVariable()) {
console.log('Данные успешно загружены и обработаны');
} else {
console.log(`Данные в ${path} не найдены, пробуем следующий путь...`);
self.tryLoadFromPaths(paths, index + 1);
}
}, 100);
};

script.onerror = function() {
console.log(`Не удалось загрузить ${path}, пробуем следующий...`);
self.tryLoadFromPaths(paths, index + 1);
};

document.head.appendChild(script);
},

showDataError: function() {
this.hideLoading();
this.elements.empty.style.display = 'block';
this.elements.empty.innerHTML = `
<div style="font-size: 48px; color: #ef4444; margin-bottom: 20px;">⚠️</div>
<h3>Не удалось загрузить данные</h3>
<p>Файл LithologyData.js не найден. Проверьте возможные расположения:</p>
<ul style="text-align: left; max-width: 500px; margin: 20px auto; font-size: 12px;">
<li>Рядом с этой страницей</li>
<li>В папке data/ или js/ рядом с этой страницей</li>
<li>В корне сайта</li>
</ul>
<p><small>Текущий путь: ${window.location.pathname}</small></p>
`;
},

render: function() {
if (!this.data || !Array.isArray(this.data)) {
this.elements.empty.innerHTML = '<p>Нет данных для отображения</p>';
this.elements.empty.style.display = 'block';
this.hideLoading();
return;
}

this.hideLoading();
this.elements.empty.style.display = 'none';
this.elements.tree.style.display = 'block';
this.elements.tree.innerHTML = '';

// Строим карту всех узлов
this.buildNodeMap(this.data);

// Рендерим дерево
this.renderNodes(this.data, this.elements.tree, 0);
this.updateStats();

console.log(`Дерево отрисовано. Всего узлов: ${this.allNodes.size}`);
},

buildNodeMap: function(nodes, parentId = null) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
this.allNodes.set(nodeId, {
...node,
parentId: parentId
});

if (node.children && node.children.length > 0) {
this.buildNodeMap(node.children, nodeId);
}
});
},

renderNodes: function(nodes, parentElement, depth) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
const hasChildren = node.children && node.children.length > 0;

const li = document.createElement('li');
li.className = 'geo-lithology-tree-node';
li.dataset.id = nodeId;
li.dataset.depth = depth;

const content = document.createElement('div');
content.className = `geo-lithology-node-content level-${depth}`;

const toggleBtn = document.createElement('div');
toggleBtn.className = 'geo-lithology-toggle-btn';
toggleBtn.textContent = hasChildren ? '+' : '•';

if (hasChildren) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleNode(nodeId);
});
}

const nameSpan = document.createElement('div');
nameSpan.className = 'geo-lithology-node-name';
nameSpan.textContent = node.name || node.node_name;

const levelSpan = document.createElement('div');
levelSpan.className = 'geo-lithology-node-level';
levelSpan.textContent = `Уровень ${node.level || 1}`;

content.appendChild(toggleBtn);
content.appendChild(nameSpan);
content.appendChild(levelSpan);

if (hasChildren) {
content.addEventListener('click', () => {
this.toggleNode(nodeId);
});
}

li.appendChild(content);

if (hasChildren) {
const childrenList = document.createElement('ul');
childrenList.className = 'geo-lithology-node-children';
childrenList.id = `children-${nodeId}`;
li.appendChild(childrenList);

// Сохраняем ссылки
node._childrenList = childrenList;
node._depth = depth + 1;
}

parentElement.appendChild(li);
});
},

toggleNode: function(nodeId) {
const childrenList = document.getElementById(`children-${nodeId}`);
const nodeElement = document.querySelector(`[data-id="${nodeId}"]`);

if (!childrenList || !nodeElement) return;

const toggleBtn = nodeElement.querySelector('.geo-lithology-toggle-btn');

if (childrenList.classList.contains('visible')) {
childrenList.classList.remove('visible');
toggleBtn.textContent = '+';
} else {
// Ленивая загрузка детей
if (childrenList.children.length === 0) {
const node = this.allNodes.get(nodeId);
if (node && node.children) {
this.renderNodes(node.children, childrenList, node._depth);
}
}
childrenList.classList.add('visible');
toggleBtn.textContent = '−';
}
},

handleSearch: function(searchTerm) {
const term = searchTerm.trim();

if (term === '') {
this.clearSearch();
this.elements.noResults.style.display = 'none';
return;
}

this.searchResults.clear();

// Ищем совпадения
this.allNodes.forEach((node, nodeId) => {
const nodeName = node.name || node.node_name || '';
if (nodeName.toLowerCase().includes(term.toLowerCase())) {
this.searchResults.add(nodeId);
}
});

if (this.searchResults.size === 0) {
this.elements.noResults.style.display = 'block';
this.elements.tree.style.display = 'none';
} else {
this.elements.noResults.style.display = 'none';
this.elements.tree.style.display = 'block';
this.highlightSearchResults(term);
}

this.updateStats();
},

highlightSearchResults: function(searchTerm) {
const allNodes = document.querySelectorAll('.geo-lithology-tree-node');

// Сначала скрываем все
allNodes.forEach(node => {
node.style.display = 'none';
});

// Показываем найденные узлы и их родителей
const nodesToShow = new Set();

const getAllParents = (nodeId) => {
const parents = new Set();
let currentId = nodeId;

while (currentId) {
const node = this.allNodes.get(currentId);
if (node && node.parentId) {
parents.add(node.parentId);
currentId = node.parentId;
} else {
break;
}
}
return parents;
};

// Добавляем найденные узлы и их родителей
this.searchResults.forEach(nodeId => {
nodesToShow.add(nodeId);
const parents = getAllParents(nodeId);
parents.forEach(parentId => nodesToShow.add(parentId));
});

// Показываем нужные узлы
nodesToShow.forEach(nodeId => {
const nodeElement = document.querySelector(`[data-id="${nodeId}"]`);
if (nodeElement) {
nodeElement.style.display = '';

// Раскрываем родителей
if (this.searchResults.has(nodeId)) {
this.expandAllParents(nodeId);
}

// Подсвечиваем текст
if (this.searchResults.has(nodeId)) {
const nameElement = nodeElement.querySelector('.geo-lithology-node-name');
const originalText = nameElement.textContent;
const regex = new RegExp(`(${searchTerm})`, 'gi');
nameElement.innerHTML = originalText.replace(regex,
'<span class="geo-lithology-search-highlight">$1</span>');
}
}
});
},

expandAllParents: function(nodeId) {
let currentId = nodeId;

while (currentId) {
const node = this.allNodes.get(currentId);
if (node && node.parentId) {
const parentElement = document.querySelector(`[data-id="${node.parentId}"]`);
if (parentElement) {
const toggleBtn = parentElement.querySelector('.geo-lithology-toggle-btn');
if (toggleBtn && toggleBtn.textContent === '+') {
this.toggleNode(node.parentId);
}
}
currentId = node.parentId;
} else {
break;
}
}
},

clearSearch: function() {
const allNodes = document.querySelectorAll('.geo-lithology-tree-node');
allNodes.forEach(node => {
node.style.display = '';
const nameElement = node.querySelector('.geo-lithology-node-name');
if (nameElement.innerHTML !== nameElement.textContent) {
nameElement.innerHTML = nameElement.textContent;
}
});
this.searchResults.clear();
this.updateStats();
},

expandAll: function() {
const toggleButtons = document.querySelectorAll('.geo-lithology-toggle-btn');
toggleButtons.forEach(btn => {
if (btn.textContent === '+') {
const nodeElement = btn.closest('.geo-lithology-tree-node');
const nodeId = nodeElement.dataset.id;
this.toggleNode(nodeId);
}
});
},

collapseAll: function() {
const toggleButtons = document.querySelectorAll('.geo-lithology-toggle-btn');
toggleButtons.forEach(btn => {
if (btn.textContent === '−') {
const nodeElement = btn.closest('.geo-lithology-tree-node');
const nodeId = nodeElement.dataset.id;
this.toggleNode(nodeId);
}
});
},

updateStats: function() {
if (!this.data) return;

let totalCount = 0;
const countNodes = (nodes) => {
nodes.forEach(node => {
totalCount++;
if (node.children) {
countNodes(node.children);
}
});
};

countNodes(this.data);
this.elements.count.textContent = totalCount;

let statsText = `Всего элементов: ${totalCount}`;
if (this.elements.search.value.trim()) {
statsText += ` | Найдено: ${this.searchResults.size}`;
}

this.elements.stats.textContent = statsText;
}
};

// Инициализация при загрузке страницы
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM загружен, инициализируем дерево литологии');
LithologyTree.init();
});
} else {
console.log('DOM уже загружен, инициализируем дерево литологии');
LithologyTree.init();
}

// Экспорт для отладки
window.LithologyTree = LithologyTree;

})();
</script>
</div>
<!-- Конец компонента литологии -->

Дальнейший текст вашей страницы...

Добавлено (2026-01-20, 09:15)
---------------------------------------------
---
title: "Литология"
---

<!-- Компонент классификации литологии -->
<div id="lithology-app" class="lithology-app">

<style>
/* Основные стили */
.lithology-app {
width: 100%;
max-width: 1600px;
margin: 2rem auto;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #e6e9ef;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
color: #2d3748;
}

/* Панель управления */
.lithology-controls {
padding: 24px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
align-items: center;
}

@media (max-width: 1024px) {
.lithology-controls {
grid-template-columns: 1fr;
}
}

/* Поиск */
.lithology-search-container {
position: relative;
}

.lithology-search-input {
width: 100%;
padding: 14px 20px 14px 52px;
border: none;
border-radius: 12px;
font-size: 15px;
background: rgba(255, 255, 255, 0.95);
color: #2d3748;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.lithology-search-input:focus {
outline: none;
background: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}

.lithology-search-icon {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
color: #764ba2;
font-size: 16px;
}

/* Кнопки действий */
.lithology-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}

.lithology-btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
background: white;
color: #4a5568;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.lithology-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}

.lithology-btn-primary {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
color: white;
}

.lithology-btn-secondary {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}

.lithology-btn-danger {
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
color: white;
}

/* Переключатель режимов */
.lithology-view-toggle {
display: flex;
background: white;
border-radius: 10px;
padding: 6px;
margin: 0 16px;
}

.view-toggle-btn {
padding: 10px 24px;
border: none;
border-radius: 8px;
background: transparent;
color: #718096;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}

.view-toggle-btn.active {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
color: white;
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
}

/* Контейнер дерева */
.lithology-container {
display: flex;
min-height: 600px;
}

/* Дерево иерархии */
.lithology-tree-wrapper {
flex: 1;
padding: 0;
border-right: 1px solid #e6e9ef;
background: #fafbfc;
}

.lithology-tree {
padding: 24px;
max-height: 600px;
overflow-y: auto;
}

/* Список результатов */
.lithology-results-wrapper {
flex: 1;
padding: 0;
background: white;
}

.lithology-results {
padding: 24px;
max-height: 600px;
overflow-y: auto;
}

/* Стили узлов дерева */
.lithology-node {
position: relative;
margin: 8px 0;
}

.lithology-node-content {
display: flex;
align-items: center;
padding: 16px 20px;
border-radius: 10px;
background: white;
border: 1px solid #e2e8f0;
transition: all 0.3s ease;
cursor: pointer;
}

.lithology-node-content:hover {
border-color: #4299e1;
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.15);
transform: translateX(4px);
}

.lithology-node-content.selected {
border-color: #4299e1;
background: linear-gradient(135deg, #ebf8ff 0%, #bee3f8 100%);
}

/* Отступы для уровней */
.lithology-node-content.level-0 { margin-left: 0; }
.lithology-node-content.level-1 { margin-left: 32px; }
.lithology-node-content.level-2 { margin-left: 64px; }
.lithology-node-content.level-3 { margin-left: 96px; }
.lithology-node-content.level-4 { margin-left: 128px; }
.lithology-node-content.level-5 { margin-left: 160px; }

/* Кнопка раскрытия */
.lithology-toggle {
width: 28px;
height: 28px;
border-radius: 8px;
border: 2px solid #cbd5e0;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
flex-shrink: 0;
font-weight: bold;
color: #4a5568;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
}

.lithology-toggle:hover {
border-color: #4299e1;
background: #ebf8ff;
color: #4299e1;
}

.lithology-toggle.expanded:after {
content: "−";
}

.lithology-toggle.collapsed:after {
content: "+";
}

.lithology-toggle.no-children:after {
content: "•";
color: #a0aec0;
}

/* Информация узла */
.lithology-node-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}

.lithology-node-name {
font-size: 15px;
font-weight: 500;
color: #2d3748;
}

.lithology-node-details {
display: flex;
gap: 12px;
font-size: 12px;
color: #718096;
}

.lithology-node-level {
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%);
padding: 4px 12px;
border-radius: 20px;
font-weight: 600;
letter-spacing: 0.5px;
}

.lithology-node-id {
font-family: 'Monaco', 'Consolas', monospace;
background: #f7fafc;
padding: 4px 8px;
border-radius: 4px;
}

/* Дочерние элементы */
.lithology-children {
display: none;
margin-left: 32px;
}

.lithology-children.visible {
display: block;
}

/* Элементы списка результатов */
.lithology-result-item {
padding: 16px 20px;
border-bottom: 1px solid #edf2f7;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.3s ease;
}

.lithology-result-item:hover {
background: #f7fafc;
}

.result-item-info {
flex: 1;
}

.result-item-name {
font-size: 15px;
font-weight: 500;
color: #2d3748;
margin-bottom: 4px;
}

.result-item-details {
display: flex;
gap: 12px;
font-size: 13px;
color: #718096;
}

/* Состояния загрузки и пустые состояния */
.lithology-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #718096;
}

.lithology-spinner {
width: 50px;
height: 50px;
border: 3px solid #e2e8f0;
border-top: 3px solid #4299e1;
border-radius: 50%;
animation: lithology-spin 1s linear infinite;
margin-bottom: 20px;
}

@keyframes lithology-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.lithology-empty {
text-align: center;
padding: 60px 32px;
color: #a0aec0;
}

.lithology-empty-icon {
font-size: 64px;
margin-bottom: 24px;
opacity: 0.5;
}

/* Панель статистики */
.lithology-stats {
padding: 20px 32px;
background: #f7fafc;
border-top: 1px solid #e6e9ef;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}

.lithology-stats-info {
display: flex;
gap: 32px;
}

.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}

.stat-label {
color: #718096;
font-size: 13px;
}

.stat-value {
font-weight: 600;
color: #2d3748;
font-size: 18px;
}

/* Подсветка поиска */
.lithology-highlight {
background: linear-gradient(135deg, #fefcbf 0%, #faf089 100%);
color: #744210;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}

/* Тултипы */
[data-tooltip] {
position: relative;
}

[data-tooltip]:before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 8px 12px;
background: #2d3748;
color: white;
border-radius: 6px;
font-size: 13px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000;
margin-bottom: 8px;
}

[data-tooltip]:hover:before {
opacity: 1;
visibility: visible;
}

/* Мобильная адаптация */
@media (max-width: 768px) {
.lithology-container {
flex-direction: column;
}

.lithology-tree-wrapper,
.lithology-results-wrapper {
width: 100%;
border-right: none;
border-bottom: 1px solid #e6e9ef;
}

.lithology-controls {
padding: 20px;
}

.lithology-actions {
justify-content: center;
}

.lithology-node-content {
padding: 14px 16px;
}

.lithology-stats {
flex-direction: column;
gap: 20px;
text-align: center;
}

.lithology-stats-info {
gap: 20px;
}
}
</style>

<!-- HTML структура -->
<div class="lithology-controls">
<div class="lithology-search-container">
<span class="lithology-search-icon">🔍</span>
<input type="text" id="lithology-search" class="lithology-search-input"
placeholder="Поиск литологии...">
</div>

<div class="lithology-actions">
<div class="lithology-view-toggle">
<button class="view-toggle-btn active" data-view="tree">Дерево</button>
<button class="view-toggle-btn" data-view="list">Список</button>
</div>

<button id="lithology-expand-all" class="lithology-btn" data-tooltip="Развернуть все узлы">
<span>📂</span> Развернуть всё
</button>

<button id="lithology-collapse-all" class="lithology-btn" data-tooltip="Свернуть все узлы">
<span>📁</span> Свернуть всё
</button>

<button id="lithology-export-excel" class="lithology-btn lithology-btn-secondary" data-tooltip="Экспорт в Excel">
<span>📊</span> Экспорт в Excel
</button>
</div>
</div>

<div class="lithology-container">
<!-- Дерево иерархии -->
<div class="lithology-tree-wrapper">
<div class="lithology-tree" id="lithology-tree">
<div class="lithology-loading">
<div class="lithology-spinner"></div>
<div>Загрузка классификации...</div>
</div>
</div>
</div>

<!-- Список результатов -->
<div class="lithology-results-wrapper" style="display: none;">
<div class="lithology-results" id="lithology-results">
<div class="lithology-empty">
<div class="lithology-empty-icon">📋</div>
<h3>Список результатов</h3>
<p>Здесь будут отображаться выбранные элементы</p>
</div>
</div>
</div>
</div>

<div class="lithology-stats">
<div id="lithology-stats-text">Загрузка статистики...</div>

<div class="lithology-stats-info">
<div class="stat-item">
<span class="stat-label">Всего элементов</span>
<span class="stat-value" id="lithology-total">0</span>
</div>

<div class="stat-item">
<span class="stat-label">Найдено</span>
<span class="stat-value" id="lithology-found">0</span>
</div>

<div class="stat-item">
<span class="stat-label">В списке</span>
<span class="stat-value" id="lithology-in-list">0</span>
</div>
</div>
</div>

<script>
// Компонент литологии
(function() {
'use strict';

const LithologyApp = {
data: null,
selectedItems: new Set(),
searchResults: new Set(),
allNodes: new Map(),
viewMode: 'tree', // 'tree' или 'list'

elements: {},

init: function() {
this.cacheElements();
this.bindEvents();
this.loadData();
},

cacheElements: function() {
this.elements = {
app: document.getElementById('lithology-app'),
search: document.getElementById('lithology-search'),
tree: document.getElementById('lithology-tree'),
results: document.getElementById('lithology-results'),
expandBtn: document.getElementById('lithology-expand-all'),
collapseBtn: document.getElementById('lithology-collapse-all'),
exportBtn: document.getElementById('lithology-export-excel'),
statsText: document.getElementById('lithology-stats-text'),
totalCount: document.getElementById('lithology-total'),
foundCount: document.getElementById('lithology-found'),
listCount: document.getElementById('lithology-in-list'),
viewToggleBtns: document.querySelectorAll('.view-toggle-btn')
};
},

bindEvents: function() {
// Поиск
let searchTimeout;
this.elements.search.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.handleSearch(e.target.value);
}, 300);
});

// Кнопки управления
this.elements.expandBtn.addEventListener('click', () => this.expandAll());
this.elements.collapseBtn.addEventListener('click', () => this.collapseAll());
this.elements.exportBtn.addEventListener('click', () => this.exportToExcel());

// Переключение режимов просмотра
this.elements.viewToggleBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const view = e.target.dataset.view;
this.switchView(view);
});
});
},

loadData: function() {
console.log('Загрузка данных литологии...');

// Сначала проверяем глобальные переменные
if (this.checkDataVariable()) return;

// Пробуем загрузить из файла
this.loadFromFile();
},

checkDataVariable: function() {
if (window.lithologyData && Array.isArray(window.lithologyData)) {
this.data = window.lithologyData;
this.render();
return true;
}
if (window.geoLithologyData && Array.isArray(window.geoLithologyData)) {
this.data = window.geoLithologyData;
this.render();
return true;
}
return false;
},

loadFromFile: function() {
const possiblePaths = [
'./LithologyData.js',
'LithologyData.js',
'./data/LithologyData.js',
'./js/LithologyData.js',
'../LithologyData.js',
'/LithologyData.js'
];

const tryLoad = (index) => {
if (index >= possiblePaths.length) {
this.showError('Файл данных не найден');
return;
}

const path = possiblePaths[index];
console.log(`Попытка загрузки: ${path}`);

const script = document.createElement('script');
script.src = path;

script.onload = () => {
setTimeout(() => {
if (this.checkDataVariable()) {
console.log('Данные успешно загружены');
} else {
tryLoad(index + 1);
}
}, 100);
};

script.onerror = () => {
tryLoad(index + 1);
};

document.head.appendChild(script);
};

tryLoad(0);
},

showError: function(message) {
this.elements.tree.innerHTML = `
<div class="lithology-empty">
<div class="lithology-empty-icon">⚠️</div>
<h3>Ошибка загрузки</h3>
<p>${message}</p>
</div>
`;
},

render: function() {
if (!this.data || !Array.isArray(this.data)) {
this.showError('Нет данных для отображения');
return;
}

this.buildNodeMap(this.data);
this.renderTree();
this.updateStats();
},

buildNodeMap: function(nodes, parentId = null) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
this.allNodes.set(nodeId, {
...node,
parentId: parentId,
selected: this.selectedItems.has(nodeId)
});

if (node.children && node.children.length > 0) {
this.buildNodeMap(node.children, nodeId);
}
});
},

renderTree: function() {
this.elements.tree.innerHTML = '';

if (this.data.length === 0) {
this.elements.tree.innerHTML = `
<div class="lithology-empty">
<div class="lithology-empty-icon">📂</div>
<h3>Нет данных</h3>
<p>Данные для отображения отсутствуют</p>
</div>
`;
return;
}

const renderNode = (node, depth = 0) => {
const nodeId = node.id || node.node_id;
const hasChildren = node.children && node.children.length > 0;

const nodeDiv = document.createElement('div');
nodeDiv.className = 'lithology-node';

const contentDiv = document.createElement('div');
contentDiv.className = `lithology-node-content level-${depth}`;
if (this.selectedItems.has(nodeId)) {
contentDiv.classList.add('selected');
}

const toggleBtn = document.createElement('div');
toggleBtn.className = 'lithology-toggle';
if (hasChildren) {
toggleBtn.classList.add('collapsed');
} else {
toggleBtn.classList.add('no-children');
}

const infoDiv = document.createElement('div');
infoDiv.className = 'lithology-node-info';

const nameDiv = document.createElement('div');
nameDiv.className = 'lithology-node-name';
nameDiv.textContent = node.name || node.node_name;

const detailsDiv = document.createElement('div');
detailsDiv.className = 'lithology-node-details';

const levelSpan = document.createElement('span');
levelSpan.className = 'lithology-node-level';
levelSpan.textContent = `Уровень ${node.level || 1}`;

const idSpan = document.createElement('span');
idSpan.className = 'lithology-node-id';
idSpan.textContent = nodeId;

detailsDiv.appendChild(levelSpan);
detailsDiv.appendChild(idSpan);

infoDiv.appendChild(nameDiv);
infoDiv.appendChild(detailsDiv);

contentDiv.appendChild(toggleBtn);
contentDiv.appendChild(infoDiv);

nodeDiv.appendChild(contentDiv);

// Обработчики событий
contentDiv.addEventListener('click', (e) => {
if (e.target === toggleBtn) return;
this.toggleSelection(nodeId);
});

if (hasChildren) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleNode(nodeId, toggleBtn);
});

const childrenDiv = document.createElement('div');
childrenDiv.className = 'lithology-children';
childrenDiv.id = `children-${nodeId}`;
nodeDiv.appendChild(childrenDiv);

node._childrenDiv = childrenDiv;
node._toggleBtn = toggleBtn;
node._depth = depth + 1;
}

return nodeDiv;
};

const renderNodes = (nodes, container, depth = 0) => {
nodes.forEach(node => {
const nodeElement = renderNode(node, depth);
container.appendChild(nodeElement);
});
};

renderNodes(this.data, this.elements.tree);
},

toggleNode: function(nodeId, toggleBtn) {
const childrenDiv = document.getElementById(`children-${nodeId}`);
if (!childrenDiv) return;

if (childrenDiv.classList.contains('visible')) {
childrenDiv.classList.remove('visible');
toggleBtn.classList.remove('expanded');
toggleBtn.classList.add('collapsed');
} else {
// Ленивая загрузка детей
if (childrenDiv.children.length === 0) {
const node = this.allNodes.get(nodeId);
if (node && node.children) {
const nodeElement = this.findNodeElement(nodeId);
const depth = node._depth || 1;

node.children.forEach(child => {
const childElement = renderNode(child, depth);
childrenDiv.appendChild(childElement);
});
}
}
childrenDiv.classList.add('visible');
toggleBtn.classList.remove('collapsed');
toggleBtn.classList.add('expanded');
}
},

findNodeElement: function(nodeId) {
return document.querySelector(`[data-node-id="${nodeId}"]`);
},

toggleSelection: function(nodeId) {
if (this.selectedItems.has(nodeId)) {
this.selectedItems.delete(nodeId);
} else {
this.selectedItems.add(nodeId);
}

// Обновляем визуальное состояние
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const contentDiv = nodeElement.querySelector('.lithology-node-content');
if (contentDiv) {
if (this.selectedItems.has(nodeId)) {
contentDiv.classList.add('selected');
} else {
contentDiv.classList.remove('selected');
}
}
}

this.updateList();
this.updateStats();
},

handleSearch: function(searchTerm) {
const term = searchTerm.trim().toLowerCase();
this.searchResults.clear();

if (term === '') {
this.clearSearchHighlight();
this.elements.foundCount.textContent = '0';
return;
}

// Ищем совпадения
this.allNodes.forEach((node, nodeId) => {
const nodeName = (node.name || node.node_name || '').toLowerCase();
if (nodeName.includes(term)) {
this.searchResults.add(nodeId);
}
});

// Подсвечиваем результаты
this.highlightSearchResults(term);
this.updateStats();
},

highlightSearchResults: function(term) {
// Сначала очищаем все подсветки
this.clearSearchHighlight();

// Подсвечиваем найденные элементы
this.searchResults.forEach(nodeId => {
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const nameDiv = nodeElement.querySelector('.lithology-node-name');
if (nameDiv) {
const originalText = nameDiv.textContent;
const regex = new RegExp(`(${term})`, 'gi');
nameDiv.innerHTML = originalText.replace(regex,
'<span class="lithology-highlight">$1</span>');
}

// Раскрываем путь к найденному элементу
this.expandPathToNode(nodeId);
}
});
},

clearSearchHighlight: function() {
const nameDivs = document.querySelectorAll('.lithology-node-name');
nameDivs.forEach(div => {
if (div.innerHTML !== div.textContent) {
div.innerHTML = div.textContent;
}
});
},

expandPathToNode: function(nodeId) {
let currentId = nodeId;
const path = [];

// Собираем путь от элемента к корню
while (currentId) {
const node = this.allNodes.get(currentId);
if (node && node.parentId) {
path.unshift(node.parentId);
currentId = node.parentId;
} else {
break;
}
}

// Раскрываем все узлы на пути
path.forEach(parentId => {
const childrenDiv = document.getElementById(`children-${parentId}`);
const toggleBtn = document.querySelector(`#children-${parentId}`)?.previousElementSibling?.querySelector('.lithology-toggle');

if (childrenDiv && toggleBtn && !childrenDiv.classList.contains('visible')) {
this.toggleNode(parentId, toggleBtn);
}
});
},

expandAll: function() {
const toggleButtons = document.querySelectorAll('.lithology-toggle');
toggleButtons.forEach(btn => {
if (btn.classList.contains('collapsed')) {
const nodeElement = btn.closest('.lithology-node');
const childrenDiv = nodeElement.querySelector('.lithology-children');
if (childrenDiv) {
const nodeId = childrenDiv.id.replace('children-', '');
this.toggleNode(nodeId, btn);
}
}
});
},

collapseAll: function() {
const toggleButtons = document.querySelectorAll('.lithology-toggle');
toggleButtons.forEach(btn => {
if (btn.classList.contains('expanded')) {
const nodeElement = btn.closest('.lithology-node');
const childrenDiv = nodeElement.querySelector('.lithology-children');
if (childrenDiv) {
const nodeId = childrenDiv.id.replace('children-', '');
this.toggleNode(nodeId, btn);
}
}
});
},

switchView: function(view) {
this.viewMode = view;

// Обновляем активную кнопку
this.elements.viewToggleBtns.forEach(btn => {
if (btn.dataset.view === view) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});

// Показываем/скрываем соответствующий контейнер
const treeWrapper = document.querySelector('.lithology-tree-wrapper');
const resultsWrapper = document.querySelector('.lithology-results-wrapper');

if (view === 'tree') {
treeWrapper.style.display = 'block';
resultsWrapper.style.display = 'none';
} else {
treeWrapper.style.display = 'none';
resultsWrapper.style.display = 'block';
this.updateList();
}
},

updateList: function() {
if (this.viewMode !== 'list') return;

this.elements.results.innerHTML = '';

if (this.selectedItems.size === 0) {
this.elements.results.innerHTML = `
<div class="lithology-empty">
<div class="lithology-empty-icon">📋</div>
<h3>Список пуст</h3>
<p>Выберите элементы в дереве для добавления в список</p>
</div>
`;
return;
}

// Создаем заголовок таблицы
const headerDiv = document.createElement('div');
headerDiv.className = 'lithology-result-item';
headerDiv.style.fontWeight = '600';
headerDiv.style.background = '#f7fafc';
headerDiv.innerHTML = `
<div class="result-item-info">
<div class="result-item-name">Название</div>
<div class="result-item-details">
<span>ID</span>
<span>Уровень</span>
</div>
</div>
<div>
<button class="lithology-btn lithology-btn-danger" style="padding: 6px 12px; font-size: 12px;"
onclick="LithologyApp.clearList()">Очистить</button>
</div>
`;
this.elements.results.appendChild(headerDiv);

// Добавляем выбранные элементы
this.selectedItems.forEach(nodeId => {
const node = this.allNodes.get(nodeId);
if (!node) return;

const itemDiv = document.createElement('div');
itemDiv.className = 'lithology-result-item';

itemDiv.innerHTML = `
<div class="result-item-info">
<div class="result-item-name">${node.name || node.node_name}</div>
<div class="result-item-details">
<span>${nodeId}</span>
<span>Уровень ${node.level || 1}</span>
</div>
</div>
<button class="lithology-btn" style="padding: 6px 12px; font-size: 12px;"
onclick="LithologyApp.removeFromList('${nodeId}')">Удалить</button>
`;

this.elements.results.appendChild(itemDiv);
});
},

removeFromList: function(nodeId) {
this.selectedItems.delete(nodeId);
this.updateList();
this.updateStats();

// Обновляем выделение в дереве
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const contentDiv = nodeElement.querySelector('.lithology-node-content');
if (contentDiv) {
contentDiv.classList.remove('selected');
}
}
},

clearList: function() {
// Снимаем выделение со всех элементов в дереве
this.selectedItems.forEach(nodeId => {
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const contentDiv = nodeElement.querySelector('.lithology-node-content');
if (contentDiv) {
contentDiv.classList.remove('selected');
}
}
});

this.selectedItems.clear();
this.updateList();
this.updateStats();
},

exportToExcel: function() {
if (this.selectedItems.size === 0) {
alert('Список для экспорта пуст. Выберите элементы в дереве.');
return;
}

// Создаем данные для экспорта
const exportData = [];

// Заголовки
exportData.push(['ID', 'Название', 'Уровень', 'Код']);

// Данные
this.selectedItems.forEach(nodeId => {
const node = this.allNodes.get(nodeId);
if (node) {
exportData.push([
nodeId,
node.name || node.node_name || '',
node.level || '',
node.lcode_speck || ''
]);
}
});

// Создаем CSV
let csvContent = '';
exportData.forEach(row => {
csvContent += row.map(cell => {
// Экранируем кавычки и запятые
if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"'))) {
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
}).join(',') + '\n';
});

// Создаем Blob и скачиваем
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');

if (navigator.ms

Добавлено (2026-01-20, 09:17)
---------------------------------------------
---
title: "Литология"
---

<!-- Компонент классификации литологии -->
<div id="lithology-app" class="lithology-app">

<style>
/* Основные стили */
.lithology-app {
width: 100%;
max-width: 1600px;
margin: 2rem auto;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #e6e9ef;
font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
color: #2d3748;
}

/* Панель управления */
.lithology-controls {
padding: 24px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
align-items: center;
}

@media (max-width: 1024px) {
.lithology-controls {
grid-template-columns: 1fr;
}
}

/* Поиск */
.lithology-search-container {
position: relative;
}

.lithology-search-input {
width: 100%;
padding: 14px 20px 14px 52px;
border: none;
border-radius: 12px;
font-size: 15px;
background: rgba(255, 255, 255, 0.95);
color: #2d3748;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}

.lithology-search-input:focus {
outline: none;
background: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}

.lithology-search-icon {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
color: #764ba2;
font-size: 16px;
}

/* Кнопки действий */
.lithology-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}

.lithology-btn {
padding: 12px 24px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
background: white;
color: #4a5568;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.lithology-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}

.lithology-btn-primary {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
color: white;
}

.lithology-btn-secondary {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}

.lithology-btn-danger {
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
color: white;
}

/* Переключатель режимов */
.lithology-view-toggle {
display: flex;
background: white;
border-radius: 10px;
padding: 6px;
margin: 0 16px;
}

.view-toggle-btn {
padding: 10px 24px;
border: none;
border-radius: 8px;
background: transparent;
color: #718096;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}

.view-toggle-btn.active {
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
color: white;
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
}

/* Контейнер дерева */
.lithology-container {
display: flex;
min-height: 600px;
}

/* Дерево иерархии */
.lithology-tree-wrapper {
flex: 1;
padding: 0;
border-right: 1px solid #e6e9ef;
background: #fafbfc;
}

.lithology-tree {
padding: 24px;
max-height: 600px;
overflow-y: auto;
}

/* Список результатов */
.lithology-results-wrapper {
flex: 1;
padding: 0;
background: white;
}

.lithology-results {
padding: 24px;
max-height: 600px;
overflow-y: auto;
}

/* Стили узлов дерева */
.lithology-node {
position: relative;
margin: 8px 0;
}

.lithology-node-content {
display: flex;
align-items: center;
padding: 16px 20px;
border-radius: 10px;
background: white;
border: 1px solid #e2e8f0;
transition: all 0.3s ease;
cursor: pointer;
}

.lithology-node-content:hover {
border-color: #4299e1;
box-shadow: 0 4px 12px rgba(66, 153, 225, 0.15);
transform: translateX(4px);
}

.lithology-node-content.selected {
border-color: #4299e1;
background: linear-gradient(135deg, #ebf8ff 0%, #bee3f8 100%);
}

/* Отступы для уровней */
.lithology-node-content.level-0 { margin-left: 0; }
.lithology-node-content.level-1 { margin-left: 32px; }
.lithology-node-content.level-2 { margin-left: 64px; }
.lithology-node-content.level-3 { margin-left: 96px; }
.lithology-node-content.level-4 { margin-left: 128px; }
.lithology-node-content.level-5 { margin-left: 160px; }

/* Кнопка раскрытия */
.lithology-toggle {
width: 28px;
height: 28px;
border-radius: 8px;
border: 2px solid #cbd5e0;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
flex-shrink: 0;
font-weight: bold;
color: #4a5568;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
}

.lithology-toggle:hover {
border-color: #4299e1;
background: #ebf8ff;
color: #4299e1;
}

.lithology-toggle.expanded:after {
content: "−";
}

.lithology-toggle.collapsed:after {
content: "+";
}

.lithology-toggle.no-children:after {
content: "•";
color: #a0aec0;
}

/* Информация узла */
.lithology-node-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}

.lithology-node-name {
font-size: 15px;
font-weight: 500;
color: #2d3748;
}

.lithology-node-details {
display: flex;
gap: 12px;
font-size: 12px;
color: #718096;
}

.lithology-node-level {
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e0 100%);
padding: 4px 12px;
border-radius: 20px;
font-weight: 600;
letter-spacing: 0.5px;
}

.lithology-node-id {
font-family: 'Monaco', 'Consolas', monospace;
background: #f7fafc;
padding: 4px 8px;
border-radius: 4px;
}

/* Дочерние элементы */
.lithology-children {
display: none;
margin-left: 32px;
}

.lithology-children.visible {
display: block;
}

/* Элементы списка результатов */
.lithology-result-item {
padding: 16px 20px;
border-bottom: 1px solid #edf2f7;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.3s ease;
}

.lithology-result-item:hover {
background: #f7fafc;
}

.result-item-info {
flex: 1;
}

.result-item-name {
font-size: 15px;
font-weight: 500;
color: #2d3748;
margin-bottom: 4px;
}

.result-item-details {
display: flex;
gap: 12px;
font-size: 13px;
color: #718096;
}

/* Состояния загрузки и пустые состояния */
.lithology-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #718096;
}

.lithology-spinner {
width: 50px;
height: 50px;
border: 3px solid #e2e8f0;
border-top: 3px solid #4299e1;
border-radius: 50%;
animation: lithology-spin 1s linear infinite;
margin-bottom: 20px;
}

@keyframes lithology-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.lithology-empty {
text-align: center;
padding: 60px 32px;
color: #a0aec0;
}

.lithology-empty-icon {
font-size: 64px;
margin-bottom: 24px;
opacity: 0.5;
}

/* Панель статистики */
.lithology-stats {
padding: 20px 32px;
background: #f7fafc;
border-top: 1px solid #e6e9ef;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}

.lithology-stats-info {
display: flex;
gap: 32px;
}

.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}

.stat-label {
color: #718096;
font-size: 13px;
}

.stat-value {
font-weight: 600;
color: #2d3748;
font-size: 18px;
}

/* Подсветка поиска */
.lithology-highlight {
background: linear-gradient(135deg, #fefcbf 0%, #faf089 100%);
color: #744210;
padding: 2px 6px;
border-radius: 4px;
font-weight: 600;
}

/* Тултипы */
[data-tooltip] {
position: relative;
}

[data-tooltip]:before {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 8px 12px;
background: #2d3748;
color: white;
border-radius: 6px;
font-size: 13px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 1000;
margin-bottom: 8px;
}

[data-tooltip]:hover:before {
opacity: 1;
visibility: visible;
}

/* Мобильная адаптация */
@media (max-width: 768px) {
.lithology-container {
flex-direction: column;
}

.lithology-tree-wrapper,
.lithology-results-wrapper {
width: 100%;
border-right: none;
border-bottom: 1px solid #e6e9ef;
}

.lithology-controls {
padding: 20px;
}

.lithology-actions {
justify-content: center;
}

.lithology-node-content {
padding: 14px 16px;
}

.lithology-stats {
flex-direction: column;
gap: 20px;
text-align: center;
}

.lithology-stats-info {
gap: 20px;
}
}
</style>

<!-- HTML структура -->
<div class="lithology-controls">
<div class="lithology-search-container">
<span class="lithology-search-icon">🔍</span>
<input type="text" id="lithology-search" class="lithology-search-input"
placeholder="Поиск литологии...">
</div>

<div class="lithology-actions">
<div class="lithology-view-toggle">
<button class="view-toggle-btn active" data-view="tree">Дерево</button>
<button class="view-toggle-btn" data-view="list">Список</button>
</div>

<button id="lithology-expand-all" class="lithology-btn" data-tooltip="Развернуть все узлы">
<span>📂</span> Развернуть всё
</button>

<button id="lithology-collapse-all" class="lithology-btn" data-tooltip="Свернуть все узлы">
<span>📁</span> Свернуть всё
</button>

<button id="lithology-export-excel" class="lithology-btn lithology-btn-secondary" data-tooltip="Экспорт в Excel">
<span>📊</span> Экспорт в Excel
</button>
</div>
</div>

<div class="lithology-container">
<!-- Дерево иерархии -->
<div class="lithology-tree-wrapper">
<div class="lithology-tree" id="lithology-tree">
<div class="lithology-loading">
<div class="lithology-spinner"></div>
<div>Загрузка классификации...</div>
</div>
</div>
</div>

<!-- Список результатов -->
<div class="lithology-results-wrapper" style="display: none;">
<div class="lithology-results" id="lithology-results">
<div class="lithology-empty">
<div class="lithology-empty-icon">📋</div>
<h3>Список результатов</h3>
<p>Здесь будут отображаться выбранные элементы</p>
</div>
</div>
</div>
</div>

<div class="lithology-stats">
<div id="lithology-stats-text">Загрузка статистики...</div>

<div class="lithology-stats-info">
<div class="stat-item">
<span class="stat-label">Всего элементов</span>
<span class="stat-value" id="lithology-total">0</span>
</div>

<div class="stat-item">
<span class="stat-label">Найдено</span>
<span class="stat-value" id="lithology-found">0</span>
</div>

<div class="stat-item">
<span class="stat-label">В списке</span>
<span class="stat-value" id="lithology-in-list">0</span>
</div>
</div>
</div>

<script>
// Компонент литологии
(function() {
'use strict';

const LithologyApp = {
data: null,
selectedItems: new Set(),
searchResults: new Set(),
allNodes: new Map(),
viewMode: 'tree', // 'tree' или 'list'

elements: {},

init: function() {
this.cacheElements();
this.bindEvents();
this.loadData();
},

cacheElements: function() {
this.elements = {
app: document.getElementById('lithology-app'),
search: document.getElementById('lithology-search'),
tree: document.getElementById('lithology-tree'),
results: document.getElementById('lithology-results'),
expandBtn: document.getElementById('lithology-expand-all'),
collapseBtn: document.getElementById('lithology-collapse-all'),
exportBtn: document.getElementById('lithology-export-excel'),
statsText: document.getElementById('lithology-stats-text'),
totalCount: document.getElementById('lithology-total'),
foundCount: document.getElementById('lithology-found'),
listCount: document.getElementById('lithology-in-list'),
viewToggleBtns: document.querySelectorAll('.view-toggle-btn')
};
},

bindEvents: function() {
// Поиск
let searchTimeout;
this.elements.search.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.handleSearch(e.target.value);
}, 300);
});

// Кнопки управления
this.elements.expandBtn.addEventListener('click', () => this.expandAll());
this.elements.collapseBtn.addEventListener('click', () => this.collapseAll());
this.elements.exportBtn.addEventListener('click', () => this.exportToExcel());

// Переключение режимов просмотра
this.elements.viewToggleBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const view = e.target.dataset.view;
this.switchView(view);
});
});
},

loadData: function() {
console.log('Загрузка данных литологии...');

// Сначала проверяем глобальные переменные
if (this.checkDataVariable()) return;

// Пробуем загрузить из файла
this.loadFromFile();
},

checkDataVariable: function() {
if (window.lithologyData && Array.isArray(window.lithologyData)) {
this.data = window.lithologyData;
this.render();
return true;
}
if (window.geoLithologyData && Array.isArray(window.geoLithologyData)) {
this.data = window.geoLithologyData;
this.render();
return true;
}
return false;
},

loadFromFile: function() {
const possiblePaths = [
'./LithologyData.js',
'LithologyData.js',
'./data/LithologyData.js',
'./js/LithologyData.js',
'../LithologyData.js',
'/LithologyData.js'
];

const tryLoad = (index) => {
if (index >= possiblePaths.length) {
this.showError('Файл данных не найден');
return;
}

const path = possiblePaths[index];
console.log(`Попытка загрузки: ${path}`);

const script = document.createElement('script');
script.src = path;

script.onload = () => {
setTimeout(() => {
if (this.checkDataVariable()) {
console.log('Данные успешно загружены');
} else {
tryLoad(index + 1);
}
}, 100);
};

script.onerror = () => {
tryLoad(index + 1);
};

document.head.appendChild(script);
};

tryLoad(0);
},

showError: function(message) {
this.elements.tree.innerHTML = `
<div class="lithology-empty">
<div class="lithology-empty-icon">⚠️</div>
<h3>Ошибка загрузки</h3>
<p>${message}</p>
</div>
`;
},

render: function() {
if (!this.data || !Array.isArray(this.data)) {
this.showError('Нет данных для отображения');
return;
}

this.buildNodeMap(this.data);
this.renderTree();
this.updateStats();
},

buildNodeMap: function(nodes, parentId = null) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
this.allNodes.set(nodeId, {
...node,
parentId: parentId,
selected: this.selectedItems.has(nodeId)
});

if (node.children && node.children.length > 0) {
this.buildNodeMap(node.children, nodeId);
}
});
},

renderTree: function() {
this.elements.tree.innerHTML = '';

if (this.data.length === 0) {
this.elements.tree.innerHTML = `
<div class="lithology-empty">
<div class="lithology-empty-icon">📂</div>
<h3>Нет данных</h3>
<p>Данные для отображения отсутствуют</p>
</div>
`;
return;
}

const renderNode = (node, depth = 0) => {
const nodeId = node.id || node.node_id;
const hasChildren = node.children && node.children.length > 0;

const nodeDiv = document.createElement('div');
nodeDiv.className = 'lithology-node';

const contentDiv = document.createElement('div');
contentDiv.className = `lithology-node-content level-${depth}`;
if (this.selectedItems.has(nodeId)) {
contentDiv.classList.add('selected');
}

const toggleBtn = document.createElement('div');
toggleBtn.className = 'lithology-toggle';
if (hasChildren) {
toggleBtn.classList.add('collapsed');
} else {
toggleBtn.classList.add('no-children');
}

const infoDiv = document.createElement('div');
infoDiv.className = 'lithology-node-info';

const nameDiv = document.createElement('div');
nameDiv.className = 'lithology-node-name';
nameDiv.textContent = node.name || node.node_name;

const detailsDiv = document.createElement('div');
detailsDiv.className = 'lithology-node-details';

const levelSpan = document.createElement('span');
levelSpan.className = 'lithology-node-level';
levelSpan.textContent = `Уровень ${node.level || 1}`;

const idSpan = document.createElement('span');
idSpan.className = 'lithology-node-id';
idSpan.textContent = nodeId;

detailsDiv.appendChild(levelSpan);
detailsDiv.appendChild(idSpan);

infoDiv.appendChild(nameDiv);
infoDiv.appendChild(detailsDiv);

contentDiv.appendChild(toggleBtn);
contentDiv.appendChild(infoDiv);

nodeDiv.appendChild(contentDiv);

// Обработчики событий
contentDiv.addEventListener('click', (e) => {
if (e.target === toggleBtn) return;
this.toggleSelection(nodeId);
});

if (hasChildren) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleNode(nodeId, toggleBtn);
});

const childrenDiv = document.createElement('div');
childrenDiv.className = 'lithology-children';
childrenDiv.id = `children-${nodeId}`;
nodeDiv.appendChild(childrenDiv);

node._childrenDiv = childrenDiv;
node._toggleBtn = toggleBtn;
node._depth = depth + 1;
}

return nodeDiv;
};

const renderNodes = (nodes, container, depth = 0) => {
nodes.forEach(node => {
const nodeElement = renderNode(node, depth);
container.appendChild(nodeElement);
});
};

renderNodes(this.data, this.elements.tree);
},

toggleNode: function(nodeId, toggleBtn) {
const childrenDiv = document.getElementById(`children-${nodeId}`);
if (!childrenDiv) return;

if (childrenDiv.classList.contains('visible')) {
childrenDiv.classList.remove('visible');
toggleBtn.classList.remove('expanded');
toggleBtn.classList.add('collapsed');
} else {
// Ленивая загрузка детей
if (childrenDiv.children.length === 0) {
const node = this.allNodes.get(nodeId);
if (node && node.children) {
const nodeElement = this.findNodeElement(nodeId);
const depth = node._depth || 1;

node.children.forEach(child => {
const childElement = renderNode(child, depth);
childrenDiv.appendChild(childElement);
});
}
}
childrenDiv.classList.add('visible');
toggleBtn.classList.remove('collapsed');
toggleBtn.classList.add('expanded');
}
},

findNodeElement: function(nodeId) {
return document.querySelector(`[data-node-id="${nodeId}"]`);
},

toggleSelection: function(nodeId) {
if (this.selectedItems.has(nodeId)) {
this.selectedItems.delete(nodeId);
} else {
this.selectedItems.add(nodeId);
}

// Обновляем визуальное состояние
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const contentDiv = nodeElement.querySelector('.lithology-node-content');
if (contentDiv) {
if (this.selectedItems.has(nodeId)) {
contentDiv.classList.add('selected');
} else {
contentDiv.classList.remove('selected');
}
}
}

this.updateList();
this.updateStats();
},

handleSearch: function(searchTerm) {
const term = searchTerm.trim().toLowerCase();
this.searchResults.clear();

if (term === '') {
this.clearSearchHighlight();
this.elements.foundCount.textContent = '0';
return;
}

// Ищем совпадения
this.allNodes.forEach((node, nodeId) => {
const nodeName = (node.name || node.node_name || '').toLowerCase();
if (nodeName.includes(term)) {
this.searchResults.add(nodeId);
}
});

// Подсвечиваем результаты
this.highlightSearchResults(term);
this.updateStats();
},

highlightSearchResults: function(term) {
// Сначала очищаем все подсветки
this.clearSearchHighlight();

// Подсвечиваем найденные элементы
this.searchResults.forEach(nodeId => {
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const nameDiv = nodeElement.querySelector('.lithology-node-name');
if (nameDiv) {
const originalText = nameDiv.textContent;
const regex = new RegExp(`(${term})`, 'gi');
nameDiv.innerHTML = originalText.replace(regex,
'<span class="lithology-highlight">$1</span>');
}

// Раскрываем путь к найденному элементу
this.expandPathToNode(nodeId);
}
});
},

clearSearchHighlight: function() {
const nameDivs = document.querySelectorAll('.lithology-node-name');
nameDivs.forEach(div => {
if (div.innerHTML !== div.textContent) {
div.innerHTML = div.textContent;
}
});
},

expandPathToNode: function(nodeId) {
let currentId = nodeId;
const path = [];

// Собираем путь от элемента к корню
while (currentId) {
const node = this.allNodes.get(currentId);
if (node && node.parentId) {
path.unshift(node.parentId);
currentId = node.parentId;
} else {
break;
}
}

// Раскрываем все узлы на пути
path.forEach(parentId => {
const childrenDiv = document.getElementById(`children-${parentId}`);
const toggleBtn = document.querySelector(`#children-${parentId}`)?.previousElementSibling?.querySelector('.lithology-toggle');

if (childrenDiv && toggleBtn && !childrenDiv.classList.contains('visible')) {
this.toggleNode(parentId, toggleBtn);
}
});
},

expandAll: function() {
const toggleButtons = document.querySelectorAll('.lithology-toggle');
toggleButtons.forEach(btn => {
if (btn.classList.contains('collapsed')) {
const nodeElement = btn.closest('.lithology-node');
const childrenDiv = nodeElement.querySelector('.lithology-children');
if (childrenDiv) {
const nodeId = childrenDiv.id.replace('children-', '');
this.toggleNode(nodeId, btn);
}
}
});
},

collapseAll: function() {
const toggleButtons = document.querySelectorAll('.lithology-toggle');
toggleButtons.forEach(btn => {
if (btn.classList.contains('expanded')) {
const nodeElement = btn.closest('.lithology-node');
const childrenDiv = nodeElement.querySelector('.lithology-children');
if (childrenDiv) {
const nodeId = childrenDiv.id.replace('children-', '');
this.toggleNode(nodeId, btn);
}
}
});
},

switchView: function(view) {
this.viewMode = view;

// Обновляем активную кнопку
this.elements.viewToggleBtns.forEach(btn => {
if (btn.dataset.view === view) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});

// Показываем/скрываем соответствующий контейнер
const treeWrapper = document.querySelector('.lithology-tree-wrapper');
const resultsWrapper = document.querySelector('.lithology-results-wrapper');

if (view === 'tree') {
treeWrapper.style.display = 'block';
resultsWrapper.style.display = 'none';
} else {
treeWrapper.style.display = 'none';
resultsWrapper.style.display = 'block';
this.updateList();
}
},

updateList: function() {
if (this.viewMode !== 'list') return;

this.elements.results.innerHTML = '';

if (this.selectedItems.size === 0) {
this.elements.results.innerHTML = `
<div class="lithology-empty">
<div class="lithology-empty-icon">📋</div>
<h3>Список пуст</h3>
<p>Выберите элементы в дереве для добавления в список</p>
</div>
`;
return;
}

// Создаем заголовок таблицы
const headerDiv = document.createElement('div');
headerDiv.className = 'lithology-result-item';
headerDiv.style.fontWeight = '600';
headerDiv.style.background = '#f7fafc';
headerDiv.innerHTML = `
<div class="result-item-info">
<div class="result-item-name">Название</div>
<div class="result-item-details">
<span>ID</span>
<span>Уровень</span>
</div>
</div>
<div>
<button class="lithology-btn lithology-btn-danger" style="padding: 6px 12px; font-size: 12px;"
onclick="LithologyApp.clearList()">Очистить</button>
</div>
`;
this.elements.results.appendChild(headerDiv);

// Добавляем выбранные элементы
this.selectedItems.forEach(nodeId => {
const node = this.allNodes.get(nodeId);
if (!node) return;

const itemDiv = document.createElement('div');
itemDiv.className = 'lithology-result-item';

itemDiv.innerHTML = `
<div class="result-item-info">
<div class="result-item-name">${node.name || node.node_name}</div>
<div class="result-item-details">
<span>${nodeId}</span>
<span>Уровень ${node.level || 1}</span>
</div>
</div>
<button class="lithology-btn" style="padding: 6px 12px; font-size: 12px;"
onclick="LithologyApp.removeFromList('${nodeId}')">Удалить</button>
`;

this.elements.results.appendChild(itemDiv);
});
},

removeFromList: function(nodeId) {
this.selectedItems.delete(nodeId);
this.updateList();
this.updateStats();

// Обновляем выделение в дереве
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const contentDiv = nodeElement.querySelector('.lithology-node-content');
if (contentDiv) {
contentDiv.classList.remove('selected');
}
}
},

clearList: function() {
// Снимаем выделение со всех элементов в дереве
this.selectedItems.forEach(nodeId => {
const nodeElement = this.findNodeElement(nodeId);
if (nodeElement) {
const contentDiv = nodeElement.querySelector('.lithology-node-content');
if (contentDiv) {
contentDiv.classList.remove('selected');
}
}
});

this.selectedItems.clear();
this.updateList();
this.updateStats();
},

exportToExcel: function() {
if (this.selectedItems.size === 0) {
alert('Список для экспорта пуст. Выберите элементы в дереве.');
return;
}

// Создаем данные для экспорта
const exportData = [];

// Заголовки
exportData.push(['ID', 'Название', 'Уровень', 'Код']);

// Данные
this.selectedItems.forEach(nodeId => {
const node = this.allNodes.get(nodeId);
if (node) {
exportData.push([
nodeId,
node.name || node.node_name || '',
node.level || '',
node.lcode_speck || ''
]);
}
});

// Создаем CSV
let csvContent = '';
exportData.forEach(row => {
csvContent += row.map(cell => {
// Экранируем кавычки и запятые
if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"'))) {
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
}).join(',') + '\n';
});

// Создаем Blob и скачиваем
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');

if (navigator.msSaveBlob) { // IE 10+
navigator.msSaveBlob(blob, 'lithology_export.csv');
} else {
link.href = URL.createObjectURL(blob);
link.download = `lithology_export_${new Date().toISOString().split('T')[0]}.csv`;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}

// Показываем уведомление
this.showNotification(`Экспортировано ${this.selectedItems.size} элементов`);
},

showNotification: function(message) {
// Создаем уведомление
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
padding: 16px 24px;
border-radius: 10px;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideIn 0.3s ease;
font-weight: 500;
`;
notification.textContent = message;

document.body.appendChild(notification);

// Удаляем через 3 секунды
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);

// Добавляем анимации
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
},

updateStats: function() {
const total = this.allNodes.size;
const found = this.searchResults.size;
const inList = this.selectedItems.size;

this.elements.totalCount.textContent = total;
this.elements.foundCount.textContent = found;
this.elements.listCount.textContent = inList;

let statsText = `Всего элементов: ${total}`;
if (found > 0) {
statsText += ` | Найдено: ${found}`;
}
if (inList > 0) {
statsText += ` | Выбрано: ${inList}`;
}

this.elements.statsText.textContent = statsText;
}
};

// Экспорт для использования в обработчиках onclick
window.LithologyApp = LithologyApp;

// Инициализация
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => LithologyApp.init());
} else {
LithologyApp.init();
}

})();
</script>
</div>
<!-- Конец компонента -->

<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

body {
background: #f8fafc;
min-height: 100vh;
padding: 20px;
color: #1e293b;
}

.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e2e8f0;
}

.header {
background: white;
padding: 24px 32px;
border-bottom: 1px solid #e2e8f0;
}

.header h1 {
font-size: 24px;
font-weight: 600;
color: #1e293b;
margin-bottom: 8px;
}

.header p {
color: #64748b;
font-size: 14px;
}

.controls {
padding: 20px 32px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
display: grid;
grid-template-columns: 1fr auto;
gap: 20px;
align-items: center;
}

.search-container {
position: relative;
}

.search-input {
width: 100%;
padding: 12px 16px 12px 44px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
background: white;
color: #1e293b;
transition: all 0.2s;
}

.search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.search-input::placeholder {
color: #94a3b8;
}

.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
width: 16px;
height: 16px;
}

.action-buttons {
display: flex;
gap: 12px;
}

.btn {
padding: 10px 20px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: white;
color: #475569;
}

.btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}

.btn-primary {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}

.btn-primary:hover {
background: #2563eb;
border-color: #2563eb;
}

.btn-secondary {
background: #10b981;
color: white;
border-color: #10b981;
}

.btn-secondary:hover {
background: #059669;
border-color: #059669;
}

.btn-export {
background: #8b5cf6;
color: white;
border-color: #8b5cf6;
}

.btn-export:hover {
background: #7c3aed;
border-color: #7c3aed;
}

.view-controls {
padding: 16px 32px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
display: flex;
gap: 12px;
align-items: center;
}

.view-toggle {
display: flex;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}

.view-btn {
padding: 8px 16px;
border: none;
background: transparent;
color: #64748b;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}

.view-btn:hover {
background: #f1f5f9;
}

.view-btn.active {
background: #3b82f6;
color: white;
}

.tree-container {
padding: 0;
min-height: 500px;
max-height: 600px;
overflow-y: auto;
background: white;
position: relative;
}

.list-container {
padding: 0;
min-height: 500px;
max-height: 600px;
overflow-y: auto;
background: white;
position: relative;
display: none;
}

.tree-list {
list-style: none;
padding: 0;
margin: 0;
}

.data-list {
list-style: none;
padding: 0;
margin: 0;
}

.tree-node {
position: relative;
}

.list-item {
display: flex;
align-items: center;
padding: 12px 32px;
border-bottom: 1px solid #f1f5f9;
min-height: 48px;
transition: background 0.2s;
position: relative;
}

.list-item:hover {
background: #f8fafc;
}

.node-content {
display: flex;
align-items: center;
padding: 12px 32px 12px 20px;
border-bottom: 1px solid #f1f5f9;
min-height: 48px;
transition: background 0.2s;
position: relative;
cursor: pointer;
width: 100%;
}

.node-content:hover {
background: #f8fafc;
}

.node-content.level-0 { padding-left: 20px; }
.node-content.level-1 { padding-left: 40px; }
.node-content.level-2 { padding-left: 60px; }
.node-content.level-3 { padding-left: 80px; }
.node-content.level-4 { padding-left: 100px; }
.node-content.level-5 { padding-left: 120px; }
.node-content.level-6 { padding-left: 140px; }

.toggle-btn {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid #cbd5e1;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
margin-right: 12px;
flex-shrink: 0;
color: #475569;
transition: all 0.2s;
cursor: pointer;
user-select: none;
}

.toggle-btn:hover {
border-color: #94a3b8;
background: #f1f5f9;
}

.toggle-btn.has-children::before {
content: '+';
}

.toggle-btn.has-children.expanded::before {
content: '−';
}

.toggle-btn.no-children {
opacity: 0.5;
cursor: default;
}

.toggle-btn.no-children::before {
content: '•';
}

.node-name {
font-size: 14px;
color: #1e293b;
flex: 1;
font-weight: 400;
}

.list-name {
font-size: 14px;
color: #1e293b;
flex: 1;
font-weight: 400;
}

.list-id {
font-size: 12px;
color: #64748b;
font-family: 'SF Mono', monospace;
background: #f1f5f9;
padding: 4px 8px;
border-radius: 6px;
margin-left: 12px;
flex-shrink: 0;
}

.node-children {
display: none;
}

.node-children.visible {
display: block;
}

.empty-state {
text-align: center;
padding: 80px 32px;
color: #64748b;
}

.empty-icon {
font-size: 48px;
margin-bottom: 20px;
opacity: 0.5;
}

.empty-state h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: #475569;
}

.empty-state p {
font-size: 14px;
max-width: 400px;
margin: 0 auto 24px;
line-height: 1.5;
}

.no-results {
text-align: center;
padding: 60px 32px;
color: #64748b;
display: none;
}

.stats-bar {
padding: 16px 32px;
background: white;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #64748b;
}

.stats-info {
display: flex;
gap: 24px;
}

.stat-item {
display: flex;
align-items: center;
gap: 6px;
}

.stat-value {
font-weight: 600;
color: #1e293b;
}

.loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.95);
z-index: 100;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 12px;
display: none;
}

.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f1f5f9;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.search-highlight {
background-color: #fef3c7;
color: #92400e;
padding: 2px 4px;
border-radius: 4px;
font-weight: 500;
}

@media (max-width: 768px) {
.controls {
grid-template-columns: 1fr;
}

.action-buttons {
justify-content: flex-start;
flex-wrap: wrap;
}

.node-content, .list-item {
padding: 12px 16px;
flex-wrap: wrap;
gap: 8px;
}

.list-id {
order: 2;
margin-left: auto;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Литостратиграфическая классификация</h1>
<p>Древовидная структура стратиграфических подразделений и литологических типов пород</p>
</div>

<div class="controls">
<div class="search-container">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="M21 21l-4.35-4.35"/>
</svg>
<input type="text" class="search-input" placeholder="Поиск по названию..." id="searchInput">
</div>

<div class="action-buttons">
<button class="btn btn-primary" id="expandAllBtn">
<span>+</span> Развернуть всё
</button>
<button class="btn" id="collapseAllBtn">
<span>−</span> Свернуть всё
</button>
<button class="btn btn-secondary" id="exportBtn">
<span>📊</span> Экспорт в Excel
</button>
<button class="btn" id="resetBtn">
<span>↺</span> Сбросить
</button>
</div>
</div>

<div class="view-controls">
<div class="view-toggle">
<button class="view-btn active" data-view="tree">
Вид по группам
</button>
<button class="view-btn" data-view="list">
Литологический список
</button>
</div>
<div style="flex: 1;"></div>
<div class="stats-info">
<div class="stat-item">
<span>Всего элементов:</span>
<span class="stat-value" id="totalCount">0</span>
</div>
<div class="stat-item">
<span>Найдено:</span>
<span class="stat-value" id="foundCount">0</span>
</div>
</div>
</div>

<div class="tree-container" id="treeView">
<div id="emptyState" class="empty-state">
<div class="empty-icon">📊</div>
<h3>Нет данных для отображения</h3>
<p>Загрузка данных...</p>
</div>

<div id="noResults" class="no-results" style="display: none;">
<div class="empty-icon">🔍</div>
<h3>Ничего не найдено</h3>
<p>Попробуйте изменить поисковый запрос</p>
</div>

<ul id="treeList" class="tree-list"></ul>

<div id="loading" class="loading">
<div class="loading-spinner"></div>
<div style="font-size: 14px; color: #64748b; font-weight: 500;">Загрузка данных...</div>
</div>
</div>

<div class="list-container" id="listView">
<div id="listEmptyState" class="empty-state" style="display: none;">
<div class="empty-icon">📊</div>
<h3>Нет данных для отображения</h3>
<p>Загрузка данных...</p>
</div>

<div id="listNoResults" class="no-results" style="display: none;">
<div class="empty-icon">🔍</div>
<h3>Ничего не найдено</h3>
<p>Попробуйте изменить поисковый запрос</p>
</div>

<ul id="dataList" class="data-list"></ul>

<div id="listLoading" class="loading" style="display: none;">
<div class="loading-spinner"></div>
<div style="font-size: 14px; color: #64748b; font-weight: 500;">Загрузка данных...</div>
</div>
</div>

<div class="stats-bar">
<div class="last-update">
<span id="lastUpdate">—</span>
</div>
<div class="export-info">
<button class="btn btn-export" id="exportListBtn" style="display: none;">
<span>📥</span> Экспортировать список в Excel
</button>
</div>
</div>
</div>

<script>
class LithologyTree {
constructor() {
this.data = [];
this.flatList = [];
this.filteredTreeData = [];
this.filteredListData = [];
this.searchTerm = '';
this.expandedNodes = new Set();
this.currentView = 'tree';

this.initElements();
this.initEventListeners();
this.loadData();
}

initElements() {
this.elements = {
treeList: document.getElementById('treeList'),
dataList: document.getElementById('dataList'),
searchInput: document.getElementById('searchInput'),
expandAllBtn: document.getElementById('expandAllBtn'),
collapseAllBtn: document.getElementById('collapseAllBtn'),
exportBtn: document.getElementById('exportBtn'),
exportListBtn: document.getElementById('exportListBtn'),
resetBtn: document.getElementById('resetBtn'),
loading: document.getElementById('loading'),
listLoading: document.getElementById('listLoading'),
emptyState: document.getElementById('emptyState'),
listEmptyState: document.getElementById('listEmptyState'),
noResults: document.getElementById('noResults'),
listNoResults: document.getElementById('listNoResults'),
totalCount: document.getElementById('totalCount'),
foundCount: document.getElementById('foundCount'),
lastUpdate: document.getElementById('lastUpdate'),
treeView: document.getElementById('treeView'),
listView: document.getElementById('listView'),
viewBtns: document.querySelectorAll('.view-btn')
};
}

initEventListeners() {
// Поиск с учетом регистра
this.elements.searchInput.addEventListener('input', (e) => {
this.searchTerm = e.target.value.trim();
this.renderCurrentView();
});

// Кнопки управления
this.elements.expandAllBtn.addEventListener('click', () => this.expandAll());
this.elements.collapseAllBtn.addEventListener('click', () => this.collapseAll());
this.elements.exportBtn.addEventListener('click', () => this.exportToExcel());
this.elements.exportListBtn.addEventListener('click', () => this.exportListToExcel());
this.elements.resetBtn.addEventListener('click', () => this.reset());

// Переключение вида
this.elements.viewBtns.forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
this.switchView(view);
});
});
}

async loadData() {
this.showLoading();

// Сначала проверяем, есть ли данные уже загруженные на странице
if (typeof window.lithologyData !== 'undefined' && window.lithologyData.length > 0) {
this.data = window.lithologyData;
this.prepareFlatList();
this.hideLoading();
this.renderCurrentView();
this.updateLastUpdate('встроенные данные');
return;
}

// Пробуем несколько возможных путей к JS файлу
const possiblePaths = [
'LithologyData.js', // В той же папке
'./LithologyData.js', // В текущей директории
'js/LithologyData.js', // В папке js
'/js/LithologyData.js', // В корневой папке js
'../LithologyData.js', // На уровень выше
'../../LithologyData.js' // На два уровня выше
];

// Пробуем каждый путь по очереди
for (const path of possiblePaths) {
try {
const data = await this.loadScript(path);
if (data) {
this.data = data;
this.prepareFlatList();
this.hideLoading();
this.renderCurrentView();
this.updateLastUpdate(`файл: ${path}`);
return;
}
} catch (error) {
console.log(`Файл не найден: ${path}`);
continue;
}
}

// Если ничего не найдено, показываем ошибку
this.showNoDataError();
}

loadScript(path) {
return new Promise((resolve, reject) => {
// Проверяем, не загружен ли уже файл
if (typeof window.lithologyData !== 'undefined') {
resolve(window.lithologyData);
return;
}

const script = document.createElement('script');
script.src = path;

// Устанавливаем таймаут для отлова ошибок
const timeout = setTimeout(() => {
script.onerror = null;
script.onload = null;
document.head.removeChild(script);
reject(new Error(`Таймаут загрузки: ${path}`));
}, 3000);

script.onload = () => {
clearTimeout(timeout);
if (typeof window.lithologyData !== 'undefined') {
resolve(window.lithologyData);
} else {
reject(new Error(`Данные не определены в: ${path}`));
}
};

script.onerror = () => {
clearTimeout(timeout);
reject(new Error(`Ошибка загрузки: ${path}`));
};

document.head.appendChild(script);
});
}

showNoDataError() {
console.warn('Данные не найдены. Используется тестовый набор.');

// Тестовые данные (можно удалить в продакшене)
this.data = [
{
"id": "A030110",
"name": "ЧЕТВЕРТИЧНЫЕ ОБРАЗОВАНИЯ",
"level": 2,
"node_id": "A030110",
"node_name": "ЧЕТВЕРТИЧНЫЕ ОБРАЗОВАНИЯ",
"lcode_speck": "",
"children": [
{
"id": "A030110000000007",
"name": "(ОТСЫПКА)",
"level": 4,
"node_id": "A030110000000007",
"node_name": "(ОТСЫПКА)",
"lcode_speck": "",
"children": [
{
"id": "A030110000000007001",
"name": "ОТСЫПКА",
"level": 8,
"node_id": "A030110000000007001",
"node_name": "ОТСЫПКА",
"lcode_speck": ""
}
]
}
]
}
];

this.prepareFlatList();
this.hideLoading();
this.renderCurrentView();
this.updateLastUpdate('тестовые данные');
}

prepareFlatList() {
this.flatList = [];
this.flattenData(this.data, this.flatList);
}

flattenData(nodes, result) {
nodes.forEach(node => {
// Добавляем только конечные элементы (без детей)
if (!node.children || node.children.length === 0) {
result.push({
id: node.id,
name: node.name,
level: node.level,
node_id: node.node_id,
node_name: node.node_name,
lcode_speck: node.lcode_speck
});
}

if (node.children) {
this.flattenData(node.children, result);
}
});
}

showLoading() {
if (this.currentView === 'tree') {
this.elements.loading.style.display = 'flex';
this.elements.emptyState.style.display = 'none';
} else {
this.elements.listLoading.style.display = 'flex';
this.elements.listEmptyState.style.display = 'none';
}
}

hideLoading() {
this.elements.loading.style.display = 'none';
this.elements.listLoading.style.display = 'none';
}

switchView(view) {
if (this.currentView === view) return;

this.currentView = view;

// Обновляем активную кнопку
this.elements.viewBtns.forEach(btn => {
if (btn.dataset.view === view) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});

// Показываем/скрываем контейнеры
if (view === 'tree') {
this.elements.treeView.style.display = 'block';
this.elements.listView.style.display = 'none';
this.elements.exportListBtn.style.display = 'none';
} else {
this.elements.treeView.style.display = 'none';
this.elements.listView.style.display = 'block';
this.elements.exportListBtn.style.display = 'flex';
this.renderListView();
}

this.renderCurrentView();
}

renderCurrentView() {
if (this.currentView === 'tree') {
this.renderTreeView();
} else {
this.renderListView();
}
}

renderTreeView() {
if (!this.data || this.data.length === 0) {
this.elements.emptyState.style.display = 'block';
this.elements.treeList.style.display = 'none';
this.elements.noResults.style.display = 'none';
this.updateStats();
return;
}

this.elements.emptyState.style.display = 'none';
this.elements.treeList.style.display = 'block';
this.elements.noResults.style.display = 'none';

// Фильтрация данных для дерева с учетом регистра
this.filteredTreeData = this.filterTreeData(this.data, this.searchTerm);

if (this.filteredTreeData.length === 0 && this.searchTerm) {
this.elements.noResults.style.display = 'block';
this.elements.treeList.style.display = 'none';
this.updateStats();
return;
}

// Очистка и рендеринг
this.elements.treeList.innerHTML = '';
const dataToRender = this.searchTerm ? this.filteredTreeData : this.data;

dataToRender.forEach(node => {
this.renderTreeNode(node, this.elements.treeList, 0);
});

this.updateStats();
this.restoreExpandedState();
}

renderListView() {
if (!this.flatList || this.flatList.length === 0) {
this.elements.listEmptyState.style.display = 'block';
this.elements.dataList.style.display = 'none';
this.elements.listNoResults.style.display = 'none';
this.updateStats();
return;
}

this.elements.listEmptyState.style.display = 'none';
this.elements.dataList.style.display = 'block';
this.elements.listNoResults.style.display = 'none';

// Фильтрация данных для списка с учетом регистра
this.filteredListData = this.filterListData(this.flatList, this.searchTerm);

if (this.filteredListData.length === 0 && this.searchTerm) {
this.elements.listNoResults.style.display = 'block';
this.elements.dataList.style.display = 'none';
this.updateStats();
return;
}

// Очистка и рендеринг
this.elements.dataList.innerHTML = '';
const dataToRender = this.searchTerm ? this.filteredListData : this.flatList;

dataToRender.forEach(item => {
this.renderListItem(item);
});

this.updateStats();
}

filterTreeData(nodes, searchTerm) {
if (!searchTerm) return nodes;

const filtered = [];

nodes.forEach(node => {
// Поиск с учетом регистра
const nodeMatches = node.name.includes(searchTerm);
const children = node.children ? this.filterTreeData(node.children, searchTerm) : [];

if (nodeMatches || children.length > 0) {
const filteredNode = { ...node };
if (children.length > 0) {
filteredNode.children = children;
}
filtered.push(filteredNode);

if (searchTerm && (nodeMatches || children.length > 0)) {
this.expandedNodes.add(node.id);
}
}
});

return filtered;
}

filterListData(items, searchTerm) {
if (!searchTerm) return items;

return items.filter(item =>
item.name.includes(searchTerm) ||
(item.node_id && item.node_id.includes(searchTerm))
);
}

countTreeItems(nodes) {
let count = 0;
nodes.forEach(node => {
count++;
if (node.children) {
count += this.countTreeItems(node.children);
}
});
return count;
}

updateStats() {
let total, found;

if (this.currentView === 'tree') {
total = this.countTreeItems(this.data);
found = this.searchTerm ? this.countTreeItems(this.filteredTreeData) : total;
} else {
total = this.flatList.length;
found = this.searchTerm ? this.filteredListData.length : total;
}

this.elements.totalCount.textContent = total;
this.elements.foundCount.textContent = found;
}

updateLastUpdate(source = 'JS данные') {
const now = new Date();
const dateStr = now.toLocaleDateString('ru-RU');
const timeStr = now.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
this.elements.lastUpdate.textContent = `${dateStr} ${timeStr} (${source})`;
}

renderTreeNode(node, parentElement, depth) {
const li = document.createElement('li');
li.className = 'tree-node';
li.dataset.nodeId = node.id;
li.dataset.level = depth;

const hasChildren = node.children && node.children.length > 0;

const nodeContent = document.createElement('div');
nodeContent.className = `node-content level-${depth}`;

// Кнопка раскрытия/скрытия
const toggleBtn = document.createElement('div');
toggleBtn.className = 'toggle-btn';

if (hasChildren) {
toggleBtn.classList.add('has-children');
if (this.expandedNodes.has(node.id) || this.searchTerm) {
toggleBtn.classList.add('expanded');
}

toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleNode(node.id, toggleBtn, li);
});
} else {
toggleBtn.classList.add('no-children');
}

nodeContent.appendChild(toggleBtn);

// Название узла с подсветкой поиска (сохраняем регистр)
const nameDiv = document.createElement('div');
nameDiv.className = 'node-name';
if (this.searchTerm) {
nameDiv.innerHTML = this.highlightText(node.name, this.searchTerm);
} else {
nameDiv.textContent = node.name;
}
nodeContent.appendChild(nameDiv);

// Обработчик клика по всей строке для раскрытия/закрытия
nodeContent.addEventListener('click', (e) => {
if (hasChildren) {
this.toggleNode(node.id, toggleBtn, li);
}
});

li.appendChild(nodeContent);

// Дочерние элементы
if (hasChildren) {
const childrenContainer = document.createElement('ul');
childrenContainer.className = 'node-children';
childrenContainer.id = `children-${node.id}`;

if (this.expandedNodes.has(node.id) || this.searchTerm) {
childrenContainer.classList.add('visible');
}

node.children.forEach(child => {
this.renderTreeNode(child, childrenContainer, depth + 1);
});

li.appendChild(childrenContainer);
}

parentElement.appendChild(li);
}

renderListItem(item) {
const li = document.createElement('li');
li.className = 'list-item';

// Название с подсветкой поиска (сохраняем регистр)
const nameDiv = document.createElement('div');
nameDiv.className = 'list-name';
if (this.searchTerm) {
nameDiv.innerHTML = this.highlightText(item.name, this.searchTerm);
} else {
nameDiv.textContent = item.name;
}

// ID элемента
const idDiv = document.createElement('div');
idDiv.className = 'list-id';
idDiv.textContent = item.node_id || item.id;

li.appendChild(nameDiv);
li.appendChild(idDiv);

this.elements.dataList.appendChild(li);
}

highlightText(text, searchTerm) {
if (!searchTerm) return text;

// Поиск с учетом регистра
const regex = new RegExp(`(${this.escapeRegExp(searchTerm)})`, 'g');
return text.replace(regex, '<span class="search-highlight">$1</span>');
}

escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

toggleNode(nodeId, toggleBtn, liElement) {
const childrenContainer = liElement.querySelector('.node-children');
if (!childrenContainer) return;

const isExpanded = childrenContainer.classList.contains('visible');

if (isExpanded) {
childrenContainer.classList.remove('visible');
toggleBtn.classList.remove('expanded');
this.expandedNodes.delete(nodeId);
} else {
childrenContainer.classList.add('visible');
toggleBtn.classList.add('expanded');
this.expandedNodes.add(nodeId);
}
}

expandAll() {
if (this.currentView !== 'tree') return;

document.querySelectorAll('.tree-node').forEach(li => {
const toggleBtn = li.querySelector('.toggle-btn.has-children');
const childrenContainer = li.querySelector('.node-children');

if (toggleBtn && childrenContainer) {
childrenContainer.classList.add('visible');
toggleBtn.classList.add('expanded');
const nodeId = li.dataset.nodeId;
this.expandedNodes.add(nodeId);
}
});
}

collapseAll() {
if (this.currentView !== 'tree') return;

document.querySelectorAll('.tree-node').forEach(li => {
const toggleBtn = li.querySelector('.toggle-btn.has-children');
const childrenContainer = li.querySelector('.node-children');

if (toggleBtn && childrenContainer) {
childrenContainer.classList.remove('visible');
toggleBtn.classList.remove('expanded');
const nodeId = li.dataset.nodeId;
this.expandedNodes.delete(nodeId);
}
});
}

restoreExpandedState() {
document.querySelectorAll('.tree-node').forEach(li => {
const nodeId = li.dataset.nodeId;
const toggleBtn = li.querySelector('.toggle-btn.has-children');
const childrenContainer = li.querySelector('.node-children');

if (toggleBtn && childrenContainer && this.expandedNodes.has(nodeId)) {
childrenContainer.classList.add('visible');
toggleBtn.classList.add('expanded');
}
});
}

reset() {
this.searchTerm = '';
this.elements.searchInput.value = '';
this.expandedNodes.clear();
this.renderCurrentView();
}

exportToExcel() {
// Экспорт всех конечных элементов (только для "Литологического списка")
const dataToExport = this.flatList.map(item => ({
'node_id': item.node_id || item.id,
'node_name': item.node_name || item.name,
'lcode_speck': item.lcode_speck || ''
}));

if (dataToExport.length === 0) {
alert('Нет данных для экспорта');
return;
}

this.generateExcelFile(dataToExport, 'lithology_export');
}

exportListToExcel() {
// Экспорт только отфильтрованного списка
const dataToExport = (this.searchTerm ? this.filteredListData : this.flatList).map(item => ({
'node_id': item.node_id || item.id,
'node_name': item.node_name || item.name,
'lcode_speck': item.lcode_speck || ''
}));

if (dataToExport.length === 0) {
alert('Нет данных для экспорта');
return;
}

this.generateExcelFile(dataToExport, 'lithology_list_export');
}

generateExcelFile(data, filename) {
// Создаем CSV контент
let csvContent = 'data:text/csv;charset=utf-8,\uFEFF';

// Заголовки - только три колонки
const headers = ['node_id', 'node_name', 'lcode_speck'];
csvContent += headers.join(';') + '\r\n';

// Данные
data.forEach(item => {
const row = headers.map(header => {
let value = item[header] || '';
// Экранируем кавычки и точки с запятой
if (typeof value === 'string') {
value = value.replace(/"/g, '""');
if (value.includes(';') || value.includes('"') || value.includes('\n')) {
value = '"' + value + '"';
}
}
return value;
}).join(';');
csvContent += row + '\r\n';
});

// Создаем ссылку для скачивания
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', `${filename}_${new Date().toISOString().slice(0,10)}.csv`);

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}

// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', () => {
window.lithologyTree = new LithologyTree();
});
</script>
---
title: "Литология"
date: 2024-01-01
---

<!-- Начало компонента литологии -->
<div id="geo-lithology-app" class="geo-lithology-container">

<style>
.geo-lithology-container {
width: 100%;
max-width: 1400px;
margin: 2rem auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
border: 1px solid #e0e0e0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.geo-lithology-controls {
padding: 16px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e0e0e0;
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}

.geo-lithology-search-container {
position: relative;
flex: 1;
min-width: 300px;
}

.geo-lithology-search-input {
width: 100%;
padding: 10px 16px 10px 40px;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 14px;
background: white;
color: #333;
transition: all 0.2s;
}

.geo-lithology-search-input:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74,144,226,0.1);
}

.geo-lithology-search-icon {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #666;
font-size: 14px;
font-weight: bold;
}

.geo-lithology-view-buttons {
display: flex;
border: 1px solid #d0d0d0;
border-radius: 6px;
overflow: hidden;
}

.geo-lithology-view-btn {
padding: 8px 16px;
background: white;
border: none;
cursor: pointer;
font-size: 14px;
color: #666;
transition: all 0.2s;
}

.geo-lithology-view-btn:hover {
background: #f0f0f0;
}

.geo-lithology-view-btn.active {
background: #4a90e2;
color: white;
}

.geo-lithology-action-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}

.geo-lithology-btn {
padding: 8px 16px;
border: 1px solid #d0d0d0;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
background: white;
color: #333;
transition: all 0.2s;
}

.geo-lithology-btn:hover {
background: #f5f5f5;
border-color: #b0b0b0;
}

.geo-lithology-btn-primary {
background: #4a90e2;
color: white;
border-color: #4a90e2;
}

.geo-lithology-btn-primary:hover {
background: #3a80d2;
border-color: #3a80d2;
}

.geo-lithology-tree-container {
padding: 0;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
background: white;
}

.geo-lithology-tree-list {
list-style: none;
padding: 0;
margin: 0;
}

.geo-lithology-tree-node {
position: relative;
}

.geo-lithology-node-content {
display: flex;
align-items: center;
padding: 12px 24px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
}

.geo-lithology-node-content:hover {
background: #f9f9f9;
}

.geo-lithology-node-content.level-0 { padding-left: 20px; }
.geo-lithology-node-content.level-1 { padding-left: 40px; }
.geo-lithology-node-content.level-2 { padding-left: 60px; }
.geo-lithology-node-content.level-3 { padding-left: 80px; }
.geo-lithology-node-content.level-4 { padding-left: 100px; }

.geo-lithology-toggle-btn {
width: 20px;
height: 20px;
border-radius: 3px;
border: 1px solid #b0b0b0;
background: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
flex-shrink: 0;
font-weight: 600;
color: #666;
cursor: pointer;
font-size: 14px;
}

.geo-lithology-toggle-btn:hover {
background: #f0f0f0;
}

.geo-lithology-node-name {
font-size: 14px;
color: #333;
flex: 1;
}

.geo-lithology-node-level {
font-size: 11px;
font-weight: 500;
color: #666;
background: #f0f0f0;
padding: 3px 8px;
border-radius: 10px;
text-transform: uppercase;
margin-left: 12px;
}

.geo-lithology-node-children {
display: none;
}

.geo-lithology-node-children.visible {
display: block;
}

.geo-lithology-empty-state,
.geo-lithology-no-results {
text-align: center;
padding: 60px 32px;
color: #666;
}

.geo-lithology-no-results {
display: none;
}

.geo-lithology-stats-bar {
padding: 12px 24px;
background: white;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #666;
}

.geo-lithology-loading {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.95);
z-index: 100;
justify-content: center;
align-items: center;
flex-direction: column;
}

.geo-lithology-loading-spinner {
width: 32px;
height: 32px;
border: 3px solid #f0f0f0;
border-top: 3px solid #4a90e2;
border-radius: 50%;
animation: geo-lithology-spin 1s linear infinite;
margin-bottom: 12px;
}

@keyframes geo-lithology-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.geo-lithology-search-highlight {
background-color: #fff3cd;
color: #856404;
padding: 1px 3px;
border-radius: 3px;
font-weight: 500;
}

/* Стиль для списка (без дерева) */
.geo-lithology-list-view .geo-lithology-node-content {
padding-left: 20px !important;
cursor: default;
}

.geo-lithology-list-view .geo-lithology-toggle-btn {
display: none;
}

.geo-lithology-list-view .geo-lithology-node-children {
display: none !important;
}

.geo-lithology-list-view .geo-lithology-tree-node {
display: none;
}

.geo-lithology-list-view .geo-lithology-tree-node.leaf-node {
display: block;
}

@media (max-width: 768px) {
.geo-lithology-controls {
flex-direction: column;
align-items: stretch;
}

.geo-lithology-search-container {
min-width: unset;
}

.geo-lithology-action-buttons {
justify-content: flex-start;
}

.geo-lithology-view-buttons {
width: 100%;
}

.geo-lithology-view-btn {
flex: 1;
text-align: center;
}
}
</style>

<!-- HTML структура -->
<div class="geo-lithology-controls">
<div class="geo-lithology-search-container">
<span class="geo-lithology-search-icon">⌕</span>
<input type="text" id="geo-lithology-search" class="geo-lithology-search-input"
placeholder="Поиск по названию литологии...">
</div>

<div class="geo-lithology-view-buttons">
<button class="geo-lithology-view-btn active" data-view="tree">Дерево</button>
<button class="geo-lithology-view-btn" data-view="list">Список</button>
</div>

<div class="geo-lithology-action-buttons">
<button id="geo-lithology-expand-all" class="geo-lithology-btn">
<span>+</span>
Развернуть
</button>
<button id="geo-lithology-collapse-all" class="geo-lithology-btn">
<span>−</span>
Свернуть
</button>
<button id="geo-lithology-export-excel" class="geo-lithology-btn geo-lithology-btn-primary">
<span>📊</span>
Excel
</button>
</div>
</div>

<div class="geo-lithology-tree-container">
<div id="geo-lithology-loading" class="geo-lithology-loading">
<div class="geo-lithology-loading-spinner"></div>
<div>Загрузка данных...</div>
</div>

<div id="geo-lithology-empty" class="geo-lithology-empty-state" style="display: none;">
<h3>Загрузка данных...</h3>
</div>

<div id="geo-lithology-no-results" class="geo-lithology-no-results">
<h3>Ничего не найдено</h3>
<p>Попробуйте изменить поисковый запрос</p>
</div>

<ul id="geo-lithology-tree" class="geo-lithology-tree-list" style="display: none;"></ul>
</div>

<div class="geo-lithology-stats-bar">
<div id="geo-lithology-stats">Загрузка...</div>
<div>
<span>Найдено: <span id="geo-lithology-count">0</span> элементов</span>
</div>
</div>

<script>
// Компонент литологии
(function() {
'use strict';

const LithologyTree = {
data: null,
elements: {},
searchResults: new Set(),
allNodes: new Map(),
leafNodes: new Set(),
currentView: 'tree', // tree или list

init: function() {
this.cacheElements();
this.bindEvents();
this.showLoading();
this.loadData();
},

cacheElements: function() {
this.elements = {
tree: document.getElementById('geo-lithology-tree'),
search: document.getElementById('geo-lithology-search'),
expandBtn: document.getElementById('geo-lithology-expand-all'),
collapseBtn: document.getElementById('geo-lithology-collapse-all'),
exportBtn: document.getElementById('geo-lithology-export-excel'),
empty: document.getElementById('geo-lithology-empty'),
noResults: document.getElementById('geo-lithology-no-results'),
stats: document.getElementById('geo-lithology-stats'),
count: document.getElementById('geo-lithology-count'),
loading: document.getElementById('geo-lithology-loading'),
viewBtns: document.querySelectorAll('.geo-lithology-view-btn')
};
},

bindEvents: function() {
let searchTimeout;
this.elements.search.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.handleSearch(e.target.value);
}, 300);
});

this.elements.expandBtn.addEventListener('click', () => {
this.expandAll();
});

this.elements.collapseBtn.addEventListener('click', () => {
this.collapseAll();
});

this.elements.exportBtn.addEventListener('click', () => {
this.exportToExcel();
});

// Переключение вида
this.elements.viewBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const view = e.target.dataset.view;
this.switchView(view);
});
});
},

showLoading: function() {
this.elements.loading.style.display = 'flex';
this.elements.empty.style.display = 'none';
},

hideLoading: function() {
this.elements.loading.style.display = 'none';
},

checkDataVariable: function() {
// Проверяем разные возможные имена переменных
if (window.lithologyData && Array.isArray(window.lithologyData)) {
this.data = window.lithologyData;
this.render();
return true;
}

if (window.geoLithologyData && Array.isArray(window.geoLithologyData)) {
this.data = window.geoLithologyData;
this.render();
return true;
}

if (window.LithologyData && Array.isArray(window.LithologyData)) {
this.data = window.LithologyData;
this.render();
return true;
}

return false;
},

loadData: function() {
// Сначала проверяем, может данные уже загружены
if (this.checkDataVariable()) {
return;
}

// Список всех возможных путей к JS файлу
const possiblePaths = [
'./LithologyData.js',
'LithologyData.js',
'./data/LithologyData.js',
'./js/LithologyData.js',
'../LithologyData.js',
'../../LithologyData.js',
'/LithologyData.js',
'/data/LithologyData.js',
'/js/LithologyData.js'
];

// Пытаемся загрузить данные из всех возможных мест
this.tryLoadFromPaths(possiblePaths, 0);
},

tryLoadFromPaths: function(paths, index) {
if (index >= paths.length) {
this.showDataError();
return;
}

const path = paths[index];
const script = document.createElement('script');
script.src = path;

const self = this;

script.onload = function() {
setTimeout(function() {
if (self.checkDataVariable()) {
console.log('Данные успешно загружены из', path);
} else {
self.tryLoadFromPaths(paths, index + 1);
}
}, 100);
};

script.onerror = function() {
self.tryLoadFromPaths(paths, index + 1);
};

document.head.appendChild(script);
},

showDataError: function() {
this.hideLoading();
this.elements.empty.style.display = 'block';
this.elements.empty.innerHTML = `
<h3>Не удалось загрузить данные</h3>
<p>Файл LithologyData.js не найден</p>
`;
},

render: function() {
if (!this.data || !Array.isArray(this.data)) {
this.elements.empty.innerHTML = '<p>Нет данных для отображения</p>';
this.elements.empty.style.display = 'block';
this.hideLoading();
return;
}

this.hideLoading();
this.elements.empty.style.display = 'none';
this.elements.tree.style.display = 'block';
this.elements.tree.innerHTML = '';

// Строим карту всех узлов
this.buildNodeMap(this.data);
// Собираем листовые узлы (без детей)
this.collectLeafNodes(this.data);

// Рендерим дерево
this.renderNodes(this.data, this.elements.tree, 0);
this.updateStats();

// Применяем текущий вид
this.applyView();
},

buildNodeMap: function(nodes, parentId = null) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
this.allNodes.set(nodeId, {
...node,
parentId: parentId
});

if (node.children && node.children.length > 0) {
this.buildNodeMap(node.children, nodeId);
}
});
},

collectLeafNodes: function(nodes) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
const hasChildren = node.children && node.children.length > 0;

if (!hasChildren) {
this.leafNodes.add(nodeId);
} else {
this.collectLeafNodes(node.children);
}
});
},

renderNodes: function(nodes, parentElement, depth) {
nodes.forEach(node => {
const nodeId = node.id || node.node_id;
const hasChildren = node.children && node.children.length > 0;

const li = document.createElement('li');
li.className = 'geo-lithology-tree-node';
li.dataset.id = nodeId;
li.dataset.depth = depth;

if (!hasChildren) {
li.classList.add('leaf-node');
}

const content = document.createElement('div');
content.className = `geo-lithology-node-content level-${depth}`;

const toggleBtn = document.createElement('div');
toggleBtn.className = 'geo-lithology-toggle-btn';
toggleBtn.textContent = hasChildren ? '+' : ''; // Убрали точки для листовых узлов

if (hasChildren) {
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleNode(nodeId);
});
}

const nameSpan = document.createElement('div');
nameSpan.className = 'geo-lithology-node-name';
nameSpan.textContent = node.name || node.node_name;

const levelSpan = document.createElement('div');
levelSpan.className = 'geo-lithology-node-level';
levelSpan.textContent = `Ур. ${node.level || 1}`;

content.appendChild(toggleBtn);
content.appendChild(nameSpan);
content.appendChild(levelSpan);

if (hasChildren) {
content.addEventListener('click', () => {
this.toggleNode(nodeId);
});
}

li.appendChild(content);

if (hasChildren) {
const childrenList = document.createElement('ul');
childrenList.className = 'geo-lithology-node-children';
childrenList.id = `children-${nodeId}`;
li.appendChild(childrenList);

node._childrenList = childrenList;
node._depth = depth + 1;
}

parentElement.appendChild(li);
});
},

toggleNode: function(nodeId) {
const childrenList = document.getElementById(`children-${nodeId}`);
const nodeElement = document.querySelector(`[data-id="${nodeId}"]`);

if (!childrenList || !nodeElement) return;

const toggleBtn = nodeElement.querySelector('.geo-lithology-toggle-btn');

if (childrenList.classList.contains('visible')) {
childrenList.classList.remove('visible');
toggleBtn.textContent = '+';
} else {
if (childrenList.children.length === 0) {
const node = this.allNodes.get(nodeId);
if (node && node.children) {
this.renderNodes(node.children, childrenList, node._depth);
}
}
childrenList.classList.add('visible');
toggleBtn.textContent = '−';
}
},

switchView: function(view) {
this.currentView = view;

// Обновляем активные кнопки
this.elements.viewBtns.forEach(btn => {
if (btn.dataset.view === view) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});

// Применяем вид
this.applyView();
},

applyView: function() {
if (this.currentView === 'tree') {
this.elements.tree.classList.remove('geo-lithology-list-view');
} else if (this.currentView === 'list') {
this.elements.tree.classList.add('geo-lithology-list-view');
}
},

handleSearch: function(searchTerm) {
const term = searchTerm.trim();

if (term === '') {
this.clearSearch();
this.elements.noResults.style.display = 'none';
return;
}

this.searchResults.clear();

// Ищем совпадения
this.allNodes.forEach((node, nodeId) => {
const nodeName = node.name || node.node_name || '';
if (nodeName.toLowerCase().includes(term.toLowerCase())) {
this.searchResults.add(nodeId);
}
});

if (this.searchResults.size === 0) {
this.elements.noResults.style.display = 'block';
this.elements.tree.style.display = 'none';
} else {
this.elements.noResults.style.display = 'none';
this.elements.tree.style.display = 'block';
this.highlightSearchResults(term);
}

this.updateStats();
},

highlightSearchResults: function(searchTerm) {
const allNodes = document.querySelectorAll('.geo-lithology-tree-node');

// Сначала скрываем все
allNodes.forEach(node => {
node.style.display = 'none';
});

// Показываем найденные узлы и их родителей
const nodesToShow = new Set();

const getAllParents = (nodeId) => {
const parents = new Set();
let currentId = nodeId;

while (currentId) {
const node = this.allNodes.get(currentId);
if (node && node.parentId) {
parents.add(node.parentId);
currentId = node.parentId;
} else {
break;
}
}
return parents;
};

// Добавляем найденные узлы и их родителей
this.searchResults.forEach(nodeId => {
nodesToShow.add(nodeId);
const parents = getAllParents(nodeId);
parents.forEach(parentId => nodesToShow.add(parentId));
});

// Показываем нужные узлы
nodesToShow.forEach(nodeId => {
const nodeElement = document.querySelector(`[data-id="${nodeId}"]`);
if (nodeElement) {
nodeElement.style.display = '';

// Раскрываем родителей
if (this.searchResults.has(nodeId)) {
this.expandAllParents(nodeId);
}

// Подсвечиваем текст
if (this.searchResults.has(nodeId)) {
const nameElement = nodeElement.querySelector('.geo-lithology-node-name');
const originalText = nameElement.textContent;
const regex = new RegExp(`(${searchTerm})`, 'gi');
nameElement.innerHTML = originalText.replace(regex,
'<span class="geo-lithology-search-highlight">$1</span>');
}
}
});
},

expandAllParents: function(nodeId) {
let currentId = nodeId;

while (currentId) {
const node = this.allNodes.get(currentId);
if (node && node.parentId) {
const parentElement = document.querySelector(`[data-id="${node.parentId}"]`);
if (parentElement) {
const toggleBtn = parentElement.querySelector('.geo-lithology-toggle-btn');
if (toggleBtn && toggleBtn.textContent === '+') {
this.toggleNode(node.parentId);
}
}
currentId = node.parentId;
} else {
break;
}
}
},

clearSearch: function() {
const allNodes = document.querySelectorAll('.geo-lithology-tree-node');
allNodes.forEach(node => {
node.style.display = '';
const nameElement = node.querySelector('.geo-lithology-node-name');
if (nameElement.innerHTML !== nameElement.textContent) {
nameElement.innerHTML = nameElement.textContent;
}
});
this.searchResults.clear();
this.updateStats();
},

expandAll: function() {
const toggleButtons = document.querySelectorAll('.geo-lithology-toggle-btn');
toggleButtons.forEach(btn => {
if (btn.textContent === '+') {
const nodeElement = btn.closest('.geo-lithology-tree-node');
const nodeId = nodeElement.dataset.id;
this.toggleNode(nodeId);
}
});
},

collapseAll: function() {
const toggleButtons = document.querySelectorAll('.geo-lithology-toggle-btn');
toggleButtons.forEach(btn => {
if (btn.textContent === '−') {
const nodeElement = btn.closest('.geo-lithology-tree-node');
const nodeId = nodeElement.dataset.id;
this.toggleNode(nodeId);
}
});
},

updateStats: function() {
if (!this.data) return;

let totalCount = 0;
const countNodes = (nodes) => {
nodes.forEach(node => {
totalCount++;
if (node.children) {
countNodes(node.children);
}
});
};

countNodes(this.data);
this.elements.count.textContent = totalCount;

let statsText = `Всего элементов: ${totalCount}`;
if (this.elements.search.value.trim()) {
statsText += ` | Найдено: ${this.searchResults.size}`;
}

this.elements.stats.textContent = statsText;
},

exportToExcel: function() {
if (!this.data) return;

// Собираем данные для экспорта
const exportData = [];

if (this.currentView === 'tree' || this.elements.search.value.trim() === '') {
// Экспорт всей структуры
this.collectExportData(this.data, exportData, 0);
} else {
// Экспорт только результатов поиска
this.searchResults.forEach(nodeId => {
const node = this.allNodes.get(nodeId);
if (node) {
exportData.push({
name: node.name || node.node_name,
level: node.level || 1,
id: node.id || node.node_id,
path: this.getNodePath(nodeId)
});
}
});
}

if (exportData.length === 0) {
alert('Нет данных для экспорта');
return;
}

// Создаем CSV содержимое
let csvContent = "data:text/csv;charset=utf-8,";

// Заголовки
csvContent += "ID;Название;Уровень;Полный путь\n";

// Данные
exportData.forEach(item => {
csvContent += `${item.id};"${item.name}";${item.level};"${item.path}"\n`;
});

// Создаем ссылку для скачивания
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", "литология_" + new Date().toISOString().slice(0,10) + ".csv");
document.body.appendChild(link);

// Скачиваем
link.click();
document.body.removeChild(link);
},

collectExportData: function(nodes, exportArray, depth) {
nodes.forEach(node => {
exportArray.push({
name: node.name || node.node_name,
level: node.level || 1,
id: node.id || node.node_id,
path: this.getNodePath(node.id || node.node_id)
});

if (node.children && node.children.length > 0) {
this.collectExportData(node.children, exportArray, depth + 1);
}
});
},

getNodePath: function(nodeId) {
const path = [];
let currentId = nodeId;

while (currentId) {
const node = this.allNodes.get(currentId);
if (node) {
path.unshift(node.name || node.node_name);
currentId = node.parentId;
} else {
break;
}
}

return path.join(' → ');
}
};

// Инициализация при загрузке страницы
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
LithologyTree.init();
});
} else {
LithologyTree.init();
}

// Экспорт для отладки
window.LithologyTree = LithologyTree;

})();
</script>
</div>
<!-- Конец компонента литологии -->

Добавлено (2026-01-20, 09:40)
---------------------------------------------
---
title: "Литостратиграфическая классификация"
date: 2024-01-01
---

<!-- Начало компонента литологии -->
<div id="geo-lithology-app" class="geo-lithology-container">

<style>
.geo-lithology-container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
overflow: hidden;
border: 1px solid #e2e8f0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.geo-lithology-controls {
padding: 20px 32px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
display: grid;
grid-template-columns: 1fr auto;
gap: 20px;
align-items: center;
}

.geo-lithology-search-container {
position: relative;
}

.geo-lithology-search-input {
width: 100%;
padding: 12px 16px 12px 44px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
background: white;
color: #1e293b;
transition: all 0.2s;
}

.geo-lithology-search-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.geo-lithology-search-input::placeholder {
color: #94a3b8;
}

.geo-lithology-search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
width: 16px;
height: 16px;
}

.geo-lithology-action-buttons {
display: flex;
gap: 12px;
}

.geo-lithology-btn {
padding: 10px 20px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
background: white;
color: #475569;
}

.geo-lithology-btn:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}

.geo-lithology-btn-primary {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}

.geo-lithology-btn-primary:hover {
background: #2563eb;
border-color: #2563eb;
}

.geo-lithology-btn-export {
background: #8b5cf6;
color: white;
border-color: #8b5cf6;
}

.geo-lithology-btn-export:hover {
background: #7c3aed;
border-color: #7c3aed;
}

.geo-lithology-view-controls {
padding: 16px 32px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
display: flex;
gap: 12px;
align-items: center;
}

.geo-lithology-view-toggle {
display: flex;
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}

.geo-lithology-view-btn {
padding: 8px 16px;
border: none;
background: transparent;
color: #64748b;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}

.geo-lithology-view-btn:hover {
background: #f1f5f9;
}

.geo-lithology-view-btn.active {
background: #3b82f6;
color: white;
}

.geo-lithology-tree-container {
padding: 0;
min-height: 500px;
max-height: 600px;
overflow-y: auto;
background: white;
position: relative;
}

.geo-lithology-list-container {
padding: 0;
min-height: 500px;
max-height: 600px;
overflow-y: auto;
background: white;
position: relative;
display: none;
}

.geo-lithology-tree-list {
list-style: none;
padding: 0;
margin: 0;
}

.geo-lithology-data-list {
list-style: none;
padding: 0;
margin: 0;
}

.geo-lithology-tree-node {
position: relative;
}

.geo-lithology-list-item {
display: flex;
align-items: center;
padding: 12px 32px;
border-bottom: 1px solid #f1f5f9;
min-height: 48px;
transition: background 0.2s;
position: relative;
}

.geo-lithology-list-item:hover {
background: #f8fafc;
}

.geo-lithology-node-content {
display: flex;
align-items: center;
padding: 12px 32px 12px 20px;
border-bottom: 1px solid #f1f5f9;
min-height: 48px;
transition: background 0.2s;
position: relative;
cursor: pointer;
width: 100%;
}

.geo-lithology-node-content:hover {
background: #f8fafc;
}

.geo-lithology-node-content.level-0 { padding-left: 20px; }
.geo-lithology-node-content.level-1 { padding-left: 40px; }
.geo-lithology-node-content.level-2 { padding-left: 60px; }
.geo-lithology-node-content.level-3 { padding-left: 80px; }
.geo-lithology-node-content.level-4 { padding-left: 100px; }
.geo-lithology-node-content.level-5 { padding-left: 120px; }
.geo-lithology-node-content.level-6 { padding-left: 140px; }

.geo-lithology-toggle-btn {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid #cbd5e1;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
margin-right: 12px;
flex-shrink: 0;
color: #475569;
transition: all 0.2s;
cursor: pointer;
user-select: none;
}

.geo-lithology-toggle-btn:hover {
border-color: #94a3b8;
background: #f1f5f9;
}

.geo-lithology-toggle-btn.has-children::before {
content: '+';
}

.geo-lithology-toggle-btn.has-children.expanded::before {
content: '−';
}

.geo-lithology-toggle-btn.no-children {
border: none;
background: transparent;
cursor: default;
width: 20px;
}

.geo-lithology-toggle-btn.no-children::before {
content: '';
}

.geo-lithology-node-name {
font-size: 14px;
color: #1e293b;
flex: 1;
font-weight: 400;
}

.geo-lithology-list-name {
font-size: 14px;
color: #1e293b;
flex: 1;
font-weight: 400;
}

.geo-lithology-list-id {
font-size: 12px;
color: #64748b;
font-family: monospace;
background: #f1f5f9;
padding: 4px 8px;
border-radius: 6px;
margin-left: 12px;
flex-shrink: 0;
}

.geo-lithology-node-children {
display: none;
}

.geo-lithology-node-children.visible {
display: block;
}

.geo-lithology-empty-state {
text-align: center;
padding: 80px 32px;
color: #64748b;
}

.geo-lithology-empty-icon {
font-size: 48px;
margin-bottom: 20px;
opacity: 0.5;
}

.geo-lithology-empty-state h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: #475569;
}

.geo-lithology-empty-state p {
font-size: 14px;
max-width: 400px;
margin: 0 auto 24px;
line-height: 1.5;
}

.geo-lithology-no-results {
text-align: center;
padding: 60px 32px;
color: #64748b;
display: none;
}

.geo-lithology-stats-bar {
padding: 16px 32px;
background: white;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #64748b;
}

.geo-lithology-stats-info {
display: flex;
gap: 24px;
}

.geo-lithology-stat-item {
display: flex;
align-items: center;
gap: 6px;
}

.geo-lithology-stat-value {
font-weight: 600;
color: #1e293b;
}

.geo-lithology-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.95);
z-index: 100;
justify-content: center;
align-items: center;
flex-direction: column;
border-radius: 12px;
display: flex;
}

.geo-lithology-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f1f5f9;
border-top: 3px solid #3b82f6;
border-radius: 50%;
animation: geo-lithology-spin 1s linear infinite;
margin-bottom: 16px;
}

@keyframes geo-lithology-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.geo-lithology-search-highlight {
background-color: #fef3c7;
color: #92400e;
padding: 2px 4px;
border-radius: 4px;
font-weight: 500;
}

.geo-lithology-export-btn-list {
display: none;
}

@media (max-width: 768px) {
.geo-lithology-controls {
grid-template-columns: 1fr;
}

.geo-lithology-action-buttons {
justify-content: flex-start;
flex-wrap: wrap;
}

.geo-lithology-node-content, .geo-lithology-list-item {
padding: 12px 16px;
flex-wrap: wrap;
gap: 8px;
}

.geo-lithology-list-id {
order: 2;
margin-left: auto;
}
}
</style>

<!-- HTML структура -->
<div class="geo-lithology-controls">
<div class="geo-lithology-search-container">
<svg class="geo-lithology-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" class="geo-lithology-search-input"
placeholder="Поиск по названию..."
id="geo-lithology-search-input">
</div>

<div class="geo-lithology-action-buttons">
<button class="geo-lithology-btn" id="geo-lithology-expand-all-btn">
<span>+</span> Развернуть всё
</button>
<button class="geo-lithology-btn" id="geo-lithology-collapse-all-btn">
<span>−</span> Свернуть всё
</button>
<button class="geo-lithology-btn geo-lithology-btn-export" id="geo-lithology-export-btn">
<span>📊</span> Экспорт в Excel
</button>
<button class="geo-lithology-btn" id="geo-lithology-reset-btn">
<span>↺</span> Сбросить
</button>
</div>
</div>

<div class="geo-lithology-view-controls">
<div class="geo-lithology-view-toggle">
<button class="geo-lithology-view-btn active" data-view="tree">
Вид по группам
</button>
<button class="geo-lithology-view-btn" data-view="list">
Литологический список
</button>
</div>
<div style="flex: 1;"></div>
<div class="geo-lithology-stats-info">
<div class="geo-lithology-stat-item">
<span>Всего:</span>
<span class="geo-lithology-stat-value" id="geo-lithology-total-count">0</span>
</div>
<div class="geo-lithology-stat-item">
<span>Найдено:</span>
<span class="geo-lithology-stat-value" id="geo-lithology-found-count">0</span>
</div>
</div>
</div>

<!-- Древовидное представление -->
<div class="geo-lithology-tree-container" id="geo-lithology-tree-view">
<div id="geo-lithology-empty-state" class="geo-lithology-empty-state">
<div class="geo-lithology-empty-icon">📊</div>
<h3>Нет данных для отображения</h3>
<p>Загрузка данных...</p>
</div>

<div id="geo-lithology-no-results" class="geo-lithology-no-results">
<div class="geo-lithology-empty-icon">🔍</div>
<h3>Ничего не найдено</h3>
<p>Попробуйте изменить поисковый запрос</p>
</div>

<ul id="geo-lithology-tree-list" class="geo-lithology-tree-list" style="display: none;"></ul>

<div id="geo-lithology-loading" class="geo-lithology-loading">
<div class="geo-lithology-loading-spinner"></div>
<div style="font-size: 14px; color: #64748b; font-weight: 500;">Загрузка данных...</div>
</div>
</div>

<!-- Списковое представление -->
<div class="geo-lithology-list-container" id="geo-lithology-list-view">
<div id="geo-lithology-list-empty-state" class="geo-lithology-empty-state" style="display: none;">
<div class="geo-lithology-empty-icon">📊</div>
<h3>Нет данных для отображения</h3>
<p>Загрузка данных...</p>
</div>

<div id="geo-lithology-list-no-results" class="geo-lithology-no-results" style="display: none;">
<div class="geo-lithology-empty-icon">🔍</div>
<h3>Ничего не найдено</h3>
<p>Попробуйте изменить поисковый запрос</p>
</div>

<ul id="geo-lithology-data-list" class="geo-lithology-data-list"></ul>

<div id="geo-lithology-list-loading" class="geo-lithology-loading" style="display: none;">
<div class="geo-lithology-loading-spinner"></div>
<div style="font-size: 14px; color: #64748b; font-weight: 500;">Загрузка данных...</div>
</div>
</div>

<div class="geo-lithology-stats-bar">
<div id="geo-lithology-last-update">—</div>
<div class="geo-lithology-export-btn-list">
<button class="geo-lithology-btn geo-lithology-btn-export" id="geo-lithology-export-list-btn">
<span>📥</span> Экспортировать список в Excel
</button>
</div>
</div>

<script>
(function() {
'use strict';

class GeoLithologyTree {
constructor() {
this.data = [];
this.flatList = [];
this.filteredTreeData = [];
this.filteredListData = [];
this.searchTerm = '';
this.expandedNodes = new Set();
this.currentView = 'tree';
this.isLoading = false;

this.initElements();
this.initEventListeners();
this.loadData();
}

initElements() {
this.elements = {
treeList: document.getElementById('geo-lithology-tree-list'),
dataList: document.getElementById('geo-lithology-data-list'),
searchInput: document.getElementById('geo-lithology-search-input'),
expandAllBtn: document.getElementById('geo-lithology-expand-all-btn'),
collapseAllBtn: document.getElementById('geo-lithology-collapse-all-btn'),
exportBtn: document.getElementById('geo-lithology-export-btn'),
exportListBtn: document.getElementById('geo-lithology-export-list-btn'),
resetBtn: document.getElementById('geo-lithology-reset-btn'),
loading: document.getElementById('geo-lithology-loading'),
listLoading: document.getElementById('geo-lithology-list-loading'),
emptyState: document.getElementById('geo-lithology-empty-state'),
listEmptyState: document.getElementById('geo-lithology-list-empty-state'),
noResults: document.getElementById('geo-lithology-no-results'),
listNoResults: document.getElementById('geo-lithology-list-no-results'),
totalCount: document.getElementById('geo-lithology-total-count'),
foundCount: document.getElementById('geo-lithology-found-count'),
lastUpdate: document.getElementById('geo-lithology-last-update'),
treeView: document.getElementById('geo-lithology-tree-view'),
listView: document.getElementById('geo-lithology-list-view'),
viewBtns: document.querySelectorAll('.geo-lithology-view-btn'),
exportBtnList: document.querySelector('.geo-lithology-export-btn-list')
};
}

initEventListeners() {
// Поиск
let searchTimeout;
this.elements.searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.searchTerm = e.target.value;
this.renderCurrentView();
}, 300);
});

// Кнопки управления
this.elements.expandAllBtn.addEventListener('click', () => this.expandAll());
this.elements.collapseAllBtn.addEventListener('click', () => this.collapseAll());
this.elements.exportBtn.addEventListener('click', () => this.exportToExcel());
this.elements.exportListBtn.addEventListener('click', () => this.exportListToExcel());
this.elements.resetBtn.addEventListener('click', () => this.reset());

// Переключение вида
this.elements.viewBtns.forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
this.switchView(view);
});
});
}

async loadData() {
this.isLoading = true;
this.showLoading();

// Список возможных путей к файлу данных
const possiblePaths = [
'LithologyData.js',
'./LithologyData.js',
'./data/LithologyData.js',
'./js/LithologyData.js',
'../LithologyData.js',
'../../LithologyData.js',
'/LithologyData.js',
'/data/LithologyData.js',
'/js/LithologyData.js'
];

// Пробуем загрузить данные
for (const path of possiblePaths) {
try {
const data = await this.loadScript(path);
if (data) {
this.data = data;
this.prepareFlatList();
this.hideLoading();
this.renderCurrentView();
this.updateLastUpdate(`файл: ${path}`);
return;
}
} catch (error) {
console.log(`Не удалось загрузить: ${path}`);
continue;
}
}

// Если ничего не загрузилось, проверяем глобальную переменную
if (window.lithologyData && Array.isArray(window.lithologyData)) {
this.data = window.lithologyData;
this.prepareFlatList();
this.hideLoading();
this.renderCurrentView();
this.updateLastUpdate('глобальные данные');
return;
}

// Если всё ещё нет данных, показываем ошибку
this.showNoDataError();
}

loadScript(path) {
return new Promise((resolve) => {
// Проверяем, не загружен ли уже файл
if (typeof window.lithologyData !== 'undefined') {
resolve(window.lithologyData);
return;
}

const script = document.createElement('script');
script.src = path;

script.onload = () => {
setTimeout(() => {
if (typeof window.lithologyData !== 'undefined') {
resolve(window.lithologyData);
} else {
resolve(null);
}
}, 100);
};

script.onerror = () => {
resolve(null);
};

document.head.appendChild(script);
});
}

showNoDataError() {
this.hideLoading();
this.elements.emptyState.innerHTML = `
<div class="geo-lithology-empty-icon">⚠️</div>
<h3>Данные не загружены</h3>
<p>Файл LithologyData.js не найден</p>
`;
this.elements.emptyState.style.display = 'block';
}

prepareFlatList() {
this.flatList = [];
this.flattenData(this.data, this.flatList);
}

flattenData(nodes, result) {
nodes.forEach(node => {
// Добавляем только конечные элементы (без детей)
if (!node.children || node.children.length === 0) {
result.push({
id: node.id,
name: node.name,
level: node.level,
node_id: node.node_id,
node_name: node.node_name,
lcode_speck: node.lcode_speck
});
}

if (node.children) {
this.flattenData(node.children, result);
}
});
}

showLoading() {
if (this.currentView === 'tree') {
this.elements.loading.style.display = 'flex';
this.elements.emptyState.style.display = 'none';
} else {
this.elements.listLoading.style.display = 'flex';
this.elements.listEmptyState.style.display = 'none';
}
}

hideLoading() {
this.isLoading = false;
this.elements.loading.style.display = 'none';
this.elements.listLoading.style.display = 'none';
}

switchView(view) {
if (this.currentView === view) return;

this.currentView = view;

// Обновляем активные кнопки
this.elements.viewBtns.forEach(btn => {
if (btn.dataset.view === view) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});

// Показываем/скрываем контейнеры
if (view === 'tree') {
this.elements.treeView.style.display = 'block';
this.elements.listView.style.display = 'none';
this.elements.exportBtnList.style.display = 'none';
} else {
this.elements.treeView.style.display = 'none';
this.elements.listView.style.display = 'block';
this.elements.exportBtnList.style.display = 'block';
}

this.renderCurrentView();
}

renderCurrentView() {
if (this.isLoading) return;

if (this.currentView === 'tree') {
this.renderTreeView();
} else {
this.renderListView();
}
}

renderTreeView() {
if (!this.data || this.data.length === 0) {
this.elements.emptyState.style.display = 'block';
this.elements.treeList.style.display = 'none';
this.elements.noResults.style.display = 'none';
this.updateStats();
return;
}

this.elements.emptyState.style.display = 'none';
this.elements.treeList.style.display = 'block';
this.elements.noResults.style.display = 'none';

// Фильтрация данных для дерева
this.filteredTreeData = this.filterTreeData(this.data, this.searchTerm);

if (this.filteredTreeData.length === 0 && this.searchTerm) {
this.elements.noResults.style.display = 'block';
this.elements.treeList.style.display = 'none';
this.updateStats();
return;
}

// Очистка и рендеринг
this.elements.treeList.innerHTML = '';
const dataToRender = this.searchTerm ? this.filteredTreeData : this.data;

dataToRender.forEach(node => {
this.renderTreeNode(node, this.elements.treeList, 0);
});

this.updateStats();
this.restoreExpandedState();
}

renderListView() {
if (!this.flatList || this.flatList.length === 0) {
this.elements.listEmptyState.style.display = 'block';
this.elements.dataList.style.display = 'none';
this.elements.listNoResults.style.display = 'none';
this.updateStats();
return;
}

this.elements.listEmptyState.style.display = 'none';
this.elements.dataList.style.display = 'block';
this.elements.listNoResults.style.display = 'none';

// Фильтрация данных для списка
this.filteredListData = this.filterListData(this.flatList, this.searchTerm);

if (this.filteredListData.length === 0 && this.searchTerm) {
this.elements.listNoResults.style.display = 'block';
this.elements.dataList.style.display = 'none';
this.updateStats();
return;
}

// Очистка и рендеринг
this.elements.dataList.innerHTML = '';
const dataToRender = this.searchTerm ? this.filteredListData : this.flatList;

dataToRender.forEach(item => {
this.renderListItem(item);
});

this.updateStats();
}

filterTreeData(nodes, searchTerm) {
if (!searchTerm) return nodes;

const filtered = [];

nodes.forEach(node => {
const nodeMatches = node.name && node.name.includes(searchTerm);
const children = node.children ? this.filterTreeData(node.children, searchTerm) : [];

if (nodeMatches || children.length > 0) {
const filteredNode = { ...node };
if (children.length > 0) {
filteredNode.children = children;
}
filtered.push(filteredNode);

if (searchTerm && (nodeMatches || children.length > 0)) {
this.expandedNodes.add(node.id);
}
}
});

return filtered;
}

filterListData(items, searchTerm) {
if (!searchTerm) return items;

return items.filter(item =>
(item.name && item.name.includes(searchTerm)) ||
(item.node_id && item.node_id.includes(searchTerm))
);
}

countTreeItems(nodes) {
let count = 0;
nodes.forEach(node => {
count++;
if (node.children) {
count += this.countTreeItems(node.children);
}
});
return count;
}

updateStats() {
let total, found;

if (this.currentView === 'tree') {
total = this.countTreeItems(this.data);
found = this.searchTerm ? this.countTreeItems(this.filteredTreeData) : total;
} else {
total = this.flatList.length;
found = this.searchTerm ? this.filteredListData.length : total;
}

this.elements.totalCount.textContent = total;
this.elements.foundCount.textContent = found;
}

updateLastUpdate(source = 'данные') {
const now = new Date();
const dateStr = now.toLocaleDateString('ru-RU');
const timeStr = now.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
this.elements.lastUpdate.textContent = `Обновлено: ${dateStr} ${timeStr} (${source})`;
}

renderTreeNode(node, parentElement, depth) {
const li = document.createElement('li');
li.className = 'geo-lithology-tree-node';
li.dataset.nodeId = node.id;
li.dataset.level = depth;

const hasChildren = node.children && node.children.length > 0;

const nodeContent = document.createElement('div');
nodeContent.className = `geo-lithology-node-content level-${depth}`;

// Кнопка раскрытия/скрытия
const toggleBtn = document.createElement('div');
toggleBtn.className = 'geo-lithology-toggle-btn';

if (hasChildren) {
toggleBtn.classList.add('has-children');
if (this.expandedNodes.has(node.id) || this.searchTerm) {
toggleBtn.classList.add('expanded');
}

toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleNode(node.id, toggleBtn, li);
});
} else {
toggleBtn.classList.add('no-children');
}

nodeContent.appendChild(toggleBtn);

// Название узла с подсветкой поиска
const nameDiv = document.createElement('div');
nameDiv.className = 'geo-lithology-node-name';
if (this.searchTerm && node.name && node.name.includes(this.searchTerm)) {
nameDiv.innerHTML = this.highlightText(node.name, this.searchTerm);
} else {
nameDiv.textContent = node.name || '';
}
nodeContent.appendChild(nameDiv);

// Обработчик клика по всей строке для раскрытия/закрытия
if (hasChildren) {
nodeContent.addEventListener('click', (e) => {
if (e.target !== toggleBtn) {
this.toggleNode(node.id, toggleBtn, li);
}
});
}

li.appendChild(nodeContent);

// Дочерние элементы
if (hasChildren) {
const childrenContainer = document.createElement('ul');
childrenContainer.className = 'geo-lithology-node-children';
childrenContainer.id = `geo-lithology-children-${node.id}`;

if (this.expandedNodes.has(node.id) || this.searchTerm) {
childrenContainer.classList.add('visible');
}

node.children.forEach(child => {
this.renderTreeNode(child, childrenContainer, depth + 1);
});

li.appendChild(childrenContainer);
}

parentElement.appendChild(li);
}

renderListItem(item) {
const li = document.createElement('li');
li.className = 'geo-lithology-list-item';

// Название с подсветкой поиска
const nameDiv = document.createElement('div');
nameDiv.className = 'geo-lithology-list-name';
if (this.searchTerm && item.name && item.name.includes(this.searchTerm)) {
nameDiv.innerHTML = this.highlightText(item.name, this.searchTerm);
} else {
nameDiv.textContent = item.name || '';
}

// ID элемента
const idDiv = document.createElement('div');
idDiv.className = 'geo-lithology-list-id';
idDiv.textContent = item.node_id || item.id || '';

li.appendChild(nameDiv);
li.appendChild(idDiv);

this.elements.dataList.appendChild(li);
}

highlightText(text, searchTerm) {
if (!searchTerm) return text;
const regex = new RegExp(`(${this.escapeRegExp(searchTerm)})`, 'g');
return text.replace(regex, '<span class="geo-lithology-search-highlight">$1</span>');
}

escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

toggleNode(nodeId, toggleBtn, liElement) {
const childrenContainer = liElement.querySelector('.geo-lithology-node-children');
if (!childrenContainer) return;

const isExpanded = childrenContainer.classList.contains('visible');

if (isExpanded) {
childrenContainer.classList.remove('visible');
toggleBtn.classList.remove('expanded');
this.expandedNodes.delete(nodeId);
} else {
childrenContainer.classList.add('visible');
toggleBtn.classList.add('expanded');
this.expandedNodes.add(nodeId);
}
}

expandAll() {
if (this.currentView !== 'tree') return;

document.querySelectorAll('.geo-lithology-tree-node').forEach(li => {
const toggleBtn = li.querySelector('.geo-lithology-toggle-btn.has-children');
const childrenContainer = li.querySelector('.geo-lithology-node-children');

if (toggleBtn && childrenContainer) {
childrenContainer.classList.add('visible');
toggleBtn.classList.add('expanded');
const nodeId = li.dataset.nodeId;
this.expandedNodes.add(nodeId);
}
});
}

collapseAll() {
if (this.currentView !== 'tree') return;

document.querySelectorAll('.geo-lithology-tree-node').forEach(li => {
const toggleBtn = li.querySelector('.geo-lithology-toggle-btn.has-children');
const childrenContainer = li.querySelector('.geo-lithology-node-children');

if (toggleBtn && childrenContainer) {
childrenContainer.classList.remove('visible');
toggleBtn.classList.remove('expanded');
const nodeId = li.dataset.nodeId;
this.expandedNodes.delete(nodeId);
}
});
}

restoreExpandedState() {
document.querySelectorAll('.geo-lithology-tree-node').forEach(li => {
const nodeId = li.dataset.nodeId;
const toggleBtn = li.querySelector('.geo-lithology-toggle-btn.has-children');
const childrenContainer = li.querySelector('.geo-lithology-node-children');

if (toggleBtn && childrenContainer && this.expandedNodes.has(nodeId)) {
childrenContainer.classList.add('visible');
toggleBtn.classList.add('expanded');
}
});
}

reset() {
this.searchTerm = '';
this.elements.searchInput.value = '';
this.expandedNodes.clear();
this.renderCurrentView();
}

exportToExcel() {
// Экспорт всех конечных элементов
const dataToExport = this.flatList.map(item => ({
'node_id': item.node_id || item.id || '',
'node_name': item.node_name || item.name || '',
'lcode_speck': item.lcode_speck || ''
}));

if (dataToExport.length === 0) {
alert('Нет данных для экспорта');
return;
}

this.generateExcelFile(dataToExport, 'lithology_full_export');
}

exportListToExcel() {
// Экспорт только отфильтрованного списка
const dataToExport = (this.searchTerm ? this.filteredListData : this.flatList).map(item => ({
'node_id': item.node_id || item.id || '',
'node_name': item.node_name || item.name || '',
'lcode_speck': item.lcode_speck || ''
}));

if (dataToExport.length === 0) {
alert('Нет данных для экспорта');
return;
}

this.generateExcelFile(dataToExport, 'lithology_list_export');
}

generateExcelFile(data, filename) {
// Создаем CSV контент
let csvContent = 'data:text/csv;charset=utf-8,\uFEFF';

// Заголовки
const headers = ['Код', 'Название', 'Код СПЕК'];
csvContent += headers.join(';') + '\r\n';

// Данные
data.forEach(item => {
const row = [
item.node_id || '',
item.node_name || '',
item.lcode_speck || ''
].map(value => {
if (typeof value === 'string') {
value = value.replace(/"/g, '""');
if (value.includes(';') || value.includes('"') || value.includes('\n')) {
value = '"' + value + '"';
}
}
return value;
}).join(';');

csvContent += row + '\r\n';
});

// Создаем ссылку для скачивания
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', `${filename}_${new Date().toISOString().slice(0,10)}.csv`);

document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}

// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', () => {
window.geoLithologyTree = new GeoLithologyTree();
});

})();
</script>
</div>
<!-- Конец компонента литологии -->

Ррр
Прикрепления:
00007754.noext (51.7 Kb)
Рррр
Прикрепления:
ttt_1.noext (49.7 Kb)
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Очистка форматирования HTML кода</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #333;
min-height: 100vh;
padding: 20px;
}

.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}

header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}

h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}

.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}

.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
padding: 30px;
}

@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
}

.input-section, .output-section {
display: flex;
flex-direction: column;
}

textarea {
width: 100%;
height: 300px;
padding: 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
resize: vertical;
transition: border-color 0.3s;
background: #f8f9fa;
}

textarea:focus {
outline: none;
border-color: #667eea;
}

.input-textarea {
flex: 1;
}

.output-textarea {
flex: 1;
background: #f0f7ff;
}

label {
font-weight: 600;
margin-bottom: 10px;
color: #444;
font-size: 1.1rem;
}

.buttons {
display: flex;
gap: 15px;
margin-top: 20px;
flex-wrap: wrap;
}

.btn {
padding: 12px 25px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}

.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}

.btn-secondary {
background: #6c757d;
color: white;
}

.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}

.btn:active {
transform: translateY(0);
}

.stats {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
border-left: 4px solid #667eea;
}

.stats h3 {
margin-bottom: 10px;
color: #444;
}

.stat-item {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px solid #eee;
}

.stat-value {
font-weight: 600;
color: #667eea;
}

.instructions {
background: #fff8e1;
padding: 20px;
border-radius: 8px;
margin: 20px 30px;
border-left: 4px solid #ffc107;
}

.instructions h3 {
color: #333;
margin-bottom: 10px;
}

.instructions ul {
padding-left: 20px;
}

.instructions li {
margin-bottom: 8px;
}

footer {
text-align: center;
padding: 20px;
background: #f8f9fa;
color: #666;
border-top: 1px solid #eee;
}

.icon {
font-size: 1.2rem;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Очистка HTML кода</h1>
<p class="subtitle">Удаление лишних пробелов и форматирования из HTML кода</p>
</header>

<div class="instructions">
<h3>📋 Как использовать:</h3>
<ul>
<li>Вставьте HTML код в левое поле</li>
<li>Нажмите "Очистить код" для удаления пробелов из начала строк</li>
<li>Используйте "Копировать" для сохранения результата</li>
<li>Нажмите "Очистить все" для сброса</li>
</ul>
</div>

<div class="main-content">
<div class="input-section">
<label for="inputCode">Исходный HTML код:</label>
<textarea id="inputCode" placeholder="Вставьте ваш HTML код сюда..."></textarea>
</div>

<div class="output-section">
<label for="outputCode">Очищенный код:</label>
<textarea id="outputCode" readonly placeholder="Здесь появится очищенный код..."></textarea>
</div>
</div>

<div class="buttons" style="padding: 0 30px;">
<button class="btn btn-primary" id="cleanBtn">
<span class="icon">🔄</span> Очистить код
</button>
<button class="btn btn-primary" id="copyBtn">
<span class="icon">📋</span> Копировать результат
</button>
<button class="btn btn-secondary" id="clearBtn">
<span class="icon">🗑️</span> Очистить все
</button>
</div>

<div class="stats" style="margin: 20px 30px;">
<h3>📊 Статистика обработки:</h3>
<div class="stat-item">
<span>Строк во входном коде:</span>
<span id="inputLines" class="stat-value">0</span>
</div>
<div class="stat-item">
<span>Строк в выходном коде:</span>
<span id="outputLines" class="stat-value">0</span>
</div>
<div class="stat-item">
<span>Удалено начальных пробелов:</span>
<span id="spacesRemoved" class="stat-value">0</span>
</div>
<div class="stat-item">
<span>Сэкономлено символов:</span>
<span id="charsSaved" class="stat-value">0</span>
</div>
</div>

<footer>
<p>© 2024 HTML Code Cleaner | Инструмент для очистки форматирования HTML кода</p>
</footer>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const inputCode = document.getElementById('inputCode');
const outputCode = document.getElementById('outputCode');
const cleanBtn = document.getElementById('cleanBtn');
const copyBtn = document.getElementById('copyBtn');
const clearBtn = document.getElementById('clearBtn');

const inputLines = document.getElementById('inputLines');
const outputLines = document.getElementById('outputLines');
const spacesRemoved = document.getElementById('spacesRemoved');
const charsSaved = document.getElementById('charsSaved');

// Функция для удаления пробелов из начала строк
function cleanHTMLCode(code) {
if (!code) return '';

const lines = code.split('\n');
let removedSpaces = 0;
let originalLength = 0;

const cleanedLines = lines.map(line => {
originalLength += line.length;

// Находим первый не-пробельный символ
const match = line.match(/^(\s*)/);
const leadingSpaces = match ? match[0] : '';
const trimmedLine = line.substring(leadingSpaces.length);

// Увеличиваем счетчик удаленных пробелов
removedSpaces += leadingSpaces.length;

return trimmedLine;
});

const cleanedCode = cleanedLines.join('\n');
const cleanedLength = cleanedCode.length;

// Обновляем статистику
inputLines.textContent = lines.length;
outputLines.textContent = cleanedLines.length;
spacesRemoved.textContent = removedSpaces;
charsSaved.textContent = originalLength - cleanedLength;

return cleanedCode;
}

// Обработчик кнопки очистки
cleanBtn.addEventListener('click', function() {
const code = inputCode.value;
const cleanedCode = cleanHTMLCode(code);
outputCode.value = cleanedCode;

// Анимация кнопки
this.innerHTML = '<span class="icon">✅</span> Готово!';
setTimeout(() => {
this.innerHTML = '<span class="icon">🔄</span> Очистить код';
}, 1500);
});

// Обработчик кнопки копирования
copyBtn.addEventListener('click', function() {
if (!outputCode.value) {
alert('Сначала очистите код!');
return;
}

outputCode.select();
outputCode.setSelectionRange(0, 99999); // Для мобильных устройств

try {
navigator.clipboard.writeText(outputCode.value).then(() => {
this.innerHTML = '<span class="icon">✅</span> Скопировано!';
setTimeout(() => {
this.innerHTML = '<span class="icon">📋</span> Копировать результат';
}, 1500);
});
} catch (err) {
// Fallback для старых браузеров
document.execCommand('copy');
this.innerHTML = '<span class="icon">✅</span> Скопировано!';
setTimeout(() => {
this.innerHTML = '<span class="icon">📋</span> Копировать результат';
}, 1500);
}
});

// Обработчик кнопки очистки
clearBtn.addEventListener('click', function() {
inputCode.value = '';
outputCode.value = '';

// Сбрасываем статистику
inputLines.textContent = '0';
outputLines.textContent = '0';
spacesRemoved.textContent = '0';
charsSaved.textContent = '0';

// Анимация кнопки
this.innerHTML = '<span class="icon">✅</span> Очищено!';
setTimeout(() => {
this.innerHTML = '<span class="icon">🗑️</span> Очистить все';
}, 1500);
});

// Автоматическая очистка при изменении ввода (опционально)
inputCode.addEventListener('input', function() {
const lines = this.value.split('\n').length;
inputLines.textContent = lines;
});

// Пример HTML кода для демонстрации
const exampleHTML = `<!DOCTYPE html>
<html>
<head>
<title>Пример</title>
</head>
<body>
<div class="container">
<h1>Заголовок</h1>
<p>
Текст с отступами
</p>
</div>
</body>
</html>`;

// Вставляем пример при загрузке (опционально)
inputCode.value = exampleHTML;
inputLines.textContent = exampleHTML.split('\n').length;
});
</script>
</body>
</html>
Рорр
Прикрепления:
shhshhori.noext (47.3 Kb)
  • Страница 5 из 5
  • «
  • 1
  • 2
  • 3
  • 4
  • 5
Поиск:
Новый ответ
Имя:
Текст сообщения: