uni-oaview 1.6.0 → 1.7.1

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.
@@ -1,7 +1,14 @@
1
1
  <template>
2
- <uni-collapse ref="collapseRef" v-model="activeNames">
2
+ <view class="toolbar">
3
+ <view class="toolbar-input-wrap">
4
+ <input v-model="keyword" class="toolbar-input" placeholder="搜索实例ID / 音源 / 操作" confirm-type="search" />
5
+ <view v-if="keyword" class="toolbar-input-clear" @click="handleClearKeyword">×</view>
6
+ </view>
7
+ <view class="toolbar-clear" @click="handleClear">清除</view>
8
+ </view>
9
+ <uni-collapse v-if="filteredAudioLogMetas.length" ref="collapseRef" v-model="activeNames">
3
10
  <uni-collapse-item
4
- v-for="meta in audioLogMetas"
11
+ v-for="meta in filteredAudioLogMetas"
5
12
  :key="meta.id"
6
13
  :name="meta.id"
7
14
  :title="meta.title"
@@ -39,10 +46,13 @@
39
46
  </view>
40
47
  </uni-collapse-item>
41
48
  </uni-collapse>
49
+ <view v-else class="empty-state">
50
+ {{ emptyText }}
51
+ </view>
42
52
  </template>
43
53
 
44
54
  <script lang="ts" setup>
45
- import { nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
55
+ import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
46
56
  import awesomeDisplayInfo from './awesome-display-info.vue';
47
57
  import { audioSubject, getAudioLogs } from 'uniapp-log-sdk';
48
58
 
@@ -73,6 +83,9 @@
73
83
 
74
84
  const collapseRef = ref<{ resize?: () => void } | null>(null);
75
85
  const activeNames = ref<string[] | string>([]);
86
+ const keyword = ref('');
87
+ const clearTimestamp = ref<number | null>(null);
88
+ const hasCleared = ref(false);
76
89
 
77
90
  const audioLogDetailMap = shallowRef<Map<string, AudioLog>>(new Map());
78
91
  const audioLogMetas = ref<AudioLogMeta[]>([]);
@@ -142,8 +155,68 @@
142
155
  return audioLogDetailMap.value.get(id) ?? null;
143
156
  };
144
157
 
158
+ const safeStringify = (value: unknown): string => {
159
+ if (value === null || value === undefined) return '';
160
+ if (typeof value === 'string') return value;
161
+ try {
162
+ return JSON.stringify(value);
163
+ } catch (error) {
164
+ return String(value);
165
+ }
166
+ };
167
+
168
+ const normalizeKeyword = (value: string): string => value.trim().toLowerCase();
169
+
170
+ const matchesAudioLog = (meta: AudioLogMeta): boolean => {
171
+ const normalizedKeyword = normalizeKeyword(keyword.value);
172
+ if (!normalizedKeyword) return true;
173
+ const detail = getAudioLogDetail(meta.id);
174
+ return [meta.title, meta.instanceId, meta.src, meta.action, safeStringify(detail?.data)]
175
+ .join(' ')
176
+ .toLowerCase()
177
+ .includes(normalizedKeyword);
178
+ };
179
+
180
+ const filteredAudioLogMetas = computed(() => audioLogMetas.value.filter(matchesAudioLog));
181
+
182
+ const emptyText = computed(() => {
183
+ if (keyword.value) return '无匹配结果';
184
+ if (hasCleared.value) return '已清空';
185
+ return '暂无日志';
186
+ });
187
+
188
+ const handleClear = () => {
189
+ clearTimestamp.value = Date.now();
190
+ hasCleared.value = true;
191
+ audioLogMetas.value = [];
192
+ audioLogDetailMap.value = new Map();
193
+ activeNames.value = [];
194
+ };
195
+
196
+ const handleClearKeyword = () => {
197
+ keyword.value = '';
198
+ };
199
+
145
200
  const upsertLogs = (logs: AudioLog[]) => {
146
- const nextLogs = logs.slice(-MAX_AUDIO_LOGS);
201
+ const visibleLogs = clearTimestamp.value ? logs.filter((log) => log.timestamp > clearTimestamp.value!) : logs;
202
+ if (
203
+ !visibleLogs.length &&
204
+ clearTimestamp.value &&
205
+ logs.length &&
206
+ logs[logs.length - 1].timestamp < clearTimestamp.value
207
+ ) {
208
+ clearTimestamp.value = null;
209
+ hasCleared.value = false;
210
+ }
211
+
212
+ if (!visibleLogs.length && logs.length && clearTimestamp.value) {
213
+ audioLogDetailMap.value = new Map();
214
+ audioLogMetas.value = [];
215
+ activeNames.value = [];
216
+ return;
217
+ }
218
+
219
+ const nextLogs = visibleLogs.slice(-MAX_AUDIO_LOGS);
147
220
  const detailMap = audioLogDetailMap.value;
148
221
 
149
222
  const nextMetas: AudioLogMeta[] = [];
@@ -160,14 +233,6 @@
160
233
  for (const key of detailMap.keys()) {
161
234
  if (!keepIds.has(key)) detailMap.delete(key);
162
235
  }
163
-
164
- // 清理 activeNames(如果展开项已被裁剪掉)
165
- const currentActive = activeNames.value;
166
- if (Array.isArray(currentActive)) {
167
- if (currentActive.length) activeNames.value = currentActive.filter((id) => keepIds.has(id));
168
- } else if (currentActive && !keepIds.has(currentActive)) {
169
- activeNames.value = '';
170
- }
171
236
  };
