import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal, ActivityIndicator, useWindowDimensions, Platform, } from 'react-native'; 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 } from '../../src/hooks/useWaveform'; import { WaveformChart } from '../../src/components/WaveformChart'; import { ParamForm } from '../../src/components/ParamForm'; import { CHANNEL_COLORS } from '../../src/protocol/constants'; import { formatUV, formatUtc, formatCoord } from '../../src/utils/format'; import { Colors, Spacing, Radius } from '../../src/design/tokens'; import type { MeasurementFrame } from '../../src/protocol/types'; // ── Param sheet ─────────────────────────────────────────────────────────────── function ParamSheet({ visible, onClose, onSetup, busy, configDirty, }: { visible: boolean; onClose: () => void; onSetup: () => Promise; busy: boolean; configDirty: boolean; }) { const { deviceStatus } = useDeviceStore(); const measuring = deviceStatus !== 'idle'; return ( 采集参数 {configDirty && ( 待下发 )} {busy ? : {measuring ? '采集中,不可下发' : '下 发 配 置'} } ); } 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%', backgroundColor: Colors.bg.surface, borderTopLeftRadius: Radius.xl, borderTopRightRadius: Radius.xl, borderTopWidth: 1, borderColor: Colors.bg.border, paddingBottom: Spacing.xxl, }, handle: { width: 36, height: 4, borderRadius: 2, backgroundColor: Colors.bg.border, alignSelf: 'center', marginTop: Spacing.sm, marginBottom: Spacing.sm, }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.lg, paddingBottom: Spacing.sm, gap: Spacing.sm }, title: { color: Colors.text.secondary, fontSize: 14, fontWeight: '700', flex: 1 }, dirtyBadge: { backgroundColor: Colors.amber.bg, borderRadius: Radius.sm, paddingHorizontal: 7, paddingVertical: 2, borderWidth: 1, borderColor: Colors.amber.border }, dirtyTxt: { color: Colors.amber.fg, fontSize: 9, fontWeight: '700' }, scroll: { flex: 1 }, setupBtn: { margin: Spacing.lg, backgroundColor: Colors.bg.raised, borderRadius: Radius.lg, paddingVertical: 14, alignItems: 'center', borderWidth: 1, borderColor: Colors.bg.border, }, setupBtnDirty: { backgroundColor: Colors.blue.bg, borderColor: Colors.blue.fg }, setupBtnDis: { opacity: 0.4 }, setupBtnTxt: { color: Colors.text.muted, fontWeight: '700', fontSize: 14, letterSpacing: 3 }, setupBtnTxtDirty: { color: Colors.blue.fg }, }); // ── 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 idle = deviceStatus === 'idle'; const running = deviceStatus === 'running'; const single = deviceStatus === 'single'; return ( {/* Primary action */} {idle && ( {busy ? : <> 连 续 采 集 } )} {running && ( 停 止 )} {single && ( 单次采集中… )} {/* Secondary: single-shot button (idle only) */} {idle && ( 单次 )} {/* Running indicator (running only) */} {running && frameId !== undefined && ( #{frameId} )} {/* Settings button — always present */} {configDirty && } ); } const CR = StyleSheet.create({ row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, gap: Spacing.sm, borderTopWidth: 1, borderTopColor: Colors.bg.border, backgroundColor: Colors.bg.surface, }, primary: { flex: 2, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, paddingVertical: 14, borderRadius: Radius.lg, borderWidth: 1, }, green: { backgroundColor: Colors.green.bg, borderColor: Colors.green.border }, red: { backgroundColor: Colors.red.bg, borderColor: Colors.red.border }, teal: { backgroundColor: Colors.teal.bg, borderColor: Colors.teal.border }, dis: { opacity: 0.35 }, primaryIcon: { fontSize: 11, color: '#ffffff88' }, 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, }, tealBtn: { backgroundColor: Colors.teal.bg, borderColor: Colors.teal.border }, secondaryIcon:{ fontSize: 11, color: '#ffffff66' }, secondaryTxt: { fontSize: 12, fontWeight: '700' }, indicator: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, paddingVertical: 14, borderRadius: Radius.lg, backgroundColor: Colors.green.bg, borderWidth: 1, borderColor: Colors.green.border, }, runDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: Colors.green.fg }, indicatorTxt: { color: Colors.green.fg, fontSize: 12, fontWeight: '700', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' }, settingsBtn: { width: 44, height: 44, borderRadius: Radius.md, backgroundColor: Colors.bg.raised, borderWidth: 1, borderColor: Colors.bg.border, alignItems: 'center', justifyContent: 'center', }, settingsTxt: { fontSize: 18, color: Colors.text.muted }, settingsDirty: { color: Colors.amber.fg }, dirtyDot: { position: 'absolute', top: 6, right: 6, width: 7, height: 7, borderRadius: 4, backgroundColor: Colors.amber.fg, borderWidth: 1, borderColor: Colors.bg.surface, }, }); // ── Peak row ────────────────────────────────────────────────────────────────── function PeakRow({ frame, visible, channelNum }: { frame: MeasurementFrame; visible: boolean[]; channelNum: number }) { return ( {frame.adcUV.map((ch, i) => { if (i >= channelNum || !visible[i]) return null; const pk = ch.reduce((m, v) => Math.max(m, Math.abs(v)), 0); return ( CH{i + 1} {formatUV(pk)} ); })} ); } const PK = StyleSheet.create({ scroll: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: Colors.bg.divider }, row: { flexDirection: 'row', paddingHorizontal: Spacing.md, paddingVertical: 7, gap: 10 }, cell: { flexDirection: 'row', alignItems: 'center', gap: 4, backgroundColor: Colors.bg.surface, borderRadius: Radius.sm, paddingHorizontal: 8, paddingVertical: 4, borderWidth: 1, borderColor: Colors.bg.border }, dot: { width: 5, height: 5, borderRadius: 3 }, label: { color: Colors.text.ghost, fontSize: 9, fontWeight: '600' }, value: { fontSize: 11, fontWeight: '600', fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' }, }); // ── Frame summary bar ───────────────────────────────────────────────────────── function FrameSummaryBar({ frame }: { frame: MeasurementFrame }) { const m = frame.meta; return ( #{frame.frameId} {' '}×{frame.accNum}叠 {m ? ` ${formatUtc(m.utc)}` : ''} {m && m.latitude ? ` ${formatCoord(m.latitude, false)} ${formatCoord(m.longitude, true)}` : ''} ); } const FS = StyleSheet.create({ bar: { paddingHorizontal: Spacing.md, paddingVertical: 5, backgroundColor: Colors.bg.void, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: Colors.bg.divider }, txt: { color: Colors.text.ghost, fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace' }, }); // ── Not connected placeholder ───────────────────────────────────────────────── function NotConnected({ onConnect }: { onConnect: () => void }) { return ( 未连接仪器 请先连接 TEM 接收机 连 接 设 备 ); } const NC = StyleSheet.create({ root: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12 }, icon: { fontSize: 48, color: Colors.bg.border }, title: { color: Colors.text.muted, fontSize: 17, fontWeight: '700' }, sub: { color: Colors.text.ghost, fontSize: 12 }, btn: { marginTop: Spacing.sm, backgroundColor: Colors.blue.bg, borderRadius: Radius.lg, paddingVertical: 13, paddingHorizontal: 32, borderWidth: 1, borderColor: Colors.blue.fg, }, btnTxt: { color: Colors.blue.fg, fontSize: 14, fontWeight: '700', letterSpacing: 2 }, }); // ── Main screen ─────────────────────────────────────────────────────────────── export default function WaveScreen() { 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 [visible, setVisible] = useState(() => Array(6).fill(true)); const [logScale, setLogScale] = useState(true); const [sheetVisible, setSheetVisible] = useState(false); const [projectName, setProjectName] = useState(null); 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)), ); }, [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 ( ); } return ( {/* ── Context bar: project · session ── */} {projectName ? {projectName} · {sessionId} : {sessionId}} {/* ── Channel selector + LOG/LIN ── */} {Array.from({ length: channelNum }, (_, i) => ( toggleChannel(i)} activeOpacity={0.7} > CH{i + 1} ))} setLogScale((v) => !v)} activeOpacity={0.7} > {logScale ? 'LOG' : 'LIN'} {/* ── Waveform chart ── */} {data ? ( ) : ( 等待采样数据… )} {/* ── Peak values ── */} {frame && } {/* ── Frame summary ── */} {frame && } {/* ── Control row ── */} setSheetVisible(true)} configDirty={configDirty} /> {/* ── Param sheet ── */} setSheetVisible(false)} onSetup={handleSetup} busy={busy} configDirty={configDirty} /> ); } const S = StyleSheet.create({ root: { flex: 1, backgroundColor: Colors.bg.base }, ctxBar: { paddingHorizontal: Spacing.md, paddingVertical: 4, backgroundColor: Colors.bg.void, borderBottomWidth: 1, borderBottomColor: Colors.bg.divider, }, ctxTxt: { color: Colors.text.ghost, fontSize: 9, fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace', }, toolbar: { flexDirection: 'row', alignItems: 'center', paddingRight: Spacing.sm, borderBottomWidth: 1, borderBottomColor: Colors.bg.divider, }, 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, borderColor: Colors.bg.border, }, chDot: { width: 6, height: 6, borderRadius: 3 }, chTxt: { color: Colors.text.ghost, fontSize: 10, fontWeight: '600' }, logBtn: { paddingHorizontal: 9, paddingVertical: 5, borderRadius: Radius.sm, borderWidth: 1, borderColor: Colors.bg.border, backgroundColor: Colors.bg.raised, marginLeft: 4, }, logBtnOn: { borderColor: Colors.blue.border, backgroundColor: Colors.blue.bg }, logTxt: { color: Colors.text.muted, fontSize: 10, fontWeight: '700', letterSpacing: 0.5 }, logTxtOn: { color: Colors.blue.fg }, chartWrap: { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: Colors.bg.divider }, chartPlaceholder: { backgroundColor: Colors.bg.surface, justifyContent: 'center', alignItems: 'center', }, placeholderTxt: { color: Colors.text.ghost, fontSize: 13 }, });