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 (
-
-
-
- } 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 (
+
+ )
+}
+
+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}
+
+ )
+}