react-native-vconsole 0.0.1 → 0.2.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.
Files changed (49) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +39 -9
  3. package/Vconsole.podspec +25 -0
  4. package/android/build.gradle +61 -129
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +1 -2
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/java/com/vconsole/VconsoleModule.kt +80 -0
  9. package/android/src/main/java/com/vconsole/VconsolePackage.kt +17 -0
  10. package/ios/Vconsole.h +6 -0
  11. package/ios/Vconsole.mm +64 -0
  12. package/lib/module/VConsole.js +769 -0
  13. package/lib/module/VConsole.js.map +1 -0
  14. package/lib/module/core/consoleProxy.js +86 -0
  15. package/lib/module/core/consoleProxy.js.map +1 -0
  16. package/lib/module/core/xhrProxy.js +247 -0
  17. package/lib/module/core/xhrProxy.js.map +1 -0
  18. package/lib/module/index.js +24 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/package.json +1 -0
  21. package/lib/module/types.js +2 -0
  22. package/lib/module/types.js.map +1 -0
  23. package/lib/typescript/package.json +1 -0
  24. package/lib/typescript/src/VConsole.d.ts +6 -0
  25. package/lib/typescript/src/VConsole.d.ts.map +1 -0
  26. package/lib/typescript/src/core/consoleProxy.d.ts +9 -0
  27. package/lib/typescript/src/core/consoleProxy.d.ts.map +1 -0
  28. package/lib/typescript/src/core/xhrProxy.d.ts +12 -0
  29. package/lib/typescript/src/core/xhrProxy.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +9 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -0
  32. package/lib/typescript/src/types.d.ts +37 -0
  33. package/lib/typescript/src/types.d.ts.map +1 -0
  34. package/package.json +138 -25
  35. package/src/VConsole.tsx +871 -0
  36. package/src/core/consoleProxy.ts +108 -0
  37. package/src/core/xhrProxy.ts +319 -0
  38. package/src/index.tsx +36 -0
  39. package/src/types.ts +42 -0
  40. package/android/README.md +0 -14
  41. package/android/src/main/java/wiki/qdc/rn/vconsole/ReactNativeVconsoleModule.java +0 -27
  42. package/android/src/main/java/wiki/qdc/rn/vconsole/ReactNativeVconsolePackage.java +0 -23
  43. package/index.js +0 -5
  44. package/ios/.DS_Store +0 -0
  45. package/ios/ReactNativeVconsole.h +0 -5
  46. package/ios/ReactNativeVconsole.m +0 -13
  47. package/ios/ReactNativeVconsole.xcodeproj/project.pbxproj +0 -281
  48. package/ios/ReactNativeVconsole.xcworkspace/contents.xcworkspacedata +0 -7
  49. package/react-native-vconsole.podspec +0 -28
