基本功能完成
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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;
|
||||
@@ -10,21 +11,33 @@ interface PickerRowProps {
|
||||
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}>
|
||||
<Text style={S.rowLabel}>{label}</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={S.optScroll}>
|
||||
<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, active && S.optChipActive]}
|
||||
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, active && S.optTextActive]}>{o.label}</Text>
|
||||
<Text style={[S.optText, { color: theme.text.muted }, active && { color: theme.blue.fg, fontWeight: '700' }]}>{o.label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -44,6 +57,7 @@ interface NumberRowProps {
|
||||
|
||||
// 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).
|
||||
@@ -62,31 +76,32 @@ function NumberRow({ label, value, min, max, onCommit, suffix }: NumberRowProps)
|
||||
})();
|
||||
|
||||
return (
|
||||
<View style={S.row}>
|
||||
<Text style={S.rowLabel}>{label}</Text>
|
||||
<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, invalid && S.inputInvalid]}
|
||||
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="#3a3a5a"
|
||||
placeholderTextColor={theme.text.muted}
|
||||
/>
|
||||
{suffix && <Text style={S.suffix}>{suffix}</Text>}
|
||||
{suffix && <Text style={[S.suffix, { color: theme.text.muted }]}>{suffix}</Text>}
|
||||
</View>
|
||||
{invalid && (
|
||||
<Text style={S.hint}>{min}–{max}</Text>
|
||||
<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}>{title}</Text>
|
||||
<Text style={[S.sectionTitle, { color: theme.text.muted }]}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -94,6 +109,7 @@ function SectionHeader({ title }: { title: string }) {
|
||||
interface FormProps { onAnyChange?: () => void }
|
||||
|
||||
export function ParamForm({ onAnyChange }: FormProps = {}) {
|
||||
const theme = useTheme();
|
||||
const { config, updateConfig } = useDeviceStore();
|
||||
const patch = (partial: Partial<typeof config>) => {
|
||||
updateConfig(partial);
|
||||
@@ -101,24 +117,24 @@ export function ParamForm({ onAnyChange }: FormProps = {}) {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={S.container}>
|
||||
<View style={[S.container, { backgroundColor: theme.bg.base }]}>
|
||||
|
||||
<SectionHeader title="采集配置" />
|
||||
|
||||
{/* Channel count */}
|
||||
<View style={S.row}>
|
||||
<Text style={S.rowLabel}>通道数</Text>
|
||||
<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, active && S.chChipActive]}
|
||||
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, active && S.chTextActive]}>{n}</Text>
|
||||
<Text style={[S.chText, { color: theme.text.muted }, active && { color: theme.blue.fg }]}>{n}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -147,22 +163,22 @@ export function ParamForm({ onAnyChange }: FormProps = {}) {
|
||||
<SectionHeader title="高级设置" />
|
||||
|
||||
{/* Reverse accumulation */}
|
||||
<View style={S.row}>
|
||||
<Text style={S.rowLabel}>反向叠加</Text>
|
||||
<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}>CH1-3</Text>
|
||||
<Text style={[S.switchLabel, { color: theme.text.muted }]}>CH1-3</Text>
|
||||
<Switch
|
||||
value={config.negAcc123}
|
||||
onValueChange={(v) => patch({ negAcc123: v })}
|
||||
thumbColor={config.negAcc123 ? '#4a9eff' : '#444'}
|
||||
trackColor={{ false: '#1e1e2e', true: '#1a3a66' }}
|
||||
thumbColor={config.negAcc123 ? theme.blue.fg : theme.text.muted}
|
||||
trackColor={{ false: theme.bg.border, true: theme.blue.border }}
|
||||
/>
|
||||
<Text style={S.switchLabel}>CH4-6</Text>
|
||||
<Text style={[S.switchLabel, { color: theme.text.muted }]}>CH4-6</Text>
|
||||
<Switch
|
||||
value={config.negAcc456}
|
||||
onValueChange={(v) => patch({ negAcc456: v })}
|
||||
thumbColor={config.negAcc456 ? '#4a9eff' : '#444'}
|
||||
trackColor={{ false: '#1e1e2e', true: '#1a3a66' }}
|
||||
thumbColor={config.negAcc456 ? theme.blue.fg : theme.text.muted}
|
||||
trackColor={{ false: theme.bg.border, true: theme.blue.border }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -175,16 +191,16 @@ export function ParamForm({ onAnyChange }: FormProps = {}) {
|
||||
/>
|
||||
|
||||
{/* File prefix */}
|
||||
<View style={S.row}>
|
||||
<Text style={S.rowLabel}>文件前缀</Text>
|
||||
<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}
|
||||
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="#3a3a5a"
|
||||
placeholderTextColor={theme.text.muted}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -194,7 +210,7 @@ export function ParamForm({ onAnyChange }: FormProps = {}) {
|
||||
}
|
||||
|
||||
const S = StyleSheet.create({
|
||||
container: { backgroundColor: '#090912' },
|
||||
container: {},
|
||||
|
||||
sectionHeader: {
|
||||
paddingHorizontal: 16,
|
||||
@@ -202,7 +218,6 @@ const S = StyleSheet.create({
|
||||
paddingBottom: 6,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: '#3a3a5a',
|
||||
fontSize: 9,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 2,
|
||||
@@ -215,34 +230,27 @@ const S = StyleSheet.create({
|
||||
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 },
|
||||
rowLabel: { 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' },
|
||||
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,
|
||||
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 },
|
||||
suffix: { fontSize: 10, marginLeft: 8 },
|
||||
hint: { fontSize: 9, marginLeft: 4 },
|
||||
|
||||
// Channel chips
|
||||
channelRow: { flexDirection: 'row', gap: 6, flex: 1 },
|
||||
@@ -251,16 +259,12 @@ const S = StyleSheet.create({
|
||||
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' },
|
||||
chText: { fontSize: 12, fontWeight: '700' },
|
||||
|
||||
// Switch row
|
||||
switchRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 },
|
||||
switchLabel: { color: '#4a4a6a', fontSize: 12 },
|
||||
switchLabel: { fontSize: 12 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user