feat: 添加状态监视器的图表显示
This commit is contained in:
Generated
+31
-1
@@ -741,6 +741,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@kurkle/color": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@levischuck/tiny-cbor": {
|
"node_modules/@levischuck/tiny-cbor": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
|
||||||
@@ -3117,6 +3123,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "4.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
|
||||||
|
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@kurkle/color": "^0.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"pnpm": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -8658,6 +8676,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chartjs": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"chart.js": "^4.1.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "9.14.4",
|
"version": "9.14.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.4.tgz",
|
||||||
@@ -9091,7 +9119,7 @@
|
|||||||
},
|
},
|
||||||
"packages/frontend": {
|
"packages/frontend": {
|
||||||
"name": "@nexus-terminal/frontend",
|
"name": "@nexus-terminal/frontend",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||||
@@ -9103,6 +9131,7 @@
|
|||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"guacamole-common-js": "^1.5.0",
|
"guacamole-common-js": "^1.5.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
@@ -9113,6 +9142,7 @@
|
|||||||
"splitpanes": "^4.0.3",
|
"splitpanes": "^4.0.3",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-i18n": "^9.14.4",
|
"vue-i18n": "^9.14.4",
|
||||||
"vue-recaptcha-v3": "^2.0.1",
|
"vue-recaptcha-v3": "^2.0.1",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@xterm/addon-search": "^0.15.0",
|
"@xterm/addon-search": "^0.15.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"guacamole-common-js": "^1.5.0",
|
"guacamole-common-js": "^1.5.0",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"splitpanes": "^4.0.3",
|
"splitpanes": "^4.0.3",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
"vite-plugin-monaco-editor": "^1.1.0",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-i18n": "^9.14.4",
|
"vue-i18n": "^9.14.4",
|
||||||
"vue-recaptcha-v3": "^2.0.1",
|
"vue-recaptcha-v3": "^2.0.1",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<template>
|
||||||
|
<div class="status-charts grid grid-cols-1 gap-4 mt-4">
|
||||||
|
<div class="chart-container bg-header rounded p-3">
|
||||||
|
<h5 class="text-sm font-medium mb-2 text-text-secondary">CPU 使用率</h5>
|
||||||
|
<div class="chart-wrapper h-40">
|
||||||
|
<Line :data="cpuChartData" :options="percentageChartOptions" :key="cpuChartKey" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container bg-header rounded p-3">
|
||||||
|
<h5 class="text-sm font-medium mb-2 text-text-secondary">内存使用率</h5>
|
||||||
|
<div class="chart-wrapper h-40">
|
||||||
|
<Line :data="memoryChartData" :options="percentageChartOptions" :key="memoryChartKey" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-container bg-header rounded p-3">
|
||||||
|
<h5 class="text-sm font-medium mb-2 text-text-secondary">{{ networkChartTitle }}</h5>
|
||||||
|
<div class="chart-wrapper h-40">
|
||||||
|
<Line :data="networkChartData" :options="networkChartOptions" :key="networkChartKey" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
import { Line } from 'vue-chartjs';
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
LineElement,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
CategoryScale,
|
||||||
|
ChartOptions,
|
||||||
|
TooltipItem,
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
LineElement,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
CategoryScale
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
serverStatus: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const MAX_DATA_POINTS = 60;
|
||||||
|
const KB_TO_MB_THRESHOLD = 1024;
|
||||||
|
|
||||||
|
const cpuChartKey = ref(0);
|
||||||
|
const memoryChartKey = ref(0);
|
||||||
|
const networkChartKey = ref(0);
|
||||||
|
const networkRateUnitIsMB = ref(false);
|
||||||
|
const networkChartTitle = ref('网络速度 (KB/s)');
|
||||||
|
|
||||||
|
const initialLabels = Array.from({ length: MAX_DATA_POINTS }, (_, i) => `-${(MAX_DATA_POINTS - 1 - i)}s`);
|
||||||
|
|
||||||
|
const cpuChartData = ref({
|
||||||
|
labels: [...initialLabels],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'CPU 使用率 (%)',
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
data: Array(MAX_DATA_POINTS).fill(0),
|
||||||
|
tension: 0.1,
|
||||||
|
pointRadius: 0,
|
||||||
|
pointHoverRadius: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const memoryChartData = ref({
|
||||||
|
labels: [...initialLabels],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: '内存使用率 (%)',
|
||||||
|
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: '下载 (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: '上传 (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 baseChartOptions: Omit<ChartOptions<'line'>, 'scales'> = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { labels: { color: '#9CA3AF' } },
|
||||||
|
tooltip: { enabled: true, mode: 'index', intersect: false },
|
||||||
|
},
|
||||||
|
interaction: { mode: 'index', intersect: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const percentageChartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
...baseChartOptions,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
ticks: { color: '#9CA3AF' },
|
||||||
|
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: false, color: '#9CA3AF', maxRotation: 0, minRotation: 0 },
|
||||||
|
grid: { display: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const networkChartOptions = ref<ChartOptions<'line'>>({
|
||||||
|
...baseChartOptions,
|
||||||
|
plugins: {
|
||||||
|
...baseChartOptions.plugins,
|
||||||
|
tooltip: {
|
||||||
|
...baseChartOptions.plugins?.tooltip,
|
||||||
|
callbacks: {
|
||||||
|
label: (context: TooltipItem<'line'>) => {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
if (label) {
|
||||||
|
label += ': ';
|
||||||
|
}
|
||||||
|
if (context.parsed.y !== null) {
|
||||||
|
const value = parseFloat(context.parsed.y.toFixed(1));
|
||||||
|
label += `${value} ${networkRateUnitIsMB.value ? 'MB/s' : 'KB/s'}`;
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
min: 0,
|
||||||
|
// max will be set dynamically
|
||||||
|
ticks: {
|
||||||
|
color: '#9CA3AF',
|
||||||
|
callback: function(value) {
|
||||||
|
return `${parseFloat(Number(value).toFixed(1))}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { color: 'rgba(156, 163, 175, 0.1)' },
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: { display: false, color: '#9CA3AF', maxRotation: 0, minRotation: 0 },
|
||||||
|
grid: { display: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const updateCharts = (newStatus: any) => {
|
||||||
|
if (!newStatus) return;
|
||||||
|
|
||||||
|
// 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 }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Memory Chart
|
||||||
|
if (typeof newStatus.memPercent === 'number') {
|
||||||
|
const newMemData = [...memoryChartData.value.datasets[0].data];
|
||||||
|
newMemData.shift();
|
||||||
|
newMemData.push(parseFloat(newStatus.memPercent.toFixed(1)));
|
||||||
|
memoryChartData.value = { ...memoryChartData.value, datasets: [{ ...memoryChartData.value.datasets[0], data: newMemData }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Network Chart
|
||||||
|
let currentNetRxRateKB = (newStatus.netRxRate || 0) / 1024;
|
||||||
|
let currentNetTxRateKB = (newStatus.netTxRate || 0) / 1024;
|
||||||
|
|
||||||
|
const newNetRxData = [...networkChartData.value.datasets[0].data];
|
||||||
|
const newNetTxData = [...networkChartData.value.datasets[1].data];
|
||||||
|
|
||||||
|
// Check if unit needs to be switched to MB/s
|
||||||
|
if (!networkRateUnitIsMB.value && (currentNetRxRateKB >= KB_TO_MB_THRESHOLD || currentNetTxRateKB >= KB_TO_MB_THRESHOLD)) {
|
||||||
|
networkRateUnitIsMB.value = true;
|
||||||
|
networkChartTitle.value = '网络速度 (MB/s)';
|
||||||
|
|
||||||
|
// Convert existing data to MB/s
|
||||||
|
networkChartData.value.datasets[0].data = newNetRxData.map(d => parseFloat((d / KB_TO_MB_THRESHOLD).toFixed(1)));
|
||||||
|
networkChartData.value.datasets[1].data = newNetTxData.map(d => parseFloat((d / KB_TO_MB_THRESHOLD).toFixed(1)));
|
||||||
|
|
||||||
|
// Update dataset labels
|
||||||
|
networkChartData.value.datasets[0].label = '下载 (MB/s)';
|
||||||
|
networkChartData.value.datasets[1].label = '上传 (MB/s)';
|
||||||
|
|
||||||
|
// Force chart re-render for label and unit changes
|
||||||
|
networkChartKey.value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare new data points based on current unit
|
||||||
|
let newRxValue, newTxValue;
|
||||||
|
if (networkRateUnitIsMB.value) {
|
||||||
|
newRxValue = parseFloat((currentNetRxRateKB / KB_TO_MB_THRESHOLD).toFixed(1));
|
||||||
|
newTxValue = parseFloat((currentNetTxRateKB / KB_TO_MB_THRESHOLD).toFixed(1));
|
||||||
|
} else {
|
||||||
|
newRxValue = parseFloat(currentNetRxRateKB.toFixed(1));
|
||||||
|
newTxValue = parseFloat(currentNetTxRateKB.toFixed(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalNewNetRxData = [...networkChartData.value.datasets[0].data];
|
||||||
|
finalNewNetRxData.shift();
|
||||||
|
finalNewNetRxData.push(newRxValue);
|
||||||
|
|
||||||
|
const finalNewNetTxData = [...networkChartData.value.datasets[1].data];
|
||||||
|
finalNewNetTxData.shift();
|
||||||
|
finalNewNetTxData.push(newTxValue);
|
||||||
|
|
||||||
|
networkChartData.value = {
|
||||||
|
...networkChartData.value,
|
||||||
|
datasets: [
|
||||||
|
{ ...networkChartData.value.datasets[0], data: finalNewNetRxData },
|
||||||
|
{ ...networkChartData.value.datasets[1], data: finalNewNetTxData },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamically adjust Y-axis for network chart
|
||||||
|
const allNetworkData = [...finalNewNetRxData, ...finalNewNetTxData];
|
||||||
|
const minMax = networkRateUnitIsMB.value ? 1 : 10; // Min max for MB/s or KB/s
|
||||||
|
const maxNetworkRate = Math.max(...allNetworkData, minMax);
|
||||||
|
|
||||||
|
if (networkChartOptions.value.scales?.y) {
|
||||||
|
const roundingFactor = networkRateUnitIsMB.value ? 1 : 10; // Round to next 1 or 10
|
||||||
|
const buffer = networkRateUnitIsMB.value ? 1 : 10; // Buffer for MB/s or KB/s
|
||||||
|
networkChartOptions.value.scales.y.max = Math.ceil(maxNetworkRate / roundingFactor) * roundingFactor + buffer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => props.serverStatus, (newStatus) => {
|
||||||
|
updateCharts(newStatus);
|
||||||
|
}, { deep: true, immediate: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Initial setup handled by watch immediate
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -104,12 +104,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 图表组件 -->
|
||||||
|
<StatusCharts :server-status="serverStatus" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import StatusCharts from './StatusCharts.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user