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
|
-
<
|
|
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
|
|
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
|
|
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="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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
|
}
|