128 lines
5.2 KiB
TypeScript
128 lines
5.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { View, Text, StyleSheet, TouchableOpacity, useWindowDimensions, ScrollView } from 'react-native';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import { DeviceStatusBar } from '../../src/components/DeviceStatusBar';
|
|
import { WaveformChart } from '../../src/components/WaveformChart';
|
|
import { ChannelSelector } from '../../src/components/ChannelSelector';
|
|
import { useWaveform } from '../../src/hooks/useWaveform';
|
|
import { useDataStore } from '../../src/stores/dataStore';
|
|
import { useDeviceStore } from '../../src/stores/deviceStore';
|
|
import { formatUV, formatUtc, formatCoord } from '../../src/utils/format';
|
|
import { useTheme } from '../../src/design/tokens';
|
|
|
|
export default function WaveformScreen() {
|
|
const insets = useSafeAreaInsets();
|
|
const theme = useTheme();
|
|
const { width, height: windowHeight } = useWindowDimensions();
|
|
const channelNum = useDeviceStore((s) => s.config.channelNum);
|
|
const [visible, setVisible] = useState(() => Array(6).fill(true));
|
|
const [logScale, setLogScale] = useState(false);
|
|
|
|
const frame = useDataStore((s) => s.currentFrame);
|
|
const data = useWaveform(visible);
|
|
|
|
const toggleChannel = (idx: number) =>
|
|
setVisible((prev) => prev.map((v, i) => (i === idx ? !v : v)));
|
|
|
|
const CHART_HEIGHT = Math.round(Math.min(300, windowHeight * 0.38));
|
|
|
|
return (
|
|
<View style={[S.container, { backgroundColor: theme.bg.base }]}>
|
|
<View style={{ paddingTop: insets.top }}>
|
|
<DeviceStatusBar />
|
|
</View>
|
|
|
|
<View style={S.toolbar}>
|
|
<ChannelSelector maxChannels={channelNum} visible={visible} onToggle={toggleChannel} />
|
|
<TouchableOpacity
|
|
style={[S.scaleBtn, { borderColor: theme.bg.border, backgroundColor: theme.bg.surface }, !logScale && { borderColor: theme.blue.fg + '55', backgroundColor: theme.blue.bg }]}
|
|
onPress={() => setLogScale((v) => !v)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Text style={[S.scaleBtnText, { color: theme.text.muted }, !logScale && { color: theme.blue.fg }]}>
|
|
{logScale ? 'LOG' : 'LIN'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Waveform chart */}
|
|
<View style={[S.chartWrap, { borderBottomColor: theme.bg.border }]}>
|
|
{data ? (
|
|
<WaveformChart
|
|
data={data}
|
|
visibleChannels={visible}
|
|
width={width}
|
|
height={CHART_HEIGHT}
|
|
logScale={logScale}
|
|
/>
|
|
) : (
|
|
<View style={[S.placeholder, { height: CHART_HEIGHT, backgroundColor: theme.bg.surface }]}>
|
|
<Text style={[S.placeholderText, { color: theme.text.muted }]}>等待采样数据...</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Per-channel peak values + metadata */}
|
|
{frame && (
|
|
<ScrollView style={S.peakScroll}>
|
|
<View style={S.peakRow}>
|
|
{frame.adcUV.map((ch, idx) => {
|
|
if (!visible[idx] || idx >= channelNum) return null;
|
|
const absMax = ch.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0);
|
|
return (
|
|
<View key={idx} style={[S.peakCell, { backgroundColor: theme.bg.surface, borderColor: theme.bg.border }]}>
|
|
<Text style={[S.peakLabel, { color: theme.text.secondary }]}>CH{idx + 1}</Text>
|
|
<Text style={[S.peakValue, { color: theme.text.primary }]}>{formatUV(absMax)}</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{frame.meta && (
|
|
<View style={[S.metaBlock, { backgroundColor: theme.bg.raised }]}>
|
|
<Text style={[S.metaText, { color: theme.text.muted }]}>
|
|
帧 #{frame.frameId} UTC: {formatUtc(frame.meta.utc)}
|
|
</Text>
|
|
<Text style={[S.metaText, { color: theme.text.muted }]}>
|
|
{formatCoord(frame.meta.latitude, false)} {formatCoord(frame.meta.longitude, true)} 海拔 {frame.meta.altitude.toFixed(1)} m
|
|
</Text>
|
|
<Text style={[S.metaText, { color: theme.text.muted }]}>
|
|
Roll {frame.meta.roll.toFixed(1)}° Pitch {frame.meta.pitch.toFixed(1)}° Yaw {frame.meta.yaw.toFixed(1)}°
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const S = StyleSheet.create({
|
|
container: { flex: 1 },
|
|
toolbar: { flexDirection: 'row', alignItems: 'center', paddingRight: 10 },
|
|
chartWrap: { borderBottomWidth: 1 },
|
|
placeholder: { justifyContent: 'center', alignItems: 'center' },
|
|
placeholderText: { fontSize: 14 },
|
|
|
|
scaleBtn: {
|
|
marginLeft: 8,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 5,
|
|
borderRadius: 6,
|
|
borderWidth: 1,
|
|
},
|
|
scaleBtnText: { fontSize: 11, fontWeight: '700', letterSpacing: 0.5 },
|
|
|
|
peakScroll: { flex: 1 },
|
|
peakRow: { flexDirection: 'row', flexWrap: 'wrap', padding: 12, gap: 10 },
|
|
peakCell: {
|
|
borderRadius: 8, padding: 10,
|
|
minWidth: 90, alignItems: 'center',
|
|
borderWidth: 1,
|
|
},
|
|
peakLabel: { fontSize: 11 },
|
|
peakValue: { fontSize: 13, fontWeight: '600', marginTop: 2, fontFamily: 'monospace' },
|
|
metaBlock: { margin: 12, borderRadius: 8, padding: 10, gap: 3 },
|
|
metaText: { fontSize: 10, fontFamily: 'monospace' },
|
|
});
|