diff --git a/web/src/pages/StatsPage.tsx b/web/src/pages/StatsPage.tsx index 1426af3..19913e9 100644 --- a/web/src/pages/StatsPage.tsx +++ b/web/src/pages/StatsPage.tsx @@ -1,27 +1,13 @@ import { useEffect, useState } from 'react' -import { Activity, Cpu, Database, Film, HardDrive, Radio, Users } from 'lucide-react' import { statsAPI } from '../api/stats' import type { Hardware, StatsSnapshot } from '../types' - -// fmtBytes is a tiny helper shared by the dashboard cards. -function fmtBytes(n: number): string { - if (!n || n <= 0) return '—' - const u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] - let v = n - let i = 0 - while (v >= 1024 && i < u.length - 1) { - v /= 1024 - i++ - } - return `${v.toFixed(2)} ${u[i]}` -} - -function fmtHours(seconds: number): string { - if (!seconds || seconds <= 0) return '—' - const h = Math.floor(seconds / 3600) - return `${h.toLocaleString()} h` -} +import { + AggregateStatsSection, + StatsHeader, + StatsTiles, + SystemMonitorSection, +} from './StatsPageSections' // StatsPage renders the operator dashboard. Aggregate stats refresh less often, // while hardware metrics are polled every 2 s for a real-time monitoring feel. @@ -90,136 +76,19 @@ export function StatsPage() { return (
-
-
-

运行状态

-

- 聚合快照:{new Date(snap.generated_at).toLocaleString()} · 实时监控每 2 秒刷新 -

-
-
- - - - -
-

- {monitorError ? '监控重试中' : '实时监控中'} -

-

- 最近刷新:{lastMonitorAt ? new Date(lastMonitorAt).toLocaleTimeString() : '—'} -

-
-
-
- -
- } label="媒体库" value={snap.libraries.toLocaleString()} /> - } label="媒体总数" value={snap.media_count.toLocaleString()} /> - } label="用户" value={snap.users_count.toLocaleString()} /> - } label="入库容量" value={fmtBytes(snap.total_size_bytes)} /> - } label="累计时长" value={fmtHours(snap.total_seconds)} /> - } label="CPU 占用" value={`${live.cpu_percent.toFixed(1)}%`} meter={live.cpu_percent} /> - } label="内存占用" value={`${memPct.toFixed(1)}%`} /> - } label="数据盘占用" value={`${diskPct.toFixed(1)}%`} /> -
- -
-
-

系统实时监控

- - - Live - -
-
-
- - - - -
-
- - - - {monitorError &&

实时监控错误:{monitorError}

} -
-
-
- -
-

聚合统计

-
- - -
-
-
- ) -} - -function Tile({ - icon, - label, - value, - meter, -}: { - icon: React.ReactNode - label: string - value: string - meter?: number -}) { - return ( -
-
- {icon} -
-
-

{label}

-

{value}

- {typeof meter === 'number' && } -
-
- ) -} - -function Meter({ label, value }: { label: string; value: number }) { - return ( -
-
- {label} - {value.toFixed(1)}% -
- -
- ) -} - -function Bar({ value }: { value: number }) { - const pct = Math.max(0, Math.min(100, value || 0)) - const color = pct > 85 ? 'bg-red-500' : pct > 65 ? 'bg-amber-500' : 'bg-emerald-500' - return ( -
-
-
- ) -} - -function Row({ label, value }: { label: string; value: string }) { - return ( -
- {label} - {value} + + + +
) } diff --git a/web/src/pages/StatsPageSections.tsx b/web/src/pages/StatsPageSections.tsx new file mode 100644 index 0000000..406becf --- /dev/null +++ b/web/src/pages/StatsPageSections.tsx @@ -0,0 +1,197 @@ +import type { ReactNode } from 'react' +import { Activity, Cpu, Database, Film, HardDrive, Radio, Users } from 'lucide-react' + +import type { Hardware, StatsSnapshot } from '../types' + +function fmtBytes(n: number): string { + if (!n || n <= 0) return '—' + const u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] + let v = n + let i = 0 + while (v >= 1024 && i < u.length - 1) { + v /= 1024 + i++ + } + return `${v.toFixed(2)} ${u[i]}` +} + +function fmtHours(seconds: number): string { + if (!seconds || seconds <= 0) return '—' + const h = Math.floor(seconds / 3600) + return `${h.toLocaleString()} h` +} + +export function StatsHeader({ + generatedAt, + lastMonitorAt, + monitorError, +}: { + generatedAt: string + lastMonitorAt: string + monitorError: string +}) { + return ( +
+
+

运行状态

+

+ 聚合快照:{new Date(generatedAt).toLocaleString()} · 实时监控每 2 秒刷新 +

+
+
+ + + + +
+

+ {monitorError ? '监控重试中' : '实时监控中'} +

+

+ 最近刷新:{lastMonitorAt ? new Date(lastMonitorAt).toLocaleTimeString() : '—'} +

+
+
+
+ ) +} + +export function StatsTiles({ + snap, + live, + memPct, + diskPct, +}: { + snap: StatsSnapshot + live: Hardware + memPct: number + diskPct: number +}) { + return ( +
+ } label="媒体库" value={snap.libraries.toLocaleString()} /> + } label="媒体总数" value={snap.media_count.toLocaleString()} /> + } label="用户" value={snap.users_count.toLocaleString()} /> + } label="入库容量" value={fmtBytes(snap.total_size_bytes)} /> + } label="累计时长" value={fmtHours(snap.total_seconds)} /> + } label="CPU 占用" value={`${live.cpu_percent.toFixed(1)}%`} meter={live.cpu_percent} /> + } label="内存占用" value={`${memPct.toFixed(1)}%`} /> + } label="数据盘占用" value={`${diskPct.toFixed(1)}%`} /> +
+ ) +} + +export function SystemMonitorSection({ + live, + memPct, + diskPct, + monitorError, +}: { + live: Hardware + memPct: number + diskPct: number + monitorError: string +}) { + return ( +
+
+

系统实时监控

+ + + Live + +
+
+
+ + + + +
+
+ + + + {monitorError &&

实时监控错误:{monitorError}

} +
+
+
+ ) +} + +export function AggregateStatsSection({ generatedAt }: { generatedAt: string }) { + return ( +
+

聚合统计

+
+ + +
+
+ ) +} + +function Tile({ + icon, + label, + value, + meter, +}: { + icon: ReactNode + label: string + value: string + meter?: number +}) { + return ( +
+
+ {icon} +
+
+

{label}

+

{value}

+ {typeof meter === 'number' && } +
+
+ ) +} + +function Meter({ label, value }: { label: string; value: number }) { + return ( +
+
+ {label} + {value.toFixed(1)}% +
+ +
+ ) +} + +function Bar({ value }: { value: number }) { + const pct = Math.max(0, Math.min(100, value || 0)) + const color = pct > 85 ? 'bg-red-500' : pct > 65 ? 'bg-amber-500' : 'bg-emerald-500' + return ( +
+
+
+ ) +} + +function Row({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +}