238 lines
9.2 KiB
TypeScript
238 lines
9.2 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';
|
||
|
||
const LOG_MAX = 60;
|
||
|
||
export default function ConnectScreen() {
|
||
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}>
|
||
<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}>◈</Text>
|
||
<Text style={S.title}>TEM Receiver</Text>
|
||
<Text style={S.subtitle}>瞬变电磁接收机上位机</Text>
|
||
</View>
|
||
|
||
{/* ── Setup guide ── */}
|
||
<View style={S.card}>
|
||
<Text style={S.cardLabel}>连接步骤</Text>
|
||
<View style={S.stepRow}>
|
||
<View style={S.stepBadge}><Text style={S.stepNum}>1</Text></View>
|
||
<Text style={S.stepText}>手机连接设备 WiFi 热点</Text>
|
||
<TouchableOpacity style={S.linkBtn} onPress={() => Linking.openSettings()}>
|
||
<Text style={S.linkBtnText}>设置 →</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
<View style={S.stepRow}>
|
||
<View style={S.stepBadge}><Text style={S.stepNum}>2</Text></View>
|
||
<Text style={S.stepText}>确认参数,点击连接</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* ── Inputs ── */}
|
||
<View style={S.card}>
|
||
<Text style={S.cardLabel}>连接参数</Text>
|
||
<View style={S.inputRow}>
|
||
<Text style={S.inputLabel}>IP 地址</Text>
|
||
<TextInput
|
||
style={S.input}
|
||
value={host}
|
||
onChangeText={setHost}
|
||
keyboardType="numeric"
|
||
placeholder="192.168.4.1"
|
||
placeholderTextColor="#3a3a5a"
|
||
autoCapitalize="none"
|
||
/>
|
||
</View>
|
||
<View style={S.inputDivider} />
|
||
<View style={S.inputRow}>
|
||
<Text style={S.inputLabel}>端 口</Text>
|
||
<TextInput
|
||
style={S.input}
|
||
value={portStr}
|
||
onChangeText={setPortStr}
|
||
keyboardType="numeric"
|
||
placeholder="4321"
|
||
placeholderTextColor="#3a3a5a"
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
{/* ── Action ── */}
|
||
{!isConnected ? (
|
||
<TouchableOpacity
|
||
style={[S.connectBtn, isConnecting && S.connectBtnBusy]}
|
||
onPress={handleConnect}
|
||
disabled={isConnecting}
|
||
activeOpacity={0.8}
|
||
>
|
||
{isConnecting
|
||
? <ActivityIndicator color="#fff" />
|
||
: <Text style={S.connectBtnText}>连 接 设 备</Text>}
|
||
</TouchableOpacity>
|
||
) : (
|
||
<View style={S.connectedBox}>
|
||
<View style={S.connectedLeft}>
|
||
<View style={S.greenDot} />
|
||
<Text style={S.connectedText}>已连接</Text>
|
||
</View>
|
||
<TouchableOpacity style={S.goBtn} onPress={() => router.push('/(tabs)/control')} activeOpacity={0.8}>
|
||
<Text style={S.goBtnText}>进入控制台 →</Text>
|
||
</TouchableOpacity>
|
||
<TouchableOpacity style={S.disconnectBtn} onPress={handleDisconnect} activeOpacity={0.8}>
|
||
<Text style={S.disconnectBtnText}>断开</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
|
||
{/* ── Log terminal ── */}
|
||
<View style={S.terminal}>
|
||
<Text style={S.terminalHeader}>● 连接日志</Text>
|
||
<View style={S.terminalBody}>
|
||
{logs.map((l, i) => (
|
||
<Text key={i} style={[S.terminalLine, i === 0 && S.terminalLineLatest]}>{l}</Text>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
</ScrollView>
|
||
</KeyboardAvoidingView>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
const S = StyleSheet.create({
|
||
safe: { flex: 1, backgroundColor: '#090912' },
|
||
flex: { flex: 1 },
|
||
scroll: { flex: 1 },
|
||
scrollContent: { padding: 20, paddingBottom: 40 },
|
||
|
||
// Brand
|
||
brand: { alignItems: 'center', marginBottom: 28, marginTop: 8 },
|
||
brandIcon: { fontSize: 36, color: '#4a9eff', marginBottom: 8 },
|
||
title: { color: '#e4e4f0', fontSize: 26, fontWeight: '700', letterSpacing: 1 },
|
||
subtitle: { color: '#4a4a6a', fontSize: 12, marginTop: 4, letterSpacing: 0.5 },
|
||
|
||
// Card
|
||
card: {
|
||
backgroundColor: '#111120',
|
||
borderRadius: 14,
|
||
borderWidth: 1,
|
||
borderColor: '#22223a',
|
||
padding: 16,
|
||
marginBottom: 12,
|
||
},
|
||
cardLabel: { color: '#4a4a6a', 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, backgroundColor: '#1a2a44', justifyContent: 'center', alignItems: 'center' },
|
||
stepNum: { color: '#4a9eff', fontSize: 11, fontWeight: '700' },
|
||
stepText: { color: '#8888aa', fontSize: 13, flex: 1 },
|
||
linkBtn: { backgroundColor: '#0d2040', borderRadius: 6, paddingHorizontal: 10, paddingVertical: 4, borderWidth: 1, borderColor: '#1a3a66' },
|
||
linkBtnText: { color: '#4a9eff', fontSize: 11, fontWeight: '600' },
|
||
|
||
// Inputs
|
||
inputRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
|
||
inputDivider: { height: StyleSheet.hairlineWidth, backgroundColor: '#1e1e30', marginVertical: 10 },
|
||
inputLabel: { color: '#5a5a7a', fontSize: 12, width: 54, letterSpacing: 0.3 },
|
||
input: {
|
||
flex: 1,
|
||
color: '#d0d0e8',
|
||
fontSize: 15,
|
||
fontWeight: '500',
|
||
paddingVertical: 6,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: '#2a2a48',
|
||
},
|
||
|
||
// Connect button
|
||
connectBtn: {
|
||
backgroundColor: '#1a3a6e',
|
||
borderRadius: 14,
|
||
paddingVertical: 16,
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
borderWidth: 1,
|
||
borderColor: '#4a9eff',
|
||
marginTop: 4,
|
||
},
|
||
connectBtnBusy: { borderColor: '#2a4a7a', backgroundColor: '#0f1f3a' },
|
||
connectBtnText: { color: '#4a9eff', 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, backgroundColor: '#3ddc84' },
|
||
connectedText: { color: '#3ddc84', fontWeight: '700', fontSize: 13 },
|
||
goBtn: { flex: 1, backgroundColor: '#0d2820', borderRadius: 10, paddingVertical: 11, alignItems: 'center', borderWidth: 1, borderColor: '#1a4a30' },
|
||
goBtnText: { color: '#3ddc84', fontWeight: '600', fontSize: 13 },
|
||
disconnectBtn: { backgroundColor: '#2a0d12', borderRadius: 10, paddingVertical: 11, paddingHorizontal: 14, borderWidth: 1, borderColor: '#4a1a22' },
|
||
disconnectBtnText: { color: '#ff5c6e', fontSize: 13, fontWeight: '600' },
|
||
|
||
// Terminal log
|
||
terminal: { backgroundColor: '#08080f', borderRadius: 12, borderWidth: 1, borderColor: '#1a1a28', overflow: 'hidden', minHeight: 120 },
|
||
terminalHeader: { color: '#3ddc84', fontSize: 10, fontWeight: '700', letterSpacing: 1.5, paddingHorizontal: 14, paddingTop: 10, paddingBottom: 6, borderBottomWidth: 1, borderBottomColor: '#141422' },
|
||
terminalBody: { padding: 12 },
|
||
terminalLine: { color: '#3a3a5a', fontSize: 10, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', marginBottom: 3, lineHeight: 16 },
|
||
terminalLineLatest: { color: '#6a6a9a' },
|
||
});
|