2026-06-20 21:43:09 +08:00

764 lines
25 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import {
View, Text, StyleSheet, TouchableOpacity, ScrollView,
Modal, ActivityIndicator, useWindowDimensions, Platform,
useColorScheme,
} from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useConnectionStore } from '../../src/stores/connectionStore';
import { useDeviceStore } from '../../src/stores/deviceStore';
import { useDataStore } from '../../src/stores/dataStore';
import { useDevice } from '../../src/hooks/useDevice';
import { useWaveform, type WaveformData } from '../../src/hooks/useWaveform';
import { WaveformChart } from '../../src/components/WaveformChart';
import { ParamForm } from '../../src/components/ParamForm';
import { NoProjectGate } from '../../src/components/NoProjectGate';
import { CHANNEL_COLORS } from '../../src/protocol/constants';
import { formatUV, formatUtc, formatCoord, sourceModeLabel, formatBattery, formatTemperature } from '../../src/utils/format';
import { useTheme } from '../../src/design/tokens';
import { Spacing, Radius } from '../../src/design/tokens';
import * as ScreenOrientation from 'expo-screen-orientation';
import type { MeasurementFrame } from '../../src/protocol/types';
// ── Param sheet ───────────────────────────────────────────────────────────────
function ParamSheet({
visible,
onClose,
onSetup,
busy,
configDirty,
}: {
visible: boolean;
onClose: () => void;
onSetup: () => Promise<void>;
busy: boolean;
configDirty: boolean;
}) {
const theme = useTheme();
const { deviceStatus } = useDeviceStore();
const measuring = deviceStatus !== 'idle';
return (
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
statusBarTranslucent
>
<TouchableOpacity style={PS.backdrop} activeOpacity={1} onPress={onClose} />
<View style={[PS.sheet, {
backgroundColor: theme.bg.surface,
borderColor: theme.bg.border,
}]}>
<View style={[PS.handle, { backgroundColor: theme.bg.border }]} />
<View style={PS.header}>
<Text style={[PS.title, { color: theme.text.secondary }]}></Text>
{configDirty && (
<View style={[PS.dirtyBadge, {
backgroundColor: theme.amber.bg,
borderColor: theme.amber.border,
}]}>
<Text style={[PS.dirtyTxt, { color: theme.amber.fg }]}></Text>
</View>
)}
</View>
<ScrollView style={PS.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
<ParamForm />
<View style={{ height: Spacing.md }} />
</ScrollView>
<TouchableOpacity
style={[
PS.setupBtn,
{
backgroundColor: theme.bg.raised,
borderColor: theme.bg.border,
},
configDirty && {
backgroundColor: theme.blue.bg,
borderColor: theme.blue.fg,
},
(busy || measuring) && PS.setupBtnDis,
]}
onPress={onSetup}
disabled={busy || measuring}
activeOpacity={0.8}
>
{busy
? <ActivityIndicator color={theme.blue.fg} />
: <Text style={[
PS.setupBtnTxt,
{ color: theme.text.muted },
configDirty && { color: theme.blue.fg },
]}>
{measuring ? '采集中,不可下发' : '下 发 配 置'}
</Text>}
</TouchableOpacity>
</View>
</Modal>
);
}
const PS = StyleSheet.create({
backdrop: { ...StyleSheet.absoluteFill, backgroundColor: 'rgba(0,0,0,0.6)' },
sheet: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
maxHeight: '85%',
borderTopLeftRadius: Radius.xl,
borderTopRightRadius: Radius.xl,
borderTopWidth: 1,
paddingBottom: Spacing.xxl,
},
handle: {
width: 36, height: 4, borderRadius: 2,
alignSelf: 'center',
marginTop: Spacing.sm, marginBottom: Spacing.sm,
},
header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.lg, paddingBottom: Spacing.sm, gap: Spacing.sm },
title: { fontSize: 14, fontWeight: '700', flex: 1 },
dirtyBadge: { borderRadius: Radius.sm, paddingHorizontal: 7, paddingVertical: 2, borderWidth: 1 },
dirtyTxt: { fontSize: 9, fontWeight: '700' },
scroll: { flex: 1 },
setupBtn: {
margin: Spacing.lg,
borderRadius: Radius.lg,
paddingVertical: 14,
alignItems: 'center',
borderWidth: 1,
},
setupBtnDis: { opacity: 0.4 },
setupBtnTxt: { fontWeight: '700', fontSize: 14, letterSpacing: 3 },
});
// ── Control row ───────────────────────────────────────────────────────────────
function ControlRow({
deviceStatus,
busy,
frameId,
onContinuous,
onSingle,
onStop,
onSettings,
configDirty,
}: {
deviceStatus: 'idle' | 'running' | 'single';
busy: boolean;
frameId: number | undefined;
onContinuous: () => void;
onSingle: () => void;
onStop: () => void;
onSettings: () => void;
configDirty: boolean;
}) {
const theme = useTheme();
const idle = deviceStatus === 'idle';
const running = deviceStatus === 'running';
const single = deviceStatus === 'single';
return (
<View style={[CR.row, {
borderTopColor: theme.bg.border,
backgroundColor: theme.bg.surface,
}]}>
{/* Primary action */}
{idle && (
<TouchableOpacity
style={[CR.primary, {
backgroundColor: theme.green.bg,
borderColor: theme.green.border,
}, !busy ? null : CR.dis]}
onPress={onContinuous}
disabled={busy}
activeOpacity={0.85}
>
{busy
? <ActivityIndicator color={theme.green.fg} />
: <>
<Text style={[CR.primaryIcon, { color: theme.text.ghost }]}></Text>
<Text style={[CR.primaryTxt, { color: theme.green.fg }]}> </Text>
</>}
</TouchableOpacity>
)}
{running && (
<TouchableOpacity
style={[CR.primary, {
backgroundColor: theme.red.bg,
borderColor: theme.red.border,
}]}
onPress={onStop}
activeOpacity={0.85}
>
<Text style={[CR.primaryIcon, { color: theme.text.ghost }]}></Text>
<Text style={[CR.primaryTxt, { color: theme.red.fg }]}> </Text>
</TouchableOpacity>
)}
{single && (
<View style={[CR.primary, {
backgroundColor: theme.teal.bg,
borderColor: theme.teal.border,
}, CR.dis]}>
<ActivityIndicator color={theme.teal.fg} size="small" />
<Text style={[CR.primaryTxt, { color: theme.teal.fg }]}></Text>
</View>
)}
{/* Secondary: single-shot button (idle only) */}
{idle && (
<TouchableOpacity
style={[CR.secondary, {
backgroundColor: theme.teal.bg,
borderColor: theme.teal.border,
}, !busy ? null : CR.dis]}
onPress={onSingle}
disabled={busy}
activeOpacity={0.85}
>
<Text style={[CR.secondaryIcon, { color: theme.text.ghost }]}></Text>
<Text style={[CR.secondaryTxt, { color: theme.teal.fg }]}></Text>
</TouchableOpacity>
)}
{/* Running indicator (running only) */}
{running && frameId !== undefined && (
<View style={[CR.indicator, {
backgroundColor: theme.green.bg,
borderColor: theme.green.border,
}]}>
<View style={[CR.runDot, { backgroundColor: theme.green.fg }]} />
<Text style={[CR.indicatorTxt, { color: theme.green.fg }]}>#{frameId}</Text>
</View>
)}
{/* Settings button — always present */}
<TouchableOpacity
style={[CR.settingsBtn, {
backgroundColor: theme.bg.raised,
borderColor: theme.bg.border,
}]}
onPress={onSettings}
activeOpacity={0.8}
>
<Text style={[CR.settingsTxt, { color: theme.text.muted }, configDirty && { color: theme.amber.fg }]}></Text>
{configDirty && (
<View style={[CR.dirtyDot, {
backgroundColor: theme.amber.fg,
borderColor: theme.bg.surface,
}]} />
)}
</TouchableOpacity>
</View>
);
}
const CR = StyleSheet.create({
row: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
gap: Spacing.sm,
borderTopWidth: 1,
},
primary: {
flex: 2,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 14,
borderRadius: Radius.lg,
borderWidth: 1,
},
dis: { opacity: 0.35 },
primaryIcon: { fontSize: 11 },
primaryTxt: { fontSize: 13, fontWeight: '700', letterSpacing: 1.5 },
secondary: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 5,
paddingVertical: 14,
borderRadius: Radius.lg,
borderWidth: 1,
},
secondaryIcon:{ fontSize: 11 },
secondaryTxt: { fontSize: 12, fontWeight: '700' },
indicator: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
paddingVertical: 14,
borderRadius: Radius.lg,
borderWidth: 1,
},
runDot: { width: 6, height: 6, borderRadius: 3 },
indicatorTxt: { fontSize: 12, fontWeight: '700', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
settingsBtn: {
width: 44, height: 44,
borderRadius: Radius.md,
borderWidth: 1,
alignItems: 'center', justifyContent: 'center',
},
settingsTxt: { fontSize: 18 },
dirtyDot: {
position: 'absolute', top: 6, right: 6,
width: 7, height: 7, borderRadius: 4,
borderWidth: 1,
},
});
// ── Status panel ─────────────────────────────────────────────────────────────
const CURRENT_RATIO = 50;
const INT32_MAX = 0x7fffffff;
function formatCurrent(raw: number): string {
const amps = (raw / INT32_MAX) * 5.0 * CURRENT_RATIO;
return `${amps.toFixed(1)} A`;
}
function StatusPanel({ frame }: { frame: MeasurementFrame }) {
const theme = useTheme();
const m = frame.meta;
const mono = { fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' } as const;
const row1 = [
{ label: '源模式', value: sourceModeLabel(m.sourceMode) },
{ label: '发射电流', value: formatCurrent(m.current) },
{ label: '电池', value: formatBattery(m.batteryVolt) },
{ label: '温度', value: formatTemperature(m.temperature) },
];
const row2 = [
{ label: 'Roll', value: `${m.roll.toFixed(1)}°` },
{ label: 'Pitch', value: `${m.pitch.toFixed(1)}°` },
{ label: 'Yaw', value: `${m.yaw.toFixed(1)}°` },
{ label: 'GPS', value: ['未定位','非差分','差分','无效PPS','固定解','浮点解','估算中'][m.gpsStatus] ?? '未知', highlight: m.gpsStatus > 0 },
{ label: 'SD', value: ['正常','未插卡','异常'][m.sdStatus] ?? '未知', highlight: m.sdStatus === 0 },
];
const renderCell = (it: { label: string; value: string; highlight?: boolean }) => (
<View key={it.label} style={[SP.cell, { borderColor: theme.bg.border, backgroundColor: theme.bg.surface }]}>
<Text style={[SP.label, { color: theme.text.muted }]}>{it.label}</Text>
<Text style={[SP.value, mono, { color: it.highlight ? theme.green.fg : theme.text.secondary }]}>{it.value}</Text>
</View>
);
return (
<View style={[SP.container, { borderTopColor: theme.bg.divider, backgroundColor: theme.bg.void }]}>
<View style={SP.row}>{row1.map(renderCell)}</View>
<View style={SP.row}>{row2.map(renderCell)}</View>
</View>
);
}
const SP = StyleSheet.create({
container: { paddingHorizontal: Spacing.md, paddingVertical: 4, gap: 4, borderTopWidth: StyleSheet.hairlineWidth },
row: { flexDirection: 'row', gap: 4 },
cell: { flex: 1, alignItems: 'center', paddingVertical: 3, borderRadius: Radius.sm, borderWidth: 1 },
label: { fontSize: 8, fontWeight: '600' },
value: { fontSize: 10, fontWeight: '700', marginTop: 1 },
});
// ── Frame summary bar ─────────────────────────────────────────────────────────
function FrameSummaryBar({ frame }: { frame: MeasurementFrame }) {
const theme = useTheme();
return (
<View style={[FS.bar, {
backgroundColor: theme.bg.void,
borderTopColor: theme.bg.divider,
}]}>
<Text style={[FS.txt, { color: theme.text.ghost }]}>
#{frame.frameId}
{' '}{frame.meta ? formatUtc(frame.meta.utc) : ''}
{frame.meta?.latitude ? ` ${formatCoord(frame.meta.latitude, false)} ${formatCoord(frame.meta.longitude, true)}` : ''}
{frame.meta ? ` R${frame.meta.roll.toFixed(1)} P${frame.meta.pitch.toFixed(1)} Y${frame.meta.yaw.toFixed(1)}` : ''}
</Text>
</View>
);
}
const FS = StyleSheet.create({
bar: { paddingHorizontal: Spacing.md, paddingVertical: 5, borderTopWidth: StyleSheet.hairlineWidth },
txt: { fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' },
});
// ── Not connected placeholder ─────────────────────────────────────────────────
function NotConnected({ onConnect }: { onConnect: () => void }) {
const theme = useTheme();
return (
<View style={NC.root}>
<Text style={[NC.icon, { color: theme.bg.border }]}></Text>
<Text style={[NC.title, { color: theme.text.muted }]}></Text>
<Text style={[NC.sub, { color: theme.text.ghost }]}> TEM </Text>
<TouchableOpacity
style={[NC.btn, {
backgroundColor: theme.blue.bg,
borderColor: theme.blue.fg,
}]}
onPress={onConnect}
activeOpacity={0.8}
>
<Text style={[NC.btnTxt, { color: theme.blue.fg }]}> </Text>
</TouchableOpacity>
</View>
);
}
const NC = StyleSheet.create({
root: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 },
icon: { fontSize: 48 },
title: { fontSize: 17, fontWeight: '700' },
sub: { fontSize: 12 },
btn: {
marginTop: Spacing.sm,
borderRadius: Radius.lg,
paddingVertical: 13,
paddingHorizontal: 32,
borderWidth: 1,
},
btnTxt: { fontSize: 14, fontWeight: '700', letterSpacing: 2 },
});
// ── Main screen ───────────────────────────────────────────────────────────────
export default function WaveScreen() {
const theme = useTheme();
const { width: sw, height: sh } = useWindowDimensions();
const { status, showModal } = useConnectionStore();
const { config, configDirty } = useDeviceStore();
const { deviceStatus } = useDeviceStore();
const { busy, setup, startContinuous, startSingle, stop } = useDevice();
const frame = useDataStore((s) => s.currentFrame);
const sessionId = useDataStore((s) => s.sessionId);
const projectId = useDataStore((s) => s.projectId);
const hasProject = useDataStore((s) => s.hasProject);
const [visible, setVisible] = useState(() => Array(6).fill(true));
const [logScale, setLogScale] = useState(false);
const [sheetVisible, setSheetVisible] = useState(false);
const [fullscreen, setFullscreen] = useState(false);
const [projectName, setProjectName] = useState<string | null>(null);
useEffect(() => {
if (fullscreen) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
}
return () => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
};
}, [fullscreen]);
const connected = status === 'connected';
const channelNum = config.channelNum;
const CHART_H = Math.round(Math.min(280, sh * 0.36));
const data = useWaveform(visible);
useEffect(() => {
if (!projectId) { setProjectName(null); return; }
import('../../src/services/StorageService').then(({ listProjects }) =>
listProjects().then((ps) => setProjectName(ps.find((p) => p.projectId === projectId)?.name ?? null)),
).catch(() => setProjectName(null));
}, [projectId]);
const toggleChannel = (idx: number) =>
setVisible((prev) => prev.map((v, i) => (i === idx ? !v : v)));
const handleSetup = async () => {
await setup();
setSheetVisible(false);
};
if (!connected) {
return (
<View style={[S.root, { backgroundColor: theme.bg.base }]}>
<NotConnected onConnect={showModal} />
</View>
);
}
if (!hasProject) {
return (
<View style={[S.root, { backgroundColor: theme.bg.base }]}>
<NoProjectGate />
</View>
);
}
return (
<View style={[S.root, { backgroundColor: theme.bg.base }]}>
{/* ── Context bar: project · session ── */}
<View style={[S.ctxBar, {
backgroundColor: theme.bg.void,
borderBottomColor: theme.bg.divider,
}]}>
{projectName
? <Text style={[S.ctxTxt, { color: theme.text.ghost }]} numberOfLines={1}>{projectName} · {sessionId}</Text>
: <Text style={[S.ctxTxt, { color: theme.text.ghost }]} numberOfLines={1}>{sessionId}</Text>}
</View>
{/* ── Channel selector + LOG/LIN ── */}
<View style={[S.toolbar, { borderBottomColor: theme.bg.divider }]}>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={S.chRow}>
{Array.from({ length: channelNum }, (_, i) => (
<TouchableOpacity
key={i}
style={[
S.chBtn,
{ borderColor: theme.bg.border },
visible[i] && { borderColor: CHANNEL_COLORS[i] + '99', backgroundColor: CHANNEL_COLORS[i] + '1a' },
]}
onPress={() => toggleChannel(i)}
activeOpacity={0.7}
>
<View style={[S.chDot, { backgroundColor: visible[i] ? CHANNEL_COLORS[i] : theme.bg.border }]} />
<Text style={[S.chTxt, { color: theme.text.ghost }, visible[i] && { color: CHANNEL_COLORS[i] }]}>CH{i + 1}</Text>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity
style={[S.logBtn, {
borderColor: theme.bg.border,
backgroundColor: theme.bg.raised,
}, !logScale && {
borderColor: theme.blue.border,
backgroundColor: theme.blue.bg,
}]}
onPress={() => setLogScale((v) => !v)}
activeOpacity={0.7}
>
<Text style={[S.logTxt, { color: theme.text.muted }, !logScale && { color: theme.blue.fg }]}>
{logScale ? 'LOG' : 'LIN'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[S.fullBtn, {
borderColor: theme.bg.border,
backgroundColor: theme.bg.raised,
}]}
onPress={() => setFullscreen(true)}
activeOpacity={0.7}
>
<Text style={[S.fullTxt, { color: theme.text.muted }]}></Text>
</TouchableOpacity>
</View>
{/* ── Waveform chart ── */}
<View style={[S.chartWrap, { borderBottomColor: theme.bg.divider }]}>
{data ? (
<WaveformChart
data={data}
visibleChannels={visible}
width={sw}
height={CHART_H}
logScale={logScale}
interactive={false}
/>
) : (
<View style={[S.chartPlaceholder, { height: CHART_H, backgroundColor: theme.bg.surface }]}>
<Text style={[S.placeholderTxt, { color: theme.text.ghost }]}></Text>
</View>
)}
</View>
{/* ── Peak values ── */}
{/* ── Frame summary ── */}
{frame && <StatusPanel frame={frame} />}
{frame && <FrameSummaryBar frame={frame} />}
{/* ── Control row ── */}
<ControlRow
deviceStatus={deviceStatus}
busy={busy}
frameId={frame?.frameId}
onContinuous={startContinuous}
onSingle={startSingle}
onStop={stop}
onSettings={() => setSheetVisible(true)}
configDirty={configDirty}
/>
{/* ── Param sheet ── */}
<ParamSheet
visible={sheetVisible}
onClose={() => setSheetVisible(false)}
onSetup={handleSetup}
busy={busy}
configDirty={configDirty}
/>
{/* ── Fullscreen waveform ── */}
<Modal
visible={fullscreen}
animationType="fade"
supportedOrientations={['portrait', 'landscape', 'landscape-left', 'landscape-right']}
statusBarTranslucent
onRequestClose={() => setFullscreen(false)}
>
<FullscreenContent
data={data}
visibleChannels={visible}
logScale={logScale}
onToggleLog={() => setLogScale((v) => !v)}
onClose={() => setFullscreen(false)}
/>
</Modal>
</View>
);
}
function FullscreenContent({
data, visibleChannels, logScale, onToggleLog, onClose,
}: {
data: WaveformData | null;
visibleChannels: boolean[];
logScale: boolean;
onToggleLog: () => void;
onClose: () => void;
}) {
const theme = useTheme();
const scheme = useColorScheme();
const isDark = scheme !== 'light';
const { width: fsW, height: fsH } = useWindowDimensions();
return (
<GestureHandlerRootView style={[S.fsRoot, { backgroundColor: theme.chart.bg }]}>
{data ? (
<WaveformChart
data={data}
visibleChannels={visibleChannels}
width={fsW}
height={fsH}
logScale={logScale}
interactive
/>
) : (
<View style={[S.chartPlaceholder, { flex: 1, backgroundColor: theme.bg.surface }]}>
<Text style={[S.placeholderTxt, { color: theme.text.ghost }]}></Text>
</View>
)}
{/* Floating toolbar */}
<View style={S.fsToolbar}>
<TouchableOpacity
style={[S.logBtn, {
borderColor: theme.bg.border,
backgroundColor: theme.bg.raised,
}, !logScale && {
borderColor: theme.blue.border,
backgroundColor: theme.blue.bg,
}]}
onPress={onToggleLog}
activeOpacity={0.7}
>
<Text style={[S.logTxt, { color: theme.text.muted }, !logScale && { color: theme.blue.fg }]}>
{logScale ? 'LOG' : 'LIN'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[S.fsCloseBtn, {
backgroundColor: isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)',
}]}
onPress={onClose}
activeOpacity={0.7}
>
<Text style={[S.fsCloseTxt, { color: isDark ? '#ffffff' : '#000000' }]}></Text>
</TouchableOpacity>
</View>
</GestureHandlerRootView>
);
}
const S = StyleSheet.create({
root: { flex: 1 },
ctxBar: {
paddingHorizontal: Spacing.md,
paddingVertical: 4,
borderBottomWidth: 1,
},
ctxTxt: {
fontSize: 9,
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
},
toolbar: {
flexDirection: 'row',
alignItems: 'center',
paddingRight: Spacing.sm,
borderBottomWidth: 1,
},
chRow: { flexDirection: 'row', paddingHorizontal: Spacing.sm, paddingVertical: 6, gap: 5 },
chBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 5,
borderRadius: Radius.md,
borderWidth: 1,
},
chDot: { width: 6, height: 6, borderRadius: 3 },
chTxt: { fontSize: 10, fontWeight: '600' },
logBtn: {
paddingHorizontal: 9,
paddingVertical: 5,
borderRadius: Radius.sm,
borderWidth: 1,
marginLeft: 4,
},
logTxt: { fontSize: 10, fontWeight: '700', letterSpacing: 0.5 },
fullBtn: {
paddingHorizontal: 8,
paddingVertical: 5,
borderRadius: Radius.sm,
borderWidth: 1,
marginLeft: 4,
},
fullTxt: { fontSize: 12 },
fsRoot: { flex: 1 },
fsToolbar: {
position: 'absolute',
top: 40,
right: 12,
flexDirection: 'row',
gap: 8,
},
fsCloseBtn: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
},
fsCloseTxt: { fontSize: 16, fontWeight: '700' },
chartWrap: { borderBottomWidth: StyleSheet.hairlineWidth },
chartPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
},
placeholderTxt: { fontSize: 13 },
});