uni-oaview 1.9.12 → 1.9.13

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,31 +6,37 @@
6
6
  </view>
7
7
  <view class="toolbar-clear" @click="handleClear">清除</view>
8
8
  </view>
9
- <uni-collapse v-if="filteredNativeEventLogMetas.length" ref="collapseRef" v-model="activeNames">
10
- <uni-collapse-item v-for="meta in filteredNativeEventLogMetas" :key="meta.id" :name="meta.id" :title="meta.title">
11
- <view style="font-size: 10px">
12
- <view style="text-align: right; margin-bottom: 8px; padding: 0 15px">
13
- <text style="color: #007aff; font-size: 12px" @click="handleCopy(meta)">复制</text>
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
- </uni-collapse-item>
27
- </uni-collapse>
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, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
39
+ import { computed, onBeforeUnmount, ref, shallowRef } from 'vue';
34
40
  import awesomeDisplayInfo from './awesome-display-info.vue';
35
41
  import { nativeEventSubject, getNativeEventLogs } from 'uniapp-log-sdk';
36
42
 
@@ -48,28 +54,31 @@
48
54
  title: string;
49
55
  }
50
56
 
51
- const MAX_NATIVE_EVENT_LOGS = 50;
52
- const FLUSH_DELAY = 16;
57
+ const MAX_NATIVE_EVENT_LOGS = 200;
58
+ const FLUSH_DELAY_FOREGROUND = 16;
59
+ const FLUSH_DELAY_BACKGROUND = 100;
60
+ const MAX_EXPANDED_ITEMS = 20; // 限制最大展开项数
53
61
 
54
- const collapseRef = ref<{ resize?: () => void } | null>(null);
55
- const activeNames = ref<string[] | string>([]);
56
62
  const keyword = ref('');
57
63
  const clearTimestamp = ref<number | null>(null);
58
64
  const hasCleared = ref(false);
65
+ const isInBackground = ref(false);
66
+
59
67
  const nativeEventDetailMap = shallowRef<Map<string, NativeEventLog>>(new Map());
60
68
  const nativeEventLogMetas = ref<NativeEventLogMeta[]>([]);
69
+ const expandedIds = ref<Set<string>>(new Set());
61
70
 
62
- let pendingLogs: NativeEventLog[] | null = null;
63
71
  let flushTimer: ReturnType<typeof setTimeout> | null = null;
72
+ let pendingLogs: NativeEventLog[] | null = null;
73
+
74
+ // 订阅和监听器取消函数
75
+ let unsubscribeLogs: (() => void) | null = null;
76
+ const cleanupFunctions: (() => void)[] = [];
64
77
 
65
78
  const buildNativeEventLogId = (log: NativeEventLog): string => {
66
79
  return `${log.startTime}|${log.endTime}|${log.key}`;
67
80
  };
68
81
 
69
- const buildNativeEventLogMatchKey = (log: NativeEventLog): string => {
70
- return `${log.startTime}|${log.key}`;
71
- };
72
-
73
82
  const formatStartTime = (timestamp: number): string => {
74
83
  const date = new Date(timestamp);
75
84
  return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(
@@ -93,40 +102,23 @@
93
102
  };
94
103
  };
95
104
 
96
- const migrateActiveNames = (
97
- previousDetailMap: Map<string, NativeEventLog>,
98
- nextMetas: NativeEventLogMeta[],
99
- ): string[] | string => {
100
- const activeList = getActiveNameList();
101
- if (!activeList.length) return activeNames.value;
102
-
103
- const nextMetaIdByMatchKey = new Map<string, string>();
104
- for (const meta of nextMetas) {
105
- const detail = nativeEventDetailMap.value.get(meta.id);
106
- if (!detail) continue;
107
- nextMetaIdByMatchKey.set(buildNativeEventLogMatchKey(detail), meta.id);
105
+ const isExpanded = (id: string): boolean => expandedIds.value.has(id);
106
+
107
+ const toggleExpand = (id: string): void => {
108
+ const newSet = new Set(expandedIds.value);
109
+ if (newSet.has(id)) {
110
+ newSet.delete(id);
111
+ } else {
112
+ newSet.add(id);
113
+ // 限制展开项数量,防止内存泄漏
114
+ if (newSet.size > MAX_EXPANDED_ITEMS) {
115
+ const firstId = newSet.values().next().value;
116
+ newSet.delete(firstId);
117
+ }
108
118
  }
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] ?? '';
119
+ expandedIds.value = newSet;
121
120
  };
122
121
 
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
122
  const getNativeEventDetail = (id: string): NativeEventLog | null => {
131
123
  return nativeEventDetailMap.value.get(id) ?? null;
132
124
  };
@@ -143,17 +135,20 @@
143
135
  }
