feat(frontend): 增加 CPU 核心详情弹窗
新增 CPU 概览卡片和“查看核心”入口, 支持按当前占用展示核心详情、平均值与最忙核心, 并补充中英日文案。
This commit is contained in:
@@ -61,6 +61,7 @@
|
||||
<div class="monitor-module__heading">
|
||||
<div>
|
||||
<span class="monitor-module__eyebrow">{{ t('statusMonitor.cpuLabel') }}</span>
|
||||
<h5 class="monitor-module__title">{{ t('statusMonitor.cpuUsageTitle') }}</h5>
|
||||
</div>
|
||||
<span class="monitor-module__pill">{{ displayCpuCores }}</span>
|
||||
</div>
|
||||
@@ -68,24 +69,24 @@
|
||||
<div class="module-split module-split--cpu">
|
||||
<StatusMonitorCpuHistoryChart :cpu-history="currentCpuHistory" />
|
||||
|
||||
<div class="cpu-core-panel">
|
||||
<div class="cpu-core-grid">
|
||||
<div class="cpu-summary-panel">
|
||||
<div class="cpu-summary-grid">
|
||||
<article
|
||||
v-for="item in cpuCoreItems"
|
||||
v-for="item in cpuSummaryItems"
|
||||
:key="item.key"
|
||||
class="usage-lane usage-lane--cpu"
|
||||
class="cpu-summary-card"
|
||||
>
|
||||
<div class="usage-lane__content">
|
||||
<div class="usage-lane__meta">
|
||||
<span class="usage-lane__label">{{ item.label }}</span>
|
||||
<span class="usage-lane__value-inline">{{ item.value }}</span>
|
||||
</div>
|
||||
<div class="usage-lane__track">
|
||||
<span class="usage-lane__fill" :style="{ width: `${item.percent}%` }"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="cpu-summary-card__label">{{ item.label }}</span>
|
||||
<span class="cpu-summary-card__value">{{ item.value }}</span>
|
||||
</article>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="monitor-action-button cpu-summary-action"
|
||||
@click="isCpuCoreModalVisible = true"
|
||||
>
|
||||
{{ t('statusMonitor.cpuViewAllCores') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -263,6 +264,12 @@
|
||||
:session-id="activeSessionId"
|
||||
@close="isProcessManagerVisible = false"
|
||||
/>
|
||||
<StatusMonitorCpuCoreModal
|
||||
:is-visible="isCpuCoreModalVisible"
|
||||
:cpu-core-items="cpuCoreItems"
|
||||
:total-cpu-percent="displayCpuPercent"
|
||||
@close="isCpuCoreModalVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -271,6 +278,7 @@ import { computed, ref, watch, type CSSProperties, type PropType } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import ProcessManagerModal from './ProcessManagerModal.vue';
|
||||
import StatusMonitorCpuCoreModal from './StatusMonitorCpuCoreModal.vue';
|
||||
import StatusMonitorCpuHistoryChart from './StatusMonitorCpuHistoryChart.vue';
|
||||
import StatusMonitorNetworkHistoryChart from './StatusMonitorNetworkHistoryChart.vue';
|
||||
import { useSessionStore } from '../stores/session.store';
|
||||
@@ -293,6 +301,7 @@ const connectionsStore = useConnectionsStore();
|
||||
const uiNotificationsStore = useUiNotificationsStore();
|
||||
const { sessions } = storeToRefs(sessionStore);
|
||||
const { statusMonitorShowIpBoolean } = storeToRefs(settingsStore);
|
||||
const isCpuCoreModalVisible = ref(false);
|
||||
const isProcessManagerVisible = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
@@ -316,6 +325,7 @@ const currentCpuHistory = computed<readonly (number | null)[]>(() => currentSess
|
||||
const currentNetRxHistory = computed<readonly (number | null)[]>(() => currentSessionState.value?.statusMonitorManager?.netRxHistory?.value ?? Array(24).fill(null));
|
||||
const currentNetTxHistory = computed<readonly (number | null)[]>(() => currentSessionState.value?.statusMonitorManager?.netTxHistory?.value ?? Array(24).fill(null));
|
||||
|
||||
const displayCpuPercent = computed(() => clampPercent(currentServerStatus.value?.cpuPercent));
|
||||
const displayMemoryPercent = computed(() => clampPercent(currentServerStatus.value?.memPercent));
|
||||
const displayDiskPercent = computed(() => clampPercent(currentServerStatus.value?.diskPercent));
|
||||
const currentStatusError = computed<string | null>(() => currentSessionState.value?.statusMonitorManager?.statusError?.value ?? null);
|
||||
@@ -530,6 +540,49 @@ const cpuCoreItems = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const cpuAveragePercent = computed(() => {
|
||||
if (cpuCoreItems.value.length === 0) {
|
||||
return displayCpuPercent.value;
|
||||
}
|
||||
|
||||
const total = cpuCoreItems.value.reduce((sum, item) => sum + item.percent, 0);
|
||||
return total / cpuCoreItems.value.length;
|
||||
});
|
||||
|
||||
const cpuBusiestCore = computed(() => {
|
||||
if (cpuCoreItems.value.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cpuCoreItems.value.reduce((highest, item) => (item.percent > highest.percent ? item : highest));
|
||||
});
|
||||
|
||||
const cpuSummaryItems = computed(() => {
|
||||
const items = [
|
||||
{
|
||||
key: 'current',
|
||||
label: t('statusMonitor.cpuCurrentStat'),
|
||||
value: `${displayCpuPercent.value.toFixed(1)}%`,
|
||||
},
|
||||
];
|
||||
|
||||
if (cpuBusiestCore.value) {
|
||||
items.push({
|
||||
key: 'busiest',
|
||||
label: t('statusMonitor.cpuBusiestCoreStat'),
|
||||
value: `${cpuBusiestCore.value.label} / ${cpuBusiestCore.value.value}`,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
key: 'average',
|
||||
label: t('statusMonitor.cpuAverageStat'),
|
||||
value: `${cpuAveragePercent.value.toFixed(1)}%`,
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
const networkFlowItems = computed(() => [
|
||||
{
|
||||
key: 'download',
|
||||
@@ -983,50 +1036,54 @@ const copyIpToClipboard = async (ipAddress: string | null) => {
|
||||
background: linear-gradient(90deg, #7dd3fc, #2563eb);
|
||||
}
|
||||
|
||||
.usage-lane--cpu {
|
||||
gap: 6px;
|
||||
border-radius: 10px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.usage-lane--cpu .usage-lane__content {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.usage-lane--cpu .usage-lane__label {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.usage-lane--cpu .usage-lane__value-inline {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.usage-lane--cpu .usage-lane__track {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.cpu-core-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(116px, 1fr));
|
||||
align-content: start;
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.cpu-core-panel {
|
||||
.cpu-summary-panel {
|
||||
min-height: 0;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.08);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.02)),
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.04), transparent 60%);
|
||||
padding: 8px 10px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cpu-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cpu-summary-card {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.08);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cpu-summary-card__label {
|
||||
color: #8ea0b1;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.cpu-summary-card__value {
|
||||
color: #f8fbff;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.cpu-summary-action {
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.module-split {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="cpu-core-modal__overlay"
|
||||
@click.self="emit('close')"
|
||||
>
|
||||
<section class="cpu-core-modal">
|
||||
<header class="cpu-core-modal__header">
|
||||
<div>
|
||||
<h4 class="cpu-core-modal__title">{{ t('statusMonitor.cpuCoreModalTitle') }}</h4>
|
||||
<p class="cpu-core-modal__subtitle">{{ t('statusMonitor.cpuCoreModalSubtitle') }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cpu-core-modal__close"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ t('common.close', '关闭') }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="cpu-core-modal__summary">
|
||||
<span class="cpu-core-modal__pill">{{ t('statusMonitor.cpuCoresValue', { count: sortedItems.length }) }}</span>
|
||||
<span class="cpu-core-modal__pill">{{ t('statusMonitor.cpuCurrentStat') }} {{ `${totalCpuPercent.toFixed(1)}%` }}</span>
|
||||
<span class="cpu-core-modal__pill">{{ t('statusMonitor.cpuAverageStat') }} {{ averagePercentDisplay }}</span>
|
||||
<span v-if="busiestCore" class="cpu-core-modal__pill">
|
||||
{{ t('statusMonitor.cpuBusiestCoreStat') }} {{ busiestCore.label }} / {{ busiestCore.value }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="sortedItems.length > 0" class="cpu-core-modal__grid">
|
||||
<article
|
||||
v-for="item in sortedItems"
|
||||
:key="item.key"
|
||||
class="cpu-core-card"
|
||||
>
|
||||
<div class="cpu-core-card__meta">
|
||||
<span class="cpu-core-card__label">{{ item.label }}</span>
|
||||
<span class="cpu-core-card__value">{{ item.value }}</span>
|
||||
</div>
|
||||
<div class="cpu-core-card__track">
|
||||
<span class="cpu-core-card__fill" :style="{ width: `${item.percent}%` }"></span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="cpu-core-modal__empty">
|
||||
{{ t('statusMonitor.cpuCoreModalEmpty', '暂无核心数据') }}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
interface CpuCoreDisplayItem {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
isVisible: boolean;
|
||||
cpuCoreItems: CpuCoreDisplayItem[];
|
||||
totalCpuPercent: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const sortedItems = computed(() => [...props.cpuCoreItems].sort((left, right) => right.percent - left.percent));
|
||||
|
||||
const averagePercentDisplay = computed(() => {
|
||||
if (sortedItems.value.length === 0) {
|
||||
return `${props.totalCpuPercent.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
const total = sortedItems.value.reduce((sum, item) => sum + item.percent, 0);
|
||||
return `${(total / sortedItems.value.length).toFixed(1)}%`;
|
||||
});
|
||||
|
||||
const busiestCore = computed(() => sortedItems.value[0] ?? null);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cpu-core-modal__overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 70;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(2, 6, 12, 0.72);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cpu-core-modal {
|
||||
width: min(840px, calc(100vw - 40px));
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
border-radius: 22px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(17, 24, 32, 0.98), rgba(11, 16, 22, 0.98)),
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 56%);
|
||||
padding: 20px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 28px 80px rgba(0, 0, 0, 0.42);
|
||||
}
|
||||
|
||||
.cpu-core-modal__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cpu-core-modal__title {
|
||||
margin: 0;
|
||||
color: #f8fbff;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.cpu-core-modal__subtitle {
|
||||
margin: 8px 0 0;
|
||||
color: #8ea0b1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cpu-core-modal__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 34px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
background: rgba(30, 41, 59, 0.58);
|
||||
padding: 0 12px;
|
||||
color: #dce6f3;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cpu-core-modal__summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cpu-core-modal__pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(96, 165, 250, 0.22);
|
||||
background: rgba(30, 64, 175, 0.18);
|
||||
padding: 0 12px;
|
||||
color: #dbeafe;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cpu-core-modal__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.cpu-core-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.08);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.cpu-core-card__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cpu-core-card__label {
|
||||
color: #d9e5f1;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cpu-core-card__value {
|
||||
color: #f8fbff;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.cpu-core-card__track {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(51, 65, 85, 0.6);
|
||||
}
|
||||
|
||||
.cpu-core-card__fill {
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #7dd3fc, #2563eb);
|
||||
}
|
||||
|
||||
.cpu-core-modal__empty {
|
||||
margin-top: 18px;
|
||||
border-radius: 14px;
|
||||
border: 1px dashed rgba(148, 163, 184, 0.16);
|
||||
padding: 16px;
|
||||
color: #8ea0b1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.cpu-core-modal {
|
||||
width: calc(100vw - 24px);
|
||||
max-height: calc(100vh - 24px);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.cpu-core-modal__header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.cpu-core-modal__close {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -692,6 +692,13 @@
|
||||
"cpuHistoryRecentPoints": "Latest {count} samples",
|
||||
"cpuUsageLabel": "CPU Usage (%)",
|
||||
"cpuCoreLabel": "Core {index}",
|
||||
"cpuCurrentStat": "Current",
|
||||
"cpuAverageStat": "Average",
|
||||
"cpuBusiestCoreStat": "Busiest Core",
|
||||
"cpuViewAllCores": "View Cores",
|
||||
"cpuCoreModalTitle": "CPU Core Details",
|
||||
"cpuCoreModalSubtitle": "Sorted by current usage",
|
||||
"cpuCoreModalEmpty": "No core data available",
|
||||
"memoryUsageLabelUnit": "Memory Usage ({unit})",
|
||||
"networkDownloadLabelUnit": "Download ({unit})",
|
||||
"networkUploadLabelUnit": "Upload ({unit})",
|
||||
|
||||
@@ -1427,6 +1427,13 @@
|
||||
"cpuHistoryRecentPoints": "直近 {count} 件のサンプル",
|
||||
"cpuUsageLabel": "CPU使用率 (%)",
|
||||
"cpuCoreLabel": "コア {index}",
|
||||
"cpuCurrentStat": "現在",
|
||||
"cpuAverageStat": "平均",
|
||||
"cpuBusiestCoreStat": "最大負荷コア",
|
||||
"cpuViewAllCores": "コアを見る",
|
||||
"cpuCoreModalTitle": "CPUコア詳細",
|
||||
"cpuCoreModalSubtitle": "現在使用率順",
|
||||
"cpuCoreModalEmpty": "コアデータがありません",
|
||||
"memoryUsageLabelUnit": "メモリ使用量 ({unit})",
|
||||
"networkDownloadLabelUnit": "ダウンロード ({unit})",
|
||||
"networkUploadLabelUnit": "アップロード ({unit})",
|
||||
|
||||
@@ -692,6 +692,13 @@
|
||||
"cpuHistoryRecentPoints": "最近 {count} 个采样点",
|
||||
"cpuUsageLabel": "CPU 使用率 (%)",
|
||||
"cpuCoreLabel": "核心 {index}",
|
||||
"cpuCurrentStat": "当前",
|
||||
"cpuAverageStat": "平均",
|
||||
"cpuBusiestCoreStat": "最忙核心",
|
||||
"cpuViewAllCores": "查看核心",
|
||||
"cpuCoreModalTitle": "CPU 核心详情",
|
||||
"cpuCoreModalSubtitle": "按当前占用排序",
|
||||
"cpuCoreModalEmpty": "暂无核心数据",
|
||||
"memoryUsageLabelUnit": "内存使用 ({unit})",
|
||||
"networkDownloadLabelUnit": "下载 ({unit})",
|
||||
"networkUploadLabelUnit": "上传 ({unit})",
|
||||
|
||||
Reference in New Issue
Block a user