TriloopTem_App/app/(tabs)/records.tsx
2026-06-06 22:13:00 +08:00

160 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from 'react-native';
import { router } from 'expo-router';
import { useDataStore } from '../../src/stores/dataStore';
import { exportCsv, shareFile } from '../../src/utils/export';
import { formatUtc, formatCoord, formatUV } from '../../src/utils/format';
import type { MeasurementFrame } from '../../src/protocol/types';
export default function RecordsScreen() {
const { history, sessionId, clearHistory } = useDataStore();
const [exporting, setExporting] = useState(false);
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 renderItem = ({ item }: { item: MeasurementFrame }) => {
const m = item.meta;
const peakUV = item.adcUV[0]
? item.adcUV[0].reduce((mx, v) => Math.max(mx, Math.abs(v)), 0)
: 0;
return (
<View style={styles.item}>
<View style={styles.itemHeader}>
<Text style={styles.frameId}>#{item.frameId}</Text>
<Text style={styles.time}>{m ? formatUtc(m.utc) : '--'}</Text>
</View>
{m && (
<>
<Text style={styles.coord}>
{formatCoord(m.latitude, false)} {formatCoord(m.longitude, true)}
</Text>
<View style={styles.itemFooter}>
<Text style={styles.meta}> {item.accNum}</Text>
<Text style={styles.meta}> ×{item.gain}</Text>
<Text style={styles.meta}>CH1峰值 {formatUV(peakUV)}</Text>
<View style={[styles.gpsIndicator, { backgroundColor: m.gpsStatus > 0 ? '#1a4a1a' : '#2a1a1a' }]}>
<Text style={{ color: m.gpsStatus > 0 ? '#44cc44' : '#884444', fontSize: 9 }}>
GPS {m.gpsStatus > 0 ? '✓' : '✗'}
</Text>
</View>
</View>
</>
)}
</View>
);
};
return (
<View style={styles.container}>
{/* Toolbar */}
<View style={styles.toolbar}>
<Text style={styles.sessionText}>: {sessionId}</Text>
<Text style={styles.countText}>{history.length} </Text>
<View style={styles.toolbarBtns}>
<TouchableOpacity
style={[styles.toolBtn, styles.exportBtn, exporting && styles.btnDisabled]}
onPress={handleExportAll}
disabled={exporting}
>
{exporting ? (
<ActivityIndicator color="#44cc44" size="small" />
) : (
<Text style={styles.exportText}> CSV</Text>
)}
</TouchableOpacity>
<TouchableOpacity style={[styles.toolBtn, styles.clearBtn]} onPress={handleClear}>
<Text style={styles.clearText}></Text>
</TouchableOpacity>
</View>
</View>
{history.length === 0 ? (
<View style={styles.empty}>
<Text style={styles.emptyText}></Text>
<Text style={styles.emptyHint}></Text>
</View>
) : (
<FlatList
data={history}
keyExtractor={(item) => String(item.frameId)}
renderItem={renderItem}
contentContainerStyle={styles.list}
ItemSeparatorComponent={() => <View style={styles.sep} />}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#0d0d0d' },
toolbar: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
backgroundColor: '#111',
borderBottomWidth: 1,
borderBottomColor: '#2a2a2a',
gap: 8,
flexWrap: 'wrap',
},
sessionText: { color: '#555', fontSize: 10, flex: 1 },
countText: { color: '#888', fontSize: 12, fontWeight: '600' },
toolbarBtns: { flexDirection: 'row', gap: 8 },
toolBtn: { paddingHorizontal: 12, paddingVertical: 6, borderRadius: 6 },
exportBtn: { backgroundColor: '#1a3a1a', borderWidth: 1, borderColor: '#2a5a2a' },
exportText: { color: '#44cc44', fontSize: 12, fontWeight: '600' },
clearBtn: { backgroundColor: '#3a1a1a', borderWidth: 1, borderColor: '#5a2a2a' },
clearText: { color: '#ff6666', fontSize: 12 },
btnDisabled: { opacity: 0.4 },
list: { padding: 10, gap: 8 },
item: {
backgroundColor: '#181818',
borderRadius: 8,
padding: 10,
borderWidth: 1,
borderColor: '#2a2a2a',
},
itemHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
frameId: { color: '#4a9eff', fontSize: 12, fontWeight: '700', fontFamily: 'monospace' },
time: { color: '#666', fontSize: 10, fontFamily: 'monospace' },
coord: { color: '#888', fontSize: 10, marginBottom: 4 },
itemFooter: { flexDirection: 'row', alignItems: 'center', gap: 8, flexWrap: 'wrap' },
meta: { color: '#555', fontSize: 10 },
gpsIndicator: { borderRadius: 4, paddingHorizontal: 4, paddingVertical: 1 },
sep: { height: StyleSheet.hairlineWidth, backgroundColor: '#222' },
empty: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 8 },
emptyText: { color: '#444', fontSize: 16 },
emptyHint: { color: '#333', fontSize: 12 },
});