feat: update monitor, terminal, and SFTP interaction flow
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
// 静默失败,不影响终端使用
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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, '\\$&')
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user