[ВОЛК] добавление закладки Площадки
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2025-12-25 20:15:02 +03:00
parent 26d12a4758
commit e03fb9a872
2 changed files with 484 additions and 32 deletions
+402 -32
View File
@@ -451,6 +451,13 @@
<i class="fas fa-calendar-alt me-1"></i> События
</button>
</li>
{% if g.workmode == 'VOLK' %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="categories-tab" data-bs-toggle="tab" data-bs-target="#categories-tab-pane" type="button" role="tab" aria-controls="categories-tab-pane" aria-selected="false">
<i class="fas fa-map-marker-alt me-1"></i> Площадки
</button>
</li>
{% endif %}
</ul>
<!-- Содержимое вкладок -->
<div class="tab-content" id="mainTabsContent">
@@ -573,11 +580,6 @@
<button class="btn btn-success" id="publish-all-events-btn">
<i class="fas fa-paper-plane me-1"></i> Опубликовать все
</button>
{% if g.workmode == 'VOLK' %}
<button class="btn btn-info ms-2" id="load-categories-btn">
<i class="fas fa-sync-alt me-1"></i> Площадки
</button>
{% endif %}
<div id="event-selected-post-info" class="cross-tab-info" style="display: none;">
<span>Выбранный пост: </span>
<span id="event-selected-post-name"></span>
@@ -661,6 +663,83 @@
</div>
</div>
</div>
{% if g.workmode == 'VOLK' %}
<!-- Вкладка площадок -->
<div class="tab-pane fade" id="categories-tab-pane" role="tabpanel" aria-labelledby="categories-tab" tabindex="0">
<div class="top-action-buttons d-flex align-items-center">
<button class="btn btn-info" id="load-categories-btn">
<i class="fas fa-sync-alt me-1"></i> Площадки
</button>
</div>
<div class="row">
<!-- Левая панель - список площадок -->
<div class="col-md-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Список площадок</h5>
<div class="d-flex align-items-center">
<div class="input-group me-2" style="width: 200px;">
<input type="text" class="form-control form-control-sm" id="categories-search" placeholder="Поиск...">
<button class="btn btn-outline-secondary btn-sm" type="button" id="categories-search-prev">
<i class="fas fa-chevron-up"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" type="button" id="categories-search-next">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<button class="btn btn-sm btn-primary" id="refresh-categories-btn">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="card-body p-0">
<!-- Контейнер для фиксированного заголовка -->
<div class="table-header-container">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="sortable-header">ID</th>
<th class="sortable-header">Название</th>
</tr>
</thead>
</table>
</div>
<!-- Контейнер для скроллируемого тела таблицы -->
<div class="scrollable-table-body">
<table class="table table-hover mb-0">
<tbody id="categories-table">
<tr>
<td colspan="2" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка данных...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Правая панель - детали площадки -->
<div class="col-md-7">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0" id="category-details-header">Детали площадки</h5>
<div id="category-actions"></div>
</div>
<div class="card-body scrollable-details" id="category-details">
<div class="text-center text-muted py-5">
<i class="fas fa-info-circle fa-3x mb-3"></i>
<p>Выберите площадку для просмотра деталей</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Модальное окно подтверждения удаления поста -->
@@ -866,6 +945,21 @@
syncColumnWidths('events');
}, 50);
});
{% if g.workmode == 'VOLK' %}
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab) {
categoriesTab.addEventListener('click', () => {
if (document.getElementById('categories-table').children.length === 1) {
loadCategoriesList();
}
// Принудительно обновляем размеры панелей и синхронизируем колонки
setTimeout(() => {
adjustPanelWidthsOnResize();
syncColumnWidths('categories');
}, 50);
});
}
{% endif %}
// Новые обработчики для кнопок событий
{% if g.workmode == 'VOLK' %}
const volkRescanBtn = document.getElementById('volk-rescan-btn');
@@ -885,6 +979,10 @@
if (loadCategoriesBtn) {
loadCategoriesBtn.addEventListener('click', loadCategories);
}
const refreshCategoriesBtn = document.getElementById('refresh-categories-btn');
if (refreshCategoriesBtn) {
refreshCategoriesBtn.addEventListener('click', loadCategoriesList);
}
{% endif %}
// Обработчики для кнопок привязки
document.getElementById('post-link-event-btn').addEventListener('click', togglePostEventLink);
@@ -1034,28 +1132,76 @@
}
// Функция для загрузки категорий
function loadCategories() {
showProcessingMessage("Загрузка категорий...");
fetch(`${PREFIX}/api/load_categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Категории загружены успешно!');
} else {
alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка загрузки категорий:', error);
alert('Произошла ошибка при загрузке категорий');
});
// Сначала проверяем наличие событий
fetch(`${PREFIX}/api/events/count`)
.then(response => response.json())
.then(data => {
const eventCount = data.count || 0;
if (eventCount > 0) {
// Если есть события, спрашиваем подтверждение
if (!confirm(`В таблице событий есть ${eventCount} записей. Обновление данных площадок может повлиять на существующие события. Вы уверены, что хотите продолжить?`)) {
return;
}
}
// Продолжаем загрузку категорий
showProcessingMessage("Загрузка категорий...");
fetch(`${PREFIX}/api/load_categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Категории загружены успешно!');
// Обновляем список категорий, если мы на вкладке площадок
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab && categoriesTab.classList.contains('active')) {
loadCategoriesList();
}
} else {
alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(error => {
hideProcessingMessage();
console.error('Ошибка загрузки категорий:', error);
alert('Произошла ошибка при загрузке категорий');
});
})
.catch(error => {
console.error('Ошибка проверки количества событий:', error);
// В случае ошибки все равно продолжаем загрузку
showProcessingMessage("Загрузка категорий...");
fetch(`${PREFIX}/api/load_categories`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
hideProcessingMessage();
if (data.success) {
alert('Категории загружены успешно!');
const categoriesTab = document.getElementById('categories-tab');
if (categoriesTab && categoriesTab.classList.contains('active')) {
loadCategoriesList();
}
} else {
alert('Ошибка загрузки категорий: ' + (data.error || 'неизвестная ошибка'));
}
})
.catch(err => {
hideProcessingMessage();
console.error('Ошибка загрузки категорий:', err);
alert('Произошла ошибка при загрузке категорий');
});
});
}
// Функция для публикации конкретного события
function publishCurrentEvent(eventId) {
@@ -1680,6 +1826,142 @@
console.error('Ошибка загрузки деталей события:', error);
}
}
// Функции для работы с категориями (площадками)
let selectedCategoryId = null;
async function loadCategoriesList() {
const tableBody = document.getElementById('categories-table');
if (!tableBody) return;
tableBody.innerHTML = `
<tr>
<td colspan="2" class="text-center py-3">
<div class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
Загрузка площадок...
</td>
</tr>`;
try {
const response = await fetch(`${PREFIX}/api/categories`);
const categories = await response.json();
if (categories.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="2" class="text-center py-5 text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p>Нет доступных площадок</p>
</td>
</tr>`;
return;
}
tableBody.innerHTML = '';
categories.forEach(category => {
const row = `
<tr class="category-row" data-id="${category.id}">
<td>${category.id || '-'}</td>
<td class="category-name-preview" title="${category.title || ''}">${category.title || 'Без названия'}</td>
</tr>`;
tableBody.innerHTML += row;
});
// Клик по строке категории
document.querySelectorAll('.category-row').forEach(row => {
row.addEventListener('click', (e) => {
const categoryId = row.dataset.id;
showCategoryDetails(categoryId);
highlightSelectedCategory(categoryId);
});
});
// Переинициализируем разделители колонок для категорий
setTimeout(() => {
syncColumnWidths('categories');
reinitializeColumnResizers('categories');
}, 0);
} catch (error) {
tableBody.innerHTML = `
<tr>
<td colspan="2" class="text-center py-5 text-danger">
<i class="fas fa-exclamation-triangle fa-2x mb-3"></i>
<p>Ошибка загрузки данных</p>
<button class="btn btn-sm btn-outline-primary mt-2" onclick="loadCategoriesList()">
Повторить попытку
</button>
</td>
</tr>`;
console.error('Ошибка загрузки категорий:', error);
}
}
// Выделение выбранной категории
function highlightSelectedCategory(categoryId) {
// Снимаем выделение со всех категорий
document.querySelectorAll('.category-row').forEach(row => {
row.classList.remove('active');
});
// Находим нужную категорию
const selectedRow = document.querySelector(`.category-row[data-id="${categoryId}"]`);
if (selectedRow) {
// Выделяем категорию
selectedRow.classList.add('active');
selectedCategoryId = categoryId;
// Прокручиваем к выбранной категории
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Показать детали категории
async function showCategoryDetails(categoryId) {
const detailsPanel = document.getElementById('category-details');
if (!detailsPanel) return;
detailsPanel.innerHTML = `
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка данных площадки...</p>
</div>`;
try {
const response = await fetch(`${PREFIX}/api/categories/${encodeURIComponent(categoryId)}`);
if (!response.ok) {
throw new Error('Площадка не найдена');
}
const category = await response.json();
// Создаем HTML для деталей
let detailsHtml = `
<div>
<div class="mb-3">
<div class="mb-2">
<strong>ID:</strong> ${category.id || 'Не указано'}
</div>
<div class="mb-2">
<strong>Название:</strong> ${category.title || 'Не указано'}
</div>
<div class="mb-2">
<strong>Количество событий:</strong> ${category.event_count || 0}
</div>
</div>`;
if (category.description) {
detailsHtml += `
<div class="mb-4 mt-3">
<strong>Описание:</strong>
<div class="border p-3 bg-light rounded mt-2">${category.description}</div>
</div>`;
}
detailsHtml += `</div>`;
detailsPanel.innerHTML = detailsHtml;
// Обновляем заголовок панели
const header = document.getElementById('category-details-header');
if (header) {
header.textContent = `Площадка: ${category.title || category.id || 'Без названия'}`;
}
} catch (error) {
detailsPanel.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Ошибка загрузки данных площадки: ${error.message}
<button class="btn btn-sm btn-outline-primary mt-2" onclick="showCategoryDetails('${categoryId}')">
Повторить попытку
</button>
</div>`;
console.error('Ошибка загрузки деталей категории:', error);
}
}
// Функция загрузки постов
async function loadPosts() {
const tableBody = document.getElementById('posts-table');
@@ -2550,11 +2832,17 @@ function setupColumnResizer(resizer, header, tableType = 'posts') {
}
// Применяем ширину к соответствующим ячейкам в теле таблицы
const bodyTable = document.querySelector(
tableType === 'posts'
? '#posts-tab-pane .scrollable-table-body table'
: '#events-tab-pane .scrollable-table-body table'
);
let bodyTableSelector;
if (tableType === 'posts') {
bodyTableSelector = '#posts-tab-pane .scrollable-table-body table';
} else if (tableType === 'events') {
bodyTableSelector = '#events-tab-pane .scrollable-table-body table';
} else if (tableType === 'categories') {
bodyTableSelector = '#categories-tab-pane .scrollable-table-body table';
} else {
bodyTableSelector = '#posts-tab-pane .scrollable-table-body table';
}
const bodyTable = document.querySelector(bodyTableSelector);
if (bodyTable) {
const bodyRows = bodyTable.querySelectorAll('tbody tr');
@@ -2595,6 +2883,9 @@ function syncColumnWidths(tableType) {
} else if (tableType === 'events') {
headerContainerSelector = '#events-tab-pane .table-header-container table';
bodyContainerSelector = '#events-tab-pane .scrollable-table-body table';
} else if (tableType === 'categories') {
headerContainerSelector = '#categories-tab-pane .table-header-container table';
bodyContainerSelector = '#categories-tab-pane .scrollable-table-body table';
} else {
console.warn('Неизвестный тип таблицы для синхронизации:', tableType);
return;
@@ -3088,6 +3379,8 @@ document.addEventListener('DOMContentLoaded', () => {
syncColumnWidths('posts');
} else if (tabId === 'events-tab-pane') {
syncColumnWidths('events');
} else if (tabId === 'categories-tab-pane') {
syncColumnWidths('categories');
}
}
}, 0);
@@ -3541,6 +3834,56 @@ document.addEventListener('DOMContentLoaded', () => {
}
// Простой поиск по событиям (без тэгов)
{% if g.workmode == 'VOLK' %}
// Функция поиска по категориям
function searchCategories(query) {
const tableBody = document.getElementById('categories-table');
if (!tableBody) return;
const rows = tableBody.querySelectorAll('.category-row');
const searchTerm = query.toLowerCase().trim();
let foundCount = 0;
let firstMatch = null;
rows.forEach((row, index) => {
const id = row.querySelector('td:first-child').textContent.toLowerCase();
const name = row.querySelector('.category-name-preview').textContent.toLowerCase();
const matches = id.includes(searchTerm) || name.includes(searchTerm);
if (matches) {
row.style.display = '';
if (foundCount === 0) {
firstMatch = row;
}
foundCount++;
} else {
row.style.display = 'none';
}
});
// Выделяем первый найденный результат
if (firstMatch && searchTerm) {
const categoryId = firstMatch.dataset.id;
highlightSelectedCategory(categoryId);
showCategoryDetails(categoryId);
firstMatch.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Навигация по результатам поиска категорий
let currentCategorySearchIndex = -1;
function navigateCategoriesSearch(direction) {
const tableBody = document.getElementById('categories-table');
if (!tableBody) return;
const visibleRows = Array.from(tableBody.querySelectorAll('.category-row')).filter(row => row.style.display !== 'none');
if (visibleRows.length === 0) return;
if (direction === 'next') {
currentCategorySearchIndex = (currentCategorySearchIndex + 1) % visibleRows.length;
} else {
currentCategorySearchIndex = (currentCategorySearchIndex - 1 + visibleRows.length) % visibleRows.length;
}
const selectedRow = visibleRows[currentCategorySearchIndex];
const categoryId = selectedRow.dataset.id;
highlightSelectedCategory(categoryId);
showCategoryDetails(categoryId);
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
{% endif %}
function searchEventsSimple(query) {
const events = document.querySelectorAll('.event-row');
eventsSearchResults = [];
@@ -3673,6 +4016,33 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
{% if g.workmode == 'VOLK' %}
const categoriesSearchInput = document.getElementById('categories-search');
if (categoriesSearchInput) {
categoriesSearchInput.addEventListener('input', (e) => {
searchCategories(e.target.value);
currentCategorySearchIndex = -1;
});
document.getElementById('categories-search-prev').addEventListener('click', () => {
navigateCategoriesSearch('prev');
});
document.getElementById('categories-search-next').addEventListener('click', () => {
navigateCategoriesSearch('next');
});
categoriesSearchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
navigateCategoriesSearch('next');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
navigateCategoriesSearch('prev');
} else if (e.key === 'ArrowDown') {
e.preventDefault();
navigateCategoriesSearch('next');
}
});
}
{% endif %}
document.getElementById('events-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();