804 lines
22 KiB
Vue
804 lines
22 KiB
Vue
<template>
|
||
<div class="server-dashboard">
|
||
<div v-if="isSnapshotLoading && !snapshotView" class="loading-wrap">
|
||
<t-loading text="加载系统状态..." />
|
||
</div>
|
||
<template v-else-if="snapshotView">
|
||
<t-cell-group title="系统" theme="card">
|
||
<t-cell title="操作系统" :note="Text.display(snapshotView?.snapshot?.os?.name)" />
|
||
<t-cell title="启动时间" :note="Time.toPassedDateTime(snapshotView?.snapshot?.os?.bootAt)" />
|
||
<t-cell
|
||
v-if="snapshotView?.snapshot?.os?.bootAt"
|
||
title="运行时长"
|
||
:note="Time.duration(Time.now() - snapshotView?.snapshot?.os?.bootAt)"
|
||
/>
|
||
<t-cell title="更多" arrow @click="router.push({
|
||
path: '/server/system-detail',
|
||
query: {
|
||
tab: 'server'
|
||
}
|
||
})"
|
||
/>
|
||
</t-cell-group>
|
||
<t-cell-group class="section performance" title="资源" theme="card">
|
||
<t-cell-info label="CPU" :value="Text.display(snapshotView?.snapshot?.cpu?.model)">
|
||
<progress-group
|
||
mode="heap"
|
||
:progress="cpuProgressList"
|
||
:note="Text.unit((snapshotView?.snapshot?.cpu?.usageTotal || 0) * 100, '%')"
|
||
/>
|
||
</t-cell-info>
|
||
<t-cell-info class="memory" label="内存" :value="memoryUsageText">
|
||
<progress-group
|
||
mode="splice"
|
||
:progress="memoryProgressList"
|
||
:note="Text.unit(memoryUsed * 100, '%')"
|
||
/>
|
||
</t-cell-info>
|
||
<t-cell-info class="memory-jvm" label="JVM 内存" :value="IOSize.format(jvmMemoryHeapMax)">
|
||
<progress-group
|
||
mode="heap"
|
||
:progress="jvmMemoryProgressList"
|
||
:note="Text.unit(jvmMemoryHeapCommited / jvmMemoryHeapMax * 100, '%')"
|
||
/>
|
||
</t-cell-info>
|
||
<t-cell title="网络发送速率" :note="IOSize.speed(snapshotView?.snapshot?.network?.txBytesPerSecond)">
|
||
<template #left-icon>
|
||
<t-icon name="arrow-up" />
|
||
</template>
|
||
</t-cell>
|
||
<t-cell title="网络接收速率" :note="IOSize.speed(snapshotView?.snapshot?.network?.rxBytesPerSecond)">
|
||
<template #left-icon>
|
||
<t-icon name="arrow-down" />
|
||
</template>
|
||
</t-cell>
|
||
<t-cell title="更多" arrow @click="router.push({
|
||
path: '/server/performance-detail',
|
||
query: {
|
||
tab: 'server'
|
||
}
|
||
})"
|
||
/>
|
||
</t-cell-group>
|
||
<t-cell-group class="section disk" title="磁盘" theme="card">
|
||
<t-cell-info
|
||
class="item"
|
||
v-for="item in snapshotView?.snapshot?.storagePartitions"
|
||
:key="item.uuid"
|
||
:label="item.mountPoint || item.partitionId"
|
||
:value="`${IOSize.format(item.used)} / ${IOSize.format(item.total)}`"
|
||
clip-text="label"
|
||
>
|
||
<progress-group
|
||
mode="heap"
|
||
:progress="[{
|
||
color: 'var(--td-brand-color)',
|
||
value: item.used / item.total
|
||
}]"
|
||
:note="Text.unit(item.used / item.total * 100, '%')"
|
||
/>
|
||
</t-cell-info>
|
||
<t-cell title="更多" arrow @click="router.push({
|
||
path: '/server/partitions-detail',
|
||
query: {
|
||
tab: 'server'
|
||
}
|
||
})"
|
||
/>
|
||
</t-cell-group>
|
||
</template>
|
||
<t-empty v-else description="暂无系统状态数据" />
|
||
<t-cell-group class="section history" title="历史采样" theme="card">
|
||
<t-tabs class="metric-tabs" :value="historyMetric" @change="handleHistoryMetricChange">
|
||
<t-tab-panel
|
||
v-for="item in historyMetricTabs"
|
||
:key="item.value"
|
||
:value="item.value"
|
||
:label="item.label"
|
||
/>
|
||
</t-tabs>
|
||
<div v-if="isHistoryLoading && !historyItems.length" class="loading-wrap">
|
||
<t-loading text="加载历史数据..." />
|
||
</div>
|
||
<template v-else-if="historyItems.length">
|
||
<v-chart
|
||
ref="chartRef"
|
||
class="chart"
|
||
:option="historyChartOption"
|
||
:update-options="historyChartUpdateOptions"
|
||
:autoresize="true"
|
||
/>
|
||
<div class="slider bg-white">
|
||
<span class="label">较早</span>
|
||
<t-slider
|
||
class="control"
|
||
:min="0"
|
||
:max="Math.max(historyItems.length - historyVisibleCount, 0)"
|
||
:step="1"
|
||
:value="historySliderValue"
|
||
@change="handleHistorySliderChange"
|
||
/>
|
||
<span class="label">最新</span>
|
||
</div>
|
||
<t-cell-group>
|
||
<t-cell v-for="item in currentHistorySummary" :key="item.label" :title="item.label" :note="item.value" />
|
||
</t-cell-group>
|
||
</template>
|
||
<t-empty v-else description="暂无历史采样数据" />
|
||
</t-cell-group>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Toast } from "tdesign-mobile-vue";
|
||
import { use } from "echarts/core";
|
||
import { SVGRenderer } from "echarts/renderers";
|
||
import { LineChart } from "echarts/charts";
|
||
import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components";
|
||
import VChart from "vue-echarts";
|
||
import { useRouter } from "vue-router";
|
||
import type { EChartsOption, SeriesOption } from "echarts";
|
||
import SystemAPI from "@/api/SystemAPI";
|
||
import type { SystemStatusHistoryPoint, SystemStatusSnapshotView } from "@/types/System";
|
||
import { useSettingStore } from "@/store/settingStore";
|
||
import { IOSize, type LabelValue, Text, Time } from "timi-web";
|
||
import TCellInfo from "@/components/TCellInfo.vue";
|
||
import type { ProgressItem } from "@/components/ProgressGroup.vue";
|
||
import ProgressGroup from "@/components/ProgressGroup.vue";
|
||
|
||
type HistoryMetric = "cpu" | "memory" | "jvm" | "network";
|
||
|
||
const SERVER_SNAPSHOT_METRICS = ["os", "cpu", "memory", "jvm", "network", "hardware", "storage"] as const;
|
||
const SERVER_HISTORY_METRICS = ["cpu", "memory", "jvm", "network"] as const;
|
||
|
||
use([
|
||
SVGRenderer,
|
||
LineChart,
|
||
GridComponent,
|
||
TooltipComponent,
|
||
LegendComponent
|
||
]);
|
||
|
||
defineOptions({
|
||
name: "ServerDashboard"
|
||
});
|
||
|
||
const router = useRouter();
|
||
const settingStore = useSettingStore();
|
||
|
||
const isSnapshotLoading = ref(false);
|
||
const snapshotView = ref<SystemStatusSnapshotView | null>(null);
|
||
|
||
// ---------- CPU ----------
|
||
|
||
const cpuProgressList = computed<ProgressItem[]>(() => {
|
||
const system = snapshotView.value?.snapshot?.cpu?.usageSystem || 0;
|
||
const total = snapshotView.value?.snapshot?.cpu?.usageTotal || 0;
|
||
return [
|
||
{
|
||
color: "var(--td-brand-color)",
|
||
value: system,
|
||
legend: `系统占用(${Text.unit(system * 100, '%')})`
|
||
},
|
||
{
|
||
color: "var(--td-brand-color-5)",
|
||
value: total,
|
||
legend: `总占用(${Text.unit(total * 100, '%')})`
|
||
}
|
||
];
|
||
});
|
||
|
||
// ---------- 内存 ----------
|
||
|
||
const memoryUsed = computed(() => {
|
||
const usedBytes = snapshotView.value?.snapshot?.memory?.usedBytes || 1;
|
||
const totalBytes = snapshotView.value?.snapshot?.memory?.totalBytes || 1;
|
||
return usedBytes / totalBytes;
|
||
});
|
||
const memoryUsageText = computed(() => {
|
||
const usedBytes = snapshotView.value?.snapshot?.memory?.usedBytes;
|
||
const totalBytes = snapshotView.value?.snapshot?.memory?.totalBytes;
|
||
return `${IOSize.format(usedBytes)} / ${IOSize.format(totalBytes)}`;
|
||
});
|
||
const memoryTotal = computed(() => {
|
||
const total = snapshotView.value?.snapshot?.memory?.totalBytes || 0;
|
||
const totalSwap = snapshotView.value?.snapshot?.memory?.swapTotalBytes || 0;
|
||
return total + totalSwap;
|
||
});
|
||
const memoryProgressList = computed<ProgressItem[]>(() => {
|
||
const used = snapshotView.value?.snapshot?.memory?.usedBytes || 0;
|
||
const usedSwap = snapshotView.value?.snapshot?.memory?.swapUsedBytes || 0;
|
||
return [
|
||
{
|
||
color: "var(--td-brand-color)",
|
||
value: used / memoryTotal.value,
|
||
legend: `物理(${IOSize.format(used)})`
|
||
},
|
||
{
|
||
color: "var(--td-brand-color-5)",
|
||
value: usedSwap / memoryTotal.value,
|
||
legend: `交换区(${IOSize.format(usedSwap)})`
|
||
}
|
||
]
|
||
});
|
||
|
||
// ---------- JVM 内存 ----------
|
||
|
||
const jvmMemoryHeapMax = computed(() => snapshotView.value?.snapshot?.jvm?.heapMaxBytes || 0);
|
||
const jvmMemoryHeapCommited = computed(() => snapshotView.value?.snapshot?.jvm?.heapCommittedBytes || 0);
|
||
const jvmMemoryHeapUsed = computed(() => snapshotView.value?.snapshot?.jvm?.heapUsedBytes || 0);
|
||
const jvmMemoryProgressList = computed<ProgressItem[]>(() => {
|
||
return [
|
||
{
|
||
color: "var(--td-brand-color)",
|
||
value: jvmMemoryHeapCommited.value / jvmMemoryHeapMax.value,
|
||
legend: `已提交(${IOSize.format(jvmMemoryHeapCommited.value)})`
|
||
},
|
||
{
|
||
color: "var(--td-brand-color-5)",
|
||
value: jvmMemoryHeapUsed.value / jvmMemoryHeapMax.value,
|
||
legend: `已使用(${IOSize.format(jvmMemoryHeapUsed.value)})`
|
||
},
|
||
]
|
||
})
|
||
|
||
// ---------- 历史采样图表 ----------
|
||
|
||
const isHistoryLoading = ref(false);
|
||
const historyPoints = ref<SystemStatusHistoryPoint[]>([]);
|
||
const chartRef = ref<InstanceType<typeof VChart> | null>(null);
|
||
const historyChartUpdateOptions = Object.freeze({
|
||
notMerge: true
|
||
});
|
||
const isDarkTheme = ref(false);
|
||
|
||
// ---------- Tab ----------
|
||
|
||
const historyMetric = ref<HistoryMetric>("cpu");
|
||
|
||
const historyMetricTabs = computed(() => {
|
||
const labelMap: Record<HistoryMetric, string> = {
|
||
cpu: "CPU",
|
||
memory: "内存",
|
||
jvm: "JVM",
|
||
network: "网络"
|
||
};
|
||
|
||
return SERVER_HISTORY_METRICS.map((metric) => ({
|
||
value: metric,
|
||
label: labelMap[metric]
|
||
}));
|
||
});
|
||
watch(
|
||
() => historyMetricTabs.value,
|
||
(tabs) => {
|
||
if (!tabs.length) {
|
||
historyMetric.value = "cpu";
|
||
return;
|
||
}
|
||
|
||
if (!tabs.some((item) => item.value === historyMetric.value)) {
|
||
historyMetric.value = tabs[0].value;
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
function handleHistoryMetricChange(value: string): void {
|
||
const nextValue = value as HistoryMetric;
|
||
if (nextValue === "cpu" || nextValue === "memory" || nextValue === "jvm" || nextValue === "network") {
|
||
historyMetric.value = nextValue;
|
||
}
|
||
}
|
||
|
||
// ---------- 图表数据 ----------
|
||
|
||
const historyItems = computed<SystemStatusHistoryPoint[]>(() => {
|
||
return historyPoints.value.slice().sort((left, right) => left.at - right.at);
|
||
});
|
||
const currentHistoryRange = computed(() => {
|
||
const length = historyItems.value.length;
|
||
if (length < 1) {
|
||
return { startValue: 0, endValue: 0 };
|
||
}
|
||
|
||
const startValue = Math.min(Math.max(Math.floor(historySliderValue.value), 0), historySliderMaxStart.value);
|
||
const endValue = Math.min(startValue + historyVisibleCount.value - 1, length - 1);
|
||
return { startValue, endValue };
|
||
});
|
||
const visibleHistoryItems = computed<SystemStatusHistoryPoint[]>(() => {
|
||
if (!historyItems.value.length) {
|
||
return [];
|
||
}
|
||
return historyItems.value.slice(currentHistoryRange.value.startValue, currentHistoryRange.value.endValue + 1);
|
||
});
|
||
|
||
// ---------- 调整图表数据范围 ----------
|
||
|
||
const isHistorySliderTouched = ref(false);
|
||
const historySliderValue = ref(0);
|
||
const HISTORY_DEFAULT_VISIBLE_COUNT = 30;
|
||
|
||
const historyVisibleCount = computed(() => {
|
||
if (historyItems.value.length < 1) {
|
||
return 1;
|
||
}
|
||
return Math.min(HISTORY_DEFAULT_VISIBLE_COUNT, historyItems.value.length);
|
||
});
|
||
const historySliderMaxStart = computed(() => {
|
||
return Math.max(historyItems.value.length - historyVisibleCount.value, 0);
|
||
});
|
||
watch(() => historyItems.value.length, (length) => {
|
||
if (length < 1) {
|
||
historySliderValue.value = 0;
|
||
isHistorySliderTouched.value = false;
|
||
return;
|
||
}
|
||
if (!isHistorySliderTouched.value) {
|
||
historySliderValue.value = historySliderMaxStart.value;
|
||
return;
|
||
}
|
||
historySliderValue.value = Math.min(historySliderValue.value, historySliderMaxStart.value);
|
||
});
|
||
|
||
function handleHistorySliderChange(value: number): void {
|
||
historySliderValue.value = Math.min(Math.max(Math.floor(value), 0), historySliderMaxStart.value);
|
||
isHistorySliderTouched.value = true;
|
||
}
|
||
|
||
// ---------- 当前图表摘要 ----------
|
||
|
||
const currentHistorySummary = computed<LabelValue<string, any>[]>(() => {
|
||
if (!historyItems.value.length) {
|
||
return [];
|
||
}
|
||
const lastPoint = historyItems.value[historyItems.value.length - 1];
|
||
const summary: LabelValue<string, any>[] = [];
|
||
if (historyMetric.value === "cpu") {
|
||
summary.push(
|
||
{ label: "总使用率", value: Text.unit(lastPoint.cpuUsagePercent * 100, "%") },
|
||
{ label: "系统使用率", value: Text.unit(lastPoint.cpuSystemPercent * 100, "%") }
|
||
);
|
||
return summary;
|
||
}
|
||
if (historyMetric.value === "memory") {
|
||
summary.push(
|
||
{ label: "物理", value: IOSize.format(lastPoint.memoryUsedBytes) },
|
||
{ label: "交换区", value: IOSize.format(lastPoint.swapUsedBytes) }
|
||
);
|
||
return summary;
|
||
}
|
||
if (historyMetric.value === "jvm") {
|
||
summary.push(
|
||
{ label: "已使用", value: IOSize.format(lastPoint.heapUsedBytes) },
|
||
{ label: "已提交", value: IOSize.format(lastPoint.heapCommittedBytes) },
|
||
{ label: "GC 周期耗时", value: Text.unit(lastPoint.gcCycleTimeMs, "毫秒") },
|
||
{ label: "GC 暂停耗时", value: Text.unit(lastPoint.gcPauseTimeMs, "毫秒") }
|
||
);
|
||
return summary;
|
||
}
|
||
summary.push(
|
||
{ label: "接收", value: IOSize.speed(lastPoint.rxBytesPerSecond) },
|
||
{ label: "发送", value: IOSize.speed(lastPoint.txBytesPerSecond) }
|
||
);
|
||
return summary;
|
||
});
|
||
|
||
// ---------- 图表配置 ----------
|
||
|
||
const historyChartOption = computed((): EChartsOption => {
|
||
const axisColor = isDarkTheme.value ? "#9aa4b2" : "#7e8a9a";
|
||
const lineColor = isDarkTheme.value ? "rgba(255, 255, 255, .12)" : "rgba(18, 42, 66, .08)";
|
||
const legendColor = isDarkTheme.value ? "#d7dee8" : "#334155";
|
||
const memoryAxisMax = Math.max(snapshotView.value?.snapshot?.memory?.totalBytes || 0, 1);
|
||
|
||
// 通用配置
|
||
const commonOption: EChartsOption = {
|
||
tooltip: {
|
||
trigger: "axis",
|
||
confine: true,
|
||
position: (point, _params, _dom, _rect, size) => {
|
||
const gap = 6;
|
||
const fingerGap = 24;
|
||
const contentSize = size?.contentSize || [0, 0];
|
||
const viewSize = size?.viewSize || [0, 0];
|
||
const maxLeft = Math.max(viewSize[0] - contentSize[0] - gap, gap);
|
||
const maxTop = Math.max(viewSize[1] - contentSize[1] - gap, gap);
|
||
const clamp = (value: number, min: number, max: number): number => {
|
||
return Math.min(Math.max(value, min), max);
|
||
};
|
||
const preferLeft = point[0] - contentSize[0] - gap;
|
||
const preferRight = point[0] + gap;
|
||
const left = point[0] < viewSize[0] / 2
|
||
? clamp(preferRight, gap, maxLeft)
|
||
: clamp(preferLeft, gap, maxLeft);
|
||
const preferTop = point[1] - contentSize[1] - fingerGap;
|
||
const preferBottom = point[1] + fingerGap;
|
||
const top = preferTop >= gap
|
||
? clamp(preferTop, gap, maxTop)
|
||
: clamp(preferBottom, gap, maxTop);
|
||
return [left, top];
|
||
}
|
||
},
|
||
legend: {
|
||
top: 10,
|
||
textStyle: {
|
||
color: legendColor
|
||
}
|
||
},
|
||
grid: {
|
||
top: 40,
|
||
left: 8,
|
||
right: 8,
|
||
bottom: 8,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: "category",
|
||
boundaryGap: false,
|
||
data: visibleHistoryItems.value.map((item) => item.at),
|
||
axisLabel: {
|
||
color: axisColor,
|
||
formatter: val => Time.toShortTime(Number(val))
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: lineColor
|
||
}
|
||
}
|
||
}
|
||
};
|
||
if (!historyItems.value.length) {
|
||
return commonOption;
|
||
}
|
||
if (historyMetric.value === "cpu") {
|
||
return {
|
||
...commonOption,
|
||
tooltip: {
|
||
...commonOption.tooltip,
|
||
formatter: params => {
|
||
const list = Array.isArray(params) ? params : [params];
|
||
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`
|
||
const lines = list.map(item => `${item.marker}${item.seriesName}: ${Text.unit(Number(item.value) * 100, " %")}`);
|
||
return [title, ...lines].join("<br />");
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
min: 0,
|
||
max: 1,
|
||
axisLabel: {
|
||
color: axisColor,
|
||
formatter: val => `${val * 100} %`
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: lineColor
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
resolveLineSeries("总使用率", visibleHistoryItems.value.map((item) => item.cpuUsagePercent!)),
|
||
resolveLineSeries("系统使用率", visibleHistoryItems.value.map((item) => item.cpuSystemPercent!))
|
||
]
|
||
};
|
||
}
|
||
if (historyMetric.value === "memory") {
|
||
return {
|
||
...commonOption,
|
||
tooltip: {
|
||
...commonOption.tooltip,
|
||
formatter: (params) => {
|
||
const list = Array.isArray(params) ? params : [params];
|
||
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`
|
||
const lines = list.map(item => `${item.marker}${item.seriesName}: ${IOSize.format(Number(item.value))}`);
|
||
return [title, ...lines].join("<br />");
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
max: memoryAxisMax,
|
||
axisLabel: {
|
||
color: axisColor,
|
||
formatter: (value: number) => IOSize.format(value)
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: lineColor
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
resolveLineSeries("物理", visibleHistoryItems.value.map((item) => item.memoryUsedBytes!)),
|
||
resolveLineSeries("交换区", visibleHistoryItems.value.map((item) => item.swapUsedBytes!))
|
||
]
|
||
};
|
||
}
|
||
if (historyMetric.value === "jvm") {
|
||
return {
|
||
...commonOption,
|
||
tooltip: {
|
||
...commonOption.tooltip,
|
||
formatter: (params) => {
|
||
const list = Array.isArray(params) ? params : [params];
|
||
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`
|
||
const lines = list.map(item => {
|
||
const seriesName = item?.seriesName || "";
|
||
if (seriesName === "GC 周期耗时" || seriesName === "GC 暂停耗时") {
|
||
return `${item.marker}${item.seriesName}: ${Text.unit(Number(item.value), " 毫秒")}`
|
||
} else {
|
||
return `${item.marker}${item.seriesName}: ${IOSize.format(Number(item.value))}`
|
||
}
|
||
});
|
||
return [title, ...lines].join("<br />");
|
||
}
|
||
},
|
||
yAxis: [
|
||
{
|
||
type: "value",
|
||
axisLabel: {
|
||
color: axisColor,
|
||
formatter: (val: number) => IOSize.format(val)
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: lineColor
|
||
}
|
||
}
|
||
},
|
||
{
|
||
type: "value",
|
||
axisLabel: {
|
||
color: axisColor,
|
||
formatter: "{value} ms"
|
||
},
|
||
splitLine: {
|
||
show: false
|
||
}
|
||
}
|
||
],
|
||
series: [
|
||
resolveLineSeries("已使用", visibleHistoryItems.value.map((item) => item.heapUsedBytes!)),
|
||
resolveLineSeries("已提交", visibleHistoryItems.value.map((item) => item.heapCommittedBytes!)),
|
||
resolveLineSeries("GC 周期", visibleHistoryItems.value.map((item) => item.gcCycleTimeMs!), 1),
|
||
resolveLineSeries("GC 暂停", visibleHistoryItems.value.map((item) => item.gcPauseTimeMs!), 1)
|
||
]
|
||
};
|
||
}
|
||
if (historyMetric.value === "network") {
|
||
return {
|
||
// 网络
|
||
...commonOption,
|
||
tooltip: {
|
||
...commonOption.tooltip,
|
||
formatter: (params) => {
|
||
const list = Array.isArray(params) ? params : [params];
|
||
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`
|
||
const lines = list.map(item => `${item.marker}${item.seriesName}: ${IOSize.speed(Number(item.value))}`);
|
||
return [title, ...lines].join("<br />");
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: "value",
|
||
axisLabel: {
|
||
color: axisColor,
|
||
formatter: (val: number) => IOSize.format(val)
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: lineColor
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
resolveLineSeries("接收", visibleHistoryItems.value.map((item) => item.rxBytesPerSecond!)),
|
||
resolveLineSeries("发送", visibleHistoryItems.value.map((item) => item.txBytesPerSecond!))
|
||
]
|
||
};
|
||
}
|
||
return commonOption;
|
||
});
|
||
|
||
watch(() => settingStore.dashboard.server, restartAutoRefresh, { deep: true });
|
||
|
||
function resolveLineSeries(name: string, data: Array<number | null>, yAxisIndex?: number): SeriesOption {
|
||
const series: SeriesOption = {
|
||
type: "line",
|
||
name,
|
||
showSymbol: false,
|
||
smooth: true,
|
||
lineStyle: {
|
||
width: 2
|
||
},
|
||
areaStyle: {
|
||
opacity: .18
|
||
},
|
||
data
|
||
};
|
||
|
||
if (typeof yAxisIndex === "number") {
|
||
series.yAxisIndex = yAxisIndex;
|
||
}
|
||
|
||
return series;
|
||
}
|
||
|
||
// ---------- 加载数据:当前状态 ----------
|
||
|
||
let snapshotTimer: number | undefined;
|
||
onUnmounted(() => {
|
||
window.clearInterval(snapshotTimer);
|
||
snapshotTimer = undefined;
|
||
});
|
||
|
||
async function refreshSnapshot(): Promise<void> {
|
||
if (isSnapshotLoading.value) {
|
||
return;
|
||
}
|
||
isSnapshotLoading.value = true;
|
||
try {
|
||
const metrics = SERVER_SNAPSHOT_METRICS.join(",");
|
||
snapshotView.value = await SystemAPI.getStatus(metrics);
|
||
} catch (error) {
|
||
Toast({
|
||
theme: "error",
|
||
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||
});
|
||
} finally {
|
||
isSnapshotLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// ---------- 加载数据:历史采集 ----------
|
||
|
||
let historyTimer: number | undefined;
|
||
|
||
async function refreshHistory(): Promise<void> {
|
||
if (isHistoryLoading.value) {
|
||
return;
|
||
}
|
||
isHistoryLoading.value = true;
|
||
try {
|
||
const metrics = SERVER_HISTORY_METRICS.join(",");
|
||
const historyView = await SystemAPI.getStatusHistory({
|
||
window: "1h",
|
||
metrics
|
||
});
|
||
historyPoints.value = historyView.points || [];
|
||
} catch (error) {
|
||
Toast({
|
||
theme: "error",
|
||
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||
});
|
||
} finally {
|
||
isHistoryLoading.value = false;
|
||
}
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
window.clearInterval(historyTimer);
|
||
historyTimer = undefined;
|
||
});
|
||
|
||
// ---------- 自动刷新 ----------
|
||
|
||
watch(() => settingStore.dashboard.server, restartAutoRefresh, { deep: true });
|
||
|
||
async function refreshDashboard(): Promise<void> {
|
||
await Promise.all([
|
||
refreshSnapshot(),
|
||
refreshHistory()
|
||
]);
|
||
}
|
||
function restartAutoRefresh(): void {
|
||
void refreshDashboard();
|
||
clearAutoRefreshTimer();
|
||
|
||
const snapshotIntervalMs = Math.max(settingStore.dashboard.server.snapshotRefreshSeconds, 1) * 1000;
|
||
const historyIntervalMs = Math.max(settingStore.dashboard.server.historyRefreshSeconds, 1) * 1000;
|
||
|
||
snapshotTimer = window.setInterval(() => {
|
||
void refreshSnapshot();
|
||
}, snapshotIntervalMs);
|
||
|
||
historyTimer = window.setInterval(() => {
|
||
void refreshHistory();
|
||
}, historyIntervalMs);
|
||
}
|
||
|
||
function clearAutoRefreshTimer(): void {
|
||
if (snapshotTimer !== null) {
|
||
window.clearInterval(snapshotTimer);
|
||
snapshotTimer = undefined;
|
||
}
|
||
if (historyTimer !== null) {
|
||
window.clearInterval(historyTimer);
|
||
historyTimer = undefined;
|
||
}
|
||
}
|
||
onMounted(restartAutoRefresh);
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
.server-dashboard {
|
||
gap: 1rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.section {
|
||
|
||
&.disk {
|
||
|
||
.collapses {
|
||
|
||
:deep(.t-cell__title) {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.content {
|
||
width: 100%;
|
||
gap: .4rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.header {
|
||
gap: .5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
|
||
.title {
|
||
color: var(--app-text);
|
||
font-size: .875rem;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.health {
|
||
font-size: .75rem;
|
||
color: var(--app-sub);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
&.history {
|
||
.metric-tabs {
|
||
width: 100%;
|
||
--td-tab-track-width: 4rem;
|
||
}
|
||
|
||
.chart {
|
||
width: 100%;
|
||
height: 16rem;
|
||
background: #FFF;
|
||
}
|
||
|
||
.slider {
|
||
gap: .5rem;
|
||
display: flex;
|
||
padding: .5rem 2rem;
|
||
align-items: center;
|
||
|
||
.label {
|
||
flex: none;
|
||
font-size: .75rem;
|
||
color: var(--app-sub);
|
||
}
|
||
|
||
.control {
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.loading-wrap {
|
||
display: flex;
|
||
min-height: 4rem;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|