144
136
  };
145
137
 
146
- const matchesNativeEventLog = (meta: NativeEventLogMeta): boolean => {
147
- const normalizedKeyword = normalizeKeyword(keyword.value);
148
- if (!normalizedKeyword) return true;
138
+ const matchesKeyword = (meta: NativeEventLogMeta, keywordValue: string): boolean => {
139
+ if (!keywordValue) return true;
149
140
  const detail = getNativeEventDetail(meta.id);
150
- return [meta.title, meta.key, safeStringify(detail?.params), safeStringify(detail?.response)]
151
- .join(' ')
152
- .toLowerCase()
153
- .includes(normalizedKeyword);
141
+ const searchText = `${meta.title} ${meta.key} ${safeStringify(detail?.params)} ${safeStringify(
142
+ detail?.response,
143
+ )}`.toLowerCase();
144
+ return searchText.includes(keywordValue);
154
145
  };
155
146
 
156
- const filteredNativeEventLogMetas = computed(() => nativeEventLogMetas.value.filter(matchesNativeEventLog));
147
+ const filteredNativeEventLogMetas = computed(() => {
148
+ const normalizedKeyword = normalizeKeyword(keyword.value);
149
+ if (!normalizedKeyword) return nativeEventLogMetas.value;
150
+ return nativeEventLogMetas.value.filter((meta) => matchesKeyword(meta, normalizedKeyword));
151
+ });
157
152
 
158
153
  const emptyText = computed(() => {
159
154
  if (keyword.value) return '无匹配结果';
@@ -161,7 +156,28 @@
161
156
  return '暂无日志';
162
157
  });
163
158
 
164
- const applyLogs = (logs: NativeEventLog[]): void => {
159
+ const handleCopy = (meta: NativeEventLogMeta) => {
160
+ const detail = getNativeEventDetail(meta.id);
161
+ const text = JSON.stringify(
162
+ {
163
+ key: detail?.key ?? meta.key,
164
+ params: detail?.params,
165
+ response: detail?.response,
166
+ startTime: detail?.startTime,
167
+ endTime: detail?.endTime,
168
+ },
169
+ null,
170
+ 2,
171
+ );
172
+ uni.setClipboardData({
173
+ data: text,
174
+ success: () => {
175
+ uni.showToast({ title: '已复制', icon: 'success' });
176
+ },
177
+ });
178
+ };
179
+
180
+ const upsertLogs = (logs: NativeEventLog[]): void => {
165
181
  const visibleLogs = clearTimestamp.value ? logs.filter((log) => log.startTime > clearTimestamp.value!) : logs;
166
182
 
167
183
  if (
@@ -177,43 +193,65 @@
177
193
  if (!visibleLogs.length && logs.length && clearTimestamp.value) {
178
194
  nativeEventDetailMap.value = new Map();
179
195
  nativeEventLogMetas.value = [];
180
- activeNames.value = [];
196
+ expandedIds.value = new Set();
181
197
  return;
182
198
  }
183
199
 
184
200
  const nextLogs = visibleLogs.slice(-MAX_NATIVE_EVENT_LOGS);
185
- const previousDetailMap = new Map(nativeEventDetailMap.value);
186
- const detailMap = new Map<string, NativeEventLog>();
187
- const nextMetas: NativeEventLogMeta[] = [];
201
+ const detailMap = nativeEventDetailMap.value;
188
202
 
203
+ const nextMetas: NativeEventLogMeta[] = [];
189
204
  for (const log of nextLogs) {
190
205
  const id = buildNativeEventLogId(log);
191
206
  detailMap.set(id, log);
192
207
  nextMetas.push(buildMetaFromLog(log));
193
208
  }
194
209
 
195
- nativeEventDetailMap.value = detailMap;
210
+ // 创建新的 Map 触发响应式更新
211
+ nativeEventDetailMap.value = new Map(detailMap);
196
212
  nativeEventLogMetas.value = nextMetas;
197
- activeNames.value = migrateActiveNames(previousDetailMap, nextMetas);
213
+
214
+ // 清理过期的详情数据
215
+ const keepIds = new Set(nextMetas.map((m) => m.id));
216
+ for (const key of detailMap.keys()) {
217
+ if (!keepIds.has(key)) detailMap.delete(key);
218
+ }
219
+
220
+ // 清理已不存在的展开项
221
+ const newExpandedIds = new Set(expandedIds.value);
222
+ for (const id of newExpandedIds) {
223
+ if (!keepIds.has(id)) {
224
+ newExpandedIds.delete(id);
225
+ }
226
+ }
227
+ expandedIds.value = newExpandedIds;
198
228
  };
199
229
 
200
230
  const flushPendingLogs = (): void => {
201
231
  flushTimer = null;
202
232
  if (!pendingLogs) return;
203
- applyLogs(pendingLogs);
204
- pendingLogs = null;
205
- };
206
233
 
207
- const scheduleFlush = (): void => {
208
- if (flushTimer) return;
209
- flushTimer = setTimeout(() => {
210
- flushPendingLogs();
211
- }, FLUSH_DELAY);
234
+ if (isInBackground.value) {
235
+ flushTimer = setTimeout(flushPendingLogs, FLUSH_DELAY_BACKGROUND);
236
+ return;
237
+ }
238
+
239
+ const logs = pendingLogs;
240
+ pendingLogs = null;
241
+ upsertLogs(logs);
212
242
  };
213
243
 
214
244
  const enqueueLogs = (logs: NativeEventLog[]): void => {
215
245
  pendingLogs = logs;
216
- scheduleFlush();
246
+
247
+ if (!flushTimer) {
248
+ const delay = isInBackground.value ? FLUSH_DELAY_BACKGROUND : FLUSH_DELAY_FOREGROUND;
249
+ flushTimer = setTimeout(flushPendingLogs, delay);
250
+ }
251
+ };
252
+
253
+ const handleClearKeyword = (): void => {
254
+ keyword.value = '';
217
255
  };
218
256
 
219
257
  const handleClear = (): void => {
@@ -221,81 +259,91 @@
221
259
  hasCleared.value = true;
222
260
  nativeEventLogMetas.value = [];
223
261
  nativeEventDetailMap.value = new Map();
224
- activeNames.value = [];
262
+ expandedIds.value = new Set();
263
+ keyword.value = '';
225
264
  };
226
265
 
227
- const handleClearKeyword = (): void => {
228
- keyword.value = '';
266
+ const cleanupTimers = (): void => {
267
+ if (flushTimer) {
268
+ clearTimeout(flushTimer);
269
+ flushTimer = null;
270
+ }
271
+ pendingLogs = null;
229
272
  };
230
273
 
231
- applyLogs(getNativeEventLogs().filter((log): log is NativeEventLog => Boolean(log)));
274
+ const setupLifecycleListeners = () => {
275
+ // #ifdef MP
276
+ const handleAppShow = () => {
277
+ if (isInBackground.value) {
278
+ isInBackground.value = false;
279
+ enqueueLogs(getNativeEventLogs());
280
+ }
281
+ };
232
282
 
233
- const stop = nativeEventSubject.subscribe((data: NativeEventLog[]) => {
234
- enqueueLogs(data);
235
- });
283
+ const handleAppHide = () => {
284
+ isInBackground.value = true;
285
+ };
286
+
287
+ // #ifdef MP-WEIXIN
288
+ if (typeof wx !== 'undefined' && wx.onAppShow) {
289
+ wx.onAppShow(handleAppShow);
290
+ }
291
+ if (typeof wx !== 'undefined' && wx.onAppHide) {
292
+ wx.onAppHide(handleAppHide);
293
+ }
294
+ // #endif
236
295
 
237
- watch(
238
- () => getActiveNameList().join('|'),
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 = '';
296
+ // #ifdef MP-ALIPAY
297
+ if (typeof my !== 'undefined' && my.onAppShow) {
298
+ my.onAppShow(handleAppShow);
299
+ }
300
+ if (typeof my !== 'undefined' && my.onAppHide) {
301
+ my.onAppHide(handleAppHide);
263
302
  }
303
+ // #endif
264
304
 
305
+ // uni.onAppShow 返回取消订阅函数
265
306
  // #ifdef MP
266
- if (getActiveNameList().length) nextTick(() => collapseRef.value?.resize?.());
267
- // #endif
268
- });
307
+ try {
308
+ const offAppShow = uni.onAppShow?.(handleAppShow);
309
+ const offAppHide = uni.onAppHide?.(handleAppHide);
269
310
 
270
- onBeforeUnmount(() => {
271
- if (flushTimer) {
272
- clearTimeout(flushTimer);
273
- flushTimer = null;
311
+ if (offAppShow) cleanupFunctions.push(offAppShow);
312
+ if (offAppHide) cleanupFunctions.push(offAppHide);
313
+ } catch (e) {
314
+ // 某些平台可能不支持,忽略错误
274
315
  }
275
- pendingLogs = null;
276
- stop?.unsubscribe();
316
+ // #endif
317
+ // #endif
318
+ };
319
+
320
+ // 初始化
321
+ enqueueLogs(getNativeEventLogs().filter((log): log is NativeEventLog => Boolean(log)));
322
+
323
+ // 订阅日志更新
324
+ const sub = nativeEventSubject.subscribe((data: NativeEventLog[]) => {
325
+ enqueueLogs(data);
277
326
  });
278
327
 
279
- const handleCopy = (meta: NativeEventLogMeta) => {
280
- const detail = getNativeEventDetail(meta.id);
281
- const text = JSON.stringify(
282
- {
283
- key: detail?.key ?? meta.key,
284
- params: detail?.params,
285
- response: detail?.response,
286
- startTime: detail?.startTime,
287
- endTime: detail?.endTime,
288
- },
289
- null,
290
- 2,
291
- );
292
- uni.setClipboardData({
293
- data: text,
294
- success: () => {
295
- uni.showToast({ title: '已复制', icon: 'success' });
296
- },
328
+ unsubscribeLogs = () => sub.unsubscribe();
329
+
330
+ setupLifecycleListeners();
331
+
332
+ onBeforeUnmount(() => {
333
+ // 清理所有订阅和监听
334
+ unsubscribeLogs?.();
335
+ cleanupTimers();
336
+
337
+ // 清理小程序生命周期监听
338
+ cleanupFunctions.forEach((fn) => {
339
+ try {
340
+ fn();
341
+ } catch (e) {
342
+ // 忽略清理错误
343
+ }
297
344
  });
298
- };
345
+ cleanupFunctions.length = 0;
346
+ });
299
347
  </script>
300
348
 
301
349
  <style lang="scss" scoped>
@@ -351,25 +399,66 @@
351
399
  border-radius: 4px;
352
400
  }
353
401
 
354
- .empty-state {
355
- padding: 16px 0;
402
+ .log-list {
403
+ flex: 1;
404
+ max-height: 70vh;
405
+ }
406
+
407
+ .log-item {
408
+ border-bottom: 1px solid #e5e5e5;
409
+ background-color: #fff;
410
+ }
411
+
412
+ .log-header {
413
+ display: flex;
414
+ justify-content: space-between;
415
+ align-items: center;
416
+ padding: 12px 15px;
417
+ background-color: #f9f9f9;
418
+ }
419
+
420
+ .log-title {
421
+ font-size: 10px;
422
+ color: #333;
423
+ flex: 1;
424
+ word-break: break-all;
425
+ }
426
+
427
+ .log-arrow {
356
428
  font-size: 12px;
357
429
  color: #999;
358
- text-align: center;
430
+ margin-left: 8px;
359
431
  }
360
432
 
361
- .uni-collapse-content {
433
+ .log-content {
434
+ padding: 10px 15px;
435
+ font-size: 10px;
436
+ background-color: #fff;
437
+ }
438
+
439
+ .log-actions {
440
+ text-align: right;
441
+ margin-bottom: 8px;
442
+ }
443
+
444
+ .log-copy {
445
+ color: #007aff;
362
446
  font-size: 12px;
363
447
  }
364
- ::v-deep {
365
- .uni-collapse-item__title-text {
366
- white-space: normal !important; /* 允许文本换行 */
367
- overflow: visible !important; /* 显示溢出的文本 */
368
- text-overflow: clip !important; /* 不显示省略标记(...) */
369
- line-height: normal !important; /* 重置行高 */
370
- word-break: break-all !important; /* 允许单词内换行 */
371
- font-size: 10px;
372
- font-weight: normal;
373
- }
448
+
449
+ .log-section {
450
+ margin-bottom: 8px;
451
+ word-break: break-all;
452
+ }
453
+
454
+ .log-label {
455
+ font-weight: bolder;
456
+ }
457
+
458
+ .empty-state {
459
+ padding: 16px 0;
460
+ font-size: 12px;
461
+ color: #999;
462
+ text-align: center;
374
463
  }
375
464
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uni-oaview",
3
- "version": "1.9.12",
3
+ "version": "1.9.13",
4
4
  "description": "uniapp小程序组件库",
5
5
  "main": "dist/index.esm.js",
6
6
  "typings": "dist/index.d.ts",