import React, { useState, useMemo } from 'react';
import {
View, Text, FlatList, TouchableOpacity, Modal,
StyleSheet, Alert, ActivityIndicator, Platform,
useWindowDimensions, ScrollView,
} from 'react-native';
import { useDataStore } from '../../src/stores/dataStore';
import { useDeviceStore } from '../../src/stores/deviceStore';
import { exportCsv, shareFile } from '../../src/utils/export';
import { formatUtc, formatCoord, formatUV } from '../../src/utils/format';
import { CHANNEL_COLORS } from '../../src/protocol/constants';
import { WaveformChart } from '../../src/components/WaveformChart';
import { computeWaveformData } from '../../src/hooks/useWaveform';
import type { MeasurementFrame } from '../../src/protocol/types';
// ── Waveform modal ──────────────────────────────────────────────────────────
function FrameWaveformModal({
frame,
onClose,
}: {
frame: MeasurementFrame;
onClose: () => void;
}) {
const { width } = useWindowDimensions();
const sampleFreqCode = useDeviceStore((s) => s.config.sampleFreq);
const [logScale, setLogScale] = useState(true);
const visible = useMemo(
() => Array(frame.meta?.channelNum ?? frame.adcUV.length).fill(true),
[frame],
);
const data = useMemo(
() => computeWaveformData(frame, visible, sampleFreqCode),
[frame, visible, sampleFreqCode],
);
const CHART_H = Math.min(260, Math.round(width * 0.6));
return (
帧 #{frame.frameId}
setLogScale((v) => !v)}
>
{logScale ? 'LOG' : 'LIN'}
✕
{data ? (
) : (
无波形数据
)}
{frame.adcUV.map((ch, i) => {
const pk = ch.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0);
return (
CH{i + 1}
{formatUV(pk)}
);
})}
{frame.meta && (
UTC {formatUtc(frame.meta.utc)} ×{frame.accNum}叠 增益×{frame.gain}
{formatCoord(frame.meta.latitude, false)} {formatCoord(frame.meta.longitude, true)}
)}
);
}
// ── Main screen ─────────────────────────────────────────────────────────────
export default function RecordsScreen() {
const { history, sessionId, projectId, clearHistory, newSession, deleteFrame } = useDataStore();
const [exporting, setExporting] = useState(false);
const [selectedFrame, setSelectedFrame] = useState(null);
const handleExportAll = async () => {
if (history.length === 0) { Alert.alert('无数据', '当前会话没有采集记录'); return; }
setExporting(true);
try {
const path = await exportCsv(history, sessionId);
await shareFile(path);
} catch (e: any) {
Alert.alert('导出失败', e.message);
} finally {
setExporting(false);
}
};
const handleClear = () => {
Alert.alert('清空记录', '确定清空当前会话的所有记录?', [
{ text: '取消', style: 'cancel' },
{ text: '清空', style: 'destructive', onPress: clearHistory },
]);
};
const handleNewSession = () => {
Alert.alert('新建测线', '结束当前测线,开始新测线?', [
{ text: '取消', style: 'cancel' },
{ text: '新建', onPress: () => newSession(projectId) },
]);
};
const handleDeleteFrame = (item: MeasurementFrame) => {
Alert.alert(
`删除测点 #${item.frameId}`,
'确定删除该测点?此操作不可撤销。',
[
{ text: '取消', style: 'cancel' },
{ text: '删除', style: 'destructive', onPress: () => deleteFrame(item.frameId) },
],
);
};
const renderItem = ({ item, index }: { item: MeasurementFrame; index: number }) => {
const m = item.meta;
return (
setSelectedFrame(item)}
activeOpacity={0.75}
>
{String(history.length - index).padStart(3, '0')}
#{item.frameId}
{m ? formatUtc(m.utc) : '--'}
{m && (
0 ? '#0d2018' : '#1a0d10' }]}>
0 ? '#3ddc84' : '#ff5c6e', fontSize: 8, fontWeight: '700' }}>
GPS {m.gpsStatus > 0 ? '✓' : '✗'}
)}
波形 ›
handleDeleteFrame(item)} hitSlop={8}>
×
{m && (
{formatCoord(m.latitude, false)} {formatCoord(m.longitude, true)}
)}
{item.adcUV.slice(0, item.meta?.channelNum ?? 0).map((ch, i) => {
const pk = ch.reduce((mx, v) => Math.max(mx, Math.abs(v)), 0);
return (
{formatUV(pk)}
);
})}
×{item.accNum}叠
增益×{item.gain}
);
};
return (
{/* Toolbar */}
{sessionId}
{history.length} 测点
{exporting
?
: 导出 CSV}
新建
清空
{history.length === 0 ? (
◌
暂无采集记录
在波形页开始采集后,数据将显示在这里
) : (
String(item.frameId)}
renderItem={renderItem}
contentContainerStyle={S.list}
/>
)}
{selectedFrame && (
setSelectedFrame(null)}
/>
)}
);
}
// ── Styles ──────────────────────────────────────────────────────────────────
const S = StyleSheet.create({
container: { flex: 1, backgroundColor: '#090912' },
toolbar: {
flexDirection: 'row', alignItems: 'center',
paddingHorizontal: 14, paddingVertical: 10,
backgroundColor: '#0c0c18',
borderBottomWidth: 1, borderBottomColor: '#1a1a2a',
gap: 8,
},
toolbarLeft: { flex: 1, gap: 2 },
sessionId: { color: '#2a2a4a', fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
count: { color: '#6a6a8a', fontSize: 13, fontWeight: '700' },
toolBtn: { paddingHorizontal: 14, paddingVertical: 7, borderRadius: 8, borderWidth: 1 },
exportBtn: { borderColor: '#1a4a28', backgroundColor: '#0d2018' },
exportText: { color: '#3ddc84', fontSize: 12, fontWeight: '700' },
newBtn: { borderColor: '#2a3a1a', backgroundColor: '#141e0d' },
newText: { color: '#aad464', fontSize: 12, fontWeight: '600' },
clearBtn: { borderColor: '#3a1520', backgroundColor: '#1e0d12' },
clearText: { color: '#ff5c6e', fontSize: 12, fontWeight: '600' },
btnDisabled: { opacity: 0.4 },
list: { padding: 12, gap: 6 },
item: {
flexDirection: 'row',
backgroundColor: '#0e0e1c',
borderRadius: 12, borderWidth: 1, borderColor: '#1a1a2a',
overflow: 'hidden',
},
itemLeft: {
width: 36, backgroundColor: '#0a0a14',
alignItems: 'center', justifyContent: 'center',
borderRightWidth: 1, borderRightColor: '#141422',
},
itemIndex: { color: '#2a2a4a', fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
itemBody: { flex: 1, padding: 10, gap: 5 },
itemHeader: { flexDirection: 'row', alignItems: 'center', gap: 8 },
frameId: { color: '#4a9eff', fontSize: 12, fontWeight: '700', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
time: { color: '#3a3a5a', fontSize: 10, flex: 1, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
gpsBadge: { borderRadius: 4, paddingHorizontal: 5, paddingVertical: 2 },
waveHint: { backgroundColor: '#0d1a2e', borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2 },
waveHintText:{ color: '#2a5a9f', fontSize: 9, fontWeight: '600' },
itemDelBtn: { width: 22, height: 22, borderRadius: 11, backgroundColor: '#2a0a0e', borderWidth: 1, borderColor: '#3a1520', alignItems: 'center', justifyContent: 'center' },
itemDelTxt: { color: '#ff5c6e', fontSize: 14, lineHeight: 18, fontWeight: '700' },
coord: { color: '#4a4a6a', fontSize: 10, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
itemFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 6 },
peakRow: { flexDirection: 'row', gap: 6, flexWrap: 'wrap' },
peakChip: { flexDirection: 'row', alignItems: 'center', gap: 3 },
peakDot: { width: 5, height: 5, borderRadius: 3 },
peakVal: { color: '#6a6a8a', fontSize: 10, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
metaRow: { flexDirection: 'row', gap: 6 },
metaTag: { color: '#2a2a4a', fontSize: 9, backgroundColor: '#111120', borderRadius: 4, paddingHorizontal: 5, paddingVertical: 2 },
empty: { flex: 1, paddingVertical: 60, justifyContent: 'center', alignItems: 'center', gap: 10 },
emptyIcon: { fontSize: 40, color: '#1a1a2a' },
emptyText: { color: '#2a2a4a', fontSize: 15 },
emptyHint: { color: '#1a1a2a', fontSize: 12 },
});
const M = StyleSheet.create({
backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'flex-end' },
sheet: { backgroundColor: '#0e0e1c', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 16, paddingBottom: 32 },
header: { flexDirection: 'row', alignItems: 'center', gap: 10, marginBottom: 12 },
title: { color: '#9090b8', fontSize: 14, fontWeight: '700', flex: 1 },
scaleBtn: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 6, borderWidth: 1, borderColor: '#333', backgroundColor: '#1a1a2a' },
scaleBtnOn: { borderColor: '#4a9eff55', backgroundColor: '#0d2040' },
scaleTxt: { color: '#555', fontSize: 11, fontWeight: '700' },
scaleTxtOn: { color: '#4a9eff' },
closeBtn: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#1a1a2a', alignItems: 'center', justifyContent: 'center' },
closeTxt: { color: '#6a6a8a', fontSize: 14 },
noData: { backgroundColor: '#111', borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
noDataTxt:{ color: '#444' },
peakRow: { flexDirection: 'row', marginTop: 10, gap: 8 },
peakCell: { alignItems: 'center', backgroundColor: '#111120', borderRadius: 8, padding: 8, borderWidth: 1, borderColor: '#1e1e30' },
dot: { width: 6, height: 6, borderRadius: 3, marginBottom: 3 },
peakCh: { color: '#4a4a6a', fontSize: 9 },
peakVal: { color: '#9090b8', fontSize: 11, fontWeight: '600', fontFamily: 'monospace' },
meta: { marginTop: 10, backgroundColor: '#0a0a12', borderRadius: 8, padding: 10, gap: 3 },
metaTxt: { color: '#3a3a5a', fontSize: 10, fontFamily: 'monospace' },
});