fix27: полный production-фикс dashboard/peer/i18n без легаси-костылей

This commit is contained in:
2026-05-09 01:59:49 +05:00
parent db3c88c58a
commit 3c569061fe
9 changed files with 293 additions and 67 deletions
+128 -21
View File
@@ -51,13 +51,10 @@
</el-radio-group>
</div>
</template>
<el-table :data="timeseriesRows" size="small" v-loading="timeseriesLoading">
<el-table-column prop="ts" :label="$t('common.createTime')" width="180" />
<el-table-column prop="download" :label="$t('dashboard.download')" />
<el-table-column prop="upload" :label="$t('dashboard.upload')" />
<el-table-column prop="cpu" :label="$t('dashboard.cpu')" width="120" />
<el-table-column prop="mem" :label="$t('dashboard.ram')" width="120" />
</el-table>
<div v-loading="timeseriesLoading" class="chart-grid">
<v-chart class="chart" autoresize :option="trafficChartOption" />
<v-chart class="chart" autoresize :option="systemChartOption" />
</div>
</el-card>
<el-card shadow="never" class="mt-3">
@@ -81,11 +78,24 @@
<script setup lang="ts">
import { useIntervalFn } from "@vueuse/core";
import { useI18n } from "vue-i18n";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { LineChart } from "echarts/charts";
import {
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
TitleComponent,
} from "echarts/components";
import VChart from "vue-echarts";
import { dashboardSecurityApi, dashboardSummaryApi, dashboardTimeseriesApi, dashboardTopPeersApi } from "@/api/dashboard";
import { DashboardSummaryVo, DashboardTimeseriesVo, DashboardTopPeerVo, SecurityRiskVo } from "@/api/dashboard/types";
import { formatBytes } from "@/utils/byte";
import { timestampToDateTime } from "@/utils/time";
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, TitleComponent]);
const { t } = useI18n();
const summary = ref<DashboardSummaryVo>({
@@ -150,23 +160,109 @@ const loadDashboard = async () => {
}
};
const timeseriesRows = computed(() => {
const trafficMap: Record<number, { download: number; upload: number }> = {};
for (const t of timeseries.value.traffic || []) {
trafficMap[t.ts] = { download: t.download || 0, upload: t.upload || 0 };
const xAxisLabels = computed(() => {
const allTs = new Set<number>();
for (const item of timeseries.value.traffic || []) {
allTs.add(item.ts);
}
return (timeseries.value.system || []).map((s) => {
const tm = trafficMap[s.ts] || { download: 0, upload: 0 };
return {
ts: timestampToDateTime(s.ts),
download: formatBytes(tm.download),
upload: formatBytes(tm.upload),
cpu: `${Math.round((s.cpu || 0) * 10) / 10}%`,
mem: `${Math.round((s.mem || 0) * 10) / 10}%`,
};
});
for (const item of timeseries.value.system || []) {
allTs.add(item.ts);
}
return Array.from(allTs).sort((a, b) => a - b);
});
const trafficMap = computed(() => {
const m = new Map<number, { download: number; upload: number }>();
for (const item of timeseries.value.traffic || []) {
m.set(item.ts, { download: item.download || 0, upload: item.upload || 0 });
}
return m;
});
const systemMap = computed(() => {
const m = new Map<number, { cpu: number; mem: number }>();
for (const item of timeseries.value.system || []) {
m.set(item.ts, { cpu: item.cpu || 0, mem: item.mem || 0 });
}
return m;
});
const trafficChartOption = computed(() => ({
title: { text: t("dashboard.trafficSeriesChart"), left: "left", textStyle: { fontSize: 14, fontWeight: 600 } },
tooltip: {
trigger: "axis",
valueFormatter: (value: number) => formatBytes(value || 0),
},
legend: { top: 0 },
grid: { left: 30, right: 20, top: 50, bottom: 50, containLabel: true },
dataZoom: [{ type: "inside" }, { type: "slider", height: 16, bottom: 10 }],
xAxis: {
type: "category",
boundaryGap: false,
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
},
yAxis: {
type: "value",
axisLabel: {
formatter: (v: number) => formatBytes(v),
},
},
series: [
{
name: t("dashboard.download"),
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.download || 0),
},
{
name: t("dashboard.upload"),
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => trafficMap.value.get(ts)?.upload || 0),
},
],
}));
const systemChartOption = computed(() => ({
title: { text: t("dashboard.systemSeriesChart"), left: "left", textStyle: { fontSize: 14, fontWeight: 600 } },
tooltip: {
trigger: "axis",
valueFormatter: (value: number) => `${Math.round((value || 0) * 10) / 10}%`,
},
legend: { top: 0 },
grid: { left: 30, right: 20, top: 50, bottom: 50, containLabel: true },
dataZoom: [{ type: "inside" }, { type: "slider", height: 16, bottom: 10 }],
xAxis: {
type: "category",
boundaryGap: false,
data: xAxisLabels.value.map((ts) => timestampToDateTime(ts)),
},
yAxis: {
type: "value",
min: 0,
max: 100,
axisLabel: { formatter: "{value}%" },
},
series: [
{
name: t("dashboard.cpu"),
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.cpu || 0),
},
{
name: t("dashboard.ram"),
type: "line",
smooth: true,
showSymbol: false,
data: xAxisLabels.value.map((ts) => systemMap.value.get(ts)?.mem || 0),
},
],
}));
const { pause: stopPolling, resume: startPolling } = useIntervalFn(
() => {
loadDashboard();
@@ -211,5 +307,16 @@ onUnmounted(() => {
align-items: center;
justify-content: space-between;
}
.chart-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
.chart {
width: 100%;
height: 340px;
}
</style>