This commit is contained in:
2026-06-19 22:08:10 +08:00
parent 091b358705
commit 84590d5477
37 changed files with 5844 additions and 900 deletions
+169 -138
View File
@@ -1,12 +1,7 @@
import React from 'react';
import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Switch } from 'react-native';
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';
import { SEND_FREQ_TABLE, SAMPLE_FREQ_TABLE, AMP_RATIO_TABLE, SOURCE_MODE_TABLE } from '../protocol/constants';
interface PickerRowProps {
label: string;
@@ -17,20 +12,22 @@ interface PickerRowProps {
function PickerRow({ label, options, value, onChange }: PickerRowProps) {
return (
<View style={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.optScroll}>
{options.map((o) => (
<TouchableOpacity
key={o.code}
style={[styles.optChip, o.code === value && styles.optChipActive]}
onPress={() => onChange(o.code)}
>
<Text style={[styles.optText, o.code === value && styles.optTextActive]}>
{o.label}
</Text>
</TouchableOpacity>
))}
<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>
);
@@ -39,34 +36,63 @@ function PickerRow({ label, options, value, onChange }: PickerRowProps) {
interface NumberRowProps {
label: string;
value: number;
onChangeText: (v: string) => void;
min: number;
max: number;
onCommit: (v: number) => void;
suffix?: string;
keyboardType?: 'numeric';
}
function NumberRow({ label, value, onChangeText, suffix, keyboardType = 'numeric' }: NumberRowProps) {
// 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={styles.row}>
<Text style={styles.rowLabel}>{label}</Text>
<View style={styles.inputWrap}>
<View style={S.row}>
<Text style={S.rowLabel}>{label}</Text>
<View style={S.inputWrap}>
<TextInput
style={styles.input}
value={String(value)}
onChangeText={onChangeText}
keyboardType={keyboardType}
style={[S.input, invalid && S.inputInvalid]}
value={draft}
onChangeText={setDraft}
onBlur={commit}
keyboardType="numeric"
selectTextOnFocus
placeholderTextColor="#555"
placeholderTextColor="#3a3a5a"
/>
{suffix && <Text style={styles.suffix}>{suffix}</Text>}
{suffix && <Text style={S.suffix}>{suffix}</Text>}
</View>
{invalid && (
<Text style={S.hint}>{min}–{max}</Text>
)}
</View>
);
}
interface FormProps {
onAnyChange?: () => void;
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>) => {
@@ -75,161 +101,166 @@ export function ParamForm({ onAnyChange }: FormProps = {}) {
};
return (
<View style={styles.container}>
{/* Channel selector */}
<View style={styles.row}>
<Text style={styles.rowLabel}>通道数</Text>
<View style={styles.channelRow}>
{[1, 2, 3, 4, 5, 6].map((n) => (
<TouchableOpacity
key={n}
style={[styles.chChip, config.channelNum === n && styles.chChipActive]}
onPress={() => patch({ channelNum: n })}
>
<Text style={[styles.chText, config.channelNum === n && styles.chTextActive]}>
{n}
</Text>
</TouchableOpacity>
))}
<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 })}
/>
<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}
onChangeText={(v) => patch({ sampleDepth: parseInt(v, 10) || 0 })}
label="采样点数" value={config.sampleDepth}
min={64} max={65535}
onCommit={(v) => { patch({ sampleDepth: v }); }}
/>
<NumberRow
label="叠加次数"
value={config.accNum}
onChangeText={(v) => patch({ accNum: parseInt(v, 10) || 0 })}
label="叠加次数" value={config.accNum}
min={1} max={9999}
onCommit={(v) => { patch({ accNum: v }); }}
/>
<PickerRow
label="增益"
options={AMP_RATIO_TABLE}
value={config.ampRatio}
onChange={(v) => patch({ ampRatio: v })}
/>
<SectionHeader title="放大与源" />
<PickerRow
label="源模式"
options={SOURCE_MODE_TABLE}
value={config.sourceMode}
onChange={(v) => patch({ sourceMode: v })}
/>
<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 })} />
{/* Reverse accumulation flags */}
<View style={styles.row}>
<Text style={styles.rowLabel}>反向叠加</Text>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>CH1-3</Text>
<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' : '#888'}
trackColor={{ false: '#333', true: '#1a4a88' }}
thumbColor={config.negAcc123 ? '#4a9eff' : '#444'}
trackColor={{ false: '#1e1e2e', true: '#1a3a66' }}
/>
<Text style={styles.switchLabel}>CH4-6</Text>
<Text style={S.switchLabel}>CH4-6</Text>
<Switch
value={config.negAcc456}
onValueChange={(v) => patch({ negAcc456: v })}
thumbColor={config.negAcc456 ? '#4a9eff' : '#888'}
trackColor={{ false: '#333', true: '#1a4a88' }}
thumbColor={config.negAcc456 ? '#4a9eff' : '#444'}
trackColor={{ false: '#1e1e2e', true: '#1a3a66' }}
/>
</View>
</View>
<NumberRow
label="补偿延时"
value={config.compDisableDelay}
onChangeText={(v) => patch({ compDisableDelay: parseInt(v, 10) || 0 })}
label="补偿延时" value={config.compDisableDelay}
min={0} max={65535}
onCommit={(v) => { patch({ compDisableDelay: v }); }}
suffix="×50μs"
/>
{/* File prefix */}
<View style={styles.row}>
<Text style={styles.rowLabel}>文件前缀</Text>
<View style={styles.inputWrap}>
<View style={S.row}>
<Text style={S.rowLabel}>文件前缀</Text>
<View style={S.inputWrap}>
<TextInput
style={styles.input}
style={S.input}
value={config.filePrefix}
onChangeText={(v) => patch({ filePrefix: v.slice(0, 15) })}
maxLength={15}
autoCapitalize="none"
placeholderTextColor="#555"
placeholderTextColor="#3a3a5a"
/>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { backgroundColor: '#111' },
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: 14,
paddingVertical: 8,
paddingHorizontal: 16,
paddingVertical: 10,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#2a2a2a',
gap: 10,
borderBottomColor: '#141422',
gap: 12,
backgroundColor: '#0e0e1c',
marginHorizontal: 0,
},
rowLabel: { color: '#aaa', fontSize: 13, width: 68, flexShrink: 0 },
optScroll: { flex: 1 },
optChip: {
borderWidth: 1,
borderColor: '#444',
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 3,
marginRight: 6,
},
optChipActive: { borderColor: '#4a9eff', backgroundColor: '#0d2b55' },
optText: { color: '#888', fontSize: 11 },
optTextActive: { color: '#4a9eff', fontWeight: '600' },
inputWrap: { flexDirection: 'row', alignItems: 'center', flex: 1 },
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,
backgroundColor: '#1e1e1e',
color: '#eee',
color: '#c0c0d8',
fontSize: 13,
borderRadius: 6,
paddingHorizontal: 8,
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
paddingVertical: 4,
borderWidth: 1,
borderColor: '#333',
borderBottomWidth: 1,
borderBottomColor: '#1e1e30',
},
suffix: { color: '#666', fontSize: 11, marginLeft: 6 },
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: 28,
height: 28,
borderRadius: 14,
width: 30,
height: 30,
borderRadius: 15,
borderWidth: 1,
borderColor: '#444',
borderColor: '#1e1e30',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#0c0c18',
},
chChipActive: { borderColor: '#4a9eff', backgroundColor: '#0d2b55' },
chText: { color: '#888', fontSize: 12, fontWeight: '600' },
chTextActive: { color: '#4a9eff' },
switchRow: { flexDirection: 'row', alignItems: 'center', gap: 6, flex: 1 },
switchLabel: { color: '#888', fontSize: 12 },
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 },
});