feat: Web服务和Docker监控区支持折叠/展开

- 所有分组标题(Web服务/自定义服务/归档服务等)可点击折叠
- Docker容器列表、镜像列表标题可点击折叠
- 点击标题切换 chevron 旋转动画(→↓)
- 折叠状态持久化到 localStorage,刷新保持
This commit is contained in:
2026-06-01 22:35:57 +08:00
parent f7722be539
commit 0c54af8d26

View File

@@ -67,6 +67,9 @@
.history-item.toggle { border-left-color: #8b5cf6; } .history-item.toggle { border-left-color: #8b5cf6; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; } .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; }
.modal-content { max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; } .modal-content { max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; }
.section-header { cursor: pointer; user-select: none; }
.section-header:hover { opacity: 0.85; }
.chevron { display: inline-block; transition: transform 0.2s ease; font-size: 14px; }
</style> </style>
</head> </head>
<body class="min-h-screen text-gray-100"> <body class="min-h-screen text-gray-100">
@@ -555,20 +558,20 @@
</div> </div>
<!-- 容器列表 --> <!-- 容器列表 -->
<h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2"> <h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2 section-header" onclick="toggleSection('docker-containers-grid', this.querySelector('.chevron'))">
<i class="ri-computer-line text-blue-400"></i> 容器列表 <i class="ri-arrow-down-s-line chevron"></i><i class="ri-computer-line text-blue-400"></i> 容器列表
<span id="dockerContainerCount" class="text-xs text-gray-500"></span> <span id="dockerContainerCount" class="text-xs text-gray-500"></span>
</h3> </h3>
<div id="dockerContainersList" class="space-y-2 mb-6"> <div id="docker-containers-grid" class="space-y-2 mb-6">
<div class="text-gray-500 text-sm py-4 text-center">加载中...</div> <div class="text-gray-500 text-sm py-4 text-center">加载中...</div>
</div> </div>
<!-- 镜像列表 --> <!-- 镜像列表 -->
<h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2"> <h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2 section-header" onclick="toggleSection('docker-images-grid', this.querySelector('.chevron'))">
<i class="ri-stack-line text-purple-400"></i> 镜像列表 <i class="ri-arrow-down-s-line chevron"></i><i class="ri-stack-line text-purple-400"></i> 镜像列表
<span id="dockerImageCount" class="text-xs text-gray-500"></span> <span id="dockerImageCount" class="text-xs text-gray-500"></span>
</h3> </h3>
<div id="dockerImagesList" class="space-y-1"> <div id="docker-images-grid" class="space-y-1">
<div class="text-gray-500 text-sm py-4 text-center">加载中...</div> <div class="text-gray-500 text-sm py-4 text-center">加载中...</div>
</div> </div>
</div> </div>
@@ -935,6 +938,40 @@
let thresholds = { cpu: 80, cpu_temp: 80, memory: 85, disk: 90, interval: 60 }; let thresholds = { cpu: 80, cpu_temp: 80, memory: 85, disk: 90, interval: 60 };
let desktopNotifyEnabled = false; let desktopNotifyEnabled = false;
// ==================== 折叠/展开 ====================
function getCollapsedState() {
const stored = localStorage.getItem('collapsedSections');
return stored ? JSON.parse(stored) : {};
}
function saveCollapsedState(state) {
localStorage.setItem('collapsedSections', JSON.stringify(state));
}
function toggleSection(sectionId, chevronEl) {
const grid = document.getElementById(sectionId);
if (!grid) return;
const state = getCollapsedState();
if (grid.classList.contains('hidden')) {
grid.classList.remove('hidden');
if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
state[sectionId] = false;
} else {
grid.classList.add('hidden');
if (chevronEl) chevronEl.style.transform = 'rotate(-90deg)';
state[sectionId] = true;
}
saveCollapsedState(state);
}
function applyCollapsedState(sectionId, gridEl, chevronEl) {
const state = getCollapsedState();
if (state[sectionId]) {
gridEl.classList.add('hidden');
if (chevronEl) chevronEl.style.transform = 'rotate(-90deg)';
} else {
gridEl.classList.remove('hidden');
if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
}
}
function switchTab(tab) { function switchTab(tab) {
currentTab = tab; currentTab = tab;
document.querySelectorAll('.tab-btn').forEach(btn => { document.querySelectorAll('.tab-btn').forEach(btn => {
@@ -1445,23 +1482,38 @@
const archivedProjs = projs.filter(p => activeStatus[p.id] === false); const archivedProjs = projs.filter(p => activeStatus[p.id] === false);
if (activeProjs.length > 0) { if (activeProjs.length > 0) {
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2"><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`; const secId = 'section-web-active';
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
activeProjs.forEach(p => { html += renderProjectCard(p, true); }); activeProjs.forEach(p => { html += renderProjectCard(p, true); });
html += '</div></div>'; html += '</div></div>';
} }
if (archivedProjs.length > 0) { if (archivedProjs.length > 0) {
html += `<div class="mb-6 opacity-60"><h2 class="text-lg font-semibold text-gray-400 mb-3 flex items-center gap-2"><i class="ri-archive-line text-gray-400"></i>归档服务<span class="text-sm text-gray-500">(${archivedProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`; const secId = 'section-web-archived';
html += `<div class="mb-6 opacity-60"><h2 class="text-lg font-semibold text-gray-400 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="ri-archive-line text-gray-400"></i>归档服务<span class="text-sm text-gray-500">(${archivedProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
archivedProjs.forEach(p => { html += renderProjectCard(p, false); }); archivedProjs.forEach(p => { html += renderProjectCard(p, false); });
html += '</div></div>'; html += '</div></div>';
} }
} else { } else {
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2"><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${projs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`; const secId = 'section-' + type;
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${projs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
projs.forEach(p => { html += renderProjectCard(p, true); }); projs.forEach(p => { html += renderProjectCard(p, true); });
html += '</div></div>'; html += '</div></div>';
} }
} }
list.innerHTML = html; list.innerHTML = html;
// 应用已保存的折叠状态
setTimeout(() => {
const state = getCollapsedState();
for (const [secId, collapsed] of Object.entries(state)) {
const grid = document.getElementById(secId);
const chevron = grid?.parentElement?.querySelector('.chevron');
if (grid && collapsed) {
grid.classList.add('hidden');
if (chevron) chevron.style.transform = 'rotate(-90deg)';
}
}
}, 10);
} }
function getActiveStatus() { function getActiveStatus() {
@@ -2828,7 +2880,7 @@
} }
function renderDockerContainers(containers) { function renderDockerContainers(containers) {
const list = document.getElementById('dockerContainersList'); const list = document.getElementById('docker-containers-grid');
if (containers.length === 0) { if (containers.length === 0) {
list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无容器</div>'; list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无容器</div>';
return; return;
@@ -2860,7 +2912,7 @@
} }
function renderDockerImages(images) { function renderDockerImages(images) {
const list = document.getElementById('dockerImagesList'); const list = document.getElementById('docker-images-grid');
if (images.length === 0) { if (images.length === 0) {
list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无镜像</div>'; list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无镜像</div>';
return; return;