import { useCallback, useEffect, useState } from 'react' import { useFocusEffect } from 'expo-router' import { View, Text, ScrollView, StyleSheet, RefreshControl, Pressable, } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import { useBind } from '@/context/BindContext' import { apiFetch } from '@/lib/api' import type { ClientOverview, NetworkPayload, RelayLivePayload, SystemMetricsPayload } from '@/lib/pcTypes' import { getMultiChannelPro } from '@/lib/storage' import { theme } from '@/constants/theme' function fmtTs(ts: unknown): string { if (typeof ts !== 'number') return '—' try { return new Date(ts).toLocaleString() } catch { return '—' } } function fmtDur(sec: number | null | undefined): string { if (sec == null || Number.isNaN(sec)) return '—' const s = Math.floor(sec) const h = Math.floor(s / 3600) const m = Math.floor((s % 3600) / 60) const r = s % 60 if (h > 0) return `${h}h${m}m${r}s` if (m > 0) return `${m}m${r}s` return `${r}s` } function SnapshotNet({ data }: { data: unknown }) { if (!data || typeof data !== 'object') return const d = data as Record if ('google' in d && 'douyin' in d) { const g = d.google as { reachable?: boolean; latency_ms?: number; country?: string } const y = d.douyin as { reachable?: boolean; latency_ms?: number; country?: string } return ( Google {g?.reachable ? '可达' : '不可达'} ·{' '} {g?.latency_ms != null && g.latency_ms >= 0 ? `${g.latency_ms.toFixed(0)} ms` : '—'} · {g?.country ?? '—'} 抖音 {y?.reachable ? '可达' : '不可达'} ·{' '} {y?.latency_ms != null && y.latency_ms >= 0 ? `${y.latency_ms.toFixed(0)} ms` : '—'} · {y?.country ?? '—'} ) } return } function SnapshotSys({ data }: { data: unknown }) { if (!data || typeof data !== 'object') return const d = data as Record if ('cpu_percent' in d || 'memory' in d) { const mem = d.memory as { percent?: number } | undefined return ( CPU {typeof d.cpu_percent === 'number' ? `${d.cpu_percent.toFixed(1)}%` : '—'} 内存 {mem?.percent != null ? `${mem.percent.toFixed(1)}%` : '—'} ) } return } export default function DeskScreen() { const { bind } = useBind() const [overview, setOverview] = useState(null) const [proLive, setProLive] = useState(null) const [proMode, setProMode] = useState(false) const [netSnap, setNetSnap] = useState(null) const [sysSnap, setSysSnap] = useState(null) const [err, setErr] = useState('') const [refreshing, setRefreshing] = useState(false) const load = useCallback(async () => { if (!bind) { setOverview(null) setProLive(null) setNetSnap(null) setSysSnap(null) setErr('') return } setErr('') try { const r = await apiFetch(bind, '/client/overview') if (r.ok) setOverview((await r.json()) as ClientOverview) else setOverview(null) const multi = await getMultiChannelPro() setProMode(multi) if (multi) { const lr = await apiFetch(bind, '/relay_pro/live_status') if (lr.ok) setProLive((await lr.json()) as RelayLivePayload) else setProLive(null) } else { setProLive(null) } const nr = await apiFetch(bind, '/network_status') if (nr.ok) setNetSnap((await nr.json()) as NetworkPayload) else setNetSnap(null) const sr = await apiFetch(bind, '/system_metrics') if (sr.ok) setSysSnap((await sr.json()) as SystemMetricsPayload) else setSysSnap(null) } catch (e) { setErr(e instanceof Error ? e.message : String(e)) } }, [bind]) useFocusEffect( useCallback(() => { void load() }, [load]), ) useEffect(() => { if (!bind || !proMode) return const id = setInterval(() => void load(), 12000) return () => clearInterval(id) }, [bind, proMode, load]) const onRefresh = useCallback(async () => { setRefreshing(true) await load() setRefreshing(false) }, [load]) return ( void onRefresh()} tintColor={theme.accent} /> ) : undefined } > 仪表盘 void load()}> 刷新 与桌面端同源:概览、Pro 实况、网络/负载快照。事件与客户端日志仅在桌面端 SQLite。App 仅远程控制 PC,手机不运行 FFmpeg。 {!bind && ( 未绑定 PC:配对后显示频道摘要与转播状态。 )} {!!err && bind ? {err} : null} {overview?.relayValidationErrors && overview.relayValidationErrors.length > 0 ? ( {overview.relayValidationErrors.map((x, i) => ( {x} ))} ) : null} {proMode && ( Pro 转播实况 {proLive?.youtubeProcess ?? '—'} · 状态 {proLive?.processStatus ?? '—'}{' '} {proLive?.processOnline ? ( 运行中 ) : ( 未在运行 )} {(proLive?.relayRows?.length ? proLive.relayRows : []).map((row, i) => ( {row.channelName || '未命名频道'} · key …{row.youtubeKeySuffix || '—'} 主播 {row.anchor ?? '—'} 抖音 {row.douyinHint ?? '—'} 标题 {row.streamTitle ?? '—'} 已播时长 {fmtDur(row.durationSec)} ))} {!proLive?.relayRows?.length ? 暂无实况 : null} )} 网络快照 更新 {fmtTs(netSnap?.meta?.at)} 本机负载快照 更新 {fmtTs(sysSnap?.ts)} 直播与任务事件 完整事件表、用户操作与客户端运行日志由桌面端本地数据库维护;请在 Windows 客户端查看明细。 ) } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: theme.bg }, scroll: { padding: 16, paddingBottom: 40 }, head: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }, h1: { fontSize: 20, fontWeight: '700', color: theme.text }, lead: { fontSize: 14, color: theme.muted, marginBottom: 14, lineHeight: 22 }, banner: { padding: 12, borderRadius: 10, backgroundColor: 'rgba(91,157,255,0.1)', borderWidth: 1, borderColor: 'rgba(91,157,255,0.25)', marginBottom: 14, }, bannerText: { fontSize: 13, color: theme.text, lineHeight: 20 }, btnSec: { paddingVertical: 8, paddingHorizontal: 14, borderRadius: 8, borderWidth: 1, borderColor: theme.border, backgroundColor: theme.bgElevated, }, btnSecText: { color: theme.text, fontWeight: '600' }, err: { color: theme.danger, marginBottom: 10 }, warnBox: { padding: 10, borderRadius: 8, backgroundColor: 'rgba(245,101,101,0.1)', marginBottom: 12, }, warnText: { color: theme.danger, fontSize: 13, marginBottom: 4 }, card: { padding: 14, borderRadius: 12, backgroundColor: theme.bgCard, borderWidth: 1, borderColor: 'rgba(110,168,254,0.25)', marginBottom: 12, }, cardTitle: { fontSize: 16, fontWeight: '600', color: theme.text, marginBottom: 10 }, meta: { fontSize: 13, color: theme.muted, marginBottom: 8 }, tagOn: { color: theme.success, fontWeight: '700' }, tagOff: { color: theme.warn, fontWeight: '700' }, liveRow: { paddingVertical: 10, borderTopWidth: 1, borderTopColor: theme.border, }, liveTitle: { fontSize: 14, fontWeight: '600', color: theme.text, marginBottom: 4 }, liveLine: { fontSize: 12, color: theme.muted, marginTop: 2 }, rowCards: { flexDirection: 'row', gap: 10, marginBottom: 12 }, miniCard: { padding: 12, borderRadius: 10, backgroundColor: theme.bgElevated, borderWidth: 1, borderColor: theme.border, minWidth: 0, }, miniTitle: { fontSize: 13, fontWeight: '600', color: theme.text, marginBottom: 4 }, miniMeta: { fontSize: 11, color: theme.muted, marginBottom: 8 }, snapGrid: { gap: 6 }, snapLine: { fontSize: 12, color: theme.text, lineHeight: 18 }, muted: { fontSize: 13, color: theme.muted, lineHeight: 20 }, })