uni-oaview 1.9.12 → 1.9.14
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.
|
@@ -6,32 +6,39 @@
|
|
|
6
6
|
</view>
|
|
7
7
|
<view class="toolbar-clear" @click="handleClear">清除</view>
|
|
8
8
|
</view>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
|
|
10
|
+
<scroll-view v-if="filteredNativeEventLogMetas.length" scroll-y class="log-list">
|
|
11
|
+
<view v-for="meta in filteredNativeEventLogMetas" :key="meta.id" class="log-item">
|
|
12
|
+
<view class="log-header" @click="toggleExpand(meta.id)">
|
|
13
|
+
<text class="log-title">{{ meta.title }}</text>
|
|
14
|
+
<text class="log-arrow">{{ isExpanded(meta.id) ? '▼' : '▶' }}</text>
|
|
15
|
+
</view>
|
|
16
|
+
|
|
17
|
+
<view v-if="isExpanded(meta.id)" class="log-content">
|
|
18
|
+
<view class="log-actions">
|
|
19
|
+
<text class="log-copy" @click="handleCopy(meta)">复制</text>
|
|
20
|
+
</view>
|
|
21
|
+
<view class="log-section">
|
|
22
|
+
<text class="log-label">发送数据:</text>
|
|
23
|
+
<awesome-display-info :log="getNativeEventDetail(meta.id)?.params" />
|
|
24
|
+
</view>
|
|
25
|
+
<view class="log-section">
|
|
26
|
+
<text class="log-label">接收数据:</text>
|
|
27
|
+
<awesome-display-info :log="getNativeEventDetail(meta.id)?.response" />
|
|
14
28
|
</view>
|
|
15
|
-
<template v-if="isExpanded(meta.id)">
|
|
16
|
-
<view style="word-break: break-all; padding: 0 15px">
|
|
17
|
-
<text :selectable="true" style="font-weight: bolder">发送数据:</text>
|
|
18
|
-
<awesome-display-info :log="getNativeEventDetail(meta.id)?.params" />
|
|
19
|
-
</view>
|
|
20
|
-
<view style="word-break: break-all; padding: 0 15px">
|
|
21
|
-
<text :selectable="true" style="font-weight: bolder">接收数据:</text>
|
|
22
|
-
<awesome-display-info :log="getNativeEventDetail(meta.id)?.response" />
|
|
23
|
-
</view>
|
|
24
|
-
</template>
|
|
25
29
|
</view>
|
|
26
|
-
</
|
|
27
|
-
</
|
|
30
|
+
</view>
|
|
31
|
+
</scroll-view>
|
|
32
|
+
|
|
28
33
|
<view v-else class="empty-state">
|
|
29
34
|
{{ emptyText }}
|
|
30
35
|
</view>
|
|
31
36
|
</template>
|
|
37
|
+
|
|
32
38
|
<script lang="ts" setup>
|
|
33
|
-
import { computed,
|
|
39
|
+
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue';
|
|
34
40
|
import awesomeDisplayInfo from './awesome-display-info.vue';
|
|
41
|
+
import { stringifyForSearch } from './utils';
|
|
35
42
|
import { nativeEventSubject, getNativeEventLogs } from 'uniapp-log-sdk';
|
|
36
43
|
|
|
37
44
|
interface NativeEventLog {
|
|
@@ -48,28 +55,31 @@
|
|
|
48
55
|
title: string;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
const MAX_NATIVE_EVENT_LOGS =
|
|
52
|
-
const
|
|
58
|
+
const MAX_NATIVE_EVENT_LOGS = 200;
|
|
59
|
+
const FLUSH_DELAY_FOREGROUND = 16;
|
|
60
|
+
const FLUSH_DELAY_BACKGROUND = 100;
|
|
61
|
+
const MAX_EXPANDED_ITEMS = 20; // 限制最大展开项数
|
|
53
62
|
|
|
54
|
-
const collapseRef = ref<{ resize?: () => void } | null>(null);
|
|
55
|
-
const activeNames = ref<string[] | string>([]);
|
|
56
63
|
const keyword = ref('');
|
|
57
64
|
const clearTimestamp = ref<number | null>(null);
|
|
58
65
|
const hasCleared = ref(false);
|
|
66
|
+
const isInBackground = ref(false);
|
|
67
|
+
|
|
59
68
|
const nativeEventDetailMap = shallowRef<Map<string, NativeEventLog>>(new Map());
|
|
60
69
|
const nativeEventLogMetas = ref<NativeEventLogMeta[]>([]);
|
|
70
|
+
const expandedIds = ref<Set<string>>(new Set());
|
|
61
71
|
|
|
62
|
-
let pendingLogs: NativeEventLog[] | null = null;
|
|
63
72
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
73
|
+
let pendingLogs: NativeEventLog[] | null = null;
|
|
74
|
+
|
|
75
|
+
// 订阅和监听器取消函数
|
|
76
|
+
let unsubscribeLogs: (() => void) | null = null;
|
|
77
|
+
const cleanupFunctions: (() => void)[] = [];
|
|
64
78
|
|
|
65
79
|
const buildNativeEventLogId = (log: NativeEventLog): string => {
|
|
66
80
|
return `${log.startTime}|${log.endTime}|${log.key}`;
|
|
67
81
|
};
|
|
68
82
|
|
|
69
|
-
const buildNativeEventLogMatchKey = (log: NativeEventLog): string => {
|
|
70
|
-
return `${log.startTime}|${log.key}`;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
83
|
const formatStartTime = (timestamp: number): string => {
|
|
74
84
|
const date = new Date(timestamp);
|
|
75
85
|
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(
|
|
@@ -93,67 +103,43 @@
|
|
|
93
103
|
};
|
|
94
104
|
};
|
|
95
105
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const isExpanded = (id: string): boolean => expandedIds.value.has(id);
|
|
107
|
+
|
|
108
|
+
const toggleExpand = (id: string): void => {
|
|
109
|
+
const newSet = new Set(expandedIds.value);
|
|
110
|
+
if (newSet.has(id)) {
|
|
111
|
+
newSet.delete(id);
|
|
112
|
+
} else {
|
|
113
|
+
newSet.add(id);
|
|
114
|
+
// 限制展开项数量,防止内存泄漏
|
|
115
|
+
if (newSet.size > MAX_EXPANDED_ITEMS) {
|
|
116
|
+
const firstId = newSet.values().next().value;
|
|
117
|
+
newSet.delete(firstId);
|
|
118
|
+
}
|
|
108
119
|
}
|
|
109
|
-
|
|
110
|
-
const migratedIds = activeList
|
|
111
|
-
.map((id) => {
|
|
112
|
-
if (nextMetaIdByMatchKey.has(id)) return id;
|
|
113
|
-
const previousDetail = previousDetailMap.get(id);
|
|
114
|
-
if (!previousDetail) return null;
|
|
115
|
-
return nextMetaIdByMatchKey.get(buildNativeEventLogMatchKey(previousDetail)) ?? null;
|
|
116
|
-
})
|
|
117
|
-
.filter((id): id is string => Boolean(id));
|
|
118
|
-
|
|
119
|
-
if (Array.isArray(activeNames.value)) return migratedIds;
|
|
120
|
-
return migratedIds[0] ?? '';
|
|
120
|
+
expandedIds.value = newSet;
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
const getActiveNameList = (): string[] => {
|
|
124
|
-
const value = activeNames.value;
|
|
125
|
-
return Array.isArray(value) ? value : value ? [value] : [];
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const isExpanded = (id: string): boolean => getActiveNameList().includes(id);
|
|
129
|
-
|
|
130
123
|
const getNativeEventDetail = (id: string): NativeEventLog | null => {
|
|
131
124
|
return nativeEventDetailMap.value.get(id) ?? null;
|
|
132
125
|
};
|
|
133
126
|
|
|
134
127
|
const normalizeKeyword = (value: string): string => value.trim().toLowerCase();
|
|
135
128
|
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
if (typeof value === 'string') return value;
|
|
139
|
-
try {
|
|
140
|
-
return JSON.stringify(value);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
return String(value);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const matchesNativeEventLog = (meta: NativeEventLogMeta): boolean => {
|
|
147
|
-
const normalizedKeyword = normalizeKeyword(keyword.value);
|
|
148
|
-
if (!normalizedKeyword) return true;
|
|
129
|
+
const matchesKeyword = (meta: NativeEventLogMeta, keywordValue: string): boolean => {
|
|
130
|
+
if (!keywordValue) return true;
|
|
149
131
|
const detail = getNativeEventDetail(meta.id);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
132
|
+
const searchText = `${meta.title} ${meta.key} ${stringifyForSearch(detail?.params)} ${stringifyForSearch(
|
|
133
|
+
detail?.response,
|
|
134
|
+
)}`.toLowerCase();
|
|
135
|
+
return searchText.includes(keywordValue);
|
|
154
136
|
};
|
|
155
137
|
|
|
156
|
-
const filteredNativeEventLogMetas = computed(() =>
|
|
138
|
+
const filteredNativeEventLogMetas = computed(() => {
|
|
139
|
+
const normalizedKeyword = normalizeKeyword(keyword.value);
|
|
140
|
+
if (!normalizedKeyword) return nativeEventLogMetas.value;
|
|
141
|
+
return nativeEventLogMetas.value.filter((meta) => matchesKeyword(meta, normalizedKeyword));
|
|
142
|
+
});
|
|
157
143
|
|
|
158
144
|
const emptyText = computed(() => {
|
|
159
145
|
if (keyword.value) return '无匹配结果';
|
|
@@ -161,7 +147,28 @@
|
|
|
161
147
|
return '暂无日志';
|
|
162
148
|
});
|
|
163
149
|
|
|
164
|
-
const
|
|
150
|
+
const handleCopy = (meta: NativeEventLogMeta) => {
|
|
151
|
+
const detail = getNativeEventDetail(meta.id);
|
|
152
|
+
const text = JSON.stringify(
|
|
153
|
+
{
|
|
154
|
+
key: detail?.key ?? meta.key,
|
|
155
|
+
params: detail?.params,
|
|
156
|
+
response: detail?.response,
|
|
157
|
+
startTime: detail?.startTime,
|
|
158
|
+
endTime: detail?.endTime,
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2,
|
|
162
|
+
);
|
|
163
|
+
uni.setClipboardData({
|
|
164
|
+
data: text,
|
|
165
|
+
success: () => {
|
|
166
|
+
uni.showToast({ title: '已复制', icon: 'success' });
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const upsertLogs = (logs: NativeEventLog[]): void => {
|
|
165
172
|
const visibleLogs = clearTimestamp.value ? logs.filter((log) => log.startTime > clearTimestamp.value!) : logs;
|
|
166
173
|
|
|
167
174
|
if (
|
|
@@ -177,43 +184,65 @@
|
|
|
177
184
|
if (!visibleLogs.length && logs.length && clearTimestamp.value) {
|
|
178
185
|
nativeEventDetailMap.value = new Map();
|
|
179
186
|
nativeEventLogMetas.value = [];
|
|
180
|
-
|
|
187
|
+
expandedIds.value = new Set();
|
|
181
188
|
return;
|
|
182
189
|
}
|
|
183
190
|
|
|
184
191
|
const nextLogs = visibleLogs.slice(-MAX_NATIVE_EVENT_LOGS);
|
|
185
|
-
const
|
|
186
|
-
const detailMap = new Map<string, NativeEventLog>();
|
|
187
|
-
const nextMetas: NativeEventLogMeta[] = [];
|
|
192
|
+
const detailMap = nativeEventDetailMap.value;
|
|
188
193
|
|
|
194
|
+
const nextMetas: NativeEventLogMeta[] = [];
|
|
189
195
|
for (const log of nextLogs) {
|
|
190
196
|
const id = buildNativeEventLogId(log);
|
|
191
197
|
detailMap.set(id, log);
|
|
192
198
|
nextMetas.push(buildMetaFromLog(log));
|
|
193
199
|
}
|
|
194
200
|
|
|
195
|
-
|
|
201
|
+
// 创建新的 Map 触发响应式更新
|
|
202
|
+
nativeEventDetailMap.value = new Map(detailMap);
|
|
196
203
|
nativeEventLogMetas.value = nextMetas;
|
|
197
|
-
|
|
204
|
+
|
|
205
|
+
// 清理过期的详情数据
|
|
206
|
+
const keepIds = new Set(nextMetas.map((m) => m.id));
|
|
207
|
+
for (const key of detailMap.keys()) {
|
|
208
|
+
if (!keepIds.has(key)) detailMap.delete(key);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 清理已不存在的展开项
|
|
212
|
+
const newExpandedIds = new Set(expandedIds.value);
|
|
213
|
+
for (const id of newExpandedIds) {
|
|
214
|
+
if (!keepIds.has(id)) {
|
|
215
|
+
newExpandedIds.delete(id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
expandedIds.value = newExpandedIds;
|
|
198
219
|
};
|
|
199
220
|
|
|
200
221
|
const flushPendingLogs = (): void => {
|
|
201
222
|
flushTimer = null;
|
|
202
223
|
if (!pendingLogs) return;
|
|
203
|
-
applyLogs(pendingLogs);
|
|
204
|
-
pendingLogs = null;
|
|
205
|
-
};
|
|
206
224
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
225
|
+
if (isInBackground.value) {
|
|
226
|
+
flushTimer = setTimeout(flushPendingLogs, FLUSH_DELAY_BACKGROUND);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const logs = pendingLogs;
|
|
231
|
+
pendingLogs = null;
|
|
232
|
+
upsertLogs(logs);
|
|
212
233
|
};
|
|
213
234
|
|
|
214
235
|
const enqueueLogs = (logs: NativeEventLog[]): void => {
|
|
215
236
|
pendingLogs = logs;
|
|
216
|
-
|
|
237
|
+
|
|
238
|
+
if (!flushTimer) {
|
|
239
|
+
const delay = isInBackground.value ? FLUSH_DELAY_BACKGROUND : FLUSH_DELAY_FOREGROUND;
|
|
240
|
+
flushTimer = setTimeout(flushPendingLogs, delay);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleClearKeyword = (): void => {
|
|
245
|
+
keyword.value = '';
|
|
217
246
|
};
|
|
218
247
|
|
|
219
248
|
const handleClear = (): void => {
|
|
@@ -221,81 +250,91 @@
|
|
|
221
250
|
hasCleared.value = true;
|
|
222
251
|
nativeEventLogMetas.value = [];
|
|
223
252
|
nativeEventDetailMap.value = new Map();
|
|
224
|
-
|
|
253
|
+
expandedIds.value = new Set();
|
|
254
|
+
keyword.value = '';
|
|
225
255
|
};
|
|
226
256
|
|
|
227
|
-
const
|
|
228
|
-
|
|
257
|
+
const cleanupTimers = (): void => {
|
|
258
|
+
if (flushTimer) {
|
|
259
|
+
clearTimeout(flushTimer);
|
|
260
|
+
flushTimer = null;
|
|
261
|
+
}
|
|
262
|
+
pendingLogs = null;
|
|
229
263
|
};
|
|
230
264
|
|
|
231
|
-
|
|
265
|
+
const setupLifecycleListeners = () => {
|
|
266
|
+
// #ifdef MP
|
|
267
|
+
const handleAppShow = () => {
|
|
268
|
+
if (isInBackground.value) {
|
|
269
|
+
isInBackground.value = false;
|
|
270
|
+
enqueueLogs(getNativeEventLogs());
|
|
271
|
+
}
|
|
272
|
+
};
|
|
232
273
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
274
|
+
const handleAppHide = () => {
|
|
275
|
+
isInBackground.value = true;
|
|
276
|
+
};
|
|
236
277
|
|
|
237
|
-
|
|
238
|
-
(
|
|
239
|
-
|
|
240
|
-
// #ifdef MP
|
|
241
|
-
nextTick(() => collapseRef.value?.resize?.());
|
|
242
|
-
// #endif
|
|
243
|
-
},
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
watch(
|
|
247
|
-
() => nativeEventLogMetas.value,
|
|
248
|
-
() => {
|
|
249
|
-
// #ifdef MP
|
|
250
|
-
if (getActiveNameList().length) nextTick(() => collapseRef.value?.resize?.());
|
|
251
|
-
// #endif
|
|
252
|
-
},
|
|
253
|
-
{ deep: false },
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
watch(keyword, () => {
|
|
257
|
-
const keepIds = new Set(filteredNativeEventLogMetas.value.map((meta) => meta.id));
|
|
258
|
-
const currentActive = activeNames.value;
|
|
259
|
-
if (Array.isArray(currentActive)) {
|
|
260
|
-
activeNames.value = currentActive.filter((id) => keepIds.has(id));
|
|
261
|
-
} else if (currentActive && !keepIds.has(currentActive)) {
|
|
262
|
-
activeNames.value = '';
|
|
278
|
+
// #ifdef MP-WEIXIN
|
|
279
|
+
if (typeof wx !== 'undefined' && wx.onAppShow) {
|
|
280
|
+
wx.onAppShow(handleAppShow);
|
|
263
281
|
}
|
|
282
|
+
if (typeof wx !== 'undefined' && wx.onAppHide) {
|
|
283
|
+
wx.onAppHide(handleAppHide);
|
|
284
|
+
}
|
|
285
|
+
// #endif
|
|
264
286
|
|
|
265
|
-
// #ifdef MP
|
|
266
|
-
if (
|
|
287
|
+
// #ifdef MP-ALIPAY
|
|
288
|
+
if (typeof my !== 'undefined' && my.onAppShow) {
|
|
289
|
+
my.onAppShow(handleAppShow);
|
|
290
|
+
}
|
|
291
|
+
if (typeof my !== 'undefined' && my.onAppHide) {
|
|
292
|
+
my.onAppHide(handleAppHide);
|
|
293
|
+
}
|
|
267
294
|
// #endif
|
|
268
|
-
});
|
|
269
295
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
296
|
+
// uni.onAppShow 返回取消订阅函数
|
|
297
|
+
// #ifdef MP
|
|
298
|
+
try {
|
|
299
|
+
const offAppShow = uni.onAppShow?.(handleAppShow);
|
|
300
|
+
const offAppHide = uni.onAppHide?.(handleAppHide);
|
|
301
|
+
|
|
302
|
+
if (offAppShow) cleanupFunctions.push(offAppShow);
|
|
303
|
+
if (offAppHide) cleanupFunctions.push(offAppHide);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
// 某些平台可能不支持,忽略错误
|
|
274
306
|
}
|
|
275
|
-
|
|
276
|
-
|
|
307
|
+
// #endif
|
|
308
|
+
// #endif
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// 初始化
|
|
312
|
+
enqueueLogs(getNativeEventLogs().filter((log): log is NativeEventLog => Boolean(log)));
|
|
313
|
+
|
|
314
|
+
// 订阅日志更新
|
|
315
|
+
const sub = nativeEventSubject.subscribe((data: NativeEventLog[]) => {
|
|
316
|
+
enqueueLogs(data);
|
|
277
317
|
});
|
|
278
318
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
uni.showToast({ title: '已复制', icon: 'success' });
|
|
296
|
-
},
|
|
319
|
+
unsubscribeLogs = () => sub.unsubscribe();
|
|
320
|
+
|
|
321
|
+
setupLifecycleListeners();
|
|
322
|
+
|
|
323
|
+
onBeforeUnmount(() => {
|
|
324
|
+
// 清理所有订阅和监听
|
|
325
|
+
unsubscribeLogs?.();
|
|
326
|
+
cleanupTimers();
|
|
327
|
+
|
|
328
|
+
// 清理小程序生命周期监听
|
|
329
|
+
cleanupFunctions.forEach((fn) => {
|
|
330
|
+
try {
|
|
331
|
+
fn();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
// 忽略清理错误
|
|
334
|
+
}
|
|
297
335
|
});
|
|
298
|
-
|
|
336
|
+
cleanupFunctions.length = 0;
|
|
337
|
+
});
|
|
299
338
|
</script>
|
|
300
339
|
|
|
301
340
|
<style lang="scss" scoped>
|
|
@@ -351,25 +390,66 @@
|
|
|
351
390
|
border-radius: 4px;
|
|
352
391
|
}
|
|
353
392
|
|
|
354
|
-
.
|
|
355
|
-
|
|
393
|
+
.log-list {
|
|
394
|
+
flex: 1;
|
|
395
|
+
max-height: 70vh;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.log-item {
|
|
399
|
+
border-bottom: 1px solid #e5e5e5;
|
|
400
|
+
background-color: #fff;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.log-header {
|
|
404
|
+
display: flex;
|
|
405
|
+
justify-content: space-between;
|
|
406
|
+
align-items: center;
|
|
407
|
+
padding: 12px 15px;
|
|
408
|
+
background-color: #f9f9f9;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.log-title {
|
|
412
|
+
font-size: 10px;
|
|
413
|
+
color: #333;
|
|
414
|
+
flex: 1;
|
|
415
|
+
word-break: break-all;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.log-arrow {
|
|
356
419
|
font-size: 12px;
|
|
357
420
|
color: #999;
|
|
358
|
-
|
|
421
|
+
margin-left: 8px;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.log-content {
|
|
425
|
+
padding: 10px 15px;
|
|
426
|
+
font-size: 10px;
|
|
427
|
+
background-color: #fff;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.log-actions {
|
|
431
|
+
text-align: right;
|
|
432
|
+
margin-bottom: 8px;
|
|
359
433
|
}
|
|
360
434
|
|
|
361
|
-
.
|
|
435
|
+
.log-copy {
|
|
436
|
+
color: #007aff;
|
|
362
437
|
font-size: 12px;
|
|
363
438
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
439
|
+
|
|
440
|
+
.log-section {
|
|
441
|
+
margin-bottom: 8px;
|
|
442
|
+
word-break: break-all;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.log-label {
|
|
446
|
+
font-weight: bolder;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.empty-state {
|
|
450
|
+
padding: 16px 0;
|
|
451
|
+
font-size: 12px;
|
|
452
|
+
color: #999;
|
|
453
|
+
text-align: center;
|
|
374
454
|
}
|
|
375
455
|
</style>
|
|
@@ -1,12 +1,121 @@
|
|
|
1
1
|
const MAX_DEPTH = 10;
|
|
2
2
|
const MAX_ARRAY_LENGTH = 100;
|
|
3
|
-
const
|
|
3
|
+
const MAX_OBJECT_KEYS = 100;
|
|
4
|
+
const MAX_STRING_LENGTH = 200;
|
|
5
|
+
const MAX_READABLE_PREVIEW_LENGTH = 200;
|
|
6
|
+
const MAX_UNREADABLE_PREVIEW_LENGTH = 50;
|
|
7
|
+
const MAX_SEARCH_DEPTH = 4;
|
|
8
|
+
const MAX_SEARCH_ARRAY_LENGTH = 20;
|
|
9
|
+
const MAX_SEARCH_OBJECT_KEYS = 20;
|
|
4
10
|
|
|
5
11
|
interface RichTextNode {
|
|
6
|
-
type:
|
|
12
|
+
type: 'text';
|
|
7
13
|
text: string;
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
17
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const isLikelyEncodedText = (value: string): boolean => {
|
|
21
|
+
if (value.length < 32 || /\s/.test(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return /^[A-Za-z0-9+/=_-]+$/.test(value);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getControlCharacterCount = (value: string): number => {
|
|
29
|
+
let count = 0;
|
|
30
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
31
|
+
const code = value.charCodeAt(index);
|
|
32
|
+
const isControlChar = (code >= 0 && code <= 8) || (code >= 14 && code <= 31) || code === 127;
|
|
33
|
+
if (isControlChar) {
|
|
34
|
+
count += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return count;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const isHumanReadableString = (value: string): boolean => {
|
|
41
|
+
if (!value) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isLikelyEncodedText(value)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const controlCharacterCount = getControlCharacterCount(value);
|
|
50
|
+
if (controlCharacterCount === 0) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return controlCharacterCount / value.length < 0.05;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const buildStringPreview = (value: string): string => {
|
|
58
|
+
const previewLength = isHumanReadableString(value) ? MAX_READABLE_PREVIEW_LENGTH : MAX_UNREADABLE_PREVIEW_LENGTH;
|
|
59
|
+
|
|
60
|
+
if (value.length <= previewLength) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `${value.slice(0, previewLength)}...`;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const formatString = (value: string): string => {
|
|
68
|
+
const preview = buildStringPreview(value);
|
|
69
|
+
if (preview === value) {
|
|
70
|
+
return `"${value}"`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return `"${preview}" (长度 ${value.length})`;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const buildSearchText = (value: unknown, depth = 0): string => {
|
|
77
|
+
if (value === null || value === undefined) {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof value === 'string') {
|
|
82
|
+
return buildStringPreview(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
86
|
+
return String(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (depth >= MAX_SEARCH_DEPTH) {
|
|
90
|
+
return '[...]';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
const items = value.slice(0, MAX_SEARCH_ARRAY_LENGTH).map((item) => buildSearchText(item, depth + 1));
|
|
95
|
+
const suffix = value.length > MAX_SEARCH_ARRAY_LENGTH ? ` ...(${value.length})` : '';
|
|
96
|
+
return `[${items.join(', ')}${suffix}]`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isPlainObject(value)) {
|
|
100
|
+
const keys = Object.keys(value);
|
|
101
|
+
const items = keys
|
|
102
|
+
.slice(0, MAX_SEARCH_OBJECT_KEYS)
|
|
103
|
+
.map((key) => `${key}:${buildSearchText(value[key], depth + 1)}`);
|
|
104
|
+
const suffix = keys.length > MAX_SEARCH_OBJECT_KEYS ? ` ...(${keys.length})` : '';
|
|
105
|
+
return `{${items.join(', ')}${suffix}}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return String(value);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return '[unserializable]';
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const stringifyForSearch = (value: unknown): string => {
|
|
116
|
+
return buildSearchText(value);
|
|
117
|
+
};
|
|
118
|
+
|
|
10
119
|
export const formatJson = (data: any, indent = 0): RichTextNode[] => {
|
|
11
120
|
const indentStr = ' '.repeat(indent);
|
|
12
121
|
|
|
@@ -62,13 +171,23 @@ const formatArray = (data: any[], indent: number, indentStr: string): RichTextNo
|
|
|
62
171
|
const formatObject = (data: Record<string, any>, indent: number, indentStr: string): RichTextNode[] => {
|
|
63
172
|
let formatted: RichTextNode[] = [{ type: 'text', text: '{\n' }];
|
|
64
173
|
const keys = Object.keys(data);
|
|
65
|
-
keys.
|
|
174
|
+
const displayKeys = keys.slice(0, MAX_OBJECT_KEYS);
|
|
175
|
+
|
|
176
|
+
displayKeys.forEach((key, index) => {
|
|
66
177
|
formatted.push({ type: 'text', text: indentStr + ' '.repeat(4) + key + ': ' });
|
|
67
178
|
formatted.push(...formatJson(data[key], indent + 4));
|
|
68
|
-
if (index <
|
|
179
|
+
if (index < displayKeys.length - 1 || keys.length > MAX_OBJECT_KEYS) {
|
|
69
180
|
formatted.push({ type: 'text', text: ',\n' });
|
|
70
181
|
}
|
|
71
182
|
});
|
|
183
|
+
|
|
184
|
+
if (keys.length > MAX_OBJECT_KEYS) {
|
|
185
|
+
formatted.push({
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: indentStr + ` ... (共 ${keys.length} 个键,显示 ${displayKeys.length} 个)\n`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
72
191
|
formatted.push({ type: 'text', text: '\n' + indentStr + '}' });
|
|
73
192
|
|
|
74
193
|
return formatted;
|
|
@@ -76,14 +195,8 @@ const formatObject = (data: Record<string, any>, indent: number, indentStr: stri
|
|
|
76
195
|
|
|
77
196
|
const formatPrimitive = (data: any): RichTextNode[] => {
|
|
78
197
|
if (typeof data === 'string') {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return [
|
|
82
|
-
{
|
|
83
|
-
type: 'text',
|
|
84
|
-
text: `"${data.slice(0, MAX_STRING_LENGTH)}..." (长度 ${data.length})`,
|
|
85
|
-
},
|
|
86
|
-
];
|
|
198
|
+
if (data.length > MAX_STRING_LENGTH || !isHumanReadableString(data)) {
|
|
199
|
+
return [{ type: 'text', text: formatString(data) }];
|
|
87
200
|
}
|
|
88
201
|
return [{ type: 'text', text: `"${data}"` }];
|
|
89
202
|
} else {
|