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' }, });