import { useCallback, useEffect, useState } from 'react' import { View, Text, ScrollView, StyleSheet, RefreshControl } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import { useBind } from '@/context/BindContext' import { apiFetch } from '@/lib/api' import type { SystemMetricsPayload } from '@/lib/pcTypes' import { theme } from '@/constants/theme' function fmtBytes(n: number | undefined): string { if (n == null || Number.isNaN(n)) return '—' if (n < 1024) return `${n} B` if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB` if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(1)} MB` return `${(n / 1024 ** 3).toFixed(2)} GB` } export default function SystemScreen() { const { bind } = useBind() const [data, setData] = useState(null) const [err, setErr] = useState(null) const [refreshing, setRefreshing] = useState(false) const load = useCallback(async () => { if (!bind) { setData(null) setErr(null) return } try { const res = await apiFetch(bind, '/system_metrics') const json = (await res.json()) as SystemMetricsPayload if (!res.ok) { setErr(json.error || `HTTP ${res.status}`) return } setErr(null) setData(json) } catch (e) { setErr(e instanceof Error ? e.message : String(e)) } }, [bind]) useEffect(() => { void load() }, [load]) useEffect(() => { if (!bind) return const id = setInterval(() => void load(), 2000) return () => clearInterval(id) }, [bind, load]) const onRefresh = useCallback(async () => { setRefreshing(true) await load() setRefreshing(false) }, [load]) const m = data?.memory const net = data?.network return ( void onRefresh()} tintColor={theme.accent} /> ) : undefined } > 系统 本机负载与网速(与桌面端「系统」页同源)。开机启动等仅在 Windows 客户端可设置。 {!bind && ( 未绑定 PC:配对后每 2 秒刷新指标。 )} 本机负载与网速 网速为全机网卡合计估算;下方列出检测到的 ffmpeg 进程。 {err && bind ? ( {err} ) : null} CPU {data?.cpu_percent != null ? `${data.cpu_percent.toFixed(1)}%` : '—'} 逻辑处理器 {data?.cpu_count_logical ?? '—'} 个 内存 {m?.percent != null ? `${m.percent.toFixed(1)}%` : '—'} {fmtBytes(m?.used)} / {fmtBytes(m?.total)}(可用 {fmtBytes(m?.available)}) 全机网速 ↑ {net?.up_mbps != null ? `${net.up_mbps.toFixed(2)} Mbps` : '—'} ↓ {net?.down_mbps != null ? `${net.down_mbps.toFixed(2)} Mbps` : '—'} 含所有网卡收发合计 {data?.swap && data.swap.total != null && data.swap.total > 0 ? ( 交换分区 {data.swap.percent != null ? `${data.swap.percent.toFixed(0)}%` : '—'} ·{' '} {fmtBytes(data.swap.used)} / {fmtBytes(data.swap.total)} ) : null} {data?.disks && data.disks.length > 0 ? ( 磁盘空间 {data.disks.slice(0, 8).map((d, i) => ( {d.mountpoint} ·{' '} {d.percent != null ? `${d.percent.toFixed(0)}%` : '—'} 已用 · 剩余 {fmtBytes(d.free)} ))} ) : null} {data?.ffmpeg_processes && data.ffmpeg_processes.length > 0 ? ( ffmpeg 进程 {data.ffmpeg_processes.map((p, i) => ( PID {p.pid} · {p.name ?? '—'} · {fmtBytes(p.rss)} ))} ) : null} 客户端与系统集成 开机启动、路径与 Electron 集成仅在桌面客户端可用。 ) } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: theme.bg }, scroll: { padding: 16, paddingBottom: 40 }, h1: { fontSize: 20, fontWeight: '700', color: theme.text, marginBottom: 6 }, lead: { fontSize: 14, color: theme.muted, marginBottom: 14, lineHeight: 22 }, banner: { padding: 12, borderRadius: 10, backgroundColor: 'rgba(124,92,255,0.08)', borderWidth: 1, borderColor: 'rgba(124,92,255,0.22)', marginBottom: 14, }, bannerText: { fontSize: 13, color: theme.text, lineHeight: 20 }, card: { padding: 14, borderRadius: 12, backgroundColor: theme.bgCard, borderWidth: 1, borderColor: 'rgba(124,92,255,0.22)', marginBottom: 12, }, cardMuted: { padding: 14, borderRadius: 12, backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border, }, cardTitle: { fontSize: 16, fontWeight: '600', color: theme.text, marginBottom: 8 }, desc: { fontSize: 13, color: theme.muted, marginBottom: 12, lineHeight: 20 }, err: { color: theme.danger, marginBottom: 10, fontSize: 13 }, metricsGrid: { gap: 12 }, block: { padding: 12, borderRadius: 10, backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border, }, blockTitle: { fontSize: 11, fontWeight: '700', color: theme.muted, marginBottom: 6 }, big: { fontSize: 22, fontWeight: '700', color: theme.text }, sub: { fontSize: 11, color: theme.muted, marginTop: 4, lineHeight: 16 }, rate: { fontSize: 15, fontWeight: '600', color: theme.accent, fontFamily: 'monospace' }, swap: { fontSize: 12, color: theme.muted, marginTop: 10, marginBottom: 8 }, diskSec: { marginTop: 8 }, diskTitle: { fontSize: 13, fontWeight: '600', color: theme.text, marginBottom: 8 }, diskLine: { fontSize: 12, color: theme.muted, marginBottom: 6 }, diskMp: { fontFamily: 'monospace', color: theme.text }, ffSec: { marginTop: 12 }, ffLine: { fontSize: 12, color: theme.muted, marginBottom: 4, fontFamily: 'monospace' }, muted: { fontSize: 13, color: theme.muted, lineHeight: 20 }, })