feat: System monitoring supports process viewing (#10821)

Refs #3437
This commit is contained in:
ssongliu 2025-10-30 16:48:18 +08:00 committed by GitHub
parent 9242bf6482
commit 9a73095a8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 404 additions and 113 deletions

View file

@ -15,6 +15,15 @@ type MonitorData struct {
Value []interface{} `json:"value"`
}
type Process struct {
Name string `json:"name"`
Pid int32 `json:"pid"`
Percent float64 `json:"percent"`
Memory uint64 `json:"memory"`
Cmd string `json:"cmd"`
User string `json:"user"`
}
type MonitorSetting struct {
MonitorStatus string `json:"monitorStatus"`
MonitorStoreDays string `json:"monitorStoreDays"`

View file

@ -2,14 +2,18 @@ package model
type MonitorBase struct {
BaseModel
Cpu float64 `json:"cpu"`
Cpu float64 `json:"cpu"`
TopCPU string `json:"topCPU"`
TopCPUItems interface{} `gorm:"-" json:"topCPUItems"`
Memory float64 `json:"memory"`
TopMem string `json:"topMem"`
TopMemItems interface{} `gorm:"-" json:"topMemItems"`
LoadUsage float64 `json:"loadUsage"`
CpuLoad1 float64 `json:"cpuLoad1"`
CpuLoad5 float64 `json:"cpuLoad5"`
CpuLoad15 float64 `json:"cpuLoad15"`
Memory float64 `json:"memory"`
}
type MonitorIO struct {

View file

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"sort"
"strconv"
"time"
@ -21,6 +22,7 @@ import (
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
"github.com/shirou/gopsutil/v4/process"
)
type MonitorService struct {
@ -64,6 +66,18 @@ func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorDa
itemData.Param = "base"
for _, base := range bases {
itemData.Date = append(itemData.Date, base.CreatedAt)
if req.Param == "all" || req.Param == "cpu" {
var processes []dto.Process
_ = json.Unmarshal([]byte(base.TopCPU), &processes)
base.TopCPUItems = processes
base.TopCPU = ""
}
if req.Param == "all" || req.Param == "mem" {
var processes []dto.Process
_ = json.Unmarshal([]byte(base.TopMem), &processes)
base.TopMemItems = processes
base.TopMem = ""
}
itemData.Value = append(itemData.Value, base)
}
data = append(data, itemData)
@ -169,8 +183,14 @@ func (m *MonitorService) Run() {
if len(totalPercent) == 1 {
itemModel.Cpu = totalPercent[0]
}
topCPU := m.loadTopCPU()
if len(topCPU) != 0 {
topItemCPU, err := json.Marshal(topCPU)
if err == nil {
itemModel.TopCPU = string(topItemCPU)
}
}
cpuCount, _ := cpu.Counts(false)
loadInfo, _ := load.Avg()
itemModel.CpuLoad1 = loadInfo.Load1
itemModel.CpuLoad5 = loadInfo.Load5
@ -179,6 +199,13 @@ func (m *MonitorService) Run() {
memoryInfo, _ := mem.VirtualMemory()
itemModel.Memory = memoryInfo.UsedPercent
topMem := m.loadTopMem()
if len(topMem) != 0 {
topMemItem, err := json.Marshal(topMem)
if err == nil {
itemModel.TopMem = string(topMemItem)
}
}
if err := settingRepo.CreateMonitorBase(itemModel); err != nil {
global.LOG.Errorf("Insert basic monitoring data failed, err: %v", err)
@ -323,6 +350,108 @@ func (m *MonitorService) saveNetDataToDB(ctx context.Context, interval float64)
}
}
func (m *MonitorService) loadTopCPU() []dto.Process {
processes, err := process.Processes()
if err != nil {
return nil
}
top5 := make([]dto.Process, 0, 5)
for _, p := range processes {
percent, err := p.CPUPercent()
if err != nil {
continue
}
minIndex := 0
if len(top5) >= 5 {
minCPU := top5[0].Percent
for i := 1; i < len(top5); i++ {
if top5[i].Percent < minCPU {
minCPU = top5[i].Percent
minIndex = i
}
}
if percent < minCPU {
continue
}
}
name, err := p.Name()
if err != nil {
name = "undifine"
}
cmd, err := p.Cmdline()
if err != nil {
cmd = "undifine"
}
user, err := p.Username()
if err != nil {
user = "undifine"
}
if len(top5) == 5 {
top5[minIndex] = dto.Process{Percent: percent, Pid: p.Pid, User: user, Name: name, Cmd: cmd}
} else {
top5 = append(top5, dto.Process{Percent: percent, Pid: p.Pid, User: user, Name: name, Cmd: cmd})
}
}
sort.Slice(top5, func(i, j int) bool {
return top5[i].Percent > top5[j].Percent
})
return top5
}
func (m *MonitorService) loadTopMem() []dto.Process {
processes, err := process.Processes()
if err != nil {
return nil
}
top5 := make([]dto.Process, 0, 5)
for _, p := range processes {
stat, err := p.MemoryInfo()
if err != nil {
continue
}
memItem := stat.RSS
minIndex := 0
if len(top5) >= 5 {
min := top5[0].Memory
for i := 1; i < len(top5); i++ {
if top5[i].Memory < min {
min = top5[i].Memory
minIndex = i
}
}
if memItem < min {
continue
}
}
name, err := p.Name()
if err != nil {
name = "undifine"
}
cmd, err := p.Cmdline()
if err != nil {
cmd = "undifine"
}
user, err := p.Username()
if err != nil {
user = "undifine"
}
percent, _ := p.MemoryPercent()
if len(top5) == 5 {
top5[minIndex] = dto.Process{Percent: float64(percent), Pid: p.Pid, User: user, Name: name, Cmd: cmd, Memory: memItem}
} else {
top5 = append(top5, dto.Process{Percent: float64(percent), Pid: p.Pid, User: user, Name: name, Cmd: cmd, Memory: memItem})
}
}
sort.Slice(top5, func(i, j int) bool {
return top5[i].Memory > top5[j].Memory
})
return top5
}
func StartMonitor(removeBefore bool, interval string) error {
if removeBefore {
monitorCancel()

View file

@ -48,6 +48,7 @@ func InitAgentDB() {
migrations.UpdateWebsiteSSLAddColumn,
migrations.AddTensorRTLLMModel,
migrations.UpdateMonitorInterval,
migrations.AddMonitorProcess,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View file

@ -659,3 +659,10 @@ var UpdateMonitorInterval = &gormigrate.Migration{
return nil
},
}
var AddMonitorProcess = &gormigrate.Migration{
ID: "20251030-add-monitor-process",
Migrate: func(tx *gorm.DB) error {
return global.MonitorDB.AutoMigrate(&model.MonitorBase{})
},
}

View file

@ -104,6 +104,7 @@ const message = {
timeRange: 'To',
dateStart: 'Date start',
dateEnd: 'Date end',
date: 'Date',
},
table: {
all: 'All',
@ -1193,10 +1194,11 @@ const message = {
readWriteTime: 'I/O latency',
today: 'Today',
yesterday: 'Yesterday',
lastNDay: 'Last {0} day(s)',
lastNDay: 'Last {0} days',
lastNMonth: 'Last {0} months',
lastHalfYear: 'Last half year',
memory: 'Memory',
percent: 'Percentage',
cache: 'Cache',
disk: 'Disk',
network: 'Network',

View file

@ -103,6 +103,7 @@ const message = {
timeRange: 'Hasta',
dateStart: 'Fecha de inicio',
dateEnd: 'Fecha de fin',
date: 'Fecha',
},
table: {
all: 'Todo',
@ -1200,8 +1201,9 @@ const message = {
yesterday: 'Ayer',
lastNDay: 'Últimos {0} días',
lastNMonth: 'Últimos {0} meses',
lastHalfYear: 'Últimos seis meses',
lastHalfYear: 'Último semestre',
memory: 'Memoria',
percent: 'Porcentaje',
cache: 'Caché',
disk: 'Disco',
network: 'Red',

View file

@ -101,6 +101,7 @@ const message = {
timeRange: 'に',
dateStart: '日付開始',
dateEnd: '日付の終わり',
date: '日付',
},
table: {
all: '全て',
@ -1157,8 +1158,11 @@ const message = {
readWriteTime: 'I/Oレイテンシ',
today: '今日',
yesterday: '昨日',
lastNDay: '最後の{0}',
lastNDay: '過去 {0} 日間',
lastNMonth: '過去 {0} ヶ月間',
lastHalfYear: '過去半年間',
memory: 'メモリ',
percent: '割合',
cache: 'キャッシュ',
disk: 'ディスク',
network: 'ネットワーク',

View file

@ -101,6 +101,7 @@ const message = {
timeRange: '부터',
dateStart: '시작 날짜',
dateEnd: '종료 날짜',
date: '날짜',
},
table: {
all: '전체',
@ -1150,7 +1151,10 @@ const message = {
today: '오늘',
yesterday: '어제',
lastNDay: '최근 {0}',
lastNMonth: '최근 {0}개월',
lastHalfYear: '최근 반년',
memory: '메모리',
percent: '비율',
cache: '캐시',
disk: '디스크',
network: '네트워크',

View file

@ -101,6 +101,7 @@ const message = {
timeRange: 'Hingga',
dateStart: 'Tarikh mula',
dateEnd: 'Tarikh tamat',
date: 'Tarikh',
},
table: {
all: 'Semua',
@ -1190,7 +1191,10 @@ const message = {
today: 'Hari ini',
yesterday: 'Semalam',
lastNDay: '{0} hari terakhir',
lastNMonth: '{0} bulan terakhir',
lastHalfYear: 'Setengah tahun terakhir',
memory: 'Memori',
percent: 'Peratusan',
cache: 'Cache',
disk: 'Cakera',
network: 'Rangkaian',

View file

@ -101,6 +101,7 @@ const message = {
timeRange: 'Até',
dateStart: 'Data inicial',
dateEnd: 'Data final',
date: 'Data',
},
table: {
all: 'Todos',
@ -1181,8 +1182,11 @@ const message = {
readWriteTime: 'Latência de I/O',
today: 'Hoje',
yesterday: 'Ontem',
lastNDay: 'Últimos {0} dia(s)',
lastNDay: 'Últimos {0} dias',
lastNMonth: 'Últimos {0} meses',
lastHalfYear: 'Último semestre',
memory: 'Memória',
percent: 'Percentual',
cache: 'Cache',
disk: 'Disco',
network: 'Rede',

View file

@ -101,6 +101,7 @@ const message = {
timeRange: 'До',
dateStart: 'Дата начала',
dateEnd: 'Дата окончания',
date: 'Дата',
},
table: {
all: 'Все',
@ -1186,7 +1187,10 @@ const message = {
today: 'Сегодня',
yesterday: 'Вчера',
lastNDay: 'Последние {0} дней',
lastNMonth: 'Последние {0} месяцев',
lastHalfYear: 'Последние полгода',
memory: 'Память',
percent: 'Процент',
cache: 'Кэш',
disk: 'Диск',
network: 'Сеть',

View file

@ -104,6 +104,7 @@ const message = {
timeRange: 'İle',
dateStart: 'Başlangıç tarihi',
dateEnd: 'Bitiş tarihi',
date: 'Tarih',
},
table: {
all: 'Tümü',
@ -1209,8 +1210,9 @@ const message = {
yesterday: 'Dün',
lastNDay: 'Son {0} gün',
lastNMonth: 'Son {0} ay',
lastHalfYear: 'Son yarı yıl',
lastHalfYear: 'Son yarım yıl',
memory: 'Bellek',
percent: 'Yüzde',
cache: 'Önbellek',
disk: 'Disk',
network: '',

View file

@ -104,6 +104,7 @@ const message = {
timeRange: '至',
dateStart: '開始日期',
dateEnd: '結束日期',
date: '日期',
},
table: {
all: '所有',
@ -1134,6 +1135,7 @@ const message = {
lastNMonth: ' {0} ',
lastHalfYear: '近半年',
memory: '記憶體',
percent: '佔比',
cache: '快取',
disk: '磁碟',
network: '網路',

View file

@ -104,6 +104,7 @@ const message = {
timeRange: '至',
dateStart: '开始日期',
dateEnd: '结束日期',
date: '日期',
},
table: {
all: '所有',
@ -1136,6 +1137,7 @@ const message = {
lastNMonth: ' {0} ',
lastHalfYear: '近半年',
memory: '内存',
percent: '占比',
cache: '缓存',
disk: '磁盘',
network: '网络',

View file

@ -220,7 +220,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { loadMonitor, getNetworkOptions, getIOOptions } from '@/api/modules/host';
import { computeSizeFromKBs, dateFormatWithoutYear } from '@/utils/util';
import { computeSize, computeSizeFromKBs, dateFormat } from '@/utils/util';
import i18n from '@/lang';
import MonitorRouter from '@/views/host/monitor/index.vue';
import { GlobalStore } from '@/store';
@ -233,7 +233,6 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
const zoomStart = ref();
const monitorBase = ref();
const timeRangeGlobal = ref<[Date, Date]>([new Date(new Date().setHours(0, 0, 0, 0)), new Date()]);
const timeRangeLoad = ref<[Date, Date]>([new Date(new Date().setHours(0, 0, 0, 0)), new Date()]);
@ -304,41 +303,13 @@ const search = async (param: string) => {
case 'base':
let baseDate = item.date.length === 0 ? loadEmptyDate(timeRangeCpu.value) : item.date;
baseDate = baseDate.map(function (item: any) {
return dateFormatWithoutYear(item);
return dateFormat(null, null, item);
});
if (param === 'cpu' || param === 'all') {
let cpuData = item.value.map(function (item: any) {
return item.cpu.toFixed(2);
});
cpuData = cpuData.length === 0 ? loadEmptyData() : cpuData;
chartsOption.value['loadCPUChart'] = {
xData: baseDate,
yData: [
{
name: 'CPU',
data: cpuData,
},
],
formatStr: '%',
};
initCPUCharts(baseDate, item);
}
if (param === 'memory' || param === 'all') {
let memoryData = item.value.map(function (item: any) {
return item.memory.toFixed(2);
});
memoryData = memoryData.length === 0 ? loadEmptyData() : memoryData;
chartsOption.value['loadMemoryChart'] = {
xData: baseDate,
yData: [
{
name: i18n.global.t('monitor.memory'),
data: memoryData,
},
],
formatStr: '%',
};
initMemCharts(baseDate, item);
}
if (param === 'load' || param === 'all') {
initLoadCharts(item);
@ -348,38 +319,8 @@ const search = async (param: string) => {
initIOCharts(item);
break;
case 'network':
let networkDate = item.date.length === 0 ? loadEmptyDate(timeRangeNetwork.value) : item.date;
networkDate = networkDate.map(function (item: any) {
return dateFormatWithoutYear(item);
});
let networkUp = item.value.map(function (item: any) {
return item.up.toFixed(2);
});
networkUp = networkUp.length === 0 ? loadEmptyData() : networkUp;
let networkOut = item.value.map(function (item: any) {
return item.down.toFixed(2);
});
networkOut = networkOut.length === 0 ? loadEmptyData() : networkOut;
chartsOption.value['loadNetworkChart'] = {
xData: networkDate,
yData: [
{
name: i18n.global.t('monitor.up'),
data: networkUp,
},
{
name: i18n.global.t('monitor.down'),
data: networkOut,
},
],
grid: {
left: getSideWidth(true),
right: getSideWidth(true),
bottom: '20%',
},
formatStr: 'KB/s',
};
initNetCharts(item);
break;
}
}
};
@ -413,7 +354,7 @@ const loadIOOptions = async () => {
function initLoadCharts(item: Host.MonitorData) {
let itemLoadDate = item.date.length === 0 ? loadEmptyDate(timeRangeLoad.value) : item.date;
let loadDate = itemLoadDate.map(function (item: any) {
return dateFormatWithoutYear(item);
return dateFormat(null, null, item);
});
let load1Data = item.value.map(function (item: any) {
return item.cpuLoad1.toFixed(2);
@ -428,9 +369,9 @@ function initLoadCharts(item: Host.MonitorData) {
});
load15Data = load15Data.length === 0 ? loadEmptyData() : load15Data;
let loadUsage = item.value.map(function (item: any) {
return item.loadUsage.toFixed(2);
return { value: item.loadUsage.toFixed(2), top: item.topCPUItems, unit: '%' };
});
loadUsage = loadUsage.length === 0 ? loadEmptyData() : loadUsage;
loadUsage = loadUsage.length === 0 ? loadTopEmptyData() : loadUsage;
chartsOption.value['loadLoadChart'] = {
xData: loadDate,
yData: [
@ -465,26 +406,109 @@ function initLoadCharts(item: Host.MonitorData) {
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
let res = datas[0].name + '<br/>';
return withCPUProcess(datas);
},
},
};
}
function initCPUCharts(baseDate: any, items: Host.MonitorData) {
let data = items.value.map(function (item: any) {
return { value: item.cpu.toFixed(2), top: item.topCPUItems, unit: '%' };
});
data = data.length === 0 ? loadTopEmptyData() : data;
chartsOption.value['loadCPUChart'] = {
xData: baseDate,
yData: [
{
name: 'CPU',
data: data,
},
],
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
return withCPUProcess(datas);
},
},
formatStr: '%',
};
}
function initMemCharts(baseDate: any, items: Host.MonitorData) {
let data = items.value.map(function (item: any) {
return { value: item.memory.toFixed(2), top: item.topMemItems };
});
data = data.length === 0 ? loadTopEmptyData() : data;
chartsOption.value['loadMemoryChart'] = {
xData: baseDate,
yData: [
{
name: i18n.global.t('monitor.memory'),
data: data,
},
],
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
return withMemProcess(datas);
},
},
formatStr: '%',
};
}
function initNetCharts(item: Host.MonitorData) {
let networkDate = item.date.length === 0 ? loadEmptyDate(timeRangeNetwork.value) : item.date;
let date = networkDate.map(function (item: any) {
return dateFormat(null, null, item);
});
let networkUp = item.value.map(function (item: any) {
return item.up.toFixed(2);
});
networkUp = networkUp.length === 0 ? loadEmptyData() : networkUp;
let networkOut = item.value.map(function (item: any) {
return item.down.toFixed(2);
});
networkOut = networkOut.length === 0 ? loadEmptyData() : networkOut;
chartsOption.value['loadNetworkChart'] = {
xData: date,
yData: [
{
name: i18n.global.t('monitor.up'),
data: networkUp,
},
{
name: i18n.global.t('monitor.down'),
data: networkOut,
},
],
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
let res = loadDate(datas[0].name);
for (const item of datas) {
if (item.seriesName === i18n.global.t('monitor.resourceUsage')) {
res +=
item.marker + ' ' + item.seriesName + i18n.global.t('commons.colon') + item.data + '%<br/>';
} else {
res +=
item.marker + ' ' + item.seriesName + i18n.global.t('commons.colon') + item.data + '<br/>';
}
res += loadSeries(item, computeSizeFromKBs(item.data), '');
}
return res;
},
},
grid: {
left: getSideWidth(true),
right: getSideWidth(true),
bottom: '20%',
},
formatStr: 'KB/s',
};
}
function initIOCharts(item: Host.MonitorData) {
let itemIODate = item.date?.length === 0 ? loadEmptyDate(timeRangeIO.value) : item.date;
let ioDate = itemIODate.map(function (item: any) {
return dateFormatWithoutYear(item);
return dateFormat(null, null, item);
});
let ioRead = item.value.map(function (item: any) {
return Number((item.read / 1024).toFixed(2));
@ -527,41 +551,19 @@ function initIOCharts(item: Host.MonitorData) {
tooltip: {
trigger: 'axis',
formatter: function (datas: any) {
let res = datas[0].name + '<br/>';
let res = loadDate(datas[0].name);
for (const item of datas) {
if (
item.seriesName === i18n.global.t('monitor.read') ||
item.seriesName === i18n.global.t('monitor.write')
) {
res +=
item.marker +
' ' +
item.seriesName +
i18n.global.t('commons.colon') +
computeSizeFromKBs(item.data) +
'<br/>';
res += loadSeries(item, computeSizeFromKBs(item.data), '');
}
if (item.seriesName === i18n.global.t('monitor.readWriteCount')) {
res +=
item.marker +
' ' +
item.seriesName +
i18n.global.t('commons.colon') +
item.data +
' ' +
i18n.global.t('commons.units.time') +
'/s' +
'<br/>';
res += loadSeries(item, item.data, i18n.global.t('commons.units.time') + '/s');
}
if (item.seriesName === i18n.global.t('monitor.readWriteTime')) {
res +=
item.marker +
' ' +
item.seriesName +
i18n.global.t('commons.colon') +
item.data +
' ms' +
'<br/>';
res += loadSeries(item, item.data, ' ms');
}
}
return res;
@ -593,13 +595,122 @@ function loadEmptyDate(timeRange: any) {
function loadEmptyData() {
return [0, 0];
}
function loadTopEmptyData() {
return [{ value: 0, top: 0, unit: '' }];
}
function withCPUProcess(datas: any) {
let tops;
let res = loadDate(datas[0].name);
for (const item of datas) {
if (item.data?.top) {
tops = item.data?.top;
}
res += loadSeries(item, item.data.value ? item.data.value : item.data, item.data.unit || '');
}
if (!tops) {
return '';
}
res += `
<div style="margin-top: 10px; border-bottom: 1px dashed black;"></div>
<table style="border-collapse: collapse; margin-top: 20px; font-size: 12px;">
<thead>
<tr>
<th style="padding: 6px 8px;">PID</th>
<th style="padding: 6px 8px;">${i18n.global.t('commons.table.user')}</th>
<th style="padding: 6px 8px;">${i18n.global.t('menu.process')}</th>
<th style="padding: 6px 8px;">${i18n.global.t('monitor.percent')}</th>
</tr>
</thead>
<tbody>
`;
for (const row of tops) {
res += `
<tr>
<td style="padding: 6px 8px; text-align: center;">
${row.pid}
</td>
<td style="padding: 6px 8px; text-align: center;">
${row.user}
</td>
<td style="padding: 6px 8px; text-align: center;">
${row.name}
</td>
<td style="padding: 6px 8px; text-align: center;">
${row.percent.toFixed(2)}%
</td>
</tr>
`;
}
return res;
}
function withMemProcess(datas: any) {
let res = loadDate(datas[0].name);
for (const item of datas) {
res += loadSeries(item, item.data.value ? item.data.value : item.data, ' %');
}
if (!datas[0].data.top) {
return res;
}
res += `
<div style="margin-top: 10px; border-bottom: 1px dashed black;"></div>
<table style="border-collapse: collapse; margin-top: 20px; font-size: 12px;">
<thead>
<tr>
<th style="padding: 6px 8px;">PID</th>
<th style="padding: 6px 8px;">${i18n.global.t('commons.table.user')}</th>
<th style="padding: 6px 8px;">${i18n.global.t('menu.process')}</th>
<th style="padding: 6px 8px;">${i18n.global.t('monitor.memory')}</th>
<th style="padding: 6px 8px;">${i18n.global.t('monitor.percent')}</th>
</tr>
</thead>
<tbody>
`;
for (const item of datas) {
for (const row of item.data.top) {
res += `
<tr>
<td style="padding: 6px 8px; text-align: center;">
<span style="display: inline-block;"></span>
${row.pid}
</td>
<td style="padding: 6px 8px; text-align: center;">
${row.user}
</td>
<td style="padding: 6px 8px; text-align: center;">
${row.name}
</td>
<td style="padding: 6px 8px; text-align: center;">
${computeSize(row.memory)}
</td>
<td style="padding: 6px 8px; text-align: center;">
${row.percent.toFixed(2)}%
</td>
</tr>
`;
}
}
return res;
}
function loadDate(name: any) {
return ` <div style="display: inline-block; width: 100%; padding-bottom: 10px;">
${i18n.global.t('commons.search.date')}: ${name}
</div>`;
}
function loadSeries(item: any, data: any, unit: any) {
return `<div style="width: 100%;">
${item.marker} ${item.seriesName}: ${data} ${unit}
</div>`;
}
function getSideWidth(b: boolean) {
return !b || document.body.clientWidth > 1600 ? '7%' : '10%';
}
onMounted(() => {
zoomStart.value = dateFormatWithoutYear(new Date(new Date().setHours(0, 0, 0, 0)));
loadNetworkOptions();
loadIOOptions();
});