11import { useQuery } from "@tanstack/react-query" ;
2+ import { m } from "framer-motion" ;
23import { useEffect , useMemo , useRef , useState } from "react" ;
34import { useTranslation } from "react-i18next" ;
45import {
@@ -32,6 +33,7 @@ import type {
3233 NezhaWebsocketResponse ,
3334} from "@/types/nezha-api" ;
3435
36+ import ChartSkeleton from "./loading/ChartSkeleton" ;
3537import { ServerDetailChartLoading } from "./loading/ServerDetailLoading" ;
3638import AnimatedCircularProgressBar from "./ui/animated-circular-progress-bar" ;
3739
@@ -94,31 +96,41 @@ function PeriodSelector({
9496 ] ;
9597
9698 return (
97- < div className = "flex gap-1 mb-3 flex-wrap -mt-5" >
99+ < div className = "flex gap-0.5 mb-3 flex-wrap -mt-5 p-0.5 bg-muted dark:bg-muted/40 rounded-full w-fit border border-border/60 dark:border-border " >
98100 { periods . map ( ( period ) => {
99101 // Only realtime and 1d are available for non-logged-in users
100102 const isLocked =
101103 ! isLogin && period . value !== "realtime" && period . value !== "1d" ;
102104 return (
103- < button
105+ < div
104106 key = { period . value }
105- type = "button"
106- disabled = { isLocked }
107107 onClick = { ( ) => {
108108 if ( ! isLocked ) {
109109 onPeriodChange ( period . value ) ;
110110 }
111111 } }
112112 className = { cn (
113- "px-2.5 py-1 text-xs rounded-md transition-all " ,
113+ "relative cursor-pointer rounded-full px-3 py-1.5 text-xs font-medium transition-colors duration-300 " ,
114114 selectedPeriod === period . value
115- ? "bg-primary text-primary- foreground font-medium "
116- : "bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground" ,
117- isLocked && "cursor-not-allowed opacity-50 " ,
115+ ? "text-foreground"
116+ : "text-muted-foreground hover:text-foreground" ,
117+ isLocked && "cursor-not-allowed opacity-40 grayscale " ,
118118 ) }
119119 >
120- { period . label }
121- </ button >
120+ { selectedPeriod === period . value && (
121+ < m . div
122+ layoutId = "period-selector-active"
123+ className = "absolute inset-0 z-10 h-full w-full bg-white dark:bg-background rounded-full ring-1 ring-border/60 dark:ring-border/40"
124+ transition = { { type : "spring" , stiffness : 250 , damping : 30 } }
125+ />
126+ ) }
127+ < div className = "relative z-20 flex items-center gap-1.5" >
128+ { period . value === "realtime" && (
129+ < span className = "inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500 dark:bg-emerald-400" > </ span >
130+ ) }
131+ { period . label }
132+ </ div >
133+ </ div >
122134 ) ;
123135 } ) }
124136 </ div >
@@ -261,15 +273,24 @@ function useHistoricalData<T>(
261273) {
262274 const [ historicalData , setHistoricalData ] = useState < T [ ] > ( [ ] ) ;
263275 const [ isLoading , setIsLoading ] = useState ( false ) ;
276+ const [ displayData , setDisplayData ] = useState < T [ ] > ( [ ] ) ;
277+ const loadingTimerRef = useRef < NodeJS . Timeout | null > ( null ) ;
264278
265279 useEffect ( ( ) => {
266280 if ( period === "realtime" ) {
267281 setHistoricalData ( [ ] ) ;
282+ setDisplayData ( [ ] ) ;
283+ if ( loadingTimerRef . current ) {
284+ clearTimeout ( loadingTimerRef . current ) ;
285+ }
268286 return ;
269287 }
270288
271289 const fetchData = async ( ) => {
272- setIsLoading ( true ) ;
290+ loadingTimerRef . current = setTimeout ( ( ) => {
291+ setIsLoading ( true ) ;
292+ } , 200 ) ;
293+
273294 try {
274295 const response = await fetchServerMetrics (
275296 serverId ,
@@ -281,18 +302,23 @@ function useHistoricalData<T>(
281302 transformData ( point . ts , point . value ) ,
282303 ) ;
283304 setHistoricalData ( transformedData ) ;
305+ setDisplayData ( transformedData ) ;
284306 }
285307 } catch ( error ) {
286308 console . error ( `Failed to fetch ${ metricName } metrics:` , error ) ;
287309 } finally {
310+ if ( loadingTimerRef . current ) {
311+ clearTimeout ( loadingTimerRef . current ) ;
312+ loadingTimerRef . current = null ;
313+ }
288314 setIsLoading ( false ) ;
289315 }
290316 } ;
291317
292318 fetchData ( ) ;
293319 } , [ serverId , metricName , period , transformData ] ) ;
294320
295- return { historicalData, isLoading } ;
321+ return { historicalData, displayData , isLoading } ;
296322}
297323
298324function GpuChart ( {
@@ -328,12 +354,8 @@ function GpuChart({
328354 [ ] ,
329355 ) ;
330356
331- const { historicalData, isLoading } = useHistoricalData < gpuChartData > (
332- id ,
333- "gpu" ,
334- period ,
335- transformGpuData ,
336- ) ;
357+ const { displayData : gpuHistoricalData , isLoading } =
358+ useHistoricalData < gpuChartData > ( id , "gpu" , period , transformGpuData ) ;
337359
338360 // 初始化历史数据
339361 useEffect ( ( ) => {
@@ -397,7 +419,7 @@ function GpuChart({
397419 } ,
398420 } satisfies ChartConfig ;
399421
400- const displayData = period === "realtime" ? gpuChartData : historicalData ;
422+ const displayData = period === "realtime" ? gpuChartData : gpuHistoricalData ;
401423
402424 return (
403425 < Card
@@ -430,11 +452,7 @@ function GpuChart({
430452 className = "aspect-auto h-[130px] w-full"
431453 >
432454 { isLoading ? (
433- < div className = "flex items-center justify-center h-full" >
434- < span className = "text-xs text-muted-foreground" >
435- Loading...
436- </ span >
437- </ div >
455+ < ChartSkeleton />
438456 ) : (
439457 < AreaChart
440458 syncId = "serverDetailCharts"
@@ -532,12 +550,8 @@ function CpuChart({
532550 [ ] ,
533551 ) ;
534552
535- const { historicalData, isLoading } = useHistoricalData < cpuChartData > (
536- data . id ,
537- "cpu" ,
538- period ,
539- transformCpuData ,
540- ) ;
553+ const { displayData : cpuHistoricalData , isLoading } =
554+ useHistoricalData < cpuChartData > ( data . id , "cpu" , period , transformCpuData ) ;
541555
542556 // 初始化历史数据
543557 useEffect ( ( ) => {
@@ -602,7 +616,7 @@ function CpuChart({
602616 } ,
603617 } satisfies ChartConfig ;
604618
605- const displayData = period === "realtime" ? cpuChartData : historicalData ;
619+ const displayData = period === "realtime" ? cpuChartData : cpuHistoricalData ;
606620
607621 return (
608622 < Card
@@ -632,11 +646,7 @@ function CpuChart({
632646 className = "aspect-auto h-[130px] w-full"
633647 >
634648 { isLoading ? (
635- < div className = "flex items-center justify-center h-full" >
636- < span className = "text-xs text-muted-foreground" >
637- Loading...
638- </ span >
639- </ div >
649+ < ChartSkeleton />
640650 ) : (
641651 < AreaChart
642652 syncId = "serverDetailCharts"
@@ -737,12 +747,13 @@ function ProcessChart({
737747 [ ] ,
738748 ) ;
739749
740- const { historicalData, isLoading } = useHistoricalData < processChartData > (
741- data . id ,
742- "process_count" ,
743- period ,
744- transformProcessData ,
745- ) ;
750+ const { displayData : processHistoricalData , isLoading } =
751+ useHistoricalData < processChartData > (
752+ data . id ,
753+ "process_count" ,
754+ period ,
755+ transformProcessData ,
756+ ) ;
746757
747758 // 初始化历史数据
748759 useEffect ( ( ) => {
@@ -807,7 +818,8 @@ function ProcessChart({
807818 } ,
808819 } satisfies ChartConfig ;
809820
810- const displayData = period === "realtime" ? processChartData : historicalData ;
821+ const displayData =
822+ period === "realtime" ? processChartData : processHistoricalData ;
811823
812824 return (
813825 < Card
@@ -830,11 +842,7 @@ function ProcessChart({
830842 className = "aspect-auto h-[130px] w-full"
831843 >
832844 { isLoading ? (
833- < div className = "flex items-center justify-center h-full" >
834- < span className = "text-xs text-muted-foreground" >
835- Loading...
836- </ span >
837- </ div >
845+ < ChartSkeleton />
838846 ) : (
839847 < AreaChart
840848 syncId = "serverDetailCharts"
@@ -1114,11 +1122,7 @@ function MemChart({
11141122 className = "aspect-auto h-[130px] w-full"
11151123 >
11161124 { isLoadingMem ? (
1117- < div className = "flex items-center justify-center h-full" >
1118- < span className = "text-xs text-muted-foreground" >
1119- Loading...
1120- </ span >
1121- </ div >
1125+ < ChartSkeleton />
11221126 ) : (
11231127 < AreaChart
11241128 syncId = "serverDetailCharts"
@@ -1238,12 +1242,13 @@ function DiskChart({
12381242 [ data . host . disk_total ] ,
12391243 ) ;
12401244
1241- const { historicalData, isLoading } = useHistoricalData < diskChartData > (
1242- data . id ,
1243- "disk" ,
1244- period ,
1245- transformDiskData ,
1246- ) ;
1245+ const { displayData : diskHistoricalData , isLoading } =
1246+ useHistoricalData < diskChartData > (
1247+ data . id ,
1248+ "disk" ,
1249+ period ,
1250+ transformDiskData ,
1251+ ) ;
12471252
12481253 // 初始化历史数据
12491254 useEffect ( ( ) => {
@@ -1308,7 +1313,8 @@ function DiskChart({
13081313 } ,
13091314 } satisfies ChartConfig ;
13101315
1311- const displayData = period === "realtime" ? diskChartData : historicalData ;
1316+ const displayData =
1317+ period === "realtime" ? diskChartData : diskHistoricalData ;
13121318
13131319 return (
13141320 < Card
@@ -1344,11 +1350,7 @@ function DiskChart({
13441350 className = "aspect-auto h-[130px] w-full"
13451351 >
13461352 { isLoading ? (
1347- < div className = "flex items-center justify-center h-full" >
1348- < span className = "text-xs text-muted-foreground" >
1349- Loading...
1350- </ span >
1351- </ div >
1353+ < ChartSkeleton />
13521354 ) : (
13531355 < AreaChart
13541356 syncId = "serverDetailCharts"
@@ -1615,11 +1617,7 @@ function NetworkChart({
16151617 className = "aspect-auto h-[130px] w-full"
16161618 >
16171619 { isLoadingNetwork ? (
1618- < div className = "flex items-center justify-center h-full" >
1619- < span className = "text-xs text-muted-foreground" >
1620- Loading...
1621- </ span >
1622- </ div >
1620+ < ChartSkeleton />
16231621 ) : (
16241622 < LineChart
16251623 syncId = "serverDetailCharts"
@@ -1876,11 +1874,7 @@ function ConnectChart({
18761874 className = "aspect-auto h-[130px] w-full"
18771875 >
18781876 { isLoadingConnect ? (
1879- < div className = "flex items-center justify-center h-full" >
1880- < span className = "text-xs text-muted-foreground" >
1881- Loading...
1882- </ span >
1883- </ div >
1877+ < ChartSkeleton />
18841878 ) : (
18851879 < LineChart
18861880 syncId = "serverDetailCharts"
0 commit comments