fix: Accordion icon toggle using Bootstrap events

- Use hidden.bs.collapse and shown.bs.collapse events instead of click
- Icons now toggle correctly between up/down chevrons
- Cookie state properly saved on toggle
This commit is contained in:
mirivlad 2026-04-17 17:26:34 +08:00
parent 4a8a2d66fb
commit 98f6244eb3
1 changed files with 56 additions and 74 deletions

View File

@ -79,12 +79,11 @@
</div> </div>
{% else %} {% else %}
{% for groupName, group in groups %} {% for groupName, group in groups %}
{% set groupSlug = groupName|lower|replace({' ': '_', ' ': ''}) %} {% set groupSlug = groupName|lower|replace({' ': '-'}) %}
<div class="card mb-3 border-0 shadow-sm"> <div class="card mb-3 border-0 shadow-sm">
<div class="card-header text-white py-2 accordion-header" <div class="card-header text-white py-2 accordion-header"
data-bs-toggle="collapse" data-bs-toggle="collapse"
data-bs-target="#group-{{ loop.index }}" data-bs-target="#group-{{ loop.index }}"
data-group="{{ groupSlug }}"
style="cursor: pointer; background-color: {{ group.color|default('#6c757d') }};"> style="cursor: pointer; background-color: {{ group.color|default('#6c757d') }};">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0"> <h5 class="mb-0">
@ -95,7 +94,7 @@
<i class="fas accordion-icon"></i> <i class="fas accordion-icon"></i>
</div> </div>
</div> </div>
<div id="group-{{ loop.index }}" class="collapse" data-group="{{ groupSlug }}"> <div id="group-{{ loop.index }}" class="collapse" data-group-slug="{{ groupSlug }}">
<div class="card-body p-2"> <div class="card-body p-2">
<div class="row g-2"> <div class="row g-2">
{% for server in group.servers %} {% for server in group.servers %}
@ -203,7 +202,6 @@
(function() { (function() {
const COOKIE_NAME = 'dashboard_accordion'; const COOKIE_NAME = 'dashboard_accordion';
// Получить состояние accordion из cookies
function getAccordionState() { function getAccordionState() {
const cookies = document.cookie.split(';'); const cookies = document.cookie.split(';');
for (let c of cookies) { for (let c of cookies) {
@ -217,90 +215,74 @@
return {}; return {};
} }
// Сохранить состояние accordion в cookies
function saveAccordionState(state) { function saveAccordionState(state) {
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(state)) + document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(state)) +
'; path=/; max-age=' + (30 * 24 * 60 * 60); // 30 дней '; path=/; max-age=' + (30 * 24 * 60 * 60);
} }
// Инициализация accordion из cookies function updateIcon(header, isOpen) {
function initAccordionFromCookies() { const icon = header.querySelector('.accordion-icon');
if (icon) {
icon.className = isOpen ? 'fas fa-chevron-up' : 'fas fa-chevron-down';
}
}
document.addEventListener('DOMContentLoaded', function() {
const state = getAccordionState(); const state = getAccordionState();
// Инициализация иконок из cookies
document.querySelectorAll('.accordion-header').forEach(header => { document.querySelectorAll('.accordion-header').forEach(header => {
const groupId = header.dataset.group; const groupId = header.dataset.group;
const targetId = header.dataset.bsTarget; const isOpen = state[groupId] === 'open';
const target = document.querySelector(targetId); updateIcon(header, isOpen);
if (target && state[groupId] === 'open') {
target.classList.add('show');
header.querySelector('.accordion-icon').className = 'fas fa-chevron-up';
} else {
header.querySelector('.accordion-icon').className = 'fas fa-chevron-down';
}
}); });
}
// Обработчик клика на accordion header // Слушаем события Bootstrap collapse
function setupAccordionHandlers() { document.querySelectorAll('.collapse').forEach(el => {
const state = getAccordionState(); el.addEventListener('hidden.bs.collapse', function() {
const groupId = this.dataset.groupSlug;
document.querySelectorAll('.accordion-header').forEach(header => { state[groupId] = 'closed';
const groupId = header.dataset.group;
const targetId = header.dataset.bsTarget;
const target = document.querySelector(targetId);
const icon = header.querySelector('.accordion-icon');
// Клик переключает состояние
header.addEventListener('click', function() {
const isOpen = target.classList.contains('show');
if (isOpen) {
target.classList.remove('show');
icon.className = 'fas fa-chevron-down';
state[groupId] = 'closed';
} else {
target.classList.add('show');
icon.className = 'fas fa-chevron-up';
state[groupId] = 'open';
}
saveAccordionState(state); saveAccordionState(state);
const header = document.querySelector('.accordion-header[data-bs-target="#' + this.id + '"]');
if (header) updateIcon(header, false);
});
el.addEventListener('shown.bs.collapse', function() {
const groupId = this.dataset.groupSlug;
state[groupId] = 'open';
saveAccordionState(state);
const header = document.querySelector('.accordion-header[data-bs-target="#' + this.id + '"]');
if (header) updateIcon(header, true);
}); });
}); });
}
// AJAX обновление данных // AJAX обновление
function updateDashboard() { function updateDashboard() {
fetch('/api/dashboard/stats') fetch('/api/dashboard/stats')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
data.forEach(server => { data.forEach(server => {
const cpuVal = document.getElementById('cpu-val-' + server.id); const cpuVal = document.getElementById('cpu-val-' + server.id);
if (cpuVal && server.metrics.cpu_load) { if (cpuVal && server.metrics.cpu_load) {
cpuVal.textContent = server.metrics.cpu_load.value + '%'; cpuVal.textContent = server.metrics.cpu_load.value + '%';
} }
const ramVal = document.getElementById('ram-val-' + server.id);
if (ramVal && server.metrics.ram_used) {
ramVal.textContent = server.metrics.ram_used.value + '%';
}
const diskVal = document.getElementById('disk-val-' + server.id);
if (diskVal && server.metrics.disk) {
diskVal.textContent = server.metrics.disk.value + '%';
}
const updatedAt = document.getElementById('updated-at-' + server.id);
if (updatedAt && server.updated_at) {
updatedAt.textContent = server.updated_at.split(' ')[1].substring(0, 5);
}
});
})
.catch(err => console.log('Dashboard update error:', err));
}
const ramVal = document.getElementById('ram-val-' + server.id);
if (ramVal && server.metrics.ram_used) {
ramVal.textContent = server.metrics.ram_used.value + '%';
}
const diskVal = document.getElementById('disk-val-' + server.id);
if (diskVal && server.metrics.disk) {
diskVal.textContent = server.metrics.disk.value + '%';
}
const updatedAt = document.getElementById('updated-at-' + server.id);
if (updatedAt && server.updated_at) {
updatedAt.textContent = server.updated_at.split(' ')[1].substring(0, 5);
}
});
})
.catch(err => console.log('Dashboard update error:', err));
}
// Запуск при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
initAccordionFromCookies();
setupAccordionHandlers();
setInterval(updateDashboard, 30000); setInterval(updateDashboard, 30000);
}); });
})(); })();