418 lines
13 KiB
HTML
418 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>服务器监控面板</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
color: #333;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
color: white;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5em;
|
|
margin-bottom: 10px;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.1em;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 8px 20px;
|
|
background: rgba(255,255,255,0.2);
|
|
border-radius: 20px;
|
|
margin-top: 15px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 15px;
|
|
border-bottom: 2px solid #f0f0f0;
|
|
}
|
|
|
|
.card-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 24px;
|
|
margin-right: 15px;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 1.3em;
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.metric {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding: 12px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.metric-label {
|
|
color: #6c757d;
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
.metric-value {
|
|
font-weight: 600;
|
|
color: #2c3e50;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.progress-container {
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 8px;
|
|
background: #e9ecef;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.progress-text {
|
|
text-align: right;
|
|
margin-top: 5px;
|
|
font-size: 0.85em;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.latency-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 12px;
|
|
margin-bottom: 10px;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.latency-host {
|
|
font-weight: 500;
|
|
color: #2c3e50;
|
|
}
|
|
|
|
.latency-value {
|
|
font-weight: 600;
|
|
padding: 4px 12px;
|
|
border-radius: 6px;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.latency-good {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
|
|
.latency-medium {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
|
|
.latency-bad {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.latency-offline {
|
|
background: #e2e3e5;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.refresh-btn {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
border: none;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
cursor: pointer;
|
|
font-size: 24px;
|
|
transition: transform 0.3s ease;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.refresh-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.refresh-btn.loading {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.last-update {
|
|
text-align: center;
|
|
color: white;
|
|
margin-top: 20px;
|
|
font-size: 0.9em;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.error-message {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.header h1 {
|
|
font-size: 1.8em;
|
|
}
|
|
|
|
.grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.refresh-btn {
|
|
bottom: 20px;
|
|
right: 20px;
|
|
width: 50px;
|
|
height: 50px;
|
|
font-size: 20px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🖥️ 服务器监控面板</h1>
|
|
<p>实时监控服务器状态与性能</p>
|
|
<div class="status-badge" id="statusBadge">正在加载...</div>
|
|
</div>
|
|
|
|
<div id="errorMessage"></div>
|
|
|
|
<div class="grid">
|
|
<!-- 服务器负载 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
|
📊
|
|
</div>
|
|
<div class="card-title">服务器负载</div>
|
|
</div>
|
|
<div id="loadMetrics"></div>
|
|
</div>
|
|
|
|
<!-- 网络延迟 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-icon" style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%);">
|
|
🌐
|
|
</div>
|
|
<div class="card-title">网络延迟</div>
|
|
</div>
|
|
<div id="latencyMetrics"></div>
|
|
</div>
|
|
|
|
<!-- 服务器响应时间 -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-icon" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
|
⚡
|
|
</div>
|
|
<div class="card-title">服务器响应时间</div>
|
|
</div>
|
|
<div id="responseTimeMetrics"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="last-update" id="lastUpdate">最后更新: --</div>
|
|
</div>
|
|
|
|
<button class="refresh-btn" id="refreshBtn" onclick="fetchServerData()">
|
|
🔄
|
|
</button>
|
|
|
|
<script>
|
|
async function fetchServerData() {
|
|
const refreshBtn = document.getElementById('refreshBtn');
|
|
const statusBadge = document.getElementById('statusBadge');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
|
|
// 记录前端发起请求的时间(用于计算用户网络延迟)
|
|
const frontendStartTime = performance.now();
|
|
|
|
refreshBtn.classList.add('loading');
|
|
statusBadge.textContent = '正在更新...';
|
|
errorMessage.innerHTML = '';
|
|
|
|
try {
|
|
const response = await fetch('server_monitor_api.php');
|
|
const data = await response.json();
|
|
|
|
// 计算用户网络延迟(前端发起请求到收到响应的总时间)
|
|
const frontendEndTime = performance.now();
|
|
const userNetworkLatency = (frontendEndTime - frontendStartTime).toFixed(2);
|
|
|
|
if (data.status === 'success') {
|
|
updateDisplay(data, userNetworkLatency);
|
|
statusBadge.textContent = '✅ 运行正常';
|
|
statusBadge.style.background = 'rgba(40, 167, 69, 0.2)';
|
|
} else {
|
|
throw new Error('服务器返回错误状态');
|
|
}
|
|
} catch (error) {
|
|
console.error('获取服务器数据失败:', error);
|
|
errorMessage.innerHTML = `<div class="error-message">❌ 获取服务器数据失败: ${error.message}</div>`;
|
|
statusBadge.textContent = '❌ 连接失败';
|
|
statusBadge.style.background = 'rgba(220, 53, 69, 0.2)';
|
|
} finally {
|
|
refreshBtn.classList.remove('loading');
|
|
}
|
|
}
|
|
|
|
function updateDisplay(data, userNetworkLatency) {
|
|
// 更新时间戳
|
|
const timestamp = data.timestamp;
|
|
document.getElementById('lastUpdate').textContent =
|
|
`最后更新: ${timestamp.datetime} (${timestamp.timezone})`;
|
|
|
|
// 更新服务器负载
|
|
const load = data.server.load;
|
|
document.getElementById('loadMetrics').innerHTML = `
|
|
<div class="metric">
|
|
<span class="metric-label">1分钟负载</span>
|
|
<span class="metric-value">${load['1min']}</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">5分钟负载</span>
|
|
<span class="metric-value">${load['5min']}</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">15分钟负载</span>
|
|
<span class="metric-value">${load['15min']}</span>
|
|
</div>
|
|
`;
|
|
|
|
// 更新网络延迟
|
|
const latency = data.network.latency;
|
|
let latencyHTML = '';
|
|
latency.forEach(item => {
|
|
const latencyClass = getLatencyClass(item.latency, item.status);
|
|
latencyHTML += `
|
|
<div class="latency-item">
|
|
<div>
|
|
<div class="latency-host">${item.host}</div>
|
|
<div style="font-size: 0.8em; color: #6c757d;">${item.ip}</div>
|
|
</div>
|
|
<div class="latency-value ${latencyClass}">
|
|
${item.status === 'online' ? item.latency + ' ms' : '离线'}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
document.getElementById('latencyMetrics').innerHTML = latencyHTML;
|
|
|
|
// 更新服务器响应时间和用户网络延迟
|
|
const serverResponseTime = data.network.server_response_time;
|
|
document.getElementById('responseTimeMetrics').innerHTML = `
|
|
<div class="metric">
|
|
<span class="metric-label">服务器响应时间</span>
|
|
<span class="metric-value">${serverResponseTime} ms</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">用户网络延迟</span>
|
|
<span class="metric-value">${userNetworkLatency} ms</span>
|
|
</div>
|
|
<div class="metric">
|
|
<span class="metric-label">总延迟</span>
|
|
<span class="metric-value">${(parseFloat(serverResponseTime) + parseFloat(userNetworkLatency)).toFixed(2)} ms</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function getLatencyClass(latency, status) {
|
|
if (status === 'offline') return 'latency-offline';
|
|
if (latency < 50) return 'latency-good';
|
|
if (latency < 150) return 'latency-medium';
|
|
return 'latency-bad';
|
|
}
|
|
|
|
// 页面加载时自动获取数据
|
|
window.onload = function() {
|
|
fetchServerData();
|
|
// 每30秒自动刷新
|
|
setInterval(fetchServerData, 30000);
|
|
};
|
|
</script>
|
|
</body>
|
|
</html> |