160 lines
5.6 KiB
TypeScript
160 lines
5.6 KiB
TypeScript
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 },
|
||
});
|