From 151dd4d5d56c25b8964c554f6170ad34703b7bf3 Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Tue, 19 Dec 2023 18:19:43 +0800 Subject: [PATCH] Add statistic route --- src/pages/system/Statistic.tsx | 956 +++++++++++++++++++++++++++++++++ src/pages/system/index.tsx | 953 +------------------------------- src/router/system.tsx | 12 +- 3 files changed, 969 insertions(+), 952 deletions(-) create mode 100644 src/pages/system/Statistic.tsx diff --git a/src/pages/system/Statistic.tsx b/src/pages/system/Statistic.tsx new file mode 100644 index 0000000..54c4b90 --- /dev/null +++ b/src/pages/system/Statistic.tsx @@ -0,0 +1,956 @@ +import React, { useEffect, useState } from 'react' +import Icon from '@ant-design/icons' +import * as echarts from 'echarts/core' +import { + TooltipComponent, + TooltipComponentOption, + GridComponent, + GridComponentOption, + ToolboxComponentOption, + DataZoomComponentOption, + ToolboxComponent, + DataZoomComponent +} from 'echarts/components' +import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts' +import { UniversalTransition } from 'echarts/features' +import { SVGRenderer } from 'echarts/renderers' +import { TopLevelFormatterParams } from 'echarts/types/dist/shared' +import '@/assets/css/pages/system/index.scss' +import { useUpdatedEffect } from '@/util/hooks' +import { formatByteSize } from '@/util/common' +import { getTimesBetweenTwoTimes, utcToLocalTime } from '@/util/datetime' +import { + r_sys_statistic_active, + r_sys_statistic_cpu, + r_sys_statistic_hardware, + r_sys_statistic_online, + r_sys_statistic_software, + r_sys_statistic_storage +} from '@/services/system' +import Card from '@/components/common/Card' +import FlexBox from '@/components/common/FlexBox' +import FitFullScreen from '@/components/common/FitFullScreen' +import HideScrollbar from '@/components/common/HideScrollbar' +import LoadingMask from '@/components/common/LoadingMask' + +echarts.use([ + TooltipComponent, + ToolboxComponent, + GridComponent, + DataZoomComponent, + BarChart, + LineChart, + SVGRenderer, + UniversalTransition +]) +type EChartsOption = echarts.ComposeOption< + | TooltipComponentOption + | ToolboxComponentOption + | GridComponentOption + | BarSeriesOption + | DataZoomComponentOption + | LineSeriesOption +> + +const barDefaultSeriesOption: BarSeriesOption = { + type: 'bar', + stack: 'total', + itemStyle: { + color: (params) => { + switch (params.seriesName) { + case 'idle': + case 'free': + return '#F5F5F5' + default: + return params.color ?? echarts.color.random() + } + } + } +} + +const barEChartsBaseOption: EChartsOption = { + tooltip: {}, + xAxis: { + show: false + }, + yAxis: { + axisLine: { + show: false + }, + axisLabel: { + show: false + }, + axisTick: { + show: false + }, + splitLine: { + show: false + }, + axisPointer: { + show: false + } + } +} + +const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => { + return (params: TopLevelFormatterParams) => + `${utcToLocalTime( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + params[0].data[0], + format + )}
${ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + params[0]['marker'] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + }${params[0]['seriesName']}${ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + params[0].data[1] + } ` +} + +const lineEChartsBaseOption: EChartsOption = { + tooltip: { + trigger: 'axis' + }, + toolbox: { + feature: { + dataZoom: { + yAxisIndex: 'none' + }, + restore: {}, + saveAsImage: {} + } + }, + xAxis: { + type: 'time' + }, + yAxis: { + type: 'value', + interval: 1 + }, + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100, + minValueSpan: 2 * 60 * 60 * 1000 + } + ], + series: [{}] +} + +interface CommonCardProps extends React.PropsWithChildren { + icon: IconComponent + title: React.ReactNode + loading?: boolean + expand?: React.ReactNode +} + +const CommonCard: React.FC = (props) => { + return ( + + + + +
{props.title}
+ {props.expand} +
+ +
+
+ ) +} + +const OnlineInfo: React.FC = () => { + const onlineInfoDivRef = useRef(null) + const onlineInfoEChartsRef = useRef(null) + const [isLoading, setIsLoading] = useState(false) + const [currentOnlineCount, setCurrentOnlineCount] = useState(-1) + const [scope, setScope] = useState('WEAK') + + useUpdatedEffect(() => { + const chartResizeObserver = new ResizeObserver(() => { + onlineInfoEChartsRef.current?.resize() + }) + + onlineInfoDivRef.current && chartResizeObserver.observe(onlineInfoDivRef.current) + + return () => { + onlineInfoDivRef.current && chartResizeObserver.unobserve(onlineInfoDivRef.current) + } + }, [isLoading]) + + useUpdatedEffect(() => { + getOnlineInfo() + }, []) + + const handleOnScopeChange = (value: string) => { + setScope(value) + getOnlineInfo(value) + } + + const handleOnRefresh = () => { + getOnlineInfo() + } + + const getOnlineInfo = (_scope: string = scope) => { + if (isLoading) { + return + } + + setIsLoading(true) + + void r_sys_statistic_online({ scope: _scope }).then((res) => { + const response = res.data + if (response.success) { + const data = response.data + if (data) { + setIsLoading(false) + + setCurrentOnlineCount(data.current) + + setTimeout(() => { + const dataList = getTimesBetweenTwoTimes( + data.history[0].time, + data.history[data.history.length - 1].time, + 'minute' + ).map((time) => [ + time, + data.history.find( + (value) => value.time.substring(0, 16) === time.substring(0, 16) + )?.record ?? 0 + ]) + + onlineInfoEChartsRef.current = echarts.init( + onlineInfoDivRef.current, + null, + { renderer: 'svg' } + ) + + onlineInfoEChartsRef.current?.setOption({ + ...lineEChartsBaseOption, + tooltip: { + ...lineEChartsBaseOption.tooltip, + formatter: getTooltipTimeFormatter('yyyy-MM-DD HH:mm') + }, + xAxis: { + ...lineEChartsBaseOption.xAxis + }, + series: [ + { + name: '在线人数', + type: 'line', + smooth: true, + symbol: 'none', + areaStyle: {}, + data: dataList + } + ] + }) + }) + } + } + }) + } + + return ( + + + 在线用户 + + 当前 {currentOnlineCount === -1 ? '获取中...' : currentOnlineCount} + + + + } + loading={isLoading} + expand={ + <> + + 今天 + 最近7天 + 最近30天 + 最近3个月 + 最近12个月 + 最近2年 + 最近3年 + 最近5年 + 全部 + + + + + + } + > + +
+ + + ) +} + +const ActiveInfo: React.FC = () => { + const activeInfoDivRef = useRef(null) + const activeInfoEChartsRef = useRef(null) + const [isLoading, setIsLoading] = useState(false) + const [scope, setScope] = useState('WEAK') + + useUpdatedEffect(() => { + const chartResizeObserver = new ResizeObserver(() => { + activeInfoEChartsRef.current?.resize() + }) + + activeInfoDivRef.current && chartResizeObserver.observe(activeInfoDivRef.current) + + return () => { + activeInfoDivRef.current && chartResizeObserver.unobserve(activeInfoDivRef.current) + } + }, [isLoading]) + + useUpdatedEffect(() => { + getActiveInfo() + }, []) + + const handleOnScopeChange = (value: string) => { + setScope(value) + getActiveInfo(value) + } + + const handleOnRefresh = () => { + getActiveInfo() + } + + const getActiveInfo = (_scope: string = scope) => { + if (isLoading) { + return + } + + setIsLoading(true) + + void r_sys_statistic_active({ scope: _scope }).then((res) => { + const response = res.data + if (response.success) { + const data = response.data + if (data) { + setIsLoading(false) + + setTimeout(() => { + const registerList = data.registerHistory.length + ? getTimesBetweenTwoTimes( + data.registerHistory[0].time, + data.registerHistory[data.registerHistory.length - 1].time, + 'day' + ).map((time) => [ + time, + data.registerHistory.find( + (value) => + value.time.substring(0, 10) === time.substring(0, 10) + )?.count ?? 0 + ]) + : [] + const loginList = data.loginHistory.length + ? getTimesBetweenTwoTimes( + data.loginHistory[0].time, + data.loginHistory[data.loginHistory.length - 1].time, + 'day' + ).map((time) => [ + time, + data.loginHistory.find( + (value) => + value.time.substring(0, 10) === time.substring(0, 10) + )?.count ?? 0 + ]) + : [] + + activeInfoEChartsRef.current = echarts.init( + activeInfoDivRef.current, + null, + { renderer: 'svg' } + ) + + activeInfoEChartsRef.current?.setOption({ + ...lineEChartsBaseOption, + tooltip: { + ...lineEChartsBaseOption.tooltip, + formatter: getTooltipTimeFormatter('yyyy-MM-DD') + }, + dataZoom: [ + { + type: 'inside', + start: 0, + end: 100, + minValueSpan: 2 * 24 * 60 * 60 * 1000 + } + ], + series: [ + { + name: '注册人数', + type: 'line', + smooth: true, + symbol: 'none', + areaStyle: {}, + data: registerList + }, + { + name: '登录人数', + type: 'line', + smooth: true, + symbol: 'none', + areaStyle: {}, + data: loginList + } + ] + }) + }) + } + } + }) + } + + return ( + + + 用户活跃 + + + } + loading={isLoading} + expand={ + <> + + 最近7天 + 最近30天 + 最近3个月 + 最近12个月 + 最近2年 + 最近3年 + 最近5年 + 全部 + + + + + + } + > + +
+ + + ) +} + +const SoftwareInfo: React.FC = () => { + const [softwareInfoData, setSoftwareInfoData] = useState() + + useUpdatedEffect(() => { + void r_sys_statistic_software().then((res) => { + const response = res.data + if (response.success) { + response.data && setSoftwareInfoData(response.data) + } else { + void message.error('获取软件信息失败,请稍后重试') + } + }) + }, []) + + return ( + + + +
操作系统
+
位数
+
Java
+
Java 供应商
+
Runtime
+
JVM
+
JVM 供应商
+
操作系统启动时间
+
后端服务器启动时间
+
+ +
{softwareInfoData?.os}
+
+ {softwareInfoData?.bitness} +
+
{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
+
{softwareInfoData?.javaVendor}
+
{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
+
{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
+
{softwareInfoData?.jvmVendor}
+
+ {softwareInfoData?.osBootTime && + utcToLocalTime(softwareInfoData?.osBootTime)} +
+
+ {softwareInfoData?.serverStartupTime && + utcToLocalTime(softwareInfoData.serverStartupTime)} +
+
+
+
+ ) +} + +const HardwareInfo: React.FC = () => { + const [hardwareInfoData, setHardwareInfoData] = useState() + + useUpdatedEffect(() => { + void r_sys_statistic_hardware().then((res) => { + const response = res.data + if (response.success) { + response.data && setHardwareInfoData(response.data) + } else { + void message.error('获取硬件信息失败,请稍后重试') + } + }) + }, []) + + return ( + + + +
CPU
+
CPU 架构
+
微架构
+
64位
+
物理 CPU
+
物理核心
+
逻辑核心
+
内存
+
磁盘
+
+ +
{hardwareInfoData?.cpu}
+
{hardwareInfoData?.arch}
+
+ {hardwareInfoData?.microarchitecture} +
+
+ {hardwareInfoData?.is64Bit ? '是' : '否'} +
+
+ {hardwareInfoData?.cpuPhysicalPackageCount} +
+
+ {hardwareInfoData?.cpuPhysicalProcessorCount} +
+
+ {hardwareInfoData?.cpuLogicalProcessorCount} +
+
{hardwareInfoData?.memories}
+
{hardwareInfoData?.disks}
+
+
+
+ ) +} + +const CPUInfo: React.FC = () => { + const keyDivRef = useRef(null) + const percentDivRef = useRef(null) + const cpuInfoDivRef = useRef(null) + const cpuInfoEChartsRef = useRef([]) + const [isLoading, setIsLoading] = useState(true) + const [refreshInterval, setRefreshInterval] = useState('5') + const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState([]) + + const cpuDefaultSeriesOption: BarSeriesOption = { + ...barDefaultSeriesOption, + tooltip: { + valueFormatter: (value) => `${((value as number) * 100).toFixed(2)}%` + } + } + + useUpdatedEffect(() => { + const chartResizeObserver = new ResizeObserver(() => { + cpuInfoEChartsRef.current.forEach((value) => value.resize()) + }) + + cpuInfoDivRef.current && chartResizeObserver.observe(cpuInfoDivRef.current) + + return () => { + cpuInfoDivRef.current && chartResizeObserver.unobserve(cpuInfoDivRef.current) + } + }, [cpuInfoDivRef.current]) + + useUpdatedEffect(() => { + const intervalId = setInterval(getCpuInfo(), parseInt(refreshInterval) * 1000) + + return () => { + clearInterval(intervalId) + } + }, [refreshInterval]) + + const getCpuInfo = () => { + void r_sys_statistic_cpu().then((res) => { + const response = res.data + if (response.success) { + const data = response.data + if (data) { + if (isLoading) { + setIsLoading(false) + } + + setTimeout(() => { + const dataList = data.processors.map((value) => + cpuInfoVoToCpuInfoData(value) + ) + dataList.unshift(cpuInfoVoToCpuInfoData(data)) + + setCpuInfoEChartsOption( + dataList.map((value, index) => ({ + ...barEChartsBaseOption, + yAxis: { + ...barEChartsBaseOption.yAxis, + data: [index === 0 ? '总占用' : `CPU ${index - 1}`] + }, + series: value + })) + ) + + if (percentDivRef.current) { + percentDivRef.current.innerHTML = '' + dataList.forEach((value) => { + const percentElement = document.createElement('div') + const idle = value.find((item) => item.name === 'idle')?.data[0] + percentElement.innerText = + idle !== undefined + ? `${((1 - idle) * 100).toFixed(2)}%` + : 'Unknown' + percentDivRef.current?.appendChild(percentElement) + }) + } + + if (!cpuInfoEChartsRef.current.length) { + keyDivRef.current && (keyDivRef.current.innerHTML = '') + cpuInfoDivRef.current && (cpuInfoDivRef.current.innerHTML = '') + for (let i = 0; i < dataList.length; i++) { + const keyElement = document.createElement('div') + keyElement.innerText = i === 0 ? '总占用' : `CPU ${i - 1}` + keyDivRef.current?.appendChild(keyElement) + + const valueElement = document.createElement('div') + cpuInfoDivRef.current?.appendChild(valueElement) + cpuInfoEChartsRef.current.push( + echarts.init(valueElement, null, { renderer: 'svg' }) + ) + } + } + }) + } + } + }) + + return getCpuInfo + } + + const cpuInfoVoToCpuInfoData = (cpuInfoVo: CpuInfoVo) => + Object.entries(cpuInfoVo) + .filter(([key]) => !['total', 'processors'].includes(key)) + .map(([key, value]) => ({ + ...cpuDefaultSeriesOption, + name: key, + data: [(value as number) / cpuInfoVo.total] + })) + .sort((a, b) => { + const order = [ + 'steal', + 'irq', + 'softirq', + 'iowait', + 'system', + 'nice', + 'user', + 'idle' + ] + return order.indexOf(a.name) - order.indexOf(b.name) + }) + + useEffect(() => { + cpuInfoEChartsRef.current?.forEach((value, index) => { + try { + value.setOption(cpuInfoEChartsOption[index]) + } catch (e) { + /* empty */ + } + }) + }, [cpuInfoEChartsOption]) + + return ( + <> + setRefreshInterval(value)} + > + 1秒 + 2秒 + 3秒 + 5秒 + 10秒 + 15秒 + 20秒 + 30秒 + 60秒 + 2分 + 3分 + 5分 + 10分 + + } + > + + + + + + + + ) +} + +const StorageInfo: React.FC = () => { + const keyDivRef = useRef(null) + const percentDivRef = useRef(null) + const storageInfoDivRef = useRef(null) + const storageInfoEChartsRef = useRef([]) + const [isLoading, setIsLoading] = useState(true) + const [refreshInterval, setRefreshInterval] = useState('5') + const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState([]) + + const storageDefaultSeriesOption: BarSeriesOption = { + ...barDefaultSeriesOption, + tooltip: { valueFormatter: (value) => formatByteSize(value as number) } + } + + useUpdatedEffect(() => { + const chartResizeObserver = new ResizeObserver(() => { + storageInfoEChartsRef.current.forEach((value) => value.resize()) + }) + + storageInfoDivRef.current && chartResizeObserver.observe(storageInfoDivRef.current) + + return () => { + storageInfoDivRef.current && chartResizeObserver.unobserve(storageInfoDivRef.current) + } + }, [storageInfoDivRef.current]) + + useUpdatedEffect(() => { + const intervalId = setInterval(getStorageInfo(), parseInt(refreshInterval) * 1000) + + return () => { + clearInterval(intervalId) + } + }, [refreshInterval]) + + const getStorageInfo = () => { + void r_sys_statistic_storage().then((res) => { + const response = res.data + if (response.success) { + const data = response.data + if (data) { + if (isLoading) { + setIsLoading(false) + } + + setTimeout(() => { + const eChartsOptions = [ + storageInfoVoToStorageEChartsOption( + '物理内存', + data.memoryTotal - data.memoryFree, + data.memoryFree + ), + storageInfoVoToStorageEChartsOption( + '虚拟内存', + data.virtualMemoryInUse, + data.virtualMemoryMax - data.virtualMemoryInUse + ), + storageInfoVoToStorageEChartsOption( + 'swap', + data.swapUsed, + data.swapTotal - data.swapUsed + ), + storageInfoVoToStorageEChartsOption( + 'jvm 内存', + data.jvmTotal - data.jvmFree, + data.jvmFree + ) + ] + data.fileStores.forEach((value) => + eChartsOptions.push( + storageInfoVoToStorageEChartsOption( + value.mount, + value.total - value.free, + value.free + ) + ) + ) + setStorageInfoEChartsOption(eChartsOptions) + + if (percentDivRef.current && keyDivRef.current) { + keyDivRef.current.innerHTML = '' + percentDivRef.current.innerHTML = '' + eChartsOptions.forEach((value) => { + const keyElement = document.createElement('div') + const percentElement = document.createElement('div') + keyElement.innerText = value.yAxis.data[0] + percentElement.innerText = `${( + (value.series[0].data[0] / + (value.series[0].data[0] + value.series[1].data[0])) * + 100 + ).toFixed(2)}%` + + keyDivRef.current?.appendChild(keyElement) + percentDivRef.current?.appendChild(percentElement) + }) + } + + if (!storageInfoEChartsRef.current.length) { + storageInfoDivRef.current && (storageInfoDivRef.current.innerHTML = '') + + eChartsOptions.forEach(() => { + const element = document.createElement('div') + storageInfoDivRef.current?.appendChild(element) + storageInfoEChartsRef.current.push( + echarts.init(element, null, { renderer: 'svg' }) + ) + }) + } + }) + } + } + }) + + return getStorageInfo + } + + const storageInfoVoToStorageEChartsOption = (label: string, used: number, free: number) => ({ + ...barEChartsBaseOption, + xAxis: { + ...barEChartsBaseOption.xAxis, + max: used + free + }, + yAxis: { + ...barEChartsBaseOption.yAxis, + data: [label] + }, + series: [ + { + ...storageDefaultSeriesOption, + name: 'used', + data: [used] + }, + { + ...storageDefaultSeriesOption, + name: 'free', + data: [free] + } + ] + }) + + useEffect(() => { + storageInfoEChartsRef.current?.forEach((value, index) => { + try { + value.setOption(storageInfoEChartsOption[index]) + } catch (e) { + /* empty */ + } + }) + }, [storageInfoEChartsOption]) + + return ( + <> + setRefreshInterval(value)} + > + 1秒 + 2秒 + 3秒 + 5秒 + 10秒 + 15秒 + 20秒 + 30秒 + 60秒 + 2分 + 3分 + 5分 + 10分 + + } + > + + + + + + + + ) +} + +const Statistic: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + ) +} + +export default Statistic diff --git a/src/pages/system/index.tsx b/src/pages/system/index.tsx index 841cfba..52eede3 100644 --- a/src/pages/system/index.tsx +++ b/src/pages/system/index.tsx @@ -1,956 +1,7 @@ -import React, { useEffect, useState } from 'react' -import Icon from '@ant-design/icons' -import * as echarts from 'echarts/core' -import { - TooltipComponent, - TooltipComponentOption, - GridComponent, - GridComponentOption, - ToolboxComponentOption, - DataZoomComponentOption, - ToolboxComponent, - DataZoomComponent -} from 'echarts/components' -import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts' -import { UniversalTransition } from 'echarts/features' -import { SVGRenderer } from 'echarts/renderers' -import { TopLevelFormatterParams } from 'echarts/types/dist/shared' -import '@/assets/css/pages/system/index.scss' -import { useUpdatedEffect } from '@/util/hooks' -import { formatByteSize } from '@/util/common' -import { getTimesBetweenTwoTimes, utcToLocalTime } from '@/util/datetime' -import { - r_sys_statistic_active, - r_sys_statistic_cpu, - r_sys_statistic_hardware, - r_sys_statistic_online, - r_sys_statistic_software, - r_sys_statistic_storage -} from '@/services/system' -import Card from '@/components/common/Card' -import FlexBox from '@/components/common/FlexBox' -import FitFullScreen from '@/components/common/FitFullScreen' -import HideScrollbar from '@/components/common/HideScrollbar' -import LoadingMask from '@/components/common/LoadingMask' - -echarts.use([ - TooltipComponent, - ToolboxComponent, - GridComponent, - DataZoomComponent, - BarChart, - LineChart, - SVGRenderer, - UniversalTransition -]) -type EChartsOption = echarts.ComposeOption< - | TooltipComponentOption - | ToolboxComponentOption - | GridComponentOption - | BarSeriesOption - | DataZoomComponentOption - | LineSeriesOption -> - -const barDefaultSeriesOption: BarSeriesOption = { - type: 'bar', - stack: 'total', - itemStyle: { - color: (params) => { - switch (params.seriesName) { - case 'idle': - case 'free': - return '#F5F5F5' - default: - return params.color ?? echarts.color.random() - } - } - } -} - -const barEChartsBaseOption: EChartsOption = { - tooltip: {}, - xAxis: { - show: false - }, - yAxis: { - axisLine: { - show: false - }, - axisLabel: { - show: false - }, - axisTick: { - show: false - }, - splitLine: { - show: false - }, - axisPointer: { - show: false - } - } -} - -const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => { - return (params: TopLevelFormatterParams) => - `${utcToLocalTime( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - params[0].data[0], - format - )}
${ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - params[0]['marker'] - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - }${params[0]['seriesName']}${ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access - params[0].data[1] - } ` -} - -const lineEChartsBaseOption: EChartsOption = { - tooltip: { - trigger: 'axis' - }, - toolbox: { - feature: { - dataZoom: { - yAxisIndex: 'none' - }, - restore: {}, - saveAsImage: {} - } - }, - xAxis: { - type: 'time' - }, - yAxis: { - type: 'value', - interval: 1 - }, - dataZoom: [ - { - type: 'inside', - start: 0, - end: 100, - minValueSpan: 2 * 60 * 60 * 1000 - } - ], - series: [{}] -} - -interface CommonCardProps extends React.PropsWithChildren { - icon: IconComponent - title: React.ReactNode - loading?: boolean - expand?: React.ReactNode -} - -const CommonCard: React.FC = (props) => { - return ( - - - - -
{props.title}
- {props.expand} -
- -
-
- ) -} - -const OnlineInfo: React.FC = () => { - const onlineInfoDivRef = useRef(null) - const onlineInfoEChartsRef = useRef(null) - const [isLoading, setIsLoading] = useState(false) - const [currentOnlineCount, setCurrentOnlineCount] = useState(-1) - const [scope, setScope] = useState('WEAK') - - useUpdatedEffect(() => { - const chartResizeObserver = new ResizeObserver(() => { - onlineInfoEChartsRef.current?.resize() - }) - - onlineInfoDivRef.current && chartResizeObserver.observe(onlineInfoDivRef.current) - - return () => { - onlineInfoDivRef.current && chartResizeObserver.unobserve(onlineInfoDivRef.current) - } - }, [isLoading]) - - useUpdatedEffect(() => { - getOnlineInfo() - }, []) - - const handleOnScopeChange = (value: string) => { - setScope(value) - getOnlineInfo(value) - } - - const handleOnRefresh = () => { - getOnlineInfo() - } - - const getOnlineInfo = (_scope: string = scope) => { - if (isLoading) { - return - } - - setIsLoading(true) - - void r_sys_statistic_online({ scope: _scope }).then((res) => { - const response = res.data - if (response.success) { - const data = response.data - if (data) { - setIsLoading(false) - - setCurrentOnlineCount(data.current) - - setTimeout(() => { - const dataList = getTimesBetweenTwoTimes( - data.history[0].time, - data.history[data.history.length - 1].time, - 'minute' - ).map((time) => [ - time, - data.history.find( - (value) => value.time.substring(0, 16) === time.substring(0, 16) - )?.record ?? 0 - ]) - - onlineInfoEChartsRef.current = echarts.init( - onlineInfoDivRef.current, - null, - { renderer: 'svg' } - ) - - onlineInfoEChartsRef.current?.setOption({ - ...lineEChartsBaseOption, - tooltip: { - ...lineEChartsBaseOption.tooltip, - formatter: getTooltipTimeFormatter('yyyy-MM-DD HH:mm') - }, - xAxis: { - ...lineEChartsBaseOption.xAxis - }, - series: [ - { - name: '在线人数', - type: 'line', - smooth: true, - symbol: 'none', - areaStyle: {}, - data: dataList - } - ] - }) - }) - } - } - }) - } - - return ( - - - 在线用户 - - 当前 {currentOnlineCount === -1 ? '获取中...' : currentOnlineCount} - - - - } - loading={isLoading} - expand={ - <> - - 今天 - 最近7天 - 最近30天 - 最近3个月 - 最近12个月 - 最近2年 - 最近3年 - 最近5年 - 全部 - - - - - - } - > - -
- - - ) -} - -const ActiveInfo: React.FC = () => { - const activeInfoDivRef = useRef(null) - const activeInfoEChartsRef = useRef(null) - const [isLoading, setIsLoading] = useState(false) - const [scope, setScope] = useState('WEAK') - - useUpdatedEffect(() => { - const chartResizeObserver = new ResizeObserver(() => { - activeInfoEChartsRef.current?.resize() - }) - - activeInfoDivRef.current && chartResizeObserver.observe(activeInfoDivRef.current) - - return () => { - activeInfoDivRef.current && chartResizeObserver.unobserve(activeInfoDivRef.current) - } - }, [isLoading]) - - useUpdatedEffect(() => { - getActiveInfo() - }, []) - - const handleOnScopeChange = (value: string) => { - setScope(value) - getActiveInfo(value) - } - - const handleOnRefresh = () => { - getActiveInfo() - } - - const getActiveInfo = (_scope: string = scope) => { - if (isLoading) { - return - } - - setIsLoading(true) - - void r_sys_statistic_active({ scope: _scope }).then((res) => { - const response = res.data - if (response.success) { - const data = response.data - if (data) { - setIsLoading(false) - - setTimeout(() => { - const registerList = data.registerHistory.length - ? getTimesBetweenTwoTimes( - data.registerHistory[0].time, - data.registerHistory[data.registerHistory.length - 1].time, - 'day' - ).map((time) => [ - time, - data.registerHistory.find( - (value) => - value.time.substring(0, 10) === time.substring(0, 10) - )?.count ?? 0 - ]) - : [] - const loginList = data.loginHistory.length - ? getTimesBetweenTwoTimes( - data.loginHistory[0].time, - data.loginHistory[data.loginHistory.length - 1].time, - 'day' - ).map((time) => [ - time, - data.loginHistory.find( - (value) => - value.time.substring(0, 10) === time.substring(0, 10) - )?.count ?? 0 - ]) - : [] - - activeInfoEChartsRef.current = echarts.init( - activeInfoDivRef.current, - null, - { renderer: 'svg' } - ) - - activeInfoEChartsRef.current?.setOption({ - ...lineEChartsBaseOption, - tooltip: { - ...lineEChartsBaseOption.tooltip, - formatter: getTooltipTimeFormatter('yyyy-MM-DD') - }, - dataZoom: [ - { - type: 'inside', - start: 0, - end: 100, - minValueSpan: 2 * 24 * 60 * 60 * 1000 - } - ], - series: [ - { - name: '注册人数', - type: 'line', - smooth: true, - symbol: 'none', - areaStyle: {}, - data: registerList - }, - { - name: '登录人数', - type: 'line', - smooth: true, - symbol: 'none', - areaStyle: {}, - data: loginList - } - ] - }) - }) - } - } - }) - } - - return ( - - - 用户活跃 - - - } - loading={isLoading} - expand={ - <> - - 最近7天 - 最近30天 - 最近3个月 - 最近12个月 - 最近2年 - 最近3年 - 最近5年 - 全部 - - - - - - } - > - -
- - - ) -} - -const SoftwareInfo: React.FC = () => { - const [softwareInfoData, setSoftwareInfoData] = useState() - - useUpdatedEffect(() => { - void r_sys_statistic_software().then((res) => { - const response = res.data - if (response.success) { - response.data && setSoftwareInfoData(response.data) - } else { - void message.error('获取软件信息失败,请稍后重试') - } - }) - }, []) - - return ( - - - -
操作系统
-
位数
-
Java
-
Java 供应商
-
Runtime
-
JVM
-
JVM 供应商
-
操作系统启动时间
-
后端服务器启动时间
-
- -
{softwareInfoData?.os}
-
- {softwareInfoData?.bitness} -
-
{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
-
{softwareInfoData?.javaVendor}
-
{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
-
{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
-
{softwareInfoData?.jvmVendor}
-
- {softwareInfoData?.osBootTime && - utcToLocalTime(softwareInfoData?.osBootTime)} -
-
- {softwareInfoData?.serverStartupTime && - utcToLocalTime(softwareInfoData.serverStartupTime)} -
-
-
-
- ) -} - -const HardwareInfo: React.FC = () => { - const [hardwareInfoData, setHardwareInfoData] = useState() - - useUpdatedEffect(() => { - void r_sys_statistic_hardware().then((res) => { - const response = res.data - if (response.success) { - response.data && setHardwareInfoData(response.data) - } else { - void message.error('获取硬件信息失败,请稍后重试') - } - }) - }, []) - - return ( - - - -
CPU
-
CPU 架构
-
微架构
-
64位
-
物理 CPU
-
物理核心
-
逻辑核心
-
内存
-
磁盘
-
- -
{hardwareInfoData?.cpu}
-
{hardwareInfoData?.arch}
-
- {hardwareInfoData?.microarchitecture} -
-
- {hardwareInfoData?.is64Bit ? '是' : '否'} -
-
- {hardwareInfoData?.cpuPhysicalPackageCount} -
-
- {hardwareInfoData?.cpuPhysicalProcessorCount} -
-
- {hardwareInfoData?.cpuLogicalProcessorCount} -
-
{hardwareInfoData?.memories}
-
{hardwareInfoData?.disks}
-
-
-
- ) -} - -const CPUInfo: React.FC = () => { - const keyDivRef = useRef(null) - const percentDivRef = useRef(null) - const cpuInfoDivRef = useRef(null) - const cpuInfoEChartsRef = useRef([]) - const [isLoading, setIsLoading] = useState(true) - const [refreshInterval, setRefreshInterval] = useState('5') - const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState([]) - - const cpuDefaultSeriesOption: BarSeriesOption = { - ...barDefaultSeriesOption, - tooltip: { - valueFormatter: (value) => `${((value as number) * 100).toFixed(2)}%` - } - } - - useUpdatedEffect(() => { - const chartResizeObserver = new ResizeObserver(() => { - cpuInfoEChartsRef.current.forEach((value) => value.resize()) - }) - - cpuInfoDivRef.current && chartResizeObserver.observe(cpuInfoDivRef.current) - - return () => { - cpuInfoDivRef.current && chartResizeObserver.unobserve(cpuInfoDivRef.current) - } - }, [cpuInfoDivRef.current]) - - useUpdatedEffect(() => { - const intervalId = setInterval(getCpuInfo(), parseInt(refreshInterval) * 1000) - - return () => { - clearInterval(intervalId) - } - }, [refreshInterval]) - - const getCpuInfo = () => { - void r_sys_statistic_cpu().then((res) => { - const response = res.data - if (response.success) { - const data = response.data - if (data) { - if (isLoading) { - setIsLoading(false) - } - - setTimeout(() => { - const dataList = data.processors.map((value) => - cpuInfoVoToCpuInfoData(value) - ) - dataList.unshift(cpuInfoVoToCpuInfoData(data)) - - setCpuInfoEChartsOption( - dataList.map((value, index) => ({ - ...barEChartsBaseOption, - yAxis: { - ...barEChartsBaseOption.yAxis, - data: [index === 0 ? '总占用' : `CPU ${index - 1}`] - }, - series: value - })) - ) - - if (percentDivRef.current) { - percentDivRef.current.innerHTML = '' - dataList.forEach((value) => { - const percentElement = document.createElement('div') - const idle = value.find((item) => item.name === 'idle')?.data[0] - percentElement.innerText = - idle !== undefined - ? `${((1 - idle) * 100).toFixed(2)}%` - : 'Unknown' - percentDivRef.current?.appendChild(percentElement) - }) - } - - if (!cpuInfoEChartsRef.current.length) { - keyDivRef.current && (keyDivRef.current.innerHTML = '') - cpuInfoDivRef.current && (cpuInfoDivRef.current.innerHTML = '') - for (let i = 0; i < dataList.length; i++) { - const keyElement = document.createElement('div') - keyElement.innerText = i === 0 ? '总占用' : `CPU ${i - 1}` - keyDivRef.current?.appendChild(keyElement) - - const valueElement = document.createElement('div') - cpuInfoDivRef.current?.appendChild(valueElement) - cpuInfoEChartsRef.current.push( - echarts.init(valueElement, null, { renderer: 'svg' }) - ) - } - } - }) - } - } - }) - - return getCpuInfo - } - - const cpuInfoVoToCpuInfoData = (cpuInfoVo: CpuInfoVo) => - Object.entries(cpuInfoVo) - .filter(([key]) => !['total', 'processors'].includes(key)) - .map(([key, value]) => ({ - ...cpuDefaultSeriesOption, - name: key, - data: [(value as number) / cpuInfoVo.total] - })) - .sort((a, b) => { - const order = [ - 'steal', - 'irq', - 'softirq', - 'iowait', - 'system', - 'nice', - 'user', - 'idle' - ] - return order.indexOf(a.name) - order.indexOf(b.name) - }) - - useEffect(() => { - cpuInfoEChartsRef.current?.forEach((value, index) => { - try { - value.setOption(cpuInfoEChartsOption[index]) - } catch (e) { - /* empty */ - } - }) - }, [cpuInfoEChartsOption]) - - return ( - <> - setRefreshInterval(value)} - > - 1秒 - 2秒 - 3秒 - 5秒 - 10秒 - 15秒 - 20秒 - 30秒 - 60秒 - 2分 - 3分 - 5分 - 10分 - - } - > - - - - - - - - ) -} - -const StorageInfo: React.FC = () => { - const keyDivRef = useRef(null) - const percentDivRef = useRef(null) - const storageInfoDivRef = useRef(null) - const storageInfoEChartsRef = useRef([]) - const [isLoading, setIsLoading] = useState(true) - const [refreshInterval, setRefreshInterval] = useState('5') - const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState([]) - - const storageDefaultSeriesOption: BarSeriesOption = { - ...barDefaultSeriesOption, - tooltip: { valueFormatter: (value) => formatByteSize(value as number) } - } - - useUpdatedEffect(() => { - const chartResizeObserver = new ResizeObserver(() => { - storageInfoEChartsRef.current.forEach((value) => value.resize()) - }) - - storageInfoDivRef.current && chartResizeObserver.observe(storageInfoDivRef.current) - - return () => { - storageInfoDivRef.current && chartResizeObserver.unobserve(storageInfoDivRef.current) - } - }, [storageInfoDivRef.current]) - - useUpdatedEffect(() => { - const intervalId = setInterval(getStorageInfo(), parseInt(refreshInterval) * 1000) - - return () => { - clearInterval(intervalId) - } - }, [refreshInterval]) - - const getStorageInfo = () => { - void r_sys_statistic_storage().then((res) => { - const response = res.data - if (response.success) { - const data = response.data - if (data) { - if (isLoading) { - setIsLoading(false) - } - - setTimeout(() => { - const eChartsOptions = [ - storageInfoVoToStorageEChartsOption( - '物理内存', - data.memoryTotal - data.memoryFree, - data.memoryFree - ), - storageInfoVoToStorageEChartsOption( - '虚拟内存', - data.virtualMemoryInUse, - data.virtualMemoryMax - data.virtualMemoryInUse - ), - storageInfoVoToStorageEChartsOption( - 'swap', - data.swapUsed, - data.swapTotal - data.swapUsed - ), - storageInfoVoToStorageEChartsOption( - 'jvm 内存', - data.jvmTotal - data.jvmFree, - data.jvmFree - ) - ] - data.fileStores.forEach((value) => - eChartsOptions.push( - storageInfoVoToStorageEChartsOption( - value.mount, - value.total - value.free, - value.free - ) - ) - ) - setStorageInfoEChartsOption(eChartsOptions) - - if (percentDivRef.current && keyDivRef.current) { - keyDivRef.current.innerHTML = '' - percentDivRef.current.innerHTML = '' - eChartsOptions.forEach((value) => { - const keyElement = document.createElement('div') - const percentElement = document.createElement('div') - keyElement.innerText = value.yAxis.data[0] - percentElement.innerText = `${( - (value.series[0].data[0] / - (value.series[0].data[0] + value.series[1].data[0])) * - 100 - ).toFixed(2)}%` - - keyDivRef.current?.appendChild(keyElement) - percentDivRef.current?.appendChild(percentElement) - }) - } - - if (!storageInfoEChartsRef.current.length) { - storageInfoDivRef.current && (storageInfoDivRef.current.innerHTML = '') - - eChartsOptions.forEach(() => { - const element = document.createElement('div') - storageInfoDivRef.current?.appendChild(element) - storageInfoEChartsRef.current.push( - echarts.init(element, null, { renderer: 'svg' }) - ) - }) - } - }) - } - } - }) - - return getStorageInfo - } - - const storageInfoVoToStorageEChartsOption = (label: string, used: number, free: number) => ({ - ...barEChartsBaseOption, - xAxis: { - ...barEChartsBaseOption.xAxis, - max: used + free - }, - yAxis: { - ...barEChartsBaseOption.yAxis, - data: [label] - }, - series: [ - { - ...storageDefaultSeriesOption, - name: 'used', - data: [used] - }, - { - ...storageDefaultSeriesOption, - name: 'free', - data: [free] - } - ] - }) - - useEffect(() => { - storageInfoEChartsRef.current?.forEach((value, index) => { - try { - value.setOption(storageInfoEChartsOption[index]) - } catch (e) { - /* empty */ - } - }) - }, [storageInfoEChartsOption]) - - return ( - <> - setRefreshInterval(value)} - > - 1秒 - 2秒 - 3秒 - 5秒 - 10秒 - 15秒 - 20秒 - 30秒 - 60秒 - 2分 - 3分 - 5分 - 10分 - - } - > - - - - - - - - ) -} +import React from 'react' const System: React.FC = () => { - return ( - <> - - - - - - - - - - - - - - ) + return <> } export default System diff --git a/src/router/system.tsx b/src/router/system.tsx index 33087c9..b49d535 100644 --- a/src/router/system.tsx +++ b/src/router/system.tsx @@ -7,9 +7,19 @@ const system: RouteJsonObject[] = [ absolutePath: '/system', id: 'system', component: React.lazy(() => import('@/pages/system')), + name: '系统管理', + icon: React.lazy(() => import('~icons/fatweb/setting.jsx')), + menu: true + }, + { + path: 'statistic', + absolutePath: '/system/statistic', + id: 'system-statistic', + component: React.lazy(() => import('@/pages/system/Statistic')), name: '系统概况', icon: React.lazy(() => import('~icons/fatweb/chart.jsx')), - menu: true + menu: true, + autoHide: true }, { path: 'settings',