mirror of
https://github.com/woodchen-ink/nezha-dash-v1.git
synced 2025-07-18 17:41:56 +08:00
feat: chart history on browser
This commit is contained in:
parent
1a1ceb4665
commit
112607740a
@ -4,7 +4,7 @@ import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
|||||||
import { formatBytes } from "@/lib/format"
|
import { formatBytes } from "@/lib/format"
|
||||||
import { cn, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
import { cn, formatNezhaInfo, formatRelativeTime } from "@/lib/utils"
|
||||||
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api"
|
import { NezhaServer, NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
import { Area, AreaChart, CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ type connectChartData = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ServerDetailChart({ server_id }: { server_id: string }) {
|
export default function ServerDetailChart({ server_id }: { server_id: string }) {
|
||||||
const { lastMessage, connected } = useWebSocketContext()
|
const { lastMessage, connected, messageHistory } = useWebSocketContext()
|
||||||
|
|
||||||
if (!connected && !lastMessage) {
|
if (!connected && !lastMessage) {
|
||||||
return <ServerDetailChartLoading />
|
return <ServerDetailChartLoading />
|
||||||
@ -73,48 +73,108 @@ export default function ServerDetailChart({ server_id }: { server_id: string })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3 server-charts">
|
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3 server-charts">
|
||||||
<CpuChart now={nezhaWsData.now} data={server} />
|
<CpuChart now={nezhaWsData.now} data={server} messageHistory={messageHistory} />
|
||||||
{gpuStats.length >= 1 && gpuList.length === gpuStats.length ? (
|
{gpuStats.length >= 1 && gpuList.length === gpuStats.length ? (
|
||||||
gpuList.map((gpu, index) => <GpuChart now={nezhaWsData.now} gpuStat={gpuStats[index]} gpuName={gpu} key={index} />)
|
gpuList.map((gpu, index) => (
|
||||||
|
<GpuChart
|
||||||
|
index={index}
|
||||||
|
id={server.id}
|
||||||
|
now={nezhaWsData.now}
|
||||||
|
gpuStat={gpuStats[index]}
|
||||||
|
gpuName={gpu}
|
||||||
|
messageHistory={messageHistory}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))
|
||||||
) : gpuStats.length > 0 ? (
|
) : gpuStats.length > 0 ? (
|
||||||
gpuStats.map((gpu, index) => <GpuChart now={nezhaWsData.now} gpuStat={gpu} gpuName={`#${index + 1}`} key={index} />)
|
gpuStats.map((gpu, index) => (
|
||||||
|
<GpuChart
|
||||||
|
index={index}
|
||||||
|
id={server.id}
|
||||||
|
now={nezhaWsData.now}
|
||||||
|
gpuStat={gpu}
|
||||||
|
gpuName={`#${index + 1}`}
|
||||||
|
messageHistory={messageHistory}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
<ProcessChart now={nezhaWsData.now} data={server} />
|
<ProcessChart now={nezhaWsData.now} data={server} messageHistory={messageHistory} />
|
||||||
<DiskChart now={nezhaWsData.now} data={server} />
|
<DiskChart now={nezhaWsData.now} data={server} messageHistory={messageHistory} />
|
||||||
<MemChart now={nezhaWsData.now} data={server} />
|
<MemChart now={nezhaWsData.now} data={server} messageHistory={messageHistory} />
|
||||||
<NetworkChart now={nezhaWsData.now} data={server} />
|
<NetworkChart now={nezhaWsData.now} data={server} messageHistory={messageHistory} />
|
||||||
<ConnectChart now={nezhaWsData.now} data={server} />
|
<ConnectChart now={nezhaWsData.now} data={server} messageHistory={messageHistory} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GpuChart({ now, gpuStat, gpuName }: { now: number; gpuStat: number; gpuName?: string }) {
|
function GpuChart({
|
||||||
const [gpuChartData, setGpuChartData] = useState([] as gpuChartData[])
|
id,
|
||||||
|
index,
|
||||||
|
gpuStat,
|
||||||
|
gpuName,
|
||||||
|
messageHistory,
|
||||||
|
}: {
|
||||||
|
now: number
|
||||||
|
id: number
|
||||||
|
index: number
|
||||||
|
gpuStat: number
|
||||||
|
gpuName?: string
|
||||||
|
messageHistory: { data: string }[]
|
||||||
|
}) {
|
||||||
|
const [gpuChartData, setGpuChartData] = useState<gpuChartData[]>([])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gpuStat) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === id)
|
||||||
|
if (!server) return null
|
||||||
|
const { gpu } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
gpu: gpu[index],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is gpuChartData => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setGpuChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gpuStat && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setGpuChartData((prevData) => {
|
||||||
let newData = [] as gpuChartData[]
|
let newData = [] as gpuChartData[]
|
||||||
if (gpuChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, gpu: gpuStat },
|
{ timeStamp: timestamp, gpu: gpuStat },
|
||||||
{ timeStamp: timestamp, gpu: gpuStat },
|
{ timeStamp: timestamp, gpu: gpuStat },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...gpuChartData, { timeStamp: timestamp, gpu: gpuStat }]
|
newData = [...prevData, { timeStamp: timestamp, gpu: gpuStat }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setGpuChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [now, gpuStat])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [gpuStat, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
gpu: {
|
gpu: {
|
||||||
@ -170,8 +230,10 @@ function GpuChart({ now, gpuStat, gpuName }: { now: number; gpuStat: number; gpu
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
function CpuChart({ now, data, messageHistory }: { now: number; data: NezhaServer; messageHistory: { data: string }[] }) {
|
||||||
const [cpuChartData, setCpuChartData] = useState([] as cpuChartData[])
|
const [cpuChartData, setCpuChartData] = useState<cpuChartData[]>([])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const { cpu } = formatNezhaInfo(now, data)
|
const { cpu } = formatNezhaInfo(now, data)
|
||||||
|
|
||||||
@ -179,24 +241,50 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
(window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === data.id)
|
||||||
|
if (!server) return null
|
||||||
|
const { cpu } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
cpu: cpu,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is cpuChartData => item !== null)
|
||||||
|
.reverse() // 保持时间顺序
|
||||||
|
|
||||||
|
setCpuChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
// 更新实时数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setCpuChartData((prevData) => {
|
||||||
let newData = [] as cpuChartData[]
|
let newData = [] as cpuChartData[]
|
||||||
if (cpuChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, cpu: cpu },
|
{ timeStamp: timestamp, cpu: cpu },
|
||||||
{ timeStamp: timestamp, cpu: cpu },
|
{ timeStamp: timestamp, cpu: cpu },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...cpuChartData, { timeStamp: timestamp, cpu: cpu }]
|
newData = [...prevData, { timeStamp: timestamp, cpu: cpu }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setCpuChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [data])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
cpu: {
|
cpu: {
|
||||||
@ -249,9 +337,11 @@ function CpuChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
|
function ProcessChart({ now, data, messageHistory }: { now: number; data: NezhaServer; messageHistory: { data: string }[] }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [processChartData, setProcessChartData] = useState([] as processChartData[])
|
const [processChartData, setProcessChartData] = useState([] as processChartData[])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
@ -259,24 +349,50 @@ function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
|
|
||||||
const { process } = formatNezhaInfo(now, data)
|
const { process } = formatNezhaInfo(now, data)
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === data.id)
|
||||||
|
if (!server) return null
|
||||||
|
const { process } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
process,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is processChartData => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setProcessChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
// 修改实时数据更新逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setProcessChartData((prevData) => {
|
||||||
let newData = [] as processChartData[]
|
let newData = [] as processChartData[]
|
||||||
if (processChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, process: process },
|
{ timeStamp: timestamp, process },
|
||||||
{ timeStamp: timestamp, process: process },
|
{ timeStamp: timestamp, process },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...processChartData, { timeStamp: timestamp, process: process }]
|
newData = [...prevData, { timeStamp: timestamp, process }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setProcessChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [data])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
process: {
|
process: {
|
||||||
@ -335,9 +451,11 @@ function ProcessChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
function MemChart({ now, data, messageHistory }: { now: number; data: NezhaServer; messageHistory: { data: string }[] }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
const [memChartData, setMemChartData] = useState([] as memChartData[])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
@ -345,24 +463,51 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
|
|
||||||
const { mem, swap } = formatNezhaInfo(now, data)
|
const { mem, swap } = formatNezhaInfo(now, data)
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === data.id)
|
||||||
|
if (!server) return null
|
||||||
|
const { mem, swap } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
mem,
|
||||||
|
swap,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is memChartData => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setMemChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
// 修改实时数据更新逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setMemChartData((prevData) => {
|
||||||
let newData = [] as memChartData[]
|
let newData = [] as memChartData[]
|
||||||
if (memChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, mem: mem, swap: swap },
|
{ timeStamp: timestamp, mem, swap },
|
||||||
{ timeStamp: timestamp, mem: mem, swap: swap },
|
{ timeStamp: timestamp, mem, swap },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...memChartData, { timeStamp: timestamp, mem: mem, swap: swap }]
|
newData = [...prevData, { timeStamp: timestamp, mem, swap }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setMemChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [data])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
mem: {
|
mem: {
|
||||||
@ -451,9 +596,11 @@ function MemChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
function DiskChart({ now, data, messageHistory }: { now: number; data: NezhaServer; messageHistory: { data: string }[] }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
const [diskChartData, setDiskChartData] = useState([] as diskChartData[])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
@ -461,24 +608,50 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
|
|
||||||
const { disk } = formatNezhaInfo(now, data)
|
const { disk } = formatNezhaInfo(now, data)
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === data.id)
|
||||||
|
if (!server) return null
|
||||||
|
const { disk } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
disk,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is diskChartData => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setDiskChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
// 修改实时数据更新逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setDiskChartData((prevData) => {
|
||||||
let newData = [] as diskChartData[]
|
let newData = [] as diskChartData[]
|
||||||
if (diskChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, disk: disk },
|
{ timeStamp: timestamp, disk },
|
||||||
{ timeStamp: timestamp, disk: disk },
|
{ timeStamp: timestamp, disk },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...diskChartData, { timeStamp: timestamp, disk: disk }]
|
newData = [...prevData, { timeStamp: timestamp, disk }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setDiskChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [data])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
disk: {
|
disk: {
|
||||||
@ -536,9 +709,11 @@ function DiskChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
function NetworkChart({ now, data, messageHistory }: { now: number; data: NezhaServer; messageHistory: { data: string }[] }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
|
const [networkChartData, setNetworkChartData] = useState([] as networkChartData[])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
@ -546,24 +721,51 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
|
|
||||||
const { up, down } = formatNezhaInfo(now, data)
|
const { up, down } = formatNezhaInfo(now, data)
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === data.id)
|
||||||
|
if (!server) return null
|
||||||
|
const { up, down } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
upload: up,
|
||||||
|
download: down,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is networkChartData => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setNetworkChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
// 修改实时数据更新逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setNetworkChartData((prevData) => {
|
||||||
let newData = [] as networkChartData[]
|
let newData = [] as networkChartData[]
|
||||||
if (networkChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, upload: up, download: down },
|
{ timeStamp: timestamp, upload: up, download: down },
|
||||||
{ timeStamp: timestamp, upload: up, download: down },
|
{ timeStamp: timestamp, upload: up, download: down },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...networkChartData, { timeStamp: timestamp, upload: up, download: down }]
|
newData = [...prevData, { timeStamp: timestamp, upload: up, download: down }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setNetworkChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [data])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
let maxDownload = Math.max(...networkChartData.map((item) => item.download))
|
||||||
maxDownload = Math.ceil(maxDownload)
|
maxDownload = Math.ceil(maxDownload)
|
||||||
@ -651,8 +853,10 @@ function NetworkChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
function ConnectChart({ now, data, messageHistory }: { now: number; data: NezhaServer; messageHistory: { data: string }[] }) {
|
||||||
const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
|
const [connectChartData, setConnectChartData] = useState([] as connectChartData[])
|
||||||
|
const hasInitialized = useRef(false)
|
||||||
|
const [historyLoaded, setHistoryLoaded] = useState(false)
|
||||||
|
|
||||||
const customBackgroundImage =
|
const customBackgroundImage =
|
||||||
// @ts-expect-error CustomBackgroundImage is a global variable
|
// @ts-expect-error CustomBackgroundImage is a global variable
|
||||||
@ -660,24 +864,51 @@ function ConnectChart({ now, data }: { now: number; data: NezhaServer }) {
|
|||||||
|
|
||||||
const { tcp, udp } = formatNezhaInfo(now, data)
|
const { tcp, udp } = formatNezhaInfo(now, data)
|
||||||
|
|
||||||
|
// 初始化历史数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (!hasInitialized.current && messageHistory.length > 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
const server = wsData.servers.find((s) => s.id === data.id)
|
||||||
|
if (!server) return null
|
||||||
|
const { tcp, udp } = formatNezhaInfo(wsData.now, server)
|
||||||
|
return {
|
||||||
|
timeStamp: wsData.now.toString(),
|
||||||
|
tcp,
|
||||||
|
udp,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is connectChartData => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setConnectChartData(historyData)
|
||||||
|
hasInitialized.current = true
|
||||||
|
setHistoryLoaded(true)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
// 修改实时数据更新逻辑
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && historyLoaded) {
|
||||||
const timestamp = Date.now().toString()
|
const timestamp = Date.now().toString()
|
||||||
|
setConnectChartData((prevData) => {
|
||||||
let newData = [] as connectChartData[]
|
let newData = [] as connectChartData[]
|
||||||
if (connectChartData.length === 0) {
|
if (prevData.length === 0) {
|
||||||
newData = [
|
newData = [
|
||||||
{ timeStamp: timestamp, tcp: tcp, udp: udp },
|
{ timeStamp: timestamp, tcp, udp },
|
||||||
{ timeStamp: timestamp, tcp: tcp, udp: udp },
|
{ timeStamp: timestamp, tcp, udp },
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
newData = [...connectChartData, { timeStamp: timestamp, tcp: tcp, udp: udp }]
|
newData = [...prevData, { timeStamp: timestamp, tcp, udp }]
|
||||||
}
|
|
||||||
if (newData.length > 30) {
|
if (newData.length > 30) {
|
||||||
newData.shift()
|
newData.shift()
|
||||||
}
|
}
|
||||||
setConnectChartData(newData)
|
|
||||||
}
|
}
|
||||||
}, [data])
|
return newData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [data, historyLoaded])
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
tcp: {
|
tcp: {
|
||||||
|
@ -3,9 +3,11 @@ import { createContext } from "react"
|
|||||||
export interface WebSocketContextType {
|
export interface WebSocketContextType {
|
||||||
lastMessage: { data: string } | null
|
lastMessage: { data: string } | null
|
||||||
connected: boolean
|
connected: boolean
|
||||||
|
messageHistory: { data: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WebSocketContext = createContext<WebSocketContextType>({
|
export const WebSocketContext = createContext<WebSocketContextType>({
|
||||||
lastMessage: null,
|
lastMessage: null,
|
||||||
connected: false,
|
connected: false,
|
||||||
|
messageHistory: [],
|
||||||
})
|
})
|
||||||
|
0
src/context/websocket-context.tsx
Normal file
0
src/context/websocket-context.tsx
Normal file
@ -9,6 +9,7 @@ interface WebSocketProviderProps {
|
|||||||
|
|
||||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
|
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, children }) => {
|
||||||
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
|
const [lastMessage, setLastMessage] = useState<{ data: string } | null>(null)
|
||||||
|
const [messageHistory, setMessageHistory] = useState<{ data: string }[]>([]) // 新增历史消息状态
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
const ws = useRef<WebSocket | null>(null)
|
const ws = useRef<WebSocket | null>(null)
|
||||||
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
|
const reconnectTimeout = useRef<NodeJS.Timeout>(null)
|
||||||
@ -42,7 +43,13 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
|
|||||||
}
|
}
|
||||||
|
|
||||||
ws.current.onmessage = (event) => {
|
ws.current.onmessage = (event) => {
|
||||||
setLastMessage({ data: event.data })
|
const newMessage = { data: event.data }
|
||||||
|
setLastMessage(newMessage)
|
||||||
|
// 更新历史消息,保持最新的30条记录
|
||||||
|
setMessageHistory((prev) => {
|
||||||
|
const updated = [newMessage, ...prev]
|
||||||
|
return updated.slice(0, 30)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.current.onerror = (error) => {
|
ws.current.onerror = (error) => {
|
||||||
@ -69,6 +76,7 @@ export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ url, child
|
|||||||
const contextValue: WebSocketContextType = {
|
const contextValue: WebSocketContextType = {
|
||||||
lastMessage,
|
lastMessage,
|
||||||
connected,
|
connected,
|
||||||
|
messageHistory, // 添加到 context value
|
||||||
}
|
}
|
||||||
|
|
||||||
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
|
return <WebSocketContext.Provider value={contextValue}>{children}</WebSocketContext.Provider>
|
||||||
|
26
src/hooks/use-chart-history.ts
Normal file
26
src/hooks/use-chart-history.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
export function useChartHistory<T>(
|
||||||
|
messageHistory: { data: string }[],
|
||||||
|
serverId: number,
|
||||||
|
formatFn: (wsData: NezhaWebsocketResponse, serverId: number) => T | null,
|
||||||
|
) {
|
||||||
|
const [data, setData] = useState<T[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageHistory.length > 0 && data.length === 0) {
|
||||||
|
const historyData = messageHistory
|
||||||
|
.map((msg) => {
|
||||||
|
const wsData = JSON.parse(msg.data) as NezhaWebsocketResponse
|
||||||
|
return formatFn(wsData, serverId)
|
||||||
|
})
|
||||||
|
.filter((item): item is T => item !== null)
|
||||||
|
.reverse()
|
||||||
|
|
||||||
|
setData(historyData)
|
||||||
|
}
|
||||||
|
}, [messageHistory])
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user