Complete main UI #37
956
src/pages/system/Statistic.tsx
Normal file
956
src/pages/system/Statistic.tsx
Normal file
@@ -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
|
||||||
|
)}<br><span style="display: flex; justify-content: space-between"><span>${
|
||||||
|
// 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']}</span><span style="font-weight: bold">${
|
||||||
|
// 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]
|
||||||
|
}</span></span> `
|
||||||
|
}
|
||||||
|
|
||||||
|
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<CommonCardProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<Card style={{ overflow: 'visible' }}>
|
||||||
|
<FlexBox className={'common-card'}>
|
||||||
|
<FlexBox direction={'horizontal'} className={'head'}>
|
||||||
|
<Icon component={props.icon} className={'icon'} />
|
||||||
|
<div className={'title'}>{props.title}</div>
|
||||||
|
{props.expand}
|
||||||
|
</FlexBox>
|
||||||
|
<LoadingMask
|
||||||
|
hidden={!props.loading}
|
||||||
|
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</LoadingMask>
|
||||||
|
</FlexBox>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnlineInfo: React.FC = () => {
|
||||||
|
const onlineInfoDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const onlineInfoEChartsRef = useRef<echarts.EChartsType | null>(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 (
|
||||||
|
<CommonCard
|
||||||
|
icon={IconFatwebOnline}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<FlexBox gap={10} direction={'horizontal'}>
|
||||||
|
<span style={{ whiteSpace: 'nowrap' }}>在线用户</span>
|
||||||
|
<AntdTag>
|
||||||
|
当前 {currentOnlineCount === -1 ? '获取中...' : currentOnlineCount}
|
||||||
|
</AntdTag>
|
||||||
|
</FlexBox>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
loading={isLoading}
|
||||||
|
expand={
|
||||||
|
<>
|
||||||
|
<AntdSelect value={scope} onChange={handleOnScopeChange} disabled={isLoading}>
|
||||||
|
<AntdSelect.Option key={'DAY'}>今天</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'WEAK'}>最近7天</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'MONTH'}>最近30天</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'QUARTER'}>最近3个月</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'YEAR'}>最近12个月</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'TWO_YEARS'}>最近2年</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'THREE_YEARS'}>最近3年</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'FIVE_YEARS'}>最近5年</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'ALL'}>全部</AntdSelect.Option>
|
||||||
|
</AntdSelect>
|
||||||
|
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
|
||||||
|
<Icon component={IconFatwebRefresh} />
|
||||||
|
</AntdButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FlexBox className={'card-content'} direction={'horizontal'}>
|
||||||
|
<div className={'big-chart'} ref={onlineInfoDivRef} />
|
||||||
|
</FlexBox>
|
||||||
|
</CommonCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActiveInfo: React.FC = () => {
|
||||||
|
const activeInfoDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const activeInfoEChartsRef = useRef<echarts.EChartsType | null>(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 (
|
||||||
|
<CommonCard
|
||||||
|
icon={IconFatwebAnalysis}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
<FlexBox gap={10} direction={'horizontal'}>
|
||||||
|
<span style={{ whiteSpace: 'nowrap' }}>用户活跃</span>
|
||||||
|
</FlexBox>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
loading={isLoading}
|
||||||
|
expand={
|
||||||
|
<>
|
||||||
|
<AntdSelect value={scope} onChange={handleOnScopeChange} disabled={isLoading}>
|
||||||
|
<AntdSelect.Option key={'WEAK'}>最近7天</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'MONTH'}>最近30天</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'QUARTER'}>最近3个月</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'YEAR'}>最近12个月</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'TWO_YEARS'}>最近2年</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'THREE_YEARS'}>最近3年</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'FIVE_YEARS'}>最近5年</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={'ALL'}>全部</AntdSelect.Option>
|
||||||
|
</AntdSelect>
|
||||||
|
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
|
||||||
|
<Icon component={IconFatwebRefresh} />
|
||||||
|
</AntdButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FlexBox className={'card-content'} direction={'horizontal'}>
|
||||||
|
<div className={'big-chart'} ref={activeInfoDivRef} />
|
||||||
|
</FlexBox>
|
||||||
|
</CommonCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SoftwareInfo: React.FC = () => {
|
||||||
|
const [softwareInfoData, setSoftwareInfoData] = useState<SoftwareInfoVo>()
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<CommonCard
|
||||||
|
icon={IconFatwebSoftware}
|
||||||
|
title={'软件信息'}
|
||||||
|
loading={softwareInfoData === undefined}
|
||||||
|
>
|
||||||
|
<FlexBox className={'card-content'} direction={'horizontal'}>
|
||||||
|
<FlexBox className={'key'}>
|
||||||
|
<div>操作系统</div>
|
||||||
|
<div>位数</div>
|
||||||
|
<div>Java</div>
|
||||||
|
<div>Java 供应商</div>
|
||||||
|
<div>Runtime</div>
|
||||||
|
<div>JVM</div>
|
||||||
|
<div>JVM 供应商</div>
|
||||||
|
<div>操作系统启动时间</div>
|
||||||
|
<div>后端服务器启动时间</div>
|
||||||
|
</FlexBox>
|
||||||
|
<FlexBox className={'value'}>
|
||||||
|
<div title={softwareInfoData?.os}>{softwareInfoData?.os}</div>
|
||||||
|
<div title={softwareInfoData?.bitness.toString()}>
|
||||||
|
{softwareInfoData?.bitness}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
title={`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
|
||||||
|
>{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}</div>
|
||||||
|
<div title={softwareInfoData?.javaVendor}>{softwareInfoData?.javaVendor}</div>
|
||||||
|
<div
|
||||||
|
title={`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
|
||||||
|
>{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}</div>
|
||||||
|
<div
|
||||||
|
title={`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
|
||||||
|
>{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}</div>
|
||||||
|
<div title={softwareInfoData?.jvmVendor}>{softwareInfoData?.jvmVendor}</div>
|
||||||
|
<div>
|
||||||
|
{softwareInfoData?.osBootTime &&
|
||||||
|
utcToLocalTime(softwareInfoData?.osBootTime)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{softwareInfoData?.serverStartupTime &&
|
||||||
|
utcToLocalTime(softwareInfoData.serverStartupTime)}
|
||||||
|
</div>
|
||||||
|
</FlexBox>
|
||||||
|
</FlexBox>
|
||||||
|
</CommonCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HardwareInfo: React.FC = () => {
|
||||||
|
const [hardwareInfoData, setHardwareInfoData] = useState<HardwareInfoVo>()
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<CommonCard
|
||||||
|
icon={IconFatwebHardware}
|
||||||
|
title={'硬件信息'}
|
||||||
|
loading={hardwareInfoData === undefined}
|
||||||
|
>
|
||||||
|
<FlexBox className={'card-content'} direction={'horizontal'}>
|
||||||
|
<FlexBox className={'key'}>
|
||||||
|
<div>CPU</div>
|
||||||
|
<div>CPU 架构</div>
|
||||||
|
<div>微架构</div>
|
||||||
|
<div>64位</div>
|
||||||
|
<div>物理 CPU</div>
|
||||||
|
<div>物理核心</div>
|
||||||
|
<div>逻辑核心</div>
|
||||||
|
<div>内存</div>
|
||||||
|
<div>磁盘</div>
|
||||||
|
</FlexBox>
|
||||||
|
<FlexBox className={'value'}>
|
||||||
|
<div title={hardwareInfoData?.cpu}>{hardwareInfoData?.cpu}</div>
|
||||||
|
<div title={hardwareInfoData?.arch}>{hardwareInfoData?.arch}</div>
|
||||||
|
<div title={hardwareInfoData?.microarchitecture}>
|
||||||
|
{hardwareInfoData?.microarchitecture}
|
||||||
|
</div>
|
||||||
|
<div title={hardwareInfoData?.is64Bit ? '是' : '否'}>
|
||||||
|
{hardwareInfoData?.is64Bit ? '是' : '否'}
|
||||||
|
</div>
|
||||||
|
<div title={hardwareInfoData?.cpuPhysicalPackageCount.toString()}>
|
||||||
|
{hardwareInfoData?.cpuPhysicalPackageCount}
|
||||||
|
</div>
|
||||||
|
<div title={hardwareInfoData?.cpuPhysicalProcessorCount.toString()}>
|
||||||
|
{hardwareInfoData?.cpuPhysicalProcessorCount}
|
||||||
|
</div>
|
||||||
|
<div title={hardwareInfoData?.cpuLogicalProcessorCount.toString()}>
|
||||||
|
{hardwareInfoData?.cpuLogicalProcessorCount}
|
||||||
|
</div>
|
||||||
|
<div title={hardwareInfoData?.memories}>{hardwareInfoData?.memories}</div>
|
||||||
|
<div title={hardwareInfoData?.disks}>{hardwareInfoData?.disks}</div>
|
||||||
|
</FlexBox>
|
||||||
|
</FlexBox>
|
||||||
|
</CommonCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CPUInfo: React.FC = () => {
|
||||||
|
const keyDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const percentDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const cpuInfoDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const cpuInfoEChartsRef = useRef<echarts.EChartsType[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [refreshInterval, setRefreshInterval] = useState('5')
|
||||||
|
const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState<EChartsOption[]>([])
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<CommonCard
|
||||||
|
icon={IconFatwebCpu}
|
||||||
|
title={'CPU 信息'}
|
||||||
|
loading={isLoading}
|
||||||
|
expand={
|
||||||
|
<AntdSelect
|
||||||
|
value={refreshInterval}
|
||||||
|
onChange={(value) => setRefreshInterval(value)}
|
||||||
|
>
|
||||||
|
<AntdSelect.Option key={1}>1秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={2}>2秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={3}>3秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={5}>5秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={10}>10秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={15}>15秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={20}>20秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={30}>30秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={60}>60秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={120}>2分</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={180}>3分</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={300}>5分</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={600}>10分</AntdSelect.Option>
|
||||||
|
</AntdSelect>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FlexBox className={'card-content'} direction={'horizontal'}>
|
||||||
|
<FlexBox className={'key'} ref={keyDivRef} />
|
||||||
|
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
|
||||||
|
<FlexBox className={'value-percent'} ref={percentDivRef} />
|
||||||
|
</FlexBox>
|
||||||
|
</CommonCard>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageInfo: React.FC = () => {
|
||||||
|
const keyDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const percentDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const storageInfoDivRef = useRef<HTMLDivElement>(null)
|
||||||
|
const storageInfoEChartsRef = useRef<echarts.EChartsType[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [refreshInterval, setRefreshInterval] = useState('5')
|
||||||
|
const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState<EChartsOption[]>([])
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<CommonCard
|
||||||
|
icon={IconFatwebMemory}
|
||||||
|
title={'内存信息'}
|
||||||
|
loading={isLoading}
|
||||||
|
expand={
|
||||||
|
<AntdSelect
|
||||||
|
value={refreshInterval}
|
||||||
|
onChange={(value) => setRefreshInterval(value)}
|
||||||
|
>
|
||||||
|
<AntdSelect.Option key={1}>1秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={2}>2秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={3}>3秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={5}>5秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={10}>10秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={15}>15秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={20}>20秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={30}>30秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={60}>60秒</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={120}>2分</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={180}>3分</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={300}>5分</AntdSelect.Option>
|
||||||
|
<AntdSelect.Option key={600}>10分</AntdSelect.Option>
|
||||||
|
</AntdSelect>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FlexBox className={'card-content'} direction={'horizontal'}>
|
||||||
|
<FlexBox className={'key'} ref={keyDivRef} />
|
||||||
|
<FlexBox className={'value-chart'} ref={storageInfoDivRef} />
|
||||||
|
<FlexBox className={'value-percent'} ref={percentDivRef} />
|
||||||
|
</FlexBox>
|
||||||
|
</CommonCard>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Statistic: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FitFullScreen>
|
||||||
|
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
|
||||||
|
<FlexBox direction={'horizontal'} className={'root-content'}>
|
||||||
|
<OnlineInfo />
|
||||||
|
<ActiveInfo />
|
||||||
|
<SoftwareInfo />
|
||||||
|
<HardwareInfo />
|
||||||
|
<CPUInfo />
|
||||||
|
<StorageInfo />
|
||||||
|
</FlexBox>
|
||||||
|
</HideScrollbar>
|
||||||
|
</FitFullScreen>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Statistic
|
||||||
@@ -1,956 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React 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
|
|
||||||
)}<br><span style="display: flex; justify-content: space-between"><span>${
|
|
||||||
// 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']}</span><span style="font-weight: bold">${
|
|
||||||
// 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]
|
|
||||||
}</span></span> `
|
|
||||||
}
|
|
||||||
|
|
||||||
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<CommonCardProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<Card style={{ overflow: 'visible' }}>
|
|
||||||
<FlexBox className={'common-card'}>
|
|
||||||
<FlexBox direction={'horizontal'} className={'head'}>
|
|
||||||
<Icon component={props.icon} className={'icon'} />
|
|
||||||
<div className={'title'}>{props.title}</div>
|
|
||||||
{props.expand}
|
|
||||||
</FlexBox>
|
|
||||||
<LoadingMask
|
|
||||||
hidden={!props.loading}
|
|
||||||
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</LoadingMask>
|
|
||||||
</FlexBox>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const OnlineInfo: React.FC = () => {
|
|
||||||
const onlineInfoDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const onlineInfoEChartsRef = useRef<echarts.EChartsType | null>(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 (
|
|
||||||
<CommonCard
|
|
||||||
icon={IconFatwebOnline}
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<FlexBox gap={10} direction={'horizontal'}>
|
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>在线用户</span>
|
|
||||||
<AntdTag>
|
|
||||||
当前 {currentOnlineCount === -1 ? '获取中...' : currentOnlineCount}
|
|
||||||
</AntdTag>
|
|
||||||
</FlexBox>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
loading={isLoading}
|
|
||||||
expand={
|
|
||||||
<>
|
|
||||||
<AntdSelect value={scope} onChange={handleOnScopeChange} disabled={isLoading}>
|
|
||||||
<AntdSelect.Option key={'DAY'}>今天</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'WEAK'}>最近7天</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'MONTH'}>最近30天</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'QUARTER'}>最近3个月</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'YEAR'}>最近12个月</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'TWO_YEARS'}>最近2年</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'THREE_YEARS'}>最近3年</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'FIVE_YEARS'}>最近5年</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'ALL'}>全部</AntdSelect.Option>
|
|
||||||
</AntdSelect>
|
|
||||||
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
|
|
||||||
<Icon component={IconFatwebRefresh} />
|
|
||||||
</AntdButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FlexBox className={'card-content'} direction={'horizontal'}>
|
|
||||||
<div className={'big-chart'} ref={onlineInfoDivRef} />
|
|
||||||
</FlexBox>
|
|
||||||
</CommonCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ActiveInfo: React.FC = () => {
|
|
||||||
const activeInfoDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const activeInfoEChartsRef = useRef<echarts.EChartsType | null>(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 (
|
|
||||||
<CommonCard
|
|
||||||
icon={IconFatwebAnalysis}
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<FlexBox gap={10} direction={'horizontal'}>
|
|
||||||
<span style={{ whiteSpace: 'nowrap' }}>用户活跃</span>
|
|
||||||
</FlexBox>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
loading={isLoading}
|
|
||||||
expand={
|
|
||||||
<>
|
|
||||||
<AntdSelect value={scope} onChange={handleOnScopeChange} disabled={isLoading}>
|
|
||||||
<AntdSelect.Option key={'WEAK'}>最近7天</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'MONTH'}>最近30天</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'QUARTER'}>最近3个月</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'YEAR'}>最近12个月</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'TWO_YEARS'}>最近2年</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'THREE_YEARS'}>最近3年</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'FIVE_YEARS'}>最近5年</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={'ALL'}>全部</AntdSelect.Option>
|
|
||||||
</AntdSelect>
|
|
||||||
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
|
|
||||||
<Icon component={IconFatwebRefresh} />
|
|
||||||
</AntdButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FlexBox className={'card-content'} direction={'horizontal'}>
|
|
||||||
<div className={'big-chart'} ref={activeInfoDivRef} />
|
|
||||||
</FlexBox>
|
|
||||||
</CommonCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SoftwareInfo: React.FC = () => {
|
|
||||||
const [softwareInfoData, setSoftwareInfoData] = useState<SoftwareInfoVo>()
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<CommonCard
|
|
||||||
icon={IconFatwebSoftware}
|
|
||||||
title={'软件信息'}
|
|
||||||
loading={softwareInfoData === undefined}
|
|
||||||
>
|
|
||||||
<FlexBox className={'card-content'} direction={'horizontal'}>
|
|
||||||
<FlexBox className={'key'}>
|
|
||||||
<div>操作系统</div>
|
|
||||||
<div>位数</div>
|
|
||||||
<div>Java</div>
|
|
||||||
<div>Java 供应商</div>
|
|
||||||
<div>Runtime</div>
|
|
||||||
<div>JVM</div>
|
|
||||||
<div>JVM 供应商</div>
|
|
||||||
<div>操作系统启动时间</div>
|
|
||||||
<div>后端服务器启动时间</div>
|
|
||||||
</FlexBox>
|
|
||||||
<FlexBox className={'value'}>
|
|
||||||
<div title={softwareInfoData?.os}>{softwareInfoData?.os}</div>
|
|
||||||
<div title={softwareInfoData?.bitness.toString()}>
|
|
||||||
{softwareInfoData?.bitness}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
title={`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
|
|
||||||
>{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}</div>
|
|
||||||
<div title={softwareInfoData?.javaVendor}>{softwareInfoData?.javaVendor}</div>
|
|
||||||
<div
|
|
||||||
title={`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
|
|
||||||
>{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}</div>
|
|
||||||
<div
|
|
||||||
title={`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
|
|
||||||
>{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}</div>
|
|
||||||
<div title={softwareInfoData?.jvmVendor}>{softwareInfoData?.jvmVendor}</div>
|
|
||||||
<div>
|
|
||||||
{softwareInfoData?.osBootTime &&
|
|
||||||
utcToLocalTime(softwareInfoData?.osBootTime)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{softwareInfoData?.serverStartupTime &&
|
|
||||||
utcToLocalTime(softwareInfoData.serverStartupTime)}
|
|
||||||
</div>
|
|
||||||
</FlexBox>
|
|
||||||
</FlexBox>
|
|
||||||
</CommonCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const HardwareInfo: React.FC = () => {
|
|
||||||
const [hardwareInfoData, setHardwareInfoData] = useState<HardwareInfoVo>()
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<CommonCard
|
|
||||||
icon={IconFatwebHardware}
|
|
||||||
title={'硬件信息'}
|
|
||||||
loading={hardwareInfoData === undefined}
|
|
||||||
>
|
|
||||||
<FlexBox className={'card-content'} direction={'horizontal'}>
|
|
||||||
<FlexBox className={'key'}>
|
|
||||||
<div>CPU</div>
|
|
||||||
<div>CPU 架构</div>
|
|
||||||
<div>微架构</div>
|
|
||||||
<div>64位</div>
|
|
||||||
<div>物理 CPU</div>
|
|
||||||
<div>物理核心</div>
|
|
||||||
<div>逻辑核心</div>
|
|
||||||
<div>内存</div>
|
|
||||||
<div>磁盘</div>
|
|
||||||
</FlexBox>
|
|
||||||
<FlexBox className={'value'}>
|
|
||||||
<div title={hardwareInfoData?.cpu}>{hardwareInfoData?.cpu}</div>
|
|
||||||
<div title={hardwareInfoData?.arch}>{hardwareInfoData?.arch}</div>
|
|
||||||
<div title={hardwareInfoData?.microarchitecture}>
|
|
||||||
{hardwareInfoData?.microarchitecture}
|
|
||||||
</div>
|
|
||||||
<div title={hardwareInfoData?.is64Bit ? '是' : '否'}>
|
|
||||||
{hardwareInfoData?.is64Bit ? '是' : '否'}
|
|
||||||
</div>
|
|
||||||
<div title={hardwareInfoData?.cpuPhysicalPackageCount.toString()}>
|
|
||||||
{hardwareInfoData?.cpuPhysicalPackageCount}
|
|
||||||
</div>
|
|
||||||
<div title={hardwareInfoData?.cpuPhysicalProcessorCount.toString()}>
|
|
||||||
{hardwareInfoData?.cpuPhysicalProcessorCount}
|
|
||||||
</div>
|
|
||||||
<div title={hardwareInfoData?.cpuLogicalProcessorCount.toString()}>
|
|
||||||
{hardwareInfoData?.cpuLogicalProcessorCount}
|
|
||||||
</div>
|
|
||||||
<div title={hardwareInfoData?.memories}>{hardwareInfoData?.memories}</div>
|
|
||||||
<div title={hardwareInfoData?.disks}>{hardwareInfoData?.disks}</div>
|
|
||||||
</FlexBox>
|
|
||||||
</FlexBox>
|
|
||||||
</CommonCard>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CPUInfo: React.FC = () => {
|
|
||||||
const keyDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const percentDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const cpuInfoDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const cpuInfoEChartsRef = useRef<echarts.EChartsType[]>([])
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const [refreshInterval, setRefreshInterval] = useState('5')
|
|
||||||
const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState<EChartsOption[]>([])
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<CommonCard
|
|
||||||
icon={IconFatwebCpu}
|
|
||||||
title={'CPU 信息'}
|
|
||||||
loading={isLoading}
|
|
||||||
expand={
|
|
||||||
<AntdSelect
|
|
||||||
value={refreshInterval}
|
|
||||||
onChange={(value) => setRefreshInterval(value)}
|
|
||||||
>
|
|
||||||
<AntdSelect.Option key={1}>1秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={2}>2秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={3}>3秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={5}>5秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={10}>10秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={15}>15秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={20}>20秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={30}>30秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={60}>60秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={120}>2分</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={180}>3分</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={300}>5分</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={600}>10分</AntdSelect.Option>
|
|
||||||
</AntdSelect>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FlexBox className={'card-content'} direction={'horizontal'}>
|
|
||||||
<FlexBox className={'key'} ref={keyDivRef} />
|
|
||||||
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
|
|
||||||
<FlexBox className={'value-percent'} ref={percentDivRef} />
|
|
||||||
</FlexBox>
|
|
||||||
</CommonCard>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const StorageInfo: React.FC = () => {
|
|
||||||
const keyDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const percentDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const storageInfoDivRef = useRef<HTMLDivElement>(null)
|
|
||||||
const storageInfoEChartsRef = useRef<echarts.EChartsType[]>([])
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const [refreshInterval, setRefreshInterval] = useState('5')
|
|
||||||
const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState<EChartsOption[]>([])
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<CommonCard
|
|
||||||
icon={IconFatwebMemory}
|
|
||||||
title={'内存信息'}
|
|
||||||
loading={isLoading}
|
|
||||||
expand={
|
|
||||||
<AntdSelect
|
|
||||||
value={refreshInterval}
|
|
||||||
onChange={(value) => setRefreshInterval(value)}
|
|
||||||
>
|
|
||||||
<AntdSelect.Option key={1}>1秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={2}>2秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={3}>3秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={5}>5秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={10}>10秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={15}>15秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={20}>20秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={30}>30秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={60}>60秒</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={120}>2分</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={180}>3分</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={300}>5分</AntdSelect.Option>
|
|
||||||
<AntdSelect.Option key={600}>10分</AntdSelect.Option>
|
|
||||||
</AntdSelect>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FlexBox className={'card-content'} direction={'horizontal'}>
|
|
||||||
<FlexBox className={'key'} ref={keyDivRef} />
|
|
||||||
<FlexBox className={'value-chart'} ref={storageInfoDivRef} />
|
|
||||||
<FlexBox className={'value-percent'} ref={percentDivRef} />
|
|
||||||
</FlexBox>
|
|
||||||
</CommonCard>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const System: React.FC = () => {
|
const System: React.FC = () => {
|
||||||
return (
|
return <></>
|
||||||
<>
|
|
||||||
<FitFullScreen>
|
|
||||||
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
|
|
||||||
<FlexBox direction={'horizontal'} className={'root-content'}>
|
|
||||||
<OnlineInfo />
|
|
||||||
<ActiveInfo />
|
|
||||||
<SoftwareInfo />
|
|
||||||
<HardwareInfo />
|
|
||||||
<CPUInfo />
|
|
||||||
<StorageInfo />
|
|
||||||
</FlexBox>
|
|
||||||
</HideScrollbar>
|
|
||||||
</FitFullScreen>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default System
|
export default System
|
||||||
|
|||||||
@@ -7,9 +7,19 @@ const system: RouteJsonObject[] = [
|
|||||||
absolutePath: '/system',
|
absolutePath: '/system',
|
||||||
id: 'system',
|
id: 'system',
|
||||||
component: React.lazy(() => import('@/pages/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: '系统概况',
|
name: '系统概况',
|
||||||
icon: React.lazy(() => import('~icons/fatweb/chart.jsx')),
|
icon: React.lazy(() => import('~icons/fatweb/chart.jsx')),
|
||||||
menu: true
|
menu: true,
|
||||||
|
autoHide: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
|
|||||||
Reference in New Issue
Block a user