react-native-vconsole 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/VConsole.tsx CHANGED
@@ -4,13 +4,16 @@ import {
4
4
  Clipboard,
5
5
  Dimensions,
6
6
  FlatList,
7
+ Keyboard,
7
8
  NativeModules,
8
9
  PanResponder,
9
10
  Platform,
10
11
  Pressable,
12
+ TextInput,
11
13
  StatusBar,
12
14
  StyleSheet,
13
15
  Text,
16
+ ToastAndroid,
14
17
  View,
15
18
  ScrollView,
16
19
  type FlatListProps,
@@ -41,7 +44,7 @@ import type {
41
44
  const BUTTON_WIDTH = 88;
42
45
  const BUTTON_HEIGHT = 36;
43
46
  const PANEL_HEIGHT_RATIO = 7 / 9;
44
- const EMPTY_FILTER: string[] = [];
47
+ const EMPTY_EXCLUDE: VConsoleExclude = {};
45
48
  const LOG_SUB_TABS: LogFilterTab[] = ['All', 'log', 'info', 'warn', 'error'];
46
49
  const ROOT_TABS: VConsoleTab[] = ['Log', 'Network', 'System', 'App'];
47
50
 
@@ -61,7 +64,12 @@ type NativeModuleShape = {
61
64
 
62
65
  export type VConsoleProps = {
63
66
  enable?: boolean;
64
- filter?: string[];
67
+ exclude?: VConsoleExclude;
68
+ };
69
+
70
+ type VConsoleExclude = {
71
+ domains?: string[];
72
+ ip?: boolean;
65
73
  };
66
74
 
67
75
  function clamp(value: number, min: number, max: number): number {
@@ -83,6 +91,36 @@ function copyToClipboard(value: string) {
83
91
  Clipboard.setString(value);
84
92
  }
85
93
 
94
+ function copyToClipboardWithFeedback(value: string) {
95
+ copyToClipboard(value);
96
+ if (Platform.OS === 'android') {
97
+ ToastAndroid.show('Copied', ToastAndroid.SHORT);
98
+ }
99
+ }
100
+
101
+ function formatMemorySize(bytes: unknown): string {
102
+ if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes < 0) {
103
+ return '-';
104
+ }
105
+ const mb = bytes / (1024 * 1024);
106
+ if (mb >= 1024) {
107
+ return `${(mb / 1024).toFixed(2)} GB`;
108
+ }
109
+ return `${mb.toFixed(2)} MB`;
110
+ }
111
+
112
+ function formatLogTime(timestamp: number): string {
113
+ const date = new Date(timestamp);
114
+ if (Number.isNaN(date.getTime())) {
115
+ return '--:--:--.---';
116
+ }
117
+ const hh = String(date.getHours()).padStart(2, '0');
118
+ const mm = String(date.getMinutes()).padStart(2, '0');
119
+ const ss = String(date.getSeconds()).padStart(2, '0');
120
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
121
+ return `${hh}:${mm}:${ss}.${ms}`;
122
+ }
123
+
86
124
  function prettyText(value: unknown): string {
87
125
  if (value === undefined) {
88
126
  return '';
@@ -97,19 +135,103 @@ function prettyText(value: unknown): string {
97
135
  }
98
136
  }
99
137
 
138
+ function isNetworkErrorEntry(item: NetworkEntry): boolean {
139
+ return item.isError === true;
140
+ }
141
+
100
142
  function buildNetworkCopyText(item: NetworkEntry): string {
101
143
  const status = item.status ?? '-';
102
144
  const duration =
103
145
  typeof item.durationMs === 'number' ? `${item.durationMs}ms` : '-';
146
+ const isError = isNetworkErrorEntry(item);
104
147
 
105
- return [
148
+ const segments = [
106
149
  `${item.method} ${item.url}`,
107
150
  `status ${status} duration ${duration}`,
108
151
  `request headers\n${prettyText(item.requestHeaders)}`,
109
152
  `request body\n${prettyText(item.requestBody)}`,
110
- `response headers\n${prettyText(item.responseHeaders)}`,
111
- `response data\n${prettyText(item.responseData)}`,
112
- ].join('\n');
153
+ ];
154
+
155
+ if (isError) {
156
+ segments.push(
157
+ `error reason\n${item.errorReason ?? 'Network request failed'}`
158
+ );
159
+ } else {
160
+ segments.push(`response headers\n${prettyText(item.responseHeaders)}`);
161
+ segments.push(`response data\n${prettyText(item.responseData)}`);
162
+ }
163
+
164
+ return segments.join('\n');
165
+ }
166
+
167
+ const FORBIDDEN_RETRY_HEADERS = new Set([
168
+ 'host',
169
+ 'content-length',
170
+ 'accept-encoding',
171
+ 'connection',
172
+ 'origin',
173
+ 'referer',
174
+ ]);
175
+
176
+ function normalizeRetryUrl(rawUrl: string): string {
177
+ if (!rawUrl) {
178
+ return '';
179
+ }
180
+ if (/^\/\//.test(rawUrl)) {
181
+ return `https:${rawUrl}`;
182
+ }
183
+ return rawUrl;
184
+ }
185
+
186
+ function buildRetryHeaders(
187
+ headers: Record<string, string> | undefined
188
+ ): Record<string, string> {
189
+ const nextHeaders: Record<string, string> = {};
190
+ if (!headers) {
191
+ return nextHeaders;
192
+ }
193
+
194
+ Object.entries(headers).forEach(([key, value]) => {
195
+ if (!FORBIDDEN_RETRY_HEADERS.has(key.toLowerCase())) {
196
+ nextHeaders[key] = value;
197
+ }
198
+ });
199
+ return nextHeaders;
200
+ }
201
+
202
+ function buildRetryBody(payload: unknown, method: string): unknown | undefined {
203
+ if (method === 'GET' || method === 'HEAD' || payload == null) {
204
+ return undefined;
205
+ }
206
+ if (typeof payload === 'string') {
207
+ return payload;
208
+ }
209
+ if (typeof payload === 'number' || typeof payload === 'boolean') {
210
+ return String(payload);
211
+ }
212
+ if (typeof FormData !== 'undefined' && payload instanceof FormData) {
213
+ return payload;
214
+ }
215
+ if (
216
+ typeof URLSearchParams !== 'undefined' &&
217
+ payload instanceof URLSearchParams
218
+ ) {
219
+ return payload;
220
+ }
221
+ if (typeof Blob !== 'undefined' && payload instanceof Blob) {
222
+ return payload;
223
+ }
224
+ if (typeof ArrayBuffer !== 'undefined' && payload instanceof ArrayBuffer) {
225
+ return payload;
226
+ }
227
+ if (ArrayBuffer.isView(payload)) {
228
+ return payload;
229
+ }
230
+ try {
231
+ return JSON.stringify(payload);
232
+ } catch {
233
+ return String(payload);
234
+ }
113
235
  }
114
236
 
115
237
  type ObjectTreeProps = {
@@ -131,9 +253,21 @@ function ObjectTree({
131
253
 
132
254
  const valueType = typeof value;
133
255
  if (valueType !== 'object') {
256
+ const displayValue = getDisplayValue(value);
257
+ if (Platform.OS === 'android') {
258
+ return (
259
+ <Pressable
260
+ onLongPress={() => copyToClipboardWithFeedback(displayValue)}
261
+ delayLongPress={250}
262
+ android_ripple={{ color: '#D0D0D0' }}
263
+ >
264
+ <Text style={styles.valuePrimitive}>{displayValue}</Text>
265
+ </Pressable>
266
+ );
267
+ }
134
268
  return (
135
269
  <Text style={styles.valuePrimitive} selectable={true}>
136
- {getDisplayValue(value)}
270
+ {displayValue}
137
271
  </Text>
138
272
  );
139
273
  }
@@ -194,7 +328,7 @@ function useFlatListRefs() {
194
328
 
195
329
  export function VConsole({
196
330
  enable = true,
197
- filter = EMPTY_FILTER,
331
+ exclude = EMPTY_EXCLUDE,
198
332
  }: VConsoleProps) {
199
333
  const nativeModule = NativeModules.Vconsole as NativeModuleShape | undefined;
200
334
  const { width, height } = Dimensions.get('window');
@@ -227,6 +361,11 @@ export function VConsole({
227
361
  const [logSubTab, setLogSubTab] = useState<LogFilterTab>('All');
228
362
  const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
229
363
  const [networkEntries, setNetworkEntries] = useState<NetworkEntry[]>([]);
364
+ const [logFilterInput, setLogFilterInput] = useState('');
365
+ const [networkFilterInput, setNetworkFilterInput] = useState('');
366
+ const [debouncedLogFilter, setDebouncedLogFilter] = useState('');
367
+ const [debouncedNetworkFilter, setDebouncedNetworkFilter] = useState('');
368
+ const [keyboardHeight, setKeyboardHeight] = useState(0);
230
369
  const [expandedMap, setExpandedMap] = useState<ExpandedMap>({});
231
370
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
232
371
  const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
@@ -235,10 +374,14 @@ export function VConsole({
235
374
  const panelTranslateY = useRef(new Animated.Value(panelHeight)).current;
236
375
  const logListRefs = useFlatListRefs();
237
376
  const networkListRef = useRef<FlatList<NetworkEntry>>(null);
238
- const normalizedFilter = useMemo(
239
- () => filter.map((item) => item.trim().toLowerCase()).filter(Boolean),
240
- [filter]
377
+ const normalizedExcludeDomains = useMemo(
378
+ () =>
379
+ (exclude.domains ?? [])
380
+ .map((item) => item.trim().toLowerCase())
381
+ .filter(Boolean),
382
+ [exclude.domains]
241
383
  );
384
+ const shouldExcludeIp = exclude.ip === true;
242
385
 
243
386
  useEffect(() => {
244
387
  if (!enable) {
@@ -247,7 +390,10 @@ export function VConsole({
247
390
  }
248
391
 
249
392
  installConsoleProxy();
250
- installXhrProxy({ filterHosts: normalizedFilter });
393
+ installXhrProxy({
394
+ excludeHosts: normalizedExcludeDomains,
395
+ excludeIp: shouldExcludeIp,
396
+ });
251
397
 
252
398
  const unsubscribeLog = subscribeLogEntries(setLogEntries);
253
399
  const unsubscribeNetwork = subscribeNetworkEntries(setNetworkEntries);
@@ -260,7 +406,7 @@ export function VConsole({
260
406
  uninstallConsoleProxy();
261
407
  uninstallXhrProxy();
262
408
  };
263
- }, [enable, normalizedFilter]);
409
+ }, [enable, normalizedExcludeDomains, shouldExcludeIp]);
264
410
 
265
411
  useEffect(() => {
266
412
  dragPosition.stopAnimation((value) => {
@@ -286,6 +432,39 @@ export function VConsole({
286
432
  }
287
433
  }, [activeTab, appInfo, nativeModule, panelVisible, systemInfo]);
288
434
 
435
+ useEffect(() => {
436
+ const showEvent =
437
+ Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
438
+ const hideEvent =
439
+ Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
440
+
441
+ const showSubscription = Keyboard.addListener(showEvent, (event) => {
442
+ setKeyboardHeight(event.endCoordinates?.height ?? 0);
443
+ });
444
+ const hideSubscription = Keyboard.addListener(hideEvent, () => {
445
+ setKeyboardHeight(0);
446
+ });
447
+
448
+ return () => {
449
+ showSubscription.remove();
450
+ hideSubscription.remove();
451
+ };
452
+ }, []);
453
+
454
+ useEffect(() => {
455
+ const timer = setTimeout(() => {
456
+ setDebouncedLogFilter(logFilterInput);
457
+ }, 1000);
458
+ return () => clearTimeout(timer);
459
+ }, [logFilterInput]);
460
+
461
+ useEffect(() => {
462
+ const timer = setTimeout(() => {
463
+ setDebouncedNetworkFilter(networkFilterInput);
464
+ }, 1000);
465
+ return () => clearTimeout(timer);
466
+ }, [networkFilterInput]);
467
+
289
468
  const panResponder = useMemo(
290
469
  () =>
291
470
  PanResponder.create({
@@ -339,15 +518,36 @@ export function VConsole({
339
518
  });
340
519
  };
341
520
 
521
+ const normalizedLogFilter = debouncedLogFilter.trim().toLowerCase();
522
+ const normalizedNetworkFilter = debouncedNetworkFilter.trim().toLowerCase();
523
+
524
+ const filteredLogEntries = useMemo(() => {
525
+ if (!normalizedLogFilter) {
526
+ return logEntries;
527
+ }
528
+ return logEntries.filter((item) =>
529
+ item.text.toLowerCase().includes(normalizedLogFilter)
530
+ );
531
+ }, [logEntries, normalizedLogFilter]);
532
+
533
+ const filteredNetworkEntries = useMemo(() => {
534
+ if (!normalizedNetworkFilter) {
535
+ return networkEntries;
536
+ }
537
+ return networkEntries.filter((item) =>
538
+ item.url.toLowerCase().includes(normalizedNetworkFilter)
539
+ );
540
+ }, [networkEntries, normalizedNetworkFilter]);
541
+
342
542
  const logDataByTab = useMemo(
343
543
  () => ({
344
- All: logEntries,
345
- log: logEntries.filter((item) => item.level === 'log'),
346
- info: logEntries.filter((item) => item.level === 'info'),
347
- warn: logEntries.filter((item) => item.level === 'warn'),
348
- error: logEntries.filter((item) => item.level === 'error'),
544
+ All: filteredLogEntries,
545
+ log: filteredLogEntries.filter((item) => item.level === 'log'),
546
+ info: filteredLogEntries.filter((item) => item.level === 'info'),
547
+ warn: filteredLogEntries.filter((item) => item.level === 'warn'),
548
+ error: filteredLogEntries.filter((item) => item.level === 'error'),
349
549
  }),
350
- [logEntries]
550
+ [filteredLogEntries]
351
551
  );
352
552
 
353
553
  const onToggleNode = (key: string) => {
@@ -376,6 +576,39 @@ export function VConsole({
376
576
  networkListRef.current?.scrollToEnd({ animated: true });
377
577
  };
378
578
 
579
+ const retryNetworkRequest = (item: NetworkEntry) => {
580
+ const method = (item.method || 'GET').toUpperCase();
581
+ const url = normalizeRetryUrl(item.url);
582
+ if (!url) {
583
+ console.error('[vConsole] Retry failed: empty request URL');
584
+ return;
585
+ }
586
+
587
+ const headers = buildRetryHeaders(item.requestHeaders);
588
+ const body = buildRetryBody(item.requestBody, method);
589
+ const hasContentType = Object.keys(headers).some(
590
+ (key) => key.toLowerCase() === 'content-type'
591
+ );
592
+
593
+ if (
594
+ body &&
595
+ typeof body === 'string' &&
596
+ typeof item.requestBody === 'object' &&
597
+ item.requestBody !== null &&
598
+ !hasContentType
599
+ ) {
600
+ headers['Content-Type'] = 'application/json';
601
+ }
602
+
603
+ fetch(url, {
604
+ method,
605
+ headers,
606
+ body: body as never,
607
+ }).catch((error: unknown) => {
608
+ console.error('[vConsole] Retry request failed', error);
609
+ });
610
+ };
611
+
379
612
  const renderRootTab = (tab: VConsoleTab) => (
380
613
  <Pressable
381
614
  key={tab}
@@ -414,6 +647,10 @@ export function VConsole({
414
647
  <View style={styles.listItemMain}>
415
648
  <Text style={[styles.logLevelText, { color: levelTheme.color }]}>
416
649
  [{item.level.toUpperCase()}]
650
+ <Text style={styles.logTimeText}>
651
+ {' '}
652
+ {formatLogTime(item.timestamp)}
653
+ </Text>
417
654
  </Text>
418
655
  <View style={styles.logPayload}>
419
656
  {item.args.map((arg, index) => (
@@ -429,9 +666,9 @@ export function VConsole({
429
666
  </View>
430
667
  <Pressable
431
668
  style={styles.copyButton}
432
- onPress={() => copyToClipboard(item.text)}
669
+ onPress={() => copyToClipboardWithFeedback(item.text)}
433
670
  >
434
- <Text style={styles.copyButtonText}>复制</Text>
671
+ <Text style={styles.copyButtonText}>Copy</Text>
435
672
  </Pressable>
436
673
  </View>
437
674
  );
@@ -440,18 +677,31 @@ export function VConsole({
440
677
  const renderNetworkItem: FlatListProps<NetworkEntry>['renderItem'] = ({
441
678
  item,
442
679
  }) => {
680
+ const isError = isNetworkErrorEntry(item);
681
+ const startedTime = formatLogTime(item.startedAt);
682
+ const finishedTime =
683
+ typeof item.finishedAt === 'number'
684
+ ? formatLogTime(item.finishedAt)
685
+ : '-';
443
686
  return (
444
- <View style={styles.listItem}>
687
+ <View
688
+ style={[
689
+ styles.listItem,
690
+ isError ? { backgroundColor: LOG_THEME.error.backgroundColor } : null,
691
+ ]}
692
+ >
445
693
  <View style={styles.listItemMain}>
446
694
  <Text style={styles.networkTitle}>
447
695
  {item.method} {item.url}
448
696
  </Text>
449
697
  <Text style={styles.networkLabel}>
450
- Status: {item.status ?? '-'}
698
+ Time: {startedTime}
699
+ {finishedTime !== '-' ? ` ~ ${finishedTime}` : ''}
451
700
  {' '}
452
701
  Duration:{' '}
453
702
  {typeof item.durationMs === 'number' ? `${item.durationMs}ms` : '-'}
454
703
  </Text>
704
+ <Text style={styles.networkLabel}>Status: {item.status ?? '-'}</Text>
455
705
  <View style={styles.networkBlock}>
456
706
  <Text style={styles.networkLabel}>Request Headers</Text>
457
707
  <ObjectTree
@@ -470,39 +720,60 @@ export function VConsole({
470
720
  onToggle={onToggleNode}
471
721
  />
472
722
  </View>
473
- <View style={styles.networkBlock}>
474
- <Text style={styles.networkLabel}>Response Headers</Text>
475
- <ObjectTree
476
- value={item.responseHeaders}
477
- nodeKey={`${item.id}.responseHeaders`}
478
- expandedMap={expandedMap}
479
- onToggle={onToggleNode}
480
- />
481
- </View>
482
- <View style={styles.networkBlock}>
483
- <Text style={styles.networkLabel}>Response Data</Text>
484
- <ScrollView horizontal={true}>
485
- <ObjectTree
486
- value={item.responseData ?? ''}
487
- nodeKey={`${item.id}.responseData`}
488
- expandedMap={expandedMap}
489
- onToggle={onToggleNode}
490
- />
491
- </ScrollView>
492
- </View>
723
+ {isError ? (
724
+ <View style={styles.networkBlock}>
725
+ <Text style={[styles.networkLabel, styles.networkErrorLabel]}>
726
+ Error Reason
727
+ </Text>
728
+ <Text style={styles.networkErrorText}>
729
+ {item.errorReason ?? 'Network request failed'}
730
+ </Text>
731
+ </View>
732
+ ) : (
733
+ <>
734
+ <View style={styles.networkBlock}>
735
+ <Text style={styles.networkLabel}>Response Headers</Text>
736
+ <ObjectTree
737
+ value={item.responseHeaders}
738
+ nodeKey={`${item.id}.responseHeaders`}
739
+ expandedMap={expandedMap}
740
+ onToggle={onToggleNode}
741
+ />
742
+ </View>
743
+ <View style={styles.networkBlock}>
744
+ <Text style={styles.networkLabel}>Response Data</Text>
745
+ <ScrollView horizontal={true}>
746
+ <ObjectTree
747
+ value={item.responseData ?? ''}
748
+ nodeKey={`${item.id}.responseData`}
749
+ expandedMap={expandedMap}
750
+ onToggle={onToggleNode}
751
+ />
752
+ </ScrollView>
753
+ </View>
754
+ </>
755
+ )}
493
756
  </View>
494
757
  <Pressable
495
758
  style={styles.copyButton}
496
- onPress={() => copyToClipboard(buildNetworkCopyText(item))}
759
+ onPress={() =>
760
+ copyToClipboardWithFeedback(buildNetworkCopyText(item))
761
+ }
762
+ >
763
+ <Text style={styles.copyButtonText}>Copy</Text>
764
+ </Pressable>
765
+ <Pressable
766
+ style={styles.retryButton}
767
+ onPress={() => retryNetworkRequest(item)}
497
768
  >
498
- <Text style={styles.copyButtonText}>复制</Text>
769
+ <Text style={styles.retryButtonText}>Retry</Text>
499
770
  </Pressable>
500
771
  </View>
501
772
  );
502
773
  };
503
774
 
504
- const renderLogPanel = () => (
505
- <View style={styles.contentArea}>
775
+ const renderLogPanel = (visible: boolean) => (
776
+ <View style={[styles.contentArea, visible ? {} : styles.hidden]}>
506
777
  <View style={styles.subTabRow}>
507
778
  {LOG_SUB_TABS.map((tab) => (
508
779
  <Pressable
@@ -540,6 +811,16 @@ export function VConsole({
540
811
  </View>
541
812
  ))}
542
813
  </View>
814
+ <View style={styles.filterInputWrap}>
815
+ <TextInput
816
+ style={styles.filterInput}
817
+ textAlignVertical="center"
818
+ value={logFilterInput}
819
+ onChangeText={setLogFilterInput}
820
+ placeholder="filter..."
821
+ placeholderTextColor="#999999"
822
+ />
823
+ </View>
543
824
  <View style={styles.actionsRow}>
544
825
  {renderActionButton('Clear', () => {
545
826
  clearLogEntries();
@@ -552,15 +833,24 @@ export function VConsole({
552
833
  </View>
553
834
  );
554
835
 
555
- const renderNetworkPanel = () => (
556
- <View style={styles.contentArea}>
836
+ const renderNetworkPanel = (visible: boolean) => (
837
+ <View style={[styles.contentArea, visible ? {} : styles.hidden]}>
557
838
  <FlatList
558
839
  ref={networkListRef}
559
- data={networkEntries}
840
+ data={filteredNetworkEntries}
560
841
  keyExtractor={(item) => `network-${item.id}`}
561
842
  renderItem={renderNetworkItem}
562
843
  ItemSeparatorComponent={ListSeparator}
563
844
  />
845
+ <View style={styles.filterInputWrap}>
846
+ <TextInput
847
+ style={styles.filterInput}
848
+ value={networkFilterInput}
849
+ onChangeText={setNetworkFilterInput}
850
+ placeholder="filter"
851
+ placeholderTextColor="#999999"
852
+ />
853
+ </View>
564
854
  <View style={styles.actionsRow}>
565
855
  {renderActionButton('Clear', () => {
566
856
  clearNetworkEntries();
@@ -573,32 +863,33 @@ export function VConsole({
573
863
  </View>
574
864
  );
575
865
 
576
- const renderSystemPanel = () => (
577
- <View style={styles.contentArea}>
866
+ const renderSystemPanel = (visible: boolean) => (
867
+ <View style={[styles.contentArea, visible ? {} : styles.hidden]}>
578
868
  <View style={styles.infoCard}>
579
869
  <Text style={styles.infoText}>
580
- 厂商/品牌: {systemInfo?.manufacturer ?? '-'}
870
+ Brand: {systemInfo?.manufacturer ?? '-'}
581
871
  </Text>
582
- <Text style={styles.infoText}>机型: {systemInfo?.model ?? '-'}</Text>
872
+ <Text style={styles.infoText}>Model: {systemInfo?.model ?? '-'}</Text>
583
873
  <Text style={styles.infoText}>
584
- 系统版本: {systemInfo?.osVersion ?? '-'}
874
+ System Version: {Platform.OS === 'android' ? 'Android' : 'iOS'}{' '}
875
+ {systemInfo?.osVersion ?? '-'}
585
876
  </Text>
586
877
  {Platform.OS === 'android' ? (
587
878
  <Text style={styles.infoText}>
588
- 网络类型: {systemInfo?.networkType ?? '-'}
879
+ Network Type: {systemInfo?.networkType ?? '-'}
589
880
  </Text>
590
881
  ) : null}
591
882
  {Platform.OS === 'android' ? (
592
883
  <Text style={styles.infoText}>
593
- 网络可达: {systemInfo?.isNetworkReachable ? 'true' : 'false'}
884
+ Network Reachable: {systemInfo?.isNetworkReachable ?? 'unknown'}
594
885
  </Text>
595
886
  ) : null}
596
887
  <Text style={styles.infoText}>
597
- 总内存: {systemInfo?.totalMemory ?? 0}
888
+ Total Memory: {formatMemorySize(systemInfo?.totalMemory)}
598
889
  </Text>
599
890
  {Platform.OS === 'android' ? (
600
891
  <Text style={styles.infoText}>
601
- 可用内存: {systemInfo?.availableMemory ?? 0}
892
+ Available Memory: {formatMemorySize(systemInfo?.availableMemory)}
602
893
  </Text>
603
894
  ) : null}
604
895
  </View>
@@ -608,8 +899,8 @@ export function VConsole({
608
899
  </View>
609
900
  );
610
901
 
611
- const renderAppPanel = () => (
612
- <View style={styles.contentArea}>
902
+ const renderAppPanel = (visible: boolean) => (
903
+ <View style={[styles.contentArea, visible ? {} : styles.hidden]}>
613
904
  <View style={styles.infoCard}>
614
905
  <Text style={styles.infoText}>
615
906
  App Version: {appInfo?.appVersion ?? '-'}
@@ -652,15 +943,16 @@ export function VConsole({
652
943
  styles.panel,
653
944
  {
654
945
  height: panelHeight,
946
+ marginBottom: keyboardHeight,
655
947
  transform: [{ translateY: panelTranslateY }],
656
948
  },
657
949
  ]}
658
950
  >
659
951
  <View style={styles.topTabRow}>{ROOT_TABS.map(renderRootTab)}</View>
660
- {activeTab === 'Log' ? renderLogPanel() : null}
661
- {activeTab === 'Network' ? renderNetworkPanel() : null}
662
- {activeTab === 'System' ? renderSystemPanel() : null}
663
- {activeTab === 'App' ? renderAppPanel() : null}
952
+ {renderLogPanel(activeTab === 'Log')}
953
+ {renderNetworkPanel(activeTab === 'Network')}
954
+ {renderSystemPanel(activeTab === 'System')}
955
+ {renderAppPanel(activeTab === 'App')}
664
956
  </Animated.View>
665
957
  </View>
666
958
  ) : null}
@@ -676,7 +968,7 @@ const styles = StyleSheet.create({
676
968
  floatingButton: {
677
969
  width: BUTTON_WIDTH,
678
970
  height: BUTTON_HEIGHT,
679
- borderRadius: 12,
971
+ borderRadius: 8,
680
972
  backgroundColor: '#22A455',
681
973
  justifyContent: 'center',
682
974
  alignItems: 'center',
@@ -726,7 +1018,7 @@ const styles = StyleSheet.create({
726
1018
  },
727
1019
  contentArea: {
728
1020
  flex: 1,
729
- paddingBottom: 16,
1021
+ paddingBottom: Platform.OS === 'android' ? 42 : 16,
730
1022
  },
731
1023
  subTabRow: {
732
1024
  flexDirection: 'row',
@@ -779,6 +1071,11 @@ const styles = StyleSheet.create({
779
1071
  fontWeight: '700',
780
1072
  marginBottom: 4,
781
1073
  },
1074
+ logTimeText: {
1075
+ fontSize: 11,
1076
+ fontWeight: '400',
1077
+ color: '#888888',
1078
+ },
782
1079
  logPayload: {
783
1080
  flex: 1,
784
1081
  },
@@ -796,11 +1093,31 @@ const styles = StyleSheet.create({
796
1093
  fontSize: 11,
797
1094
  color: '#333333',
798
1095
  },
1096
+ retryButton: {
1097
+ position: 'absolute',
1098
+ right: 8,
1099
+ top: 40,
1100
+ borderWidth: StyleSheet.hairlineWidth,
1101
+ borderColor: '#D0D0D0',
1102
+ borderRadius: 6,
1103
+ paddingVertical: 4,
1104
+ paddingHorizontal: 8,
1105
+ },
1106
+ retryButtonText: {
1107
+ fontSize: 11,
1108
+ color: '#333333',
1109
+ },
799
1110
  valuePrimitive: {
800
1111
  color: '#222222',
801
1112
  fontSize: 12,
802
1113
  flexShrink: 1,
803
1114
  },
1115
+ valuePrimitiveInput: {
1116
+ paddingVertical: 0,
1117
+ paddingHorizontal: 0,
1118
+ margin: 0,
1119
+ textAlignVertical: 'top',
1120
+ },
804
1121
  treeNode: {
805
1122
  flexDirection: 'column',
806
1123
  marginBottom: 4,
@@ -852,6 +1169,32 @@ const styles = StyleSheet.create({
852
1169
  color: '#444444',
853
1170
  marginBottom: 2,
854
1171
  },
1172
+ networkErrorLabel: {
1173
+ color: LOG_THEME.error.color,
1174
+ fontWeight: '600',
1175
+ },
1176
+ networkErrorText: {
1177
+ color: LOG_THEME.error.color,
1178
+ fontSize: 12,
1179
+ },
1180
+ filterInputWrap: {
1181
+ borderTopWidth: StyleSheet.hairlineWidth,
1182
+ borderTopColor: '#E1E1E1',
1183
+ paddingHorizontal: 8,
1184
+ paddingTop: 8,
1185
+ paddingBottom: 6,
1186
+ },
1187
+ filterInput: {
1188
+ height: 34,
1189
+ borderWidth: StyleSheet.hairlineWidth,
1190
+ borderColor: '#D0D0D0',
1191
+ borderRadius: 8,
1192
+ paddingHorizontal: 10,
1193
+ fontSize: 12,
1194
+ color: '#222222',
1195
+ backgroundColor: '#FFFFFF',
1196
+ paddingVertical: 0,
1197
+ },
855
1198
  actionsRow: {
856
1199
  borderTopWidth: StyleSheet.hairlineWidth,
857
1200
  borderTopColor: '#E1E1E1',