This commit is contained in:
2023-12-07 18:38:49 +08:00
parent e1e2a83618
commit 5c14e0e86b
12 changed files with 518 additions and 1079 deletions

1141
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,11 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.2.6", "@ant-design/icons": "^5.2.6",
"@ant-design/charts": "^2.0.0-beta.0",
"antd": "^5.12.1", "antd": "^5.12.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"echarts": "^5.4.3",
"fast-deep-equal": "^3.1.3",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -27,14 +28,15 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router": "^6.20.1", "react-router": "^6.20.1",
"react-router-dom": "^6.20.1" "react-router-dom": "^6.20.1",
"size-sensor": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@svgr/core": "^8.1.0", "@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.6",
"@types/lodash": "^4.14.202", "@types/lodash": "^4.14.202",
"@types/node": "^20.10.3", "@types/node": "^20.10.4",
"@types/react": "^18.2.42", "@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/eslint-plugin": "^6.13.2",
@@ -52,9 +54,9 @@
"prettier": "^3.1.0", "prettier": "^3.1.0",
"sass": "^1.69.5", "sass": "^1.69.5",
"stylelint-config-prettier": "^9.0.5", "stylelint-config-prettier": "^9.0.5",
"typescript": "^5.3.2", "typescript": "^5.3.3",
"unplugin-auto-import": "^0.17.2", "unplugin-auto-import": "^0.17.2",
"unplugin-icons": "^0.18.1", "unplugin-icons": "^0.18.1",
"vite": "^5.0.5" "vite": "^5.0.6"
} }
} }

View File

