267 lines
8.2 KiB
TypeScript
267 lines
8.2 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Switch, Platform } from 'react-native';
|
||
import { useDeviceStore } from '../stores/deviceStore';
|
||
import { SEND_FREQ_TABLE, SAMPLE_FREQ_TABLE, AMP_RATIO_TABLE, SOURCE_MODE_TABLE } from '../protocol/constants';
|
||
|
||
interface PickerRowProps {
|
||
label: string;
|
||
options: readonly { code: number; label: string }[];
|
||
value: number;
|
||
onChange: (code: number) => void;
|
||
}
|
||
|
||
function PickerRow({ label, options, value, onChange }: PickerRowProps) {
|
||
return (
|
||
<View style={S.row}>
|
||
<Text style={S.rowLabel}>{label}</Text>
|
||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={S.optScroll}>
|
||
{options.map((o) => {
|
||
const active = o.code === value;
|
||
return (
|
||
<TouchableOpacity
|
||
key={o.code}
|
||
style={[S.optChip, active && S.optChipActive]}
|
||
onPress={() => onChange(o.code)}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[S.optText, active && S.optTextActive]}>{o.label}</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
interface NumberRowProps {
|
||
label: string;
|
||
value: number;
|
||
min: number;
|
||
max: number;
|
||
onCommit: (v: number) => void;
|
||
suffix?: string;
|
||
}
|
||
|
||
// Keeps a local draft string while typing; commits a clamped integer on blur.
|
||
function NumberRow({ label, value, min, max, onCommit, suffix }: NumberRowProps) {
|
||
const [draft, setDraft] = useState(String(value));
|
||
|
||
// Sync when the store value changes externally (e.g. presets).
|
||
useEffect(() => { setDraft(String(value)); }, [value]);
|
||
|
||
const commit = () => {
|
||
const n = parseInt(draft, 10);
|
||
const clamped = isNaN(n) ? min : Math.max(min, Math.min(max, n));
|
||
setDraft(String(clamped));
|
||
if (clamped !== value) onCommit(clamped);
|
||
};
|
||
|
||
const invalid = (() => {
|
||
const n = parseInt(draft, 10);
|
||
return isNaN(n) || n < min || n > max;
|
||
})();
|
||
|
||
return (
|
||
<View style={S.row}>
|
||
<Text style={S.rowLabel}>{label}</Text>
|
||
<View style={S.inputWrap}>
|
||
<TextInput
|
||
style={[S.input, invalid && S.inputInvalid]}
|
||
value={draft}
|
||
onChangeText={setDraft}
|
||
onBlur={commit}
|
||
keyboardType="numeric"
|
||
selectTextOnFocus
|
||
placeholderTextColor="#3a3a5a"
|
||
/>
|
||
{suffix && <Text style={S.suffix}>{suffix}</Text>}
|
||
</View>
|
||
{invalid && (
|
||
<Text style={S.hint}>{min}–{max}</Text>
|
||
)}
|
||
</View>
|
||
);
|
||
}
|
||
|
||
function SectionHeader({ title }: { title: string }) {
|
||
return (
|
||
<View style={S.sectionHeader}>
|
||
<Text style={S.sectionTitle}>{title}</Text>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
interface FormProps { onAnyChange?: () => void }
|
||
|
||
export function ParamForm({ onAnyChange }: FormProps = {}) {
|
||
const { config, updateConfig } = useDeviceStore();
|
||
const patch = (partial: Partial<typeof config>) => {
|
||
updateConfig(partial);
|
||
onAnyChange?.();
|
||
};
|
||
|
||
return (
|
||
<View style={S.container}>
|
||
|
||
<SectionHeader title="采集配置" />
|
||
|
||
{/* Channel count */}
|
||
<View style={S.row}>
|
||
<Text style={S.rowLabel}>通道数</Text>
|
||
<View style={S.channelRow}>
|
||
{[1, 2, 3, 4, 5, 6].map((n) => {
|
||
const active = config.channelNum === n;
|
||
return (
|
||
<TouchableOpacity
|
||
key={n}
|
||
style={[S.chChip, active && S.chChipActive]}
|
||
onPress={() => patch({ channelNum: n })}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[S.chText, active && S.chTextActive]}>{n}</Text>
|
||
</TouchableOpacity>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
|
||
<PickerRow label="发射频率" options={SEND_FREQ_TABLE} value={config.sendFreq} onChange={(v) => patch({ sendFreq: v })} />
|
||
<PickerRow label="采样频率" options={SAMPLE_FREQ_TABLE} value={config.sampleFreq} onChange={(v) => patch({ sampleFreq: v })} />
|
||
|
||
<NumberRow
|
||
label="采样点数" value={config.sampleDepth}
|
||
min={64} max={65535}
|
||
onCommit={(v) => { patch({ sampleDepth: v }); }}
|
||
/>
|
||
<NumberRow
|
||
label="叠加次数" value={config.accNum}
|
||
min={1} max={9999}
|
||
onCommit={(v) => { patch({ accNum: v }); }}
|
||
/>
|
||
|
||
<SectionHeader title="放大与源" />
|
||
|
||
<PickerRow label="增 益" options={AMP_RATIO_TABLE} value={config.ampRatio} onChange={(v) => patch({ ampRatio: v })} />
|
||
<PickerRow label="源模式" options={SOURCE_MODE_TABLE} value={config.sourceMode} onChange={(v) => patch({ sourceMode: v })} />
|
||
|
||
<SectionHeader title="高级设置" />
|
||
|
||
{/* Reverse accumulation */}
|
||
<View style={S.row}>
|
||
<Text style={S.rowLabel}>反向叠加</Text>
|
||
<View style={S.switchRow}>
|
||
<Text style={S.switchLabel}>CH1-3</Text>
|
||
<Switch
|
||
value={config.negAcc123}
|
||
onValueChange={(v) => patch({ negAcc123: v })}
|
||
thumbColor={config.negAcc123 ? '#4a9eff' : '#444'}
|
||
trackColor={{ false: '#1e1e2e', true: '#1a3a66' }}
|
||
/>
|
||
<Text style={S.switchLabel}>CH4-6</Text>
|
||
<Switch
|
||
value={config.negAcc456}
|
||
onValueChange={(v) => patch({ negAcc456: v })}
|
||
thumbColor={config.negAcc456 ? '#4a9eff' : '#444'}
|
||
trackColor={{ false: '#1e1e2e', true: '#1a3a66' }}
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
<NumberRow
|
||
label="补偿延时" value={config.compDisableDelay}
|
||
min={0} max={65535}
|
||
onCommit={(v) => { patch({ compDisableDelay: v }); }}
|
||
suffix="×50μs"
|
||
/>
|
||
|
||
{/* File prefix */}
|
||
<View style={S.row}>
|
||
<Text style={S.rowLabel}>文件前缀</Text>
|
||
<View style={S.inputWrap}>
|
||
<TextInput
|
||
style={S.input}
|
||
value={config.filePrefix}
|
||
onChangeText={(v) => patch({ filePrefix: v.slice(0, 15) })}
|
||
maxLength={15}
|
||
autoCapitalize="none"
|
||
placeholderTextColor="#3a3a5a"
|
||
/>
|
||
</View>
|
||
</View>
|
||
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const S = StyleSheet.create({
|
||
container: { backgroundColor: '#090912' },
|
||
|
||
sectionHeader: {
|
||
paddingHorizontal: 16,
|
||
paddingTop: 14,
|
||
paddingBottom: 6,
|
||
},
|
||
sectionTitle: {
|
||
color: '#3a3a5a',
|
||
fontSize: 9,
|
||
fontWeight: '700',
|
||
letterSpacing: 2,
|
||
textTransform: 'uppercase',
|
||
},
|
||
|
||
row: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 10,
|
||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||
borderBottomColor: '#141422',
|
||
gap: 12,
|
||
backgroundColor: '#0e0e1c',
|
||
marginHorizontal: 0,
|
||
},
|
||
rowLabel: { color: '#4a4a6a', fontSize: 12, width: 68, flexShrink: 0, letterSpacing: 0.3 },
|
||
|
||
// Picker chips
|
||
optScroll: { flex: 1 },
|
||
optChip: { borderWidth: 1, borderColor: '#1e1e30', borderRadius: 6, paddingHorizontal: 9, paddingVertical: 4, marginRight: 5 },
|
||
optChipActive: { borderColor: '#4a9eff55', backgroundColor: '#0d2040' },
|
||
optText: { color: '#3a3a5a', fontSize: 11 },
|
||
optTextActive: { color: '#4a9eff', fontWeight: '700' },
|
||
|
||
// Number input
|
||
inputWrap: { flexDirection: 'row', alignItems: 'center', flex: 1 },
|
||
input: {
|
||
flex: 1,
|
||
color: '#c0c0d8',
|
||
fontSize: 13,
|
||
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
||
paddingVertical: 4,
|
||
borderBottomWidth: 1,
|
||
borderBottomColor: '#1e1e30',
|
||
},
|
||
inputInvalid: { borderBottomColor: '#ff5c6e' },
|
||
suffix: { color: '#3a3a5a', fontSize: 10, marginLeft: 8 },
|
||
hint: { color: '#ff5c6e', fontSize: 9, marginLeft: 4 },
|
||
|
||
// Channel chips
|
||
channelRow: { flexDirection: 'row', gap: 6, flex: 1 },
|
||
chChip: {
|
||
width: 30,
|
||
height: 30,
|
||
borderRadius: 15,
|
||
borderWidth: 1,
|
||
borderColor: '#1e1e30',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
backgroundColor: '#0c0c18',
|
||
},
|
||
chChipActive: { borderColor: '#4a9eff66', backgroundColor: '#0d1e3a' },
|
||
chText: { color: '#3a3a5a', fontSize: 12, fontWeight: '700' },
|
||
chTextActive: { color: '#4a9eff' },
|
||
|
||
// Switch row
|
||
switchRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 },
|
||
switchLabel: { color: '#4a4a6a', fontSize: 12 },
|
||
});
|