@@ -0,0 +1,871 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ Clipboard,
5
+ Dimensions,
6
+ FlatList,
7
+ NativeModules,
8
+ PanResponder,
9
+ Platform,
10
+ Pressable,
11
+ StatusBar,
12
+ StyleSheet,
13
+ Text,
14
+ View,
15
+ type FlatListProps,
16
+ } from 'react-native';
17
+ import {
18
+ clearLogEntries,
19
+ getLogEntries,
20
+ installConsoleProxy,
21
+ subscribeLogEntries,
22
+ uninstallConsoleProxy,
23
+ } from './core/consoleProxy';
24
+ import {
25
+ clearNetworkEntries,
26
+ getNetworkEntries,
27
+ installXhrProxy,
28
+ subscribeNetworkEntries,
29
+ uninstallXhrProxy,
30
+ } from './core/xhrProxy';
31
+ import type {
32
+ AppInfo,
33
+ LogEntry,
34
+ LogFilterTab,
35
+ NetworkEntry,
36
+ SystemInfo,
37
+ VConsoleTab,
38
+ } from './types';
39
+
40
+ const BUTTON_WIDTH = 88;
41
+ const BUTTON_HEIGHT = 36;
42
+ const PANEL_HEIGHT_RATIO = 7 / 9;
43
+ const EMPTY_FILTER: string[] = [];
44
+ const LOG_SUB_TABS: LogFilterTab[] = ['All', 'log', 'info', 'warn', 'error'];
45
+ const ROOT_TABS: VConsoleTab[] = ['Log', 'Network', 'System', 'App'];
46
+
47
+ const LOG_THEME = {
48
+ log: { backgroundColor: '#FFFFFF', color: '#111111' },
49
+ info: { backgroundColor: '#FFFFFF', color: '#246BFD' },
50
+ warn: { backgroundColor: '#FFF8E6', color: '#A65A00' },
51
+ error: { backgroundColor: '#FFECEC', color: '#9C1C1C' },
52
+ } as const;
53
+
54
+ type ExpandedMap = Record<string, boolean>;
55
+
56
+ type NativeModuleShape = {
57
+ getSystemInfo?: () => Promise<SystemInfo>;
58
+ getAppInfo?: () => Promise<AppInfo>;
59
+ };
60
+
61
+ export type VConsoleProps = {
62
+ enable?: boolean;
63
+ filter?: string[];
64
+ };
65
+
66
+ function clamp(value: number, min: number, max: number): number {
67
+ return Math.min(Math.max(value, min), max);
68
+ }
69
+
70
+ function getDisplayValue(value: unknown): string {
71
+ if (typeof value === 'string') {
72
+ return value;
73
+ }
74
+ try {
75
+ return JSON.stringify(value);
76
+ } catch {
77
+ return String(value);
78
+ }
79
+ }
80
+
81
+ function copyToClipboard(value: string) {
82
+ Clipboard.setString(value);
83
+ }
84
+
85
+ function prettyText(value: unknown): string {
86
+ if (value === undefined) {
87
+ return '';
88
+ }
89
+ if (typeof value === 'string') {
90
+ return value;
91
+ }
92
+ try {
93
+ return JSON.stringify(value, null, 2);
94
+ } catch {
95
+ return String(value);
96
+ }
97
+ }
98
+
99
+ function buildNetworkCopyText(item: NetworkEntry): string {
100
+ const status = item.status ?? '-';
101
+ const duration =
102
+ typeof item.durationMs === 'number' ? `${item.durationMs}ms` : '-';
103
+
104
+ return [
105
+ `${item.method} ${item.url}`,
106
+ `status ${status} duration ${duration}`,
107
+ `request headers\n${prettyText(item.requestHeaders)}`,
108
+ `request body\n${prettyText(item.requestBody)}`,
109
+ `response headers\n${prettyText(item.responseHeaders)}`,
110
+ `response data\n${prettyText(item.responseData)}`,
111
+ ].join('\n');
112
+ }
113
+
114
+ type ObjectTreeProps = {
115
+ value: unknown;
116
+ nodeKey: string;
117
+ expandedMap: ExpandedMap;
118
+ onToggle: (key: string) => void;
119
+ };
120
+
121
+ function ObjectTree({
122
+ value,
123
+ nodeKey,
124
+ expandedMap,
125
+ onToggle,
126
+ }: ObjectTreeProps) {
127
+ if (value === null || value === undefined) {
128
+ return <Text style={styles.valuePrimitive}>{String(value)}</Text>;
129
+ }
130
+
131
+ const valueType = typeof value;
132
+ if (valueType !== 'object') {
133
+ return <Text style={styles.valuePrimitive}>{getDisplayValue(value)}</Text>;
134
+ }
135
+
136
+ const isArray = Array.isArray(value);
137
+ const entries = Object.entries(value as Record<string, unknown>);
138
+ const opened = !!expandedMap[nodeKey];
139
+
140
+ return (
141
+ <View style={styles.treeNode}>
142
+ <Pressable onPress={() => onToggle(nodeKey)} style={styles.treeHeader}>
143
+ <Text style={styles.arrow}>{opened ? '▼' : '▶'}</Text>
144
+ <Text style={styles.treeLabel}>
145
+ {isArray ? `Array(${entries.length})` : `Object(${entries.length})`}
146
+ </Text>
147
+ </Pressable>
148
+ {opened ? (
149
+ <View style={styles.treeChildren}>
150
+ {entries.map(([key, item]) => (
151
+ <View key={`${nodeKey}.${key}`} style={styles.treeChildRow}>
152
+ <Text style={styles.treeKey}>{key}: </Text>
153
+ <ObjectTree
154
+ value={item}
155
+ nodeKey={`${nodeKey}.${key}`}
156
+ expandedMap={expandedMap}
157
+ onToggle={onToggle}
158
+ />
159
+ </View>
160
+ ))}
161
+ </View>
162
+ ) : null}
163
+ </View>
164
+ );
165
+ }
166
+
167
+ function ListSeparator() {
168
+ return <View style={styles.separator} />;
169
+ }
170
+
171
+ function useFlatListRefs() {
172
+ const allRef = useRef<FlatList<LogEntry>>(null);
173
+ const logRef = useRef<FlatList<LogEntry>>(null);
174
+ const infoRef = useRef<FlatList<LogEntry>>(null);
175
+ const warnRef = useRef<FlatList<LogEntry>>(null);
176
+ const errorRef = useRef<FlatList<LogEntry>>(null);
177
+
178
+ return useMemo(
179
+ () => ({
180
+ All: allRef,
181
+ log: logRef,
182
+ info: infoRef,
183
+ warn: warnRef,
184
+ error: errorRef,
185
+ }),
186
+ [allRef, errorRef, infoRef, logRef, warnRef]
187
+ );
188
+ }
189
+
190
+ export function VConsole({
191
+ enable = true,
192
+ filter = EMPTY_FILTER,
193
+ }: VConsoleProps) {
194
+ const nativeModule = NativeModules.Vconsole as NativeModuleShape | undefined;
195
+ const { width, height } = Dimensions.get('window');
196
+
197
+ const topInset = Platform.select({
198
+ ios: 44,
199
+ android: (StatusBar.currentHeight ?? 0) + 8,
200
+ default: 24,
201
+ });
202
+ const bottomInset = Platform.select({
203
+ ios: 34,
204
+ android: 56,
205
+ default: 24,
206
+ });
207
+
208
+ const minX = 0;
209
+ const maxX = width - BUTTON_WIDTH;
210
+ const minY = topInset;
211
+ const maxY = height - bottomInset - BUTTON_HEIGHT;
212
+
213
+ const initialY = clamp(height - bottomInset - BUTTON_HEIGHT - 12, minY, maxY);
214
+
215
+ const dragPosition = useRef(
216
+ new Animated.ValueXY({ x: 12, y: initialY })
217
+ ).current;
218
+ const dragStartPoint = useRef({ x: 12, y: initialY });
219
+
220
+ const [panelVisible, setPanelVisible] = useState(false);
221
+ const [activeTab, setActiveTab] = useState<VConsoleTab>('Log');
222
+ const [logSubTab, setLogSubTab] = useState<LogFilterTab>('All');
223
+ const [logEntries, setLogEntries] = useState<LogEntry[]>([]);
224
+ const [networkEntries, setNetworkEntries] = useState<NetworkEntry[]>([]);
225
+ const [expandedMap, setExpandedMap] = useState<ExpandedMap>({});
226
+ const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
227
+ const [appInfo, setAppInfo] = useState<AppInfo | null>(null);
228
+
229
+ const panelHeight = Math.floor(height * PANEL_HEIGHT_RATIO);
230
+ const panelTranslateY = useRef(new Animated.Value(panelHeight)).current;
231
+ const logListRefs = useFlatListRefs();
232
+ const networkListRef = useRef<FlatList<NetworkEntry>>(null);
233
+ const normalizedFilter = useMemo(
234
+ () => filter.map((item) => item.trim().toLowerCase()).filter(Boolean),
235
+ [filter]
236
+ );
237
+
238
+ useEffect(() => {
239
+ if (!enable) {
240
+ setPanelVisible(false);
241
+ return;
242
+ }
243
+
244
+ installConsoleProxy();
245
+ installXhrProxy({ filterHosts: normalizedFilter });
246
+
247
+ const unsubscribeLog = subscribeLogEntries(setLogEntries);
248
+ const unsubscribeNetwork = subscribeNetworkEntries(setNetworkEntries);
249
+ setLogEntries(getLogEntries());
250
+ setNetworkEntries(getNetworkEntries());
251
+
252
+ return () => {
253
+ unsubscribeLog();
254
+ unsubscribeNetwork();
255
+ uninstallConsoleProxy();
256
+ uninstallXhrProxy();
257
+ };
258
+ }, [enable, normalizedFilter]);
259
+
260
+ useEffect(() => {
261
+ dragPosition.stopAnimation((value) => {
262
+ const nextX = clamp(value.x, minX, maxX);
263
+ const nextY = clamp(value.y, minY, maxY);
264
+ dragPosition.setValue({ x: nextX, y: nextY });
265
+ dragStartPoint.current = { x: nextX, y: nextY };
266
+ });
267
+ }, [dragPosition, maxX, maxY, minX, minY]);
268
+
269
+ useEffect(() => {
270
+ if (panelVisible && activeTab === 'System' && !systemInfo) {
271
+ nativeModule
272
+ ?.getSystemInfo?.()
273
+ .then((result) => setSystemInfo(result))
274
+ .catch(() => undefined);
275
+ }
276
+ if (panelVisible && activeTab === 'App' && !appInfo) {
277
+ nativeModule
278
+ ?.getAppInfo?.()
279
+ .then((result) => setAppInfo(result))
280
+ .catch(() => undefined);
281
+ }
282
+ }, [activeTab, appInfo, nativeModule, panelVisible, systemInfo]);
283
+
284
+ const panResponder = useMemo(
285
+ () =>
286
+ PanResponder.create({
287
+ onMoveShouldSetPanResponder: () => true,
288
+ onPanResponderGrant: () => {
289
+ dragPosition.stopAnimation((value) => {
290
+ dragStartPoint.current = { x: value.x, y: value.y };
291
+ });
292
+ },
293
+ onPanResponderMove: (_, gestureState) => {
294
+ const nextX = clamp(
295
+ dragStartPoint.current.x + gestureState.dx,
296
+ minX,
297
+ maxX
298
+ );
299
+ const nextY = clamp(
300
+ dragStartPoint.current.y + gestureState.dy,
301
+ minY,
302
+ maxY
303
+ );
304
+ dragPosition.setValue({ x: nextX, y: nextY });
305
+ },
306
+ onPanResponderRelease: () => {
307
+ dragPosition.stopAnimation((value) => {
308
+ dragStartPoint.current = { x: value.x, y: value.y };
309
+ });
310
+ },
311
+ }),
312
+ [dragPosition, maxX, maxY, minX, minY]
313
+ );
314
+
315
+ const openPanel = () => {
316
+ setPanelVisible(true);
317
+ panelTranslateY.setValue(panelHeight);
318
+ Animated.timing(panelTranslateY, {
319
+ toValue: 0,
320
+ duration: 220,
321
+ useNativeDriver: true,
322
+ }).start();
323
+ };
324
+
325
+ const closePanel = () => {
326
+ Animated.timing(panelTranslateY, {
327
+ toValue: panelHeight,
328
+ duration: 220,
329
+ useNativeDriver: true,
330
+ }).start(({ finished }) => {
331
+ if (finished) {
332
+ setPanelVisible(false);
333
+ }
334
+ });
335
+ };
336
+
337
+ const logDataByTab = useMemo(
338
+ () => ({
339
+ All: logEntries,
340
+ log: logEntries.filter((item) => item.level === 'log'),
341
+ info: logEntries.filter((item) => item.level === 'info'),
342
+ warn: logEntries.filter((item) => item.level === 'warn'),
343
+ error: logEntries.filter((item) => item.level === 'error'),
344
+ }),
345
+ [logEntries]
346
+ );
347
+
348
+ const onToggleNode = (key: string) => {
349
+ setExpandedMap((prev) => ({
350
+ ...prev,
351
+ [key]: !prev[key],
352
+ }));
353
+ };
354
+
355
+ const scrollLogTop = () => {
356
+ logListRefs[logSubTab].current?.scrollToOffset({
357
+ offset: 0,
358
+ animated: true,
359
+ });
360
+ };
361
+
362
+ const scrollLogBottom = () => {
363
+ logListRefs[logSubTab].current?.scrollToEnd({ animated: true });
364
+ };
365
+
366
+ const scrollNetworkTop = () => {
367
+ networkListRef.current?.scrollToOffset({ offset: 0, animated: true });
368
+ };
369
+
370
+ const scrollNetworkBottom = () => {
371
+ networkListRef.current?.scrollToEnd({ animated: true });
372
+ };
373
+
374
+ const renderRootTab = (tab: VConsoleTab) => (
375
+ <Pressable
376
+ key={tab}
377
+ style={[
378
+ styles.topTabButton,
379
+ activeTab === tab && styles.topTabButtonActive,
380
+ ]}
381
+ onPress={() => setActiveTab(tab)}
382
+ >
383
+ <Text
384
+ style={[
385
+ styles.topTabText,
386
+ activeTab === tab && styles.topTabTextActive,
387
+ ]}
388
+ >
389
+ {tab}
390
+ </Text>
391
+ </Pressable>
392
+ );
393
+
394
+ const renderActionButton = (label: string, onPress: () => void) => (
395
+ <Pressable key={label} style={styles.actionButton} onPress={onPress}>
396
+ <Text style={styles.actionButtonText}>{label}</Text>
397
+ </Pressable>
398
+ );
399
+
400
+ const renderLogItem: FlatListProps<LogEntry>['renderItem'] = ({ item }) => {
401
+ const levelTheme = LOG_THEME[item.level];
402
+ return (
403
+ <View
404
+ style={[
405
+ styles.listItem,
406
+ { backgroundColor: levelTheme.backgroundColor },
407
+ ]}
408
+ >
409
+ <View style={styles.listItemMain}>
410
+ <Text style={[styles.logLevelText, { color: levelTheme.color }]}>
411
+ [{item.level.toUpperCase()}]
412
+ </Text>
413
+ <View style={styles.logPayload}>
414
+ {item.args.map((arg, index) => (
415
+ <ObjectTree
416
+ key={`${item.id}.arg.${index}`}
417
+ value={arg}
418
+ nodeKey={`${item.id}.arg.${index}`}
419
+ expandedMap={expandedMap}
420
+ onToggle={onToggleNode}
421
+ />
422
+ ))}
423
+ </View>
424
+ </View>
425
+ <Pressable
426
+ style={styles.copyButton}
427
+ onPress={() => copyToClipboard(item.text)}
428
+ >
429
+ <Text style={styles.copyButtonText}>复制</Text>
430
+ </Pressable>
431
+ </View>
432
+ );
433
+ };
434
+
435
+ const renderNetworkItem: FlatListProps<NetworkEntry>['renderItem'] = ({
436
+ item,
437
+ }) => {
438
+ return (
439
+ <View style={styles.listItem}>
440
+ <View style={styles.listItemMain}>
441
+ <Text style={styles.networkTitle}>
442
+ {item.method} {item.url}
443
+ </Text>
444
+ <Text style={styles.networkLabel}>
445
+ status {item.status ?? '-'} duration{' '}
446
+ {typeof item.durationMs === 'number' ? `${item.durationMs}ms` : '-'}
447
+ </Text>
448
+ <View style={styles.networkBlock}>
449
+ <Text style={styles.networkLabel}>request headers</Text>
450
+ <ObjectTree
451
+ value={item.requestHeaders}
452
+ nodeKey={`${item.id}.requestHeaders`}
453
+ expandedMap={expandedMap}
454
+ onToggle={onToggleNode}
455
+ />
456
+ </View>
457
+ <View style={styles.networkBlock}>
458
+ <Text style={styles.networkLabel}>request body</Text>
459
+ <ObjectTree
460
+ value={item.requestBody ?? ''}
461
+ nodeKey={`${item.id}.requestBody`}
462
+ expandedMap={expandedMap}
463
+ onToggle={onToggleNode}
464
+ />
465
+ </View>
466
+ <View style={styles.networkBlock}>
467
+ <Text style={styles.networkLabel}>response headers</Text>
468
+ <ObjectTree
469
+ value={item.responseHeaders}
470
+ nodeKey={`${item.id}.responseHeaders`}
471
+ expandedMap={expandedMap}
472
+ onToggle={onToggleNode}
473
+ />
474
+ </View>
475
+ <View style={styles.networkBlock}>
476
+ <Text style={styles.networkLabel}>response data</Text>
477
+ <ObjectTree
478
+ value={item.responseData ?? ''}
479
+ nodeKey={`${item.id}.responseData`}
480
+ expandedMap={expandedMap}
481
+ onToggle={onToggleNode}
482
+ />
483
+ </View>
484
+ </View>
485
+ <Pressable
486
+ style={styles.copyButton}
487
+ onPress={() => copyToClipboard(buildNetworkCopyText(item))}
488
+ >
489
+ <Text style={styles.copyButtonText}>复制</Text>
490
+ </Pressable>
491
+ </View>
492
+ );
493
+ };
494
+
495
+ const renderLogPanel = () => (
496
+ <View style={styles.contentArea}>
497
+ <View style={styles.subTabRow}>
498
+ {LOG_SUB_TABS.map((tab) => (
499
+ <Pressable
500
+ key={tab}
501
+ style={[
502
+ styles.subTabButton,
503
+ logSubTab === tab && styles.subTabButtonActive,
504
+ ]}
505
+ onPress={() => setLogSubTab(tab)}
506
+ >
507
+ <Text
508
+ style={[
509
+ styles.subTabText,
510
+ logSubTab === tab && styles.subTabTextActive,
511
+ ]}
512
+ >
513
+ {tab}
514
+ </Text>
515
+ </Pressable>
516
+ ))}
517
+ </View>
518
+ <View style={styles.logListsWrap}>
519
+ {LOG_SUB_TABS.map((tab) => (
520
+ <View
521
+ key={tab}
522
+ style={[styles.listHost, logSubTab !== tab && styles.hidden]}
523
+ >
524
+ <FlatList
525
+ ref={logListRefs[tab]}
526
+ data={logDataByTab[tab]}
527
+ keyExtractor={(item) => `${tab}-${item.id}`}
528
+ renderItem={renderLogItem}
529
+ ItemSeparatorComponent={ListSeparator}
530
+ />
531
+ </View>
532
+ ))}
533
+ </View>
534
+ <View style={styles.actionsRow}>
535
+ {renderActionButton('Clear', () => {
536
+ clearLogEntries();
537
+ setExpandedMap({});
538
+ })}
539
+ {renderActionButton('Top', scrollLogTop)}
540
+ {renderActionButton('Bottom', scrollLogBottom)}
541
+ {renderActionButton('Hide', closePanel)}
542
+ </View>
543
+ </View>
544
+ );
545
+
546
+ const renderNetworkPanel = () => (
547
+ <View style={styles.contentArea}>
548
+ <FlatList
549
+ ref={networkListRef}
550
+ data={networkEntries}
551
+ keyExtractor={(item) => `network-${item.id}`}
552
+ renderItem={renderNetworkItem}
553
+ ItemSeparatorComponent={ListSeparator}
554
+ />
555
+ <View style={styles.actionsRow}>
556
+ {renderActionButton('Clear', () => {
557
+ clearNetworkEntries();
558
+ setExpandedMap({});
559
+ })}
560
+ {renderActionButton('Top', scrollNetworkTop)}
561
+ {renderActionButton('Bottom', scrollNetworkBottom)}
562
+ {renderActionButton('Hide', closePanel)}
563
+ </View>
564
+ </View>
565
+ );
566
+
567
+ const renderSystemPanel = () => (
568
+ <View style={styles.contentArea}>
569
+ <View style={styles.infoCard}>
570
+ <Text style={styles.infoText}>
571
+ 厂商/品牌: {systemInfo?.manufacturer ?? '-'}
572
+ </Text>
573
+ <Text style={styles.infoText}>机型: {systemInfo?.model ?? '-'}</Text>
574
+ <Text style={styles.infoText}>
575
+ 系统版本: {systemInfo?.osVersion ?? '-'}
576
+ </Text>
577
+ {Platform.OS === 'android' ? (
578
+ <Text style={styles.infoText}>
579
+ 网络类型: {systemInfo?.networkType ?? '-'}
580
+ </Text>
581
+ ) : null}
582
+ {Platform.OS === 'android' ? (
583
+ <Text style={styles.infoText}>
584
+ 网络可达: {systemInfo?.isNetworkReachable ? 'true' : 'false'}
585
+ </Text>
586
+ ) : null}
587
+ <Text style={styles.infoText}>
588
+ 总内存: {systemInfo?.totalMemory ?? 0}
589
+ </Text>
590
+ {Platform.OS === 'android' ? (
591
+ <Text style={styles.infoText}>
592
+ 可用内存: {systemInfo?.availableMemory ?? 0}
593
+ </Text>
594
+ ) : null}
595
+ </View>
596
+ <View style={styles.actionsRow}>
597
+ {renderActionButton('Hide', closePanel)}
598
+ </View>
599
+ </View>
600
+ );
601
+
602
+ const renderAppPanel = () => (
603
+ <View style={styles.contentArea}>
604
+ <View style={styles.infoCard}>
605
+ <Text style={styles.infoText}>
606
+ App Version: {appInfo?.appVersion ?? '-'}
607
+ </Text>
608
+ <Text style={styles.infoText}>
609
+ Build Number: {appInfo?.buildNumber ?? '-'}
610
+ </Text>
611
+ </View>
612
+ <View style={styles.actionsRow}>
613
+ {renderActionButton('Hide', closePanel)}
614
+ </View>
615
+ </View>
616
+ );
617
+
618
+ if (!enable) {
619
+ return null;
620
+ }
621
+
622
+ return (
623
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
624
+ {!panelVisible ? (
625
+ <Animated.View
626
+ style={[
627
+ styles.floatingButtonWrap,
628
+ { transform: dragPosition.getTranslateTransform() },
629
+ ]}
630
+ {...panResponder.panHandlers}
631
+ >
632
+ <Pressable style={styles.floatingButton} onPress={openPanel}>
633
+ <Text style={styles.floatingButtonText}>vConsole</Text>
634
+ </Pressable>
635
+ </Animated.View>
636
+ ) : null}
637
+
638
+ {panelVisible ? (
639
+ <View style={styles.overlayWrap}>
640
+ <Pressable style={styles.mask} onPress={closePanel} />
641
+ <Animated.View
642
+ style={[
643
+ styles.panel,
644
+ {
645
+ height: panelHeight,
646
+ transform: [{ translateY: panelTranslateY }],
647
+ },
648
+ ]}
649
+ >
650
+ <View style={styles.topTabRow}>{ROOT_TABS.map(renderRootTab)}</View>
651
+ {activeTab === 'Log' ? renderLogPanel() : null}
652
+ {activeTab === 'Network' ? renderNetworkPanel() : null}
653
+ {activeTab === 'System' ? renderSystemPanel() : null}
654
+ {activeTab === 'App' ? renderAppPanel() : null}
655
+ </Animated.View>
656
+ </View>
657
+ ) : null}
658
+ </View>
659
+ );
660
+ }
661
+
662
+ const styles = StyleSheet.create({
663
+ floatingButtonWrap: {
664
+ position: 'absolute',
665
+ zIndex: 9999,
666
+ },
667
+ floatingButton: {
668
+ width: BUTTON_WIDTH,
669
+ height: BUTTON_HEIGHT,
670
+ borderRadius: 12,
671
+ backgroundColor: '#22A455',
672
+ justifyContent: 'center',
673
+ alignItems: 'center',
674
+ },
675
+ floatingButtonText: {
676
+ color: '#FFFFFF',
677
+ fontSize: 12,
678
+ fontWeight: '600',
679
+ },
680
+ overlayWrap: {
681
+ ...StyleSheet.absoluteFillObject,
682
+ justifyContent: 'flex-end',
683
+ },
684
+ mask: {
685
+ ...StyleSheet.absoluteFillObject,
686
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
687
+ },
688
+ panel: {
689
+ width: '100%',
690
+ backgroundColor: '#FFFFFF',
691
+ borderTopLeftRadius: 14,
692
+ borderTopRightRadius: 14,
693
+ overflow: 'hidden',
694
+ },
695
+ topTabRow: {
696
+ flexDirection: 'row',
697
+ borderBottomWidth: StyleSheet.hairlineWidth,
698
+ borderBottomColor: '#D9D9D9',
699
+ paddingHorizontal: 8,
700
+ paddingVertical: 8,
701
+ },
702
+ topTabButton: {
703
+ paddingHorizontal: 12,
704
+ paddingVertical: 8,
705
+ borderRadius: 8,
706
+ },
707
+ topTabButtonActive: {
708
+ backgroundColor: '#EEF5FF',
709
+ },
710
+ topTabText: {
711
+ color: '#444444',
712
+ fontSize: 13,
713
+ fontWeight: '500',
714
+ },
715
+ topTabTextActive: {
716
+ color: '#246BFD',
717
+ },
718
+ contentArea: {
719
+ flex: 1,
720
+ paddingBottom: 16,
721
+ },
722
+ subTabRow: {
723
+ flexDirection: 'row',
724
+ paddingHorizontal: 8,
725
+ paddingVertical: 8,
726
+ borderBottomWidth: StyleSheet.hairlineWidth,
727
+ borderBottomColor: '#E0E0E0',
728
+ },
729
+ subTabButton: {
730
+ marginRight: 8,
731
+ paddingHorizontal: 10,
732
+ paddingVertical: 6,
733
+ borderRadius: 8,
734
+ },
735
+ subTabButtonActive: {
736
+ backgroundColor: '#EEF5FF',
737
+ },
738
+ subTabText: {
739
+ color: '#666666',
740
+ fontSize: 12,
741
+ },
742
+ subTabTextActive: {
743
+ color: '#246BFD',
744
+ },
745
+ logListsWrap: {
746
+ flex: 1,
747
+ },
748
+ listHost: {
749
+ flex: 1,
750
+ },
751
+ hidden: {
752
+ display: 'none',
753
+ },
754
+ listItem: {
755
+ paddingHorizontal: 10,
756
+ paddingVertical: 10,
757
+ flexDirection: 'row',
758
+ justifyContent: 'space-between',
759
+ alignItems: 'flex-start',
760
+ },
761
+ listItemMain: {
762
+ flex: 1,
763
+ marginRight: 8,
764
+ },
765
+ separator: {
766
+ height: StyleSheet.hairlineWidth,
767
+ backgroundColor: '#DFDFDF',
768
+ },
769
+ logLevelText: {
770
+ fontSize: 11,
771
+ fontWeight: '700',
772
+ marginBottom: 4,
773
+ },
774
+ logPayload: {
775
+ flex: 1,
776
+ },
777
+ copyButton: {
778
+ borderWidth: StyleSheet.hairlineWidth,
779
+ borderColor: '#D0D0D0',
780
+ borderRadius: 6,
781
+ paddingVertical: 4,
782
+ paddingHorizontal: 8,
783
+ },
784
+ copyButtonText: {
785
+ fontSize: 11,
786
+ color: '#333333',
787
+ },
788
+ valuePrimitive: {
789
+ color: '#222222',
790
+ fontSize: 12,
791
+ flexShrink: 1,
792
+ },
793
+ treeNode: {
794
+ flexDirection: 'column',
795
+ marginBottom: 4,
796
+ },
797
+ treeHeader: {
798
+ flexDirection: 'row',
799
+ alignItems: 'center',
800
+ },
801
+ arrow: {
802
+ color: '#666666',
803
+ fontSize: 11,
804
+ marginRight: 4,
805
+ },
806
+ treeLabel: {
807
+ color: '#444444',
808
+ fontSize: 12,
809
+ fontWeight: '500',
810
+ },
811
+ treeChildren: {
812
+ marginLeft: 14,
813
+ marginTop: 4,
814
+ },
815
+ treeChildRow: {
816
+ flexDirection: 'row',
817
+ alignItems: 'flex-start',
818
+ marginBottom: 2,
819
+ },
820
+ treeKey: {
821
+ color: '#666666',
822
+ fontSize: 12,
823
+ },
824
+ networkTitle: {
825
+ fontSize: 12,
826
+ color: '#111111',
827
+ fontWeight: '600',
828
+ marginBottom: 6,
829
+ },
830
+ networkBlock: {
831
+ marginTop: 2,
832
+ marginBottom: 6,
833
+ },
834
+ networkLabel: {
835
+ fontSize: 12,
836
+ color: '#444444',
837
+ marginBottom: 2,
838
+ },
839
+ actionsRow: {
840
+ borderTopWidth: StyleSheet.hairlineWidth,
841
+ borderTopColor: '#E1E1E1',
842
+ paddingHorizontal: 8,
843
+ paddingVertical: 8,
844
+ flexDirection: 'row',
845
+ justifyContent: 'space-around',
846
+ },
847
+ actionButton: {
848
+ minWidth: 62,
849
+ borderRadius: 8,
850
+ borderWidth: StyleSheet.hairlineWidth,
851
+ borderColor: '#D0D0D0',
852
+ paddingHorizontal: 10,
853
+ paddingVertical: 7,
854
+ alignItems: 'center',
855
+ },
856
+ actionButtonText: {
857
+ color: '#333333',
858
+ fontSize: 12,
859
+ fontWeight: '500',
860
+ },
861
+ infoCard: {
862
+ flex: 1,
863
+ paddingHorizontal: 12,
864
+ paddingVertical: 12,
865
+ },
866
+ infoText: {
867
+ fontSize: 13,
868
+ color: '#222222',
869
+ marginBottom: 8,
870
+ },
871
+ });