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