145 lines
5.7 KiB
TypeScript
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' },
|
|
});
|