This commit is contained in:
Baobhan Sith
2025-04-23 19:47:51 +08:00
parent 714f173a46
commit fb7a5374ae
3 changed files with 103 additions and 261 deletions
+101 -261
View File
@@ -1,87 +1,109 @@
<template>
<div class="status-monitor">
<h4>{{ t('statusMonitor.title') }}</h4>
<!-- Corrected state display logic -->
<div v-if="statusError" class="status-error">
{{ t('statusMonitor.errorPrefix') }} {{ statusError }}
<!-- Root element with padding, background, border, and text styles -->
<div class="status-monitor p-4 border-l border-border bg-background text-foreground h-full overflow-y-auto text-sm">
<!-- Title with margin, border, padding, font size, and color -->
<h4 class="mt-0 mb-4 border-b border-border pb-2 text-base font-medium">
{{ t('statusMonitor.title') }}
</h4>
<!-- Error State -->
<div v-if="statusError" 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>
</div>
<div v-else-if="!serverStatus" class="loading-status">
<i class="fas fa-spinner fa-spin"></i>
{{ t('statusMonitor.loading') }}
<!-- Loading State -->
<div v-else-if="!serverStatus" 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>
<div v-else class="status-grid">
<!-- Status items remain here -->
<div class="status-item cpu-model">
<label>{{ t('statusMonitor.cpuModelLabel') }}</label>
<!-- 使用 displayCpuModel 计算属性 -->
<span class="cpu-model-value" :title="displayCpuModel">{{ displayCpuModel }}</span>
<!-- Status Grid -->
<div v-else class="status-grid grid gap-3">
<!-- CPU Model -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.cpuModelLabel') }}</label>
<span class="cpu-model-value truncate text-left" :title="displayCpuModel">{{ displayCpuModel }}</span>
</div>
<!-- Added OS Name Display -->
<div class="status-item os-name">
<label>{{ t('statusMonitor.osLabel') }}</label>
<!-- 使用 displayOsName 计算属性 -->
<span class="os-name-value" :title="displayOsName">{{ displayOsName }}</span>
<!-- OS Name -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.osLabel') }}</label>
<span class="os-name-value truncate text-left" :title="displayOsName">{{ displayOsName }}</span>
</div>
<div class="status-item">
<label>{{ t('statusMonitor.cpuLabel') }}</label>
<!-- Wrap progress bar and percentage in a div -->
<div class="value-wrapper">
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: `${serverStatus.cpuPercent ?? 0}%` }"></div>
<!-- CPU Usage -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<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"> <!-- Reduced height -->
<div class="progress-bar bg-blue-500 h-full transition-width duration-300 ease-in-out" :style="{ width: `${serverStatus.cpuPercent ?? 0}%` }"></div>
</div>
<span>{{ serverStatus.cpuPercent?.toFixed(1) ?? t('statusMonitor.notAvailable') }}%</span>
<span class="font-mono text-left text-xs w-12 text-right">{{ serverStatus.cpuPercent?.toFixed(1) ?? t('statusMonitor.notAvailable') }}%</span> <!-- Fixed width and right align -->
</div>
</div>
<div class="status-item">
<label>{{ t('statusMonitor.memoryLabel') }}</label>
<div class="value-wrapper">
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: `${serverStatus.memPercent ?? 0}%` }"></div>
<!-- Memory Usage -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<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>
<span class="mem-disk-details">{{ memDisplay }}</span>
<span class="mem-disk-details font-mono text-xs whitespace-nowrap text-left">{{ memDisplay }}</span>
</div>
</div>
<!-- Removed v-if, Swap will always show -->
<div class="status-item">
<label>{{ t('statusMonitor.swapLabel') }}</label>
<div class="value-wrapper">
<div class="progress-bar-container">
<div class="progress-bar swap-bar" :style="{ width: `${serverStatus.swapPercent ?? 0}%` }"></div>
<!-- Swap Usage -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<label class="font-semibold text-text-secondary text-left whitespace-nowrap">{{ t('statusMonitor.swapLabel') }}</label>
<div class="value-wrapper flex items-center gap-2">
<div class="progress-bar-container bg-header rounded h-3 overflow-hidden flex-grow">
<!-- Conditional color for 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>
<span class="mem-disk-details">{{ swapDisplay }}</span>
<span class="mem-disk-details font-mono text-xs whitespace-nowrap text-left">{{ swapDisplay }}</span>
</div>
</div>
<div class="status-item">
<label>{{ t('statusMonitor.diskLabel') }}</label> <!-- 移除 (/) -->
<div class="value-wrapper">
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: `${serverStatus.diskPercent ?? 0}%` }"></div>
<!-- Disk Usage -->
<div class="status-item grid grid-cols-[auto_1fr] items-center gap-3">
<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>
<span class="mem-disk-details">{{ diskDisplay }}</span>
<span class="mem-disk-details font-mono text-xs whitespace-nowrap text-left">{{ diskDisplay }}</span>
</div>
</div>
<div class="status-item network-rate">
<label>{{ t('statusMonitor.networkLabel') }} ({{ serverStatus.netInterface || '...' }}):</label>
<!-- Wrap rates in a div for alignment -->
<div class="value-wrapper network-values">
<span class="rate down">{{ formatBytesPerSecond(serverStatus.netRxRate) }}</span>
<span class="rate up">{{ formatBytesPerSecond(serverStatus.netTxRate) }}</span>
<!-- Network Rate -->
<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>
<div class="network-values flex items-center justify-start gap-4"> <!-- Reduced gap -->
<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 icon -->
<span class="font-mono">{{ formatBytesPerSecond(serverStatus.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 icon -->
<span class="font-mono">{{ formatBytesPerSecond(serverStatus.netTxRate) }}</span>
</span>
</div>
</div>
</div>
<!-- Error display moved up for correct v-if/v-else-if logic -->
</div>
<!-- Removed extra closing div -->
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'; // 引入 watch
import { useI18n } from 'vue-i18n'; // 引入 useI18n
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
// 获取 t 函数
const { t } = useI18n();
// 接口定义,与后端 ServerStatusDetails 匹配
// Interface remains the same
interface ServerStatus {
cpuPercent?: number;
memPercent?: number;
@@ -97,24 +119,21 @@ interface ServerStatus {
netRxRate?: number; // Bytes per second
netTxRate?: number; // Bytes per second
netInterface?: string;
osName?: string; // 操作系统名称
osName?: string;
}
// 更新 Props 定义
const props = defineProps<{
sessionId: string; // 添加会话 ID
serverStatus: ServerStatus | null; // 更改名称从 statusData 到 serverStatus
statusError?: string | null; // 更改名称从 error 到 statusError
sessionId: string;
serverStatus: ServerStatus | null;
statusError?: string | null;
}>();
// --- 缓存状态 ---
// --- Caching logic remains the same ---
const cachedCpuModel = ref<string | null>(null);
const cachedOsName = ref<string | null>(null);
// 监听传入的 serverStatus 变化以更新缓存 (更新引用)
watch(() => props.serverStatus, (newData) => {
if (newData) {
// 仅当新数据有效时更新缓存
if (newData.cpuModel !== undefined && newData.cpuModel !== null && newData.cpuModel !== '') {
cachedCpuModel.value = newData.cpuModel;
}
@@ -122,22 +141,17 @@ watch(() => props.serverStatus, (newData) => {
cachedOsName.value = newData.osName;
}
}
// 如果 newData 为 null (例如断开连接),不清除缓存
}, { immediate: true }); // 立即执行一次以初始化缓存
}, { immediate: true });
// --- 显示计算属性 (包含缓存逻辑) - 更新引用 ---
// --- Computed properties remain the same ---
const displayCpuModel = computed(() => {
// 优先使用当前有效数据,否则回退到缓存,最后是 'N/A'
return (props.serverStatus?.cpuModel ?? cachedCpuModel.value) || t('statusMonitor.notAvailable');
});
const displayOsName = computed(() => {
// 优先使用当前有效数据,否则回退到缓存,最后是 'N/A'
return (props.serverStatus?.osName ?? cachedOsName.value) || t('statusMonitor.notAvailable');
});
// 辅助函数:格式化字节/秒为合适的单位 (B, KB, MB, GB)
const formatBytesPerSecond = (bytes?: number): string => {
if (bytes === undefined || bytes === null || isNaN(bytes)) return t('statusMonitor.notAvailable');
if (bytes < 1024) return `${bytes} ${t('statusMonitor.bytesPerSecond')}`;
@@ -146,220 +160,46 @@ const formatBytesPerSecond = (bytes?: number): string => {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} ${t('statusMonitor.gigaBytesPerSecond')}`;
};
// 辅助函数:格式化 KB 为 GB
const formatKbToGb = (kb?: number): string => {
if (kb === undefined || kb === null) return t('statusMonitor.notAvailable'); // 处理无效输入
if (kb === 0) return `0.0 ${t('statusMonitor.gigaBytes')}`; // 处理 0 的情况
if (kb === undefined || kb === null) return t('statusMonitor.notAvailable');
if (kb === 0) return `0.0 ${t('statusMonitor.gigaBytes')}`;
const gb = kb / 1024 / 1024;
return `${gb.toFixed(1)} ${t('statusMonitor.gigaBytes')}`;
};
// 计算属性用于显示内存信息 (更新引用)
const memDisplay = computed(() => {
const data = props.serverStatus;
if (!data || data.memUsed === undefined || data.memTotal === undefined) return t('statusMonitor.notAvailable'); // 检查数据有效性
const percent = data.memPercent !== undefined ? `(${data.memPercent.toFixed(1)}%)` : '';
// 确保 MB 值在是整数时不显示小数
if (!data || data.memUsed === undefined || data.memTotal === undefined) return t('statusMonitor.notAvailable');
const percent = data.memPercent !== undefined ? `(${(data.memPercent).toFixed(1)}%)` : ''; // Keep 1 decimal for percent
const usedMb = Number.isInteger(data.memUsed) ? data.memUsed : data.memUsed.toFixed(1);
const totalMb = Number.isInteger(data.memTotal) ? data.memTotal : data.memTotal.toFixed(1);
return `${usedMb} ${t('statusMonitor.megaBytes')} / ${totalMb} ${t('statusMonitor.megaBytes')} ${percent}`;
return `${usedMb} / ${totalMb} ${t('statusMonitor.megaBytes')} ${percent}`; // Removed extra space before MB
});
// 计算属性用于显示磁盘信息 (更新引用)
const diskDisplay = computed(() => {
const data = props.serverStatus;
if (!data || data.diskUsed === undefined || data.diskTotal === undefined) return t('statusMonitor.notAvailable'); // 检查数据有效性
// 百分比代表已用空间
const percent = data.diskPercent !== undefined ? `(${data.diskPercent.toFixed(1)}%)` : '';
// 显示 已用 / 总量
if (!data || data.diskUsed === undefined || data.diskTotal === undefined) return t('statusMonitor.notAvailable');
const percent = data.diskPercent !== undefined ? `(${(data.diskPercent).toFixed(1)}%)` : ''; // Keep 1 decimal for percent
return `${formatKbToGb(data.diskUsed)} / ${formatKbToGb(data.diskTotal)} ${percent}`;
});
// 计算属性用于显示 Swap 信息 (更新引用)
const swapDisplay = computed(() => {
const data = props.serverStatus;
// 处理 swap 可能为 undefined 或 0 的情况
const used = data?.swapUsed ?? 0;
const total = data?.swapTotal ?? 0;
const percentVal = data?.swapPercent ?? 0;
const percent = `(${percentVal.toFixed(1)}%)`;
// Only show details if swap total > 0
if (total === 0) {
return t('statusMonitor.swapNotAvailable'); // Or a more specific message
}
const percent = `(${(percentVal).toFixed(1)}%)`; // Keep 1 decimal for percent
const usedMb = Number.isInteger(used) ? used : used.toFixed(1);
const totalMb = Number.isInteger(total) ? total : total.toFixed(1);
return `${usedMb} ${t('statusMonitor.megaBytes')} / ${totalMb} ${t('statusMonitor.megaBytes')} ${percent}`;
return `${usedMb} / ${totalMb} ${t('statusMonitor.megaBytes')} ${percent}`; // Removed extra space before MB
});
</script>
<style scoped>
.status-monitor {
padding: var(--base-padding); /* Use theme variable */
border-left: 1px solid var(--border-color); /* Use theme variable */
background-color: var(--app-bg-color); /* Use theme variable */
height: 100%;
overflow-y: auto;
font-size: 0.9em;
}
.status-monitor h4 {
margin-top: 0;
margin-bottom: var(--base-padding); /* Use theme variable */
border-bottom: 1px solid var(--border-color); /* Use theme variable */
padding-bottom: var(--base-margin); /* Use theme variable */
font-size: 1em;
color: var(--text-color); /* Use theme variable */
}
.loading-status, .status-error {
color: var(--text-color-secondary); /* Use theme variable */
text-align: center;
margin-top: var(--base-padding); /* Use theme variable */
}
.status-error {
color: #dc3545;
}
.status-grid {
display: grid;
gap: 0.8rem;
}
.status-item {
display: grid;
/* Simplified grid columns: Label | Value Area - Further increased label width */
grid-template-columns: 75px 1fr;
align-items: center;
gap: 0.8rem; /* Keep increased gap */
}
/* New wrapper for value area (progress bar + text or just text) */
.value-wrapper {
display: flex;
align-items: center;
gap: 0.5rem; /* Space between progress bar and text */
}
/* Specific style for CPU model row - Keep consistent with general status-item */
.status-item.cpu-model {
/* grid-template-columns is inherited */
/* gap is inherited */
margin-bottom: 0.5rem; /* Add some space below CPU model */
}
.cpu-model-value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* No longer needs grid-column span */
text-align: left;
color: var(--text-color); /* Use theme variable */
}
/* Specific style for OS name row - Keep consistent with general status-item */
.status-item.os-name {
/* grid-template-columns is inherited */
/* Ensure the item itself doesn't align right if the parent has text-align */
text-align: left;
}
/* Increased specificity to override generic span rule */
/* OS name value should just occupy the second column */
.status-item.os-name .os-name-value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left; /* Explicitly left align text */
/* justify-self: start; No longer needed with 2-col grid */
color: var(--text-color); /* Use theme variable */
min-width: auto; /* Override generic min-width */
}
.status-item label {
font-weight: bold;
color: var(--text-color-secondary); /* Use theme variable */
text-align: left; /* 改为左对齐 */
white-space: nowrap;
}
.progress-bar-container {
background-color: var(--header-bg-color); /* Use theme variable */
border-radius: 0.25rem;
height: 1rem; /* Adjust height */
overflow: hidden;
flex-grow: 1;
}
.progress-bar {
background-color: var(--button-bg-color); /* Use theme variable for default bar */
height: 100%;
transition: width 0.3s ease-in-out;
text-align: center;
color: white;
font-size: 0.75em;
line-height: 1rem; /* Match container height */
}
.progress-bar.swap-bar {
background-color: #ffc107; /* Yellow for Swap */
}
.status-item span:not(.cpu-model-value) { /* Style for percentage spans */
font-variant-numeric: tabular-nums; /* Keep numbers aligned */
min-width: 45px; /* Ensure space for percentage */
text-align: left; /* 改为左对齐 */
color: var(--text-color); /* Use theme variable */
}
.mem-disk-details {
font-size: 0.9em; /* Slightly smaller font for details */
white-space: nowrap;
text-align: left; /* 改为左对齐 */
}
/* Network Rate Styles */
/* Network Rate Styles - uses the 2-col grid */
.status-item.network-rate {
/* grid-template-columns is inherited */
margin-top: 0.5rem; /* Add space above network */
align-items: center; /* Try centering label and rates vertically */
}
/* Adjust network value wrapper */
.network-values {
justify-content: start; /* Align rates to the start */
gap: 1rem; /* Increase gap between rates */
/* Removed margin-left, rely on grid gap */
/* Ensure the wrapper itself aligns correctly if needed */
/* align-self: center; */ /* Or baseline */
}
.network-rate .rate {
font-size: 0.9em;
white-space: nowrap;
text-align: left; /* 改为左对齐 */
min-width: auto; /* Remove min-width or adjust */
/* Rely on parent flexbox for alignment */
display: inline-flex; /* Ensure pseudo-element is part of flex flow */
align-items: center; /* Vertically align arrow with text */
gap: 0.3em; /* Add space between arrow and text */
}
.network-rate .rate.down {
color: #28a745; /* Green for download */
}
.network-rate .rate.down::before {
content: '⬇';
/* Removed absolute positioning */
font-size: 1em; /* Match parent font size */
line-height: 1; /* Adjust line-height for better vertical alignment */
}
.network-rate .rate.up {
color: #fd7e14; /* Orange for upload */
}
.network-rate .rate.up::before {
content: '⬆';
/* Removed absolute positioning */
font-size: 1em; /* Match parent font size */
line-height: 1; /* Adjust line-height for better vertical alignment */
}
</style>
<!-- No <style scoped> needed anymore -->