dashboard/static/index.html
2025-05-21 11:26:54 +08:00

408 lines
16 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Avcnet 系统运维监控</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
<link href="https://unpkg.com/xterm@5.3.0/css/xterm.css" rel="stylesheet" />
<style>
#terminal { height: 400px; }
#file-list { max-height: 300px; overflow-y: auto; }
.tooltip {
display: none;
position: absolute;
background: #1a1a1a;
color: white;
padding: 6px 10px;
border-radius: 4px;
z-index: 10;
max-width: 320px;
font-size: 13px;
line-height: 1.5;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
pointer-events: none; /* 防止提示框干扰鼠标事件 */
}
</style>
</head>
<body class="bg-gray-100 font-sans">
<div class="container mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Avcnet 系统运维监控</h1>
<button id="logout" class="bg-red-500 text-white px-4 py-2 rounded mb-4">登出</button>
<!-- 系统信息 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('cpu-tooltip', event)" onmouseout="hideTooltip('cpu-tooltip')">
<h2 class="text-lg font-semibold">CPU 使用率</h2>
<p id="cpu-usage" class="text-xl">0%</p>
<div id="cpu-tooltip" class="tooltip"></div>
</div>
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('memory-tooltip', event)" onmouseout="hideTooltip('memory-tooltip')">
<h2 class="text-lg font-semibold">内存使用率</h2>
<p id="memory-usage" class="text-xl">0%</p>
<div id="memory-tooltip" class="tooltip"></div>
</div>
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('disk-tooltip', event)" onmouseout="hideTooltip('disk-tooltip')">
<h2 class="text-lg font-semibold">磁盘使用率</h2>
<p id="disk-usage" class="text-xl">0%</p>
<div id="disk-tooltip" class="tooltip"></div>
</div>
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('load-tooltip', event)" onmouseout="hideTooltip('load-tooltip')">
<h2 class="text-lg font-semibold">系统负载</h2>
<p id="load-avg" class="text-xl">0</p>
<div id="load-tooltip" class="tooltip"></div>
</div>
</div>
<!-- 文件上传 -->
<div class="bg-white p-4 rounded shadow mb-8">
<h2 class="text-lg font-semibold mb-2">上传文件</h2>
<div class="mb-4">
<label class="block text-gray-700 mb-2" for="target-dir">目标目录(绝对路径,如 /tmp/uploads</label>
<input id="target-dir" type="text" class="w-full px-3 py-2 border rounded" placeholder="请输入目标目录">
</div>
<div class="mb-4">
<label class="block text-gray-700 mb-2" for="file">选择文件最大100MB</label>
<input id="file" type="file" class="w-full px-3 py-2 border rounded">
</div>
<button id="upload" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">上传</button>
<p id="upload-message" class="mt-4 text-green-500 hidden"></p>
<p id="upload-error" class="mt-4 text-red-500 hidden"></p>
</div>
<!-- 文件浏览 -->
<div class="bg-white p-4 rounded shadow mb-8">
<h2 class="text-lg font-semibold mb-2">浏览文件</h2>
<div class="mb-4 flex items-center">
<label class="block text-gray-700 mr-2" for="browse-dir">当前路径:</label>
<input id="browse-dir" type="text" class="flex-grow px-3 py-2 border rounded" placeholder="请输入目录路径(如 /tmp">
<button id="list-files" class="ml-2 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">刷新</button>
<button id="parent-dir" class="ml-2 bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">上一级</button>
</div>
<div id="file-list" class="border rounded p-2">
<table class="w-full">
<thead>
<tr>
<th class="text-left">名称</th>
<th class="text-left">类型</th>
<th class="text-left">大小</th>
<th class="text-left">修改时间</th>
<th class="text-left">操作</th>
</tr>
</thead>
<tbody id="file-table"></tbody>
</table>
</div>
<p id="file-error" class="mt-4 text-red-500 hidden"></p>
</div>
<!-- 终端 -->
<div class="bg-white p-4 rounded shadow">
<h2 class="text-lg font-semibold mb-2">交互式终端</h2>
<div id="terminal"></div>
</div>
</div>
<script>
// 检查登录状态
async function checkLoginStatus() {
const response = await fetch('/api/system-info');
if (response.status === 401) {
window.location.href = '/login';
}
}
// 格式化字节为MB或GB
function formatBytes(bytes) {
if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + ' KB';
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
}
// 格式化时间(秒)
function formatUptime(seconds) {
const days = Math.floor(seconds / (3600 * 24));
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}${hours}小时 ${minutes}分钟`;
}
// 显示提示框
function showTooltip(tooltipId, event) {
const tooltip = document.getElementById(tooltipId);
const systemInfo = window.lastSystemInfo || {};
// 如果提示框已显示,不重新定位
if (tooltip.style.display === 'block') {
return;
}
let content = '';
if (tooltipId === 'cpu-tooltip') {
content = `
使用率: ${systemInfo.cpu_usage ? systemInfo.cpu_usage.toFixed(2) : 0}% <br>
核心数: ${systemInfo.cpu_cores || '未知'} <br>
主频: ${systemInfo.cpu_mhz ? systemInfo.cpu_mhz.toFixed(2) : '未知'} MHz
`;
} else if (tooltipId === 'memory-tooltip') {
content = `
使用率: ${systemInfo.memory_usage ? systemInfo.memory_usage.toFixed(2) : 0}% <br>
总内存: ${systemInfo.memory_total ? formatBytes(systemInfo.memory_total) : '未知'} <br>
可用内存: ${systemInfo.memory_free ? formatBytes(systemInfo.memory_free) : '未知'}
`;
} else if (tooltipId === 'disk-tooltip') {
content = systemInfo.disk_partitions ? systemInfo.disk_partitions.map(p => `
挂载点: ${p.mountpoint} <br>
使用率: ${p.used_percent.toFixed(2)}% <br>
总空间: ${formatBytes(p.total)} <br>
已用: ${formatBytes(p.used)}
`).join('<hr>') : '无分区信息';
} else if (tooltipId === 'load-tooltip') {
content = `
1分钟负载: ${systemInfo.load_avg ? systemInfo.load_avg.toFixed(2) : 0} <br>
系统运行时间: ${systemInfo.uptime ? formatUptime(systemInfo.uptime) : '未知'}
`;
}
tooltip.innerHTML = content;
tooltip.style.display = 'block';
// 使用鼠标坐标定位(仅计算一次)
const offsetX = 5; // 水平偏移(更近)
const offsetY = 5; // 垂直偏移(更近)
let x = event.clientX + window.scrollX + offsetX;
let y = event.clientY + window.scrollY + offsetY;
// 防止提示框超出视口
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 如果右侧超出,移到鼠标左侧
if (x + tooltipRect.width > viewportWidth) {
x = event.clientX + window.scrollX - tooltipRect.width - offsetX;
}
// 如果下方超出,移到鼠标上方
if (y + tooltipRect.height > viewportHeight) {
y = event.clientY + window.scrollY - tooltipRect.height - offsetY;
}
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
}
// 隐藏提示框
function hideTooltip(tooltipId) {
const tooltip = document.getElementById(tooltipId);
tooltip.style.display = 'none';
}
// 更新系统信息
async function updateSystemInfo() {
try {
const response = await fetch('/api/system-info');
if (response.status === 401) {
window.location.href = '/login';
return;
}
const data = await response.json();
window.lastSystemInfo = data; // 缓存最新数据
document.getElementById('cpu-usage').textContent = data.cpu_usage.toFixed(2) + '%';
document.getElementById('memory-usage').textContent = data.memory_usage.toFixed(2) + '%';
document.getElementById('disk-usage').textContent = data.disk_usage.toFixed(2) + '%';
document.getElementById('load-avg').textContent = data.load_avg.toFixed(2);
} catch (error) {
console.error('Failed to fetch system info:', error);
}
}
// 初始化终端
const term = new Terminal({
cursorBlink: true,
theme: {
background: '#1a1a1a',
foreground: '#ffffff',
},
});
term.open(document.getElementById('terminal'));
// 连接 WebSocket
const ws = new WebSocket('ws://' + window.location.host + '/ws/terminal');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
console.log('WebSocket connected');
term.write('Connected to terminal\r\n');
};
ws.onmessage = (event) => {
const data = new Uint8Array(event.data);
term.write(new TextDecoder().decode(data));
};
ws.onerror = (event) => {
term.write('\r\nWebSocket error');
console.error('WebSocket error:', event);
};
ws.onclose = () => {
term.write('\r\nWebSocket disconnected');
window.location.href = '/login';
};
term.onData(data => {
ws.send(data);
});
// 登出
document.getElementById('logout').addEventListener('click', async () => {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/login';
});
// 文件上传
document.getElementById('upload').addEventListener('click', async () => {
const fileInput = document.getElementById('file');
const targetDir = document.getElementById('target-dir').value;
const messageEl = document.getElementById('upload-message');
const errorEl = document.getElementById('upload-error');
messageEl.classList.add('hidden');
errorEl.classList.add('hidden');
if (!fileInput.files[0]) {
errorEl.textContent = '请选择一个文件';
errorEl.classList.remove('hidden');
return;
}
if (!targetDir) {
errorEl.textContent = '请输入目标目录';
errorEl.classList.remove('hidden');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('target_dir', targetDir);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
messageEl.textContent = data.message;
messageEl.classList.remove('hidden');
} else {
errorEl.textContent = data.error || '上传失败';
errorEl.classList.remove('hidden');
}
} catch (error) {
console.error('Upload error:', error);
errorEl.textContent = '网络错误';
errorEl.classList.remove('hidden');
}
});
// 规范化路径
function normalizePath(base, name) {
// 移除多余斜杠并拼接路径
let path = base.replace(/\/+$/, '') + '/' + name.replace(/^\/+/, '');
// 确保以单个斜杠开头
path = '/' + path.replace(/^\/+/, '');
return path;
}
// 文件浏览
async function listFiles(dirPath) {
const fileTable = document.getElementById('file-table');
const errorEl = document.getElementById('file-error');
const browseDirInput = document.getElementById('browse-dir');
fileTable.innerHTML = '';
errorEl.classList.add('hidden');
if (!dirPath) {
errorEl.textContent = '请输入目录路径';
errorEl.classList.remove('hidden');
return;
}
// 规范化路径
const normalizedPath = dirPath.replace(/\/+$/, '').replace(/^\/+/, '/');
console.log('Listing files for path:', normalizedPath);
try {
const response = await fetch(`/api/files?path=${encodeURIComponent(normalizedPath)}`);
if (response.status === 401) {
window.location.href = '/login';
return;
}
const data = await response.json();
if (response.ok) {
browseDirInput.value = normalizedPath; // 更新输入框路径
data.files.forEach(file => {
const row = document.createElement('tr');
const nextPath = normalizePath(normalizedPath, file.name);
const nameCell = file.is_dir
? `<td><a href="#" class="text-blue-500 hover:underline" onclick="listFiles('${nextPath.replace(/'/g, "\\'")}')">${file.name}</a></td>`
: `<td>${file.name}</td>`;
row.innerHTML = `
${nameCell}
<td>${file.is_dir ? '目录' : '文件'}</td>
<td>${file.is_dir ? '-' : (file.size / 1024).toFixed(2) + ' KB'}</td>
<td>${new Date(file.mod_time).toLocaleString()}</td>
<td><a href="/api/download?path=${encodeURIComponent(nextPath)}" class="text-blue-500 hover:underline">下载</a></td>
`;
fileTable.appendChild(row);
});
} else {
errorEl.textContent = data.error || '无法列出文件';
errorEl.classList.remove('hidden');
}
} catch (error) {
console.error('List files error:', error);
errorEl.textContent = '网络错误';
errorEl.classList.remove('hidden');
}
}
// 刷新文件列表
document.getElementById('list-files').addEventListener('click', () => {
const dirPath = document.getElementById('browse-dir').value;
listFiles(dirPath);
});
// 上一级目录
document.getElementById('parent-dir').addEventListener('click', () => {
const currentPath = document.getElementById('browse-dir').value;
if (currentPath === '/' || !currentPath) {
return;
}
const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
listFiles(parentPath);
});
// 设置默认地址
window.onload = function() {
const defaultPath = '/tmp/uploads';
const defaultListPath = '/app/avcnet';
document.getElementById('target-dir').value = defaultPath; // 上传地址默认值
document.getElementById('browse-dir').value = defaultListPath; // 浏览地址默认值
listFiles(defaultListPath); // 自动加载默认路径文件列表
};
// 初始检查登录状态并定期更新系统信息
checkLoginStatus();
updateSystemInfo();
setInterval(updateSystemInfo, 5000);
</script>
</body>
</html>