TriloopTem_App/src/components/ParamForm.tsx
2026-06-20 21:43:09 +08:00

271 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef, useCallback } 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';
import { useTheme } from '../design/tokens';
interface PickerRowProps {
label: string;
options: readonly { code: number; label: string }[];
value: number;
onChange: (code: number) => void;
}
const CHIP_W = 72;
function PickerRow({ label, options, value, onChange }: PickerRowProps) {
const theme = useTheme();
const scrollRef = useRef<ScrollView>(null);
const activeIdx = options.findIndex((o) => o.code === value);
useEffect(() => {
if (activeIdx > 0 && scrollRef.current) {
scrollRef.current.scrollTo({ x: Math.max(0, activeIdx * CHIP_W - CHIP_W), animated: false });
}
}, [activeIdx]);
return (
<View style={[S.row, { borderBottomColor: theme.bg.divider, backgroundColor: theme.bg.surface }]}>
<Text style={[S.rowLabel, { color: theme.text.muted }]}>{label}</Text>
<ScrollView ref={scrollRef} horizontal showsHorizontalScrollIndicator={false} style={S.optScroll}>
{options.map((o) => {
const active = o.code === value;
return (
<TouchableOpacity
key={o.code}
style={[S.optChip, { borderColor: theme.bg.border }, active && { borderColor: theme.blue.fg + '55', backgroundColor: theme.blue.bg }]}
onPress={() => onChange(o.code)}
activeOpacity={0.7}
>
<Text style={[S.optText, { color: theme.text.muted }, active && { color: theme.blue.fg, fontWeight: '700' }]}>{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 theme = useTheme();
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, { borderBottomColor: theme.bg.divider, backgroundColor: theme.bg.surface }]}>
<Text style={[S.rowLabel, { color: theme.text.muted }]}>{label}</Text>
<View style={S.inputWrap}>
<TextInput
style={[S.input, { color: theme.text.primary, borderBottomColor: theme.bg.border }, invalid && { borderBottomColor: theme.red.fg }]}
value={draft}
onChangeText={setDraft}
onBlur={commit}
keyboardType="numeric"
selectTextOnFocus
placeholderTextColor={theme.text.muted}
/>
{suffix && <Text style={[S.suffix, { color: theme.text.muted }]}>{suffix}</Text>}
</View>
{invalid && (
<Text style={[S.hint, { color: theme.red.fg }]}>{min}{max}</Text>
)}
</View>
);
}
function SectionHeader({ title }: { title: string }) {
const theme = useTheme();
return (
<View style={S.sectionHeader}>
<Text style={[S.sectionTitle, { color: theme.text.muted }]}>{title}</Text>
</View>
);
}
interface FormProps { onAnyChange?: () => void }
export function ParamForm({ onAnyChange }: FormProps = {}) {
const theme = useTheme();
const { config, updateConfig } = useDeviceStore();
const patch = (partial: Partial<typeof config>) => {
updateConfig(partial);
onAnyChange?.();
};
return (
<View style={[S.container, { backgroundColor: theme.bg.base }]}>
<SectionHeader title="采集配置" />
{/* Channel count */}
<View style={[S.row, { borderBottomColor: theme.bg.divider, backgroundColor: theme.bg.surface }]}>
<Text style={[S.rowLabel, { color: theme.text.muted }]}></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, { borderColor: theme.bg.border, backgroundColor: theme.bg.surface }, active && { borderColor: theme.blue.fg + '66', backgroundColor: theme.blue.bg }]}
onPress={() => patch({ channelNum: n })}
activeOpacity={0.7}
>
<Text style={[S.chText, { color: theme.text.muted }, active && { color: theme.blue.fg }]}>{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, { borderBottomColor: theme.bg.divider, backgroundColor: theme.bg.surface }]}>
<Text style={[S.rowLabel, { color: theme.text.muted }]}></Text>
<View style={S.switchRow}>
<Text style={[S.switchLabel, { color: theme.text.muted }]}>CH1-3</Text>
<Switch
value={config.negAcc123}
onValueChange={(v) => patch({ negAcc123: v })}
thumbColor={config.negAcc123 ? theme.blue.fg : theme.text.muted}
trackColor={{ false: theme.bg.border, true: theme.blue.border }}
/>
<Text style={[S.switchLabel, { color: theme.text.muted }]}>CH4-6</Text>
<Switch
value={config.negAcc456}
onValueChange={(v) => patch({ negAcc456: v })}
thumbColor={config.negAcc456 ? theme.blue.fg : theme.text.muted}
trackColor={{ false: theme.bg.border, true: theme.blue.border }}
/>
</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, { borderBottomColor: theme.bg.divider, backgroundColor: theme.bg.surface }]}>
<Text style={[S.rowLabel, { color: theme.text.muted }]}></Text>
<View style={S.inputWrap}>
<TextInput
style={[S.input, { color: theme.text.primary, borderBottomColor: theme.bg.border }]}
value={config.filePrefix}
onChangeText={(v) => patch({ filePrefix: v.slice(0, 15) })}
maxLength={15}
autoCapitalize="none"
placeholderTextColor={theme.text.muted}
/>
</View>
</View>
</View>
);
}
const S = StyleSheet.create({
container: {},
sectionHeader: {
paddingHorizontal: 16,
paddingTop: 14,
paddingBottom: 6,
},
sectionTitle: {
fontSize: 9,
fontWeight: '700',
letterSpacing: 2,
textTransform: 'uppercase',
},
row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
gap: 12,
marginHorizontal: 0,
},
rowLabel: { fontSize: 12, width: 68, flexShrink: 0, letterSpacing: 0.3 },
// Picker chips
optScroll: { flex: 1 },
optChip: { borderWidth: 1, borderRadius: 6, paddingHorizontal: 9, paddingVertical: 4, marginRight: 5 },
optText: { fontSize: 11 },
// Number input
inputWrap: { flexDirection: 'row', alignItems: 'center', flex: 1 },
input: {
flex: 1,
fontSize: 13,
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
paddingVertical: 4,
borderBottomWidth: 1,
},
suffix: { fontSize: 10, marginLeft: 8 },
hint: { fontSize: 9, marginLeft: 4 },
// Channel chips
channelRow: { flexDirection: 'row', gap: 6, flex: 1 },
chChip: {
width: 30,
height: 30,
borderRadius: 15,
borderWidth: 1,
alignItems: 'center',
justifyContent: 'center',
},
chText: { fontSize: 12, fontWeight: '700' },
// Switch row
switchRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 },
switchLabel: { fontSize: 12 },
});