216 lines
8.9 KiB
TypeScript
216 lines
8.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { View, Text, TouchableOpacity, StyleSheet, ScrollView, ActivityIndicator, Platform } from 'react-native';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { router } from 'expo-router';
|
|
import { DeviceStatusBar } from '../../src/components/DeviceStatusBar';
|
|
import { ParamForm } from '../../src/components/ParamForm';
|
|
import { useDevice } from '../../src/hooks/useDevice';
|
|
import { useDataStore } from '../../src/stores/dataStore';
|
|
import { useTheme } from '../../src/design/tokens';
|
|
|
|
export default function ControlScreen() {
|
|
const insets = useSafeAreaInsets();
|
|
const theme = useTheme();
|
|
const { busy, connected, deviceStatus, setup, startContinuous, startSingle, stop } = useDevice();
|
|
const frame = useDataStore((s) => s.currentFrame);
|
|
const sessionId = useDataStore((s) => s.sessionId);
|
|
const projectId = useDataStore((s) => s.projectId);
|
|
const [configDirty, setConfigDirty] = useState(false);
|
|
const [projectName, setProjectName] = React.useState<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!projectId) { setProjectName(null); return; }
|
|
import('../../src/services/StorageService').then(({ listProjects }) =>
|
|
listProjects().then((ps) => setProjectName(ps.find(p => p.projectId === projectId)?.name ?? null)),
|
|
);
|
|
}, [projectId]);
|
|
|
|
const handleSetup = async () => {
|
|
await setup();
|
|
setConfigDirty(false);
|
|
};
|
|
|
|
if (!connected) {
|
|
return (
|
|
<View style={[S.notConnected, { paddingTop: insets.top, backgroundColor: theme.bg.base }]}>
|
|
<Text style={[S.notConnectedIcon, { color: theme.text.ghost }]}>⊘</Text>
|
|
<Text style={[S.notConnectedText, { color: theme.text.muted }]}>未连接设备</Text>
|
|
<TouchableOpacity
|
|
style={[S.goConnectBtn, { backgroundColor: theme.bg.raised, borderColor: theme.blue.fg }]}
|
|
onPress={() => router.push('/connect')}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text style={[S.goConnectBtnText, { color: theme.blue.fg }]}>前往连接</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const running = deviceStatus === 'running';
|
|
const single = deviceStatus === 'single';
|
|
const canStart = !running && !single && !busy;
|
|
const canStop = (running || single) && !busy;
|
|
|
|
return (
|
|
<View style={[S.container, { paddingTop: insets.top, backgroundColor: theme.bg.base }]}>
|
|
<DeviceStatusBar />
|
|
{/* Project / line context */}
|
|
<View style={[S.contextBar, { backgroundColor: theme.bg.void, borderBottomColor: theme.bg.divider }]}>
|
|
{projectName
|
|
? <Text style={[S.contextText, { color: theme.text.ghost }]} numberOfLines={1}>{projectName} · {sessionId}</Text>
|
|
: <Text style={[S.contextText, { color: theme.text.ghost }]} numberOfLines={1}>{sessionId}</Text>
|
|
}
|
|
</View>
|
|
|
|
{/* Control buttons */}
|
|
<View style={S.ctrlRow}>
|
|
<TouchableOpacity
|
|
style={[S.ctrlBtn, { backgroundColor: theme.green.bg, borderColor: theme.green.border }, !canStart && S.btnDisabled]}
|
|
onPress={startContinuous}
|
|
disabled={!canStart}
|
|
activeOpacity={0.8}
|
|
>
|
|
{busy && !canStop
|
|
? <ActivityIndicator color={theme.green.fg} size="small" />
|
|
: <>
|
|
<Text style={S.ctrlIcon}>▶</Text>
|
|
<Text style={[S.ctrlText, { color: theme.green.fg }]}>连续采集</Text>
|
|
</>
|
|
}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[S.ctrlBtn, { backgroundColor: theme.blue.bg, borderColor: theme.blue.border }, !canStart && S.btnDisabled]}
|
|
onPress={startSingle}
|
|
disabled={!canStart}
|
|
activeOpacity={0.8}
|
|
>
|
|
<Text style={S.ctrlIcon}>◎</Text>
|
|
<Text style={[S.ctrlText, { color: theme.blue.fg }]}>单 次</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[S.ctrlBtn, { backgroundColor: theme.red.bg, borderColor: theme.red.border }, !canStop && S.btnDisabled]}
|
|
onPress={stop}
|
|
disabled={!canStop}
|
|
activeOpacity={0.8}
|
|
>
|
|
{busy && canStop
|
|
? <ActivityIndicator color={theme.red.fg} size="small" />
|
|
: <>
|
|
<Text style={S.ctrlIcon}>■</Text>
|
|
<Text style={[S.ctrlText, { color: theme.red.fg }]}>停 止</Text>
|
|
</>
|
|
}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Frame info */}
|
|
{frame && (
|
|
<View style={[S.frameBar, { backgroundColor: theme.bg.surface, borderColor: theme.bg.border }]}>
|
|
<View style={S.frameItem}>
|
|
<Text style={[S.frameItemLabel, { color: theme.text.muted }]}>帧号</Text>
|
|
<Text style={[S.frameItemValue, { color: theme.text.secondary }]}>#{frame.frameId}</Text>
|
|
</View>
|
|
<View style={[S.frameSep, { backgroundColor: theme.bg.border }]} />
|
|
<View style={S.frameItem}>
|
|
<Text style={[S.frameItemLabel, { color: theme.text.muted }]}>叠加</Text>
|
|
<Text style={[S.frameItemValue, { color: theme.text.secondary }]}>{frame.accNum}次</Text>
|
|
</View>
|
|
<View style={[S.frameSep, { backgroundColor: theme.bg.border }]} />
|
|
<View style={S.frameItem}>
|
|
<Text style={[S.frameItemLabel, { color: theme.text.muted }]}>通道</Text>
|
|
<Text style={[S.frameItemValue, { color: theme.text.secondary }]}>{frame.meta?.channelNum ?? '-'}ch</Text>
|
|
</View>
|
|
<View style={[S.frameSep, { backgroundColor: theme.bg.border }]} />
|
|
<View style={S.frameItem}>
|
|
<Text style={[S.frameItemLabel, { color: theme.text.muted }]}>电流</Text>
|
|
<Text style={[S.frameItemValue, { color: theme.text.secondary }]}>{frame.meta?.current ?? '--'}</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Param form */}
|
|
<ScrollView style={S.formScroll} keyboardShouldPersistTaps="handled">
|
|
<View style={S.formHeader}>
|
|
<Text style={[S.formTitle, { color: theme.text.muted }]}>采集参数</Text>
|
|
{configDirty && (
|
|
<View style={[S.dirtyBadge, { backgroundColor: theme.amber.bg, borderColor: theme.amber.border }]}>
|
|
<Text style={[S.dirtyBadgeText, { color: theme.amber.fg }]}>待下发</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<ParamForm onAnyChange={() => setConfigDirty(true)} />
|
|
<TouchableOpacity
|
|
style={[S.setupBtn, { backgroundColor: theme.bg.raised, borderColor: theme.blue.fg }, busy && S.btnDisabled]}
|
|
onPress={handleSetup}
|
|
disabled={busy}
|
|
activeOpacity={0.8}
|
|
>
|
|
{busy
|
|
? <ActivityIndicator color={theme.blue.fg} />
|
|
: <Text style={[S.setupBtnText, { color: theme.blue.fg }]}>下 发 配 置</Text>}
|
|
</TouchableOpacity>
|
|
<View style={{ height: 32 }} />
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const S = StyleSheet.create({
|
|
container: { flex: 1 },
|
|
notConnected: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 },
|
|
notConnectedIcon: { fontSize: 48 },
|
|
notConnectedText: { fontSize: 15 },
|
|
goConnectBtn: { borderRadius: 10, paddingVertical: 10, paddingHorizontal: 28, borderWidth: 1 },
|
|
goConnectBtnText: { fontWeight: '600' },
|
|
|
|
// Control buttons
|
|
ctrlRow: { flexDirection: 'row', gap: 8, padding: 12 },
|
|
ctrlBtn: {
|
|
flex: 1,
|
|
paddingVertical: 14,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
gap: 4,
|
|
borderWidth: 1,
|
|
},
|
|
btnDisabled: { opacity: 0.3 },
|
|
ctrlIcon: { fontSize: 12, color: '#ffffff88' },
|
|
ctrlText: { fontSize: 12, fontWeight: '700', letterSpacing: 1 },
|
|
|
|
// Frame info bar
|
|
frameBar: {
|
|
flexDirection: 'row',
|
|
marginHorizontal: 12,
|
|
marginBottom: 8,
|
|
borderRadius: 10,
|
|
borderWidth: 1,
|
|
overflow: 'hidden',
|
|
},
|
|
frameItem: { flex: 1, alignItems: 'center', paddingVertical: 8 },
|
|
frameItemLabel: { fontSize: 9, fontWeight: '600', letterSpacing: 0.5, marginBottom: 2 },
|
|
frameItemValue: { fontSize: 12, fontWeight: '600', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
|
|
frameSep: { width: StyleSheet.hairlineWidth, marginVertical: 6 },
|
|
|
|
// Form
|
|
formScroll: { flex: 1 },
|
|
formHeader: { flexDirection: 'row', alignItems: 'center', gap: 10, paddingHorizontal: 16, paddingVertical: 10 },
|
|
formTitle: { fontSize: 10, fontWeight: '700', letterSpacing: 1.5, textTransform: 'uppercase' },
|
|
dirtyBadge: { borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2, borderWidth: 1 },
|
|
dirtyBadgeText: { fontSize: 9, fontWeight: '700' },
|
|
|
|
setupBtn: {
|
|
margin: 16,
|
|
borderRadius: 12,
|
|
paddingVertical: 14,
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
},
|
|
setupBtnText: { fontWeight: '700', fontSize: 14, letterSpacing: 3 },
|
|
|
|
contextBar: { paddingHorizontal: 16, paddingVertical: 5, borderBottomWidth: 1 },
|
|
contextText: { fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
|
|
});
|