Complete main UI #37

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

1141
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -3,9 +3,12 @@
.root-content {
padding: 30px;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
.root-row {
gap: 20px;
>.card-box {
width: 48%;
flex: 0 0 auto;
.common-card {
padding: 20px;
@@ -48,6 +51,15 @@
}
}
.value-chart {
>div {
height: 12px;
>* {
transform: translateY(1px);
}
}
}
> * {
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_SOFTWARE = `${URL_SYS_STATISTICS}/software`
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_AVATAR_RANDOM_BASE64 = `${URL_API_V1}/avatar/base64`

21
src/global.d.ts vendored
View File

@@ -351,3 +351,24 @@ interface HardwareInfoVo {
memories: 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 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 { useUpdatedEffect } from '@/util/hooks'
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 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])
type EChartsOption = echarts.ComposeOption<
TooltipComponentOption | GridComponentOption | BarSeriesOption
>
interface CommonCardProps extends React.PropsWithChildren {
icon: IconComponent
title: string
@@ -19,7 +36,7 @@ interface CommonCardProps extends React.PropsWithChildren {
const CommonCard: React.FC<CommonCardProps> = (props) => {
return (
<Card>
<Card style={{ overflow: 'visible' }}>
<FlexBox className={'common-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
@@ -27,7 +44,7 @@ const CommonCard: React.FC<CommonCardProps> = (props) => {
</FlexBox>
<LoadingMask
hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 10 }} />}
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
>
{props.children}
</LoadingMask>
@@ -124,9 +141,9 @@ const HardwareInfo: React.FC = () => {
<div>CPU </div>
<div></div>
<div>64</div>
<div>CPU </div>
<div>CPU </div>
<div>CPU </div>
<div> CPU</div>
<div></div>
<div></div>
<div></div>
<div></div>
</FlexBox>
@@ -156,76 +173,122 @@ const HardwareInfo: React.FC = () => {
)
}
const CPUInfo: React.FC = () => {
const [cpuInfoData, setCpuInfoData] = useState<BarSeriesOption[]>()
const defaultSeriesOption: BarSeriesOption = {
type: 'bar',
stack: 'total',
emphasis: {
focus: 'series'
}
}
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 = () => {
/*
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 = [
{ time: '2023-12-01', type: 'register', number: 23 },
{ time: '2023-12-02', type: 'register', number: 123 },
{ time: '2023-12-03', type: 'register', number: 1432 },
{ time: '2023-12-05', type: 'register', number: 1 },
{ 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'
}
]
}
*/
return (
<>
<FitFullScreen>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox className={'root-content'}>
<FlexBox direction={'horizontal'} className={'root-row'}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<SoftwareInfo />
<HardwareInfo />
</FlexBox>
<FlexBox direction={'horizontal'} className={'root-row'}>
<Card style={{ height: '400px' }}>
{/*<DualAxes {...dualAxesConfig} />*/}
</Card>
<CPUInfo />
<MemoryInfo />
<JvmInfo />
<div />
</FlexBox>
</FlexBox>
</HideScrollbar>
</FitFullScreen>
</>

View File

@@ -10,7 +10,10 @@ import {
URL_SYS_LOG,
URL_SYS_SETTINGS_MAIL,
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'
import request from '@/services/index'
@@ -80,3 +83,9 @@ export const r_sys_statistics_software = () =>
export const r_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)
}
export const usePrevious = <T,>(value: T): T | undefined => {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
})
return ref.current
}