feat: 优化状态监视器,重构为单例模式
This commit is contained in:
@@ -159,12 +159,9 @@ const componentProps = computed(() => {
|
||||
// FileManager 可能也需要转发事件,例如文件操作相关的,暂时省略
|
||||
};
|
||||
case 'statusMonitor':
|
||||
// 仅当有活动会话时才返回实际 props,否则返回空对象
|
||||
if (!currentActiveSession) return {};
|
||||
// 始终渲染,传递 activeSessionId
|
||||
return {
|
||||
sessionId: props.activeSessionId ?? '', // 确保 sessionId 不为 null
|
||||
serverStatus: currentActiveSession.statusMonitorManager.serverStatus.value, // 此时 currentActiveSession 必不为 null
|
||||
statusError: currentActiveSession.statusMonitorManager.statusError.value, // 此时 currentActiveSession 必不为 null
|
||||
activeSessionId: props.activeSessionId, // 传递 activeSessionId
|
||||
class: 'pane-content',
|
||||
};
|
||||
case 'editor':
|
||||
@@ -253,17 +250,11 @@ const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | '
|
||||
return baseProps; // Return only base props if no active session
|
||||
}
|
||||
case 'statusMonitor':
|
||||
// Only provide props if there's an active session
|
||||
if (activeSession.value) {
|
||||
return {
|
||||
...baseProps,
|
||||
sessionId: activeSession.value.sessionId, // Pass session ID
|
||||
serverStatus: activeSession.value.statusMonitorManager.serverStatus.value,
|
||||
statusError: activeSession.value.statusMonitorManager.statusError.value,
|
||||
};
|
||||
} else {
|
||||
return baseProps; // Return only base props if no active session
|
||||
}
|
||||
// 始终渲染,传递 activeSessionId
|
||||
return {
|
||||
...baseProps,
|
||||
activeSessionId: props.activeSessionId, // 传递 activeSessionId
|
||||
};
|
||||
// Add cases for other components if they need specific props or event forwarding in the sidebar
|
||||
// case 'commandHistory': return { ...baseProps, onExecuteCommand: (cmd: string) => emit('sendCommand', cmd) };
|
||||
// case 'quickCommands': return { ...baseProps, onExecuteCommand: (cmd: string) => emit('sendCommand', cmd) };
|
||||
@@ -493,20 +484,13 @@ onMounted(() => {
|
||||
</template>
|
||||
<!-- StatusMonitor -->
|
||||
<template v-else-if="layoutNode.component === 'statusMonitor'">
|
||||
<component
|
||||
v-if="activeSession"
|
||||
:is="currentMainComponent"
|
||||
:key="activeSessionId"
|
||||
v-bind="componentProps"
|
||||
class="flex-grow overflow-auto"
|
||||
/>
|
||||
<div v-else class="flex-grow flex justify-center items-center text-center text-text-secondary bg-header text-sm p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8 w-full h-full">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium text-text-secondary mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs text-text-secondary mt-2">{{ t('layout.noActiveSession.message') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<keep-alive>
|
||||
<component
|
||||
:is="currentMainComponent"
|
||||
v-bind="componentProps"
|
||||
class="flex-grow overflow-auto"
|
||||
/>
|
||||
</keep-alive>
|
||||
</template>
|
||||
<!-- Other Panes -->
|
||||
<template v-else-if="currentMainComponent">
|
||||
@@ -547,26 +531,22 @@ onMounted(() => {
|
||||
<KeepAlive>
|
||||
<div :key="`left-sidebar-content-${activeLeftSidebarPane ?? 'none'}`" class="relative flex flex-col flex-grow overflow-hidden pt-10"> <!-- Added pt-10 -->
|
||||
<component
|
||||
v-if="currentLeftSidebarComponent && activeLeftSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)"
|
||||
:is="currentLeftSidebarComponent"
|
||||
:key="`left-comp-${activeLeftSidebarPane}`"
|
||||
v-bind="sidebarProps(activeLeftSidebarPane, 'left')"
|
||||
class="flex flex-col flex-grow">
|
||||
</component>
|
||||
<div v-else-if="activeLeftSidebarPane === 'fileManager' && !activeSession" class="flex flex-col flex-grow justify-center items-center text-center text-text-secondary p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs mt-2">{{ t('layout.noActiveSession.fileManagerSidebar') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="activeLeftSidebarPane === 'statusMonitor' && !activeSession" class="flex flex-col flex-grow justify-center items-center text-center text-text-secondary p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs mt-2">{{ t('layout.noActiveSession.statusMonitorSidebar') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
v-if="currentLeftSidebarComponent && activeLeftSidebarPane && (activeLeftSidebarPane === 'statusMonitor' || activeLeftSidebarPane !== 'fileManager' || activeSession)"
|
||||
:is="currentLeftSidebarComponent"
|
||||
:key="`left-comp-${activeLeftSidebarPane}`"
|
||||
v-bind="sidebarProps(activeLeftSidebarPane, 'left')"
|
||||
class="flex flex-col flex-grow">
|
||||
</component>
|
||||
<!-- 'fileManager' 且无 activeSession 的提示 -->
|
||||
<div v-else-if="activeLeftSidebarPane === 'fileManager' && !activeSession" class="flex flex-col flex-grow justify-center items-center text-center text-text-secondary p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs mt-2">{{ t('layout.noActiveSession.fileManagerSidebar') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移除 statusMonitor 的 v-else-if -->
|
||||
<div v-else class="flex flex-col flex-grow">
|
||||
</div>
|
||||
</div>
|
||||
@@ -583,26 +563,21 @@ onMounted(() => {
|
||||
<KeepAlive>
|
||||
<div :key="`right-sidebar-content-${activeRightSidebarPane ?? 'none'}`" class="relative flex flex-col flex-grow overflow-hidden pt-10"> <!-- Added pt-10 -->
|
||||
<component
|
||||
v-if="currentRightSidebarComponent && activeRightSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)"
|
||||
:is="currentRightSidebarComponent"
|
||||
:key="`right-comp-${activeRightSidebarPane}`"
|
||||
v-bind="sidebarProps(activeRightSidebarPane, 'right')"
|
||||
class="flex flex-col flex-grow">
|
||||
</component>
|
||||
<div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="flex flex-col flex-grow justify-center items-center text-center text-text-secondary p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs mt-2">{{ t('layout.noActiveSession.fileManagerSidebar') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="activeRightSidebarPane === 'statusMonitor' && !activeSession" class="flex flex-col flex-grow justify-center items-center text-center text-text-secondary p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs mt-2">{{ t('layout.noActiveSession.statusMonitorSidebar') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
v-if="currentRightSidebarComponent && activeRightSidebarPane && (activeRightSidebarPane === 'statusMonitor' || activeRightSidebarPane !== 'fileManager' || activeSession)"
|
||||
:is="currentRightSidebarComponent"
|
||||
:key="`right-comp-${activeRightSidebarPane}`"
|
||||
v-bind="sidebarProps(activeRightSidebarPane, 'right')"
|
||||
class="flex flex-col flex-grow">
|
||||
</component>
|
||||
<!-- 'fileManager' 且无 activeSession 的提示 -->
|
||||
<div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="flex flex-col flex-grow justify-center items-center text-center text-text-secondary p-4">
|
||||
<div class="flex flex-col items-center justify-center p-8">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
<div class="text-xs mt-2">{{ t('layout.noActiveSession.fileManagerSidebar') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 移除 statusMonitor 的 v-else-if -->
|
||||
<div v-else class="flex flex-col flex-grow">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { ref, watch, onMounted, computed, type PropType } from 'vue'; // 添加 PropType
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Line } from 'vue-chartjs';
|
||||
import { useSessionStore } from '../stores/session.store'; // 注入 sessionStore
|
||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
Title,
|
||||
@@ -78,15 +80,24 @@ interface ServerStatusData {
|
||||
// ... other properties if needed
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
serverStatus: ServerStatusData | null;
|
||||
}>();
|
||||
const props = defineProps({
|
||||
serverStatus: { // Keep serverStatus for current values and Y-axis scaling logic
|
||||
type: Object as PropType<ServerStatusData | null>,
|
||||
required: true,
|
||||
},
|
||||
activeSessionId: {
|
||||
type: String as PropType<string | null>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const MAX_DATA_POINTS = 60;
|
||||
const KB_TO_MB_THRESHOLD = 1024; // For network
|
||||
const MB_TO_GB_THRESHOLD = 1024; // For memory
|
||||
|
||||
const { t } = useI18n();
|
||||
const sessionStore = useSessionStore();
|
||||
const { sessions } = storeToRefs(sessionStore); // 获取响应式的 sessions
|
||||
|
||||
const cpuChartKey = ref(0);
|
||||
const memoryChartKey = ref(0);
|
||||
@@ -101,62 +112,130 @@ const memoryUnitIsGB = ref(false);
|
||||
|
||||
const initialLabels = Array.from({ length: MAX_DATA_POINTS }, (_, i) => `-${(MAX_DATA_POINTS - 1 - i)}s`);
|
||||
|
||||
const cpuChartData = ref({
|
||||
labels: [...initialLabels],
|
||||
// --- 计算属性:从 Store 获取当前会话的历史数据 ---
|
||||
const currentSessionStatusManager = computed(() => {
|
||||
return props.activeSessionId ? sessions.value.get(props.activeSessionId)?.statusMonitorManager : null;
|
||||
});
|
||||
|
||||
const currentCpuHistory = computed(() => {
|
||||
return currentSessionStatusManager.value?.cpuHistory.value ?? Array(MAX_DATA_POINTS).fill(null);
|
||||
});
|
||||
|
||||
const currentMemUsedHistory = computed(() => {
|
||||
// 返回 MB 为单位的历史数据
|
||||
return currentSessionStatusManager.value?.memUsedHistory.value ?? Array(MAX_DATA_POINTS).fill(null);
|
||||
});
|
||||
|
||||
const currentNetRxHistory = computed(() => {
|
||||
// 返回 Bytes/sec 为单位的历史数据
|
||||
return currentSessionStatusManager.value?.netRxHistory.value ?? Array(MAX_DATA_POINTS).fill(null);
|
||||
});
|
||||
|
||||
const currentNetTxHistory = computed(() => {
|
||||
// 返回 Bytes/sec 为单位的历史数据
|
||||
return currentSessionStatusManager.value?.netTxHistory.value ?? Array(MAX_DATA_POINTS).fill(null);
|
||||
});
|
||||
|
||||
|
||||
// --- 图表数据结构,现在 data 指向 computed 属性 ---
|
||||
const cpuChartData = computed(() => ({
|
||||
labels: initialLabels, // 标签保持不变
|
||||
datasets: [
|
||||
{
|
||||
label: computed(() => t('statusMonitor.cpuUsageLabel')),
|
||||
label: t('statusMonitor.cpuUsageLabel'),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1,
|
||||
data: Array(MAX_DATA_POINTS).fill(0),
|
||||
data: currentCpuHistory.value.map(v => v ?? 0), // 将 null 映射为 0 用于图表
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// 动态计算内存图表数据(转换单位)
|
||||
const memoryChartData = computed(() => {
|
||||
const historyMB = currentMemUsedHistory.value;
|
||||
let displayData: (number | null)[];
|
||||
|
||||
// 检查是否需要转换为 GB (基于当前值或历史峰值)
|
||||
const currentTotalMB = props.serverStatus?.memTotal ?? 0;
|
||||
const historyPeakMB = Math.max(...historyMB.filter((v): v is number => v !== null), 0);
|
||||
const requiresGB = currentTotalMB >= MB_TO_GB_THRESHOLD || historyPeakMB >= MB_TO_GB_THRESHOLD;
|
||||
memoryUnitIsGB.value = requiresGB; // 更新单位标志
|
||||
|
||||
if (requiresGB) {
|
||||
displayData = historyMB.map(mb => mb === null ? null : parseFloat((mb / MB_TO_GB_THRESHOLD).toFixed(1)));
|
||||
} else {
|
||||
displayData = historyMB.map(mb => mb === null ? null : parseFloat(mb.toFixed(1)));
|
||||
}
|
||||
|
||||
return {
|
||||
labels: initialLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('statusMonitor.memoryUsageLabelUnit', { unit: requiresGB ? 'GB' : 'MB' }),
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
borderWidth: 1,
|
||||
data: displayData.map(v => v ?? 0), // 将 null 映射为 0
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const memoryChartData = ref({
|
||||
labels: [...initialLabels],
|
||||
datasets: [
|
||||
{
|
||||
label: computed(() => t('statusMonitor.memoryUsageLabelUnit', { unit: memoryUnitIsGB.value ? 'GB' : 'MB' })),
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
borderWidth: 1,
|
||||
data: Array(MAX_DATA_POINTS).fill(0),
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const networkChartData = ref({
|
||||
labels: [...initialLabels],
|
||||
datasets: [
|
||||
{
|
||||
label: computed(() => t('statusMonitor.networkDownloadLabelUnit', { unit: networkRateUnitIsMB.value ? 'MB/s' : 'KB/s' })),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1,
|
||||
data: Array(MAX_DATA_POINTS).fill(0),
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
{
|
||||
label: computed(() => t('statusMonitor.networkUploadLabelUnit', { unit: networkRateUnitIsMB.value ? 'MB/s' : 'KB/s' })),
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderColor: 'rgba(255, 159, 64, 1)',
|
||||
borderWidth: 1,
|
||||
data: Array(MAX_DATA_POINTS).fill(0),
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
// 动态计算网络图表数据(转换单位)
|
||||
const networkChartData = computed(() => {
|
||||
const historyRxBps = currentNetRxHistory.value;
|
||||
const historyTxBps = currentNetTxHistory.value;
|
||||
let displayRxData: (number | null)[];
|
||||
let displayTxData: (number | null)[];
|
||||
|
||||
// 检查是否需要转换为 MB/s (基于当前值或历史峰值)
|
||||
const currentRxKB = (props.serverStatus?.netRxRate ?? 0) / 1024;
|
||||
const currentTxKB = (props.serverStatus?.netTxRate ?? 0) / 1024;
|
||||
const historyPeakRxKB = Math.max(...historyRxBps.filter((v): v is number => v !== null).map(bps => bps / 1024), 0);
|
||||
const historyPeakTxKB = Math.max(...historyTxBps.filter((v): v is number => v !== null).map(bps => bps / 1024), 0);
|
||||
const requiresMB = currentRxKB >= KB_TO_MB_THRESHOLD || currentTxKB >= KB_TO_MB_THRESHOLD ||
|
||||
historyPeakRxKB >= KB_TO_MB_THRESHOLD || historyPeakTxKB >= KB_TO_MB_THRESHOLD;
|
||||
networkRateUnitIsMB.value = requiresMB; // 更新单位标志
|
||||
|
||||
const divisor = requiresMB ? (1024 * 1024) : 1024;
|
||||
const precision = requiresMB ? 2 : 1;
|
||||
|
||||
displayRxData = historyRxBps.map(bps => bps === null ? null : parseFloat((bps / divisor).toFixed(precision)));
|
||||
displayTxData = historyTxBps.map(bps => bps === null ? null : parseFloat((bps / divisor).toFixed(precision)));
|
||||
|
||||
return {
|
||||
labels: initialLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: t('statusMonitor.networkDownloadLabelUnit', { unit: requiresMB ? 'MB/s' : 'KB/s' }),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1,
|
||||
data: displayRxData.map(v => v ?? 0), // 将 null 映射为 0
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
{
|
||||
label: t('statusMonitor.networkUploadLabelUnit', { unit: requiresMB ? 'MB/s' : 'KB/s' }),
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderColor: 'rgba(255, 159, 64, 1)',
|
||||
borderWidth: 1,
|
||||
data: displayTxData.map(v => v ?? 0), // 将 null 映射为 0
|
||||
tension: 0.1,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 5,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const baseChartOptions: Omit<ChartOptions<'line'>, 'scales'> = {
|
||||
@@ -280,174 +359,96 @@ const networkChartOptions = ref<ChartOptions<'line'>>({
|
||||
});
|
||||
|
||||
|
||||
const updateCharts = (newStatus: ServerStatusData | null) => {
|
||||
if (!newStatus) return;
|
||||
// --- 函数:动态更新 Y 轴范围和单位(基于当前 serverStatus 和 store 中的历史数据) ---
|
||||
const updateAxisAndUnits = () => {
|
||||
// 内存 Y 轴和单位
|
||||
if (props.serverStatus && memoryChartOptions.value.scales?.y) {
|
||||
const historyMB = currentMemUsedHistory.value;
|
||||
const memTotal = props.serverStatus.memTotal ?? 0;
|
||||
const requiresGB = memoryUnitIsGB.value; // memoryChartData computed prop already sets this
|
||||
|
||||
// Update CPU Chart
|
||||
if (typeof newStatus.cpuPercent === 'number') {
|
||||
const newCpuData = [...cpuChartData.value.datasets[0].data];
|
||||
newCpuData.shift();
|
||||
newCpuData.push(parseFloat(newStatus.cpuPercent.toFixed(1)));
|
||||
cpuChartData.value = { ...cpuChartData.value, datasets: [{ ...cpuChartData.value.datasets[0], data: newCpuData }] };
|
||||
}
|
||||
let yAxisTopValue = requiresGB
|
||||
? parseFloat((memTotal / MB_TO_GB_THRESHOLD).toFixed(1))
|
||||
: parseFloat(memTotal.toFixed(1));
|
||||
|
||||
// Update Memory Chart
|
||||
if (typeof newStatus.memUsed === 'number' && typeof newStatus.memTotal === 'number') {
|
||||
let currentMemUsed = newStatus.memUsed; // MB
|
||||
const memTotal = newStatus.memTotal; // MB
|
||||
let currentData = [...memoryChartData.value.datasets[0].data];
|
||||
|
||||
if (!memoryUnitIsGB.value && (memTotal >= MB_TO_GB_THRESHOLD || currentMemUsed >= MB_TO_GB_THRESHOLD)) {
|
||||
memoryUnitIsGB.value = true;
|
||||
currentData = currentData.map(d => parseFloat((d / MB_TO_GB_THRESHOLD).toFixed(1)));
|
||||
memoryChartKey.value++; // Force re-render
|
||||
} else if (memoryUnitIsGB.value && memTotal < MB_TO_GB_THRESHOLD && currentMemUsed < MB_TO_GB_THRESHOLD) {
|
||||
// This case is less likely if total is large, but handles potential fluctuations or incorrect initial state
|
||||
memoryUnitIsGB.value = false;
|
||||
currentData = currentData.map(d => parseFloat((d * MB_TO_GB_THRESHOLD).toFixed(1)));
|
||||
memoryChartKey.value++;
|
||||
}
|
||||
|
||||
let newMemValue;
|
||||
if (memoryUnitIsGB.value) {
|
||||
newMemValue = parseFloat((currentMemUsed / MB_TO_GB_THRESHOLD).toFixed(1));
|
||||
} else {
|
||||
newMemValue = parseFloat(currentMemUsed.toFixed(1));
|
||||
}
|
||||
|
||||
currentData.shift();
|
||||
currentData.push(newMemValue);
|
||||
memoryChartData.value = { ...memoryChartData.value, datasets: [{ ...memoryChartData.value.datasets[0], data: currentData }] };
|
||||
|
||||
if (memoryChartOptions.value.scales?.y && typeof memTotal === 'number') {
|
||||
let yAxisTopValue = memoryUnitIsGB.value
|
||||
? parseFloat((memTotal / MB_TO_GB_THRESHOLD).toFixed(1))
|
||||
: parseFloat(memTotal.toFixed(1));
|
||||
|
||||
// Ensure the yAxisTopValue is at least slightly larger than the max data point if data exceeds total.
|
||||
// Also, handle cases where memTotal might be 0 or very small.
|
||||
const currentMaxDataPoint = Math.max(...currentData, 0);
|
||||
// The y-axis max should primarily be driven by memTotal.
|
||||
// If current data somehow exceeds memTotal (which shouldn't happen for 'used' vs 'total'),
|
||||
// then the axis should expand. Otherwise, stick to memTotal.
|
||||
const currentMaxDataPoint = Math.max(...historyMB.filter((v): v is number => v !== null).map(mb => requiresGB ? mb / MB_TO_GB_THRESHOLD : mb), 0);
|
||||
yAxisTopValue = Math.max(yAxisTopValue, currentMaxDataPoint);
|
||||
|
||||
// If memTotal is 0 (and thus yAxisTopValue is 0 after parseFloat), set a small default max.
|
||||
if (yAxisTopValue === 0) {
|
||||
yAxisTopValue = memoryUnitIsGB.value ? 1 : 100; // Default small max if total is 0
|
||||
yAxisTopValue = requiresGB ? 1 : 100;
|
||||
} else {
|
||||
// Add a very tiny buffer if yAxisTopValue is not 0, to ensure the top line is visible if data hits the max.
|
||||
// This helps if Chart.js clips data exactly at the 'max'.
|
||||
// For example, if max is 8.0GB, a data point of 8.0GB might be at the very edge.
|
||||
// A small increment ensures it's clearly within the chart area.
|
||||
const epsilon = memoryUnitIsGB.value ? 0.01 : 1;
|
||||
yAxisTopValue += epsilon;
|
||||
// Re-round after adding epsilon to maintain clean ticks if possible
|
||||
if (memoryUnitIsGB.value) {
|
||||
yAxisTopValue = Math.ceil(yAxisTopValue * 100) / 100; // Round to 2 decimal places for GB after epsilon
|
||||
} else {
|
||||
yAxisTopValue = Math.ceil(yAxisTopValue); // Round to next integer for MB after epsilon
|
||||
}
|
||||
const epsilon = requiresGB ? 0.01 : 1;
|
||||
yAxisTopValue += epsilon;
|
||||
yAxisTopValue = requiresGB ? Math.ceil(yAxisTopValue * 100) / 100 : Math.ceil(yAxisTopValue);
|
||||
}
|
||||
|
||||
if (memoryChartOptions.value.scales.y.max !== yAxisTopValue) {
|
||||
memoryChartOptions.value.scales.y.max = yAxisTopValue;
|
||||
memoryChartKey.value++; // 强制重绘以应用新的 max
|
||||
}
|
||||
memoryChartOptions.value.scales.y.max = yAxisTopValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 网络 Y 轴和单位
|
||||
if (props.serverStatus && networkChartOptions.value.scales?.y) {
|
||||
const historyRxBps = currentNetRxHistory.value;
|
||||
const historyTxBps = currentNetTxHistory.value;
|
||||
const requiresMB = networkRateUnitIsMB.value; // networkChartData computed prop already sets this
|
||||
const divisor = requiresMB ? (1024 * 1024) : 1024;
|
||||
|
||||
// Update Network Chart
|
||||
let currentNetRxRateKB = (newStatus.netRxRate || 0) / 1024;
|
||||
let currentNetTxRateKB = (newStatus.netTxRate || 0) / 1024;
|
||||
let newNetRxData = [...networkChartData.value.datasets[0].data];
|
||||
let newNetTxData = [...networkChartData.value.datasets[1].data];
|
||||
const allNetworkData = [
|
||||
...historyRxBps.filter((v): v is number => v !== null).map(bps => bps / divisor),
|
||||
...historyTxBps.filter((v): v is number => v !== null).map(bps => bps / divisor)
|
||||
];
|
||||
const currentMaxDataPoint = Math.max(...allNetworkData, 0);
|
||||
|
||||
if (!networkRateUnitIsMB.value && (currentNetRxRateKB >= KB_TO_MB_THRESHOLD || currentNetTxRateKB >= KB_TO_MB_THRESHOLD)) {
|
||||
networkRateUnitIsMB.value = true;
|
||||
newNetRxData = newNetRxData.map(d => parseFloat((d / KB_TO_MB_THRESHOLD).toFixed(2)));
|
||||
newNetTxData = newNetTxData.map(d => parseFloat((d / KB_TO_MB_THRESHOLD).toFixed(2)));
|
||||
networkChartKey.value++;
|
||||
}
|
||||
|
||||
let newRxValue, newTxValue;
|
||||
if (networkRateUnitIsMB.value) {
|
||||
newRxValue = parseFloat((currentNetRxRateKB / KB_TO_MB_THRESHOLD).toFixed(2));
|
||||
newTxValue = parseFloat((currentNetTxRateKB / KB_TO_MB_THRESHOLD).toFixed(2));
|
||||
} else {
|
||||
newRxValue = parseFloat(currentNetRxRateKB.toFixed(1));
|
||||
newTxValue = parseFloat(currentNetTxRateKB.toFixed(1));
|
||||
}
|
||||
|
||||
newNetRxData.shift();
|
||||
newNetRxData.push(newRxValue);
|
||||
newNetTxData.shift();
|
||||
newNetTxData.push(newTxValue);
|
||||
|
||||
networkChartData.value = {
|
||||
...networkChartData.value,
|
||||
datasets: [
|
||||
{ ...networkChartData.value.datasets[0], data: newNetRxData },
|
||||
{ ...networkChartData.value.datasets[1], data: newNetTxData },
|
||||
],
|
||||
};
|
||||
|
||||
if (networkChartOptions.value.scales?.y) {
|
||||
const allNetworkData = [...newNetRxData, ...newNetTxData];
|
||||
const currentMaxDataPoint = Math.max(...allNetworkData, 0);
|
||||
|
||||
let suggestedMax;
|
||||
const baseMultiplier = 1.2; // 20% buffer
|
||||
|
||||
if (currentMaxDataPoint === 0) {
|
||||
// If no data or all data is zero, set a default max based on unit
|
||||
suggestedMax = networkRateUnitIsMB.value ? 5 : 500; // Default 5MB/s or 500KB/s
|
||||
} else {
|
||||
suggestedMax = currentMaxDataPoint * baseMultiplier;
|
||||
}
|
||||
|
||||
// Determine a sensible minimum for the y-axis max based on the unit
|
||||
// This prevents the y-axis from being too small if data values are tiny (e.g., 0.01 MB/s)
|
||||
const absoluteMinMax = networkRateUnitIsMB.value ? 1 : 100; // Min 1MB/s or 100KB/s
|
||||
|
||||
// Ensure suggestedMax is at least the absoluteMinMax
|
||||
suggestedMax = Math.max(suggestedMax, absoluteMinMax);
|
||||
|
||||
// Round up to the next sensible integer for MB/s or a larger step for KB/s for cleaner ticks
|
||||
if (networkRateUnitIsMB.value) {
|
||||
suggestedMax = Math.ceil(suggestedMax); // Round up to the next whole number for MB/s
|
||||
} else {
|
||||
// For KB/s, round up to the nearest 50 or 100 for cleaner ticks
|
||||
if (suggestedMax <= 100) { // if max is very low (e.g. 10KB/s), round to 10s or 20s
|
||||
suggestedMax = Math.ceil(suggestedMax / 10) * 10;
|
||||
if (suggestedMax === 0 && currentMaxDataPoint > 0) suggestedMax = 10; // ensure at least 10 if there's data
|
||||
} else if (suggestedMax <= 500) {
|
||||
suggestedMax = Math.ceil(suggestedMax / 50) * 50; // Round to nearest 50 if under 500KB/s
|
||||
let suggestedMax;
|
||||
const baseMultiplier = 1.2;
|
||||
if (currentMaxDataPoint === 0) {
|
||||
suggestedMax = requiresMB ? 5 : 500;
|
||||
} else {
|
||||
suggestedMax = Math.ceil(suggestedMax / 100) * 100; // Round to nearest 100 if over 500KB/s
|
||||
suggestedMax = currentMaxDataPoint * baseMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// Final safety check: if there was some data, max should not be zero.
|
||||
if (currentMaxDataPoint > 0 && suggestedMax === 0) {
|
||||
suggestedMax = networkRateUnitIsMB.value ? 1 : (allNetworkData.some(d => d > 0 && d < 10) ? 10 : 100) ;
|
||||
}
|
||||
const absoluteMinMax = requiresMB ? 1 : 100;
|
||||
suggestedMax = Math.max(suggestedMax, absoluteMinMax);
|
||||
|
||||
// If all data points are zero, ensure a minimum default axis.
|
||||
if (currentMaxDataPoint === 0 && suggestedMax === 0) {
|
||||
suggestedMax = networkRateUnitIsMB.value ? 1 : 100;
|
||||
}
|
||||
if (requiresMB) {
|
||||
suggestedMax = Math.ceil(suggestedMax);
|
||||
} else {
|
||||
if (suggestedMax <= 100) {
|
||||
suggestedMax = Math.ceil(suggestedMax / 10) * 10;
|
||||
if (suggestedMax === 0 && currentMaxDataPoint > 0) suggestedMax = 10;
|
||||
} else if (suggestedMax <= 500) {
|
||||
suggestedMax = Math.ceil(suggestedMax / 50) * 50;
|
||||
} else {
|
||||
suggestedMax = Math.ceil(suggestedMax / 100) * 100;
|
||||
}
|
||||
}
|
||||
|
||||
if (networkChartOptions.value.scales.y.max !== suggestedMax) {
|
||||
networkChartOptions.value.scales.y.max = suggestedMax;
|
||||
networkChartKey.value++; // Force re-render if max value changed
|
||||
}
|
||||
if (currentMaxDataPoint > 0 && suggestedMax === 0) {
|
||||
suggestedMax = requiresMB ? 1 : (allNetworkData.some(d => d > 0 && d < 10 / divisor) ? 10 : 100);
|
||||
}
|
||||
if (currentMaxDataPoint === 0 && suggestedMax === 0) {
|
||||
suggestedMax = requiresMB ? 1 : 100;
|
||||
}
|
||||
|
||||
if (networkChartOptions.value.scales.y.max !== suggestedMax) {
|
||||
networkChartOptions.value.scales.y.max = suggestedMax;
|
||||
networkChartKey.value++; // 强制重绘以应用新的 max
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.serverStatus, (newStatus) => {
|
||||
updateCharts(newStatus);
|
||||
}, { deep: true, immediate: true });
|
||||
// --- 监听 props.serverStatus 的变化,仅用于更新 Y 轴范围和单位 ---
|
||||
// 数据本身由 computed 属性从 store 获取
|
||||
watch(() => props.serverStatus, () => {
|
||||
updateAxisAndUnits();
|
||||
}, { deep: true, immediate: true }); // immediate: true 确保初始加载时设置好轴
|
||||
|
||||
// 移除监听 activeSessionId 的 watcher 和 resetChartData 函数
|
||||
|
||||
onMounted(() => {
|
||||
// Initial setup handled by watch immediate
|
||||
// 初始轴和单位设置由 watch immediate 处理
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -6,14 +6,20 @@
|
||||
{{ t('statusMonitor.title') }}
|
||||
</h4>
|
||||
|
||||
<!-- 无活动会话状态 -->
|
||||
<div v-if="!activeSessionId" class="no-session-status flex flex-col items-center justify-center text-center text-text-secondary mt-4 h-full">
|
||||
<i class="fas fa-plug text-4xl mb-3 text-text-secondary"></i>
|
||||
<span class="text-lg font-medium mb-2">{{ t('layout.noActiveSession.title') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="statusError" class="status-error flex flex-col items-center justify-center text-center text-red-500 mt-4 h-full">
|
||||
<div v-else-if="currentStatusError" class="status-error flex flex-col items-center justify-center text-center text-red-500 mt-4 h-full">
|
||||
<i class="fas fa-exclamation-triangle text-2xl mb-2"></i>
|
||||
<span>{{ t('statusMonitor.errorPrefix') }} {{ statusError }}</span>
|
||||
<span>{{ t('statusMonitor.errorPrefix') }} {{ currentStatusError }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-else-if="!serverStatus" class="loading-status flex flex-col items-center justify-center text-center text-text-secondary mt-4 h-full">
|
||||
<div v-else-if="!currentServerStatus" class="loading-status flex flex-col items-center justify-center text-center text-text-secondary mt-4 h-full">
|
||||
<i class="fas fa-spinner fa-spin text-2xl mb-2"></i>
|
||||
<span>{{ t('statusMonitor.loading') }}</span>
|
||||
</div>
|
||||
@@ -40,10 +46,10 @@
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow"> <!-- 减小高度 -->
|
||||
<div class="progress-bar bg-blue-500 h-full transition-width duration-300 ease-in-out" :style="{ width: `${serverStatus.cpuPercent ?? 0}%` }"></div>
|
||||
<div class="progress-bar bg-blue-500 h-full" :class="{ 'transition-width duration-300 ease-in-out': !isSwitchingSession }" :style="{ width: `${displayCpuPercent}%` }"></div>
|
||||
</div>
|
||||
<!-- 移除 w-12 和 text-right 以实现左对齐 -->
|
||||
<span class="font-mono text-left text-xs">{{ serverStatus.cpuPercent?.toFixed(1) ?? t('statusMonitor.notAvailable') }}%</span>
|
||||
<span class="font-mono text-left text-xs">{{ currentServerStatus.cpuPercent?.toFixed(1) ?? t('statusMonitor.notAvailable') }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +59,7 @@
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.memoryLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow">
|
||||
<div class="progress-bar bg-green-500 h-full transition-width duration-300 ease-in-out" :style="{ width: `${serverStatus.memPercent ?? 0}%` }"></div>
|
||||
<div class="progress-bar bg-green-500 h-full" :class="{ 'transition-width duration-300 ease-in-out': !isSwitchingSession }" :style="{ width: `${displayMemPercent}%` }"></div>
|
||||
</div>
|
||||
<span class="mem-disk-details font-mono text-xs whitespace-nowrap text-left">{{ memDisplay }}</span>
|
||||
</div>
|
||||
@@ -66,9 +72,9 @@
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow">
|
||||
<!-- swap颜色 -->
|
||||
<div class="progress-bar h-full transition-width duration-300 ease-in-out"
|
||||
:class="serverStatus?.swapPercent && serverStatus.swapPercent > 0 ? 'bg-yellow-500' : 'bg-gray-500'"
|
||||
:style="{ width: `${serverStatus?.swapPercent ?? 0}%` }"></div>
|
||||
<div class="progress-bar h-full"
|
||||
:class="[{ 'transition-width duration-300 ease-in-out': !isSwitchingSession }, (currentServerStatus?.swapPercent ?? 0) > 0 ? 'bg-yellow-500' : 'bg-gray-500']"
|
||||
:style="{ width: `${displaySwapPercent}%` }"></div>
|
||||
</div>
|
||||
<span class="mem-disk-details font-mono text-xs whitespace-nowrap text-left">{{ swapDisplay }}</span>
|
||||
</div>
|
||||
@@ -80,7 +86,7 @@
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.diskLabel') }}</label>
|
||||
<div class="value-wrapper flex items-center gap-2">
|
||||
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow">
|
||||
<div class="progress-bar bg-purple-500 h-full transition-width duration-300 ease-in-out" :style="{ width: `${serverStatus.diskPercent ?? 0}%` }"></div>
|
||||
<div class="progress-bar bg-purple-500 h-full" :class="{ 'transition-width duration-300 ease-in-out': !isSwitchingSession }" :style="{ width: `${displayDiskPercent}%` }"></div>
|
||||
</div>
|
||||
<span class="mem-disk-details font-mono text-xs whitespace-nowrap text-left">{{ diskDisplay }}</span>
|
||||
</div>
|
||||
@@ -91,32 +97,36 @@
|
||||
|
||||
<!-- 网络速率 -->
|
||||
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3 mt-2">
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.networkLabel') }} ({{ serverStatus?.netInterface || '...' }}):</label>
|
||||
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.networkLabel') }} ({{ currentServerStatus?.netInterface || '...' }}):</label>
|
||||
<div class="network-values flex items-center justify-start gap-4"> <!-- 减小间距 -->
|
||||
<span class="rate down inline-flex items-center gap-1 text-green-500 text-xs whitespace-nowrap">
|
||||
<i class="fas fa-arrow-down w-3 text-center"></i> <!-- Font Awesome 图标 -->
|
||||
<span class="font-mono">{{ formatBytesPerSecond(serverStatus?.netRxRate) }}</span>
|
||||
<span class="font-mono">{{ formatBytesPerSecond(currentServerStatus?.netRxRate) }}</span>
|
||||
</span>
|
||||
<span class="rate up inline-flex items-center gap-1 text-orange-500 text-xs whitespace-nowrap">
|
||||
<i class="fas fa-arrow-up w-3 text-center"></i> <!-- Font Awesome 图标 -->
|
||||
<span class="font-mono">{{ formatBytesPerSecond(serverStatus?.netTxRate) }}</span>
|
||||
<span class="font-mono">{{ formatBytesPerSecond(currentServerStatus?.netTxRate) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- 图表组件 -->
|
||||
<StatusCharts :server-status="serverStatus" />
|
||||
<!-- 仅当有活动会话且有数据时渲染图表 -->
|
||||
<StatusCharts v-if="activeSessionId && currentServerStatus" :server-status="currentServerStatus" :active-session-id="activeSessionId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed, watch, type PropType, nextTick } from 'vue'; // 添加 PropType 和 nextTick
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import StatusCharts from './StatusCharts.vue';
|
||||
|
||||
import { useSessionStore } from '../stores/session.store'; // 注入 sessionStore
|
||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||
const { t } = useI18n();
|
||||
|
||||
const sessionStore = useSessionStore();
|
||||
const { sessions } = storeToRefs(sessionStore); // 获取响应式的 sessions
|
||||
const isSwitchingSession = ref(false);
|
||||
|
||||
interface ServerStatus {
|
||||
cpuPercent?: number;
|
||||
@@ -136,17 +146,53 @@ interface ServerStatus {
|
||||
osName?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string;
|
||||
serverStatus: ServerStatus | null;
|
||||
statusError?: string | null;
|
||||
}>();
|
||||
// --- Props ---
|
||||
const props = defineProps({
|
||||
activeSessionId: {
|
||||
type: String as PropType<string | null>,
|
||||
required: false, // 允许为 null
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// --- Computed properties to get current session data ---
|
||||
const currentSessionState = computed(() => {
|
||||
return props.activeSessionId ? sessions.value.get(props.activeSessionId) : null;
|
||||
});
|
||||
|
||||
const currentServerStatus = computed<ServerStatus | null>(() => {
|
||||
return currentSessionState.value?.statusMonitorManager?.serverStatus?.value ?? null;
|
||||
});
|
||||
|
||||
// --- 计算属性,用于绑定到进度条宽度 ---
|
||||
// 始终返回当前状态的百分比。动画由 CSS 类控制。
|
||||
const displayCpuPercent = computed(() => {
|
||||
return currentServerStatus.value?.cpuPercent ?? 0;
|
||||
});
|
||||
|
||||
const displayMemPercent = computed(() => {
|
||||
return currentServerStatus.value?.memPercent ?? 0;
|
||||
});
|
||||
|
||||
const displaySwapPercent = computed(() => {
|
||||
return currentServerStatus.value?.swapPercent ?? 0;
|
||||
});
|
||||
|
||||
const displayDiskPercent = computed(() => {
|
||||
return currentServerStatus.value?.diskPercent ?? 0;
|
||||
});
|
||||
|
||||
const currentStatusError = computed<string | null>(() => {
|
||||
return currentSessionState.value?.statusMonitorManager?.statusError?.value ?? null;
|
||||
});
|
||||
|
||||
// --- 缓存逻辑保持不变 ---
|
||||
const cachedCpuModel = ref<string | null>(null);
|
||||
const cachedOsName = ref<string | null>(null);
|
||||
|
||||
watch(() => props.serverStatus, (newData) => {
|
||||
// --- Watcher for caching CPU Model and OS Name ---
|
||||
// 现在监听 currentServerStatus
|
||||
watch(currentServerStatus, (newData) => {
|
||||
if (newData) {
|
||||
if (newData.cpuModel !== undefined && newData.cpuModel !== null && newData.cpuModel !== '') {
|
||||
cachedCpuModel.value = newData.cpuModel;
|
||||
@@ -157,13 +203,24 @@ watch(() => props.serverStatus, (newData) => {
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// --- 计算属性保持不变 ---
|
||||
// --- 监听 activeSessionId 变化以处理会话切换状态 ---
|
||||
watch(() => props.activeSessionId, async (newId, oldId) => {
|
||||
if (newId !== oldId) {
|
||||
isSwitchingSession.value = true;
|
||||
await nextTick(); // 等待DOM更新(currentServerStatus已改变,displayPercent们会返回0)
|
||||
isSwitchingSession.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// --- Computed properties for display ---
|
||||
const displayCpuModel = computed(() => {
|
||||
return (props.serverStatus?.cpuModel ?? cachedCpuModel.value) || t('statusMonitor.notAvailable');
|
||||
// 使用 currentServerStatus
|
||||
return (currentServerStatus.value?.cpuModel ?? cachedCpuModel.value) || t('statusMonitor.notAvailable');
|
||||
});
|
||||
|
||||
const displayOsName = computed(() => {
|
||||
return (props.serverStatus?.osName ?? cachedOsName.value) || t('statusMonitor.notAvailable');
|
||||
// 使用 currentServerStatus
|
||||
return (currentServerStatus.value?.osName ?? cachedOsName.value) || t('statusMonitor.notAvailable');
|
||||
});
|
||||
|
||||
const formatBytesPerSecond = (bytes?: number): string => {
|
||||
@@ -194,21 +251,21 @@ const formatMemorySize = (mb?: number): string => {
|
||||
};
|
||||
|
||||
const memDisplay = computed(() => {
|
||||
const data = props.serverStatus;
|
||||
const data = currentServerStatus.value; // 使用 currentServerStatus
|
||||
if (!data || data.memUsed === undefined || data.memTotal === undefined) return t('statusMonitor.notAvailable');
|
||||
const percent = data.memPercent !== undefined ? `(${(data.memPercent).toFixed(1)}%)` : ''; // 百分比保留 1 位小数
|
||||
return `${formatMemorySize(data.memUsed)} / ${formatMemorySize(data.memTotal)} ${percent}`;
|
||||
});
|
||||
|
||||
const diskDisplay = computed(() => {
|
||||
const data = props.serverStatus;
|
||||
const data = currentServerStatus.value; // 使用 currentServerStatus
|
||||
if (!data || data.diskUsed === undefined || data.diskTotal === undefined) return t('statusMonitor.notAvailable');
|
||||
const percent = data.diskPercent !== undefined ? `(${(data.diskPercent).toFixed(1)}%)` : ''; // 百分比保留 1 位小数
|
||||
return `${formatKbToGb(data.diskUsed)} / ${formatKbToGb(data.diskTotal)} ${percent}`;
|
||||
});
|
||||
|
||||
const swapDisplay = computed(() => {
|
||||
const data = props.serverStatus;
|
||||
const data = currentServerStatus.value; // 使用 currentServerStatus
|
||||
const used = data?.swapUsed ?? 0;
|
||||
const total = data?.swapTotal ?? 0;
|
||||
const percentVal = data?.swapPercent ?? 0;
|
||||
@@ -223,4 +280,3 @@ const swapDisplay = computed(() => {
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,10 +18,28 @@ export interface StatusMonitorDependencies {
|
||||
*/
|
||||
export function createStatusMonitorManager(sessionId: string, wsDeps: StatusMonitorDependencies) {
|
||||
const { onMessage, isConnected } = wsDeps;
|
||||
const MAX_HISTORY_POINTS = 60; // 图表显示的点数
|
||||
|
||||
const serverStatus = ref<ServerStatus | null>(null);
|
||||
const statusError = ref<string | null>(null); // 存储状态获取错误
|
||||
|
||||
// --- 历史数据存储 ---
|
||||
// 初始化为包含60个 null 或 0 的数组,这样图表初始时有占位
|
||||
const cpuHistory = ref<(number | null)[]>(Array(MAX_HISTORY_POINTS).fill(null));
|
||||
const memUsedHistory = ref<(number | null)[]>(Array(MAX_HISTORY_POINTS).fill(null)); // Store memUsed in MB
|
||||
const netRxHistory = ref<(number | null)[]>(Array(MAX_HISTORY_POINTS).fill(null)); // Store rate in Bytes/sec
|
||||
const netTxHistory = ref<(number | null)[]>(Array(MAX_HISTORY_POINTS).fill(null)); // Store rate in Bytes/sec
|
||||
|
||||
// --- 辅助函数:更新历史数据数组 ---
|
||||
const updateHistory = (historyRef: Ref<(number | null)[]>, newValue: number | undefined) => {
|
||||
const currentHistory = historyRef.value;
|
||||
currentHistory.shift(); // 移除最旧的数据点
|
||||
// 如果新值无效(undefined 或 null),推入 null,否则推入数字
|
||||
currentHistory.push((newValue === undefined || newValue === null || isNaN(newValue)) ? null : newValue);
|
||||
historyRef.value = [...currentHistory]; // 触发响应式更新
|
||||
};
|
||||
|
||||
|
||||
// --- WebSocket 消息处理 ---
|
||||
const handleStatusUpdate = (payload: MessagePayload, message?: WebSocketMessage) => {
|
||||
// 检查消息是否属于此会话
|
||||
@@ -29,12 +47,20 @@ export function createStatusMonitorManager(sessionId: string, wsDeps: StatusMoni
|
||||
return; // 忽略不属于此会话的消息
|
||||
}
|
||||
|
||||
// console.debug(`[会话 ${sessionId}][状态监控模块] 收到 status_update:`, JSON.stringify(payload)); // 添加日志
|
||||
if (payload && payload.status) {
|
||||
serverStatus.value = payload.status;
|
||||
// console.debug(`[会话 ${sessionId}][状态监控模块] 收到 status_update:`, JSON.stringify(payload));
|
||||
if (payload?.status) {
|
||||
const newStatus: ServerStatus = payload.status;
|
||||
serverStatus.value = newStatus;
|
||||
statusError.value = null; // 收到有效状态时清除错误
|
||||
|
||||
// 更新历史数据
|
||||
updateHistory(cpuHistory, newStatus.cpuPercent);
|
||||
updateHistory(memUsedHistory, newStatus.memUsed);
|
||||
updateHistory(netRxHistory, newStatus.netRxRate);
|
||||
updateHistory(netTxHistory, newStatus.netTxRate);
|
||||
|
||||
} else {
|
||||
console.warn(`[会话 ${sessionId}][状态监控模块] 收到缺少 payload.status 的 status_update 消息`);
|
||||
console.warn(`[会话 ${sessionId}][状态监控模块] 收到无效的 status_update 消息`);
|
||||
// 可以选择设置一个错误状态,表明数据格式不正确
|
||||
// statusError.value = '收到的状态数据格式无效';
|
||||
}
|
||||
@@ -113,11 +139,17 @@ export function createStatusMonitorManager(sessionId: string, wsDeps: StatusMoni
|
||||
|
||||
// --- 暴露接口 ---
|
||||
return {
|
||||
serverStatus: readonly(serverStatus), // 只读状态
|
||||
statusError: readonly(statusError), // 只读错误状态
|
||||
registerStatusHandlers, // 暴露注册函数,以便在需要时可以重新注册
|
||||
unregisterAllStatusHandlers, // 暴露注销函数,以便在需要时可以手动注销
|
||||
cleanup, // 暴露清理函数,在会话关闭时调用
|
||||
serverStatus: readonly(serverStatus), // 当前状态
|
||||
statusError: readonly(statusError), // 错误状态
|
||||
// --- 暴露历史数据 ---
|
||||
cpuHistory: readonly(cpuHistory),
|
||||
memUsedHistory: readonly(memUsedHistory),
|
||||
netRxHistory: readonly(netRxHistory),
|
||||
netTxHistory: readonly(netTxHistory),
|
||||
// --- 控制函数 ---
|
||||
registerStatusHandlers,
|
||||
unregisterAllStatusHandlers,
|
||||
cleanup,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@ export interface ServerStatus {
|
||||
diskUsed?: number; // KB
|
||||
diskTotal?: number; // KB
|
||||
cpuModel?: string;
|
||||
// 可以根据后端实际发送的数据添加更多字段
|
||||
// 例如:swapPercent?, swapUsed?, swapTotal?, netRxRate?, netTxRate?, netInterface?, osName?, loadAvg?, timestamp?
|
||||
swapPercent?: number;
|
||||
swapUsed?: number; // MB
|
||||
swapTotal?: number; // MB
|
||||
netRxRate?: number; // Bytes/sec
|
||||
netTxRate?: number; // Bytes/sec
|
||||
netInterface?: string;
|
||||
osName?: string;
|
||||
}
|
||||
|
||||
// 可以根据需要添加其他与服务器或连接状态相关的类型
|
||||
|
||||
Reference in New Issue
Block a user