Complete main UI #37

Merged
FatttSnake merged 192 commits from FatttSnake into dev 2024-02-23 16:31:17 +08:00
5 changed files with 141 additions and 233 deletions
Showing only changes of commit 4e2d13c064 - Show all commits

View File

@@ -35,7 +35,7 @@
padding: 0 10px;
gap: 10px;
.key {
.key, .value-percent {
flex: 0 0 auto;
color: constants.$font-main-color;
}
@@ -52,7 +52,10 @@
}
.value-chart {
justify-content: space-around;
width: 0;
>div {
max-height: 12px;
height: 12px;
>* {
transform: translateY(1px);

View File

@@ -1,181 +0,0 @@
import React, { CSSProperties, useState } from 'react'
import echarts, { EChartsCoreOption, EChartsType } from 'echarts/core'
import { RendererType } from 'echarts/types/src/util/types'
import { LocaleOption } from 'echarts/types/src/core/locale'
import { bind, clear } from 'size-sensor'
import isEqual from 'fast-deep-equal'
import { usePrevious } from '@/util/hooks'
interface EChartsInitOpts {
locale?: string | LocaleOption
renderer?: RendererType
devicePixelRatio?: number
useDirtyRect?: boolean
useCoarsePointer?: boolean
pointerSize?: number
ssr?: boolean
width?: number | string
height?: number | string
}
interface EChartsReactProps {
echarts: typeof echarts
className?: string
style?: CSSProperties
option: EChartsCoreOption
theme?: string | object | null
notMerge?: boolean
lazyUpdate?: boolean
showLoading?: boolean
loadingOption?: object
opts?: EChartsInitOpts
onChartReady?: (instance: EChartsType) => void
onEvents?: Record<string, () => void>
shouldSetOption?: (prevProps: EChartsReactProps, props: EChartsReactProps) => boolean
}
const EChartReact: React.FC<EChartsReactProps> = (props) => {
const elementRef = useRef<HTMLDivElement>(null)
const prevProps = usePrevious(props)
const [echarts] = useState(props.echarts)
const [isInitialResize, setIsInitialResize] = useState(true)
const { style, className = '', theme, opts } = props
const renderNewECharts = () => {
const { onEvents, onChartReady } = props
const eChartsInstance = updateEChartsOption()
bindEvents(eChartsInstance, onEvents || {})
if (typeof onChartReady === 'function') {
onChartReady(eChartsInstance)
}
if (elementRef.current) {
bind(elementRef.current, () => {
resize()
})
}
return () => {
dispose()
}
}
const updateEChartsOption = () => {
const { option, notMerge = false, lazyUpdate = false, showLoading, loadingOption } = props
const eChartsInstance = getEChartsInstance()
eChartsInstance.setOption(option, notMerge, lazyUpdate)
if (showLoading) {
eChartsInstance.showLoading(loadingOption)
} else {
eChartsInstance.hideLoading()
}
return eChartsInstance
}
const getEChartsInstance = () =>
(elementRef.current && echarts.getInstanceByDom(elementRef.current)) ||
echarts.init(elementRef.current, theme, opts)
const bindEvents = (instance: EChartsType, events: EChartsReactProps['onEvents']) => {
const _bindEvents = (
eventName: string,
func: (param: unknown, instance: EChartsType) => void
) => {
if (typeof func === 'function') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
instance.on(eventName, (param: unknown) => {
func(param, instance)
})
}
}
for (const eventName in events) {
if (Object.prototype.hasOwnProperty.call(events, eventName)) {
_bindEvents(eventName, events[eventName])
}
}
}
const resize = () => {
const eChartsInstance = getEChartsInstance()
if (!isInitialResize) {
try {
eChartsInstance.resize()
} catch (e) {
console.warn(e)
}
}
setIsInitialResize(false)
}
const dispose = () => {
if (elementRef.current) {
try {
clear(elementRef.current)
} catch (e) {
console.warn(e)
}
echarts.dispose(elementRef.current)
}
}
const pick = (obj: EChartsReactProps | undefined, keys: string[]): Record<string, unknown> => {
const r = {}
keys.forEach((key) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
r[key] = obj[key]
})
return r
}
useEffect(() => {
renderNewECharts()
})
useEffect(() => {
const { shouldSetOption } = props
if (
typeof shouldSetOption === 'function' &&
prevProps &&
!shouldSetOption(prevProps, props)
) {
return
}
if (
!isEqual(prevProps?.theme, props.theme) ||
!isEqual(prevProps?.opts, props.opts) ||
!isEqual(prevProps?.onEvents, props.onEvents)
) {
dispose()
renderNewECharts()
return
}
const pickKeys = ['option', 'notMerge', 'lazyUpdate', 'showLoading', 'loadingOption']
if (!isEqual(pick(props, pickKeys), pick(prevProps, pickKeys))) {
updateEChartsOption()
}
if (
!isEqual(prevProps?.style, props.style) ||
!isEqual(prevProps?.className, props.className)
) {
resize()
}
}, [props])
return <div ref={elementRef} style={style} className={`echarts-react ${className}`}></div>
}
export default EChartReact

1
src/global.d.ts vendored
View File

@@ -361,6 +361,7 @@ interface CpuInfoVo {
irq: number
softirq: number
steal: number
total: number
processors: CpuInfoVo[]
}

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import Icon from '@ant-design/icons'
import * as echarts from 'echarts/core'
import {
@@ -8,7 +8,7 @@ import {
GridComponentOption
} from 'echarts/components'
import { BarChart, BarSeriesOption } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import { SVGRenderer } from 'echarts/renderers'
import '@/assets/css/pages/system/index.scss'
import { useUpdatedEffect } from '@/util/hooks'
import { utcToLocalTime } from '@/util/datetime'
@@ -22,12 +22,12 @@ import FlexBox from '@/components/common/FlexBox'
import FitFullScreen from '@/components/common/FitFullScreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import LoadingMask from '@/components/common/LoadingMask'
import EChartReact from '@/components/common/echarts/EChartReact'
echarts.use([TooltipComponent, GridComponent, BarChart, CanvasRenderer])
echarts.use([TooltipComponent, GridComponent, BarChart, SVGRenderer])
type EChartsOption = echarts.ComposeOption<
TooltipComponentOption | GridComponentOption | BarSeriesOption
>
interface CommonCardProps extends React.PropsWithChildren {
icon: IconComponent
title: string
@@ -174,47 +174,54 @@ const HardwareInfo: React.FC = () => {
}
const CPUInfo: React.FC = () => {
const [cpuInfoData, setCpuInfoData] = useState<BarSeriesOption[]>()
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const cpuInfoDivRef = useRef<HTMLDivElement>(null)
const cpuInfoEChatsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState<EChartsOption[]>([])
const defaultSeriesOption: BarSeriesOption = {
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
itemStyle: {
color: (params) => {
switch (params.seriesName) {
case 'idle':
return '#F5F5F5'
default:
return params.color ?? echarts.color.random()
}
}
},
tooltip: {
valueFormatter: (value) => `${((value as number) * 100).toFixed(2)}%`
}
}
useUpdatedEffect(() => {
setInterval(
() =>
r_sys_statistics_cpu().then((res) => {
const response = res.data
if (response.success) {
const data = response.data
if (data) {
const cpuInfoData = Object.entries(data)
.filter(([key]) => key !== 'processors')
.map(([key, value]) => ({
...defaultSeriesOption,
name: key,
data: [value as number]
}))
console.log(cpuInfoData)
setCpuInfoData(cpuInfoData)
}
}
}),
5000
)
const intervalId = setInterval(() => getCpuInfo(), 2000)
const handleOnWindowResize = () => {
setTimeout(() => {
cpuInfoEChatsRef.current.forEach((value) => value.resize())
}, 50)
}
window.addEventListener('resize', handleOnWindowResize)
return () => {
clearInterval(intervalId)
window.removeEventListener('resize', handleOnWindowResize)
}
}, [])
const option: EChartsOption = {
const cpuInfoEChartBaseOption: EChartsOption = {
tooltip: {},
xAxis: {
show: false
},
yAxis: {
data: ['总使用'],
axisLine: {
show: false
},
@@ -230,30 +237,109 @@ const CPUInfo: React.FC = () => {
axisPointer: {
show: false
}
},
series: cpuInfoData
}
}
const getCpuInfo = () => {
void r_sys_statistics_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) => ({
...cpuInfoEChartBaseOption,
yAxis: {
...cpuInfoEChartBaseOption.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 (cpuInfoDivRef.current?.childElementCount !== dataList.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)
cpuInfoEChatsRef.current.push(
echarts.init(valueElement, null, { renderer: 'svg' })
)
}
}
})
}
}
})
}
const cpuInfoVoToCpuInfoData = (cpuInfoVo: CpuInfoVo) =>
Object.entries(cpuInfoVo)
.filter(([key]) => !['total', 'processors'].includes(key))
.map(([key, value]) => ({
...defaultSeriesOption,
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(() => {
cpuInfoEChatsRef.current?.forEach((value, index) => {
try {
value.setOption(cpuInfoEChartsOption[index])
} catch (e) {
/* empty */
}
})
}, [cpuInfoEChartsOption])
return (
<>
<CommonCard icon={IconFatwebCpu} title={'CPU 信息'} loading={false}>
<CommonCard icon={IconFatwebCpu} title={'CPU 信息'} loading={isLoading}>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div></div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value-chart'}>
<div>
<EChartReact
echarts={echarts}
opts={{ renderer: 'svg', height: 12 }}
option={option}
/>
</div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
@@ -287,7 +373,6 @@ const System: React.FC = () => {
<CPUInfo />
<MemoryInfo />
<JvmInfo />
<div />
</FlexBox>
</HideScrollbar>
</FitFullScreen>

View File

@@ -10,7 +10,7 @@ export const useUpdatedEffect = (
if (isFirstRender.current) {
isFirstRender.current = false
} else {
effect()
return effect()
}
}, dependencies)
}