171 lines
4.1 KiB
Vue
171 lines
4.1 KiB
Vue
<template>
|
||
<div class="error-boundary">
|
||
<slot v-if="!hasError"></slot>
|
||
<transition name="fade">
|
||
<div v-if="hasError" class="error-page">
|
||
<div class="error-content">
|
||
<el-icon class="error-icon"><WarningFilled /></el-icon>
|
||
<h3>页面加载失败</h3>
|
||
<p>{{ errorMessage || '当前页面发生了错误,请尝试刷新或返回首页' }}</p>
|
||
<div class="error-actions">
|
||
<el-button type="primary" @click="handleRetry">重试</el-button>
|
||
<el-button @click="handleGoHome">返回首页</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, watch, onErrorCaptured, onUnmounted } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { WarningFilled } from '@element-plus/icons-vue'
|
||
import { useTabsStore } from '@/stores/modules/tabs'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const tabsStore = useTabsStore()
|
||
|
||
const hasError = ref(false)
|
||
const errorMessage = ref('')
|
||
let unwatchRoute: (() => void) | null = null
|
||
|
||
// 判断是否为 API 请求错误(这些错误不应该触发错误边界)
|
||
const isApiError = (err: any): boolean => {
|
||
console.log('err:', err);
|
||
// axios 错误通常有 config 属性
|
||
if ((err as any).config) return true
|
||
// 网络错误
|
||
if (err.message === 'Network Error') return true
|
||
// 超时错误
|
||
if (err.message?.includes('timeout') || err.message?.includes('Timeout')) return true
|
||
// 请求被取消
|
||
if (err.message?.includes('cancel') || err.name === 'CanceledError') return true
|
||
// 包含请求相关信息的错误
|
||
if (err.message?.includes('request') || err.message?.includes('Request') || err.data || err.msg) return true
|
||
return false
|
||
}
|
||
|
||
// 全局错误捕获
|
||
onErrorCaptured((err: Error, instance: any, info: string) => {
|
||
console.error('❌ ErrorBoundary 捕获到错误:', err)
|
||
console.error('错误来源组件:', instance?.$options?.name || '未知组件')
|
||
console.error('错误信息:', info)
|
||
|
||
// 如果是 API 请求错误,不触发错误边界,让 API 模块自己处理
|
||
if (isApiError(err)) {
|
||
console.log('API 请求错误,由 API 模块处理')
|
||
return false
|
||
}
|
||
|
||
// 只有真正的渲染错误才触发错误边界
|
||
hasError.value = true
|
||
errorMessage.value = err.message || '未知错误'
|
||
|
||
// 返回 false 阻止错误继续向上传播
|
||
return false
|
||
})
|
||
|
||
// 监听路由变化,自动清除错误状态
|
||
unwatchRoute = watch(
|
||
() => route.path,
|
||
() => {
|
||
// 路由变化时,清除错误状态
|
||
if (hasError.value) {
|
||
hasError.value = false
|
||
errorMessage.value = ''
|
||
}
|
||
}
|
||
)
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
if (unwatchRoute) {
|
||
unwatchRoute()
|
||
unwatchRoute = null
|
||
}
|
||
})
|
||
|
||
// 重试当前页面
|
||
const handleRetry = () => {
|
||
hasError.value = false
|
||
errorMessage.value = ''
|
||
// 刷新当前路由的 key,触发重新渲染
|
||
tabsStore.refreshTab(route.path)
|
||
router.replace(route.fullPath)
|
||
}
|
||
|
||
// 返回首页
|
||
const handleGoHome = () => {
|
||
hasError.value = false
|
||
errorMessage.value = ''
|
||
router.push('/dashboard')
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.error-boundary {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.error-page {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--el-bg-color-page, #f5f7fa);
|
||
z-index: 100;
|
||
|
||
.error-content {
|
||
text-align: center;
|
||
padding: 40px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||
max-width: 400px;
|
||
|
||
.error-icon {
|
||
font-size: 64px;
|
||
color: #e6a23c;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
h3 {
|
||
font-size: 24px;
|
||
color: #303133;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
p {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
margin-bottom: 24px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.error-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 过渡动画
|
||
.fade-enter-active,
|
||
.fade-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.fade-enter-from,
|
||
.fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
</style> |