172
237
 
173
238
  upsertLogs(getAudioLogs());
@@ -197,12 +262,85 @@
197
262
  { deep: false },
198
263
  );
199
264
 
265
+ watch(keyword, () => {
266
+ const keepIds = new Set(filteredAudioLogMetas.value.map((meta) => meta.id));
267
+ const currentActive = activeNames.value;
268
+ if (Array.isArray(currentActive)) {
269
+ activeNames.value = currentActive.filter((id) => keepIds.has(id));
270
+ } else if (currentActive && !keepIds.has(currentActive)) {
271
+ activeNames.value = '';
272
+ }
273
+
274
+ // #ifdef MP
275
+ if (getActiveNameList().length) nextTick(() => collapseRef.value?.resize?.());
276
+ // #endif
277
+ });
278
+
200
279
  onBeforeUnmount(() => {
201
280
  stop?.unsubscribe();
202
281
  });
203
282
  </script>
204
283
 
205
284
  <style lang="scss" scoped>
285
+ .toolbar {
286
+ position: sticky;
287
+ top: 0;
288
+ z-index: 1;
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 8px;
292
+ margin-bottom: 8px;
293
+ padding-bottom: 8px;
294
+ background-color: #fff;
295
+ }
296
+
297
+ .toolbar-input {
298
+ width: 100%;
299
+ height: 32px;
300
+ padding: 0 28px 0 10px;
301
+ font-size: 12px;
302
+ border: 1px solid #dcdfe6;
303
+ border-radius: 4px;
304
+ background-color: #fff;
305
+ box-sizing: border-box;
306
+ }
307
+
308
+ .toolbar-input-wrap {
309
+ position: relative;
310
+ flex: 1;
311
+ min-width: 0;
312
+ }
313
+
314
+ .toolbar-input-clear {
315
+ position: absolute;
316
+ top: 50%;
317
+ right: 8px;
318
+ transform: translateY(-50%);
319
+ width: 16px;
320
+ height: 16px;
321
+ line-height: 16px;
322
+ font-size: 12px;
323
+ color: #999;
324
+ text-align: center;
325
+ border-radius: 50%;
326
+ }
327
+
328
+ .toolbar-clear {
329
+ flex-shrink: 0;
330
+ padding: 6px 10px;
331
+ font-size: 12px;
332
+ color: #119af5;
333
+ border: 1px solid #119af5;
334
+ border-radius: 4px;
335
+ }
336
+
337
+ .empty-state {
338
+ padding: 16px 0;
339
+ font-size: 12px;
340
+ color: #999;
341
+ text-align: center;
342
+ }
343
+
206
344
  .uni-collapse-content {
207
345
  font-size: 12px;
208
346
  }
