From 87420648b31374e7701b3d2ef23b5c593a7d8a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Mon, 12 Jan 2026 19:29:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=9B=91=E6=8E=A7=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/service/monitor.go | 103 ++- pkg/tools/tools.go | 2 +- pkg/types/monitor.go | 26 +- web/src/views/monitor/IndexView.vue | 1205 ++++++++++++++++++--------- 4 files changed, 896 insertions(+), 440 deletions(-) diff --git a/internal/service/monitor.go b/internal/service/monitor.go index db683b10..64030e3d 100644 --- a/internal/service/monitor.go +++ b/internal/service/monitor.go @@ -69,22 +69,46 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { return } if len(monitors) == 0 { - Success(w, types.MonitorData{}) + Success(w, types.MonitorDetail{}) return } - var list types.MonitorData - var bytesSent uint64 - var bytesRecv uint64 - var bytesSent2 uint64 - var bytesRecv2 uint64 + var list types.MonitorDetail + + // 用于存储每个网卡的累计流量 + netDeviceData := make(map[string]*types.Network) + netDevicePrev := make(map[string]struct { + sent uint64 + recv uint64 + }) + + // 用于存储每个磁盘的IO数据 + diskIOData := make(map[string]*types.DiskIO) + diskIOPrev := make(map[string]struct { + read uint64 + write uint64 + }) + + // 初始化第一条数据的网卡和磁盘数据 for _, net := range monitors[0].Info.Net { if net.Name == "lo" { continue } - bytesSent += net.BytesSent - bytesRecv += net.BytesRecv + netDevicePrev[net.Name] = struct { + sent uint64 + recv uint64 + }{sent: net.BytesSent, recv: net.BytesRecv} + netDeviceData[net.Name] = &types.Network{Name: net.Name} } + + for _, disk := range monitors[0].Info.DiskIO { + diskIOPrev[disk.Name] = struct { + read uint64 + write uint64 + }{read: disk.ReadBytes, write: disk.WriteBytes} + diskIOData[disk.Name] = &types.DiskIO{Name: disk.Name} + } + for i, monitor := range monitors { // 跳过第一条数据,因为第一条数据的流量为 0 if i == 0 { @@ -93,13 +117,55 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { list.SWAP.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024) continue } + + // 处理网络数据 for _, net := range monitor.Info.Net { if net.Name == "lo" { continue } - bytesSent2 += net.BytesSent - bytesRecv2 += net.BytesRecv + + // 按网卡分组 + if _, ok := netDeviceData[net.Name]; !ok { + netDeviceData[net.Name] = &types.Network{Name: net.Name} + netDevicePrev[net.Name] = struct { + sent uint64 + recv uint64 + }{sent: 0, recv: 0} + } + prev := netDevicePrev[net.Name] + device := netDeviceData[net.Name] + device.Sent = append(device.Sent, fmt.Sprintf("%.2f", float64(net.BytesSent)/1024/1024)) + device.Recv = append(device.Recv, fmt.Sprintf("%.2f", float64(net.BytesRecv)/1024/1024)) + device.Tx = append(device.Tx, fmt.Sprintf("%.2f", float64(net.BytesSent-prev.sent)/60/1024/1024)) + device.Rx = append(device.Rx, fmt.Sprintf("%.2f", float64(net.BytesRecv-prev.recv)/60/1024/1024)) + netDevicePrev[net.Name] = struct { + sent uint64 + recv uint64 + }{sent: net.BytesSent, recv: net.BytesRecv} } + + // 处理磁盘IO数据 + for _, disk := range monitor.Info.DiskIO { + if _, ok := diskIOData[disk.Name]; !ok { + diskIOData[disk.Name] = &types.DiskIO{Name: disk.Name} + diskIOPrev[disk.Name] = struct { + read uint64 + write uint64 + }{read: 0, write: 0} + } + prev := diskIOPrev[disk.Name] + diskData := diskIOData[disk.Name] + diskData.ReadBytes = append(diskData.ReadBytes, fmt.Sprintf("%.2f", float64(disk.ReadBytes)/1024/1024)) + diskData.WriteBytes = append(diskData.WriteBytes, fmt.Sprintf("%.2f", float64(disk.WriteBytes)/1024/1024)) + // 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的速度 (KB/s) + diskData.ReadSpeed = append(diskData.ReadSpeed, fmt.Sprintf("%.2f", float64(disk.ReadBytes-prev.read)/60/1024)) + diskData.WriteSpeed = append(diskData.WriteSpeed, fmt.Sprintf("%.2f", float64(disk.WriteBytes-prev.write)/60/1024)) + diskIOPrev[disk.Name] = struct { + read uint64 + write uint64 + }{read: disk.ReadBytes, write: disk.WriteBytes} + } + list.Times = append(list.Times, monitor.CreatedAt.Format(time.DateTime)) list.Load.Load1 = append(list.Load.Load1, monitor.Info.Load.Load1) list.Load.Load5 = append(list.Load.Load5, monitor.Info.Load.Load5) @@ -109,17 +175,14 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { list.Mem.Used = append(list.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024)) list.SWAP.Used = append(list.SWAP.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024)) list.SWAP.Free = append(list.SWAP.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024)) - list.Net.Sent = append(list.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024))) - list.Net.Recv = append(list.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024))) + } - // 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量 - list.Net.Tx = append(list.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024)) - list.Net.Rx = append(list.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024)) - - bytesSent = bytesSent2 - bytesRecv = bytesRecv2 - bytesSent2 = 0 - bytesRecv2 = 0 + // 将 map 转换为 slice + for _, device := range netDeviceData { + list.Net = append(list.Net, *device) + } + for _, disk := range diskIOData { + list.DiskIO = append(list.DiskIO, *disk) } Success(w, list) diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 024d4255..a8b8b41c 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -66,7 +66,7 @@ func CurrentInfo(nets, disks []string) types.CurrentInfo { } // 网络 if len(nets) == 0 { - netInfo, _ := net.IOCounters(false) + netInfo, _ := net.IOCounters(true) res.Net = netInfo } else { var netStats []net.IOCountersStat diff --git a/pkg/types/monitor.go b/pkg/types/monitor.go index d340ac85..07d7b110 100644 --- a/pkg/types/monitor.go +++ b/pkg/types/monitor.go @@ -22,18 +22,30 @@ type SWAP struct { Free []string `json:"free"` } +// Network 网卡数据 type Network struct { + Name string `json:"name"` Sent []string `json:"sent"` Recv []string `json:"recv"` Tx []string `json:"tx"` Rx []string `json:"rx"` } -type MonitorData struct { - Times []string `json:"times"` - Load Load `json:"load"` - CPU CPU `json:"cpu"` - Mem Mem `json:"mem"` - SWAP SWAP `json:"swap"` - Net Network `json:"net"` +// DiskIO 磁盘IO数据 +type DiskIO struct { + Name string `json:"name"` + ReadBytes []string `json:"read_bytes"` + WriteBytes []string `json:"write_bytes"` + ReadSpeed []string `json:"read_speed"` + WriteSpeed []string `json:"write_speed"` +} + +type MonitorDetail struct { + Times []string `json:"times"` + Load Load `json:"load"` + CPU CPU `json:"cpu"` + Mem Mem `json:"mem"` + SWAP SWAP `json:"swap"` + Net []Network `json:"net"` + DiskIO []DiskIO `json:"disk_io"` } diff --git a/web/src/views/monitor/IndexView.vue b/web/src/views/monitor/IndexView.vue index 7e584e44..33d4f49a 100644 --- a/web/src/views/monitor/IndexView.vue +++ b/web/src/views/monitor/IndexView.vue @@ -3,6 +3,7 @@ defineOptions({ name: 'monitor-index' }) +import type { EChartsOption } from 'echarts' import { LineChart } from 'echarts/charts' import { DataZoomComponent, @@ -13,7 +14,6 @@ import { } from 'echarts/components' import { use } from 'echarts/core' import { CanvasRenderer } from 'echarts/renderers' -import { NButton } from 'naive-ui' import VChart from 'vue-echarts' import { useGettext } from 'vue3-gettext' @@ -31,384 +31,521 @@ use([ DataZoomComponent ]) -const start = ref(Math.floor(new Date(new Date().setHours(0, 0, 0, 0)).getTime())) -const end = ref(Math.floor(Date.now())) +// 监控设置 +const monitorSwitch = ref(false) +const saveDay = ref(30) useRequest(monitor.setting()).onSuccess(({ data }) => { monitorSwitch.value = data.enabled saveDay.value = data.days }) -const { loading, data } = useWatcher(monitor.list(start.value, end.value), [start, end], { - initialData: { - times: [], - load: {}, - cpu: {}, - mem: {}, - swap: {}, - net: {} - }, - debounce: [500], - immediate: true +// 时间预设选项 +type TimePreset = 'yesterday' | 'today' | 'week' | 'custom' + +interface TimeRange { + start: number + end: number + preset: TimePreset + customRange: [number, number] | null +} + +// 获取时间范围 +function getTimeRange( + preset: TimePreset, + customRange?: [number, number] +): { start: number; end: number } { + const now = new Date() + const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime() + + switch (preset) { + case 'yesterday': { + const yesterdayStart = todayStart - 24 * 60 * 60 * 1000 + return { start: yesterdayStart, end: todayStart } + } + case 'today': + return { start: todayStart, end: Date.now() } + case 'week': { + const weekStart = todayStart - 7 * 24 * 60 * 60 * 1000 + return { start: weekStart, end: Date.now() } + } + case 'custom': + if (customRange) { + return { start: customRange[0], end: customRange[1] } + } + return { start: todayStart, end: Date.now() } + default: + return { start: todayStart, end: Date.now() } + } +} + +// 各图表的时间范围 +const loadTime = ref({ start: 0, end: 0, preset: 'today', customRange: null }) +const cpuTime = ref({ start: 0, end: 0, preset: 'today', customRange: null }) +const memTime = ref({ start: 0, end: 0, preset: 'today', customRange: null }) +const netTime = ref({ start: 0, end: 0, preset: 'today', customRange: null }) +const diskIOTime = ref({ start: 0, end: 0, preset: 'today', customRange: null }) + +// 初始化时间范围 +function initTimeRanges() { + const todayRange = getTimeRange('today') + loadTime.value = { ...todayRange, preset: 'today', customRange: null } + cpuTime.value = { ...todayRange, preset: 'today', customRange: null } + memTime.value = { ...todayRange, preset: 'today', customRange: null } + netTime.value = { ...todayRange, preset: 'today', customRange: null } + diskIOTime.value = { ...todayRange, preset: 'today', customRange: null } +} +initTimeRanges() + +// 更新时间范围 +function updateTimeRange( + key: 'load' | 'cpu' | 'mem' | 'net' | 'diskIO', + preset: TimePreset, + customRange?: [number, number] +) { + const range = getTimeRange(preset, customRange) + const newValue = { ...range, preset, customRange: customRange || null } + + switch (key) { + case 'load': + loadTime.value = newValue + break + case 'cpu': + cpuTime.value = newValue + break + case 'mem': + memTime.value = newValue + break + case 'net': + netTime.value = newValue + break + case 'diskIO': + diskIOTime.value = newValue + break + } +} + +// 自定义时间 popover 状态 +const loadCustomPopover = ref(false) +const cpuCustomPopover = ref(false) +const memCustomPopover = ref(false) +const netCustomPopover = ref(false) +const diskIOCustomPopover = ref(false) + +// 临时时间范围 +const loadTempRange = ref<[number, number] | null>(null) +const cpuTempRange = ref<[number, number] | null>(null) +const memTempRange = ref<[number, number] | null>(null) +const netTempRange = ref<[number, number] | null>(null) +const diskIOTempRange = ref<[number, number] | null>(null) + +function confirmCustomTime(key: 'load' | 'cpu' | 'mem' | 'net' | 'diskIO') { + switch (key) { + case 'load': + if (loadTempRange.value) { + updateTimeRange('load', 'custom', loadTempRange.value) + } + loadCustomPopover.value = false + break + case 'cpu': + if (cpuTempRange.value) { + updateTimeRange('cpu', 'custom', cpuTempRange.value) + } + cpuCustomPopover.value = false + break + case 'mem': + if (memTempRange.value) { + updateTimeRange('mem', 'custom', memTempRange.value) + } + memCustomPopover.value = false + break + case 'net': + if (netTempRange.value) { + updateTimeRange('net', 'custom', netTempRange.value) + } + netCustomPopover.value = false + break + case 'diskIO': + if (diskIOTempRange.value) { + updateTimeRange('diskIO', 'custom', diskIOTempRange.value) + } + diskIOCustomPopover.value = false + break + } +} + +// 数据请求 +interface MonitorData { + times: string[] + load: { load1: number[]; load5: number[]; load15: number[] } + cpu: { percent: string[] } + mem: { total: string; available: string[]; used: string[] } + swap: { total: string; used: string[]; free: string[] } + net: Array<{ name: string; sent: string[]; recv: string[]; tx: string[]; rx: string[] }> + disk_io: Array<{ + name: string + read_bytes: string[] + write_bytes: string[] + read_speed: string[] + write_speed: string[] + }> +} + +const emptyData: MonitorData = { + times: [], + load: { load1: [], load5: [], load15: [] }, + cpu: { percent: [] }, + mem: { total: '0', available: [], used: [] }, + swap: { total: '0', used: [], free: [] }, + net: [], + disk_io: [] +} + +// 各图表数据 +const { loading: loadLoading, data: loadData } = useWatcher( + () => monitor.list(loadTime.value.start, loadTime.value.end), + [loadTime], + { initialData: emptyData, debounce: [300], immediate: true } +) + +const { loading: cpuLoading, data: cpuData } = useWatcher( + () => monitor.list(cpuTime.value.start, cpuTime.value.end), + [cpuTime], + { initialData: emptyData, debounce: [300], immediate: true } +) + +const { loading: memLoading, data: memData } = useWatcher( + () => monitor.list(memTime.value.start, memTime.value.end), + [memTime], + { initialData: emptyData, debounce: [300], immediate: true } +) + +const { loading: netLoading, data: netData } = useWatcher( + () => monitor.list(netTime.value.start, netTime.value.end), + [netTime], + { initialData: emptyData, debounce: [300], immediate: true } +) + +const { loading: diskIOLoading, data: diskIOData } = useWatcher( + () => monitor.list(diskIOTime.value.start, diskIOTime.value.end), + [diskIOTime], + { initialData: emptyData, debounce: [300], immediate: true } +) + +// 网卡和磁盘筛选 +const selectedNetDevices = ref([]) +const selectedDisks = ref([]) + +// 可用的网卡和磁盘列表 +const availableNetDevices = computed(() => { + return netData.value?.net?.map((d: { name: string }) => d.name) || [] }) -const monitorSwitch = ref(false) -const saveDay = ref(30) +const availableDisks = computed(() => { + return diskIOData.value?.disk_io?.map((d: { name: string }) => d.name) || [] +}) -const load = ref({ - title: { - text: $gettext('Load'), - textAlign: 'left', - textStyle: { - fontSize: 20 - } - }, - tooltip: { - trigger: 'axis' - }, - legend: { - align: 'left', - data: [$gettext('1 minute'), $gettext('5 minutes'), $gettext('15 minutes')] - }, - xAxis: [{ type: 'category', boundaryGap: false, data: data.value.times }], - yAxis: [ - { - type: 'value' - } - ], - dataZoom: { - show: true, - realtime: true, - start: 0, - end: 100 - }, - series: [ - { - name: $gettext('1 minute'), - type: 'line', - smooth: true, - data: data.value.load.load1, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] +// 当数据加载完成后,默认选中所有设备 +watch(availableNetDevices, (devices) => { + if (devices.length > 0 && selectedNetDevices.value.length === 0) { + selectedNetDevices.value = [...devices] + } +}) + +watch(availableDisks, (disks) => { + if (disks.length > 0 && selectedDisks.value.length === 0) { + selectedDisks.value = [...disks] + } +}) + +// 格式化字节速率 (KB/s -> MB/s -> GB/s) +function formatSpeed(value: number | string): string { + const num = typeof value === 'string' ? parseFloat(value) : value + if (isNaN(num)) return '0 KB/s' + if (num >= 1024 * 1024) { + return `${(num / 1024 / 1024).toFixed(2)} GB/s` + } else if (num >= 1024) { + return `${(num / 1024).toFixed(2)} MB/s` + } + return `${num.toFixed(2)} KB/s` +} + +// 格式化内存大小 (MB -> GB) +function formatMemory(value: number | string): string { + const num = typeof value === 'string' ? parseFloat(value) : value + if (isNaN(num)) return '0 MB' + if (num >= 1024) { + return `${(num / 1024).toFixed(2)} GB` + } + return `${num.toFixed(2)} MB` +} + +// 基础图表配置 +function createBaseOption(valueFormatter?: any) { + const xAxisConfig = { + type: 'category' as const, + boundaryGap: false, + data: [] as string[], + axisLabel: { + formatter: (value: string) => { + // 只显示时间部分 + return value.split(' ')[1] || value } + } + } + return { + tooltip: { + trigger: 'axis' as const, + valueFormatter: valueFormatter }, - { - name: $gettext('5 minutes'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.load.load5, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } + legend: { + type: 'scroll' as const, + left: 20, + top: 0 }, - { - name: $gettext('15 minutes'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.load.load15, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } - } - ] -}) - -const cpu = ref({ - title: { - text: 'CPU', - textAlign: 'left', - textStyle: { - fontSize: 20 - } - }, - tooltip: { - trigger: 'axis' - }, - xAxis: [{ type: 'category', boundaryGap: false, data: data.value.times }], - yAxis: [ - { - name: $gettext('Unit %'), - min: 0, - max: 100, - type: 'value', - axisLabel: { - formatter: '{value} %' - } - } - ], - dataZoom: { - show: true, - realtime: true, - start: 0, - end: 100 - }, - series: [ - { - name: $gettext('Usage'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.cpu.percent, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } - } - ] -}) - -const mem = ref({ - title: { - text: $gettext('Memory'), - textAlign: 'left', - textStyle: { - fontSize: 20 - } - }, - tooltip: { - trigger: 'axis' - }, - legend: { - align: 'left', - data: [$gettext('Memory'), 'Swap'] - }, - xAxis: [{ type: 'category', boundaryGap: false, data: data.value.times }], - yAxis: [ - { - name: $gettext('Unit MB'), - min: 0, - max: data.value.mem.total, - type: 'value', - axisLabel: { - formatter: '{value} M' - } - } - ], - dataZoom: { - show: true, - realtime: true, - start: 0, - end: 100 - }, - series: [ - { - name: $gettext('Memory'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.mem.used, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } + grid: { + left: 60, + right: 20, + top: 50, + bottom: 60 }, - { - name: 'Swap', - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' + xAxis: xAxisConfig, + yAxis: [ + { + type: 'value' as const + } + ], + dataZoom: { + type: 'slider' as const, + show: true, + realtime: true, + start: 0, + end: 100, + bottom: 10 + } + } +} + +// 负载图表配置 +const loadOption = computed(() => { + const base = createBaseOption() + return { + ...base, + xAxis: { ...base.xAxis, data: loadData.value?.times || [] }, + series: [ + { + name: $gettext('1 minute'), + type: 'line', + smooth: true, + data: loadData.value?.load?.load1 || [], + markPoint: { + data: [ + { type: 'max', name: $gettext('Maximum') }, + { type: 'min', name: $gettext('Minimum') } + ] + }, + markLine: { + data: [{ type: 'average', name: $gettext('Average') }] } }, - data: data.value.swap.used, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] + { + name: $gettext('5 minutes'), + type: 'line', + smooth: true, + data: loadData.value?.load?.load5 || [] }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] + { + name: $gettext('15 minutes'), + type: 'line', + smooth: true, + data: loadData.value?.load?.load15 || [] } - } - ] -}) - -const net = ref({ - title: { - text: $gettext('Network'), - textAlign: 'left', - textStyle: { - fontSize: 20 - } - }, - tooltip: { - trigger: 'axis' - }, - legend: { - align: 'left', - data: [ - $gettext('Total Out'), - $gettext('Total In'), - $gettext('Per Second Out'), - $gettext('Per Second In') ] - }, - xAxis: [{ type: 'category', boundaryGap: false, data: data.value.times }], - yAxis: [ - { - name: $gettext('Unit MB'), - type: 'value', - axisLabel: { - formatter: '{value} MB' - } - } - ], - dataZoom: { - show: true, - realtime: true, - start: 0, - end: 100 - }, - series: [ - { - name: $gettext('Total Out'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.net.sent, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } - }, - { - name: $gettext('Total In'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.net.recv, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } - }, - { - name: $gettext('Per Second Out'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.net.tx, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } - }, - { - name: $gettext('Per Second In'), - type: 'line', - smooth: true, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - }, - data: data.value.net.rx, - markPoint: { - data: [ - { type: 'max', name: $gettext('Maximum') }, - { type: 'min', name: $gettext('Minimum') } - ] - }, - markLine: { - data: [{ type: 'average', name: $gettext('Average') }] - } - } - ] + } }) +// CPU图表配置 +const cpuOption = computed(() => { + const base = createBaseOption((value: any) => `${value}%`) + return { + ...base, + xAxis: { ...base.xAxis, data: cpuData.value?.times || [] }, + yAxis: [ + { + type: 'value', + name: $gettext('Usage %'), + min: 0, + max: 100, + axisLabel: { + formatter: '{value}%' + } + } + ], + series: [ + { + name: $gettext('Usage'), + type: 'line', + smooth: true, + areaStyle: { + opacity: 0.3 + }, + data: cpuData.value?.cpu?.percent || [], + markPoint: { + data: [ + { type: 'max', name: $gettext('Maximum') }, + { type: 'min', name: $gettext('Minimum') } + ] + }, + markLine: { + data: [{ type: 'average', name: $gettext('Average') }] + } + } + ] + } +}) + +// 内存图表配置 +const memOption = computed(() => { + const base = createBaseOption(formatMemory) + const total = parseFloat(memData.value?.mem?.total || '0') + return { + ...base, + legend: { + ...base.legend, + data: [$gettext('Memory'), 'Swap'] + }, + xAxis: { ...base.xAxis, data: memData.value?.times || [] }, + yAxis: [ + { + type: 'value', + name: $gettext('Unit MB'), + min: 0, + max: total > 0 ? total : undefined, + axisLabel: { + formatter: '{value} M' + } + } + ], + series: [ + { + name: $gettext('Memory'), + type: 'line', + smooth: true, + areaStyle: { + opacity: 0.3 + }, + data: memData.value?.mem?.used || [], + markPoint: { + data: [ + { type: 'max', name: $gettext('Maximum') }, + { type: 'min', name: $gettext('Minimum') } + ] + }, + markLine: { + data: [{ type: 'average', name: $gettext('Average') }] + } + }, + { + name: 'Swap', + type: 'line', + smooth: true, + data: memData.value?.swap?.used || [] + } + ] + } +}) + +// 网络图表配置 +const netOption = computed(() => { + const base = createBaseOption(formatSpeed) + + // 根据选中的网卡筛选数据 + const devices = + netData.value?.net?.filter((d: { name: string }) => + selectedNetDevices.value.includes(d.name) + ) || [] + + const series: any[] = [] + devices.forEach((device: { name: string; tx: string[]; rx: string[] }) => { + series.push({ + name: `${device.name} ${$gettext('Upload')}`, + type: 'line', + smooth: true, + data: device.tx + }) + series.push({ + name: `${device.name} ${$gettext('Download')}`, + type: 'line', + smooth: true, + data: device.rx + }) + }) + + return { + ...base, + xAxis: { ...base.xAxis, data: netData.value?.times || [] }, + yAxis: [ + { + type: 'value', + name: 'KB/s', + axisLabel: { + formatter: '{value}' + } + } + ], + series + } +}) + +// 磁盘IO图表配置 +const diskIOOption = computed(() => { + const base = createBaseOption(formatSpeed) + + // 根据选中的磁盘筛选数据 + const disks = + diskIOData.value?.disk_io?.filter((d: { name: string }) => + selectedDisks.value.includes(d.name) + ) || [] + + const series: any[] = [] + disks.forEach((disk: { name: string; read_speed: string[]; write_speed: string[] }) => { + series.push({ + name: `${disk.name} ${$gettext('Read')}`, + type: 'line', + smooth: true, + areaStyle: { + opacity: 0.3 + }, + data: disk.read_speed + }) + series.push({ + name: `${disk.name} ${$gettext('Write')}`, + type: 'line', + smooth: true, + areaStyle: { + opacity: 0.3 + }, + data: disk.write_speed + }) + }) + + return { + ...base, + xAxis: { ...base.xAxis, data: diskIOData.value?.times || [] }, + yAxis: [ + { + type: 'value', + name: 'KB/s', + axisLabel: { + formatter: '{value}' + } + } + ], + series + } +}) + +// 操作函数 const handleUpdate = async () => { useRequest(monitor.updateSetting(monitorSwitch.value, saveDay.value)).onSuccess(() => { window.$message.success($gettext('Operation successful')) @@ -420,39 +557,20 @@ const handleClear = async () => { window.$message.success($gettext('Operation successful')) }) } - -// 监听 data 的变化 -watch(data, () => { - load.value.xAxis[0].data = data.value.times - load.value.series[0].data = data.value.load.load1 - load.value.series[1].data = data.value.load.load5 - load.value.series[2].data = data.value.load.load15 - cpu.value.xAxis[0].data = data.value.times - cpu.value.series[0].data = data.value.cpu.percent - mem.value.xAxis[0].data = data.value.times - mem.value.yAxis[0].max = data.value.mem.total - mem.value.series[0].data = data.value.mem.used - mem.value.series[1].data = data.value.swap.used - net.value.xAxis[0].data = data.value.times - net.value.series[0].data = data.value.net.sent - net.value.series[1].data = data.value.net.recv - net.value.series[2].data = data.value.net.tx - net.value.series[3].data = data.value.net.rx -})