feat: update monitor, terminal, and SFTP interaction flow

This commit is contained in:
liumangmang
2026-04-01 15:22:51 +08:00
parent 832d55c722
commit 9f133bd337
6 changed files with 644 additions and 564 deletions

View File

@@ -46,27 +46,68 @@ public class MonitorController {
Map<String, Object> metrics = new HashMap<>();
// 获取CPU使用率
// 获取CPU使用率(兼容多种系统)
try {
double cpuUsage = 0;
try {
// 优先使用top
String cpuOutput = sshService.executeCommand(conn, password, privateKey, passphrase,
"top -bn1 | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/' | awk '{print 100 - $1}'");
double cpuUsage = Double.parseDouble(cpuOutput.trim());
"top -bn1 2>/dev/null | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/' | awk '{print 100 - $1}'");
if (!cpuOutput.trim().isEmpty()) {
cpuUsage = Double.parseDouble(cpuOutput.trim());
} else {
throw new Exception("top command failed");
}
} catch (Exception e) {
// 备用方案:使用/proc/stat计算需要两次采样这里简化处理用vmstat
try {
String vmstatOutput = sshService.executeCommand(conn, password, privateKey, passphrase,
"vmstat 1 2 | tail -1 | awk '{print 100 - $15}'");
cpuUsage = Double.parseDouble(vmstatOutput.trim());
} catch (Exception e2) {
throw new Exception("Both top and vmstat failed");
}
}
metrics.put("cpuUsage", Math.round(cpuUsage * 10.0) / 10.0);
} catch (Exception e) {
metrics.put("cpuUsage", null);
}
// 获取内存信息
// 获取内存信息(兼容多种系统)
try {
long totalMem = 0;
long usedMem = 0;
try {
// 优先使用free -b
String memOutput = sshService.executeCommand(conn, password, privateKey, passphrase,
"free -b | grep Mem");
"free -b 2>/dev/null | grep Mem");
if (!memOutput.trim().isEmpty()) {
String[] memParts = memOutput.trim().split("\\s+");
long totalMem = Long.parseLong(memParts[1]);
long usedMem = Long.parseLong(memParts[2]);
if (memParts.length >= 3) {
totalMem = Long.parseLong(memParts[1]);
usedMem = Long.parseLong(memParts[2]);
}
}
} catch (Exception e) {
// 备用方案:从/proc/meminfo读取
String meminfoOutput = sshService.executeCommand(conn, password, privateKey, passphrase,
"cat /proc/meminfo | grep -E 'MemTotal|MemAvailable'");
String[] lines = meminfoOutput.trim().split("\n");
if (lines.length >= 2) {
totalMem = Long.parseLong(lines[0].replaceAll("\\D+", "")) * 1024;
long availableMem = Long.parseLong(lines[1].replaceAll("\\D+", "")) * 1024;
usedMem = totalMem - availableMem;
}
}
if (totalMem > 0 && usedMem >= 0) {
double memUsage = (double) usedMem / totalMem * 100;
metrics.put("memTotal", totalMem);
metrics.put("memUsed", usedMem);
metrics.put("memUsage", Math.round(memUsage * 10.0) / 10.0);
} else {
throw new Exception("Failed to parse memory info");
}
} catch (Exception e) {
metrics.put("memTotal", null);
metrics.put("memUsed", null);

View File

@@ -3,7 +3,7 @@ import { nextTick, ref, onMounted, onUnmounted, watch } from 'vue'
import { Terminal } from 'xterm'
import { FitAddon } from '@xterm/addon-fit'
import { useAuthStore } from '../stores/auth'
import axios from 'axios'
import client from '../api/client'
import { Activity, Cpu, HardDrive, MemoryStick, Clock, ChevronUp, ChevronDown } from 'lucide-vue-next'
import 'xterm/css/xterm.css'
@@ -74,7 +74,7 @@ async function fetchMonitorData() {
if (!props.active || status.value !== 'connected') return
monitorLoading.value = true
try {
const res = await axios.get(`/api/monitor/${props.connectionId}`)
const res = await client.get(`/monitor/${props.connectionId}`)
monitorData.value = res.data
} catch {
// 静默失败,不影响终端使用

View File

@@ -193,7 +193,7 @@ function handleSftpTabClose(tabId: string, connectionId: number, event: Event) {
<RouterView v-slot="{ Component, route }">
<template v-if="!showTerminalWorkspace">
<keep-alive :max="10" v-if="route.meta.keepAlive">
<component :is="Component" :key="route.fullPath" />
<component :is="Component" :key="route.params.id" />
</keep-alive>
<component :is="Component" :key="route.fullPath" v-else />
</template>

View File

@@ -12,6 +12,7 @@ export interface SftpTab {
export const useSftpTabsStore = defineStore('sftpTabs', () => {
const tabs = ref<SftpTab[]>([])
const activeTabId = ref<string | null>(null)
const connectionLoadState = ref<Map<number, number>>(new Map())
const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value) || null)
@@ -60,7 +61,14 @@ export const useSftpTabsStore = defineStore('sftpTabs', () => {
const index = tabs.value.findIndex(t => t.id === tabId)
if (index === -1) return
const tab = tabs.value[index]
if (!tab) return
const wasActive = activeTabId.value === tabId
// Clean up connection state
connectionLoadState.value.delete(tab.connectionId)
tabs.value.splice(index, 1)
if (wasActive && tabs.value.length > 0) {
@@ -73,6 +81,18 @@ export const useSftpTabsStore = defineStore('sftpTabs', () => {
syncActiveState()
}
function setConnectionLoaded(connectionId: number) {
connectionLoadState.value.set(connectionId, connectionId)
}
function getLastLoadedConnectionId(connectionId: number): number | null {
return connectionLoadState.value.get(connectionId) ?? null
}
function clearConnectionState(connectionId: number) {
connectionLoadState.value.delete(connectionId)
}
return {
tabs,
activeTabId,
@@ -80,5 +100,8 @@ export const useSftpTabsStore = defineStore('sftpTabs', () => {
openOrFocus,
activate,
close,
setConnectionLoaded,
getLastLoadedConnectionId,
clearConnectionState,
}
})

View File

@@ -28,10 +28,11 @@ const sftpTabsStore = useSftpTabsStore()
const showForm = ref(false)
const editingConn = ref<Connection | null>(null)
const searchQuery = ref('')
let searchDebounceTimer = 0
const keyword = computed(() => route.query.q as string || '')
const filteredConnections = computed(() => {
const q = keyword.value.trim().toLowerCase()
const q = searchQuery.value.trim().toLowerCase()
if (!q) {
return store.connections
@@ -59,7 +60,10 @@ onMounted(() => {
})
watch(searchQuery, (val) => {
clearTimeout(searchDebounceTimer)
searchDebounceTimer = window.setTimeout(() => {
updateSearchParam(val)
}, 300)
})
function openCreate() {
@@ -107,7 +111,7 @@ function clearSearch() {
}
function highlightMatch(text: string): string {
const q = keyword.value.trim()
const q = searchQuery.value.trim()
if (!q) return text
const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')

View File

@@ -212,7 +212,7 @@ function resetVolatileSftpState() {
watch(
() => route.params.id,
async (routeId, _, onCleanup) => {
async (routeId, _oldRouteId, onCleanup) => {
const requestId = ++routeInitRequestId
let cleanedUp = false
onCleanup(() => {
@@ -220,11 +220,18 @@ watch(
})
const isRouteInitCancelled = () => cleanedUp
resetVolatileSftpState()
const rawId = Array.isArray(routeId) ? routeId[0] : routeId
const parsedId = Number(rawId)
// 只在切换到不同连接时重置状态
// 使用 store 中的状态跟踪,确保在组件重建后仍能正确判断
const lastLoaded = sftpTabsStore.getLastLoadedConnectionId(parsedId)
const isConnectionChanged = lastLoaded !== parsedId
if (isConnectionChanged) {
resetVolatileSftpState()
}
if (!rawId || !Number.isInteger(parsedId) || parsedId <= 0) {
if (isStaleRouteInit(requestId, isRouteInitCancelled)) return
conn.value = undefined
@@ -255,7 +262,12 @@ watch(
if (isStaleRouteInit(requestId, isRouteInitCancelled)) return
conn.value = targetConnection
sftpTabsStore.openOrFocus(targetConnection)
// 只在切换到不同连接时初始化路径
if (isConnectionChanged) {
sftpTabsStore.setConnectionLoaded(parsedId)
await initPath(parsedId, requestId, isRouteInitCancelled)
}
},
{ immediate: true }
)