基本功能完成

This commit is contained in:
2026-06-20 21:43:09 +08:00
parent 84590d5477
commit a89d23b43c
45 changed files with 3508 additions and 1261 deletions
+54 -50
View File
@@ -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 },
});