优化结构
This commit is contained in:
@@ -646,6 +646,34 @@ input:checked+.slider:before {
|
|||||||
background: rgba(99, 102, 241, 0.05);
|
background: rgba(99, 102, 241, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon-sm {
|
||||||
|
background: var(--bg-input);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-sm:hover {
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
color: var(--accent-indigo);
|
||||||
|
background: rgba(99, 102, 241, 0.05);
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon-sm svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-title {
|
.chart-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -242,7 +242,8 @@
|
|||||||
<h2 class="chart-title">
|
<h2 class="chart-title">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="chart-title-icon">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="chart-title-icon">
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
<path
|
||||||
|
d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
||||||
</svg>
|
</svg>
|
||||||
全球服务器分布
|
全球服务器分布
|
||||||
</h2>
|
</h2>
|
||||||
@@ -279,6 +280,13 @@
|
|||||||
服务器详情
|
服务器详情
|
||||||
</h2>
|
</h2>
|
||||||
<div class="chart-header-right">
|
<div class="chart-header-right">
|
||||||
|
<button id="btnResetSort" class="btn-icon-sm" title="重置排序">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round">
|
||||||
|
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
|
||||||
|
<path d="M3 3v5h5"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<select id="sourceFilter" class="source-select">
|
<select id="sourceFilter" class="source-select">
|
||||||
<option value="all">所有数据源</option>
|
<option value="all">所有数据源</option>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
109
public/js/app.js
109
public/js/app.js
@@ -82,7 +82,8 @@
|
|||||||
globeContainer: document.getElementById('globeContainer'),
|
globeContainer: document.getElementById('globeContainer'),
|
||||||
globeTotalNodes: document.getElementById('globeTotalNodes'),
|
globeTotalNodes: document.getElementById('globeTotalNodes'),
|
||||||
globeTotalRegions: document.getElementById('globeTotalRegions'),
|
globeTotalRegions: document.getElementById('globeTotalRegions'),
|
||||||
sourceFilter: document.getElementById('sourceFilter')
|
sourceFilter: document.getElementById('sourceFilter'),
|
||||||
|
btnResetSort: document.getElementById('btnResetSort')
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
@@ -94,7 +95,18 @@
|
|||||||
let currentSourceFilter = 'all';
|
let currentSourceFilter = 'all';
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let pageSize = 20;
|
let pageSize = 20;
|
||||||
|
|
||||||
|
// Load sort state from localStorage or use default
|
||||||
let currentSort = { column: 'up', direction: 'desc' };
|
let currentSort = { column: 'up', direction: 'desc' };
|
||||||
|
try {
|
||||||
|
const savedSort = localStorage.getItem('serverListSort');
|
||||||
|
if (savedSort) {
|
||||||
|
currentSort = JSON.parse(savedSort);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load sort state', e);
|
||||||
|
}
|
||||||
|
|
||||||
let myMap2D = null;
|
let myMap2D = null;
|
||||||
|
|
||||||
// ---- Initialize ----
|
// ---- Initialize ----
|
||||||
@@ -177,6 +189,19 @@
|
|||||||
// Server table header sorting
|
// Server table header sorting
|
||||||
const tableHeader = document.querySelector('.server-table thead');
|
const tableHeader = document.querySelector('.server-table thead');
|
||||||
if (tableHeader) {
|
if (tableHeader) {
|
||||||
|
// Sync UI with initial state
|
||||||
|
const initialHeaders = tableHeader.querySelectorAll('th.sortable');
|
||||||
|
initialHeaders.forEach(th => {
|
||||||
|
const col = th.getAttribute('data-sort');
|
||||||
|
if (col === currentSort.column) {
|
||||||
|
th.classList.add('active');
|
||||||
|
th.setAttribute('data-dir', currentSort.direction);
|
||||||
|
} else {
|
||||||
|
th.classList.remove('active');
|
||||||
|
th.removeAttribute('data-dir');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tableHeader.addEventListener('click', (e) => {
|
tableHeader.addEventListener('click', (e) => {
|
||||||
const th = e.target.closest('th.sortable');
|
const th = e.target.closest('th.sortable');
|
||||||
if (th) {
|
if (th) {
|
||||||
@@ -213,6 +238,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset sort listener
|
||||||
|
if (dom.btnResetSort) {
|
||||||
|
dom.btnResetSort.addEventListener('click', resetSort);
|
||||||
|
}
|
||||||
|
|
||||||
// Check auth status
|
// Check auth status
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
|
|
||||||
@@ -583,20 +613,27 @@
|
|||||||
|
|
||||||
// Secondary sort based on user choice
|
// Secondary sort based on user choice
|
||||||
let valA, valB;
|
let valA, valB;
|
||||||
switch (currentSort.column) {
|
const col = currentSort.column;
|
||||||
|
const dir = currentSort.direction;
|
||||||
|
|
||||||
|
switch (col) {
|
||||||
case 'up':
|
case 'up':
|
||||||
return 0; // Already handled above
|
// If we reached here, status is the same (both online or both offline)
|
||||||
case 'job':
|
// Fall back to job name sorting
|
||||||
valA = a.job || '';
|
valA = a.job || '';
|
||||||
valB = b.job || '';
|
valB = b.job || '';
|
||||||
return currentSort.direction === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
return valA.localeCompare(valB);
|
||||||
|
case 'job':
|
||||||
|
valA = (a.job || '').toLowerCase();
|
||||||
|
valB = (b.job || '').toLowerCase();
|
||||||
|
return dir === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
||||||
case 'source':
|
case 'source':
|
||||||
valA = a.source || '';
|
valA = (a.source || '').toLowerCase();
|
||||||
valB = b.source || '';
|
valB = (b.source || '').toLowerCase();
|
||||||
return currentSort.direction === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
return dir === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
||||||
case 'cpu':
|
case 'cpu':
|
||||||
valA = a.cpuPercent || 0;
|
valA = a.cpuPercent ?? 0;
|
||||||
valB = b.cpuPercent || 0;
|
valB = b.cpuPercent ?? 0;
|
||||||
break;
|
break;
|
||||||
case 'mem':
|
case 'mem':
|
||||||
valA = a.memTotal > 0 ? (a.memUsed / a.memTotal) : 0;
|
valA = a.memTotal > 0 ? (a.memUsed / a.memTotal) : 0;
|
||||||
@@ -607,21 +644,25 @@
|
|||||||
valB = b.diskTotal > 0 ? (b.diskUsed / b.diskTotal) : 0;
|
valB = b.diskTotal > 0 ? (b.diskUsed / b.diskTotal) : 0;
|
||||||
break;
|
break;
|
||||||
case 'netRx':
|
case 'netRx':
|
||||||
valA = a.netRx || 0;
|
valA = a.netRx ?? 0;
|
||||||
valB = b.netRx || 0;
|
valB = b.netRx ?? 0;
|
||||||
break;
|
break;
|
||||||
case 'netTx':
|
case 'netTx':
|
||||||
valA = a.netTx || 0;
|
valA = a.netTx ?? 0;
|
||||||
valB = b.netTx || 0;
|
valB = b.netTx ?? 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
valA = a.job || '';
|
valA = (a.job || '').toLowerCase();
|
||||||
valB = b.job || '';
|
valB = (b.job || '').toLowerCase();
|
||||||
return valA.localeCompare(valB);
|
return valA.localeCompare(valB);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSort.direction === 'asc') return valA - valB;
|
// Numeric comparison
|
||||||
return valB - valA;
|
if (valA === valB) {
|
||||||
|
// If values are same, secondary fallback to job name for stable sort
|
||||||
|
return (a.job || '').localeCompare(b.job || '');
|
||||||
|
}
|
||||||
|
return dir === 'asc' ? valA - valB : valB - valA;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalFiltered = filtered.length;
|
const totalFiltered = filtered.length;
|
||||||
@@ -668,15 +709,43 @@
|
|||||||
renderFilteredServers();
|
renderFilteredServers();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resetSort() {
|
||||||
|
currentSort = { column: 'up', direction: 'desc' };
|
||||||
|
localStorage.removeItem('serverListSort');
|
||||||
|
|
||||||
|
// Update UI headers
|
||||||
|
const headers = document.querySelectorAll('.server-table th.sortable');
|
||||||
|
headers.forEach(th => {
|
||||||
|
const col = th.getAttribute('data-sort');
|
||||||
|
if (col === currentSort.column) {
|
||||||
|
th.classList.add('active');
|
||||||
|
th.setAttribute('data-dir', currentSort.direction);
|
||||||
|
} else {
|
||||||
|
th.classList.remove('active');
|
||||||
|
th.removeAttribute('data-dir');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderFilteredServers();
|
||||||
|
}
|
||||||
|
|
||||||
function handleHeaderSort(column) {
|
function handleHeaderSort(column) {
|
||||||
if (currentSort.column === column) {
|
if (currentSort.column === column) {
|
||||||
// Toggle direction
|
if (currentSort.direction === 'desc') {
|
||||||
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
currentSort.direction = 'asc';
|
||||||
|
} else {
|
||||||
|
// Cycle back to default (Status desc)
|
||||||
|
resetSort();
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentSort.column = column;
|
currentSort.column = column;
|
||||||
currentSort.direction = 'desc'; // Default to desc for most metrics
|
currentSort.direction = 'desc'; // Default to desc for most metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist sort state
|
||||||
|
localStorage.setItem('serverListSort', JSON.stringify(currentSort));
|
||||||
|
|
||||||
// Update UI headers
|
// Update UI headers
|
||||||
const headers = document.querySelectorAll('.server-table th.sortable');
|
const headers = document.querySelectorAll('.server-table th.sortable');
|
||||||
headers.forEach(th => {
|
headers.forEach(th => {
|
||||||
|
|||||||
Reference in New Issue
Block a user