2572 lines
126 KiB
HTML
2572 lines
126 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Управление постами VK и событиями</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||
<style>
|
||
/* Table styling fixes */
|
||
.table-header-container table,
|
||
.table-header-container th,
|
||
.table-header-container td,
|
||
.scrollable-table-body table,
|
||
.scrollable-table-body th,
|
||
.scrollable-table-body td {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.scrollable-table-body {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.post-text-preview {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 200px;
|
||
}
|
||
.shortname-preview {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 150px;
|
||
}
|
||
.event-name-preview {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 200px;
|
||
}
|
||
.detail-image {
|
||
max-height: 400px;
|
||
object-fit: contain;
|
||
}
|
||
.card-header {
|
||
background-color: #f8f9fa;
|
||
font-weight: 600;
|
||
}
|
||
.action-buttons .btn {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
/* Отступ для колонки действий от скроллбара */
|
||
.action-buttons {
|
||
padding-right: 20px !important;
|
||
min-width: 120px;
|
||
}
|
||
|
||
/* Отступ для заголовка колонки действий */
|
||
th.text-end:last-child {
|
||
padding-right: 20px !important;
|
||
min-width: 120px;
|
||
}
|
||
.post-row, .event-row {
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.post-row:hover, .event-row:hover {
|
||
background-color: #f8f9fa;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.post-row:hover:not(.active), .event-row:hover:not(.active) {
|
||
background-color: #f1f1f1;
|
||
}
|
||
.post-row.active, .event-row.active {
|
||
background-color: #e3f2fd !important;
|
||
background-image: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%) !important;
|
||
outline: 2px solid #0d6efd;
|
||
outline-offset: 0px;
|
||
box-shadow: 0 2px 4px rgba(13, 110, 253, 0.15);
|
||
}
|
||
|
||
.post-row.active td, .event-row.active td {
|
||
background-color: transparent !important;
|
||
}
|
||
.status-badge {
|
||
font-size: 0.85rem;
|
||
padding: 0.25rem 0.5rem;
|
||
}
|
||
|
||
/* Улучшенная видимость бейджей на выбранных строках */
|
||
.post-row.active .status-badge, .event-row.active .status-badge {
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* Улучшенная видимость иконок на выбранных строках */
|
||
.post-row.active .btn, .event-row.active .btn {
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
/* Улучшенная контрастность текста на выбранных строках */
|
||
.post-row.active td, .event-row.active td {
|
||
color: #1a1a1a;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* Специальный стиль для превью текста на выбранных строках */
|
||
.post-row.active .post-text-preview,
|
||
.post-row.active .shortname-preview,
|
||
.event-row.active .event-name-preview {
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
}
|
||
/* Обеспечиваем одинаковую высоту для всех карточек в панелях */
|
||
.row .card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Карточка детализации занимает всю высоту родительского контейнера */
|
||
.card.h-100 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.card.h-100 .card-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Скроллируемая область детализации */
|
||
.scrollable-details {
|
||
flex: 1;
|
||
overflow-y: auto !important;
|
||
padding: 1rem !important;
|
||
margin: 0 !important;
|
||
position: relative;
|
||
min-height: 0;
|
||
height: 0;
|
||
max-height: 100%;
|
||
}
|
||
|
||
/* Контент внутри детализации */
|
||
.scrollable-details > div {
|
||
position: relative !important;
|
||
top: auto !important;
|
||
left: auto !important;
|
||
right: auto !important;
|
||
bottom: auto !important;
|
||
overflow: visible !important;
|
||
box-sizing: border-box;
|
||
width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
/* Обеспечиваем одинаковую высоту для левой и правой панелей */
|
||
.row .col-md-5,
|
||
.row .col-md-7 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100vh - 150px);
|
||
max-height: calc(100vh - 150px);
|
||
}
|
||
|
||
/* Ограничиваем высоту карточек */
|
||
.card.h-100 {
|
||
height: 100%;
|
||
max-height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.navbar-nav .dropdown-menu {
|
||
min-width: 200px;
|
||
}
|
||
.top-action-buttons {
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.top-action-buttons .btn {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
/* Убираем лишние отступы на вкладке событий */
|
||
#events-tab-pane .top-action-buttons {
|
||
margin-bottom: 10px; /* Уменьшаем отступ */
|
||
}
|
||
|
||
/* Убираем отступы у скрытых элементов */
|
||
#event-selected-post-info {
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
/* Добавляем отступ снизу для вкладки событий */
|
||
#events-tab-pane {
|
||
padding-bottom: 10px;
|
||
box-sizing: border-box;
|
||
max-height: calc(100vh - 150px);
|
||
}
|
||
|
||
#processing-message {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 1050;
|
||
display: none;
|
||
}
|
||
.nav-tabs .nav-link {
|
||
font-weight: 500;
|
||
}
|
||
.tab-content {
|
||
padding-top: 15px;
|
||
}
|
||
.event-details-row {
|
||
margin-bottom: 10px;
|
||
}
|
||
.event-details-label {
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
}
|
||
.event-details-value {
|
||
padding: 8px;
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
min-height: 38px;
|
||
}
|
||
.cross-tab-info {
|
||
background-color: #f8f9fa;
|
||
border-radius: 4px;
|
||
padding: 8px 12px;
|
||
margin-left: 15px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
}
|
||
.cross-tab-info span {
|
||
margin-right: 8px;
|
||
}
|
||
.linked-items-list {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
}
|
||
/* Добавляем стили для сортируемых заголовков и разделителей */
|
||
.sortable-header {
|
||
cursor: pointer;
|
||
position: relative;
|
||
user-select: none;
|
||
}
|
||
.sortable-header:hover {
|
||
background-color: #e9ecef;
|
||
}
|
||
.sortable-header::after {
|
||
content: '';
|
||
display: inline-block;
|
||
margin-left: 5px;
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 5px solid transparent;
|
||
border-right: 5px solid transparent;
|
||
opacity: 0.5;
|
||
}
|
||
.sortable-header.asc::after {
|
||
border-bottom: 5px solid #000;
|
||
}
|
||
.sortable-header.desc::after {
|
||
border-top: 5px solid #000;
|
||
}
|
||
/* Стили для разделителей колонок */
|
||
.column-resizer {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 5px;
|
||
height: 100%;
|
||
cursor: col-resize;
|
||
user-select: none;
|
||
}
|
||
/* Стили для разделителя панелей */
|
||
.panel-resizer {
|
||
width: 10px;
|
||
background-color: #f0f0f0;
|
||
cursor: col-resize;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
.panel-resizer:hover {
|
||
background-color: #d0d0d0;
|
||
}
|
||
/* Убираем стандартное поведение Bootstrap колонок */
|
||
.resizable-panel {
|
||
transition: none;
|
||
overflow: auto;
|
||
}
|
||
/* Гарантируем, что панели будут в одной строке */
|
||
.resizable-row {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
width: 100%;
|
||
}
|
||
/* Table structure */
|
||
.table-container {
|
||
position: relative;
|
||
}
|
||
|
||
.table-header-container {
|
||
background-color: #f8f9fa;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
border-top: 1px solid #dee2e6;
|
||
border-bottom: 1px solid #dee2e6;
|
||
}
|
||
|
||
.scrollable-table-body {
|
||
max-height: calc(100vh - 290px);
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.table-header-container th,
|
||
.scrollable-table-body td {
|
||
vertical-align: top;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.table-header-container,
|
||
.scrollable-table-body {
|
||
width: 100%;
|
||
}
|
||
|
||
.table-header-container table,
|
||
.scrollable-table-body table {
|
||
table-layout: fixed;
|
||
width: 100%;
|
||
margin: 0;
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
}
|
||
|
||
.table-header-container .table,
|
||
.scrollable-table-body .table {
|
||
margin-bottom: 0;
|
||
}
|
||
/* Добавляем тень для визуального отделения скроллируемой области */
|
||
.scrollable-table-body {
|
||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||
<div class="container-fluid">
|
||
<a class="navbar-brand" href="#">Управление постами VK и событиями</a>
|
||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||
<span class="navbar-toggler-icon"></span>
|
||
</button>
|
||
<div class="collapse navbar-collapse" id="navbarNav">
|
||
<ul class="navbar-nav ms-auto">
|
||
<li class="nav-item dropdown">
|
||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
|
||
data-bs-toggle="dropdown" aria-expanded="false">
|
||
<i class="fas fa-user me-1"></i> {{ current_user.username }}
|
||
</a>
|
||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||
<li>
|
||
<a class="dropdown-item" href="{{ url_for('zilant.change_password') }}">
|
||
<i class="fas fa-key me-2"></i> Сменить пароль
|
||
</a>
|
||
</li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li>
|
||
<a class="dropdown-item" href="#" id="reset-layout-btn">
|
||
<i class="fas fa-undo me-2"></i> Сбросить настройки интерфейса
|
||
</a>
|
||
</li>
|
||
<li><hr class="dropdown-divider"></li>
|
||
<li>
|
||
<a class="dropdown-item" href="{{ url_for('zilant.logout') }}">
|
||
<i class="fas fa-sign-out-alt me-2"></i> Выйти
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<div class="container-fluid mt-3">
|
||
<!-- Навигационные вкладки -->
|
||
<ul class="nav nav-tabs" id="mainTabs" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" id="posts-tab" data-bs-toggle="tab" data-bs-target="#posts-tab-pane" type="button" role="tab" aria-controls="posts-tab-pane" aria-selected="true">
|
||
<i class="fas fa-newspaper me-1"></i> Посты
|
||
</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" id="events-tab" data-bs-toggle="tab" data-bs-target="#events-tab-pane" type="button" role="tab" aria-controls="events-tab-pane" aria-selected="false">
|
||
<i class="fas fa-calendar-alt me-1"></i> События
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
<!-- Содержимое вкладок -->
|
||
<div class="tab-content" id="mainTabsContent">
|
||
<!-- Вкладка постов -->
|
||
<div class="tab-pane fade show active" id="posts-tab-pane" role="tabpanel" aria-labelledby="posts-tab" tabindex="0">
|
||
<!-- Кнопки запуска скриптов -->
|
||
<div class="top-action-buttons d-flex align-items-center">
|
||
<button class="btn btn-primary" id="rescan-btn">
|
||
<i class="fas fa-search me-1"></i> Рескан ВК
|
||
</button>
|
||
<button class="btn btn-success" id="publish-btn">
|
||
<i class="fas fa-paper-plane me-1"></i> Опубликовать
|
||
</button>
|
||
<button class="btn btn-info" id="generate-names-btn">
|
||
<i class="fas fa-font me-1"></i> Названия
|
||
</button>
|
||
<div id="post-selected-event-info" class="cross-tab-info" style="display: none;">
|
||
<span>Выбранное событие: </span>
|
||
<span id="post-selected-event-name"></span>
|
||
<button class="btn btn-sm btn-outline-primary ms-2" id="post-link-event-btn">Привязать</button>
|
||
</div>
|
||
</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>
|
||
<button class="btn btn-sm btn-success" id="add-post-btn">
|
||
<i class="fas fa-plus"></i> Добавить
|
||
</button>
|
||
<button class="btn btn-sm btn-primary" id="refresh-posts-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">VK ID</th>
|
||
<th class="sortable-header">Название</th>
|
||
<th id="post-text-column-header" style="display: none;" class="sortable-header">Текст</th>
|
||
<th class="sortable-header">Статус</th>
|
||
<th class="text-end">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
</table>
|
||
</div>
|
||
<!-- Контейнер для скроллируемого тела таблицы -->
|
||
<div class="scrollable-table-body">
|
||
<table class="table table-hover mb-0">
|
||
<tbody id="posts-table">
|
||
<tr>
|
||
<td colspan="6" 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="post-details-header">Детали поста</h5>
|
||
<div id="post-actions"></div>
|
||
</div>
|
||
<div class="card-body scrollable-details" id="post-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>
|
||
<!-- Вкладка событий -->
|
||
<div class="tab-pane fade" id="events-tab-pane" role="tabpanel" aria-labelledby="events-tab" tabindex="0">
|
||
<div class="top-action-buttons d-flex align-items-center">
|
||
<button class="btn btn-success" id="add-event-btn">
|
||
<i class="fas fa-plus me-1"></i> Добавить событие
|
||
</button>
|
||
<button class="btn btn-primary" id="zk-rescan-btn">
|
||
<i class="fas fa-search me-1"></i> Рескан Зилант-сервера
|
||
</button>
|
||
<button class="btn btn-success" id="publish-all-events-btn">
|
||
<i class="fas fa-paper-plane me-1"></i> Опубликовать все
|
||
</button>
|
||
<div id="event-selected-post-info" class="cross-tab-info" style="display: none;">
|
||
<span>Выбранный пост: </span>
|
||
<span id="event-selected-post-name"></span>
|
||
<button class="btn btn-sm btn-outline-primary ms-2" id="event-link-post-btn">Привязать</button>
|
||
</div>
|
||
</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>
|
||
<button class="btn btn-sm btn-success" id="add-event-btn-sm">
|
||
<i class="fas fa-plus"></i> Добавить
|
||
</button>
|
||
<button class="btn btn-sm btn-primary" id="refresh-events-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>
|
||
<th class="sortable-header">Название</th>
|
||
<th class="sortable-header">Площадка</th>
|
||
<th class="sortable-header">Добавлено</th>
|
||
<th class="sortable-header">Обновлено</th>
|
||
<th class="sortable-header">Статус</th>
|
||
<th class="text-end">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
</table>
|
||
</div>
|
||
<!-- Контейнер для скроллируемого тела таблицы -->
|
||
<div class="scrollable-table-body">
|
||
<table class="table table-hover mb-0">
|
||
<tbody id="events-table">
|
||
<tr>
|
||
<td colspan="8" 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="event-details-header">Детали события</h5>
|
||
<div id="event-actions"></div>
|
||
</div>
|
||
<div class="card-body scrollable-details" id="event-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>
|
||
</div>
|
||
</div>
|
||
<!-- Модальное окно подтверждения удаления поста -->
|
||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Подтверждение удаления</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Вы уверены, что хотите удалить этот пост? Это действие нельзя отменить.</p>
|
||
<input type="hidden" id="delete-post-id">
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="button" class="btn btn-danger" id="confirm-delete">Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Модальное окно подтверждения удаления события -->
|
||
<div class="modal fade" id="deleteEventModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Подтверждение удаления</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>Вы уверены, что хотите удалить это событие? Это действие нельзя отменить.</p>
|
||
<input type="hidden" id="delete-event-id">
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||
<button type="button" class="btn btn-danger" id="confirm-event-delete">Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Сообщение о процессе выполнения -->
|
||
<div id="processing-message" class="alert alert-info">
|
||
<div class="d-flex align-items-center">
|
||
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||
<span class="visually-hidden">Загрузка...</span>
|
||
</div>
|
||
<span id="processing-text">Обработка...</span>
|
||
</div>
|
||
</div>
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
const PREFIX = "{{ g.prefix }}";
|
||
console.log("Lister Prefix is:", PREFIX); // Должно выводить "/zilant"
|
||
// Текущий выбранный пост и событие
|
||
let selectedPostId = null;
|
||
let selectedEventId = null;
|
||
// Данные о выбранных элементах для отображения на других вкладках
|
||
let selectedPostData = null;
|
||
let selectedEventData = null;
|
||
// Загрузка постов при запуске
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadPosts();
|
||
// Добавляем обработчики для кнопок запуска скриптов
|
||
document.getElementById('rescan-btn').addEventListener('click', runRescanScript);
|
||
document.getElementById('publish-btn').addEventListener('click', runPublishScript);
|
||
document.getElementById('generate-names-btn').addEventListener('click', generateAllShortnames);
|
||
document.getElementById('refresh-events-btn').addEventListener('click', loadEvents);
|
||
// Обработчики для кнопок добавления события
|
||
document.getElementById('add-event-btn').addEventListener('click', () => {
|
||
window.location.href = `${PREFIX}/add_event`;
|
||
});
|
||
document.getElementById('add-event-btn-sm').addEventListener('click', () => {
|
||
window.location.href = `${PREFIX}/add_event`;
|
||
});
|
||
// Подтверждение удаления события
|
||
document.getElementById('confirm-event-delete').addEventListener('click', () => {
|
||
const eventId = document.getElementById('delete-event-id').value;
|
||
deleteEvent(eventId);
|
||
});
|
||
// Обработчики для вкладок
|
||
document.getElementById('posts-tab').addEventListener('click', () => {
|
||
if (document.getElementById('posts-table').children.length === 1) {
|
||
loadPosts();
|
||
updateSelectedEventInfo();
|
||
}
|
||
// Принудительно обновляем размеры панелей и синхронизируем колонки
|
||
setTimeout(() => {
|
||
adjustPanelWidthsOnResize();
|
||
syncColumnWidths('posts');
|
||
}, 50);
|
||
});
|
||
document.getElementById('events-tab').addEventListener('click', () => {
|
||
if (document.getElementById('events-table').children.length === 1) {
|
||
loadEvents();
|
||
updateSelectedPostInfo();
|
||
}
|
||
// Принудительно обновляем размеры панелей и синхронизируем колонки
|
||
setTimeout(() => {
|
||
adjustPanelWidthsOnResize();
|
||
syncColumnWidths('events');
|
||
}, 50);
|
||
});
|
||
// Новые обработчики для кнопок событий
|
||
document.getElementById('zk-rescan-btn').addEventListener('click', runZkRescan);
|
||
document.getElementById('publish-all-events-btn').addEventListener('click', publishAllEvents);
|
||
// Обработчики для кнопок привязки
|
||
document.getElementById('post-link-event-btn').addEventListener('click', togglePostEventLink);
|
||
document.getElementById('event-link-post-btn').addEventListener('click', togglePostEventLink);
|
||
// Обработка параметров URL для активации вкладок
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const selectedPostId = urlParams.get('selected_post');
|
||
const selectedEventId = urlParams.get('selected_event');
|
||
const tabParam = urlParams.get('tab');
|
||
// Если есть selectedEventId или параметр tab=events, активируем вкладку событий
|
||
if (selectedEventId || tabParam === 'events') {
|
||
const eventsTab = document.getElementById('events-tab');
|
||
if (eventsTab) {
|
||
// Активируем вкладку событий
|
||
const tabInstance = new bootstrap.Tab(eventsTab);
|
||
tabInstance.show();
|
||
// Также загружаем события, если они еще не загружены
|
||
if (document.getElementById('events-table').children.length === 1) {
|
||
loadEvents();
|
||
}
|
||
}
|
||
}
|
||
// Если есть selectedPostId, активируем вкладку постов (по умолчанию и так активна, но если есть selectedEventId, то переключится на события)
|
||
if (selectedPostId) {
|
||
// Убедимся, что вкладка постов активна, если нет selectedEventId
|
||
if (!selectedEventId && tabParam !== 'events') {
|
||
const postsTab = document.getElementById('posts-tab');
|
||
if (postsTab) {
|
||
const tabInstance = new bootstrap.Tab(postsTab);
|
||
tabInstance.show();
|
||
}
|
||
}
|
||
}
|
||
// Затем выделяем элементы
|
||
if (selectedPostId) {
|
||
highlightSelectedPost(selectedPostId);
|
||
showPostDetails(selectedPostId);
|
||
}
|
||
if (selectedEventId) {
|
||
highlightSelectedEvent(selectedEventId);
|
||
showEventDetails(selectedEventId);
|
||
}
|
||
});
|
||
// Кнопка обновления постов
|
||
document.getElementById('refresh-posts-btn').addEventListener('click', loadPosts);
|
||
// Кнопка добавления поста
|
||
document.getElementById('add-post-btn').addEventListener('click', () => {
|
||
window.location.href = `${PREFIX}/add_post`;
|
||
});
|
||
// Подтверждение удаления поста
|
||
document.getElementById('confirm-delete').addEventListener('click', () => {
|
||
const postId = document.getElementById('delete-post-id').value;
|
||
deletePost(postId);
|
||
});
|
||
// Запуск скрипта рескана ВК
|
||
function runRescanScript() {
|
||
runScript('rescan');
|
||
}
|
||
// Запуск скрипта публикации
|
||
function runPublishScript() {
|
||
runScript('publish');
|
||
}
|
||
// Рескан Зилант-сервера
|
||
function runZkRescan() {
|
||
showProcessingMessage("Рескан Зилант-сервера...");
|
||
fetch(`${PREFIX}/api/run_zk_rescan`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Рескан Зилант-сервера выполнен успешно!');
|
||
loadEvents();
|
||
} else {
|
||
alert('Ошибка рескана Зилант-сервера: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка рескана Зилант-сервера:', error);
|
||
alert('Произошла ошибка при рескане Зилант-сервера');
|
||
});
|
||
}
|
||
// Функция для публикации всех событий
|
||
function publishAllEvents() {
|
||
const currentEventId = selectedEventId;
|
||
showProcessingMessage("Публикация всех событий...");
|
||
// Используем тот же подход, что и для других асинхронных операций
|
||
fetch(`${PREFIX}/api/publish_all_events`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Публикация всех событий выполнена успешно!');
|
||
loadEvents().then(() => {
|
||
if (currentEventId) {
|
||
highlightSelectedEvent(currentEventId);
|
||
showEventDetails(currentEventId);
|
||
}
|
||
});
|
||
} else {
|
||
alert('Ошибка публикации всех событий: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка публикации всех событий:', error);
|
||
alert('Произошла ошибка при публикации всех событий');
|
||
});
|
||
}
|
||
// Функция для публикации конкретного события
|
||
function publishCurrentEvent(eventId) {
|
||
if (!eventId) return;
|
||
const currentEventId = eventId;
|
||
showProcessingMessage("Публикация события...");
|
||
// Используем тот же подход, что и для других асинхронных операций
|
||
fetch(`${PREFIX}/api/publish_event/${currentEventId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Событие опубликовано успешно!');
|
||
loadEvents().then(() => {
|
||
highlightSelectedEvent(currentEventId);
|
||
showEventDetails(currentEventId);
|
||
});
|
||
} else {
|
||
alert('Ошибка публикации события: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка публикации события:', error);
|
||
alert('Произошла ошибка при публикации события');
|
||
});
|
||
}
|
||
// Привязка/отвязка поста и события
|
||
// Замените текущую функцию togglePostEventLink на эту обновленную версию
|
||
function togglePostEventLink() {
|
||
// Определяем, на какой вкладке мы находимся и какие элементы выбраны
|
||
const isOnPostsTab = document.getElementById('posts-tab-pane').classList.contains('show');
|
||
const isOnEventsTab = document.getElementById('events-tab-pane').classList.contains('show');
|
||
let postId, eventId;
|
||
if (isOnPostsTab) {
|
||
// Мы на вкладке постов - используем выбранный пост и выбранное событие из другой вкладки
|
||
postId = selectedPostId;
|
||
eventId = selectedEventData ? selectedEventData.id : null;
|
||
} else if (isOnEventsTab) {
|
||
// Мы на вкладке событий - используем выбранный пост из другой вкладки и выбранное событие
|
||
postId = selectedPostData ? selectedPostData.id : null;
|
||
eventId = selectedEventId;
|
||
}
|
||
if (!postId || !eventId) {
|
||
alert('Не выбран пост или событие для привязки');
|
||
return;
|
||
}
|
||
// Проверяем текущее состояние связи
|
||
fetch(`${PREFIX}/api/interlinks?post_id=${postId}&event_id=${eventId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.exists) {
|
||
// Удаляем связь
|
||
return fetch(`${PREFIX}/api/interlinks`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify({ post_id: postId, event_id: eventId })
|
||
});
|
||
} else {
|
||
// Создаем связь
|
||
return fetch(`${PREFIX}/api/interlinks`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify({ post_id: postId, event_id: eventId })
|
||
});
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// Обновляем информацию о связях
|
||
updateInterlinkInfo();
|
||
// Обновляем детали выбранного элемента
|
||
if (selectedPostId) showPostDetails(selectedPostId);
|
||
if (selectedEventId) showEventDetails(selectedEventId);
|
||
alert('Связь успешно ' + (data.exists ? 'удалена' : 'установлена'));
|
||
} else {
|
||
alert('Ошибка при изменении связи: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Ошибка при изменении связи:', error);
|
||
alert('Произошла ошибка при изменении связи');
|
||
});
|
||
}
|
||
// Обновите функцию updateInterlinkInfo для правильной работы с новыми переменными
|
||
function updateInterlinkInfo() {
|
||
// Для вкладки постов
|
||
if (selectedPostId && selectedEventData) {
|
||
fetch(`${PREFIX}/api/interlinks?post_id=${selectedPostId}&event_id=${selectedEventData.id}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const btn = document.getElementById('post-link-event-btn');
|
||
if (btn) {
|
||
if (data.exists) {
|
||
btn.textContent = 'Отвязать';
|
||
btn.classList.remove('btn-outline-primary');
|
||
btn.classList.add('btn-outline-danger');
|
||
} else {
|
||
btn.textContent = 'Привязать';
|
||
btn.classList.remove('btn-outline-danger');
|
||
btn.classList.add('btn-outline-primary');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Ошибка проверки связи:', error);
|
||
});
|
||
}
|
||
// Для вкладки событий
|
||
if (selectedEventId && selectedPostData) {
|
||
fetch(`${PREFIX}/api/interlinks?post_id=${selectedPostData.id}&event_id=${selectedEventId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const btn = document.getElementById('event-link-post-btn');
|
||
if (btn) {
|
||
if (data.exists) {
|
||
btn.textContent = 'Отвязать';
|
||
btn.classList.remove('btn-outline-primary');
|
||
btn.classList.add('btn-outline-danger');
|
||
} else {
|
||
btn.textContent = 'Привязать';
|
||
btn.classList.remove('btn-outline-danger');
|
||
btn.classList.add('btn-outline-primary');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Ошибка проверки связи:', error);
|
||
});
|
||
}
|
||
}
|
||
// Общая функция запуска скриптов
|
||
function runScript(scriptType) {
|
||
showProcessingMessage("Запуск скрипта...");
|
||
fetch(`${PREFIX}/api/run_script`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify({ script: scriptType })
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Скрипт успешно запущен!');
|
||
// Обновляем список постов после выполнения скрипта
|
||
loadPosts();
|
||
} else {
|
||
alert('Ошибка запуска скрипта: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка запуска скрипта:', error);
|
||
alert('Произошла ошибка при запуске скрипта');
|
||
});
|
||
}
|
||
// Функция для генерации всех названий
|
||
function generateAllShortnames() {
|
||
showProcessingMessage("Генерация названий для всех постов...");
|
||
fetch(`${PREFIX}/api/generate_all_shortnames`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Названия успешно сгенерированы!');
|
||
loadPosts(); // Перезагружаем список постов
|
||
} else {
|
||
alert('Ошибка генерации названий: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка генерации названий:', error);
|
||
alert('Произошла ошибка при генерации названий');
|
||
});
|
||
}
|
||
// Функция для генерации названия для конкретного поста
|
||
function generateShortname(postId) {
|
||
showProcessingMessage("Генерация названия для поста...");
|
||
fetch(`${PREFIX}/api/generate_shortname/${postId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Название успешно сгенерировано: ' + data.shortname);
|
||
loadPosts(); // Перезагружаем список постов
|
||
// Восстанавливаем выбранный пост
|
||
highlightSelectedPost(postId);
|
||
showPostDetails(postId);
|
||
} else {
|
||
alert('Ошибка генерации названия: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка генерации названия:', error);
|
||
alert('Произошла ошибка при генерации названия');
|
||
});
|
||
}
|
||
// Функция для публикации конкретного поста
|
||
function publishPost(postId) {
|
||
showProcessingMessage("Публикация поста...");
|
||
fetch(`${PREFIX}/api/publish_post/${postId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
alert('Пост успешно опубликован!');
|
||
loadPosts(); // Перезагружаем список постов
|
||
// Восстанавливаем выбранный пост
|
||
highlightSelectedPost(postId);
|
||
showPostDetails(postId);
|
||
} else {
|
||
alert('Ошибка публикации поста: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка публикации поста:', error);
|
||
alert('Произошла ошибка при публикации поста');
|
||
});
|
||
}
|
||
// Функция для переключения статуса публикации события
|
||
async function toggleEventPublication(eventId) {
|
||
showProcessingMessage("Изменение статуса публикации...");
|
||
fetch(`${PREFIX}/api/events/${eventId}/toggle_publication`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
}
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
hideProcessingMessage();
|
||
if (data.success) {
|
||
// Обновляем список событий
|
||
loadEvents();
|
||
// Обновляем детали события
|
||
showEventDetails(eventId);
|
||
} else {
|
||
alert('Ошибка изменения статуса публикации: ' + (data.error || 'неизвестная ошибка'));
|
||
}
|
||
})
|
||
.catch(error => {
|
||
hideProcessingMessage();
|
||
console.error('Ошибка изменения статуса публикации:', error);
|
||
alert('Произошла ошибка при изменении статуса публикации');
|
||
});
|
||
}
|
||
// Функция для удаления события
|
||
async function deleteEvent(eventId) {
|
||
const deleteModal = bootstrap.Modal.getInstance('#deleteEventModal');
|
||
try {
|
||
const response = await fetch(`${PREFIX}/api/events/${eventId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
deleteModal.hide();
|
||
loadEvents();
|
||
// Сбрасываем детали события
|
||
document.getElementById('event-details').innerHTML = `
|
||
<div class="text-center text-muted py-5">
|
||
<i class="fas fa-info-circle fa-3x mb-3"></i>
|
||
<p>Выберите событие для просмотра деталей</p>
|
||
</div>`;
|
||
} else {
|
||
throw new Error('Ошибка при удалении события');
|
||
}
|
||
} catch (error) {
|
||
alert(`Ошибка удаления: ${error.message}`);
|
||
}
|
||
}
|
||
// Функции для показа/скрытия сообщения о обработке
|
||
function showProcessingMessage(message) {
|
||
const processingElement = document.getElementById('processing-message');
|
||
const processingText = document.getElementById('processing-text');
|
||
processingText.textContent = message;
|
||
processingElement.style.display = 'flex';
|
||
}
|
||
function hideProcessingMessage() {
|
||
const processingElement = document.getElementById('processing-message');
|
||
processingElement.style.display = 'none';
|
||
}
|
||
// Функция загрузки событий
|
||
async function loadEvents() {
|
||
const tableBody = document.getElementById('events-table');
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" 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/events`);
|
||
const events = await response.json();
|
||
if (events.length === 0) {
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" class="text-center py-5 text-muted">
|
||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||
<p>Нет доступных событий</p>
|
||
</td>
|
||
</tr>`;
|
||
return;
|
||
}
|
||
tableBody.innerHTML = '';
|
||
events.forEach(event => {
|
||
// Форматируем даты
|
||
const formatDateTime = (dateStr) => {
|
||
if (!dateStr) return '-';
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
const visibleBadge = event.is_visible
|
||
? '<span class="badge bg-success status-badge">Видно</span>'
|
||
: '<span class="badge bg-secondary status-badge">Не видно</span>';
|
||
const publicationBadge = event.marked_to_publication
|
||
? '<span class="badge bg-warning status-badge ms-1">Для публикации</span>'
|
||
: '';
|
||
const postedBadge = event.is_posted_tg
|
||
? '<span class="badge bg-primary status-badge ms-1">Опубликовано</span>'
|
||
: '';
|
||
const row = `
|
||
<tr class="event-row" data-id="${event.id}">
|
||
<td>${event.id}</td>
|
||
<td>${event.number}</td>
|
||
<td class="event-name-preview" title="${event.name || ''}">${event.name || '-'}</td>
|
||
<td class="shortname-preview" title="${event.unit_name || ''}">${event.unit_name || '-'}</td>
|
||
<td>${formatDateTime(event.added)}</td>
|
||
<td>${formatDateTime(event.modified)}</td>
|
||
<td>${visibleBadge}${publicationBadge}${postedBadge}</td>
|
||
<td class="text-end action-buttons">
|
||
<button class="btn btn-sm btn-outline-warning edit-event-btn">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger delete-event-btn">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>`;
|
||
tableBody.innerHTML += row;
|
||
});
|
||
// Добавляем обработчики событий для кнопок
|
||
document.querySelectorAll('.edit-event-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const eventId = e.target.closest('tr').dataset.id;
|
||
window.location.href = `${PREFIX}/edit_event/${eventId}`;
|
||
});
|
||
});
|
||
document.querySelectorAll('.delete-event-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const eventId = e.target.closest('tr').dataset.id;
|
||
document.getElementById('delete-event-id').value = eventId;
|
||
new bootstrap.Modal('#deleteEventModal').show();
|
||
});
|
||
});
|
||
// Клик по строке события
|
||
document.querySelectorAll('.event-row').forEach(row => {
|
||
row.addEventListener('click', (e) => {
|
||
const eventId = row.dataset.id;
|
||
showEventDetails(eventId);
|
||
highlightSelectedEvent(eventId);
|
||
});
|
||
});
|
||
// Переинициализируем разделители колонок для событий
|
||
setTimeout(() => {
|
||
syncColumnWidths('events');
|
||
reinitializeColumnResizers('events');
|
||
}, 0);
|
||
|
||
// Автоматически выбираем событие из параметра URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const selectedEventId = urlParams.get('selected_event');
|
||
if (selectedEventId) {
|
||
// Выделяем событие
|
||
highlightSelectedEvent(selectedEventId);
|
||
// Показываем детали события
|
||
showEventDetails(selectedEventId);
|
||
// Убираем параметр из URL без перезагрузки страницы
|
||
const newUrl = window.location.pathname;
|
||
window.history.replaceState({}, document.title, newUrl);
|
||
}
|
||
} catch (error) {
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="8" 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="loadEvents()">
|
||
Повторить попытку
|
||
</button>
|
||
</td>
|
||
</tr>`;
|
||
console.error('Ошибка загрузки событий:', error);
|
||
}
|
||
}
|
||
// Выделение выбранного события
|
||
function highlightSelectedEvent(eventId) {
|
||
// Снимаем выделение со всех событий
|
||
document.querySelectorAll('.event-row').forEach(row => {
|
||
row.classList.remove('active');
|
||
});
|
||
// Находим нужное событие
|
||
const selectedRow = document.querySelector(`.event-row[data-id="${eventId}"]`);
|
||
if (selectedRow) {
|
||
// Выделяем событие
|
||
selectedRow.classList.add('active');
|
||
selectedEventId = eventId;
|
||
// Сохраняем данные о выбранном событии
|
||
const eventName = selectedRow.querySelector('.event-name-preview').textContent;
|
||
selectedEventData = {
|
||
id: eventId,
|
||
name: eventName
|
||
};
|
||
// Прокручиваем к выбранному событию
|
||
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
// Обновляем информацию о выбранном событии на вкладке постов
|
||
updateSelectedEventInfo();
|
||
}
|
||
// Показать детали события
|
||
async function showEventDetails(eventId) {
|
||
const detailsPanel = document.getElementById('event-details');
|
||
const actionsPanel = document.getElementById('event-actions');
|
||
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/events/${eventId}`);
|
||
if (!response.ok) {
|
||
throw new Error('Событие не найдено');
|
||
}
|
||
const event = await response.json();
|
||
// Форматируем даты
|
||
const formatDateTime = (dateStr) => {
|
||
if (!dateStr) return 'Не указано';
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
// Форматируем булевы значения
|
||
const formatBoolean = (value) => {
|
||
return value ? 'Да' : 'Нет';
|
||
};
|
||
// Загружаем связанные посты
|
||
const linkedPosts = await fetch(`${PREFIX}/api/events/${eventId}/linked_posts`)
|
||
.then(response => response.json())
|
||
.catch(error => {
|
||
console.error('Ошибка загрузки связанных постов:', error);
|
||
return [];
|
||
});
|
||
// Создаем HTML для деталей
|
||
let detailsHtml = `
|
||
<div>
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<div></div>
|
||
<div>
|
||
<button class="btn btn-sm btn-primary me-2" id="publish-event-btn">
|
||
<i class="fas fa-paper-plane me-1"></i>Опубликовать
|
||
</button>
|
||
<button class="btn btn-sm btn-info me-2" id="toggle-event-pub-btn">
|
||
${event.marked_to_publication ?
|
||
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
|
||
'<i class="fas fa-check-circle me-1"></i>Отметить для публикации'}
|
||
</button>
|
||
<button class="btn btn-sm btn-warning" onclick="location.href='${PREFIX}/edit_event/${event.id}'">
|
||
<i class="fas fa-edit me-1"></i>Редактировать
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="mb-3">
|
||
<div class="d-flex flex-wrap gap-2 mb-3 align-items-center">
|
||
<div class="border rounded p-2 d-flex gap-2 align-items-center">
|
||
<span class="${event.accepted ? 'fw-bold' : 'text-decoration-line-through'}">Принято</span>
|
||
<span class="${event.denied ? 'fw-bold' : 'text-decoration-line-through'}">Отказано</span>
|
||
<span class="${event.is_canceled ? 'fw-bold' : 'text-decoration-line-through'}">Отменено</span>
|
||
${event.is_visible ? '<span class="badge bg-success">Видно</span>' : '<span class="badge bg-secondary">Не видно</span>'}
|
||
</div>
|
||
${event.marked_to_publication && !event.is_posted_tg ? '<span class="badge bg-warning align-middle">Для публикации</span>' : ''}
|
||
${!event.marked_to_publication && !event.is_posted_tg ? '<span class="badge bg-secondary align-middle">Не для публикации</span>' : ''}
|
||
${event.is_posted_tg ? '<span class="badge bg-primary align-middle">Опубликовано</span>' : ''}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>Площадка:</strong> ${event.unit_name || 'Не указано'}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>Тэги:</strong> ${event.tags || 'Не указаны'}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>Создано</strong> <i class="fas fa-clock me-1"></i>${formatDateTime(event.added)}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>Обновлено</strong> <i class="fas fa-clock me-1"></i>${formatDateTime(event.modified)}
|
||
</div>
|
||
${event.is_posted_tg ? `
|
||
<div class="mb-2">
|
||
<strong>Телеграм</strong> <i class="fab fa-telegram-plane me-1"></i>${event.tg_message_id || 'Не указан'} <i class="fas fa-clock me-1"></i>${formatDateTime(event.tg_posted_date)}
|
||
</div>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
${event.about_social_picture ? `
|
||
<div class="text-center">
|
||
<img src="${PREFIX}/image_proxy?url=${encodeURIComponent(event.about_social_picture)}"
|
||
class="img-fluid rounded shadow-sm" style="max-width: 33%; min-width: 200px;">
|
||
</div>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="mb-4 mt-3">
|
||
<div class="border p-3 bg-light rounded">${event.about || 'Нет описания'}</div>
|
||
</div>`;
|
||
// Добавляем блок связанных постов
|
||
if (linkedPosts.length > 0) {
|
||
detailsHtml += `
|
||
<div class="card border-info mb-4">
|
||
<div class="card-header bg-info text-white">
|
||
<i class="fas fa-link me-1"></i> Связанные посты (${linkedPosts.length})
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div class="linked-items-list">
|
||
<div class="list-group mb-0">
|
||
${linkedPosts.map(post => `
|
||
<a href="#" class="list-group-item list-group-item-action py-1"
|
||
onclick="highlightSelectedPost(${post.id}); document.getElementById('posts-tab').click(); return false;">
|
||
<small>${post.shortname || `Пост ${post.id}`}</small>
|
||
<button class="btn btn-sm btn-outline-danger float-end"
|
||
onclick="event.stopPropagation(); unlinkEventFromPost(${event.id}, ${post.id}); return false;">
|
||
<i class="fas fa-unlink"></i>
|
||
</button>
|
||
</a>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
detailsHtml += `</div>`;
|
||
detailsPanel.innerHTML = detailsHtml;
|
||
// Обновляем заголовок панели
|
||
document.getElementById('event-details-header').textContent = `Событие #${event.number}: ${event.name || 'Без названия'}`;
|
||
// Обработчик для кнопки публикации события
|
||
document.getElementById('publish-event-btn').addEventListener('click', () => {
|
||
publishCurrentEvent(event.id);
|
||
});
|
||
// Обработчик для кнопки переключения публикации события
|
||
document.getElementById('toggle-event-pub-btn').addEventListener('click', () => {
|
||
toggleEventPublication(event.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="showEventDetails(${eventId})">
|
||
Повторить попытку
|
||
</button>
|
||
</div>`;
|
||
console.error('Ошибка загрузки деталей события:', error);
|
||
}
|
||
}
|
||
// Функция загрузки постов
|
||
async function loadPosts() {
|
||
const tableBody = document.getElementById('posts-table');
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" 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/posts`);
|
||
const posts = await response.json();
|
||
if (posts.length === 0) {
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" class="text-center py-5 text-muted">
|
||
<i class="fas fa-inbox fa-3x mb-3"></i>
|
||
<p>Нет доступных постов</p>
|
||
</td>
|
||
</tr>`;
|
||
return;
|
||
}
|
||
tableBody.innerHTML = '';
|
||
// Проверяем, нужно ли показывать колонку "Текст"
|
||
let showTextColumn = false;
|
||
posts.forEach(post => {
|
||
if (!post.shortname) {
|
||
showTextColumn = true;
|
||
}
|
||
});
|
||
// Показываем/скрываем колонку "Текст"
|
||
const textColumnHeader = document.getElementById('post-text-column-header');
|
||
textColumnHeader.style.display = showTextColumn ? 'table-cell' : 'none';
|
||
// Синхронизация после изменения видимости колонки
|
||
setTimeout(() => {
|
||
syncColumnWidths('posts');
|
||
reinitializeColumnResizers('posts');
|
||
}, 0);
|
||
posts.forEach(post => {
|
||
const statusBadge = post.marked_for_publication
|
||
? '<span class="badge bg-success status-badge">Для публикации</span>'
|
||
: '<span class="badge bg-secondary status-badge">Не для публикации</span>';
|
||
const publishedBadge = post.published_in_tg
|
||
? '<span class="badge bg-primary status-badge ms-1">Опубликовано</span>'
|
||
: '';
|
||
const eventBadge = post.is_event
|
||
? '<span class="badge bg-info status-badge ms-1">Событие</span>'
|
||
: '';
|
||
const pollBadge = post.is_poll
|
||
? '<span class="badge bg-warning status-badge ms-1">Опрос</span>'
|
||
: '';
|
||
// Показываем текст только если нет shortname
|
||
const textCell = showTextColumn
|
||
? `<td class="post-text-preview">${!post.shortname ? post.text_preview : ''}</td>`
|
||
: '';
|
||
const row = `
|
||
<tr class="post-row" data-id="${post.id}">
|
||
<td>${post.id}</td>
|
||
<td>${post.vk_post_id}</td>
|
||
<td class="shortname-preview" title="${post.shortname || ''}">${post.shortname || '-'}</td>
|
||
${textCell}
|
||
<td>${statusBadge}${publishedBadge}${eventBadge}${pollBadge}</td>
|
||
<td class="text-end action-buttons">
|
||
<button class="btn btn-sm btn-outline-primary view-btn">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-warning edit-btn">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-danger delete-btn">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</td>
|
||
</tr>`;
|
||
tableBody.innerHTML += row;
|
||
});
|
||
// Добавляем обработчики событий
|
||
document.querySelectorAll('.view-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const postId = e.target.closest('tr').dataset.id;
|
||
showPostDetails(postId);
|
||
highlightSelectedPost(postId);
|
||
});
|
||
});
|
||
document.querySelectorAll('.edit-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const postId = e.target.closest('tr').dataset.id;
|
||
window.location.href = `${PREFIX}/edit_post/${postId}`;
|
||
});
|
||
});
|
||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const postId = e.target.closest('tr').dataset.id;
|
||
document.getElementById('delete-post-id').value = postId;
|
||
new bootstrap.Modal('#deleteModal').show();
|
||
});
|
||
});
|
||
// Клик по строке
|
||
document.querySelectorAll('.post-row').forEach(row => {
|
||
row.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.action-buttons')) {
|
||
const postId = row.dataset.id;
|
||
showPostDetails(postId);
|
||
highlightSelectedPost(postId);
|
||
}
|
||
});
|
||
});
|
||
// Автоматически выбираем пост из параметра URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const selectedPostId = urlParams.get('selected_post');
|
||
if (selectedPostId) {
|
||
// Выделяем пост
|
||
highlightSelectedPost(selectedPostId);
|
||
// Показываем детали поста
|
||
showPostDetails(selectedPostId);
|
||
// Убираем параметр из URL без перезагрузки страницы
|
||
const newUrl = window.location.pathname;
|
||
window.history.replaceState({}, document.title, newUrl);
|
||
}
|
||
} catch (error) {
|
||
tableBody.innerHTML = `
|
||
<tr>
|
||
<td colspan="6" 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="loadPosts()">
|
||
Повторить попытку
|
||
</button>
|
||
</td>
|
||
</tr>`;
|
||
console.error('Ошибка загрузки постов:', error);
|
||
}
|
||
}
|
||
// Выделение выбранного поста
|
||
function highlightSelectedPost(postId) {
|
||
// Снимаем выделение со всех постов
|
||
document.querySelectorAll('.post-row').forEach(row => {
|
||
row.classList.remove('active');
|
||
});
|
||
// Находим нужный пост
|
||
const selectedRow = document.querySelector(`.post-row[data-id="${postId}"]`);
|
||
if (selectedRow) {
|
||
// Выделяем пост
|
||
selectedRow.classList.add('active');
|
||
selectedPostId = postId;
|
||
// Сохраняем данные о выбранном посте
|
||
const postName = selectedRow.querySelector('.shortname-preview').textContent;
|
||
selectedPostData = {
|
||
id: postId,
|
||
name: postName
|
||
};
|
||
// Прокручиваем к выбранному посту
|
||
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
// Обновляем информацию о выбранном посте на вкладке событий
|
||
updateSelectedPostInfo();
|
||
}
|
||
// Показать детали поста
|
||
async function showPostDetails(postId) {
|
||
const detailsPanel = document.getElementById('post-details');
|
||
const actionsPanel = document.getElementById('post-actions');
|
||
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/posts/${postId}`);
|
||
if (!response.ok) {
|
||
throw new Error('Пост не найден');
|
||
}
|
||
const post = await response.json();
|
||
// Форматируем даты
|
||
const formatDate = (dateStr) => {
|
||
if (!dateStr) return 'Не указано';
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleString('ru-RU', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
};
|
||
// Декодируем Unicode строки
|
||
const decodeUnicode = (str) => {
|
||
if (!str) return str;
|
||
return str.replace(/\\u[\dA-F]{4}/gi, (match) => {
|
||
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16));
|
||
});
|
||
};
|
||
// Загружаем связанные события
|
||
const linkedEvents = await fetch(`${PREFIX}/api/posts/${postId}/linked_events`)
|
||
.then(response => response.json())
|
||
.catch(error => {
|
||
console.error('Ошибка загрузки связанных событий:', error);
|
||
return [];
|
||
});
|
||
// Создаем HTML для деталей
|
||
let detailsHtml = `
|
||
<div>
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<div></div>
|
||
<div>
|
||
<button class="btn btn-sm btn-primary me-2" onclick="publishPost(${post.id})">
|
||
<i class="fas fa-paper-plane me-1"></i>Опубликовать
|
||
</button>
|
||
<button class="btn btn-sm btn-secondary me-2" onclick="generateShortname(${post.id})">
|
||
<i class="fas fa-font me-1"></i>Назвать
|
||
</button>
|
||
<button class="btn btn-sm btn-info me-2" id="toggle-pub-btn">
|
||
${post.marked_for_publication ?
|
||
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
|
||
'<i class="fas fa-check-circle me-1"></i>Отметить для публикации'}
|
||
</button>
|
||
<button class="btn btn-sm btn-warning" onclick="location.href='${PREFIX}/edit_post/${post.id}'">
|
||
<i class="fas fa-edit me-1"></i>Редактировать
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-8">
|
||
<div class="mb-3">
|
||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||
${post.marked_for_publication && !post.published_in_tg ? '<span class="badge bg-warning">Для публикации</span>' : ''}
|
||
${post.published_in_tg ? '<span class="badge bg-primary">Опубликовано</span>' : ''}
|
||
${post.is_event ? '<span class="badge bg-info">Событие</span>' : ''}
|
||
${post.is_poll ? '<span class="badge bg-success">Опрос</span>' : ''}
|
||
${!post.marked_for_publication && !post.published_in_tg && !post.is_event && !post.is_poll ? '<span class="badge bg-secondary">Никакое</span>' : ''}
|
||
</div>
|
||
<div class="mb-2">
|
||
<strong>ВКонтакте:</strong> <a href="${post.vk_post_url || '#'}" target="_blank" class="text-decoration-none">${post.vk_post_url || 'Не указана'}</a>
|
||
</div>
|
||
<div class="mb-2">
|
||
<i class="fas fa-clock me-1"></i>${formatDate(post.published_at)}
|
||
</div>
|
||
${post.published_in_tg ? `
|
||
<div class="mb-2">
|
||
<strong>Телеграм</strong> <i class="fab fa-telegram-plane me-1"></i>${post.tg_message_id || 'Не указан'} <i class="fas fa-clock me-1"></i>${formatDate(post.tg_publication_date)}
|
||
</div>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="col-md-4">
|
||
${post.image_url ? `
|
||
<div class="text-center">
|
||
<img src="${PREFIX}/image_proxy?url=${encodeURIComponent(post.image_url)}"
|
||
class="img-fluid rounded shadow-sm" style="max-width: 33%; min-width: 200px;">
|
||
</div>` : ''}
|
||
</div>
|
||
</div>
|
||
<div class="mb-4 mt-3">
|
||
<div class="border p-3 bg-light rounded">${post.text || 'Нет текста'}</div>
|
||
</div>
|
||
${post.is_poll ? `
|
||
<div class="card border-primary mb-4">
|
||
<div class="card-header bg-primary text-white">
|
||
<i class="fas fa-poll me-1"></i> Информация об опросе
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div class="mb-1">
|
||
<small class="text-muted fw-bold">Вопрос опроса:</small>
|
||
<div class="bg-light p-2 rounded">${post.poll_question || 'Не указан'}</div>
|
||
</div>
|
||
<div class="mb-1">
|
||
<small class="text-muted fw-bold">Варианты ответов:</small>
|
||
<div class="bg-light p-2 rounded">${decodeUnicode(post.poll_options) || 'Не указаны'}</div>
|
||
</div>
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<small class="text-muted fw-bold">Множественный выбор:</small>
|
||
<div class="bg-light p-2 rounded">${post.poll_multiple ? 'Да' : 'Нет'}</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<small class="text-muted fw-bold">Окончание опроса:</small>
|
||
<div class="bg-light p-2 rounded">${formatDate(post.poll_end_date)}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>` : ''}
|
||
${post.tg_poll_results ? `
|
||
<div class="card border-info mb-4">
|
||
<div class="card-header bg-info text-white">
|
||
<i class="fas fa-chart-bar me-1"></i> Результаты опроса в Telegram
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<pre class="bg-light p-2 rounded mb-0" style="font-size: 0.85em; line-height: 1.3; max-height: 150px; overflow-y: auto;">${post.tg_poll_results}</pre>
|
||
</div>
|
||
</div>` : ''}
|
||
${post.marks && post.marks.length > 0 ? `
|
||
<div class="card border-success mb-4">
|
||
<div class="card-header bg-success text-white">
|
||
<i class="fas fa-users me-1"></i> Подписки (${post.marks.length})
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div class="table-responsive">
|
||
<table class="table table-sm mb-0" style="font-size: 0.9em;">
|
||
<thead>
|
||
<tr>
|
||
<th>Имя пользователя</th>
|
||
<th>ID пользователя</th>
|
||
<th>Дата подписки</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${post.marks.map(mark => `
|
||
<tr>
|
||
<td>${mark.tg_user_name || 'Не указано'}</td>
|
||
<td>${mark.tg_user_id || 'Не указан'}</td>
|
||
<td>${formatDate(mark.created_at)}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>` : ''}`;
|
||
// Добавляем блок связанных событий
|
||
if (linkedEvents.length > 0) {
|
||
detailsHtml += `
|
||
<div class="card border-warning mb-4">
|
||
<div class="card-header bg-warning text-white">
|
||
<i class="fas fa-link me-1"></i> Связанные события (${linkedEvents.length})
|
||
</div>
|
||
<div class="card-body p-2">
|
||
<div class="linked-items-list">
|
||
<div class="list-group mb-0">
|
||
${linkedEvents.map(event => `
|
||
<a href="#" class="list-group-item list-group-item-action py-1"
|
||
onclick="highlightSelectedEvent(${event.id}); document.getElementById('events-tab').click(); return false;">
|
||
<small>${event.name || `Событие ${event.id}`}</small>
|
||
<button class="btn btn-sm btn-outline-danger float-end"
|
||
onclick="event.stopPropagation(); unlinkPostFromEvent(${post.id}, ${event.id}); return false;">
|
||
<i class="fas fa-unlink"></i>
|
||
</button>
|
||
</a>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
detailsHtml += `</div>`;
|
||
detailsPanel.innerHTML = detailsHtml;
|
||
// Обновляем заголовок панели
|
||
document.getElementById('post-details-header').textContent = `Пост #${post.vk_post_id}: ${post.shortname || 'Без названия'}`;
|
||
// Обработчик для кнопки публикации
|
||
document.getElementById('toggle-pub-btn').addEventListener('click', () => {
|
||
togglePublication(post.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="showPostDetails(${postId})">
|
||
Повторить попытку
|
||
</button>
|
||
</div>`;
|
||
console.error('Ошибка загрузки деталей поста:', error);
|
||
}
|
||
}
|
||
// Переключение статуса публикации
|
||
async function togglePublication(postId) {
|
||
const btn = document.getElementById('toggle-pub-btn');
|
||
const originalHtml = btn.innerHTML;
|
||
btn.innerHTML = `
|
||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||
Обновление...
|
||
`;
|
||
btn.disabled = true;
|
||
try {
|
||
const response = await fetch(`${PREFIX}/api/posts/${postId}/toggle_publication`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
// Обновляем кнопку
|
||
btn.innerHTML = result.new_value ?
|
||
'<i class="fas fa-times-circle me-1"></i>Снять с публикации' :
|
||
'<i class="fas fa-check-circle me-1"></i>Отметить для публикации';
|
||
// Обновляем список постов
|
||
loadPosts();
|
||
} else {
|
||
throw new Error('Ошибка обновления статуса');
|
||
}
|
||
} catch (error) {
|
||
alert(`Ошибка: ${error.message}`);
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
// Удаление поста
|
||
async function deletePost(postId) {
|
||
const deleteModal = bootstrap.Modal.getInstance('#deleteModal');
|
||
try {
|
||
const response = await fetch(`${PREFIX}/api/posts/${postId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
deleteModal.hide();
|
||
loadPosts();
|
||
// Сбрасываем детали поста
|
||
document.getElementById('post-details').innerHTML = `
|
||
<div class="text-center text-muted py-5">
|
||
<i class="fas fa-info-circle fa-3x mb-3"></i>
|
||
<p>Выберите пост для просмотра деталей</p>
|
||
</div>`;
|
||
} else {
|
||
throw new Error('Ошибка при удалении поста');
|
||
}
|
||
} catch (error) {
|
||
alert(`Ошибка удаления: ${error.message}`);
|
||
}
|
||
}
|
||
// Обновление информации о выбранном посте на вкладке событий
|
||
function updateSelectedPostInfo() {
|
||
const postInfoElement = document.getElementById('event-selected-post-info');
|
||
const postNameElement = document.getElementById('event-selected-post-name');
|
||
if (selectedPostData) {
|
||
postNameElement.textContent = selectedPostData.name || `Пост ${selectedPostData.id}`;
|
||
postInfoElement.style.display = 'flex';
|
||
updateInterlinkInfo();
|
||
} else {
|
||
postInfoElement.style.display = 'none';
|
||
}
|
||
}
|
||
// Обновление информации о выбранном событии на вкладке постов
|
||
function updateSelectedEventInfo() {
|
||
const eventInfoElement = document.getElementById('post-selected-event-info');
|
||
const eventNameElement = document.getElementById('post-selected-event-name');
|
||
if (selectedEventData) {
|
||
eventNameElement.textContent = selectedEventData.name || `Событие ${selectedEventData.id}`;
|
||
eventInfoElement.style.display = 'flex';
|
||
updateInterlinkInfo();
|
||
} else {
|
||
eventInfoElement.style.display = 'none';
|
||
}
|
||
}
|
||
// Добавляем функции для отвязывания постов от событий и наоборот
|
||
async function unlinkPostFromEvent(postId, eventId) {
|
||
if (!confirm('Вы уверены, что хотите отвязать это событие от поста?')) return;
|
||
try {
|
||
const response = await fetch(`${PREFIX}/api/interlinks`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify({ post_id: postId, event_id: eventId })
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
// Обновляем детали поста
|
||
showPostDetails(postId);
|
||
} else {
|
||
alert('Ошибка при отвязывании: ' + (result.error || 'неизвестная ошибка'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при отвязывании:', error);
|
||
alert('Произошла ошибка при отвязывании');
|
||
}
|
||
}
|
||
async function unlinkEventFromPost(eventId, postId) {
|
||
if (!confirm('Вы уверены, что хотите отвязать этот пост от события?')) return;
|
||
try {
|
||
const response = await fetch(`${PREFIX}/api/interlinks`, {
|
||
method: 'DELETE',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Requested-With': 'XMLHttpRequest'
|
||
},
|
||
body: JSON.stringify({ post_id: postId, event_id: eventId })
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
// Обновляем детали события
|
||
showEventDetails(eventId);
|
||
} else {
|
||
alert('Ошибка при отвязывании: ' + (result.error || 'неизвестная ошибка'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка при отвязывании:', error);
|
||
alert('Произошла ошибка при отвязывании');
|
||
}
|
||
}
|
||
// Сделаем функции глобальными для обработчиков в HTML
|
||
window.loadPosts = loadPosts;
|
||
window.loadEvents = loadEvents;
|
||
window.showPostDetails = showPostDetails;
|
||
window.showEventDetails = showEventDetails;
|
||
window.togglePublication = togglePublication;
|
||
window.toggleEventPublication = toggleEventPublication;
|
||
window.deletePost = deletePost;
|
||
window.deleteEvent = deleteEvent;
|
||
window.generateAllShortnames = generateAllShortnames;
|
||
window.generateShortname = generateShortname;
|
||
window.publishPost = publishPost;
|
||
window.publishCurrentEvent = publishCurrentEvent;
|
||
window.showProcessingMessage = showProcessingMessage;
|
||
window.hideProcessingMessage = hideProcessingMessage;
|
||
window.unlinkPostFromEvent = unlinkPostFromEvent;
|
||
window.unlinkEventFromPost = unlinkEventFromPost;
|
||
</script>
|
||
<script>
|
||
// Column resizer setup
|
||
function reinitializeColumnResizers(tableType) {
|
||
const selector = `#${tableType}-tab-pane .table-header-container thead th`;
|
||
const headers = document.querySelectorAll(selector);
|
||
headers.forEach((header) => {
|
||
const existingResizer = header.querySelector('.column-resizer');
|
||
if (existingResizer) {
|
||
existingResizer.remove();
|
||
}
|
||
|
||
const resizer = document.createElement('div');
|
||
resizer.classList.add('column-resizer');
|
||
header.style.position = 'relative';
|
||
header.appendChild(resizer);
|
||
|
||
setupColumnResizer(resizer, header, tableType);
|
||
});
|
||
}
|
||
|
||
// Column width adjustment
|
||
function setupColumnResizer(resizer, header, tableType = 'posts') {
|
||
let startX, startWidth, columnIndex, nextColumnIndex, startNextWidth;
|
||
|
||
// Определяем индекс колонки
|
||
const headerCells = Array.from(header.parentNode.children);
|
||
columnIndex = headerCells.indexOf(header);
|
||
nextColumnIndex = columnIndex + 1;
|
||
|
||
resizer.addEventListener('mousedown', (e) => {
|
||
startX = e.clientX;
|
||
const rect = header.getBoundingClientRect();
|
||
startWidth = rect.width;
|
||
|
||
// Получаем ширину следующей колонки
|
||
const nextHeader = headerCells[nextColumnIndex];
|
||
if (nextHeader) {
|
||
const nextRect = nextHeader.getBoundingClientRect();
|
||
startNextWidth = nextRect.width;
|
||
}
|
||
|
||
document.addEventListener('mousemove', resizeHandler);
|
||
document.addEventListener('mouseup', stopResize);
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
|
||
function resizeHandler(e) {
|
||
const deltaX = e.clientX - startX;
|
||
const newWidth = Math.max(30, startWidth + deltaX); // Минимальная ширина 30px
|
||
|
||
// Вычисляем новую ширину следующей колонки
|
||
const nextWidth = Math.max(30, startNextWidth - deltaX);
|
||
|
||
// Применяем ширину к текущей колонке
|
||
header.style.width = `${newWidth}px`;
|
||
header.style.minWidth = `${newWidth}px`;
|
||
|
||
// Применяем ширину к следующей колонке
|
||
const nextHeader = headerCells[nextColumnIndex];
|
||
if (nextHeader) {
|
||
nextHeader.style.width = `${nextWidth}px`;
|
||
nextHeader.style.minWidth = `${nextWidth}px`;
|
||
}
|
||
|
||
// Применяем ширину к соответствующим ячейкам в теле таблицы
|
||
const bodyTable = document.querySelector(
|
||
tableType === 'posts'
|
||
? '#posts-tab-pane .scrollable-table-body table'
|
||
: '#events-tab-pane .scrollable-table-body table'
|
||
);
|
||
|
||
if (bodyTable) {
|
||
const bodyRows = bodyTable.querySelectorAll('tbody tr');
|
||
bodyRows.forEach(row => {
|
||
// Обновляем текущую колонку
|
||
if (row.cells && row.cells[columnIndex]) {
|
||
const bodyCell = row.cells[columnIndex];
|
||
if (window.getComputedStyle(bodyCell).display !== 'none') {
|
||
bodyCell.style.width = `${newWidth}px`;
|
||
bodyCell.style.minWidth = `${newWidth}px`;
|
||
}
|
||
}
|
||
|
||
// Обновляем следующую колонку
|
||
if (row.cells && row.cells[nextColumnIndex]) {
|
||
const nextBodyCell = row.cells[nextColumnIndex];
|
||
if (window.getComputedStyle(nextBodyCell).display !== 'none') {
|
||
nextBodyCell.style.width = `${nextWidth}px`;
|
||
nextBodyCell.style.minWidth = `${nextWidth}px`;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function stopResize() {
|
||
document.removeEventListener('mousemove', resizeHandler);
|
||
document.removeEventListener('mouseup', stopResize);
|
||
}
|
||
}
|
||
// Table column synchronization
|
||
function syncColumnWidths(tableType) {
|
||
// Определяем селекторы в зависимости от типа таблицы
|
||
let headerContainerSelector, bodyContainerSelector;
|
||
if (tableType === 'posts') {
|
||
headerContainerSelector = '#posts-tab-pane .table-header-container table';
|
||
bodyContainerSelector = '#posts-tab-pane .scrollable-table-body table';
|
||
} else if (tableType === 'events') {
|
||
headerContainerSelector = '#events-tab-pane .table-header-container table';
|
||
bodyContainerSelector = '#events-tab-pane .scrollable-table-body table';
|
||
} else {
|
||
console.warn('Неизвестный тип таблицы для синхронизации:', tableType);
|
||
return;
|
||
}
|
||
const headerTable = document.querySelector(headerContainerSelector);
|
||
const bodyTable = document.querySelector(bodyContainerSelector);
|
||
if (!headerTable || !bodyTable) {
|
||
return;
|
||
}
|
||
|
||
// Получаем видимые заголовки (исключая скрытые, например, колонку "Текст")
|
||
const allHeaderCells = headerTable.querySelectorAll('thead th');
|
||
const headerCells = Array.from(allHeaderCells).filter(cell => {
|
||
return window.getComputedStyle(cell).display !== 'none';
|
||
});
|
||
|
||
// Получаем строки тела
|
||
const bodyRows = bodyTable.querySelectorAll('tbody tr');
|
||
if (headerCells.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// Получаем общую ширину контейнера таблицы
|
||
const containerWidth = headerTable.parentElement.offsetWidth;
|
||
|
||
// Вычисляем общую ширину всех колонок
|
||
let totalWidth = 0;
|
||
const columnWidths = [];
|
||
|
||
for (let i = 0; i < headerCells.length; i++) {
|
||
const headerCell = headerCells[i];
|
||
let computedWidth = 0;
|
||
const rect = headerCell.getBoundingClientRect();
|
||
computedWidth = rect.width;
|
||
if (computedWidth <= 0 || isNaN(computedWidth)) {
|
||
computedWidth = headerCell.offsetWidth;
|
||
}
|
||
computedWidth = Math.max(30, computedWidth); // Минимальная ширина
|
||
columnWidths.push(computedWidth);
|
||
totalWidth += computedWidth;
|
||
}
|
||
|
||
// Если общая ширина превышает контейнер, масштабируем все колонки
|
||
if (totalWidth > containerWidth) {
|
||
const scaleFactor = containerWidth / totalWidth;
|
||
columnWidths.forEach((width, index) => {
|
||
columnWidths[index] = Math.max(30, width * scaleFactor);
|
||
});
|
||
}
|
||
|
||
// Применяем вычисленные ширины
|
||
for (let i = 0; i < headerCells.length; i++) {
|
||
const headerCell = headerCells[i];
|
||
const width = columnWidths[i];
|
||
|
||
// Применяем ширину к заголовку
|
||
headerCell.style.width = `${width}px`;
|
||
headerCell.style.minWidth = `${width}px`;
|
||
|
||
// Применяем ширину к соответствующим ячейкам в теле таблицы
|
||
bodyRows.forEach(row => {
|
||
if (row.cells && row.cells[i]) {
|
||
const bodyCell = row.cells[i];
|
||
if (window.getComputedStyle(bodyCell).display !== 'none') {
|
||
bodyCell.style.width = `${width}px`;
|
||
bodyCell.style.minWidth = `${width}px`;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// Panel resizing setup
|
||
function setupPanelResizing() {
|
||
// Обрабатываем обе вкладки
|
||
['posts-tab-pane', 'events-tab-pane'].forEach(tabId => {
|
||
const tabPane = document.getElementById(tabId);
|
||
if (!tabPane) return;
|
||
const row = tabPane.querySelector('.row');
|
||
if (!row) return;
|
||
// Добавляем класс для flex-контейнера
|
||
row.classList.add('resizable-row');
|
||
// Находим левую и правую панели
|
||
const leftPanel = row.querySelector('.col-md-5');
|
||
const rightPanel = row.querySelector('.col-md-7');
|
||
if (!leftPanel || !rightPanel) return;
|
||
// Заменяем классы Bootstrap на наши собственные
|
||
leftPanel.classList.remove('col-md-5');
|
||
rightPanel.classList.remove('col-md-7');
|
||
leftPanel.classList.add('resizable-panel');
|
||
rightPanel.classList.add('resizable-panel');
|
||
// Создаем разделитель
|
||
const resizer = document.createElement('div');
|
||
resizer.classList.add('panel-resizer');
|
||
// Вставляем разделитель между панелями
|
||
leftPanel.after(resizer);
|
||
// Настройка обработчиков для разделителя
|
||
setupPanelResizer(resizer, leftPanel, rightPanel);
|
||
});
|
||
}
|
||
// Panel resizer handler
|
||
function setupPanelResizer(resizer, leftPanel, rightPanel) {
|
||
let startX, startLeftWidth, startRightWidth;
|
||
resizer.addEventListener('mousedown', (e) => {
|
||
startX = e.clientX;
|
||
startLeftWidth = parseInt(getComputedStyle(leftPanel).width);
|
||
startRightWidth = parseInt(getComputedStyle(rightPanel).width);
|
||
document.addEventListener('mousemove', resizeHandler);
|
||
document.addEventListener('mouseup', stopResize);
|
||
e.preventDefault();
|
||
});
|
||
function resizeHandler(e) {
|
||
const dx = e.clientX - startX;
|
||
const newLeftWidth = startLeftWidth + dx;
|
||
const newRightWidth = startRightWidth - dx;
|
||
// Минимальная ширина панелей
|
||
if (newLeftWidth > 100 && newRightWidth > 100) {
|
||
leftPanel.style.width = `${newLeftWidth}px`;
|
||
rightPanel.style.width = `${newRightWidth}px`;
|
||
// Column sync during resize (disabled for performance)
|
||
}
|
||
}
|
||
function stopResize() {
|
||
document.removeEventListener('mousemove', resizeHandler);
|
||
document.removeEventListener('mouseup', stopResize);
|
||
// Сохраняем настройки в localStorage
|
||
savePanelSizes();
|
||
// Final column sync after resize
|
||
const isPostsTab = leftPanel.closest('#posts-tab-pane') !== null;
|
||
const isEventsTab = leftPanel.closest('#events-tab-pane') !== null;
|
||
setTimeout(() => {
|
||
if (isPostsTab) {
|
||
syncColumnWidths('posts');
|
||
} else if (isEventsTab) {
|
||
syncColumnWidths('events');
|
||
}
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
// Panel size management
|
||
function savePanelSizes() {
|
||
const leftPanel = document.querySelector('#posts-tab-pane .resizable-panel:first-child');
|
||
const rightPanel = document.querySelector('#posts-tab-pane .resizable-panel:last-child');
|
||
if (leftPanel && rightPanel) {
|
||
localStorage.setItem('panelSizes', JSON.stringify({
|
||
left: leftPanel.style.width,
|
||
right: rightPanel.style.width
|
||
}));
|
||
}
|
||
}
|
||
// Функция для применения стандартного макета
|
||
function applyDefaultLayout() {
|
||
const container = document.querySelector('.container-fluid');
|
||
if (!container) return;
|
||
// Сбрасываем размеры панелей к значениям по умолчанию
|
||
const availableWidth = container.offsetWidth - 30; // Учитываем возможный padding
|
||
const defaultLeft = availableWidth * 0.4;
|
||
const defaultRight = availableWidth * 0.6;
|
||
document.querySelectorAll('.resizable-panel').forEach((panel, index) => {
|
||
// Применяем стандартные размеры
|
||
panel.style.width = (index % 2 === 0) ? `${defaultLeft}px` : `${defaultRight}px`;
|
||
// Сбрасываем возможные inline-стили min-width/max-width, если они мешают
|
||
panel.style.minWidth = '';
|
||
panel.style.maxWidth = '';
|
||
});
|
||
// Сбрасываем размеры колонок таблиц
|
||
// Для таблицы постов
|
||
const postsHeaderCells = document.querySelectorAll('#posts-tab-pane .table-header-container thead th');
|
||
const postsBodyRows = document.querySelectorAll('#posts-tab-pane .scrollable-table-body tbody tr');
|
||
if (postsHeaderCells.length > 0) {
|
||
// Скрываем колонку "Текст" по умолчанию (она показывается в loadPosts при необходимости)
|
||
const textColumnHeader = document.getElementById('post-text-column-header');
|
||
if (textColumnHeader) {
|
||
textColumnHeader.style.display = 'none';
|
||
}
|
||
// Сбрасываем inline-стили ширины для видимых заголовков
|
||
postsHeaderCells.forEach(cell => {
|
||
if (window.getComputedStyle(cell).display !== 'none') {
|
||
cell.style.width = '';
|
||
cell.style.minWidth = '';
|
||
}
|
||
});
|
||
// Сбрасываем inline-стили ширины для ячеек тела
|
||
postsBodyRows.forEach(row => {
|
||
Array.from(row.cells).forEach(cell => {
|
||
// Сбрасываем только для видимых ячеек
|
||
if (window.getComputedStyle(cell).display !== 'none') {
|
||
cell.style.width = '';
|
||
cell.style.minWidth = '';
|
||
}
|
||
});
|
||
});
|
||
// Синхронизируем после сброса
|
||
setTimeout(() => {
|
||
syncColumnWidths('posts');
|
||
}, 0);
|
||
}
|
||
// Для таблицы событий
|
||
const eventsHeaderCells = document.querySelectorAll('#events-tab-pane .table-header-container thead th');
|
||
const eventsBodyRows = document.querySelectorAll('#events-tab-pane .scrollable-table-body tbody tr');
|
||
if (eventsHeaderCells.length > 0) {
|
||
eventsHeaderCells.forEach(cell => {
|
||
cell.style.width = '';
|
||
cell.style.minWidth = '';
|
||
});
|
||
eventsBodyRows.forEach(row => {
|
||
Array.from(row.cells).forEach(cell => {
|
||
cell.style.width = '';
|
||
cell.style.minWidth = '';
|
||
});
|
||
});
|
||
setTimeout(() => {
|
||
syncColumnWidths('events');
|
||
}, 0);
|
||
}
|
||
// Сбрасываем сортировку в заголовках таблиц (удаляем классы asc/desc)
|
||
document.querySelectorAll('.sortable-header').forEach(header => {
|
||
header.classList.remove('asc', 'desc');
|
||
});
|
||
// Скрываем информацию о перекрестных связях, если она была показана
|
||
const crossTabInfos = document.querySelectorAll('.cross-tab-info');
|
||
crossTabInfos.forEach(info => {
|
||
// Проверяем, есть ли у нее стиль display, установленный в JS (например, flex)
|
||
// Проще всего просто скрыть, если данные не выбраны
|
||
// Или можно проверить selectedPostData/selectedEventData
|
||
// Для простоты скроем, они появятся при выборе
|
||
if (info.style.display !== 'none') {
|
||
info.style.display = 'none';
|
||
}
|
||
});
|
||
console.log("Настройки интерфейса сброшены к значениям по умолчанию.");
|
||
}
|
||
// Load panel sizes from localStorage
|
||
function loadPanelSizes() {
|
||
const savedSizes = localStorage.getItem('panelSizes');
|
||
const container = document.querySelector('.container-fluid');
|
||
if (!container) return;
|
||
// Функция для применения размеров
|
||
const applySizes = (leftWidth, rightWidth) => {
|
||
document.querySelectorAll('#posts-tab-pane .resizable-panel, #events-tab-pane .resizable-panel').forEach((panel, index) => {
|
||
// Применяем размеры: 0,2 - левая, 1,3 - правая
|
||
if (index % 2 === 0) { // Левая панель
|
||
panel.style.width = `${leftWidth}px`;
|
||
} else { // Правая панель
|
||
panel.style.width = `${rightWidth}px`;
|
||
}
|
||
});
|
||
// Sync columns after applying sizes
|
||
setTimeout(() => {
|
||
syncColumnWidths('posts');
|
||
syncColumnWidths('events');
|
||
}, 0);
|
||
};
|
||
if (savedSizes) {
|
||
try {
|
||
const sizes = JSON.parse(savedSizes);
|
||
// Получаем доступную ширину контейнера
|
||
const availableWidth = container.offsetWidth - 30; // Учитываем padding
|
||
// Проверяем, помещаются ли панели
|
||
const leftWidth = parseInt(sizes.left);
|
||
const rightWidth = parseInt(sizes.right);
|
||
if (!isNaN(leftWidth) && !isNaN(rightWidth) && leftWidth > 0 && rightWidth > 0) {
|
||
if (leftWidth + rightWidth > availableWidth) {
|
||
// Масштабируем размеры панелей
|
||
const scaleFactor = availableWidth / (leftWidth + rightWidth);
|
||
const newLeftWidth = Math.max(100, leftWidth * scaleFactor);
|
||
const newRightWidth = Math.max(100, rightWidth * scaleFactor);
|
||
// Применяем новые размеры
|
||
applySizes(newLeftWidth, newRightWidth);
|
||
} else {
|
||
// Применяем сохраненные размеры
|
||
applySizes(leftWidth, rightWidth);
|
||
}
|
||
} else {
|
||
// Если данные повреждены, применяем по умолчанию
|
||
throw new Error("Invalid saved sizes");
|
||
}
|
||
} catch (e) {
|
||
console.error("Ошибка при парсинге или применении сохраненных размеров панелей:", e);
|
||
// Применяем размеры по умолчанию при ошибке
|
||
const availableWidth = container.offsetWidth - 30;
|
||
const defaultLeft = availableWidth * 0.4;
|
||
const defaultRight = availableWidth * 0.6;
|
||
applySizes(defaultLeft, defaultRight);
|
||
}
|
||
} else {
|
||
// Установка размеров по умолчанию
|
||
const availableWidth = container.offsetWidth - 30;
|
||
const defaultLeft = availableWidth * 0.4;
|
||
const defaultRight = availableWidth * 0.6;
|
||
applySizes(defaultLeft, defaultRight);
|
||
}
|
||
}
|
||
|
||
// Layout reset functionality
|
||
function resetLayout() {
|
||
if (confirm("Вы уверены, что хотите сбросить настройки интерфейса к значениям по умолчанию?")) {
|
||
try {
|
||
// Удаляем данные из localStorage
|
||
localStorage.removeItem('panelSizes');
|
||
// Можно также удалить любые другие ключи, связанные с настройками интерфейса
|
||
// localStorage.removeItem('columnWidths_posts'); // Если бы использовались
|
||
// localStorage.removeItem('columnWidths_events'); // Если бы использовались
|
||
// Применяем стандартный макет
|
||
applyDefaultLayout();
|
||
alert("Настройки интерфейса успешно сброшены.");
|
||
} catch (e) {
|
||
console.error("Ошибка при сбросе настроек:", e);
|
||
alert("Произошла ошибка при сбросе настроек.");
|
||
}
|
||
}
|
||
}
|
||
// Table sorting and column resizing setup
|
||
function setupTableSorting() {
|
||
// Вспомогательная функция для сортировки строк
|
||
function sortRows(rows, columnIndex, direction) {
|
||
return rows.sort((a, b) => {
|
||
let aValue = a.cells[columnIndex].textContent.trim();
|
||
let bValue = b.cells[columnIndex].textContent.trim();
|
||
// Пытаемся преобразовать в числа, если возможно
|
||
if (!isNaN(aValue) && !isNaN(bValue)) {
|
||
aValue = parseFloat(aValue);
|
||
bValue = parseFloat(bValue);
|
||
}
|
||
if (direction === 'asc') {
|
||
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
|
||
} else {
|
||
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Функция для настройки разделителей колонок
|
||
function setupColumnResizers(headers, tableType) {
|
||
headers.forEach((header, index) => {
|
||
// Удаляем существующий разделитель, если есть
|
||
const existingResizer = header.querySelector('.column-resizer');
|
||
if (existingResizer) {
|
||
existingResizer.remove();
|
||
}
|
||
|
||
// Создаем новый разделитель
|
||
const resizer = document.createElement('div');
|
||
resizer.classList.add('column-resizer');
|
||
header.style.position = 'relative';
|
||
header.appendChild(resizer);
|
||
|
||
// Настраиваем обработчик изменения ширины колонки
|
||
setupColumnResizer(resizer, header, tableType);
|
||
});
|
||
}
|
||
|
||
// Настройка сортировки для таблицы постов
|
||
const postsHeaderTable = document.querySelector('#posts-tab-pane .table-header-container table');
|
||
const postsBodyTable = document.querySelector('#posts-tab-pane .scrollable-table-body table');
|
||
if (postsHeaderTable && postsBodyTable) {
|
||
const postsHeaders = postsHeaderTable.querySelectorAll('thead th');
|
||
postsHeaders.forEach((header, index) => {
|
||
if (header.classList.contains('text-end')) return; // Пропускаем колонку с действиями
|
||
header.classList.add('sortable-header');
|
||
header.addEventListener('click', () => {
|
||
// Определяем текущее направление сортировки
|
||
let direction = 'asc';
|
||
if (header.classList.contains('asc')) {
|
||
direction = 'desc';
|
||
} else if (header.classList.contains('desc')) {
|
||
direction = 'asc';
|
||
}
|
||
// Сбрасываем сортировку у всех заголовков
|
||
postsHeaders.forEach(h => {
|
||
h.classList.remove('asc', 'desc');
|
||
});
|
||
// Устанавливаем классы для визуального отображения
|
||
header.classList.add(direction);
|
||
// Получаем строки из тела таблицы
|
||
const tbody = postsBodyTable.querySelector('tbody');
|
||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
// Сортируем строки
|
||
const sortedRows = sortRows(rows, index, direction);
|
||
// Очищаем и вставляем отсортированные строки
|
||
while (tbody.firstChild) {
|
||
tbody.removeChild(tbody.firstChild);
|
||
}
|
||
sortedRows.forEach(row => tbody.appendChild(row));
|
||
// Синхронизируем ширины после сортировки
|
||
syncColumnWidths('posts');
|
||
});
|
||
});
|
||
// Настраиваем разделители для всех колонок
|
||
setupColumnResizers(postsHeaders, 'posts');
|
||
}
|
||
|
||
// Настройка сортировки для таблицы событий
|
||
const eventsHeaderTable = document.querySelector('#events-tab-pane .table-header-container table');
|
||
const eventsBodyTable = document.querySelector('#events-tab-pane .scrollable-table-body table');
|
||
if (eventsHeaderTable && eventsBodyTable) {
|
||
const eventsHeaders = eventsHeaderTable.querySelectorAll('thead th');
|
||
eventsHeaders.forEach((header, index) => {
|
||
if (header.classList.contains('text-end')) return; // Пропускаем колонку с действиями
|
||
header.classList.add('sortable-header');
|
||
header.addEventListener('click', () => {
|
||
// Определяем текущее направление сортировки
|
||
let direction = 'asc';
|
||
if (header.classList.contains('asc')) {
|
||
direction = 'desc';
|
||
} else if (header.classList.contains('desc')) {
|
||
direction = 'asc';
|
||
}
|
||
// Сбрасываем сортировку у всех заголовков
|
||
eventsHeaders.forEach(h => {
|
||
h.classList.remove('asc', 'desc');
|
||
});
|
||
// Устанавливаем классы для визуального отображения
|
||
header.classList.add(direction);
|
||
// Получаем строки из тела таблицы
|
||
const tbody = eventsBodyTable.querySelector('tbody');
|
||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
// Сортируем строки
|
||
const sortedRows = sortRows(rows, index, direction);
|
||
// Очищаем и вставляем отсортированные строки
|
||
while (tbody.firstChild) {
|
||
tbody.removeChild(tbody.firstChild);
|
||
}
|
||
sortedRows.forEach(row => tbody.appendChild(row));
|
||
// Синхронизируем ширины после сортировки
|
||
syncColumnWidths('events');
|
||
});
|
||
});
|
||
// Настраиваем разделители для всех колонок
|
||
setupColumnResizers(eventsHeaders, 'events');
|
||
}
|
||
}
|
||
|
||
// DOM initialization
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Инициализируем функционал после полной загрузки страницы
|
||
window.addEventListener('load', () => {
|
||
setupTableSorting();
|
||
setupPanelResizing();
|
||
loadPanelSizes();
|
||
// Добавляем обработчик события для кнопки сброса
|
||
const resetBtn = document.getElementById('reset-layout-btn');
|
||
if (resetBtn) {
|
||
resetBtn.addEventListener('click', (e) => {
|
||
e.preventDefault(); // Предотвращаем переход по ссылке
|
||
resetLayout();
|
||
});
|
||
}
|
||
// Синхронизируем ширины колонок после инициализации
|
||
setTimeout(() => {
|
||
syncColumnWidths('posts');
|
||
syncColumnWidths('events');
|
||
}, 100);
|
||
});
|
||
|
||
// Функция для пересчета ширины панелей при изменении размера окна
|
||
function adjustPanelWidthsOnResize() {
|
||
// Получаем доступную ширину из активной вкладки (она видима)
|
||
const activeTabPane = document.querySelector('.tab-pane.show.active');
|
||
if (!activeTabPane) return;
|
||
|
||
const activeContainer = activeTabPane.querySelector('.row');
|
||
if (!activeContainer) return;
|
||
|
||
const availableWidth = activeContainer.offsetWidth;
|
||
|
||
// Обновляем размеры всех вкладок с одинаковой доступной шириной
|
||
const allTabPanes = document.querySelectorAll('.tab-pane');
|
||
|
||
allTabPanes.forEach(tabPane => {
|
||
const leftPanel = tabPane.querySelector('.resizable-panel:first-child');
|
||
const rightPanel = tabPane.querySelector('.resizable-panel:last-child');
|
||
|
||
if (!leftPanel || !rightPanel) return;
|
||
|
||
// Применяем пропорциональные размеры (40% левая, 60% правая)
|
||
const leftWidth = Math.max(100, availableWidth * 0.4);
|
||
const rightWidth = Math.max(100, availableWidth * 0.6);
|
||
|
||
// Применяем новые ширины
|
||
leftPanel.style.width = `${leftWidth}px`;
|
||
rightPanel.style.width = `${rightWidth}px`;
|
||
});
|
||
|
||
// Синхронизируем колонки для активной вкладки
|
||
setTimeout(() => {
|
||
if (activeTabPane) {
|
||
const tabId = activeTabPane.id;
|
||
if (tabId === 'posts-tab-pane') {
|
||
syncColumnWidths('posts');
|
||
} else if (tabId === 'events-tab-pane') {
|
||
syncColumnWidths('events');
|
||
}
|
||
}
|
||
}, 0);
|
||
}
|
||
|
||
// Debounce для оптимизации производительности
|
||
let resizeTimeout;
|
||
|
||
// Обработчик изменения размера окна браузера
|
||
window.addEventListener('resize', () => {
|
||
// Очищаем предыдущий таймер
|
||
if (resizeTimeout) {
|
||
clearTimeout(resizeTimeout);
|
||
}
|
||
|
||
// Устанавливаем новый таймер
|
||
resizeTimeout = setTimeout(() => {
|
||
// Пересчитываем ширину панелей при изменении размера окна
|
||
adjustPanelWidthsOnResize();
|
||
}, 100); // Задержка 100ms
|
||
});
|
||
});
|
||
// Убедимся, что функция доступна глобально, если потребуется вызвать из HTML
|
||
window.resetLayout = resetLayout;
|
||
window.applyDefaultLayout = applyDefaultLayout;
|
||
</script>
|
||
</body>
|
||
</html>
|