@@ -3,9 +3,12 @@
.root-content { .root-content {
padding: 30px; padding: 30px;
gap: 20px; gap: 20px;
flex-wrap: wrap;
justify-content: center;
.root-row { >.card-box {
gap: 20px; width: 48%;
flex: 0 0 auto;
.common-card { .common-card {
padding: 20px; padding: 20px;
@@ -48,6 +51,15 @@
} }
} }
.value-chart {
>div {
height: 12px;
>* {
transform: translateY(1px);
}
}
}
> * { > * {
gap: 5px; gap: 5px;
} }

1
src/assets/svg/cpu.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M256 768h512V256H256v512z m341.3 85.3H426.6v85.3h-85.3v-85.3h-128a42.7 42.7 0 0 1-42.7-42.7v-128H85.3v-85.3h85.3V426.6H85.3v-85.3h85.3v-128a42.7 42.7 0 0 1 42.7-42.7h128V85.3h85.3v85.3h170.7V85.3h85.3v85.3h128a42.7 42.7 0 0 1 42.7 42.7v128h85.3v85.3h-85.3v170.7h85.3v85.3h-85.3v128a42.7 42.7 0 0 1-42.7 42.7h-128v85.3h-85.3v-85.3z m-256-512h341.3v341.3H341.3V341.3z" /></svg>

After

Width:  |  Height:  |  Size: 448 B

1
src/assets/svg/java.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M564 96c44.864 105.888-160.864 170.752-180 259.008-17.504 80.992 121.888 176 122.016 176-21.28-33.504-36.768-61.76-58.016-114.016C412 328.736 667.36 249.12 564 96z m136 147.008s-179.872 11.36-188.992 128.992c-4.16 52.384 47.488 79.872 48.992 118.016 1.28 31.104-31.008 56.96-31.008 56.96s57.888-10.464 76-57.984c20-52.736-38.976-88.736-32.992-131.008 5.76-40.352 128-114.976 128-114.976z m44 270.976c-18.88-0.864-40.128 6.144-59.008 20 37.248-8.224 69.024 15.136 69.024 42.016 0 60.256-86.016 116.992-86.016 116.992s132.992-14.88 132.992-113.984c0-40.896-25.6-63.52-56.992-65.024z m-352.992 1.024c-46.4 1.6-139.008 9.248-139.008 44.992 0 49.76 215.744 53.632 370.016 23.008 0 0 41.984-29.248 52.992-40-101.12 20.992-332 24.256-332 5.984 0-16.736 73.984-33.984 73.984-33.984s-10.496-0.512-25.984 0z m-14.016 92c-25.376 0-63.008 19.744-63.008 38.976 0 38.752 191.04 68.512 332.032 12l-49.024-29.984c-95.616 31.264-272.256 20.864-220-20.992z m24 86.976c-34.624 0-56.992 21.888-56.992 38.016 0 49.6 206.88 54.496 288.992 4l-52-34.016c-61.248 26.4-215.36 30.272-180-8z m-116 45.024C228.512 737.888 192 763.488 192 784.96c0 114.368 579.008 108.896 579.008-8 0-19.36-22.88-28.608-31.008-32.992 47.264 111.744-472.992 103.008-472.992 36.992 0-14.976 38.496-29.984 73.984-22.976l-29.984-17.024a190.784 190.784 0 0 0-26.016-1.984zM832 816c-88 85.12-310.72 115.616-535.008 63.008 224.256 93.76 533.888 41.6 535.008-63.008z" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 1024"><path d="M1280 261.88V192c0-35.34-28.66-64-64-64H64C28.66 128 0 156.66 0 192v69.88c37.2 13.22 64 48.38 64 90.12s-26.8 76.9-64 90.12V640h1280v-197.88c-37.2-13.22-64-48.38-64-90.12s26.8-76.9 64-90.12zM448 512h-128V256h128v256z m256 0h-128V256h128v256z m256 0h-128V256h128v256zM0 896h128v-53.34c0-17.68 14.32-32 32-32s32 14.32 32 32V896h256v-53.34c0-17.68 14.32-32 32-32s32 14.32 32 32V896h256v-53.34c0-17.68 14.32-32 32-32s32 14.32 32 32V896h256v-53.34c0-17.68 14.32-32 32-32s32 14.32 32 32V896h128v-192H0v192z" /></svg>

After

Width:  |  Height:  |  Size: 582 B

View File

@@ -0,0 +1,181 @@
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

View File

@@ -14,6 +14,9 @@ export const URL_SYS_SETTINGS_MAIL = `${URL_SYS_SETTINGS}/mail`
export const URL_SYS_STATISTICS = '/system/statistics' export const URL_SYS_STATISTICS = '/system/statistics'
export const URL_SYS_STATISTICS_SOFTWARE = `${URL_SYS_STATISTICS}/software` export const URL_SYS_STATISTICS_SOFTWARE = `${URL_SYS_STATISTICS}/software`
export const URL_SYS_STATISTICS_HARDWARE = `${URL_SYS_STATISTICS}/hardware` export const URL_SYS_STATISTICS_HARDWARE = `${URL_SYS_STATISTICS}/hardware`
export const URL_SYS_STATISTICS_CPU = `${URL_SYS_STATISTICS}/cpu`
export const URL_SYS_STATISTICS_MEMORY = `${URL_SYS_STATISTICS}/memory`
export const URL_SYS_STATISTICS_JVM = `${URL_SYS_STATISTICS}/jvm`
export const URL_API_V1 = '/api/v1' export const URL_API_V1 = '/api/v1'
export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64` export const URL_API_V1_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64`

21
src/global.d.ts vendored
View File

@@ -351,3 +351,24 @@ interface HardwareInfoVo {
memories: string memories: string
disks: string disks: string
} }
interface CpuInfoVo {
user: number
nice: number
system: number
idle: number
iowait: number
irq: number
softirq: number
steal: number
processors: CpuInfoVo[]
}
interface MemoryInfoVo {
total: number
free: number
virtualInUse: number
virtualMax: number
swapTotal: number
swapUsed: number
}

View File

@@ -1,16 +1,33 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import Icon from '@ant-design/icons' import Icon from '@ant-design/icons'
// import { DualAxes, DualAxesConfig } from '@ant-design/plots' import * as echarts from 'echarts/core'
import {
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption
} from 'echarts/components'
import { BarChart, BarSeriesOption } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import '@/assets/css/pages/system/index.scss' import '@/assets/css/pages/system/index.scss'
import { useUpdatedEffect } from '@/util/hooks' import { useUpdatedEffect } from '@/util/hooks'
import { utcToLocalTime } from '@/util/datetime' import { utcToLocalTime } from '@/util/datetime'
import { r_sys_statistics_hardware, r_sys_statistics_software } from '@/services/system' import {
r_sys_statistics_cpu,
r_sys_statistics_hardware,
r_sys_statistics_software
} from '@/services/system'
import Card from '@/components/common/Card' import Card from '@/components/common/Card'
import FlexBox from '@/components/common/FlexBox' import FlexBox from '@/components/common/FlexBox'
import FitFullScreen from '@/components/common/FitFullScreen' import FitFullScreen from '@/components/common/FitFullScreen'
import HideScrollbar from '@/components/common/HideScrollbar' import HideScrollbar from '@/components/common/HideScrollbar'
import LoadingMask from '@/components/common/LoadingMask' import LoadingMask from '@/components/common/LoadingMask'
import EChartReact from '@/components/common/echarts/EChartReact'
echarts.use([TooltipComponent, GridComponent, BarChart, CanvasRenderer])
type EChartsOption = echarts.ComposeOption<
TooltipComponentOption | GridComponentOption | BarSeriesOption
>
interface CommonCardProps extends React.PropsWithChildren { interface CommonCardProps extends React.PropsWithChildren {
icon: IconComponent icon: IconComponent
title: string title: string
@@ -19,7 +36,7 @@ interface CommonCardProps extends React.PropsWithChildren {
const CommonCard: React.FC<CommonCardProps> = (props) => { const CommonCard: React.FC<CommonCardProps> = (props) => {
return ( return (
<Card> <Card style={{ overflow: 'visible' }}>
<FlexBox className={'common-card'}> <FlexBox className={'common-card'}>
<FlexBox direction={'horizontal'} className={'head'}> <FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} /> <Icon component={props.icon} className={'icon'} />
@@ -27,7 +44,7 @@ const CommonCard: React.FC<CommonCardProps> = (props) => {
</FlexBox> </FlexBox>
<LoadingMask <LoadingMask
hidden={!props.loading} hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 10 }} />} maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
> >
{props.children} {props.children}
</LoadingMask> </LoadingMask>
@@ -124,9 +141,9 @@ const HardwareInfo: React.FC = () => {
<div>CPU </div> <div>CPU </div>
<div></div> <div></div>
<div>64</div> <div>64</div>
<div>CPU </div> <div> CPU</div>
<div>CPU </div> <div></div>
<div>CPU </div> <div></div>
<div></div> <div></div>
<div></div> <div></div>
</FlexBox> </FlexBox>
@@ -156,75 +173,121 @@ const HardwareInfo: React.FC = () => {
) )
} }
const System: React.FC = () => { const CPUInfo: React.FC = () => {
/* const [cpuInfoData, setCpuInfoData] = useState<BarSeriesOption[]>()
const dualAxesData = [
{ year: '1991', value: 3, count: 10 },
{ year: '1992', value: 4, count: 4 },
{ year: '1993', value: 3.5, count: 5 },
{ year: '1994', value: 5, count: 5 },
{ year: '1995', value: 4.9, count: 4.9 },
{ year: '1996', value: 6, count: 35 },
{ year: '1997', value: 7, count: 7 },
{ year: '1998', value: 9, count: 1 },
{ year: '1999', value: 13, count: 20 }
]
const userStatisticsData = [ const defaultSeriesOption: BarSeriesOption = {
{ time: '2023-12-01', type: 'register', number: 23 }, type: 'bar',
{ time: '2023-12-02', type: 'register', number: 123 }, stack: 'total',
{ time: '2023-12-03', type: 'register', number: 1432 }, emphasis: {
{ time: '2023-12-05', type: 'register', number: 1 }, focus: 'series'
{ time: '2023-12-04', type: 'register', number: 234 }, }
{ time: '2023-12-06', type: 'register', number: 23 },
{ time: '2023-12-07', type: 'register', number: 54 },
{ time: '2023-12-08', type: 'register', number: 87 },
{ time: '2023-12-09', type: 'register', number: 12 },
{ time: '2023-12-10', type: 'register', number: 123 },
{ time: '2023-12-11', type: 'register', number: 20 },
{ time: '2023-12-01', type: 'login', number: 433 },
{ time: '2023-12-02', type: 'login', number: 2 },
{ time: '2023-12-03', type: 'login', number: 34 },
{ time: '2023-12-05', type: 'login', number: 12 },
{ time: '2023-12-04', type: 'login', number: 345 },
{ time: '2023-12-06', type: 'login', number: 121 },
{ time: '2023-12-07', type: 'login', number: 2 },
{ time: '2023-12-08', type: 'login', number: 435 },
{ time: '2023-12-09', type: 'login', number: 1 },
{ time: '2023-12-10', type: 'login', number: 54 },
{ time: '2023-12-11', type: 'login', number: 56 }
]
const dualAxesConfig: DualAxesConfig = {
data: userStatisticsData,
slider: { x: true },
shapeField: 'smooth',
xField: 'time',
children: [
{
type: 'line',
yField: 'number',
colorField: 'type'
}
]
} }
*/
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 option: EChartsOption = {
tooltip: {},
xAxis: {
show: false
},
yAxis: {
data: ['总使用'],
axisLine: {
show: false
},
axisLabel: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisPointer: {
show: false
}
},
series: cpuInfoData
}
return (
<>
<CommonCard icon={IconFatwebCpu} title={'CPU 信息'} loading={false}>
<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>
</CommonCard>
</>
)
}
const MemoryInfo: React.FC = () => {
return (
<>
<CommonCard icon={IconFatwebMemory} title={'内存信息'} loading={true}></CommonCard>
</>
)
}
const JvmInfo: React.FC = () => {
return (
<>
<CommonCard icon={IconFatwebJava} title={'JVM 信息'} loading={true}></CommonCard>
</>
)
}
const System: React.FC = () => {
return ( return (
<> <>
<FitFullScreen> <FitFullScreen>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}> <HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox className={'root-content'}> <FlexBox direction={'horizontal'} className={'root-content'}>
<FlexBox direction={'horizontal'} className={'root-row'}> <SoftwareInfo />
<SoftwareInfo /> <HardwareInfo />
<HardwareInfo /> <CPUInfo />
</FlexBox> <MemoryInfo />
<FlexBox direction={'horizontal'} className={'root-row'}> <JvmInfo />
<Card style={{ height: '400px' }}> <div />
{/*<DualAxes {...dualAxesConfig} />*/}
</Card>
<div />
</FlexBox>
</FlexBox> </FlexBox>
</HideScrollbar> </HideScrollbar>
</FitFullScreen> </FitFullScreen>

View File

@@ -10,7 +10,10 @@ import {
URL_SYS_LOG, URL_SYS_LOG,
URL_SYS_SETTINGS_MAIL, URL_SYS_SETTINGS_MAIL,
URL_SYS_STATISTICS_SOFTWARE, URL_SYS_STATISTICS_SOFTWARE,
URL_SYS_STATISTICS_HARDWARE URL_SYS_STATISTICS_HARDWARE,
URL_SYS_STATISTICS_CPU,
URL_SYS_STATISTICS_MEMORY,
URL_SYS_STATISTICS_JVM
} from '@/constants/urls.constants' } from '@/constants/urls.constants'
import request from '@/services/index' import request from '@/services/index'
@@ -80,3 +83,9 @@ export const r_sys_statistics_software = () =>
export const r_sys_statistics_hardware = () => export const r_sys_statistics_hardware = () =>
request.get<HardwareInfoVo>(URL_SYS_STATISTICS_HARDWARE) request.get<HardwareInfoVo>(URL_SYS_STATISTICS_HARDWARE)
export const r_sys_statistics_cpu = () => request.get<CpuInfoVo>(URL_SYS_STATISTICS_CPU)
export const r_sys_statistics_memory = () => request.get<MemoryInfoVo>(URL_SYS_STATISTICS_MEMORY)
export const r_sys_statistics_jvm = () => request.get<MemoryInfoVo>(URL_SYS_STATISTICS_JVM)

View File

@@ -14,3 +14,11 @@ export const useUpdatedEffect = (
} }
}, dependencies) }, dependencies)
} }
export const usePrevious = <T,>(value: T): T | undefined => {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
})
return ref.current
}