foreign-admin-ui/src/components/common/ErrorBoundary.vue

171 lines
4.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>