Files
TriloopTem_App/src/components/SessionSelector.tsx
T
2026-06-20 21:43:09 +08:00

145 lines
5.7 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View, Text, TouchableOpacity, Modal, FlatList,
StyleSheet, ActivityIndicator, Platform,
} from 'react-native';
import * as StorageService from '../services/StorageService';
import type { SessionInfo } from '../services/StorageService';
import { useTheme } from '../design/tokens';
import { Radius } from '../design/tokens';
interface Props {
selectedSessionId: string;
projectId: string;
onSelect: (sessionId: string) => void;
}
export function SessionSelector({ selectedSessionId, projectId, onSelect }: Props) {
const theme = useTheme();
const [visible, setVisible] = useState(false);
const [sessions, setSessions] = useState<SessionInfo[]>([]);
const [loading, setLoading] = useState(false);
const current = sessions.find((s) => s.sessionId === selectedSessionId);
const reload = useCallback(async () => {
setLoading(true);
const data = await StorageService.listSessionsByProject(projectId);
setSessions(data);
setLoading(false);
}, [projectId]);
useEffect(() => { void reload(); }, [reload]);
const handleOpen = () => {
void reload();
setVisible(true);
};
const handleSelect = (sid: string) => {
setVisible(false);
if (sid !== selectedSessionId) onSelect(sid);
};
return (
<>
<TouchableOpacity
style={[S.trigger, { backgroundColor: theme.bg.raised, borderColor: theme.bg.border }]}
onPress={handleOpen}
activeOpacity={0.7}
>
<View style={S.triggerLeft}>
<Text style={[S.label, { color: theme.text.muted }]}>测线</Text>
<Text style={[S.sessionId, { color: theme.text.primary }]} numberOfLines={1}>
{selectedSessionId || '--'}
</Text>
</View>
{current && (
<Text style={[S.count, { color: theme.text.ghost }]}>{current.frameCount} 点</Text>
)}
<Text style={[S.arrow, { color: theme.text.muted }]}>▾</Text>
</TouchableOpacity>
<Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
<TouchableOpacity style={S.backdrop} activeOpacity={1} onPress={() => setVisible(false)} />
<View style={[S.sheet, { backgroundColor: theme.bg.surface }]}>
<View style={[S.sheetHandle, { backgroundColor: theme.bg.border }]} />
<Text style={[S.sheetTitle, { color: theme.text.secondary }]}>选择测线</Text>
{loading ? (
<ActivityIndicator style={{ padding: 24 }} color={theme.blue.fg} />
) : sessions.length === 0 ? (
<Text style={[S.empty, { color: theme.text.ghost }]}>当前工程无测线</Text>
) : (
<FlatList
data={sessions}
keyExtractor={(s) => s.sessionId}
style={{ maxHeight: 400 }}
renderItem={({ item }) => {
const active = item.sessionId === selectedSessionId;
const date = new Date(item.createdAt);
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
return (
<TouchableOpacity
style={[S.item, { borderBottomColor: theme.bg.divider }, active && { backgroundColor: theme.blue.bg }]}
onPress={() => handleSelect(item.sessionId)}
activeOpacity={0.7}
>
{active && <View style={[S.dot, { backgroundColor: theme.blue.fg }]} />}
<View style={S.itemLeft}>
<Text style={[S.itemId, { color: active ? theme.blue.fg : theme.text.primary }]} numberOfLines={1}>
{item.sessionId}
</Text>
<Text style={[S.itemSub, { color: theme.text.muted }]}>
{dateStr} · {item.frameCount} 测点
</Text>
</View>
{active && <Text style={[S.activeLabel, { color: theme.blue.fg }]}>当前</Text>}
</TouchableOpacity>
);
}}
/>
)}
</View>
</Modal>
</>
);
}
const MONO = { fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' } as const;
const S = StyleSheet.create({
trigger: {
flexDirection: 'row', alignItems: 'center',
paddingHorizontal: 12, paddingVertical: 8,
borderRadius: Radius.md, borderWidth: 1, gap: 8,
},
triggerLeft: { flex: 1, gap: 2 },
label: { fontSize: 9, fontWeight: '600' },
sessionId: { fontSize: 11, fontWeight: '600', ...MONO },
count: { fontSize: 10, ...MONO },
arrow: { fontSize: 12 },
backdrop: { ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0,0,0,0.5)' },
sheet: {
position: 'absolute', bottom: 0, left: 0, right: 0,
maxHeight: '70%',
borderTopLeftRadius: 20, borderTopRightRadius: 20,
paddingBottom: 32,
},
sheetHandle: { width: 36, height: 4, borderRadius: 2, alignSelf: 'center', marginTop: 10, marginBottom: 10 },
sheetTitle: { fontSize: 14, fontWeight: '700', paddingHorizontal: 16, paddingBottom: 10 },
empty: { fontSize: 12, textAlign: 'center', paddingVertical: 24 },
item: {
flexDirection: 'row', alignItems: 'center',
paddingHorizontal: 16, paddingVertical: 12,
borderBottomWidth: StyleSheet.hairlineWidth, gap: 8,
},
dot: { width: 6, height: 6, borderRadius: 3 },
itemLeft: { flex: 1, gap: 2 },
itemId: { fontSize: 12, fontWeight: '600', ...MONO },
itemSub: { fontSize: 10 },
activeLabel:{ fontSize: 10, fontWeight: '700' },
});