@@ -1,18 +1,35 @@
1
1
  <template>
2
- <view
3
- class="log-item"
4
- v-for="log in consoleLogs"
5
- style="font-size: 10px; width: calc(100vw - 16px); word-break: break-all"
6
- >
7
- {{ log }}
2
+ <view class="toolbar">
3
+ <view class="toolbar-input-wrap">
4
+ <input v-model="keyword" class="toolbar-input" placeholder="搜索日志内容" confirm-type="search" />
5
+ <view v-if="keyword" class="toolbar-input-clear" @click="handleClearKeyword">×</view>
6
+ </view>
7
+ <view class="toolbar-clear" @click="handleClear">清除</view>
8
+ </view>
9
+ <template v-if="filteredConsoleLogs.length">
10
+ <view
11
+ v-for="(log, index) in filteredConsoleLogs"
12
+ :key="`${index}-${log}`"
13
+ class="log-item"
14
+ style="font-size: 10px; width: calc(100vw - 16px); word-break: break-all"
15
+ >
16
+ {{ log }}
17
+ </view>
18
+ </template>
19
+ <view v-else class="empty-state">
20
+ {{ emptyText }}
8
21
  </view>
9
22
  </template>
10
23
  <script lang="ts" setup>
11
- import { onBeforeUnmount, ref } from 'vue';
24
+ import { computed, onBeforeUnmount, ref } from 'vue';
12
25
  import { consoleSubject, getConsoleLogs } from 'uniapp-log-sdk';
13
26
 
14
27
  const MAX_CONSOLE_LOGS = 500;
15
28
 
29
+ const keyword = ref('');
30
+ const hasCleared = ref(false);
31
+ const clearedLogCount = ref(0);
32
+
16
33
  const logConvert = (items: any[]): string =>
