feat: 初版实现状态轮询

This commit is contained in:
chaoszhu
2025-07-25 22:12:44 +08:00
parent 2574cd5387
commit 83244c8247
9 changed files with 1876 additions and 18 deletions
+2
View File
@@ -10,6 +10,7 @@ const wsSftpV2 = require('./socket/sftp-v2')
const wsDocker = require('./socket/docker')
const wsClientInfo = require('./socket/clients')
const wsOnekey = require('./socket/onekey')
const wsServerStatus = require('./socket/server-status')
const { throwError } = require('./utils/tools')
const httpServer = () => {
@@ -32,6 +33,7 @@ function serverHandler(app, server) {
wsDocker(server) // docker
wsOnekey(server) // 一键指令
wsClientInfo(server) // 客户端信息
wsServerStatus(server) // 服务器状态监控
app.context.throwError = throwError // 常用方法挂载全局ctx上
app.use(compose(middlewares))
// 捕获error.js模块抛出的服务错误
+934
View File
@@ -0,0 +1,934 @@
const { Server } = require('socket.io')
const { Client: SSHClient } = require('ssh2')
const { verifyAuthSync } = require('../utils/verify-auth')
const { isAllowedIp } = require('../utils/tools')
const { createTerminal } = require('./terminal')
module.exports = (httpServer) => {
const serverIo = new Server(httpServer, {
path: '/server-status',
cors: {
origin: '*'
}
})
let connectionCount = 0
serverIo.on('connection', (socket) => {
connectionCount++
consola.success(`server-status websocket 已连接 - 当前连接数: ${ connectionCount }`)
let requestIP = socket.handshake.headers['x-forwarded-for'] || socket.handshake.address
if (!isAllowedIp(requestIP)) {
socket.emit('ip_forbidden', 'IP地址不在白名单中')
socket.disconnect()
return
}
let targetSSHClient = null
let jumpSshClients = []
let monitorTimer = null
let sendDataTimer = null
let lastNetStats = null // 用于计算网络速率
let serverError = false // 服务器错误标志
let previousCpuStats = null // 用于计算CPU使用率
let defaultNetInterface = null // 默认网络接口
// 系统信息缓存(只获取一次的信息)
let staticSystemInfo = {
cpuCount: null,
cpuModel: null,
osInfo: null
}
// 服务器状态数据(外层维护,提升及时性)
let statusData = {
connect: false,
cpuInfo: {},
memInfo: {},
swapInfo: {},
driveInfo: {},
netstatInfo: {},
osInfo: {}
}
socket.on('ws_server_status', async ({ hostId, token }) => {
try {
const { code } = await verifyAuthSync(token, requestIP)
if (code !== 1) {
socket.emit('token_verify_fail')
socket.disconnect()
return
}
targetSSHClient = new SSHClient()
let { jumpSshClients: statusJumpSshClients } = await createTerminal(hostId, socket, targetSSHClient, false)
jumpSshClients.push(...statusJumpSshClients || [])
// 开始监控
startMonitoring()
} catch (error) {
consola.error('ws_server_status 事件处理失败:', error.message)
socket.emit('server_status_error', `连接失败: ${ error.message }`)
}
})
// 检查是否是关键的服务器错误
const isServerCriticalError = (errorMessage) => {
const criticalErrors = [
'free: command not found', // free命令不存在
'/proc/uptime: No such file or directory', // /proc/uptime文件不存在
'/proc/net/dev: No such file or directory', // /proc/net/dev文件不存在
'/proc/stat: No such file or directory', // /proc/stat文件不存在
'/proc/cpuinfo: No such file or directory' // /proc/cpuinfo文件不存在
]
return criticalErrors.some(error => errorMessage.includes(error))
}
// 设置服务器错误状态并停止监控
const setServerError = (reason) => {
if (serverError) return // 已经设置过错误状态
serverError = true
consola.error(`检测到服务器关键错误,停止监控: ${ reason }`)
// 停止监控定时器
if (monitorTimer) {
clearInterval(monitorTimer)
monitorTimer = null
}
// 发送错误状态给前端
socket.emit('server_status_data', {
connect: false,
error: true,
errorReason: reason,
cpuInfo: {},
memInfo: {},
swapInfo: {},
driveInfo: {},
netstatInfo: {},
osInfo: {}
})
}
// 执行SSH命令的通用函数(带重试机制)
const executeCommand = (command, maxRetries = 2) => {
return new Promise((resolve, reject) => {
const attemptExecution = (retryCount) => {
if (!targetSSHClient || !targetSSHClient._sock || !targetSSHClient._sock.writable) {
reject(new Error('SSH 连接已断开'))
return
}
targetSSHClient.exec(command, (err, stream) => {
if (err) {
if (err.message.includes('Channel open failure') && retryCount < maxRetries) {
// 通道打开失败,等待一段时间后重试
setTimeout(() => {
attemptExecution(retryCount + 1)
}, 100 * (retryCount + 1)) // 递增延迟
return
}
reject(err)
return
}
let stdout = ''
let stderr = ''
stream
.on('data', (data) => {
stdout += data.toString()
})
.stderr.on('data', (data) => {
stderr += data.toString()
})
.on('close', (code) => {
if (code !== 0 && stderr) {
reject(new Error(stderr))
} else {
resolve(stdout.trim())
}
})
.on('error', (error) => {
reject(error)
})
})
}
attemptExecution(0)
})
}
// 获取静态CPU信息(CPU核心数和型号,只获取一次)
const getStaticCpuInfo = async () => {
if (staticSystemInfo.cpuCount !== null && staticSystemInfo.cpuModel !== null) {
return {
cpuCount: staticSystemInfo.cpuCount,
cpuModel: staticSystemInfo.cpuModel
}
}
let cpuCount = 0
let cpuModel = 'Unknown'
// 获取CPU核心数
try {
const cpuCountResult = await executeCommand('nproc')
cpuCount = parseInt(cpuCountResult) || 0
// 如果nproc失败,尝试其他方法
if (cpuCount === 0) {
const lscpuResult = await executeCommand('lscpu | grep "^CPU(s):" | head -1')
const cpuCountMatch = lscpuResult.match(/CPU\(s\):\s*(\d+)/)
cpuCount = cpuCountMatch ? parseInt(cpuCountMatch[1]) : 0
}
} catch (error) {
consola.warn('获取CPU核心数失败:', error.message)
}
// 获取CPU型号
try {
const cpuModelResult = await executeCommand('cat /proc/cpuinfo | grep "model name" | head -1')
const cpuModelMatch = cpuModelResult.match(/model name\s*:\s*(.+)/)
if (cpuModelMatch) {
cpuModel = cpuModelMatch[1].trim()
} else {
// 如果/proc/cpuinfo失败,尝试lscpu
const lscpuResult = await executeCommand('lscpu | grep "Model name:" | head -1')
const lscpuModelMatch = lscpuResult.match(/Model name:\s*(.+)/)
cpuModel = lscpuModelMatch ? lscpuModelMatch[1].trim() : 'Unknown'
}
} catch (error) {
consola.warn('获取CPU型号失败:', error.message)
// 检查是否是关键错误
if (isServerCriticalError(error.message)) {
setServerError(`获取CPU型号失败: ${ error.message }`)
return { cpuCount: 0, cpuModel: 'Unknown' }
}
}
// 缓存结果
staticSystemInfo.cpuCount = cpuCount
staticSystemInfo.cpuModel = cpuModel
return { cpuCount, cpuModel }
}
// 解析/proc/stat输出
const parseProcStat = (procStatOutput) => {
try {
const firstLine = procStatOutput.split('\n')[0]
const values = firstLine.split(' ').slice(2).map(Number)
const idle = values[3] || 0
const total = values.reduce((a, b) => a + b, 0)
return { idle, total }
} catch (error) {
return null
}
}
// 获取CPU信息(优化的CPU使用率计算)
const getCpuInfo = async () => {
let cpuUsage = 0
try {
const procStatOutput = await executeCommand('cat /proc/stat')
const currentCpuStats = parseProcStat(procStatOutput)
const now = Date.now()
if (currentCpuStats) {
if (previousCpuStats && previousCpuStats.timestamp < now) {
const totalDiff = currentCpuStats.total - previousCpuStats.total
const idleDiff = currentCpuStats.idle - previousCpuStats.idle
const timeDiffMs = now - previousCpuStats.timestamp
// 确保时间差>500ms以获得更准确的计算,避免除零错误
if (totalDiff > 0 && timeDiffMs > 500) {
const usageRatio = 1.0 - (idleDiff / totalDiff)
// 限制在0-100范围内
cpuUsage = Math.max(0, Math.min(100, usageRatio * 100))
} else {
// 时间间隔太短或没有变化,保持上次的值或设为0
cpuUsage = 0
}
} else {
// 第一次运行,无法计算使用率
cpuUsage = 0
}
// 存储当前统计信息供下次使用
previousCpuStats = { ...currentCpuStats, timestamp: now }
}
} catch (error) {
consola.warn('获取CPU使用率失败:', error.message)
// 检查是否是关键错误
if (isServerCriticalError(error.message)) {
setServerError(`获取CPU使用率失败: ${ error.message }`)
return { cpuUsage: 0, cpuCount: 0, cpuModel: 'Unknown' }
}
}
// 获取静态CPU信息
const { cpuCount, cpuModel } = await getStaticCpuInfo()
// 获取负载平均值
let loadAvg = [0, 0, 0]
try {
const uptimeOutput = await executeCommand('uptime')
const match = uptimeOutput.match(/load average(?:s)?:\s*([\d.]+)[, ]?\s*([\d.]+)[, ]?\s*([\d.]+)/)
if (match) {
loadAvg = [parseFloat(match[1]), parseFloat(match[2]), parseFloat(match[3])]
}
} catch (error) {
// 静默处理负载平均值获取失败
}
return {
cpuUsage: parseFloat(cpuUsage.toFixed(2)),
cpuCount,
cpuModel,
loadAvg
}
}
// 获取内存和交换空间信息(统一处理,支持BusyBox)
const getMemoryInfo = async () => {
const defaultReturn = {
memInfo: { totalMemMb: 0, usedMemMb: 0, freeMemMb: 0, usedMemPercentage: 0, freeMemPercentage: 0 },
swapInfo: { swapTotal: 0, swapUsed: 0, swapFree: 0, swapPercentage: '0' }
}
try {
// 检查是否为BusyBox环境
let freeCommand = 'free -m'
let isBusyBox = false
try {
const busyboxCheck = await executeCommand('busybox --help')
if (busyboxCheck.includes('BusyBox')) {
freeCommand = 'free'
isBusyBox = true
}
} catch (err) {
// 如果检查失败,默认使用 free -m
}
const freeOutput = await executeCommand(freeCommand)
const lines = freeOutput.split('\n')
// 使用更鲁棒的方式查找内存和交换空间行
const memLine = lines.find(line => line.trim().startsWith('Mem:'))
const swapLine = lines.find(line => line.trim().startsWith('Swap:'))
let memInfo = defaultReturn.memInfo
let swapInfo = defaultReturn.swapInfo
// 处理内存信息
if (memLine) {
const parts = memLine.trim().split(/\s+/)
if (parts.length >= 3) {
let totalVal = parseInt(parts[1], 10)
let usedVal = parseInt(parts[2], 10)
let freeVal = parts[3] ? parseInt(parts[3], 10) : (totalVal - usedVal)
// BusyBox环境下需要从KB转换为MB
if (isBusyBox) {
if (!isNaN(totalVal)) totalVal = Math.round(totalVal / 1024)
if (!isNaN(usedVal)) usedVal = Math.round(usedVal / 1024)
if (!isNaN(freeVal)) freeVal = Math.round(freeVal / 1024)
}
if (!isNaN(totalVal) && !isNaN(usedVal)) {
const usedMemPercentage = totalVal > 0 ? parseFloat(((usedVal / totalVal) * 100).toFixed(2)) : 0
const freeMemPercentage = totalVal > 0 ? parseFloat(((freeVal / totalVal) * 100).toFixed(2)) : 0
memInfo = {
totalMemMb: totalVal,
usedMemMb: usedVal,
freeMemMb: freeVal,
usedMemPercentage,
freeMemPercentage
}
}
}
}
// 处理交换空间信息
if (swapLine) {
const parts = swapLine.trim().split(/\s+/)
if (parts.length >= 3) {
let totalVal = parseInt(parts[1], 10)
let usedVal = parseInt(parts[2], 10)
let freeVal = parts[3] ? parseInt(parts[3], 10) : (totalVal - usedVal)
// BusyBox环境下需要从KB转换为MB
if (isBusyBox) {
if (!isNaN(totalVal)) totalVal = Math.round(totalVal / 1024)
if (!isNaN(usedVal)) usedVal = Math.round(usedVal / 1024)
if (!isNaN(freeVal)) freeVal = Math.round(freeVal / 1024)
}
if (!isNaN(totalVal) && !isNaN(usedVal)) {
const swapPercentage = totalVal > 0 ? ((usedVal / totalVal) * 100).toFixed(1) : '0'
swapInfo = {
swapTotal: totalVal,
swapUsed: usedVal,
swapFree: freeVal,
swapPercentage
}
}
}
} else {
// 没有交换空间
swapInfo = { swapTotal: 0, swapUsed: 0, swapFree: 0, swapPercentage: '0' }
}
return { memInfo, swapInfo }
} catch (error) {
consola.error('获取内存信息失败:', error.message)
// 检查是否是关键错误
if (isServerCriticalError(error.message)) {
setServerError(`获取内存信息失败: ${ error.message }`)
}
return defaultReturn
}
}
// 获取内存信息(兼容性包装函数)
const getMemInfo = async () => {
const { memInfo } = await getMemoryInfo()
return memInfo
}
// 获取Swap信息(兼容性包装函数)
const getSwapInfo = async () => {
const { swapInfo } = await getMemoryInfo()
return swapInfo
}
// 获取磁盘信息
const getDriveInfo = async () => {
try {
const driveInfo = await executeCommand('df -kP / | tail -1')
const driveLine = driveInfo.split(/\s+/)
const totalKb = parseInt(driveLine[1])
const usedKb = parseInt(driveLine[2])
const freeKb = parseInt(driveLine[3])
const usedPercentage = parseFloat(driveLine[4].replace('%', ''))
return {
totalGb: (totalKb / 1024 / 1024).toFixed(1),
usedGb: (usedKb / 1024 / 1024).toFixed(1),
freeGb: (freeKb / 1024 / 1024).toFixed(1),
usedPercentage: usedPercentage.toFixed(1),
freePercentage: (100 - usedPercentage).toFixed(1)
}
} catch (error) {
consola.error('获取磁盘信息失败:', error.message)
return { totalGb: '0', usedGb: '0', freeGb: '0', usedPercentage: '0', freePercentage: '0' }
}
}
// 获取默认网络接口
const getDefaultInterface = async () => {
if (defaultNetInterface) {
return defaultNetInterface
}
try {
// 尝试通过路由表获取默认接口
const routeOutput = await executeCommand('ip route get 1')
const match = routeOutput.match(/dev\s+(\w+)/)
if (match) {
defaultNetInterface = match[1]
return defaultNetInterface
}
} catch (error) {
// 静默处理,继续尝试其他方法
}
try {
// 备用方法:获取第一个非lo接口
const netDev = await executeCommand('cat /proc/net/dev')
const lines = netDev.split('\n').slice(2)
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 9 && parts[0]) {
const interfaceName = parts[0].replace(':', '')
if (interfaceName !== 'lo') {
defaultNetInterface = interfaceName
return defaultNetInterface
}
}
}
} catch (error) {
// 静默处理
}
return null
}
// 解析网络设备统计信息
const parseNetworkStats = async () => {
try {
const netDev = await executeCommand('cat /proc/net/dev')
const lines = netDev.split('\n').slice(2)
const stats = {}
lines.forEach(line => {
const parts = line.trim().split(/\s+/)
if (parts.length >= 9 && parts[0]) {
const interfaceName = parts[0].replace(':', '')
const rxBytes = parseInt(parts[1]) || 0
const txBytes = parseInt(parts[9]) || 0
stats[interfaceName] = { rxBytes, txBytes }
}
})
return stats
} catch (error) {
return null
}
}
// 获取网络信息(优化版)
const getNetstatInfo = async () => {
try {
const currentStats = await parseNetworkStats()
if (!currentStats) {
return { total: { inputMb: 0, outputMb: 0 } }
}
const defaultInterface = await getDefaultInterface()
const timestamp = Date.now()
let netstatInfo = {}
// 计算总流量(所有接口)
let totalInput = 0
let totalOutput = 0
Object.values(currentStats).forEach(stat => {
totalInput += stat.rxBytes
totalOutput += stat.txBytes
})
if (lastNetStats && lastNetStats.timestamp < timestamp) {
const timeDiffSeconds = (timestamp - lastNetStats.timestamp) / 1000
if (timeDiffSeconds > 0.1) {
// 总流量速率
netstatInfo.total = {
inputMb: ((totalInput - lastNetStats.totalInput) / timeDiffSeconds / 1024 / 1024).toFixed(3),
outputMb: ((totalOutput - lastNetStats.totalOutput) / timeDiffSeconds / 1024 / 1024).toFixed(3)
}
// 默认接口的速率
if (defaultInterface && currentStats[defaultInterface] && lastNetStats.interfaces[defaultInterface]) {
const currentRx = currentStats[defaultInterface].rxBytes
const currentTx = currentStats[defaultInterface].txBytes
const lastRx = lastNetStats.interfaces[defaultInterface].rxBytes
const lastTx = lastNetStats.interfaces[defaultInterface].txBytes
netstatInfo.default = {
interface: defaultInterface,
inputMb: ((currentRx - lastRx) / timeDiffSeconds / 1024 / 1024).toFixed(3),
outputMb: ((currentTx - lastTx) / timeDiffSeconds / 1024 / 1024).toFixed(3)
}
}
// 各个接口的速率
Object.keys(currentStats).forEach(interfaceName => {
if (lastNetStats.interfaces[interfaceName]) {
const currentInterface = currentStats[interfaceName]
const lastInterface = lastNetStats.interfaces[interfaceName]
netstatInfo[interfaceName] = {
inputMb: ((currentInterface.rxBytes - lastInterface.rxBytes) / timeDiffSeconds / 1024 / 1024).toFixed(3),
outputMb: ((currentInterface.txBytes - lastInterface.txBytes) / timeDiffSeconds / 1024 / 1024).toFixed(3)
}
}
})
} else {
// 时间间隔太短,设为0
netstatInfo.total = { inputMb: '0.000', outputMb: '0.000' }
if (defaultInterface) {
netstatInfo.default = {
interface: defaultInterface,
inputMb: '0.000',
outputMb: '0.000'
}
}
}
} else {
// 第一次运行,初始化为0
netstatInfo.total = { inputMb: '0.000', outputMb: '0.000' }
if (defaultInterface) {
netstatInfo.default = {
interface: defaultInterface,
inputMb: '0.000',
outputMb: '0.000'
}
}
}
// 保存当前状态
lastNetStats = {
totalInput,
totalOutput,
interfaces: currentStats,
timestamp
}
return netstatInfo
} catch (error) {
consola.error('获取网络信息失败:', error.message)
// 检查是否是关键错误
if (isServerCriticalError(error.message)) {
setServerError(`获取网络信息失败: ${ error.message }`)
return { total: { inputMb: '0.000', outputMb: '0.000' } }
}
return { total: { inputMb: '0.000', outputMb: '0.000' } }
}
}
// 获取静态系统信息(只获取一次的信息)
const getStaticOsInfo = async () => {
if (staticSystemInfo.osInfo !== null) {
return staticSystemInfo.osInfo
}
const defaultStaticInfo = {
hostname: 'Unknown',
type: 'Linux',
platform: 'linux',
release: 'Unknown',
arch: 'Unknown'
}
try {
// 串行执行命令避免并发问题
let hostname = 'Unknown'
try {
hostname = await executeCommand('hostname') || 'Unknown'
} catch (e) {
consola.warn('获取hostname失败:', e.message)
}
// 通过读取 /etc/os-release 文件获取操作系统信息
let type = 'Linux'
let release = 'Unknown'
let arch = 'Unknown'
try {
// 读取 /etc/os-release 文件获取系统信息
const osReleaseContent = await executeCommand('cat /etc/os-release')
const osReleaseLines = osReleaseContent.split('\n')
// 解析系统名称和版本
osReleaseLines.forEach(line => {
if (line.startsWith('NAME=')) {
type = line.replace('NAME=', '').replace(/"/g, '') || 'Linux'
} else if (line.startsWith('VERSION=')) {
release = line.replace('VERSION=', '').replace(/"/g, '') || 'Unknown'
} else if (line.startsWith('PRETTY_NAME=')) {
// 如果有PRETTY_NAME,可以作为更友好的显示名称
const prettyName = line.replace('PRETTY_NAME=', '').replace(/"/g, '')
if (prettyName) {
type = prettyName.split(' ')[0] // 取第一个词作为系统类型
// 提取版本信息
const versionMatch = prettyName.match(/(\d+\.?\d*\.?\d*)/)
if (versionMatch) {
release = versionMatch[1]
}
}
}
})
} catch (e) {
consola.warn('读取 /etc/os-release 失败,尝试使用 uname 命令:', e.message)
// 如果读取 /etc/os-release 失败,回退到 uname 命令
try {
type = await executeCommand('uname -s') || 'Linux'
} catch (e2) {
consola.warn('获取系统类型失败:', e2.message)
}
try {
release = await executeCommand('uname -r') || 'Unknown'
} catch (e2) {
consola.warn('获取系统版本失败:', e2.message)
}
}
// 获取系统架构
try {
arch = await executeCommand('uname -m') || 'Unknown'
} catch (e) {
consola.warn('获取系统架构失败:', e.message)
}
const staticInfo = {
hostname: hostname.trim(),
type: type.trim(),
platform: 'linux',
release: release.trim(),
arch: arch.trim()
}
// 缓存结果
staticSystemInfo.osInfo = staticInfo
return staticInfo
} catch (error) {
consola.error('获取静态系统信息失败:', error.message)
staticSystemInfo.osInfo = defaultStaticInfo
return defaultStaticInfo
}
}
// 获取系统信息(结合静态信息和动态信息)
const getOsInfo = async () => {
try {
// 获取静态信息
const staticInfo = await getStaticOsInfo()
// 获取动态信息:系统运行时间
let uptime = 0
try {
const uptimeStr = await executeCommand('cat /proc/uptime | cut -d" " -f1') || '0'
uptime = parseFloat(uptimeStr) || 0
} catch (e) {
consola.warn('获取系统运行时间失败:', e.message)
// 检查是否是关键错误
if (isServerCriticalError(e.message)) {
setServerError(`获取系统运行时间失败: ${ e.message }`)
return { ...staticInfo, uptime: 0 }
}
}
return {
...staticInfo,
uptime
}
} catch (error) {
consola.error('获取系统信息失败:', error.message)
return {
hostname: 'Unknown',
type: 'Linux',
platform: 'linux',
release: 'Unknown',
arch: 'Unknown',
uptime: 0
}
}
}
// 更新CPU信息到全局statusData
const updateCpuInfo = async () => {
try {
const cpuInfo = await getCpuInfo()
statusData.cpuInfo = cpuInfo
statusData.connect = true // 标记连接状态
} catch (error) {
consola.error('更新CPU信息失败:', error.message)
statusData.cpuInfo = {}
}
}
// 更新内存信息到全局statusData
const updateMemoryInfo = async () => {
try {
const { memInfo, swapInfo } = await getMemoryInfo()
statusData.memInfo = memInfo
statusData.swapInfo = swapInfo
statusData.connect = true
} catch (error) {
consola.error('更新内存信息失败:', error.message)
statusData.memInfo = {}
statusData.swapInfo = {}
}
}
// 更新磁盘信息到全局statusData
const updateDriveInfo = async () => {
try {
const driveInfo = await getDriveInfo()
statusData.driveInfo = driveInfo
statusData.connect = true
} catch (error) {
consola.error('更新磁盘信息失败:', error.message)
statusData.driveInfo = {}
}
}
// 更新网络信息到全局statusData
const updateNetworkInfo = async () => {
try {
const netstatInfo = await getNetstatInfo()
statusData.netstatInfo = netstatInfo
statusData.connect = true
} catch (error) {
consola.error('更新网络信息失败:', error.message)
statusData.netstatInfo = {}
}
}
// 更新系统信息到全局statusData
const updateOsInfo = async () => {
try {
const osInfo = await getOsInfo()
statusData.osInfo = osInfo
statusData.connect = true
} catch (error) {
consola.error('更新系统信息失败:', error.message)
statusData.osInfo = {}
}
}
// 更新所有服务器状态信息(改为增量更新模式)
const updateServerStatus = async () => {
try {
// 并行执行一些不冲突的数据获取,每完成一个就立即更新statusData
const updateTasks = [
updateCpuInfo(),
updateMemoryInfo(),
updateDriveInfo(),
updateNetworkInfo(),
updateOsInfo()
]
// 使用Promise.allSettled确保即使某个更新失败,其他更新仍能继续
const results = await Promise.allSettled(updateTasks)
// 检查是否有任何成功的更新
const hasSuccessfulUpdate = results.some(result => result.status === 'fulfilled')
if (!hasSuccessfulUpdate) {
// 如果所有更新都失败,标记为断开连接
statusData.connect = false
}
} catch (error) {
consola.error('更新服务器状态过程中出错:', error.message)
statusData.connect = false
}
}
// 开始监控(解耦数据收集和数据发送,提升及时性)
let isCollecting = false
const startMonitoring = () => {
// 数据收集函数 - 每2秒执行一次
const collectData = async () => {
try {
if (isCollecting) return
isCollecting = true
// 如果已经检测到服务器错误,停止监控
if (serverError) {
if (monitorTimer) {
clearInterval(monitorTimer)
monitorTimer = null
}
if (sendDataTimer) {
clearInterval(sendDataTimer)
sendDataTimer = null
}
consola.info('服务器存在关键错误,已停止监控')
return
}
// 更新全局statusData
await updateServerStatus()
} catch (error) {
consola.error('数据收集过程中出错:', error.message)
} finally {
isCollecting = false
}
}
// 数据发送函数 - 每1秒执行一次
const sendData = () => {
try {
// 如果检测到服务器错误,停止发送
if (serverError) {
if (sendDataTimer) {
clearInterval(sendDataTimer)
sendDataTimer = null
}
return
}
// 发送当前statusData给前端
socket.emit('server_status_data', statusData)
} catch (error) {
consola.error('数据发送过程中出错:', error.message)
}
}
// 立即执行一次数据收集
collectData()
// 启动定时器(如果没有错误)
if (!serverError) {
// 每2秒收集一次数据
monitorTimer = setInterval(collectData, 3000)
// 每1秒发送一次数据(1秒后开始,避免与首次收集冲突)
if (!serverError) {
sendDataTimer = setInterval(sendData, 1500)
}
}
}
socket.on('disconnect', (reason) => {
connectionCount--
// 清理所有定时器
if (monitorTimer) {
clearInterval(monitorTimer)
monitorTimer = null
}
if (sendDataTimer) {
clearInterval(sendDataTimer)
sendDataTimer = null
}
targetSSHClient && targetSSHClient.end()
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
// 清理所有状态变量
targetSSHClient = null
jumpSshClients = null
lastNetStats = null
previousCpuStats = null
defaultNetInterface = null
serverError = false
// 清理系统信息缓存
staticSystemInfo = {
cpuCount: null,
cpuModel: null,
osInfo: null
}
// 重置状态数据
statusData = {
connect: false,
cpuInfo: {},
memInfo: {},
swapInfo: {},
driveInfo: {},
netstatInfo: {},
osInfo: {}
}
consola.info(`server-status websocket 连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
})
})
}
+1
View File
@@ -32,6 +32,7 @@
"@xterm/xterm": "^5.5.0",
"ant-design-x-vue": "^1.0.7",
"axios": "^1.8.4",
"chart.js": "^4.5.0",
"codemirror": "^6.0.1",
"crypto-js": "^4.2.0",
"csv-parse": "^5.6.0",
+4 -6
View File
@@ -59,12 +59,10 @@ html.dark {
}
}
}
.info_box {
border-right: 1px solid #454242;
.el-progress-bar__innerText {
span {
color: #fff !important;
}
.el-progress-bar__innerText {
span {
color: #ffffff !important; // 强制白色文字,确保在各种背景下都能看清
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); // 添加文字阴影增强可读性
}
}
+9 -2
View File
@@ -5,11 +5,18 @@ export default {
value = Number(value)
return isNaN(value) ? '--' : value.toFixed(count)
},
formatTime(second = 0) {
formatTime(second = 0, target = 'day') {
let day = Math.floor(second / 60 / 60 / 24)
let hour = Math.floor(second / 60 / 60 % 24)
let minute = Math.floor(second / 60 % 60)
return `${ day }天${ hour }时${ minute }分`
if (target === 'day') {
return `${ day }天`
} else if (target === 'hour') {
return `${ day }天${ hour }时`
} else if (target === 'minute') {
return `${ day }天${ hour }时${ minute }分`
}
return `${ day }天${ hour }时${ minute }分${ second }秒`
},
formatNetSpeed(netSpeedMB) {
netSpeedMB = Number(netSpeedMB) || 0
+1 -1
View File
@@ -83,7 +83,7 @@ const useStore = defineStore('global', {
await this.getPlusInfo()
this.getAIConfig()
this.getChatHistory()
this.wsClientsStatus()
// this.wsClientsStatus()
},
async getHostList() {
let { data: newHostList } = await $api.getHostList()
@@ -0,0 +1,905 @@
<template>
<div class="info_container" :style="{ width: visible ? `250px` : 0 }">
<!-- <el-divider class="first-divider" content-position="center">地理位置</el-divider> -->
<el-descriptions
class="margin-top"
:column="1"
size="small"
border
>
<el-descriptions-item>
<template #label>
<div class="item-title">
IP
</div>
</template>
<span class="host-info-ip" :title="host">{{ host }}</span>
<template v-if="pingMs">
<el-tooltip effect="dark" content="该值为EasyNode服务端主机到目标主机的ping值" placement="bottom">
<span class="host-ping" :style="{backgroundColor: handlePingColor(pingMs)}">{{ pingMs }}ms</span>
</el-tooltip>
</template>
<el-tag size="small" style="cursor: pointer;margin-left: 10px;" @click="handleCopy">复制</el-tag>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
在线
</div>
</template>
<div size="small">
{{ $tools.formatTime(osInfo.uptime, 'day') }}
</div>
</el-descriptions-item>
</el-descriptions>
<!-- <el-divider content-position="center">实时监控</el-divider> -->
<br>
<el-descriptions
class="margin-top"
:column="1"
size="small"
border
>
<el-descriptions-item>
<template #label>
<div class="item-title">
CPU
</div>
</template>
<el-progress
:text-inside="true"
:stroke-width="18"
:percentage="cpuUsage"
:color="handleUsedColor(cpuUsage)"
/>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
负载
</div>
</template>
<el-tooltip effect="dark" content="过去1分钟、5分钟、15分钟CPU负载" placement="bottom">
<div class="load-avg-display">
<span
v-for="(load, index) in loadAvgFormatted"
:key="index"
:class="['load-value', { 'high-load': load.isHigh }]"
>
{{ load.value }}{{ index < loadAvgFormatted.length - 1 ? ',' : '' }}
</span>
</div>
</el-tooltip>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
内存
</div>
</template>
<el-progress
:text-inside="true"
:stroke-width="18"
:percentage="usedMemPercentage"
:color="handleUsedColor(usedMemPercentage)"
/>
<div class="position-right">
{{ $tools.toFixed(memInfo.usedMemMb / 1024) }}/{{ $tools.toFixed(memInfo.totalMemMb / 1024) }}G
</div>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
交换
</div>
</template>
<el-progress
:text-inside="true"
:stroke-width="18"
:percentage="swapPercentage"
:color="handleUsedColor(swapPercentage)"
/>
<div class="position-right">
{{ $tools.toFixed(swapInfo.swapUsed / 1024) }}/{{ $tools.toFixed(swapInfo.swapTotal / 1024) }}G
</div>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
硬盘
</div>
</template>
<el-progress
:text-inside="true"
:stroke-width="18"
:percentage="usedPercentage"
:color="handleUsedColor(usedPercentage)"
/>
<div class="position-right">
{{ driveInfo.usedGb || '--' }}/{{ driveInfo.totalGb || '--' }}G
</div>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
网络
</div>
</template>
<div class="netstat-info">
<div class="count_wrap">
<div class="wrap">
<img src="@/assets/upload.png" alt="">
<span class="upload">{{ output || 0 }}</span>
</div>
<div class="wrap">
<img src="@/assets/download.png" alt="">
<span class="download">{{ input || 0 }}</span>
</div>
</div>
<div class="chart-container">
<canvas ref="networkChart" width="200" height="80" />
</div>
</div>
</el-descriptions-item>
</el-descriptions>
<!-- <el-divider content-position="center">系统信息</el-divider> -->
<br>
<el-descriptions
class="margin-top"
:column="1"
size="small"
border
>
<el-descriptions-item>
<template #label>
<div class="item-title">
名称
</div>
</template>
<div size="small">
{{ osInfo.hostname }}
</div>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
核心
</div>
</template>
<div size="small">
{{ cpuInfo.cpuCount }}
</div>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
型号
</div>
</template>
<div size="small">
{{ cpuInfo.cpuModel }}
</div>
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="item-title">
类型
</div>
</template>
<div size="small">
{{ osInfo.type }} {{ osInfo.release }} {{ osInfo.arch }}
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, watch, onBeforeUnmount, nextTick, onMounted, toRaw, shallowRef } from 'vue'
import { io } from 'socket.io-client'
import { Chart, registerables } from 'chart.js'
// 注册Chart.js所有组件
Chart.register(...registerables)
const { proxy: { $message, $tools, $store } } = getCurrentInstance()
const props = defineProps({
hostId: {
required: true,
type: String
},
visible: {
required: true,
type: Boolean
}
})
// 状态数据
const serverData = ref({
connect: false,
cpuInfo: {},
memInfo: {},
swapInfo: {},
driveInfo: {},
netstatInfo: {},
osInfo: {}
})
const pingData = ref({})
const socket = ref(null)
const reconnectTimer = ref(null)
const reconnectAttempts = ref(0)
const maxReconnectAttempts = 10
const reconnectInterval = 3000 // 3秒重连间隔
const isConnecting = ref(false)
// 网速图表相关
const networkChart = ref(null) // canvas元素引用
const chartInstance = shallowRef(null) // Chart.js实例,使用shallowRef避免深度响应式
const networkHistory = shallowRef({
upload: [], // 上传速度历史数据
download: [], // 下载速度历史数据
timestamps: [] // 时间戳
})
const maxHistoryLength = 120 // 保存120次数据(约2分钟)
// 计算属性
const host = computed(() => {
const hostInfo = $store.hostList.find(h => h.id === props.hostId)
return hostInfo?.host || ''
})
const cpuInfo = computed(() => serverData.value.cpuInfo || {})
const memInfo = computed(() => serverData.value.memInfo || {})
const swapInfo = computed(() => serverData.value.swapInfo || {})
const osInfo = computed(() => serverData.value.osInfo || {})
const driveInfo = computed(() => serverData.value.driveInfo || {})
const netstatInfo = computed(() => {
let { total: netTotal, ...netCards } = serverData.value.netstatInfo || {}
return { netTotal, netCards: netCards || {} }
})
const cpuUsage = computed(() => Number(cpuInfo.value?.cpuUsage) || 0)
const cpuCount = computed(() => Number(cpuInfo.value?.cpuCount) || 1)
const loadAvg = computed(() => cpuInfo.value?.loadAvg || [0, 0, 0,])
// 格式化负载数据,包含颜色判断
const loadAvgFormatted = computed(() => {
return loadAvg.value.map(load => {
const loadValue = Number(load) || 0
return {
value: loadValue.toFixed(2),
isHigh: loadValue >= cpuCount.value // 负载超过CPU核心数时认为是高负载
}
})
})
const usedMemPercentage = computed(() => Number(memInfo.value?.usedMemPercentage) || 0)
const swapPercentage = computed(() => {
let swapPercentage = swapInfo.value?.swapPercentage
let isNaN = swapPercentage === 'NaN' || Number.isNaN(swapInfo.value?.swapPercentage)
return isNaN ? 0 : Number(swapPercentage || 0)
})
const usedPercentage = computed(() => Number(driveInfo.value?.usedPercentage) || 0)
const output = computed(() => {
let outputMb = Number(netstatInfo.value.netTotal?.outputMb) || 0
if (outputMb >= 1) return `${ outputMb.toFixed(2) } MB/s`
return `${ (outputMb * 1024).toFixed(1) } KB/s`
})
const input = computed(() => {
let inputMb = Number(netstatInfo.value.netTotal?.inputMb) || 0
if (inputMb >= 1) return `${ inputMb.toFixed(2) } MB/s`
return `${ (inputMb * 1024).toFixed(1) } KB/s`
})
const pingMs = computed(() => {
let curPingData = pingData.value[host.value] || {}
if (!curPingData?.success) return false
return Number(curPingData?.time).toFixed(0)
})
// 清理重连定时器
const clearReconnectTimer = () => {
if (reconnectTimer.value) {
clearTimeout(reconnectTimer.value)
reconnectTimer.value = null
}
}
// 启动重连
const startReconnect = () => {
if (reconnectAttempts.value >= maxReconnectAttempts) {
console.warn('server-status websocket 已达到最大重连次数,停止重连')
return
}
clearReconnectTimer()
reconnectTimer.value = setTimeout(() => {
if (props.visible && props.hostId && !isConnecting.value) {
reconnectAttempts.value++
console.log(`server-status websocket 尝试重连 (${ reconnectAttempts.value }/${ maxReconnectAttempts })`)
connectWebSocket()
}
}, reconnectInterval)
}
// WebSocket连接函数
const connectWebSocket = () => {
if (isConnecting.value) {
return
}
isConnecting.value = true
// 如果已有连接,先断开
if (socket.value) {
socket.value.disconnect()
socket.value = null
}
try {
socket.value = io($store.serviceURI, {
path: '/server-status',
forceNew: true,
timeout: 10000, // 10秒连接超时
reconnection: false // 禁用socket.io自带重连,使用自定义重连
})
socket.value.on('connect', () => {
console.log('server-status websocket 已连接:', socket.value.id)
isConnecting.value = false
reconnectAttempts.value = 0 // 重置重连计数器
clearReconnectTimer()
const token = $store.token
socket.value.emit('ws_server_status', { hostId: props.hostId, token })
})
socket.value.on('server_status_data', (data) => {
serverData.value = data
// 更新网速图表数据
if (data.connect && data.netstatInfo && data.netstatInfo.total) {
const uploadMb = data.netstatInfo.total.outputMb || 0
const downloadMb = data.netstatInfo.total.inputMb || 0
updateNetworkHistory(uploadMb, downloadMb)
}
// 如果服务器返回错误状态,停止重连并断开连接
if (data.error) {
console.warn('服务器状态监控出现关键错误:', data.errorReason)
// 停止重连机制
clearReconnectTimer()
reconnectAttempts.value = maxReconnectAttempts // 设置为最大值,防止重连
console.warn(`服务器状态监控已停止: ${ data.errorReason }`)
}
})
socket.value.on('token_verify_fail', (message) => {
console.log('token 验证失败:', message)
isConnecting.value = false
// token失败不重连,需要重新登录
})
socket.value.on('disconnect', (reason) => {
console.log('server-status websocket 连接断开:', reason)
isConnecting.value = false
// 只有在组件仍然可见且需要连接时才重连
if (props.visible && props.hostId && reason !== 'io client disconnect') {
startReconnect()
}
})
socket.value.on('connect_error', (error) => {
console.error('server-status websocket 连接出错:', error.message)
isConnecting.value = false
// 连接错误时启动重连
if (props.visible && props.hostId) {
startReconnect()
}
})
socket.value.on('error', (error) => {
console.error('server-status websocket 发生错误:', error)
isConnecting.value = false
})
} catch (error) {
console.error('创建 server-status websocket 连接失败:', error)
isConnecting.value = false
if (props.visible && props.hostId) {
startReconnect()
}
}
}
// 初始化WebSocket连接
const initWebSocket = () => {
// 重置重连状态
reconnectAttempts.value = 0
clearReconnectTimer()
connectWebSocket()
}
// 断开WebSocket连接
const disconnectWebSocket = () => {
clearReconnectTimer()
isConnecting.value = false
if (socket.value) {
socket.value.disconnect()
socket.value = null
}
}
// 监听hostId变化重新连接
watch(
() => props.hostId,
(newHostId) => {
if (newHostId && props.visible) {
initWebSocket()
}
},
{ immediate: true }
)
// 监听visible变化
watch(
() => props.visible,
(newVisible) => {
if (newVisible && props.hostId) {
initWebSocket()
} else if (!newVisible) {
disconnectWebSocket()
}
}
)
// 工具函数
const handleCopy = async () => {
await navigator.clipboard.writeText(host.value)
$message.success({ message: '复制成功', center: true })
}
const handleUsedColor = (num) => {
if (num < 60) return '#13ce66'
if (num < 80) return '#e6a23c'
if (num <= 100) return '#ff4949'
}
const handlePingColor = (num) => {
if (num < 100) return 'rgba(19, 206, 102, 0.5)' // #13ce66
if (num < 250) return 'rgba(230, 162, 60, 0.5)' // #e6a23c
return 'rgba(255, 73, 73, 0.5)' // #ff4949
}
// 初始化网速图表
const initNetworkChart = () => {
if (!networkChart.value) return
const ctx = networkChart.value.getContext('2d')
chartInstance.value = new Chart(ctx, {
type: 'line',
data: {
labels: [], // 时间标签
datasets: [
{
label: '上传',
data: [],
borderColor: '#CF8A20',
backgroundColor: 'rgba(207, 138, 32, 0.1)',
borderWidth: 1.5,
fill: false,
tension: 0.2,
pointRadius: 0,
pointHoverRadius: 3
},
{
label: '下载',
data: [],
borderColor: '#67c23a',
backgroundColor: 'rgba(103, 194, 58, 0.1)',
borderWidth: 1.5,
fill: false,
tension: 0.2,
pointRadius: 0,
pointHoverRadius: 3
},
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
scales: {
x: {
display: false, // 隐藏x轴标签
grid: {
display: false
}
},
y: {
display: true,
position: 'left',
ticks: {
font: {
size: 10
},
maxTicksLimit: 3,
callback: function(value) {
if (value >= 1) {
return value.toFixed(1) + 'M'
}
return (value * 1024).toFixed(0) + 'K'
}
},
grid: {
color: 'rgba(255, 255, 255, 0.1)'
}
}
},
plugins: {
legend: {
display: false // 隐藏图例
},
tooltip: {
enabled: true,
mode: 'index',
intersect: false,
callbacks: {
title: function() {
return '' // 隐藏tooltip标题(时间)
},
label: function(context) {
const value = context.parsed.y
const label = context.dataset.label
if (value >= 1) {
return `${ label }: ${ value.toFixed(2) } MB/s`
}
return `${ label }: ${ (value * 1024).toFixed(1) } KB/s`
}
}
}
},
interaction: {
mode: 'index',
intersect: false
}
}
})
}
// 更新网速历史数据
const updateNetworkHistory = (uploadMb, downloadMb) => {
const now = new Date()
const timeLabel = now.toLocaleTimeString('zh-CN', {
hour12: false,
minute: '2-digit',
second: '2-digit'
})
// 获取原始数据并直接操作,避免响应式追踪
const history = toRaw(networkHistory.value)
// 添加新数据
history.upload.push(parseFloat(uploadMb) || 0)
history.download.push(parseFloat(downloadMb) || 0)
history.timestamps.push(timeLabel)
// 保持数据长度不超过maxHistoryLength
if (history.upload.length > maxHistoryLength) {
history.upload.shift()
history.download.shift()
history.timestamps.shift()
}
// 手动触发响应式更新
networkHistory.value = { ...history }
// 更新图表 - 直接使用原始数据
if (chartInstance.value) {
chartInstance.value.data.labels = [...history.timestamps,]
chartInstance.value.data.datasets[0].data = [...history.upload,]
chartInstance.value.data.datasets[1].data = [...history.download,]
chartInstance.value.update('none') // 不使用动画以提高性能
}
}
onMounted(() => {
// 等待DOM渲染完成后初始化图表
nextTick(() => {
initNetworkChart()
})
})
onBeforeUnmount(() => {
// 清理图表实例
if (chartInstance.value) {
chartInstance.value.destroy()
chartInstance.value = null
}
// 清理历史数据
networkHistory.value = {
upload: [],
download: [],
timestamps: []
}
// 清理WebSocket连接和重连定时器
disconnectWebSocket()
})
</script>
<style lang="scss" scoped>
.info_container {
transition: all 0.15s;
height: 100%;
// 连接状态指示器
.connection-status {
padding: 8px 12px;
margin-bottom: 10px;
background: var(--el-fill-color-extra-light);
border-radius: 4px;
font-size: 12px;
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
transition: background-color 0.3s;
}
.status-text {
font-weight: 500;
transition: color 0.3s;
}
.reconnect-info {
color: var(--el-color-warning);
font-size: 11px;
}
&.connected {
.status-dot {
background-color: var(--el-color-success);
box-shadow: 0 0 4px rgba(103, 194, 58, 0.6);
}
.status-text {
color: var(--el-color-success);
}
}
&.connecting {
.status-dot {
background-color: var(--el-color-warning);
animation: pulse 1.5s infinite;
}
.status-text {
color: var(--el-color-warning);
}
}
&.disconnected {
.status-dot {
background-color: var(--el-color-danger);
}
.status-text {
color: var(--el-color-danger);
}
}
&.error {
.status-dot {
background-color: var(--el-color-danger);
animation: none; // 停止动画
}
.status-text {
color: var(--el-color-danger);
font-weight: 600;
}
}
}
.error-info {
display: block;
color: var(--el-color-danger);
font-size: 10px;
margin-top: 2px;
font-weight: normal;
opacity: 0.8;
word-break: break-all;
}
}
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.item-title {
user-select: none;
white-space: nowrap;
text-align: center;
min-width: 30px;
max-width: 30px;
}
.host-info-ip {
word-break: break-all;
max-width: 92px;
display: inline-block;
vertical-align: top;
margin-right: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.host-ping {
display: inline-block;
font-size: 10px;
padding: 0 5px;
border-radius: 2px;
}
.load-avg-display {
font-size: 13px;
font-family: 'Courier New', monospace;
.load-value {
color: #13ce66;
transition: color 0.3s ease;
&.high-load {
color: #ff4949;
font-weight: 600;
}
}
}
// 分割线title
:deep(.el-divider__text) {
color: #a0cfff;
padding: 0 8px;
user-select: none;
}
// 分割线间距
:deep(.el-divider--horizontal) {
margin: 28px 0 10px;
}
.first-divider {
margin: 15px 0 10px;
}
// 表格
:deep(.el-descriptions__table) {
tr {
display: flex;
.el-descriptions__label {
min-width: 35px;
flex-shrink: 0;
}
.el-descriptions__content {
position: relative;
flex: 1;
display: flex;
align-items: center;
.el-progress {
width: 100%;
}
// 进度条右边参数定位
.position-right {
position: absolute;
right: 15px;
}
}
}
}
// 进度条
:deep(.el-progress-bar__inner) {
display: flex;
align-items: center;
.el-progress-bar__innerText {
display: flex;
span {
color: #242323;
}
}
}
// 网络
.netstat-info {
width: 100%;
height: 100%;
.count_wrap {
display: flex;
// flex-direction: column;
justify-content: space-between;
margin-bottom: 8px;
}
.wrap {
flex: 1;
display: flex;
align-items: center;
// justify-content: center;
padding: 0 5px;
img {
width: 15px;
margin-right: 5px;
}
.upload {
color: #CF8A20;
}
.download {
color: #67c23a;
}
}
.chart-container {
width: 100%;
height: 80px;
margin-top: 5px;
position: relative;
canvas {
width: 100% !important;
height: 100% !important;
border-radius: 4px;
background: rgba(0, 0, 0, 0.1);
}
}
}
}
</style>
<style scoped>
.el-descriptions__label {
vertical-align: middle;
max-width: 35px;
}
</style>
@@ -282,20 +282,18 @@
direction="ltr"
class="mobile_menu_drawer"
>
<InfoSide
<ServerStatus
ref="infoSideRef"
:host-info="curHost"
:visible="visible"
:ping-data="pingData"
:host-id="item.id"
:visible="showInfoSide"
/>
</el-drawer>
<!-- PC端 -->
<div v-else :class="['tab_content_main_info_side', { 'show_info_side': showInfoSide }]">
<InfoSide
<ServerStatus
ref="infoSideRef"
:host-info="curHost"
:visible="visible"
:ping-data="pingData"
:host-id="item.id"
:visible="showInfoSide"
/>
</div>
<div
@@ -393,7 +391,8 @@ import { ArrowDown } from '@element-plus/icons-vue'
import useMobileWidth from '@/composables/useMobileWidth'
import { terminalStatusList } from '@/utils/enum'
import Terminal from './terminal.vue'
import InfoSide from './info-side.vue'
// import InfoSide from './info-side.vue'
import ServerStatus from './server-status.vue'
import HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
import FooterBar from './footer-bar.vue'
+12
View File
@@ -865,6 +865,11 @@
dependencies:
vary "^1.1.2"
"@kurkle/color@^0.3.0":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf"
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0", "@lezer/common@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
@@ -2028,6 +2033,13 @@ chalk@^4.0.0, chalk@^4.1.1, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.5.0.tgz#11a1ef6c4befc514b1b0b613ebac226c4ad2740b"
integrity sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==
dependencies:
"@kurkle/color" "^0.3.0"
chokidar@^3.5.2, chokidar@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"