d300566f89
默认视图移除 CPU 使用率卡和网络速度卡,保留内存、 磁盘与进程管理作为常驻概览,以减少右侧窄栏中的重复 信息。 同时压缩 CPU/网络历史图的高度,避免在窄侧栏下撑开 布局。
353 lines
8.8 KiB
Vue
353 lines
8.8 KiB
Vue
<template>
|
|
<div ref="chartHostRef" class="network-history-chart">
|
|
<div class="network-history-chart__header">
|
|
<div>
|
|
<p class="network-history-chart__subtitle">{{ t('statusMonitor.networkHistoryRecentPoints', { count: displayPointCount }) }}</p>
|
|
<h6 class="network-history-chart__title">
|
|
{{ t('statusMonitor.networkSpeedTitleUnit', { unit: networkRateUnitIsMB ? 'MB/s' : 'KB/s' }) }}
|
|
</h6>
|
|
</div>
|
|
|
|
<div class="network-history-chart__legend">
|
|
<span class="network-history-chart__legend-item">
|
|
<span class="network-history-chart__legend-dot network-history-chart__legend-dot--download"></span>
|
|
{{ t('statusMonitor.downloadLabel') }}
|
|
</span>
|
|
<span class="network-history-chart__legend-item">
|
|
<span class="network-history-chart__legend-dot network-history-chart__legend-dot--upload"></span>
|
|
{{ t('statusMonitor.uploadLabel') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="network-history-chart__canvas">
|
|
<Line :data="networkChartData" :options="networkChartOptions" :key="chartKey" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type PropType } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { Line } from 'vue-chartjs';
|
|
import {
|
|
CategoryScale,
|
|
Chart as ChartJS,
|
|
type ChartOptions,
|
|
Legend,
|
|
LineElement,
|
|
LinearScale,
|
|
PointElement,
|
|
Title,
|
|
Tooltip,
|
|
type TooltipItem,
|
|
} from 'chart.js';
|
|
|
|
ChartJS.register(
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
LineElement,
|
|
LinearScale,
|
|
PointElement,
|
|
CategoryScale,
|
|
);
|
|
|
|
const DISPLAY_POINTS = 24;
|
|
const KB_TO_MB_THRESHOLD = 1024;
|
|
|
|
const props = defineProps({
|
|
downloadHistory: {
|
|
type: Array as PropType<readonly (number | null)[]>,
|
|
required: true,
|
|
},
|
|
uploadHistory: {
|
|
type: Array as PropType<readonly (number | null)[]>,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const { t } = useI18n();
|
|
|
|
const chartHostRef = ref<HTMLElement | null>(null);
|
|
const chartKey = ref(0);
|
|
let chartResizeObserver: ResizeObserver | null = null;
|
|
let lastChartHostWidth = 0;
|
|
|
|
const recentDownloadHistory = computed(() => props.downloadHistory.slice(-DISPLAY_POINTS));
|
|
const recentUploadHistory = computed(() => props.uploadHistory.slice(-DISPLAY_POINTS));
|
|
const displayPointCount = computed(() => Math.max(recentDownloadHistory.value.length, recentUploadHistory.value.length, DISPLAY_POINTS));
|
|
|
|
const peakHistoryRateKB = computed(() => {
|
|
const values = [
|
|
...recentDownloadHistory.value.map(value => (value ?? 0) / 1024),
|
|
...recentUploadHistory.value.map(value => (value ?? 0) / 1024),
|
|
];
|
|
return Math.max(...values, 0);
|
|
});
|
|
|
|
const networkRateUnitIsMB = computed(() => peakHistoryRateKB.value >= KB_TO_MB_THRESHOLD);
|
|
|
|
const chartDivisor = computed(() => (networkRateUnitIsMB.value ? 1024 * 1024 : 1024));
|
|
|
|
const chartPrecision = computed(() => (networkRateUnitIsMB.value ? 2 : 1));
|
|
|
|
const chartLabels = computed(() =>
|
|
Array.from({ length: displayPointCount.value }, (_, index) => `${index + 1}`),
|
|
);
|
|
|
|
const networkChartData = computed(() => ({
|
|
labels: chartLabels.value,
|
|
datasets: [
|
|
{
|
|
label: t('statusMonitor.networkDownloadLabelUnit', { unit: networkRateUnitIsMB.value ? 'MB/s' : 'KB/s' }),
|
|
data: recentDownloadHistory.value.map(value =>
|
|
value === null || value === undefined ? null : Number((value / chartDivisor.value).toFixed(chartPrecision.value)),
|
|
),
|
|
borderColor: 'rgba(96, 165, 250, 1)',
|
|
backgroundColor: 'rgba(96, 165, 250, 0.18)',
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
tension: 0.24,
|
|
spanGaps: true,
|
|
},
|
|
{
|
|
label: t('statusMonitor.networkUploadLabelUnit', { unit: networkRateUnitIsMB.value ? 'MB/s' : 'KB/s' }),
|
|
data: recentUploadHistory.value.map(value =>
|
|
value === null || value === undefined ? null : Number((value / chartDivisor.value).toFixed(chartPrecision.value)),
|
|
),
|
|
borderColor: 'rgba(52, 211, 153, 1)',
|
|
backgroundColor: 'rgba(52, 211, 153, 0.16)',
|
|
borderWidth: 2,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
tension: 0.24,
|
|
spanGaps: true,
|
|
},
|
|
],
|
|
}));
|
|
|
|
const suggestedYAxisMax = computed(() => {
|
|
const allValues = [
|
|
...recentDownloadHistory.value,
|
|
...recentUploadHistory.value,
|
|
].filter((value): value is number => value !== null && value !== undefined && Number.isFinite(value))
|
|
.map(value => value / chartDivisor.value);
|
|
|
|
const currentMax = Math.max(...allValues, 0);
|
|
if (currentMax === 0) {
|
|
return networkRateUnitIsMB.value ? 1 : 100;
|
|
}
|
|
|
|
if (networkRateUnitIsMB.value) {
|
|
return Math.max(1, Math.ceil(currentMax * 1.2));
|
|
}
|
|
|
|
if (currentMax <= 100) {
|
|
return Math.max(10, Math.ceil((currentMax * 1.2) / 10) * 10);
|
|
}
|
|
|
|
if (currentMax <= 500) {
|
|
return Math.ceil((currentMax * 1.2) / 50) * 50;
|
|
}
|
|
|
|
return Math.ceil((currentMax * 1.2) / 100) * 100;
|
|
});
|
|
|
|
const networkChartOptions = computed<ChartOptions<'line'>>(() => ({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false,
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
mode: 'index',
|
|
intersect: false,
|
|
displayColors: true,
|
|
callbacks: {
|
|
title: () => '',
|
|
label: (context: TooltipItem<'line'>) => {
|
|
const label = context.dataset.label ?? '';
|
|
const value = context.parsed.y;
|
|
if (value === null) {
|
|
return label;
|
|
}
|
|
|
|
return `${label}: ${Number(value).toFixed(chartPrecision.value)} ${networkRateUnitIsMB.value ? 'MB/s' : 'KB/s'}`;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: false,
|
|
grid: {
|
|
display: false,
|
|
},
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
min: 0,
|
|
max: suggestedYAxisMax.value,
|
|
ticks: {
|
|
color: '#8fa0b3',
|
|
callback: value => {
|
|
const numericValue = Number(value);
|
|
return Number.isFinite(numericValue) ? numericValue.toFixed(networkRateUnitIsMB.value ? 2 : 0) : '';
|
|
},
|
|
},
|
|
grid: {
|
|
color: 'rgba(148, 163, 184, 0.12)',
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
|
|
const rerenderChart = (): void => {
|
|
chartKey.value += 1;
|
|
};
|
|
|
|
watch(
|
|
() => [recentDownloadHistory.value, recentUploadHistory.value, suggestedYAxisMax.value],
|
|
() => {
|
|
rerenderChart();
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
onMounted(() => {
|
|
const host = chartHostRef.value;
|
|
if (!host || typeof ResizeObserver === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
lastChartHostWidth = Math.round(host.getBoundingClientRect().width);
|
|
chartResizeObserver = new ResizeObserver(entries => {
|
|
const entry = entries[0];
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
|
|
const nextWidth = Math.round(entry.contentRect.width);
|
|
if (!nextWidth || Math.abs(nextWidth - lastChartHostWidth) < 2) {
|
|
return;
|
|
}
|
|
|
|
lastChartHostWidth = nextWidth;
|
|
nextTick(() => {
|
|
rerenderChart();
|
|
});
|
|
});
|
|
|
|
chartResizeObserver.observe(host);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
chartResizeObserver?.disconnect();
|
|
chartResizeObserver = null;
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.network-history-chart {
|
|
display: grid;
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
gap: 8px;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
height: auto;
|
|
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.06), transparent 62%);
|
|
padding: 10px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.network-history-chart__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
|
|
.network-history-chart__subtitle {
|
|
margin: 0;
|
|
color: #8fa0b3;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.network-history-chart__title {
|
|
margin: 6px 0 0;
|
|
color: #f8fbff;
|
|
font-size: 14px;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.network-history-chart__legend {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.network-history-chart__legend-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: #d9e5f1;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.network-history-chart__legend-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.network-history-chart__legend-dot--download {
|
|
background: rgba(96, 165, 250, 1);
|
|
}
|
|
|
|
.network-history-chart__legend-dot--upload {
|
|
background: rgba(52, 211, 153, 1);
|
|
}
|
|
|
|
.network-history-chart__canvas {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
width: 100%;
|
|
height: 88px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.network-history-chart__canvas :deep(canvas) {
|
|
width: 100% !important;
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
@container (max-width: 300px) {
|
|
.network-history-chart__header {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.network-history-chart__legend {
|
|
justify-content: flex-start;
|
|
}
|
|
}
|
|
</style>
|