vue_zhongyou 1.0.1 → 1.0.2
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.
- package/package.json +1 -1
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/255/230/345/220/216/347/253/257/347/211/210/347/233/221/346/216/247/errorLogPage.vue +422 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/255/230/345/220/216/347/253/257/347/211/210/347/233/221/346/216/247/errorMonitor.js +375 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/255/230/345/220/216/347/253/257/347/211/210/347/233/221/346/216/247/request.js +99 -0
- package//345/212/237/350/203/275/344/273/243/347/240/201//345/255/230/345/220/216/347/253/257/347/211/210/347/233/221/346/216/247/testError.vue +500 -0
package/package.json
CHANGED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="error-log-page">
|
|
3
|
+
<!-- 顶部统计卡片 -->
|
|
4
|
+
<div class="stats-section">
|
|
5
|
+
<div class="stat-card" v-for="(value, key) in statistics" :key="key">
|
|
6
|
+
<div class="stat-label">{{ getStatLabel(key) }}</div>
|
|
7
|
+
<div class="stat-value">{{ value }}</div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- 筛选和操作栏 -->
|
|
12
|
+
<div class="toolbar">
|
|
13
|
+
<van-radio-group v-model="filterType" direction="horizontal" @change="handleFilterChange">
|
|
14
|
+
<van-radio name="">全部</van-radio>
|
|
15
|
+
<van-radio name="api">接口</van-radio>
|
|
16
|
+
<van-radio name="page">页面</van-radio>
|
|
17
|
+
<van-radio name="promise">Promise</van-radio>
|
|
18
|
+
</van-radio-group>
|
|
19
|
+
<div class="actions">
|
|
20
|
+
<van-button size="small" type="danger" plain @click="handleClear">清空</van-button>
|
|
21
|
+
<van-button size="small" type="primary" @click="handleExportJSON">导出JSON</van-button>
|
|
22
|
+
<van-button size="small" type="primary" @click="handleExportText">导出TXT</van-button>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- 日志列表 -->
|
|
27
|
+
<div class="logs-container">
|
|
28
|
+
<van-empty v-if="filteredLogs.length === 0" description="暂无错误日志" />
|
|
29
|
+
<div v-else class="logs-list">
|
|
30
|
+
<div
|
|
31
|
+
v-for="log in filteredLogs"
|
|
32
|
+
:key="log.id"
|
|
33
|
+
class="log-item"
|
|
34
|
+
:class="`log-${log.type}`"
|
|
35
|
+
@click="toggleLogDetail(log)"
|
|
36
|
+
>
|
|
37
|
+
<div class="log-header">
|
|
38
|
+
<div class="log-type-badge" :class="`badge-${log.type}`">
|
|
39
|
+
{{ getTypeLabel(log.type) }}
|
|
40
|
+
</div>
|
|
41
|
+
<div class="log-time">{{ log.time }}</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="log-message">{{ log.message || '无错误信息' }}</div>
|
|
44
|
+
<div v-if="log.type === 'api'" class="log-api-info">
|
|
45
|
+
<span class="method">{{ log.method }}</span>
|
|
46
|
+
<span class="url">{{ log.url }}</span>
|
|
47
|
+
<span v-if="log.status" class="status" :class="getStatusClass(log.status)">
|
|
48
|
+
{{ log.status }} {{ log.statusText }}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div v-if="expandedLogs[log.id]" class="log-detail">
|
|
52
|
+
<div class="detail-row">
|
|
53
|
+
<span class="detail-label">页面URL:</span>
|
|
54
|
+
<span class="detail-value">{{ log.pageUrl }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div v-if="log.type === 'api'" class="detail-row">
|
|
57
|
+
<span class="detail-label">请求方法:</span>
|
|
58
|
+
<span class="detail-value">{{ log.method }}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div v-if="log.type === 'api'" class="detail-row">
|
|
61
|
+
<span class="detail-label">请求URL:</span>
|
|
62
|
+
<span class="detail-value">{{ log.url }}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div v-if="log.type === 'api' && Object.keys(log.params || {}).length > 0" class="detail-row">
|
|
65
|
+
<span class="detail-label">请求参数:</span>
|
|
66
|
+
<pre class="detail-value">{{ JSON.stringify(log.params, null, 2) }}</pre>
|
|
67
|
+
</div>
|
|
68
|
+
<div v-if="log.fileName" class="detail-row">
|
|
69
|
+
<span class="detail-label">文件:</span>
|
|
70
|
+
<span class="detail-value">{{ log.fileName }}:{{ log.lineNumber }}:{{ log.columnNumber }}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div v-if="log.componentName" class="detail-row">
|
|
73
|
+
<span class="detail-label">组件:</span>
|
|
74
|
+
<span class="detail-value">{{ log.componentName }}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div v-if="log.stack" class="detail-row">
|
|
77
|
+
<span class="detail-label">堆栈信息:</span>
|
|
78
|
+
<pre class="detail-value stack">{{ log.stack }}</pre>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="detail-row">
|
|
81
|
+
<span class="detail-label">用户代理:</span>
|
|
82
|
+
<span class="detail-value small">{{ log.userAgent }}</span>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup>
|
|
92
|
+
import { ref, computed, onMounted } from 'vue'
|
|
93
|
+
import { showConfirmDialog, showToast, showSuccessToast, showFailToast } from 'vant'
|
|
94
|
+
import errorMonitor from '@/utils/errorMonitor'
|
|
95
|
+
|
|
96
|
+
const filterType = ref('')
|
|
97
|
+
const expandedLogs = ref({})
|
|
98
|
+
const statistics = ref({
|
|
99
|
+
total: 0,
|
|
100
|
+
api: 0,
|
|
101
|
+
page: 0,
|
|
102
|
+
promise: 0,
|
|
103
|
+
today: 0,
|
|
104
|
+
thisWeek: 0,
|
|
105
|
+
thisMonth: 0
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const allLogs = ref([])
|
|
109
|
+
const filteredLogs = computed(() => {
|
|
110
|
+
if (!filterType.value) {
|
|
111
|
+
return allLogs.value
|
|
112
|
+
}
|
|
113
|
+
return allLogs.value.filter(log => log.type === filterType.value)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// 获取统计标签
|
|
117
|
+
const getStatLabel = (key) => {
|
|
118
|
+
const labels = {
|
|
119
|
+
total: '总计',
|
|
120
|
+
api: '接口错误',
|
|
121
|
+
page: '页面错误',
|
|
122
|
+
promise: 'Promise错误',
|
|
123
|
+
today: '今日',
|
|
124
|
+
thisWeek: '本周',
|
|
125
|
+
thisMonth: '本月'
|
|
126
|
+
}
|
|
127
|
+
return labels[key] || key
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 获取类型标签
|
|
131
|
+
const getTypeLabel = (type) => {
|
|
132
|
+
const labels = {
|
|
133
|
+
api: '接口',
|
|
134
|
+
page: '页面',
|
|
135
|
+
promise: 'Promise'
|
|
136
|
+
}
|
|
137
|
+
return labels[type] || type
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 获取状态样式类
|
|
141
|
+
const getStatusClass = (status) => {
|
|
142
|
+
if (status >= 200 && status < 300) return 'status-success'
|
|
143
|
+
if (status >= 400 && status < 500) return 'status-client-error'
|
|
144
|
+
if (status >= 500) return 'status-server-error'
|
|
145
|
+
return 'status-unknown'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 切换日志详情
|
|
149
|
+
const toggleLogDetail = (log) => {
|
|
150
|
+
expandedLogs.value[log.id] = !expandedLogs.value[log.id]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 筛选改变
|
|
154
|
+
const handleFilterChange = () => {
|
|
155
|
+
expandedLogs.value = {}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 加载日志
|
|
159
|
+
const loadLogs = async () => {
|
|
160
|
+
try {
|
|
161
|
+
const [logs, stats] = await Promise.all([
|
|
162
|
+
errorMonitor.getLogs(),
|
|
163
|
+
errorMonitor.getStatistics()
|
|
164
|
+
])
|
|
165
|
+
allLogs.value = logs
|
|
166
|
+
statistics.value = stats
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('加载日志失败:', error)
|
|
169
|
+
showFailToast('加载日志失败')
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 清空日志
|
|
174
|
+
const handleClear = async () => {
|
|
175
|
+
try {
|
|
176
|
+
await showConfirmDialog({
|
|
177
|
+
title: '确认清空',
|
|
178
|
+
message: '确定要清空所有错误日志吗?此操作不可恢复。'
|
|
179
|
+
})
|
|
180
|
+
await errorMonitor.clearLogs()
|
|
181
|
+
await loadLogs()
|
|
182
|
+
showSuccessToast('日志已清空')
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error !== 'cancel') {
|
|
185
|
+
console.error('清空日志失败:', error)
|
|
186
|
+
showFailToast('清空日志失败')
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 导出JSON
|
|
192
|
+
const handleExportJSON = async () => {
|
|
193
|
+
try {
|
|
194
|
+
await errorMonitor.exportLogs()
|
|
195
|
+
showSuccessToast('导出成功')
|
|
196
|
+
} catch (error) {
|
|
197
|
+
showFailToast('导出失败')
|
|
198
|
+
console.error('导出失败:', error)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 导出TXT
|
|
203
|
+
const handleExportText = async () => {
|
|
204
|
+
try {
|
|
205
|
+
await errorMonitor.exportLogsAsText()
|
|
206
|
+
showSuccessToast('导出成功')
|
|
207
|
+
} catch (error) {
|
|
208
|
+
showFailToast('导出失败')
|
|
209
|
+
console.error('导出失败:', error)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
onMounted(() => {
|
|
214
|
+
loadLogs()
|
|
215
|
+
})
|
|
216
|
+
</script>
|
|
217
|
+
|
|
218
|
+
<style lang="scss" scoped>
|
|
219
|
+
.error-log-page {
|
|
220
|
+
padding: 16px;
|
|
221
|
+
background-color: #f5f5f5;
|
|
222
|
+
min-height: 100vh;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.stats-section {
|
|
226
|
+
display: grid;
|
|
227
|
+
grid-template-columns: repeat(2, 1fr);
|
|
228
|
+
gap: 12px;
|
|
229
|
+
margin-bottom: 16px;
|
|
230
|
+
|
|
231
|
+
.stat-card {
|
|
232
|
+
background: #fff;
|
|
233
|
+
border-radius: 8px;
|
|
234
|
+
padding: 16px;
|
|
235
|
+
text-align: center;
|
|
236
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
237
|
+
|
|
238
|
+
.stat-label {
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
color: #666;
|
|
241
|
+
margin-bottom: 8px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.stat-value {
|
|
245
|
+
font-size: 24px;
|
|
246
|
+
font-weight: bold;
|
|
247
|
+
color: #333;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.toolbar {
|
|
253
|
+
background: #fff;
|
|
254
|
+
border-radius: 8px;
|
|
255
|
+
padding: 12px;
|
|
256
|
+
margin-bottom: 16px;
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
gap: 12px;
|
|
260
|
+
|
|
261
|
+
.actions {
|
|
262
|
+
display: flex;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
justify-content: flex-end;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.logs-container {
|
|
269
|
+
.logs-list {
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-direction: column;
|
|
272
|
+
gap: 12px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.log-item {
|
|
276
|
+
background: #fff;
|
|
277
|
+
border-radius: 8px;
|
|
278
|
+
padding: 16px;
|
|
279
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
transition: all 0.2s;
|
|
282
|
+
|
|
283
|
+
&:active {
|
|
284
|
+
transform: scale(0.98);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.log-header {
|
|
288
|
+
display: flex;
|
|
289
|
+
justify-content: space-between;
|
|
290
|
+
align-items: center;
|
|
291
|
+
margin-bottom: 8px;
|
|
292
|
+
|
|
293
|
+
.log-type-badge {
|
|
294
|
+
display: inline-block;
|
|
295
|
+
padding: 4px 8px;
|
|
296
|
+
border-radius: 4px;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
font-weight: 500;
|
|
299
|
+
color: #fff;
|
|
300
|
+
|
|
301
|
+
&.badge-api {
|
|
302
|
+
background-color: #ff6b6b;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
&.badge-page {
|
|
306
|
+
background-color: #4ecdc4;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
&.badge-promise {
|
|
310
|
+
background-color: #95e1d3;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.log-time {
|
|
315
|
+
font-size: 12px;
|
|
316
|
+
color: #999;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.log-message {
|
|
321
|
+
font-size: 14px;
|
|
322
|
+
color: #333;
|
|
323
|
+
margin-bottom: 8px;
|
|
324
|
+
word-break: break-all;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.log-api-info {
|
|
328
|
+
display: flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
gap: 8px;
|
|
331
|
+
flex-wrap: wrap;
|
|
332
|
+
font-size: 12px;
|
|
333
|
+
|
|
334
|
+
.method {
|
|
335
|
+
padding: 2px 6px;
|
|
336
|
+
background-color: #f0f0f0;
|
|
337
|
+
border-radius: 3px;
|
|
338
|
+
font-weight: 500;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.url {
|
|
342
|
+
color: #666;
|
|
343
|
+
flex: 1;
|
|
344
|
+
word-break: break-all;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.status {
|
|
348
|
+
padding: 2px 6px;
|
|
349
|
+
border-radius: 3px;
|
|
350
|
+
font-weight: 500;
|
|
351
|
+
|
|
352
|
+
&.status-success {
|
|
353
|
+
background-color: #d4edda;
|
|
354
|
+
color: #155724;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
&.status-client-error {
|
|
358
|
+
background-color: #f8d7da;
|
|
359
|
+
color: #721c24;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
&.status-server-error {
|
|
363
|
+
background-color: #f5c6cb;
|
|
364
|
+
color: #721c24;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
&.status-unknown {
|
|
368
|
+
background-color: #fff3cd;
|
|
369
|
+
color: #856404;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.log-detail {
|
|
375
|
+
margin-top: 12px;
|
|
376
|
+
padding-top: 12px;
|
|
377
|
+
border-top: 1px solid #f0f0f0;
|
|
378
|
+
|
|
379
|
+
.detail-row {
|
|
380
|
+
margin-bottom: 8px;
|
|
381
|
+
|
|
382
|
+
.detail-label {
|
|
383
|
+
font-size: 12px;
|
|
384
|
+
color: #999;
|
|
385
|
+
margin-right: 8px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.detail-value {
|
|
389
|
+
font-size: 12px;
|
|
390
|
+
color: #666;
|
|
391
|
+
word-break: break-all;
|
|
392
|
+
|
|
393
|
+
&.small {
|
|
394
|
+
font-size: 11px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
&.stack {
|
|
398
|
+
background-color: #f5f5f5;
|
|
399
|
+
padding: 8px;
|
|
400
|
+
border-radius: 4px;
|
|
401
|
+
white-space: pre-wrap;
|
|
402
|
+
font-family: monospace;
|
|
403
|
+
max-height: 200px;
|
|
404
|
+
overflow-y: auto;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
pre {
|
|
409
|
+
margin: 4px 0;
|
|
410
|
+
white-space: pre-wrap;
|
|
411
|
+
font-size: 12px;
|
|
412
|
+
background-color: #f5f5f5;
|
|
413
|
+
padding: 8px;
|
|
414
|
+
border-radius: 4px;
|
|
415
|
+
overflow-x: auto;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
</style>
|
|
422
|
+
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误监控工具
|
|
3
|
+
* 用于收集接口错误和前端页面错误,发送到后端服务器
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios from 'axios'
|
|
7
|
+
|
|
8
|
+
class ErrorMonitor {
|
|
9
|
+
constructor() {
|
|
10
|
+
// 错误日志发送到后端的接口地址,可以通过环境变量配置
|
|
11
|
+
this.apiUrl = import.meta.env.VITE_ERROR_LOG_API || '/api/error-logs'
|
|
12
|
+
// 是否启用错误监控
|
|
13
|
+
this.enabled = true
|
|
14
|
+
// 发送失败的日志队列(用于重试)
|
|
15
|
+
this.failedLogs = []
|
|
16
|
+
// 最大重试次数
|
|
17
|
+
this.maxRetries = 3
|
|
18
|
+
// 创建独立的 axios 实例用于发送错误日志,避免循环依赖
|
|
19
|
+
this.httpClient = axios.create({
|
|
20
|
+
baseURL: import.meta.env.VITE_API_BASE_URL || '',
|
|
21
|
+
timeout: 5000,
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json;charset=UTF-8'
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 记录接口错误
|
|
30
|
+
* @param {Object} errorInfo - 错误信息
|
|
31
|
+
*/
|
|
32
|
+
logApiError(errorInfo) {
|
|
33
|
+
const log = {
|
|
34
|
+
id: this.generateId(),
|
|
35
|
+
type: 'api', // 错误类型:接口错误
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
time: new Date().toLocaleString('zh-CN'),
|
|
38
|
+
url: errorInfo.url || '',
|
|
39
|
+
method: errorInfo.method || 'GET',
|
|
40
|
+
params: errorInfo.params || {},
|
|
41
|
+
data: errorInfo.data || {},
|
|
42
|
+
status: errorInfo.status || 0,
|
|
43
|
+
statusText: errorInfo.statusText || '',
|
|
44
|
+
message: errorInfo.message || '',
|
|
45
|
+
stack: errorInfo.stack || '',
|
|
46
|
+
userAgent: navigator.userAgent,
|
|
47
|
+
pageUrl: window.location.href,
|
|
48
|
+
...errorInfo
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.addLog(log)
|
|
52
|
+
this.consoleError('接口错误', log)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 记录前端页面错误
|
|
57
|
+
* @param {Object} errorInfo - 错误信息
|
|
58
|
+
*/
|
|
59
|
+
logPageError(errorInfo) {
|
|
60
|
+
const log = {
|
|
61
|
+
id: this.generateId(),
|
|
62
|
+
type: 'page', // 错误类型:页面错误
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
time: new Date().toLocaleString('zh-CN'),
|
|
65
|
+
message: errorInfo.message || '',
|
|
66
|
+
stack: errorInfo.stack || '',
|
|
67
|
+
fileName: errorInfo.fileName || '',
|
|
68
|
+
lineNumber: errorInfo.lineNumber || 0,
|
|
69
|
+
columnNumber: errorInfo.columnNumber || 0,
|
|
70
|
+
componentName: errorInfo.componentName || '',
|
|
71
|
+
props: errorInfo.props || {},
|
|
72
|
+
userAgent: navigator.userAgent,
|
|
73
|
+
pageUrl: window.location.href,
|
|
74
|
+
...errorInfo
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.addLog(log)
|
|
78
|
+
this.consoleError('页面错误', log)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 记录Promise未捕获错误
|
|
83
|
+
* @param {Object} errorInfo - 错误信息
|
|
84
|
+
*/
|
|
85
|
+
logPromiseError(errorInfo) {
|
|
86
|
+
const log = {
|
|
87
|
+
id: this.generateId(),
|
|
88
|
+
type: 'promise', // 错误类型:Promise错误
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
time: new Date().toLocaleString('zh-CN'),
|
|
91
|
+
message: errorInfo.message || '',
|
|
92
|
+
stack: errorInfo.stack || '',
|
|
93
|
+
reason: errorInfo.reason || '',
|
|
94
|
+
userAgent: navigator.userAgent,
|
|
95
|
+
pageUrl: window.location.href,
|
|
96
|
+
...errorInfo
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.addLog(log)
|
|
100
|
+
this.consoleError('Promise错误', log)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 添加日志(直接发送到后端,不存储在前端)
|
|
105
|
+
* @param {Object} log - 日志对象
|
|
106
|
+
*/
|
|
107
|
+
addLog(log) {
|
|
108
|
+
// 如果错误监控未启用,直接返回
|
|
109
|
+
if (!this.enabled) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 控制台输出错误(开发环境)
|
|
114
|
+
this.consoleError(log.type === 'api' ? '接口错误' : log.type === 'page' ? '页面错误' : 'Promise错误', log)
|
|
115
|
+
|
|
116
|
+
// 直接发送到服务器
|
|
117
|
+
this.sendToServer(log).catch(error => {
|
|
118
|
+
// 如果发送失败,加入失败队列等待重试
|
|
119
|
+
console.error('发送错误日志到服务器失败:', error)
|
|
120
|
+
this.failedLogs.push({
|
|
121
|
+
log,
|
|
122
|
+
retries: 0
|
|
123
|
+
})
|
|
124
|
+
// 尝试重试
|
|
125
|
+
this.retryFailedLogs()
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 控制台输出错误(开发环境)
|
|
131
|
+
*/
|
|
132
|
+
consoleError(title, log) {
|
|
133
|
+
if (import.meta.env.DEV) {
|
|
134
|
+
console.group(`[错误监控] ${title}`)
|
|
135
|
+
console.error(log)
|
|
136
|
+
console.groupEnd()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 生成唯一ID
|
|
142
|
+
*/
|
|
143
|
+
generateId() {
|
|
144
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 从后端获取所有日志(需要后端接口支持)
|
|
149
|
+
*/
|
|
150
|
+
async getLogs(type = null, params = {}) {
|
|
151
|
+
try {
|
|
152
|
+
const response = await this.httpClient.get(this.apiUrl, {
|
|
153
|
+
params: {
|
|
154
|
+
type,
|
|
155
|
+
...params
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
return response?.data?.data || response?.data || response || []
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('获取错误日志失败:', error)
|
|
161
|
+
return []
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 获取接口错误日志
|
|
167
|
+
*/
|
|
168
|
+
async getApiLogs(params = {}) {
|
|
169
|
+
return this.getLogs('api', params)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 获取页面错误日志
|
|
174
|
+
*/
|
|
175
|
+
async getPageLogs(params = {}) {
|
|
176
|
+
return this.getLogs('page', params)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 获取Promise错误日志
|
|
181
|
+
*/
|
|
182
|
+
async getPromiseLogs(params = {}) {
|
|
183
|
+
return this.getLogs('promise', params)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 导出日志为JSON(从后端获取)
|
|
188
|
+
*/
|
|
189
|
+
async exportLogs() {
|
|
190
|
+
try {
|
|
191
|
+
const logs = await this.getLogs()
|
|
192
|
+
const dataStr = JSON.stringify(logs, null, 2)
|
|
193
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
|
194
|
+
const url = URL.createObjectURL(dataBlob)
|
|
195
|
+
const link = document.createElement('a')
|
|
196
|
+
link.href = url
|
|
197
|
+
link.download = `error-logs-${new Date().toISOString().split('T')[0]}.json`
|
|
198
|
+
document.body.appendChild(link)
|
|
199
|
+
link.click()
|
|
200
|
+
document.body.removeChild(link)
|
|
201
|
+
URL.revokeObjectURL(url)
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error('导出日志失败:', error)
|
|
204
|
+
throw error
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 导出日志为文本(从后端获取)
|
|
210
|
+
*/
|
|
211
|
+
async exportLogsAsText() {
|
|
212
|
+
try {
|
|
213
|
+
const logs = await this.getLogs()
|
|
214
|
+
let text = '错误日志导出\n'
|
|
215
|
+
text += `导出时间: ${new Date().toLocaleString('zh-CN')}\n`
|
|
216
|
+
text += `总计: ${logs.length} 条\n\n`
|
|
217
|
+
text += '='.repeat(80) + '\n\n'
|
|
218
|
+
|
|
219
|
+
logs.reverse().forEach((log, index) => {
|
|
220
|
+
text += `[${index + 1}] ${log.type.toUpperCase()} 错误\n`
|
|
221
|
+
text += `时间: ${log.time}\n`
|
|
222
|
+
text += `页面: ${log.pageUrl}\n`
|
|
223
|
+
|
|
224
|
+
if (log.type === 'api') {
|
|
225
|
+
text += `接口: ${log.method} ${log.url}\n`
|
|
226
|
+
text += `状态: ${log.status} ${log.statusText}\n`
|
|
227
|
+
if (log.params && Object.keys(log.params).length > 0) {
|
|
228
|
+
text += `参数: ${JSON.stringify(log.params, null, 2)}\n`
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
if (log.fileName) {
|
|
232
|
+
text += `文件: ${log.fileName}:${log.lineNumber}:${log.columnNumber}\n`
|
|
233
|
+
}
|
|
234
|
+
if (log.componentName) {
|
|
235
|
+
text += `组件: ${log.componentName}\n`
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
text += `错误信息: ${log.message}\n`
|
|
240
|
+
if (log.stack) {
|
|
241
|
+
text += `堆栈信息:\n${log.stack}\n`
|
|
242
|
+
}
|
|
243
|
+
text += '\n' + '-'.repeat(80) + '\n\n'
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const dataBlob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
|
247
|
+
const url = URL.createObjectURL(dataBlob)
|
|
248
|
+
const link = document.createElement('a')
|
|
249
|
+
link.href = url
|
|
250
|
+
link.download = `error-logs-${new Date().toISOString().split('T')[0]}.txt`
|
|
251
|
+
document.body.appendChild(link)
|
|
252
|
+
link.click()
|
|
253
|
+
document.body.removeChild(link)
|
|
254
|
+
URL.revokeObjectURL(url)
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('导出日志失败:', error)
|
|
257
|
+
throw error
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 发送日志到服务器
|
|
263
|
+
* @param {Object} log - 日志对象
|
|
264
|
+
*/
|
|
265
|
+
async sendToServer(log) {
|
|
266
|
+
try {
|
|
267
|
+
// 使用独立的 httpClient 发送错误日志到后端,避免循环依赖
|
|
268
|
+
await this.httpClient.post(this.apiUrl, log)
|
|
269
|
+
return true
|
|
270
|
+
} catch (error) {
|
|
271
|
+
// 如果发送失败,不记录错误(避免循环记录)
|
|
272
|
+
// 但会在 addLog 中加入失败队列
|
|
273
|
+
throw error
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 重试发送失败的日志
|
|
279
|
+
*/
|
|
280
|
+
async retryFailedLogs() {
|
|
281
|
+
if (this.failedLogs.length === 0) {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 延迟重试,避免频繁请求
|
|
286
|
+
if (this.retryTimer) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.retryTimer = setTimeout(async () => {
|
|
291
|
+
this.retryTimer = null
|
|
292
|
+
const logsToRetry = [...this.failedLogs]
|
|
293
|
+
this.failedLogs = []
|
|
294
|
+
|
|
295
|
+
for (const item of logsToRetry) {
|
|
296
|
+
if (item.retries >= this.maxRetries) {
|
|
297
|
+
// 超过最大重试次数,放弃
|
|
298
|
+
console.error('错误日志发送失败,已超过最大重试次数:', item.log)
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await this.sendToServer(item.log)
|
|
304
|
+
// 发送成功,不再处理
|
|
305
|
+
} catch (error) {
|
|
306
|
+
// 发送失败,增加重试次数并重新加入队列
|
|
307
|
+
item.retries++
|
|
308
|
+
this.failedLogs.push(item)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 如果还有失败的日志,继续重试
|
|
313
|
+
if (this.failedLogs.length > 0) {
|
|
314
|
+
setTimeout(() => this.retryFailedLogs(), 10000) // 10秒后重试
|
|
315
|
+
}
|
|
316
|
+
}, 2000) // 2秒后开始重试
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 获取错误统计信息(从后端获取)
|
|
321
|
+
*/
|
|
322
|
+
async getStatistics() {
|
|
323
|
+
try {
|
|
324
|
+
const response = await this.httpClient.get(`${this.apiUrl}/statistics`)
|
|
325
|
+
return response?.data?.data || response?.data || response || {
|
|
326
|
+
total: 0,
|
|
327
|
+
api: 0,
|
|
328
|
+
page: 0,
|
|
329
|
+
promise: 0,
|
|
330
|
+
today: 0,
|
|
331
|
+
thisWeek: 0,
|
|
332
|
+
thisMonth: 0
|
|
333
|
+
}
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.error('获取错误统计失败:', error)
|
|
336
|
+
// 返回默认统计信息
|
|
337
|
+
return {
|
|
338
|
+
total: 0,
|
|
339
|
+
api: 0,
|
|
340
|
+
page: 0,
|
|
341
|
+
promise: 0,
|
|
342
|
+
today: 0,
|
|
343
|
+
thisWeek: 0,
|
|
344
|
+
thisMonth: 0
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 启用/禁用错误监控
|
|
351
|
+
* @param {Boolean} enabled - 是否启用
|
|
352
|
+
*/
|
|
353
|
+
setEnabled(enabled) {
|
|
354
|
+
this.enabled = enabled
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 清空日志(请求后端接口)
|
|
359
|
+
*/
|
|
360
|
+
async clearLogs() {
|
|
361
|
+
try {
|
|
362
|
+
await this.httpClient.delete(this.apiUrl)
|
|
363
|
+
return true
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('清空错误日志失败:', error)
|
|
366
|
+
throw error
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 创建单例
|
|
372
|
+
const errorMonitor = new ErrorMonitor()
|
|
373
|
+
|
|
374
|
+
export default errorMonitor
|
|
375
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import errorMonitor from '@/utils/errorMonitor'
|
|
3
|
+
|
|
4
|
+
// 创建 axios 实例
|
|
5
|
+
const request = axios.create({
|
|
6
|
+
baseURL: import.meta.env.VITE_API_BASE_URL || '', // 接口基础地址
|
|
7
|
+
timeout: 10000, // 请求超时时间
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json;charset=UTF-8'
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// 请求拦截器
|
|
14
|
+
request.interceptors.request.use(
|
|
15
|
+
config => {
|
|
16
|
+
// 在发送请求之前做些什么
|
|
17
|
+
// 例如:添加token
|
|
18
|
+
// const token = localStorage.getItem('token')
|
|
19
|
+
// if (token) {
|
|
20
|
+
// config.headers.Authorization = `Bearer ${token}`
|
|
21
|
+
// }
|
|
22
|
+
return config
|
|
23
|
+
},
|
|
24
|
+
error => {
|
|
25
|
+
// 对请求错误做些什么
|
|
26
|
+
console.error('请求错误:', error)
|
|
27
|
+
|
|
28
|
+
// 如果请求标记了跳过错误日志,则不记录
|
|
29
|
+
if (error.config?.__skipErrorLog) {
|
|
30
|
+
return Promise.reject(error)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 记录接口错误
|
|
34
|
+
errorMonitor.logApiError({
|
|
35
|
+
url: error.config?.url || '',
|
|
36
|
+
method: error.config?.method?.toUpperCase() || 'GET',
|
|
37
|
+
params: error.config?.params || {},
|
|
38
|
+
data: error.config?.data || {},
|
|
39
|
+
message: error.message || '请求错误',
|
|
40
|
+
stack: error.stack || ''
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return Promise.reject(error)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// 响应拦截器
|
|
48
|
+
request.interceptors.response.use(
|
|
49
|
+
response => {
|
|
50
|
+
// 对响应数据做点什么
|
|
51
|
+
const { data } = response
|
|
52
|
+
|
|
53
|
+
// 如果业务代码返回错误,也可以在这里记录
|
|
54
|
+
// 例如:if (data.code !== 200) { ... }
|
|
55
|
+
|
|
56
|
+
return data
|
|
57
|
+
},
|
|
58
|
+
error => {
|
|
59
|
+
// 对响应错误做点什么
|
|
60
|
+
console.error('响应错误:', error)
|
|
61
|
+
|
|
62
|
+
// 如果请求标记了跳过错误日志,则不记录(避免循环记录错误日志请求本身的错误)
|
|
63
|
+
if (error.config?.__skipErrorLog) {
|
|
64
|
+
return Promise.reject(error)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 记录接口错误
|
|
68
|
+
const errorInfo = {
|
|
69
|
+
url: error.config?.url || '',
|
|
70
|
+
method: error.config?.method?.toUpperCase() || 'GET',
|
|
71
|
+
params: error.config?.params || {},
|
|
72
|
+
data: error.config?.data || {},
|
|
73
|
+
message: error.message || '响应错误',
|
|
74
|
+
stack: error.stack || ''
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 如果是HTTP错误响应
|
|
78
|
+
if (error.response) {
|
|
79
|
+
errorInfo.status = error.response.status
|
|
80
|
+
errorInfo.statusText = error.response.statusText
|
|
81
|
+
errorInfo.responseData = error.response.data
|
|
82
|
+
} else if (error.request) {
|
|
83
|
+
// 请求已发出但没有收到响应
|
|
84
|
+
errorInfo.status = 0
|
|
85
|
+
errorInfo.statusText = '网络错误或无响应'
|
|
86
|
+
errorInfo.message = '网络错误,请检查网络连接'
|
|
87
|
+
} else {
|
|
88
|
+
// 其他错误
|
|
89
|
+
errorInfo.status = 0
|
|
90
|
+
errorInfo.statusText = '请求配置错误'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
errorMonitor.logApiError(errorInfo)
|
|
94
|
+
|
|
95
|
+
return Promise.reject(error)
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
export default request
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="test-error-page">
|
|
3
|
+
<div class="header">
|
|
4
|
+
<h2>错误捕获测试页面</h2>
|
|
5
|
+
<van-button
|
|
6
|
+
type="primary"
|
|
7
|
+
size="small"
|
|
8
|
+
@click="goToLogPage"
|
|
9
|
+
>
|
|
10
|
+
查看错误日志
|
|
11
|
+
</van-button>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- 统计信息 -->
|
|
15
|
+
<div class="stats-card">
|
|
16
|
+
<div class="stat-item">
|
|
17
|
+
<div class="stat-label">总计</div>
|
|
18
|
+
<div class="stat-value">{{ statistics.total }}</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="stat-item">
|
|
21
|
+
<div class="stat-label">接口错误</div>
|
|
22
|
+
<div class="stat-value">{{ statistics.api }}</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="stat-item">
|
|
25
|
+
<div class="stat-label">页面错误</div>
|
|
26
|
+
<div class="stat-value">{{ statistics.page }}</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="stat-item">
|
|
29
|
+
<div class="stat-label">Promise错误</div>
|
|
30
|
+
<div class="stat-value">{{ statistics.promise }}</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- 接口错误测试 -->
|
|
35
|
+
<div class="test-section">
|
|
36
|
+
<div class="section-title">接口错误测试</div>
|
|
37
|
+
<div class="button-group">
|
|
38
|
+
<van-button
|
|
39
|
+
type="primary"
|
|
40
|
+
:loading="loading"
|
|
41
|
+
@click="fetchNoticeList"
|
|
42
|
+
>
|
|
43
|
+
正常请求(返回noticeList)
|
|
44
|
+
</van-button>
|
|
45
|
+
<van-button
|
|
46
|
+
type="danger"
|
|
47
|
+
@click="test404Error"
|
|
48
|
+
>
|
|
49
|
+
测试404错误
|
|
50
|
+
</van-button>
|
|
51
|
+
<van-button
|
|
52
|
+
type="danger"
|
|
53
|
+
@click="test500Error"
|
|
54
|
+
>
|
|
55
|
+
测试500错误
|
|
56
|
+
</van-button>
|
|
57
|
+
<van-button
|
|
58
|
+
type="warning"
|
|
59
|
+
@click="testNetworkError"
|
|
60
|
+
>
|
|
61
|
+
测试网络错误
|
|
62
|
+
</van-button>
|
|
63
|
+
<van-button
|
|
64
|
+
type="warning"
|
|
65
|
+
@click="testTimeoutError"
|
|
66
|
+
>
|
|
67
|
+
测试超时错误
|
|
68
|
+
</van-button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- 请求结果显示 -->
|
|
72
|
+
<div v-if="requestResult" class="result-box">
|
|
73
|
+
<div class="result-title">请求结果:</div>
|
|
74
|
+
<pre class="result-content">{{ requestResult }}</pre>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- 页面错误测试 -->
|
|
79
|
+
<div class="test-section">
|
|
80
|
+
<div class="section-title">页面错误测试</div>
|
|
81
|
+
<div class="button-group">
|
|
82
|
+
<van-button
|
|
83
|
+
type="danger"
|
|
84
|
+
@click="testPageError"
|
|
85
|
+
>
|
|
86
|
+
测试JavaScript错误
|
|
87
|
+
</van-button>
|
|
88
|
+
<van-button
|
|
89
|
+
type="danger"
|
|
90
|
+
@click="testReferenceError"
|
|
91
|
+
>
|
|
92
|
+
测试引用错误
|
|
93
|
+
</van-button>
|
|
94
|
+
<van-button
|
|
95
|
+
type="danger"
|
|
96
|
+
@click="testTypeError"
|
|
97
|
+
>
|
|
98
|
+
测试类型错误
|
|
99
|
+
</van-button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Promise错误测试 -->
|
|
104
|
+
<div class="test-section">
|
|
105
|
+
<div class="section-title">Promise错误测试</div>
|
|
106
|
+
<div class="button-group">
|
|
107
|
+
<van-button
|
|
108
|
+
type="warning"
|
|
109
|
+
@click="testPromiseError"
|
|
110
|
+
>
|
|
111
|
+
测试Promise未捕获错误
|
|
112
|
+
</van-button>
|
|
113
|
+
<van-button
|
|
114
|
+
type="warning"
|
|
115
|
+
@click="testAsyncError"
|
|
116
|
+
>
|
|
117
|
+
测试Async函数错误
|
|
118
|
+
</van-button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- Vue组件错误测试 -->
|
|
123
|
+
<div class="test-section">
|
|
124
|
+
<div class="section-title">Vue组件错误测试</div>
|
|
125
|
+
<div class="button-group">
|
|
126
|
+
<van-button
|
|
127
|
+
type="danger"
|
|
128
|
+
@click="testVueError"
|
|
129
|
+
>
|
|
130
|
+
测试Vue组件渲染错误
|
|
131
|
+
</van-button>
|
|
132
|
+
</div>
|
|
133
|
+
<!-- 用于触发Vue错误的组件 -->
|
|
134
|
+
<div v-if="shouldRenderError">
|
|
135
|
+
{{ undefinedVariable.property }}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- 刷新统计按钮 -->
|
|
140
|
+
<div class="refresh-section">
|
|
141
|
+
<van-button
|
|
142
|
+
type="default"
|
|
143
|
+
block
|
|
144
|
+
@click="refreshStatistics"
|
|
145
|
+
>
|
|
146
|
+
刷新统计信息
|
|
147
|
+
</van-button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</template>
|
|
151
|
+
|
|
152
|
+
<script setup>
|
|
153
|
+
import { ref, onMounted } from 'vue'
|
|
154
|
+
import { useRouter } from 'vue-router'
|
|
155
|
+
import { showToast, showFailToast, showSuccessToast } from 'vant'
|
|
156
|
+
import request from '@/api/request'
|
|
157
|
+
import errorMonitor from '@/utils/errorMonitor'
|
|
158
|
+
|
|
159
|
+
const router = useRouter()
|
|
160
|
+
const loading = ref(false)
|
|
161
|
+
const requestResult = ref('')
|
|
162
|
+
const shouldRenderError = ref(false)
|
|
163
|
+
const statistics = ref({
|
|
164
|
+
total: 0,
|
|
165
|
+
api: 0,
|
|
166
|
+
page: 0,
|
|
167
|
+
promise: 0
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// 刷新统计信息
|
|
171
|
+
const refreshStatistics = async () => {
|
|
172
|
+
try {
|
|
173
|
+
statistics.value = await errorMonitor.getStatistics()
|
|
174
|
+
showToast('统计信息已刷新')
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error('刷新统计信息失败:', error)
|
|
177
|
+
showToast('刷新统计信息失败')
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 跳转到日志页面
|
|
182
|
+
const goToLogPage = () => {
|
|
183
|
+
router.push('/error-log')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============ 接口错误测试 ============
|
|
187
|
+
|
|
188
|
+
// 正常请求 - 获取noticeList
|
|
189
|
+
const fetchNoticeList = async () => {
|
|
190
|
+
loading.value = true
|
|
191
|
+
requestResult.value = ''
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// 模拟一个正常的接口请求
|
|
195
|
+
const response = await request.get('/api/notice/list', {
|
|
196
|
+
params: {
|
|
197
|
+
page: 1,
|
|
198
|
+
pageSize: 10
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// 如果请求成功,显示结果
|
|
203
|
+
if (response && response.data) {
|
|
204
|
+
requestResult.value = JSON.stringify(response.data, null, 2)
|
|
205
|
+
showSuccessToast('请求成功,数据已更新')
|
|
206
|
+
} else {
|
|
207
|
+
// 模拟返回的数据
|
|
208
|
+
const mockData = [
|
|
209
|
+
{ id: 1, title: '通知公告1', content: '这是通知公告1的内容' },
|
|
210
|
+
{ id: 2, title: '通知公告2', content: '这是通知公告2的内容' },
|
|
211
|
+
{ id: 3, title: '通知公告3', content: '这是通知公告3的内容' },
|
|
212
|
+
{ id: 4, title: '通知公告4', content: '这是通知公告4的内容' },
|
|
213
|
+
{ id: 5, title: '通知公告5', content: '这是通知公告5的内容' }
|
|
214
|
+
]
|
|
215
|
+
requestResult.value = JSON.stringify(mockData, null, 2)
|
|
216
|
+
showSuccessToast('使用模拟数据(接口不存在)')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
refreshStatistics()
|
|
220
|
+
} catch (error) {
|
|
221
|
+
// 错误会被错误监控自动捕获
|
|
222
|
+
requestResult.value = `错误: ${error.message || '未知错误'}`
|
|
223
|
+
showFailToast('请求失败:' + (error.message || '未知错误'))
|
|
224
|
+
refreshStatistics()
|
|
225
|
+
} finally {
|
|
226
|
+
loading.value = false
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 测试404错误
|
|
231
|
+
const test404Error = async () => {
|
|
232
|
+
loading.value = true
|
|
233
|
+
requestResult.value = ''
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await request.get('/api/not-found-endpoint-404')
|
|
237
|
+
} catch (error) {
|
|
238
|
+
requestResult.value = `404错误已捕获: ${error.message}`
|
|
239
|
+
showFailToast('404错误已捕获')
|
|
240
|
+
refreshStatistics()
|
|
241
|
+
} finally {
|
|
242
|
+
loading.value = false
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 测试500错误
|
|
247
|
+
const test500Error = async () => {
|
|
248
|
+
loading.value = true
|
|
249
|
+
requestResult.value = ''
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
await request.get('/api/server-error-500')
|
|
253
|
+
} catch (error) {
|
|
254
|
+
requestResult.value = `500错误已捕获: ${error.message}`
|
|
255
|
+
showFailToast('500错误已捕获')
|
|
256
|
+
refreshStatistics()
|
|
257
|
+
} finally {
|
|
258
|
+
loading.value = false
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 测试网络错误
|
|
263
|
+
const testNetworkError = async () => {
|
|
264
|
+
loading.value = true
|
|
265
|
+
requestResult.value = ''
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// 临时修改baseURL来模拟网络错误
|
|
269
|
+
const originalBaseURL = request.defaults.baseURL
|
|
270
|
+
request.defaults.baseURL = 'https://non-existent-domain-12345-xyz.com'
|
|
271
|
+
|
|
272
|
+
await request.get('/api/test-network-error')
|
|
273
|
+
|
|
274
|
+
// 恢复baseURL
|
|
275
|
+
request.defaults.baseURL = originalBaseURL
|
|
276
|
+
} catch (error) {
|
|
277
|
+
requestResult.value = `网络错误已捕获: ${error.message}`
|
|
278
|
+
showFailToast('网络错误已捕获')
|
|
279
|
+
// 恢复baseURL
|
|
280
|
+
request.defaults.baseURL = import.meta.env.VITE_API_BASE_URL || ''
|
|
281
|
+
refreshStatistics()
|
|
282
|
+
} finally {
|
|
283
|
+
loading.value = false
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 测试超时错误
|
|
288
|
+
const testTimeoutError = async () => {
|
|
289
|
+
loading.value = true
|
|
290
|
+
requestResult.value = ''
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// 创建一个超时时间很短的请求
|
|
294
|
+
await request.get('/api/slow-endpoint', {
|
|
295
|
+
timeout: 100 // 100ms超时
|
|
296
|
+
})
|
|
297
|
+
} catch (error) {
|
|
298
|
+
requestResult.value = `超时错误已捕获: ${error.message}`
|
|
299
|
+
showFailToast('超时错误已捕获')
|
|
300
|
+
refreshStatistics()
|
|
301
|
+
} finally {
|
|
302
|
+
loading.value = false
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ============ 页面错误测试 ============
|
|
307
|
+
|
|
308
|
+
// 测试JavaScript错误
|
|
309
|
+
const testPageError = () => {
|
|
310
|
+
// 使用 setTimeout 确保错误不会被 try-catch 捕获
|
|
311
|
+
setTimeout(() => {
|
|
312
|
+
throw new Error('测试页面错误:这是一个模拟的前端JavaScript错误')
|
|
313
|
+
}, 0)
|
|
314
|
+
showToast('页面错误已触发,请查看控制台和日志')
|
|
315
|
+
setTimeout(() => {
|
|
316
|
+
refreshStatistics()
|
|
317
|
+
}, 100)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 测试引用错误
|
|
321
|
+
const testReferenceError = () => {
|
|
322
|
+
setTimeout(() => {
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
undefinedVariable.someProperty // 这会触发 ReferenceError
|
|
325
|
+
}, 0)
|
|
326
|
+
showToast('引用错误已触发,请查看控制台和日志')
|
|
327
|
+
setTimeout(() => {
|
|
328
|
+
refreshStatistics()
|
|
329
|
+
}, 100)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 测试类型错误
|
|
333
|
+
const testTypeError = () => {
|
|
334
|
+
setTimeout(() => {
|
|
335
|
+
const obj = null
|
|
336
|
+
// @ts-ignore
|
|
337
|
+
obj.property // 这会触发 TypeError
|
|
338
|
+
}, 0)
|
|
339
|
+
showToast('类型错误已触发,请查看控制台和日志')
|
|
340
|
+
setTimeout(() => {
|
|
341
|
+
refreshStatistics()
|
|
342
|
+
}, 100)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ============ Promise错误测试 ============
|
|
346
|
+
|
|
347
|
+
// 测试Promise未捕获错误
|
|
348
|
+
const testPromiseError = () => {
|
|
349
|
+
// 创建一个未处理的Promise错误
|
|
350
|
+
Promise.reject(new Error('测试Promise错误:这是一个未捕获的Promise错误'))
|
|
351
|
+
showToast('Promise错误已触发,请查看控制台和日志')
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
refreshStatistics()
|
|
354
|
+
}, 100)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 测试Async函数错误
|
|
358
|
+
const testAsyncError = async () => {
|
|
359
|
+
// 创建一个异步函数,但不捕获错误
|
|
360
|
+
const asyncFunc = async () => {
|
|
361
|
+
throw new Error('测试Async函数错误:这是一个异步函数中的未捕获错误')
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 不等待,让它自己失败
|
|
365
|
+
asyncFunc().catch(() => {
|
|
366
|
+
// 即使这里捕获了,但如果在其他地方没有捕获,错误监控还是会捕获
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// 触发一个真正未捕获的Promise错误
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
Promise.reject(new Error('Async函数中的未捕获Promise错误'))
|
|
372
|
+
}, 0)
|
|
373
|
+
|
|
374
|
+
showToast('Async错误已触发,请查看控制台和日志')
|
|
375
|
+
setTimeout(() => {
|
|
376
|
+
refreshStatistics()
|
|
377
|
+
}, 100)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============ Vue组件错误测试 ============
|
|
381
|
+
|
|
382
|
+
// 测试Vue组件渲染错误
|
|
383
|
+
const testVueError = () => {
|
|
384
|
+
shouldRenderError.value = true
|
|
385
|
+
showToast('Vue渲染错误已触发,请查看控制台和日志')
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
shouldRenderError.value = false
|
|
388
|
+
refreshStatistics()
|
|
389
|
+
}, 100)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
// 初始化
|
|
394
|
+
onMounted(() => {
|
|
395
|
+
refreshStatistics()
|
|
396
|
+
})
|
|
397
|
+
</script>
|
|
398
|
+
|
|
399
|
+
<style lang="scss" scoped>
|
|
400
|
+
.test-error-page {
|
|
401
|
+
padding: 16px;
|
|
402
|
+
background-color: #f5f5f5;
|
|
403
|
+
min-height: 100vh;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.header {
|
|
407
|
+
display: flex;
|
|
408
|
+
justify-content: space-between;
|
|
409
|
+
align-items: center;
|
|
410
|
+
margin-bottom: 16px;
|
|
411
|
+
|
|
412
|
+
h2 {
|
|
413
|
+
margin: 0;
|
|
414
|
+
font-size: 20px;
|
|
415
|
+
font-weight: bold;
|
|
416
|
+
color: #333;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.stats-card {
|
|
421
|
+
display: grid;
|
|
422
|
+
grid-template-columns: repeat(4, 1fr);
|
|
423
|
+
gap: 12px;
|
|
424
|
+
margin-bottom: 20px;
|
|
425
|
+
|
|
426
|
+
.stat-item {
|
|
427
|
+
background: #fff;
|
|
428
|
+
border-radius: 8px;
|
|
429
|
+
padding: 16px;
|
|
430
|
+
text-align: center;
|
|
431
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
432
|
+
|
|
433
|
+
.stat-label {
|
|
434
|
+
font-size: 12px;
|
|
435
|
+
color: #666;
|
|
436
|
+
margin-bottom: 8px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.stat-value {
|
|
440
|
+
font-size: 24px;
|
|
441
|
+
font-weight: bold;
|
|
442
|
+
color: #1989fa;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.test-section {
|
|
448
|
+
background: #fff;
|
|
449
|
+
border-radius: 8px;
|
|
450
|
+
padding: 16px;
|
|
451
|
+
margin-bottom: 16px;
|
|
452
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
453
|
+
|
|
454
|
+
.section-title {
|
|
455
|
+
font-size: 16px;
|
|
456
|
+
font-weight: bold;
|
|
457
|
+
color: #333;
|
|
458
|
+
margin-bottom: 12px;
|
|
459
|
+
padding-bottom: 8px;
|
|
460
|
+
border-bottom: 1px solid #f0f0f0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.button-group {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
gap: 8px;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.result-box {
|
|
470
|
+
margin-top: 16px;
|
|
471
|
+
padding: 12px;
|
|
472
|
+
background-color: #f8f9fa;
|
|
473
|
+
border-radius: 4px;
|
|
474
|
+
border: 1px solid #e9ecef;
|
|
475
|
+
|
|
476
|
+
.result-title {
|
|
477
|
+
font-size: 14px;
|
|
478
|
+
font-weight: 500;
|
|
479
|
+
color: #333;
|
|
480
|
+
margin-bottom: 8px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.result-content {
|
|
484
|
+
margin: 0;
|
|
485
|
+
font-size: 12px;
|
|
486
|
+
color: #666;
|
|
487
|
+
white-space: pre-wrap;
|
|
488
|
+
word-break: break-all;
|
|
489
|
+
max-height: 200px;
|
|
490
|
+
overflow-y: auto;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.refresh-section {
|
|
496
|
+
margin-top: 20px;
|
|
497
|
+
margin-bottom: 20px;
|
|
498
|
+
}
|
|
499
|
+
</style>
|
|
500
|
+
|