's'
This commit is contained in:
208
app/(tabs)/system.tsx
Normal file
208
app/(tabs)/system.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
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<SystemMetricsPayload | null>(null)
|
||||
const [err, setErr] = useState<string | null>(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 (
|
||||
<SafeAreaView style={styles.safe} edges={['bottom']}>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
refreshControl={
|
||||
bind ? (
|
||||
<RefreshControl refreshing={refreshing} onRefresh={() => void onRefresh()} tintColor={theme.accent} />
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<Text style={styles.h1}>系统</Text>
|
||||
<Text style={styles.lead}>本机负载与网速(与桌面端「系统」页同源)。开机启动等仅在 Windows 客户端可设置。</Text>
|
||||
|
||||
{!bind && (
|
||||
<View style={styles.banner}>
|
||||
<Text style={styles.bannerText}>未绑定 PC:配对后每 2 秒刷新指标。</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>本机负载与网速</Text>
|
||||
<Text style={styles.desc}>
|
||||
网速为全机网卡合计估算;下方列出检测到的 ffmpeg 进程。
|
||||
</Text>
|
||||
{err && bind ? (
|
||||
<Text style={styles.err}>{err}</Text>
|
||||
) : null}
|
||||
|
||||
<View style={styles.metricsGrid}>
|
||||
<View style={styles.block}>
|
||||
<Text style={styles.blockTitle}>CPU</Text>
|
||||
<Text style={styles.big}>{data?.cpu_percent != null ? `${data.cpu_percent.toFixed(1)}%` : '—'}</Text>
|
||||
<Text style={styles.sub}>逻辑处理器 {data?.cpu_count_logical ?? '—'} 个</Text>
|
||||
</View>
|
||||
<View style={styles.block}>
|
||||
<Text style={styles.blockTitle}>内存</Text>
|
||||
<Text style={styles.big}>{m?.percent != null ? `${m.percent.toFixed(1)}%` : '—'}</Text>
|
||||
<Text style={styles.sub}>
|
||||
{fmtBytes(m?.used)} / {fmtBytes(m?.total)}(可用 {fmtBytes(m?.available)})
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.block}>
|
||||
<Text style={styles.blockTitle}>全机网速</Text>
|
||||
<Text style={styles.rate}>
|
||||
↑ {net?.up_mbps != null ? `${net.up_mbps.toFixed(2)} Mbps` : '—'}
|
||||
</Text>
|
||||
<Text style={styles.rate}>
|
||||
↓ {net?.down_mbps != null ? `${net.down_mbps.toFixed(2)} Mbps` : '—'}
|
||||
</Text>
|
||||
<Text style={styles.sub}>含所有网卡收发合计</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{data?.swap && data.swap.total != null && data.swap.total > 0 ? (
|
||||
<Text style={styles.swap}>
|
||||
交换分区 {data.swap.percent != null ? `${data.swap.percent.toFixed(0)}%` : '—'} ·{' '}
|
||||
{fmtBytes(data.swap.used)} / {fmtBytes(data.swap.total)}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{data?.disks && data.disks.length > 0 ? (
|
||||
<View style={styles.diskSec}>
|
||||
<Text style={styles.diskTitle}>磁盘空间</Text>
|
||||
{data.disks.slice(0, 8).map((d, i) => (
|
||||
<Text key={i} style={styles.diskLine}>
|
||||
<Text style={styles.diskMp}>{d.mountpoint}</Text> ·{' '}
|
||||
{d.percent != null ? `${d.percent.toFixed(0)}%` : '—'} 已用 · 剩余 {fmtBytes(d.free)}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{data?.ffmpeg_processes && data.ffmpeg_processes.length > 0 ? (
|
||||
<View style={styles.ffSec}>
|
||||
<Text style={styles.diskTitle}>ffmpeg 进程</Text>
|
||||
{data.ffmpeg_processes.map((p, i) => (
|
||||
<Text key={i} style={styles.ffLine}>
|
||||
PID {p.pid} · {p.name ?? '—'} · {fmtBytes(p.rss)}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={styles.cardMuted}>
|
||||
<Text style={styles.cardTitle}>客户端与系统集成</Text>
|
||||
<Text style={styles.muted}>开机启动、路径与 Electron 集成仅在桌面客户端可用。</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
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 },
|
||||
})
|
||||
Reference in New Issue
Block a user