232 lines
9.8 KiB
TypeScript
232 lines
9.8 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import {
|
||
View,
|
||
Text,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
StyleSheet,
|
||
ScrollView,
|
||
Linking,
|
||
ActivityIndicator,
|
||
KeyboardAvoidingView,
|
||
Platform,
|
||
} from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
import { router } from 'expo-router';
|
||
import { useConnectionStore } from '../src/stores/connectionStore';
|
||
import { useDevice } from '../src/hooks/useDevice';
|
||
import { useTheme } from '../src/design/tokens';
|
||
|
||
const LOG_MAX = 60;
|
||
|
||
export default function ConnectScreen() {
|
||
const theme = useTheme();
|
||
const { host, port, status, lastError, setHost, setPort } = useConnectionStore();
|
||
const { connect, disconnect } = useDevice();
|
||
const [logs, setLogs] = useState<string[]>(['Ready.']);
|
||
const [portStr, setPortStr] = useState(String(port));
|
||
const prevStatusRef = useRef(status);
|
||
|
||
const addLog = (msg: string) => {
|
||
setLogs((prev) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev].slice(0, LOG_MAX));
|
||
};
|
||
|
||
useEffect(() => {
|
||
const prev = prevStatusRef.current;
|
||
prevStatusRef.current = status;
|
||
if (status === prev) return;
|
||
if (status === 'connected') {
|
||
addLog('连接成功 ✓');
|
||
router.push('/(tabs)/control');
|
||
} else if (status === 'error') {
|
||
addLog(`错误: ${lastError}`);
|
||
} else if (status === 'reconnecting') {
|
||
addLog('断线,正在重连...');
|
||
}
|
||
}, [status, lastError]);
|
||
|
||
const handleConnect = () => {
|
||
const p = parseInt(portStr, 10);
|
||
if (!host || isNaN(p)) { addLog('请检查 IP 和端口'); return; }
|
||
setPort(p);
|
||
addLog(`正在连接 ${host}:${p} ...`);
|
||
connect();
|
||
};
|
||
|
||
const handleDisconnect = () => {
|
||
disconnect();
|
||
addLog('已断开连接');
|
||
};
|
||
|
||
const isConnecting = status === 'connecting' || status === 'reconnecting';
|
||
const isConnected = status === 'connected';
|
||
|
||
return (
|
||
<SafeAreaView style={[S.safe, { backgroundColor: theme.bg.base }]}>
|
||
<KeyboardAvoidingView style={S.flex} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
||
<ScrollView style={S.scroll} contentContainerStyle={S.scrollContent} keyboardShouldPersistTaps="handled">
|
||
|
||
{/* ── Brand ── */}
|
||
<View style={S.brand}>
|
||
<Text style={[S.brandIcon, { color: theme.blue.fg }]}>◈</Text>
|
||
<Text style={[S.title, { color: theme.text.primary }]}>TEM Receiver</Text>
|
||
<Text style={[S.subtitle, { color: theme.text.muted }]}>瞬变电磁接收机上位机</Text>
|
||
</View>
|
||
|
||
{/* ── Setup guide ── */}
|
||
<View style={[S.card, { backgroundColor: theme.bg.raised, borderColor: theme.bg.border }]}>
|
||
<Text style={[S.cardLabel, { color: theme.text.muted }]}>连接步骤</Text>
|
||
<View style={S.stepRow}>
|
||
<View style={[S.stepBadge, { backgroundColor: theme.blue.bg }]}><Text style={[S.stepNum, { color: theme.blue.fg }]}>1</Text></View>
|
||
<Text style={[S.stepText, { color: theme.text.secondary }]}>手机连接设备 WiFi 热点</Text>
|
||
<TouchableOpacity style={[S.linkBtn, { backgroundColor: theme.blue.bg, borderColor: theme.blue.border }]} onPress={() => Linking.openSettings()}>
|
||
<Text style={[S.linkBtnText, { color: theme.blue.fg }]}>设置 →</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
<View style={S.stepRow}>
|
||
<View style={[S.stepBadge, { backgroundColor: theme.blue.bg }]}><Text style={[S.stepNum, { color: theme.blue.fg }]}>2</Text></View>
|
||
<Text style={[S.stepText, { color: theme.text.secondary }]}>确认参数,点击连接</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* ── Inputs ── */}
|
||
<View style={[S.card, { backgroundColor: theme.bg.raised, borderColor: theme.bg.border }]}>
|
||
<Text style={[S.cardLabel, { color: theme.text.muted }]}>连接参数</Text>
|
||
<View style={S.inputRow}>
|
||
<Text style={[S.inputLabel, { color: theme.text.secondary }]}>IP 地址</Text>
|
||
<TextInput
|
||
style={[S.input, { color: theme.text.primary, borderBottomColor: theme.bg.border }]}
|
||
value={host}
|
||
onChangeText={setHost}
|
||
keyboardType="numeric"
|
||
placeholder="192.168.4.1"
|
||
placeholderTextColor={theme.text.muted}
|
||
autoCapitalize="none"
|
||
/>
|
||
</View>
|
||
<View style={[S.inputDivider, { backgroundColor: theme.bg.border }]} />
|
||
<View style={S.inputRow}>
|
||
<Text style={[S.inputLabel, { color: theme.text.secondary }]}>端 口</Text>
|
||
<TextInput
|
||
style={[S.input, { color: theme.text.primary, borderBottomColor: theme.bg.border }]}
|
||
value={portStr}
|
||
onChangeText={setPortStr}
|
||
keyboardType="numeric"
|
||
placeholder="4321"
|
||
placeholderTextColor={theme.text.muted}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* ── Action ── */}
|
||
{!isConnected ? (
|
||
<TouchableOpacity
|
||
style={[S.connectBtn, { backgroundColor: theme.blue.border, borderColor: theme.blue.fg }, isConnecting && { borderColor: theme.text.ghost, backgroundColor: theme.blue.bg }]}
|
||
onPress={handleConnect}
|
||
disabled={isConnecting}
|
||
activeOpacity={0.8}
|
||
>
|
||
{isConnecting
|
||
? <ActivityIndicator color={theme.text.primary} />
|
||
: <Text style={[S.connectBtnText, { color: theme.blue.fg }]}>连 接 设 备</Text>}
|
||
</TouchableOpacity>
|
||
) : (
|
||
<View style={S.connectedBox}>
|
||
<View style={S.connectedLeft}>
|
||
<View style={[S.greenDot, { backgroundColor: theme.green.fg }]} />
|
||
<Text style={[S.connectedText, { color: theme.green.fg }]}>已连接</Text>
|
||
</View>
|
||
<TouchableOpacity style={[S.goBtn, { backgroundColor: theme.green.bg, borderColor: theme.green.border }]} onPress={() => router.push('/(tabs)/control')} activeOpacity={0.8}>
|
||
<Text style={[S.goBtnText, { color: theme.green.fg }]}>进入控制台 →</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity style={[S.disconnectBtn, { backgroundColor: theme.red.bg, borderColor: theme.red.border }]} onPress={handleDisconnect} activeOpacity={0.8}>
|
||
<Text style={[S.disconnectBtnText, { color: theme.red.fg }]}>断开</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
|
||
{/* ── Log terminal ── */}
|
||
<View style={[S.terminal, { backgroundColor: theme.bg.void, borderColor: theme.bg.border }]}>
|
||
<Text style={[S.terminalHeader, { color: theme.green.fg, borderBottomColor: theme.bg.divider }]}>● 连接日志</Text>
|
||
<View style={S.terminalBody}>
|
||
{logs.map((l, i) => (
|
||
<Text key={i} style={[S.terminalLine, { color: theme.text.muted }, i === 0 && { color: theme.text.secondary }]}>{l}</Text>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
</ScrollView>
|
||
</KeyboardAvoidingView>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
const S = StyleSheet.create({
|
||
safe: { flex: 1 },
|
||
flex: { flex: 1 },
|
||
scroll: { flex: 1 },
|
||
scrollContent: { padding: 20, paddingBottom: 40 },
|
||
|
||
// Brand
|
||
brand: { alignItems: 'center', marginBottom: 28, marginTop: 8 },
|
||
brandIcon: { fontSize: 36, marginBottom: 8 },
|
||
title: { fontSize: 26, fontWeight: '700', letterSpacing: 1 },
|
||
subtitle: { fontSize: 12, marginTop: 4, letterSpacing: 0.5 },
|
||
|
||
// Card
|
||
card: {
|
||
borderRadius: 14,
|
||
borderWidth: 1,
|
||
padding: 16,
|
||
marginBottom: 12,
|
||
},
|
||
cardLabel: { fontSize: 10, fontWeight: '600', letterSpacing: 1.5, marginBottom: 12, textTransform: 'uppercase' },
|
||
|
||
// Steps
|
||
stepRow: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 8 },
|
||
stepBadge: { width: 20, height: 20, borderRadius: 10, justifyContent: 'center', alignItems: 'center' },
|
||
stepNum: { fontSize: 11, fontWeight: '700' },
|
||
stepText: { fontSize: 13, flex: 1 },
|
||
linkBtn: { borderRadius: 6, paddingHorizontal: 10, paddingVertical: 4, borderWidth: 1 },
|
||
linkBtnText: { fontSize: 11, fontWeight: '600' },
|
||
|
||
// Inputs
|
||
inputRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||
inputDivider: { height: StyleSheet.hairlineWidth, marginVertical: 10 },
|
||
inputLabel: { fontSize: 12, width: 54, letterSpacing: 0.3 },
|
||
input: {
|
||
flex: 1,
|
||
fontSize: 15,
|
||
fontWeight: '500',
|
||
paddingVertical: 6,
|
||
borderBottomWidth: 1,
|
||
},
|
||
|
||
// Connect button
|
||
connectBtn: {
|
||
borderRadius: 14,
|
||
paddingVertical: 16,
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
borderWidth: 1,
|
||
marginTop: 4,
|
||
},
|
||
connectBtnText: { fontSize: 16, fontWeight: '700', letterSpacing: 3 },
|
||
|
||
// Connected state
|
||
connectedBox: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12, marginTop: 4 },
|
||
connectedLeft: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||
greenDot: { width: 8, height: 8, borderRadius: 4 },
|
||
connectedText: { fontWeight: '700', fontSize: 13 },
|
||
goBtn: { flex: 1, borderRadius: 10, paddingVertical: 11, alignItems: 'center', borderWidth: 1 },
|
||
goBtnText: { fontWeight: '600', fontSize: 13 },
|
||
disconnectBtn: { borderRadius: 10, paddingVertical: 11, paddingHorizontal: 14, borderWidth: 1 },
|
||
disconnectBtnText: { fontSize: 13, fontWeight: '600' },
|
||
|
||
// Terminal log
|
||
terminal: { borderRadius: 12, borderWidth: 1, overflow: 'hidden', minHeight: 120 },
|
||
terminalHeader: { fontSize: 10, fontWeight: '700', letterSpacing: 1.5, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 6, borderBottomWidth: 1 },
|
||
terminalBody: { padding: 12 },
|
||
terminalLine: { fontSize: 10, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', marginBottom: 3, lineHeight: 16 },
|
||
});
|