17
34
  items.reduce((prev, current, index) => {
18
35
  try {
@@ -26,11 +43,44 @@
26
43
  return prev;
27
44
  }, '');
28
45
 
29
- const consoleLogs = ref<string[]>(getConsoleLogs().slice(-MAX_CONSOLE_LOGS).map(logConvert));
46
+ const normalizeKeyword = (value: string): string => value.trim().toLowerCase();
47
+
48
+ const buildVisibleLogs = (logs: any[]): string[] => {
49
+ const nextLogs = clearedLogCount.value > 0 ? logs.slice(clearedLogCount.value) : logs;
50
+ return nextLogs.slice(-MAX_CONSOLE_LOGS).map(logConvert);
51
+ };
52
+
53
+ const initialLogs = getConsoleLogs();
54
+ const consoleLogs = ref<string[]>(buildVisibleLogs(initialLogs));
55
+
56
+ const filteredConsoleLogs = computed(() => {
57
+ const normalizedKeyword = normalizeKeyword(keyword.value);
58
+ if (!normalizedKeyword) return consoleLogs.value;
59
+ return consoleLogs.value.filter((log) => log.toLowerCase().includes(normalizedKeyword));
60
+ });
61
+
62
+ const emptyText = computed(() => {
63
+ if (keyword.value) return '无匹配结果';
64
+ if (hasCleared.value) return '已清空';
65
+ return '暂无日志';
66
+ });
67
+
68
+ const handleClear = () => {
69
+ hasCleared.value = true;
70
+ clearedLogCount.value = getConsoleLogs().length;
71
+ consoleLogs.value = [];
72
+ };
73
+
74
+ const handleClearKeyword = () => {
75
+ keyword.value = '';
76
+ };
30
77
 
31
78
  const stop = consoleSubject.subscribe((data: any) => {
32
- const converted = data.slice(-MAX_CONSOLE_LOGS).map(logConvert);
33
- consoleLogs.value = converted;
79
+ if (data.length < clearedLogCount.value) {
80
+ clearedLogCount.value = 0;
81
+ hasCleared.value = false;
82
+ }
83
+ consoleLogs.value = buildVisibleLogs(data);
34
84
  });
35
85
 
36
86
  onBeforeUnmount(() => {
@@ -39,7 +89,66 @@
39
89
  </script>
40
90
 
41
91
  <style lang="scss" scoped>
92
+ .toolbar {
93
+ position: sticky;
94
+ top: 0;
95
+ z-index: 1;
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 8px;
99
+ margin-bottom: 8px;
100
+ padding-bottom: 8px;
101
+ background-color: #fff;
102
+ }
103
+
104
+ .toolbar-input {
105
+ width: 100%;
106
+ height: 32px;
107
+ padding: 0 28px 0 10px;
108
+ font-size: 12px;
109
+ border: 1px solid #dcdfe6;
110
+ border-radius: 4px;
111
+ background-color: #fff;
112
+ box-sizing: border-box;
113
+ }
114
+
115
+ .toolbar-input-wrap {
116
+ position: relative;
117
+ flex: 1;
118
+ min-width: 0;
119
+ }
120
+
121
+ .toolbar-input-clear {
122
+ position: absolute;
123
+ top: 50%;
124
+ right: 8px;
125
+ transform: translateY(-50%);
126
+ width: 16px;
127
+ height: 16px;
128
+ line-height: 16px;
129
+ font-size: 12px;
130
+ color: #999;
131
+ text-align: center;
132
+ border-radius: 50%;
133
+ }
134
+
135
+ .toolbar-clear {
136
+ flex-shrink: 0;
137
+ padding: 6px 10px;
138
+ font-size: 12px;
139
+ color: #119af5;
140
+ border: 1px solid #119af5;
141
+ border-radius: 4px;
142
+ }
143
+
42
144
  .log-item:nth-of-type(odd) {
43
145
  background-color: #eee;
44
146
  }
147
+
148
+ .empty-state {
149
+ padding: 16px 0;
150
+ font-size: 12px;
151
+ color: #999;
152
+ text-align: center;
153
+ }
45
154
  </style>
@@ -1,7 +1,14 @@
1
1
  <template>
2
- <uni-collapse ref="collapseRef" v-model="activeNames">
2
+ <view class="toolbar">
3
+ <view class="toolbar-input-wrap">
4
+ <input v-model="keyword" class="toolbar-input" placeholder="搜索 URL / 方法 / 状态码" confirm-type="search" />
5
+ <view v-if="keyword" class="toolbar-input-clear" @click="handleClearKeyword">×</view>
6
+ </view>
7
+ <view class="toolbar-clear" @click="handleClear">清除</view>
8
+ </view>
9
+ <uni-collapse v-if="filteredNetworkLogMetas.length" ref="collapseRef" v-model="activeNames">
3
10
  <uni-collapse-item
4
- v-for="meta in networkLogMetas"
11
+ v-for="meta in filteredNetworkLogMetas"
5
12
  :key="meta.id"
6
13
  :name="meta.id"
7
14
  :title="meta.title"
@@ -32,10 +39,13 @@
32
39
  </view>
33
40
  </uni-collapse-item>
34
41
  </uni-collapse>
42
+ <view v-else class="empty-state">
43
+ {{ emptyText }}
44
+ </view>
35
45
  </template>
36
46
 
37
47
  <script lang="ts" setup>
38
- import { nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
48
+ import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
39
49
  import awesomeDisplayInfo from './awesome-display-info.vue';
40
50
  import { networkSubject, getNetworkLogs } from 'uniapp-log-sdk';
41
51
 
@@ -79,6 +89,9 @@
79
89
  const collapseRef = ref<{ resize?: () => void } | null>(null);
80
90
  // uni-collapse 的 v-model 在不同模式下可能是 string 或 string[]
81
91
  const activeNames = ref<string[] | string>([]);
92
+ const keyword = ref('');
93
+ const clearTimestamp = ref<number | null>(null);
94
+ const hasCleared = ref(false);
82
95
 
83
96
  const networkLogDetailMap = shallowRef<Map<string, NetworkLog>>(new Map());
84
97
  const networkLogMetas = ref<NetworkLogMeta[]>([]);
@@ -133,15 +146,85 @@
133
146
  return networkLogDetailMap.value.get(id) ?? null;
134
147
  };
135
148
 
149
+ const normalizeKeyword = (value: string): string => value.trim().toLowerCase();
150
+
151
+ const safeStringify = (value: unknown): string => {
152
+ if (value === null || value === undefined) return '';
153
+ if (typeof value === 'string') return value;
154
+ try {
155
+ return JSON.stringify(value);
156
+ } catch (error) {
157
+ return String(value);
158
+ }
159
+ };
160
+
161
+ const matchesNetworkLog = (meta: NetworkLogMeta): boolean => {
162
+ const normalizedKeyword = normalizeKeyword(keyword.value);
163
+ if (!normalizedKeyword) return true;
164
+ const detail = getNetworkLogDetail(meta.id);
165
+ return [
166
+ meta.title,
167
+ meta.method,
168
+ meta.statusCode,
169
+ detail?.request?.url,
170
+ safeStringify(detail?.request?.data),
171
+ safeStringify(detail?.response),
172
+ ]
173
+ .join(' ')
174
+ .toLowerCase()
175
+ .includes(normalizedKeyword);
176
+ };
177
+
178
+ const filteredNetworkLogMetas = computed(() => networkLogMetas.value.filter(matchesNetworkLog));
179
+
180
+ const emptyText = computed(() => {
181
+ if (keyword.value) return '无匹配结果';
182
+ if (hasCleared.value) return '已清空';
183
+ return '暂无日志';
184
+ });
185
+
136
186
  // 使用类型守卫过滤出 HTTP 日志
137
187
  const isHttpLog = (log: any): log is NetworkLog => {
138
188
  return log && log.type !== 'websocket';
139
189
  };
140
190
 
191
+ const handleClear = () => {
192
+ clearTimestamp.value = Date.now();
193
+ hasCleared.value = true;
194
+ networkLogMetas.value = [];
195
+ networkLogDetailMap.value = new Map();
196
+ activeNames.value = [];
197
+ };
198
+
199
+ const handleClearKeyword = () => {
200
+ keyword.value = '';
201
+ };
202
+
141
203
  const upsertLogs = (logs: any[]): void => {
142
204
  // 过滤出非 websocket 的日志
143
205
  const httpLogs = logs.filter(isHttpLog);
144
- const nextLogs = httpLogs.slice(-MAX_NETWORK_LOGS);
206
+ const visibleLogs = clearTimestamp.value
207
+ ? httpLogs.filter((log) => log.startTime > clearTimestamp.value!)
208
+ : httpLogs;
209
+
210
+ if (
211
+ !visibleLogs.length &&
212
+ clearTimestamp.value &&
213
+ httpLogs.length &&
214
+ httpLogs[httpLogs.length - 1].startTime < clearTimestamp.value
215
+ ) {
216
+ clearTimestamp.value = null;
217
+ hasCleared.value = false;
218
+ }
219
+
220
+ if (!visibleLogs.length && httpLogs.length && clearTimestamp.value) {
221
+ networkLogDetailMap.value = new Map();
222
+ networkLogMetas.value = [];
223
+ activeNames.value = [];
224
+ return;
225
+ }
226
+
227
+ const nextLogs = visibleLogs.slice(-MAX_NETWORK_LOGS);
145
228
  const detailMap = networkLogDetailMap.value;
146
229
 
147
230
  const nextMetas: NetworkLogMeta[] = [];
@@ -158,14 +241,6 @@
158
241
  for (const key of detailMap.keys()) {
159
242
  if (!keepIds.has(key)) detailMap.delete(key);
160
243
  }
161
-
162
- // 清理 activeNames(如果展开项已被裁剪掉)
163
- const currentActive = activeNames.value;
164
- if (Array.isArray(currentActive)) {
165
- if (currentActive.length) activeNames.value = currentActive.filter((id) => keepIds.has(id));
166
- } else if (currentActive && !keepIds.has(currentActive)) {
167
- activeNames.value = '';
168
- }
169
244
  };
170
245
 
171
246
  upsertLogs(getNetworkLogs());
@@ -195,12 +270,85 @@
195
270
  { deep: false },
196
271
  );
197
272
 
273
+ watch(keyword, () => {
274
+ const keepIds = new Set(filteredNetworkLogMetas.value.map((meta) => meta.id));
275
+ const currentActive = activeNames.value;
276
+ if (Array.isArray(currentActive)) {
277
+ activeNames.value = currentActive.filter((id) => keepIds.has(id));
278
+ } else if (currentActive && !keepIds.has(currentActive)) {
279
+ activeNames.value = '';
280
+ }
281
+
282
+ // #ifdef MP
283
+ if (getActiveNameList().length) nextTick(() => collapseRef.value?.resize?.());
284
+ // #endif
285
+ });
286
+
198
287
  onBeforeUnmount(() => {
199
288
  stop?.unsubscribe();
200
289
  });
201
290
  </script>
202
291
 
203
292
  <style lang="scss" scoped>
293
+ .toolbar {
294
+ position: sticky;
295
+ top: 0;
296
+ z-index: 1;
297
+ display: flex;
298
+ align-items: center;
299
+ gap: 8px;
300
+ margin-bottom: 8px;
301
+ padding-bottom: 8px;
302
+ background-color: #fff;
303
+ }
304
+
305
+ .toolbar-input {
306
+ width: 100%;
307
+ height: 32px;
308
+ padding: 0 28px 0 10px;
309
+ font-size: 12px;
310
+ border: 1px solid #dcdfe6;
311
+ border-radius: 4px;
312
+ background-color: #fff;
313
+ box-sizing: border-box;
314
+ }
315
+
316
+ .toolbar-input-wrap {
317
+ position: relative;
318
+ flex: 1;
319
+ min-width: 0;
320
+ }
321
+
322
+ .toolbar-input-clear {
323
+ position: absolute;
324
+ top: 50%;
325
+ right: 8px;
326
+ transform: translateY(-50%);
327
+ width: 16px;
328
+ height: 16px;
329
+ line-height: 16px;
330
+ font-size: 12px;
331
+ color: #999;
332
+ text-align: center;
333
+ border-radius: 50%;
334
+ }
335
+
336
+ .toolbar-clear {
337
+ flex-shrink: 0;
338
+ padding: 6px 10px;
339
+ font-size: 12px;
340
+ color: #119af5;
341
+ border: 1px solid #119af5;
342
+ border-radius: 4px;
343
+ }
344
+
345
+ .empty-state {
346
+ padding: 16px 0;
347
+ font-size: 12px;
348
+ color: #999;
349
+ text-align: center;
350
+ }
351
+
204
352
  .uni-collapse-content {
205
353
  font-size: 12px;
206
354
  }
@@ -1,7 +1,14 @@
1
1
  <template>
2
- <uni-collapse ref="collapseRef" v-model="activeNames">
2
+ <view class="toolbar">
3
+ <view class="toolbar-input-wrap">
4
+ <input v-model="keyword" class="toolbar-input" placeholder="搜索 URL / 状态 / 协议" confirm-type="search" />
5
+ <view v-if="keyword" class="toolbar-input-clear" @click="handleClearKeyword">×</view>
6
+ </view>
7
+ <view class="toolbar-clear" @click="handleClear">清除</view>
8
+ </view>
9
+ <uni-collapse v-if="filteredWebsocketLogMetas.length" ref="collapseRef" v-model="activeNames">
3
10
  <uni-collapse-item
4
- v-for="meta in websocketLogMetas"
11
+ v-for="meta in filteredWebsocketLogMetas"
5
12
  :key="meta.id"
6
13
  :name="meta.id"
7
14
  :title="meta.title"
@@ -69,10 +76,13 @@
69
76
  </view>
70
77
  </uni-collapse-item>
71
78
  </uni-collapse>
79
+ <view v-else class="empty-state">
80
+ {{ emptyText }}
81
+ </view>
72
82
  </template>
73
83
 
74
84
  <script lang="ts" setup>
75
- import { nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
85
+ import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue';
76
86
  import awesomeDisplayInfo from './awesome-display-info.vue';
77
87
  import { websocketSubject, getWebsocketLogs } from 'uniapp-log-sdk';
78
88
 
@@ -125,6 +135,9 @@
125
135
 
126
136
  const collapseRef = ref<{ resize?: () => void } | null>(null);
127
137
  const activeNames = ref<string[] | string>([]);
138
+ const keyword = ref('');
139
+ const clearTimestamp = ref<number | null>(null);
140
+ const hasCleared = ref(false);
128
141
 
129
142
  const websocketLogDetailMap = shallowRef<Map<string, WebsocketLog>>(new Map());
130
143
  const websocketLogMetas = ref<WebsocketLogMeta[]>([]);
@@ -184,6 +197,62 @@
184
197
  return websocketLogDetailMap.value.get(id) ?? null;
185
198
  };
186
199
 
200
+ const safeStringify = (value: unknown): string => {
201
+ if (value === null || value === undefined) return '';
202
+ if (typeof value === 'string') return value;
203
+ try {
204
+ return JSON.stringify(value);
205
+ } catch (error) {
206
+ return String(value);
207
+ }
208
+ };
209
+
210
+ const normalizeKeyword = (value: string): string => value.trim().toLowerCase();
211
+
212
+ const matchesWebsocketLog = (meta: WebsocketLogMeta): boolean => {
213
+ const normalizedKeyword = normalizeKeyword(keyword.value);
214
+ if (!normalizedKeyword) return true;
215
+ const detail = getWebsocketLogDetail(meta.id);
216
+ return [meta.title, meta.url, meta.status, meta.protocols, safeStringify(detail?.errorInfo)]
217
+ .join(' ')
218
+ .toLowerCase()
219
+ .includes(normalizedKeyword);
220
+ };
221
+
222
+ const filteredWebsocketLogMetas = computed(() => websocketLogMetas.value.filter(matchesWebsocketLog));
223
+
224
+ const emptyText = computed(() => {
225
+ if (keyword.value) return '无匹配结果';
226
+ if (hasCleared.value) return '已清空';
227
+ return '暂无日志';
228
+ });
229
+
230
+ const handleClear = () => {
231
+ clearTimestamp.value = Date.now();
232
+ hasCleared.value = true;
233
+ websocketLogMetas.value = [];
234
+ websocketLogDetailMap.value = new Map();
235
+ activeNames.value = [];
236
+ };
237
+
238
+ const handleClearKeyword = () => {
239
+ keyword.value = '';
240
+ };
241
+
242
+ const buildVisibleWebsocketLog = (log: WebsocketLog): WebsocketLog | null => {
243
+ if (!clearTimestamp.value) return log;
244
+
245
+ const hasConnectionLifecycleAfterClear =
246
+ log.startTime > clearTimestamp.value || log.openTime > clearTimestamp.value;
247
+ const hasNewError = Boolean(log.errorInfo) && (!log.endTime || log.endTime > clearTimestamp.value);
248
+
249
+ if (!hasConnectionLifecycleAfterClear && !hasNewError) {
250
+ return null;
251
+ }
252
+
253
+ return log;
254
+ };
255
+
187
256
  const getMessageList = (id: string): Array<MessageRecord & { dataPreview: string }> => {
188
257
  const log = getWebsocketLogDetail(id);
189
258
  if (!log) return [];
@@ -194,7 +263,16 @@
194
263
  };
195
264
 
196
265
  const upsertLogs = (logs: WebsocketLog[]) => {
197
- const nextLogs = logs.slice(-MAX_WEBSOCKET_LOGS);
266
+ const visibleLogs = logs.map(buildVisibleWebsocketLog).filter((log): log is WebsocketLog => Boolean(log));
267
+
268
+ if (!visibleLogs.length && clearTimestamp.value && logs.length) {
269
+ websocketLogDetailMap.value = new Map();
270
+ websocketLogMetas.value = [];
271
+ activeNames.value = [];
272
+ return;
273
+ }
274
+
275
+ const nextLogs = visibleLogs.slice(-MAX_WEBSOCKET_LOGS);
198
276
  const detailMap = websocketLogDetailMap.value;
199
277
 
200
278
  const nextMetas: WebsocketLogMeta[] = [];
@@ -211,14 +289,6 @@
211
289
  for (const key of detailMap.keys()) {
212
290
  if (!keepIds.has(key)) detailMap.delete(key);
213
291
  }
214
-
215
- // 清理 activeNames(如果展开项已被裁剪掉)
216
- const currentActive = activeNames.value;
217
- if (Array.isArray(currentActive)) {
218
- if (currentActive.length) activeNames.value = currentActive.filter((id) => keepIds.has(id));
219
- } else if (currentActive && !keepIds.has(currentActive)) {
220
- activeNames.value = '';
221
- }
222
292
  };
223
293
 
224
294
  upsertLogs(getWebsocketLogs());
@@ -248,12 +318,85 @@
248
318
  { deep: false },
249
319
  );
250
320
 
321
+ watch(keyword, () => {
322
+ const keepIds = new Set(filteredWebsocketLogMetas.value.map((meta) => meta.id));
323
+ const currentActive = activeNames.value;
324
+ if (Array.isArray(currentActive)) {
325
+ activeNames.value = currentActive.filter((id) => keepIds.has(id));
326
+ } else if (currentActive && !keepIds.has(currentActive)) {
327
+ activeNames.value = '';
328
+ }
329
+
330
+ // #ifdef MP
331
+ if (getActiveNameList().length) nextTick(() => collapseRef.value?.resize?.());
332
+ // #endif
333
+ });
334
+
251
335
  onBeforeUnmount(() => {
252
336
  stop?.unsubscribe();
253
337
  });
254
338
  </script>
255
339
 
256
340
  <style lang="scss" scoped>
341
+ .toolbar {
342
+ position: sticky;
343
+ top: 0;
344
+ z-index: 1;
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 8px;
348
+ margin-bottom: 8px;
349
+ padding-bottom: 8px;
350
+ background-color: #fff;
351
+ }
352
+
353
+ .toolbar-input {
354
+ width: 100%;
355
+ height: 32px;
356
+ padding: 0 28px 0 10px;
357
+ font-size: 12px;
358
+ border: 1px solid #dcdfe6;
359
+ border-radius: 4px;
360
+ background-color: #fff;
361
+ box-sizing: border-box;
362
+ }
363
+
364
+ .toolbar-input-wrap {
365
+ position: relative;
366
+ flex: 1;
367
+ min-width: 0;
368
+ }
369
+
370
+ .toolbar-input-clear {
371
+ position: absolute;
372
+ top: 50%;
373
+ right: 8px;
374
+ transform: translateY(-50%);
375
+ width: 16px;
376
+ height: 16px;
377
+ line-height: 16px;
378
+ font-size: 12px;
379
+ color: #999;
380
+ text-align: center;
381
+ border-radius: 50%;
382
+ }
383
+
384
+ .toolbar-clear {
385
+ flex-shrink: 0;
386
+ padding: 6px 10px;
387
+ font-size: 12px;
388
+ color: #119af5;
389
+ border: 1px solid #119af5;
390
+ border-radius: 4px;
391
+ }
392
+
393
+ .empty-state {
394
+ padding: 16px 0;
395
+ font-size: 12px;
396
+ color: #999;
397
+ text-align: center;
398
+ }
399
+
257
400
  .uni-collapse-content {
258
401
  font-size: 12px;
259
402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uni-oaview",
3
- "version": "1.6.00",
3
+ "version": "1.7.01",
4
4
  "description": "uniapp小程序组件库",
5
5
  "main": "dist/index.esm.js",
6
6
  "typings": "dist/index.d.ts",