增加磁盘和网卡硬件信息显示
This commit is contained in:
152
backend/main.go
152
backend/main.go
@@ -1,10 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -34,10 +40,119 @@ func isDefaultDBConfig(cfg *config.Config) bool {
|
||||
cfg.DB.Database == "monitor"
|
||||
}
|
||||
|
||||
// daemonize 实现守护进程功能
|
||||
func daemonize() error {
|
||||
// 检查是否已经是守护进程模式
|
||||
if os.Getenv("DAEMONIZED") == "1" {
|
||||
// 设置工作目录
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
return fmt.Errorf("failed to chdir to /: %v", err)
|
||||
}
|
||||
|
||||
// 重设文件权限掩码
|
||||
syscall.Umask(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取可执行文件的绝对路径
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %v", err)
|
||||
}
|
||||
|
||||
// 创建环境变量,标记为守护进程模式
|
||||
env := append(os.Environ(), "DAEMONIZED=1")
|
||||
|
||||
// 启动新进程
|
||||
cmd := exec.Command(execPath, os.Args[1:]...)
|
||||
cmd.Env = env
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.Dir = "/"
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setsid: true,
|
||||
}
|
||||
|
||||
// 启动进程
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start daemon: %v", err)
|
||||
}
|
||||
|
||||
// 父进程退出
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// savePID 保存进程ID到文件
|
||||
func savePID() error {
|
||||
if pidFile == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取当前进程ID
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
|
||||
// 写入PID文件
|
||||
if err := os.WriteFile(pidFile, []byte(pid), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write PID file: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removePID 删除PID文件
|
||||
func removePID() {
|
||||
if pidFile != "" {
|
||||
os.Remove(pidFile)
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行参数
|
||||
var (
|
||||
// 是否以守护进程模式运行
|
||||
daemonMode bool
|
||||
// 日志文件路径
|
||||
logFilePath string
|
||||
// 进程ID文件路径
|
||||
pidFile string
|
||||
)
|
||||
|
||||
// main 函数启动服务器
|
||||
func main() {
|
||||
// 解析命令行参数
|
||||
flag.BoolVar(&daemonMode, "daemon", false, "Run as daemon (background process)")
|
||||
flag.BoolVar(&daemonMode, "d", false, "Run as daemon (background process) - shorthand")
|
||||
flag.StringVar(&logFilePath, "log-file", "", "Path to log file")
|
||||
flag.StringVar(&logFilePath, "l", "", "Path to log file - shorthand")
|
||||
flag.StringVar(&pidFile, "pid-file", "/tmp/monitor-backend.pid", "Path to PID file")
|
||||
flag.StringVar(&pidFile, "p", "/tmp/monitor-backend.pid", "Path to PID file - shorthand")
|
||||
flag.Parse()
|
||||
|
||||
// 配置日志:同时输出到文件和标准输出
|
||||
logFileName := fmt.Sprintf("monitor-backend-%s.log", time.Now().Format("2006-01-02"))
|
||||
|
||||
// 处理日志文件路径
|
||||
if logFilePath != "" {
|
||||
// 如果是相对路径,则使用可执行文件所在的目录作为基准目录
|
||||
if !filepath.IsAbs(logFilePath) {
|
||||
// 获取可执行文件的目录
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to get executable path, using current directory for log file")
|
||||
logFileName = logFilePath
|
||||
} else {
|
||||
execDir := filepath.Dir(execPath)
|
||||
logFileName = filepath.Join(execDir, logFilePath)
|
||||
}
|
||||
} else {
|
||||
logFileName = logFilePath
|
||||
}
|
||||
}
|
||||
|
||||
// 打开日志文件
|
||||
logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to open log file %s, logging only to stdout: %v", logFileName, err)
|
||||
@@ -73,6 +188,28 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// 如果指定了守护进程模式,则启动守护进程
|
||||
if daemonMode {
|
||||
if err := daemonize(); err != nil {
|
||||
log.Fatalf("Failed to daemonize: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存PID文件
|
||||
if err := savePID(); err != nil {
|
||||
log.Printf("Warning: Failed to save PID file: %v", err)
|
||||
}
|
||||
|
||||
// 注册信号处理,确保进程退出时删除PID文件
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
// 只处理可以捕获的信号
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
<-sigCh
|
||||
removePID()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// 创建存储实例
|
||||
store := storage.NewStorage(cfg)
|
||||
defer store.Close()
|
||||
@@ -156,6 +293,19 @@ func main() {
|
||||
handler.RegisterRoutes(r)
|
||||
|
||||
// 处理所有未匹配的路由,返回静态文件或index.html
|
||||
// 静态文件服务的路径处理:如果是相对路径,使用可执行文件所在目录作为基准目录
|
||||
staticDir := "./static"
|
||||
if !filepath.IsAbs(staticDir) {
|
||||
// 获取可执行文件的目录
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to get executable path, using current directory for static files")
|
||||
} else {
|
||||
execDir := filepath.Dir(execPath)
|
||||
staticDir = filepath.Join(execDir, staticDir)
|
||||
}
|
||||
}
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
// 尝试提供静态文件
|
||||
file := c.Request.URL.Path
|
||||
@@ -164,7 +314,7 @@ func main() {
|
||||
}
|
||||
|
||||
// 从static目录提供文件
|
||||
c.File("./static" + file)
|
||||
c.File(staticDir + file)
|
||||
})
|
||||
|
||||
// 启动服务器
|
||||
|
||||
BIN
backend/monitor-backend-2025-12-07.log
Normal file
BIN
backend/monitor-backend-2025-12-07.log
Normal file
Binary file not shown.
Binary file not shown.
5040
backend/nohup.out
Normal file
5040
backend/nohup.out
Normal file
File diff suppressed because it is too large
Load Diff
0
backend/server.log
Normal file
0
backend/server.log
Normal file
157
backend/start-monitor.sh
Executable file
157
backend/start-monitor.sh
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
# 启动/停止/重启脚本
|
||||
|
||||
# ===================== 配置区 =====================
|
||||
# 程序路径
|
||||
AGENT_PATH="/root/monitor/monitor-agent"
|
||||
# 日志文件路径
|
||||
LOG_FILE="/root/monitor/agent.log"
|
||||
# PID文件路径(记录进程ID)
|
||||
PID_FILE="/root/monitor/agent.pid"
|
||||
# 启动参数(根据实际需求调整)
|
||||
START_ARGS=""
|
||||
# ==================== 配置区结束 ====================
|
||||
|
||||
# 检查程序文件是否存在
|
||||
check_agent_exists() {
|
||||
if [ ! -f "${AGENT_PATH}" ]; then
|
||||
echo "错误:程序文件 ${AGENT_PATH} 不存在!"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "${AGENT_PATH}" ]; then
|
||||
echo "错误:程序文件 ${AGENT_PATH} 没有执行权限,正在尝试添加..."
|
||||
chmod +x "${AGENT_PATH}"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误:添加执行权限失败,请手动执行 chmod +x ${AGENT_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查进程是否运行
|
||||
check_running() {
|
||||
if [ -f "${PID_FILE}" ]; then
|
||||
PID=$(cat "${PID_FILE}")
|
||||
if ps -p "${PID}" > /dev/null 2>&1; then
|
||||
return 0 # 运行中
|
||||
else
|
||||
rm -f "${PID_FILE}" # PID文件存在但进程已死,清理PID文件
|
||||
fi
|
||||
fi
|
||||
return 1 # 未运行
|
||||
}
|
||||
|
||||
# 启动程序
|
||||
start_agent() {
|
||||
if check_running; then
|
||||
echo "✅ monitor-agent 已在运行(PID: $(cat ${PID_FILE}))"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "🚀 正在启动 monitor-agent..."
|
||||
# 创建日志目录(如果不存在)
|
||||
mkdir -p "$(dirname ${LOG_FILE})"
|
||||
# 后台启动程序,重定向日志,记录PID
|
||||
nohup "${AGENT_PATH}" ${START_ARGS} > "${LOG_FILE}" 2>&1 &
|
||||
AGENT_PID=$!
|
||||
echo "${AGENT_PID}" > "${PID_FILE}"
|
||||
|
||||
# 等待2秒检查是否启动成功
|
||||
sleep 2
|
||||
if check_running; then
|
||||
echo "✅ monitor-agent 启动成功(PID: ${AGENT_PID})"
|
||||
echo "日志文件:${LOG_FILE}"
|
||||
else
|
||||
echo "❌ monitor-agent 启动失败!请查看日志:${LOG_FILE}"
|
||||
rm -f "${PID_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止程序
|
||||
stop_agent() {
|
||||
if ! check_running; then
|
||||
echo "ℹ️ monitor-agent 未运行"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PID=$(cat "${PID_FILE}")
|
||||
echo "🛑 正在停止 monitor-agent(PID: ${PID})..."
|
||||
# 优雅停止(先尝试TERM信号,失败则强制KILL)
|
||||
kill "${PID}" > /dev/null 2>&1
|
||||
sleep 3
|
||||
|
||||
if ps -p "${PID}" > /dev/null 2>&1; then
|
||||
echo "⚠️ 优雅停止失败,强制杀死进程..."
|
||||
kill -9 "${PID}" > /dev/null 2>&1
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# 清理PID文件
|
||||
rm -f "${PID_FILE}"
|
||||
echo "✅ monitor-agent 已停止"
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
status_agent() {
|
||||
if check_running; then
|
||||
echo "✅ monitor-agent 运行中(PID: $(cat ${PID_FILE}))"
|
||||
else
|
||||
echo "ℹ️ monitor-agent 未运行"
|
||||
fi
|
||||
}
|
||||
|
||||
# 重启程序
|
||||
restart_agent() {
|
||||
echo "🔄 正在重启 monitor-agent..."
|
||||
stop_agent
|
||||
sleep 2
|
||||
start_agent
|
||||
}
|
||||
|
||||
# 帮助信息
|
||||
show_help() {
|
||||
echo "使用方法:$0 [start|stop|restart|status|help]"
|
||||
echo " start - 启动 monitor-agent"
|
||||
echo " stop - 停止 monitor-agent"
|
||||
echo " restart - 重启 monitor-agent"
|
||||
echo " status - 查看 monitor-agent 运行状态"
|
||||
echo " help - 显示帮助信息"
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
main() {
|
||||
# 检查是否为root用户(可选,根据需求调整)
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "警告:建议使用root用户运行此脚本(当前用户:$(whoami))"
|
||||
# exit 1 # 如果强制要求root,取消注释
|
||||
fi
|
||||
|
||||
check_agent_exists
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start_agent
|
||||
;;
|
||||
stop)
|
||||
stop_agent
|
||||
;;
|
||||
restart)
|
||||
restart_agent
|
||||
;;
|
||||
status)
|
||||
status_agent
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "错误:无效参数 '$1'"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 执行主逻辑
|
||||
main "$@"
|
||||
@@ -442,12 +442,10 @@
|
||||
<!-- 磁盘详细信息展示 -->
|
||||
<div id="diskDetailsContainer" class="mt-8 hidden">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">磁盘详细信息</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- 磁盘详细信息将通过JavaScript动态添加 -->
|
||||
<div id="diskDetailsContent"></div>
|
||||
<!-- 磁盘信息分页容器 -->
|
||||
<div id="diskPaginationContainer" class="mt-4"></div>
|
||||
</div>
|
||||
<!-- 磁盘详细信息将通过JavaScript动态添加 -->
|
||||
<div id="diskDetailsContent"></div>
|
||||
<!-- 磁盘信息分页容器 -->
|
||||
<div id="diskPaginationContainer" class="mt-4"></div>
|
||||
</div>
|
||||
<br>
|
||||
<!-- 网络 图表 -->
|
||||
@@ -490,6 +488,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<!-- 网卡详细信息展示 -->
|
||||
<div id="networkInterfaceContainer" class="mt-8 hidden">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">网卡详细信息</h3>
|
||||
<div id="networkInterfaceContent"></div>
|
||||
<div id="networkInterfacePaginationContainer" class="mt-4"></div>
|
||||
</div>
|
||||
<br>
|
||||
<!-- 系统日志 信息 -->
|
||||
<div id="logInfoContainer" class="mt-8 hidden">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">系统日志</h3>
|
||||
@@ -511,7 +516,7 @@
|
||||
<!-- 日志信息分页容器 -->
|
||||
<div id="logPaginationContainer" class="mt-4"></div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<!-- 缩放控件已移动到各个图表容器内 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
// API 基础 URL
|
||||
const API_BASE_URL = '/api';
|
||||
// 自动检测当前页面的协议和域名,构建完整的API URL
|
||||
const API_BASE_URL = `${window.location.protocol}//${window.location.host}/api`;
|
||||
|
||||
// 带超时的fetch请求封装
|
||||
function fetchWithTimeout(url, options = {}, timeout = 5000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('请求超时')), timeout)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// 通用的数据验证和转换函数
|
||||
function validateAndTransformData(data, expectedType, defaultValue = null) {
|
||||
// 检查数据类型是否匹配
|
||||
if (typeof data === expectedType) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 特殊处理数组类型
|
||||
if (expectedType === 'array' && Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 特殊处理对象类型
|
||||
if (expectedType === 'object' && typeof data === 'object' && data !== null) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 类型转换逻辑
|
||||
switch (expectedType) {
|
||||
case 'string':
|
||||
// 将非字符串类型转换为字符串,布尔值转换为默认值
|
||||
return typeof data === 'boolean' ? defaultValue : String(data);
|
||||
case 'number':
|
||||
// 将非数字类型转换为数字,布尔值转换为0或1
|
||||
const num = typeof data === 'boolean' ? (data ? 1 : 0) : Number(data);
|
||||
return isNaN(num) ? defaultValue : num;
|
||||
case 'boolean':
|
||||
// 将非布尔值转换为布尔值
|
||||
return Boolean(data);
|
||||
case 'array':
|
||||
// 将非数组转换为默认值
|
||||
return defaultValue;
|
||||
case 'object':
|
||||
// 将非对象转换为默认值
|
||||
return defaultValue;
|
||||
default:
|
||||
// 未知类型,返回默认值
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局状态
|
||||
let state = {
|
||||
@@ -207,23 +259,102 @@ async function loadHomeData() {
|
||||
|
||||
// 加载业务视图数据
|
||||
async function loadBusinessViewData() {
|
||||
// 显示加载状态
|
||||
const tableBody = document.getElementById('businessViewTableBody');
|
||||
if (tableBody) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-12 text-center text-gray-500">
|
||||
<div class="flex items-center justify-center">
|
||||
<i class="fa fa-spinner fa-spin text-xl mr-2"></i>
|
||||
加载中...
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/devices/`);
|
||||
const response = await fetchWithTimeout(`${API_BASE_URL}/devices/`, {}, 3000);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const devices = data.devices || [];
|
||||
const devices = Array.isArray(data.devices) ? data.devices : [];
|
||||
|
||||
renderBusinessView(devices);
|
||||
// 添加重试机制的fetch请求
|
||||
const fetchWithRetry = async (url, options = {}, timeout = 5000, maxRetries = 1) => {
|
||||
for (let i = 0; i <= maxRetries; i++) {
|
||||
try {
|
||||
return await fetchWithTimeout(url, options, timeout);
|
||||
} catch (error) {
|
||||
if (i === maxRetries) {
|
||||
// 最后一次重试失败,抛出错误
|
||||
throw error;
|
||||
}
|
||||
// 重试之间的延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 为每个设备获取完整信息,使用并行API调用优化加载速度
|
||||
const devicesWithDetails = await Promise.allSettled(devices.map(async (device) => {
|
||||
let ip = '未知';
|
||||
let os = '未知';
|
||||
|
||||
// 获取设备详情,包括IP地址(最多重试1次)
|
||||
try {
|
||||
const deviceDetailResponse = await fetchWithRetry(`${API_BASE_URL}/devices/${device.id}`, {}, 3000, 1);
|
||||
if (deviceDetailResponse.ok) {
|
||||
const deviceDetailData = await deviceDetailResponse.json();
|
||||
if (deviceDetailData && deviceDetailData.device) {
|
||||
ip = deviceDetailData.device.ip || '未知';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取设备 ${device.id} IP地址失败:`, error);
|
||||
}
|
||||
|
||||
// 获取硬件信息,包括操作系统(最多重试1次)
|
||||
try {
|
||||
const hardwareResponse = await fetchWithRetry(`${API_BASE_URL}/metrics/hardware?device_id=${device.id}`, {}, 8000, 1);
|
||||
if (hardwareResponse.ok) {
|
||||
const hardwareData = await hardwareResponse.json();
|
||||
if (hardwareData && hardwareData.hardware && hardwareData.hardware.os) {
|
||||
// 确保操作系统信息始终是字符串类型
|
||||
const fullname = hardwareData.hardware.os.fullname;
|
||||
os = typeof fullname === 'string' ? fullname : '未知';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`获取设备 ${device.id} 操作系统失败:`, error);
|
||||
}
|
||||
|
||||
return {
|
||||
...device,
|
||||
ip: ip,
|
||||
os: os
|
||||
};
|
||||
}));
|
||||
|
||||
// 处理Promise.allSettled的结果,只保留成功的结果
|
||||
const successfulDevices = devicesWithDetails
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => result.value);
|
||||
|
||||
renderBusinessView(successfulDevices);
|
||||
} catch (error) {
|
||||
console.error('加载业务视图数据失败:', error);
|
||||
// 只显示错误信息,不使用模拟数据
|
||||
const tableBody = document.getElementById('businessViewTableBody');
|
||||
// 显示错误信息
|
||||
if (tableBody) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-center text-red-500">加载业务视图数据失败</td>
|
||||
<td colspan="4" class="px-6 py-8 text-center text-red-500">
|
||||
<i class="fa fa-exclamation-circle text-xl mb-2"></i>
|
||||
<p>加载业务视图数据失败</p>
|
||||
<p class="text-sm text-gray-500 mt-1">${error.message}</p>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
@@ -237,12 +368,25 @@ function renderBusinessView(devices) {
|
||||
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
devices.forEach(device => {
|
||||
// 确保devices是数组类型
|
||||
const deviceList = Array.isArray(devices) ? devices : [];
|
||||
|
||||
// 处理空数组情况
|
||||
if (deviceList.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-4 text-center text-gray-500">暂无设备数据</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
deviceList.forEach(device => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50 transition-colors cursor-pointer';
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name || '未知设备'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip || '未知'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.os || '未知'}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
@@ -253,7 +397,9 @@ function renderBusinessView(devices) {
|
||||
|
||||
// 为表格行添加点击事件
|
||||
row.addEventListener('click', () => {
|
||||
goToServerMonitor(device.id);
|
||||
if (device.id) {
|
||||
goToServerMonitor(device.id);
|
||||
}
|
||||
});
|
||||
|
||||
tableBody.appendChild(row);
|
||||
@@ -3124,9 +3270,9 @@ function initChartTabs() {
|
||||
tab.classList.add('active', 'text-blue-600', 'border-blue-600');
|
||||
tab.classList.remove('text-gray-600', 'border-transparent');
|
||||
|
||||
// 隐藏所有图表容器
|
||||
// 隐藏所有服务器监控页面中的图表容器
|
||||
const tabId = tab.dataset.tab;
|
||||
const chartContainers = document.querySelectorAll('.chart-container');
|
||||
const chartContainers = document.querySelectorAll('#cpuChartContainer, #memoryChartContainer, #diskChartContainer, #networkChartContainer, #speedChartContainer');
|
||||
chartContainers.forEach(container => {
|
||||
container.classList.add('hidden');
|
||||
});
|
||||
@@ -3140,10 +3286,11 @@ function initChartTabs() {
|
||||
console.error(`Chart container not found: ${tabId}ChartContainer`);
|
||||
}
|
||||
|
||||
// 显示/隐藏进程信息、磁盘详细信息和系统日志
|
||||
// 显示/隐藏进程信息、磁盘详细信息、系统日志和网卡详细信息
|
||||
const processInfoContainer = document.getElementById('processInfoContainer');
|
||||
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
|
||||
const logInfoContainer = document.getElementById('logInfoContainer');
|
||||
const networkInterfaceContainer = document.getElementById('networkInterfaceContainer');
|
||||
|
||||
// 隐藏所有附加信息容器
|
||||
if (processInfoContainer) {
|
||||
@@ -3155,6 +3302,9 @@ function initChartTabs() {
|
||||
if (logInfoContainer) {
|
||||
logInfoContainer.classList.add('hidden');
|
||||
}
|
||||
if (networkInterfaceContainer) {
|
||||
networkInterfaceContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 根据选项卡显示相应的附加信息
|
||||
if (tabId === 'cpu') {
|
||||
@@ -3178,6 +3328,13 @@ function initChartTabs() {
|
||||
// 加载系统日志
|
||||
loadSystemLogs();
|
||||
}
|
||||
} else if (tabId === 'network' || tabId === 'speed') {
|
||||
// 显示网卡详细信息
|
||||
if (networkInterfaceContainer) {
|
||||
networkInterfaceContainer.classList.remove('hidden');
|
||||
// 加载网卡详细信息
|
||||
loadNetworkInterfaceDetails();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示/隐藏网卡选择下拉框
|
||||
@@ -3199,50 +3356,65 @@ function initChartTabs() {
|
||||
if (activeTab) {
|
||||
const tabId = activeTab.dataset.tab;
|
||||
|
||||
// 显示当前选中的图表容器
|
||||
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
|
||||
if (activeContainer) {
|
||||
activeContainer.classList.remove('hidden');
|
||||
} else {
|
||||
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
|
||||
}
|
||||
// 显示当前选中的图表容器,隐藏其他服务器监控图表容器
|
||||
const chartContainers = document.querySelectorAll('#cpuChartContainer, #memoryChartContainer, #diskChartContainer, #networkChartContainer, #speedChartContainer');
|
||||
chartContainers.forEach(container => {
|
||||
container.classList.add('hidden');
|
||||
});
|
||||
|
||||
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
|
||||
if (activeContainer) {
|
||||
activeContainer.classList.remove('hidden');
|
||||
} else {
|
||||
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
|
||||
}
|
||||
|
||||
// 显示/隐藏进程信息、磁盘详细信息和系统日志
|
||||
const processInfoContainer = document.getElementById('processInfoContainer');
|
||||
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
|
||||
const logInfoContainer = document.getElementById('logInfoContainer');
|
||||
|
||||
// 隐藏所有附加信息容器
|
||||
if (processInfoContainer) {
|
||||
processInfoContainer.classList.add('hidden');
|
||||
}
|
||||
if (diskDetailsContainer) {
|
||||
diskDetailsContainer.classList.add('hidden');
|
||||
}
|
||||
if (logInfoContainer) {
|
||||
logInfoContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 根据选项卡显示相应的附加信息
|
||||
if (tabId === 'cpu') {
|
||||
// 显示进程信息
|
||||
// 显示/隐藏进程信息、磁盘详细信息、系统日志和网卡详细信息
|
||||
const processInfoContainer = document.getElementById('processInfoContainer');
|
||||
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
|
||||
const logInfoContainer = document.getElementById('logInfoContainer');
|
||||
const networkInterfaceContainer = document.getElementById('networkInterfaceContainer');
|
||||
|
||||
// 隐藏所有附加信息容器
|
||||
if (processInfoContainer) {
|
||||
processInfoContainer.classList.remove('hidden');
|
||||
processInfoContainer.classList.add('hidden');
|
||||
}
|
||||
loadProcessInfo();
|
||||
} else if (tabId === 'disk') {
|
||||
// 显示磁盘详细信息
|
||||
if (diskDetailsContainer) {
|
||||
diskDetailsContainer.classList.remove('hidden');
|
||||
diskDetailsContainer.classList.add('hidden');
|
||||
}
|
||||
loadDiskDetails();
|
||||
} else if (tabId === 'logs') {
|
||||
// 显示系统日志
|
||||
if (logInfoContainer) {
|
||||
logInfoContainer.classList.remove('hidden');
|
||||
logInfoContainer.classList.add('hidden');
|
||||
}
|
||||
if (networkInterfaceContainer) {
|
||||
networkInterfaceContainer.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 根据选项卡显示相应的附加信息
|
||||
if (tabId === 'cpu') {
|
||||
// 显示进程信息
|
||||
if (processInfoContainer) {
|
||||
processInfoContainer.classList.remove('hidden');
|
||||
}
|
||||
loadProcessInfo();
|
||||
} else if (tabId === 'disk') {
|
||||
// 显示磁盘详细信息
|
||||
if (diskDetailsContainer) {
|
||||
diskDetailsContainer.classList.remove('hidden');
|
||||
}
|
||||
loadDiskDetails();
|
||||
} else if (tabId === 'logs') {
|
||||
// 显示系统日志
|
||||
if (logInfoContainer) {
|
||||
logInfoContainer.classList.remove('hidden');
|
||||
}
|
||||
loadSystemLogs();
|
||||
} else if (tabId === 'network' || tabId === 'speed') {
|
||||
// 显示网卡详细信息
|
||||
if (networkInterfaceContainer) {
|
||||
networkInterfaceContainer.classList.remove('hidden');
|
||||
}
|
||||
loadNetworkInterfaceDetails();
|
||||
}
|
||||
loadSystemLogs();
|
||||
}
|
||||
|
||||
// 显示/隐藏网卡选择下拉框
|
||||
if (interfaceContainer) {
|
||||
@@ -3446,6 +3618,16 @@ let diskPagination = {
|
||||
lastDeviceID: '' // 上次请求数据的设备ID
|
||||
};
|
||||
|
||||
// 网卡信息分页状态
|
||||
let networkInterfacePagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 5,
|
||||
totalItems: 0,
|
||||
totalPages: 0,
|
||||
allInterfaces: [],
|
||||
lastDeviceID: '' // 上次请求数据的设备ID
|
||||
};
|
||||
|
||||
// 创建分页控件
|
||||
function createPaginationControls(container, paginationState, loadFunction) {
|
||||
// 清空容器
|
||||
@@ -3585,35 +3767,79 @@ async function loadDiskDetails(page = 1) {
|
||||
|
||||
if (!diskDetailsContent) return;
|
||||
|
||||
// 显示加载状态
|
||||
diskDetailsContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-12 text-center text-gray-500">
|
||||
<div class="flex items-center justify-center">
|
||||
<i class="fa fa-spinner fa-spin text-xl mr-2"></i>
|
||||
加载磁盘详细信息中...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams();
|
||||
if (state.currentDeviceID) {
|
||||
params.append('device_id', state.currentDeviceID);
|
||||
if (!state.currentDeviceID) {
|
||||
diskDetailsContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
|
||||
<i class="fa fa-info-circle text-2xl mb-2"></i>
|
||||
<p>请选择设备查看磁盘详细信息</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (diskPaginationContainer) {
|
||||
diskPaginationContainer.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查设备ID是否变化
|
||||
if (diskPagination.lastDeviceID !== state.currentDeviceID) {
|
||||
// 设备ID变化,清空旧数据
|
||||
diskPagination.allDisks = [];
|
||||
diskPagination.totalItems = 0;
|
||||
diskPagination.totalPages = 0;
|
||||
diskPagination.currentPage = 1;
|
||||
}
|
||||
// 检查是否已经有缓存数据,且设备ID没有变化
|
||||
const shouldReload = !diskPagination.allDisks || diskPagination.lastDeviceID !== state.currentDeviceID;
|
||||
|
||||
// 如果是第一次加载或设备ID变化,获取全部数据
|
||||
if (diskPagination.allDisks.length === 0) {
|
||||
// 发送请求
|
||||
const response = await fetch(`${API_BASE_URL}/metrics/disk_details?${params.toString()}`);
|
||||
if (shouldReload) {
|
||||
// 使用hardware API获取磁盘详细信息
|
||||
const response = await fetchWithTimeout(`${API_BASE_URL}/metrics/hardware?device_id=${state.currentDeviceID}`, {}, 8000);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch disk details');
|
||||
throw new Error('Failed to fetch hardware details');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
diskPagination.allDisks = data.data || [];
|
||||
diskPagination.totalItems = diskPagination.allDisks.length;
|
||||
let diskDetails = [];
|
||||
|
||||
// 提取磁盘详细信息并进行严格的数据验证
|
||||
if (data.hardware && Array.isArray(data.hardware.disk_details)) {
|
||||
diskDetails = data.hardware.disk_details.map(disk => {
|
||||
// 确保每个磁盘信息是对象类型
|
||||
if (typeof disk !== 'object' || disk === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证和转换各个字段
|
||||
return {
|
||||
id: typeof disk.id === 'string' ? disk.id : Math.random().toString(36).substr(2, 9),
|
||||
// 确保状态是字符串类型,不是布尔值
|
||||
status: typeof disk.status === 'boolean' ? 'N/A' : (disk.status || 'N/A'),
|
||||
// 确保路径是字符串类型
|
||||
path: typeof disk.path === 'string' ? disk.path : 'N/A',
|
||||
// 确保类型是字符串类型
|
||||
type: typeof disk.type === 'boolean' ? 'N/A' : (disk.type || 'N/A'),
|
||||
// 确保大小是数字类型,不是布尔值
|
||||
size_gb: typeof disk.size_gb === 'boolean' ? 0 : (disk.size_gb || 0),
|
||||
// 确保文件系统是字符串类型
|
||||
file_system: typeof disk.file_system === 'string' ? disk.file_system : 'N/A',
|
||||
// 确保厂商是字符串类型
|
||||
vendor: typeof disk.vendor === 'string' ? disk.vendor : 'N/A',
|
||||
// 确保型号是字符串类型
|
||||
model: typeof disk.model === 'string' ? disk.model : 'N/A',
|
||||
// 确保接口类型是字符串类型
|
||||
interface_type: typeof disk.interface_type === 'boolean' ? 'N/A' : (disk.interface_type || 'N/A')
|
||||
};
|
||||
}).filter(Boolean); // 过滤掉null值
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
diskPagination.allDisks = diskDetails || [];
|
||||
diskPagination.totalItems = diskDetails.length;
|
||||
diskPagination.totalPages = Math.ceil(diskPagination.totalItems / diskPagination.itemsPerPage);
|
||||
// 更新上次请求数据的设备ID
|
||||
diskPagination.lastDeviceID = state.currentDeviceID;
|
||||
}
|
||||
|
||||
@@ -3645,52 +3871,121 @@ async function loadDiskDetails(page = 1) {
|
||||
const endIndex = Math.min(startIndex + diskPagination.itemsPerPage, diskPagination.totalItems);
|
||||
const paginatedDisks = diskPagination.allDisks.slice(startIndex, endIndex);
|
||||
|
||||
// 填充磁盘详细信息卡片
|
||||
// 创建磁盘详细信息容器
|
||||
const container = document.createElement('div');
|
||||
container.className = 'space-y-4';
|
||||
|
||||
// 创建响应式表格容器
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'overflow-x-auto';
|
||||
|
||||
// 创建表格
|
||||
const table = document.createElement('table');
|
||||
table.className = 'w-full bg-white border border-gray-200 rounded-lg';
|
||||
|
||||
// 创建表头
|
||||
const thead = document.createElement('thead');
|
||||
thead.className = 'bg-gray-50';
|
||||
thead.innerHTML = `
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">路径</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">大小 (GB)</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">文件系统</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">厂商</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">型号</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">接口类型</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
|
||||
// 创建表体
|
||||
const tbody = document.createElement('tbody');
|
||||
tbody.className = 'divide-y divide-gray-200';
|
||||
|
||||
paginatedDisks.forEach(disk => {
|
||||
const diskCard = document.createElement('div');
|
||||
diskCard.className = 'bg-white rounded-lg shadow-md p-6 border border-gray-100';
|
||||
diskCard.innerHTML = `
|
||||
<h4 class="text-md font-semibold text-gray-900 mb-3">${disk.device_id || 'Unknown Device'}</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">状态:</span>
|
||||
<span class="text-gray-900 font-medium">${disk.status || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">类型:</span>
|
||||
<span class="text-gray-900 font-medium">${disk.type || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">大小:</span>
|
||||
<span class="text-gray-900 font-medium">${disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00'} GB</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">型号:</span>
|
||||
<span class="text-gray-900 font-medium">${disk.model || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">接口类型:</span>
|
||||
<span class="text-gray-900 font-medium">${disk.interface_type || 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">描述:</span>
|
||||
<span class="text-gray-900 font-medium truncate max-w-xs">${disk.description || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
// 跳过无效的磁盘数据
|
||||
if (!disk || !disk.id) return;
|
||||
|
||||
// 格式化数据,空值显示为N/A
|
||||
const status = disk.status || 'N/A';
|
||||
const path = disk.path || 'N/A';
|
||||
const type = disk.type || 'N/A';
|
||||
const size = disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00';
|
||||
const fileSystem = disk.file_system || 'N/A';
|
||||
const vendor = disk.vendor || 'N/A';
|
||||
const model = disk.model || 'N/A';
|
||||
const interfaceType = disk.interface_type || 'N/A';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50';
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${status}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${path}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${type}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${size}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${fileSystem}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${vendor}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${model}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${interfaceType}</td>
|
||||
`;
|
||||
diskDetailsContent.appendChild(diskCard);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
// 创建分页控件
|
||||
table.appendChild(tbody);
|
||||
tableContainer.appendChild(table);
|
||||
container.appendChild(tableContainer);
|
||||
|
||||
// 创建分页信息和控件容器
|
||||
const paginationContainer = document.createElement('div');
|
||||
paginationContainer.className = 'flex justify-between items-center';
|
||||
|
||||
// 创建每页显示数量选择
|
||||
const pageSizeDiv = document.createElement('div');
|
||||
pageSizeDiv.className = 'flex items-center gap-2';
|
||||
pageSizeDiv.innerHTML = `
|
||||
<label for="diskPageSize" class="text-sm text-gray-600">每页显示:</label>
|
||||
<select id="diskPageSize" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
<option value="5" ${diskPagination.itemsPerPage === 5 ? 'selected' : ''}>5条</option>
|
||||
<option value="10" ${diskPagination.itemsPerPage === 10 ? 'selected' : ''}>10条</option>
|
||||
<option value="20" ${diskPagination.itemsPerPage === 20 ? 'selected' : ''}>20条</option>
|
||||
<option value="50" ${diskPagination.itemsPerPage === 50 ? 'selected' : ''}>50条</option>
|
||||
</select>
|
||||
`;
|
||||
|
||||
// 添加每页显示数量变化事件
|
||||
const pageSizeSelect = pageSizeDiv.querySelector('#diskPageSize');
|
||||
pageSizeSelect.addEventListener('change', (e) => {
|
||||
const newPageSize = parseInt(e.target.value, 10);
|
||||
if (newPageSize !== diskPagination.itemsPerPage) {
|
||||
diskPagination.itemsPerPage = newPageSize;
|
||||
loadDiskDetails(1); // 重置为第一页
|
||||
}
|
||||
});
|
||||
|
||||
paginationContainer.appendChild(pageSizeDiv);
|
||||
|
||||
// 创建显示当前页和总条数的文本
|
||||
const infoText = document.createElement('div');
|
||||
infoText.className = 'text-sm text-gray-500';
|
||||
infoText.textContent = `显示 ${startIndex + 1} 至 ${endIndex},共 ${diskPagination.totalItems} 条`;
|
||||
paginationContainer.appendChild(infoText);
|
||||
|
||||
// 添加分页控件
|
||||
if (diskPaginationContainer) {
|
||||
createPaginationControls(diskPaginationContainer, diskPagination, loadDiskDetails);
|
||||
}
|
||||
|
||||
container.appendChild(paginationContainer);
|
||||
diskDetailsContent.appendChild(container);
|
||||
} catch (error) {
|
||||
console.error('Error loading disk details:', error);
|
||||
diskDetailsContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-red-500">
|
||||
<i class="fa fa-exclamation-circle text-2xl mb-2"></i>
|
||||
<p>加载磁盘详细信息失败</p>
|
||||
<p class="text-sm text-gray-500 mt-1">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3700,6 +3995,222 @@ async function loadDiskDetails(page = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载网卡详细信息
|
||||
async function loadNetworkInterfaceDetails(page = 1) {
|
||||
const networkInterfaceContent = document.getElementById('networkInterfaceContent');
|
||||
const networkInterfacePaginationContainer = document.getElementById('networkInterfacePaginationContainer');
|
||||
|
||||
if (!networkInterfaceContent) return;
|
||||
|
||||
// 显示加载状态
|
||||
networkInterfaceContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-12 text-center text-gray-500">
|
||||
<div class="flex items-center justify-center">
|
||||
<i class="fa fa-spinner fa-spin text-xl mr-2"></i>
|
||||
加载网卡详细信息中...
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
if (!state.currentDeviceID) {
|
||||
networkInterfaceContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
|
||||
<i class="fa fa-info-circle text-2xl mb-2"></i>
|
||||
<p>请选择设备查看网卡详细信息</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (networkInterfacePaginationContainer) {
|
||||
networkInterfacePaginationContainer.innerHTML = '';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经有缓存数据,且设备ID没有变化
|
||||
const shouldReload = !networkInterfacePagination.allInterfaces || networkInterfacePagination.lastDeviceID !== state.currentDeviceID;
|
||||
|
||||
if (shouldReload) {
|
||||
// 使用hardware API获取网卡详细信息
|
||||
const response = await fetchWithTimeout(`${API_BASE_URL}/metrics/hardware?device_id=${state.currentDeviceID}`, {}, 8000);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch hardware details');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
let networkInterfaces = [];
|
||||
|
||||
// 提取网卡详细信息并进行严格的数据验证
|
||||
if (data.hardware && Array.isArray(data.hardware.network_cards)) {
|
||||
networkInterfaces = data.hardware.network_cards.map(iface => {
|
||||
// 确保每个网卡信息是对象类型
|
||||
if (typeof iface !== 'object' || iface === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证和转换各个字段
|
||||
return {
|
||||
name: typeof iface.name === 'string' ? iface.name : 'N/A',
|
||||
mac: typeof iface.mac === 'string' ? iface.mac : 'N/A',
|
||||
// 确保ip_addresses始终是数组类型
|
||||
ip_addresses: Array.isArray(iface.ip_addresses) ? iface.ip_addresses : [],
|
||||
// 确保mtu是数字或字符串,不是布尔值
|
||||
mtu: typeof iface.mtu === 'boolean' ? 'N/A' : (iface.mtu || 'N/A'),
|
||||
// 确保speed是数字或字符串,不是布尔值
|
||||
speed: typeof iface.speed === 'boolean' ? 'N/A' : (iface.speed || 'N/A')
|
||||
};
|
||||
}).filter(Boolean); // 过滤掉null值
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
networkInterfacePagination.allInterfaces = networkInterfaces || [];
|
||||
networkInterfacePagination.totalItems = networkInterfaces.length;
|
||||
networkInterfacePagination.totalPages = Math.ceil(networkInterfacePagination.totalItems / networkInterfacePagination.itemsPerPage);
|
||||
networkInterfacePagination.lastDeviceID = state.currentDeviceID;
|
||||
}
|
||||
|
||||
// 更新当前页码
|
||||
networkInterfacePagination.currentPage = page;
|
||||
|
||||
// 清空内容
|
||||
networkInterfaceContent.innerHTML = '';
|
||||
|
||||
if (networkInterfacePagination.totalItems === 0) {
|
||||
// 没有网卡数据,显示提示
|
||||
networkInterfaceContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
|
||||
<i class="fa fa-wifi text-2xl mb-2"></i>
|
||||
<p>暂无网卡详细信息</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (networkInterfacePaginationContainer) {
|
||||
networkInterfacePaginationContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算分页数据
|
||||
const startIndex = (networkInterfacePagination.currentPage - 1) * networkInterfacePagination.itemsPerPage;
|
||||
const endIndex = Math.min(startIndex + networkInterfacePagination.itemsPerPage, networkInterfacePagination.totalItems);
|
||||
const paginatedInterfaces = networkInterfacePagination.allInterfaces.slice(startIndex, endIndex);
|
||||
|
||||
// 创建网卡详细信息容器
|
||||
const container = document.createElement('div');
|
||||
container.className = 'space-y-4';
|
||||
|
||||
// 创建响应式表格容器
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'overflow-x-auto';
|
||||
|
||||
// 创建表格
|
||||
const table = document.createElement('table');
|
||||
table.className = 'w-full bg-white border border-gray-200 rounded-lg';
|
||||
|
||||
// 创建表头
|
||||
const thead = document.createElement('thead');
|
||||
thead.className = 'bg-gray-50';
|
||||
thead.innerHTML = `
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名称</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MTU</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">速度</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
|
||||
// 创建表体
|
||||
const tbody = document.createElement('tbody');
|
||||
tbody.className = 'divide-y divide-gray-200';
|
||||
|
||||
paginatedInterfaces.forEach(iface => {
|
||||
// 跳过无效的网卡数据
|
||||
if (!iface) return;
|
||||
|
||||
// 格式化数据,空值显示为N/A
|
||||
const name = iface.name || 'N/A';
|
||||
const mac = iface.mac || 'N/A';
|
||||
const ipAddresses = iface.ip_addresses ? iface.ip_addresses.join(', ') : 'N/A';
|
||||
const mtu = iface.mtu || 'N/A';
|
||||
const speed = iface.speed || 'N/A';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-gray-50';
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${name}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${mac}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-900">${ipAddresses}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${mtu}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${speed}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
tableContainer.appendChild(table);
|
||||
container.appendChild(tableContainer);
|
||||
|
||||
// 创建分页信息和控件容器
|
||||
const paginationContainer = document.createElement('div');
|
||||
paginationContainer.className = 'flex justify-between items-center';
|
||||
|
||||
// 创建每页显示数量选择
|
||||
const pageSizeDiv = document.createElement('div');
|
||||
pageSizeDiv.className = 'flex items-center gap-2';
|
||||
pageSizeDiv.innerHTML = `
|
||||
<label for="networkInterfacePageSize" class="text-sm text-gray-600">每页显示:</label>
|
||||
<select id="networkInterfacePageSize" class="px-2 py-1 border border-gray-300 rounded text-sm">
|
||||
<option value="5" ${networkInterfacePagination.itemsPerPage === 5 ? 'selected' : ''}>5条</option>
|
||||
<option value="10" ${networkInterfacePagination.itemsPerPage === 10 ? 'selected' : ''}>10条</option>
|
||||
<option value="20" ${networkInterfacePagination.itemsPerPage === 20 ? 'selected' : ''}>20条</option>
|
||||
<option value="50" ${networkInterfacePagination.itemsPerPage === 50 ? 'selected' : ''}>50条</option>
|
||||
</select>
|
||||
`;
|
||||
|
||||
// 添加每页显示数量变化事件
|
||||
const pageSizeSelect = pageSizeDiv.querySelector('#networkInterfacePageSize');
|
||||
pageSizeSelect.addEventListener('change', (e) => {
|
||||
const newPageSize = parseInt(e.target.value, 10);
|
||||
if (newPageSize !== networkInterfacePagination.itemsPerPage) {
|
||||
networkInterfacePagination.itemsPerPage = newPageSize;
|
||||
loadNetworkInterfaceDetails(1); // 重置为第一页
|
||||
}
|
||||
});
|
||||
|
||||
paginationContainer.appendChild(pageSizeDiv);
|
||||
|
||||
// 创建显示当前页和总条数的文本
|
||||
const infoText = document.createElement('div');
|
||||
infoText.className = 'text-sm text-gray-500';
|
||||
infoText.textContent = `显示 ${startIndex + 1} 至 ${endIndex},共 ${networkInterfacePagination.totalItems} 条`;
|
||||
paginationContainer.appendChild(infoText);
|
||||
|
||||
// 添加分页控件
|
||||
if (networkInterfacePaginationContainer) {
|
||||
createPaginationControls(networkInterfacePaginationContainer, networkInterfacePagination, loadNetworkInterfaceDetails);
|
||||
}
|
||||
|
||||
container.appendChild(paginationContainer);
|
||||
networkInterfaceContent.appendChild(container);
|
||||
} catch (error) {
|
||||
console.error('Error loading network interface details:', error);
|
||||
networkInterfaceContent.innerHTML = `
|
||||
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-red-500">
|
||||
<i class="fa fa-exclamation-circle text-2xl mb-2"></i>
|
||||
<p>加载网卡详细信息失败</p>
|
||||
<p class="text-sm text-gray-500 mt-1">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (networkInterfacePaginationContainer) {
|
||||
networkInterfacePaginationContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间为易识别的日期样式
|
||||
function formatLogTime(timeString) {
|
||||
if (!timeString) return new Date().toLocaleString('zh-CN');
|
||||
|
||||
Reference in New Issue
Block a user