处理异常页面
This commit is contained in:
parent
5c8204706e
commit
d0204a5ab6
15
src/App.vue
15
src/App.vue
|
|
@ -10,6 +10,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import TopLoading from '@/components/common/TopLoading.vue'
|
||||
import { useTabsStore } from '@/stores/modules/tabs'
|
||||
|
||||
const tabsStore = useTabsStore()
|
||||
|
||||
const topLoadingRef = ref<InstanceType<typeof TopLoading>>()
|
||||
|
||||
|
|
@ -17,5 +20,17 @@ onMounted(() => {
|
|||
// 将 TopLoading 实例挂载到 window 上,供 api 模块调用
|
||||
// @ts-ignore
|
||||
window.__topLoading__ = topLoadingRef.value
|
||||
|
||||
// 检查并恢复标签页状态
|
||||
tabsStore.validateAndRecoverTabs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
<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
|
||||
|
||||
// 全局错误捕获
|
||||
onErrorCaptured((err: Error, instance: any, info: string) => {
|
||||
console.error('❌ ErrorBoundary 捕获到错误:', err)
|
||||
console.error('错误来源组件:', instance?.$options?.name || '未知组件')
|
||||
console.error('错误信息:', info)
|
||||
|
||||
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>
|
||||
|
|
@ -21,10 +21,13 @@
|
|||
style="height: 100%; display: flex; flex-direction: column"
|
||||
>
|
||||
<router-view v-slot="{ Component }">
|
||||
<KeepAlive v-if="route.meta.keepAlive">
|
||||
<component :is="Component" :key="refreshKey" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" :key="refreshKey" />
|
||||
<!-- 使用 :key 确保每次路由变化都会创建新的 ErrorBoundary 实例 -->
|
||||
<ErrorBoundary :key="route.path + '-' + refreshKey">
|
||||
<KeepAlive v-if="route.meta.keepAlive">
|
||||
<component :is="Component" :key="refreshKey" />
|
||||
</KeepAlive>
|
||||
<component v-else :is="Component" :key="refreshKey" />
|
||||
</ErrorBoundary>
|
||||
</router-view>
|
||||
</transition>
|
||||
</main>
|
||||
|
|
@ -40,6 +43,7 @@ defineOptions({
|
|||
import Header from './Header.vue'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import Tabs from './Tabs.vue'
|
||||
import ErrorBoundary from '@/components/common/ErrorBoundary.vue'
|
||||
import { useTabsStore } from '@/stores/modules/tabs'
|
||||
|
||||
const route = useRoute()
|
||||
|
|
|
|||
|
|
@ -263,6 +263,42 @@ export const useTabsStore = defineStore('tabs', () => {
|
|||
removeStorage('activeTab')
|
||||
}
|
||||
|
||||
// 初始化默认首页标签
|
||||
const initDefaultTab = () => {
|
||||
// 检查是否已有有效的标签页
|
||||
if (tabs.value.length === 0) {
|
||||
tabs.value.push({
|
||||
title: '首页',
|
||||
path: '/dashboard'
|
||||
})
|
||||
activeTab.value = '/dashboard'
|
||||
saveTabs()
|
||||
}
|
||||
}
|
||||
|
||||
// 验证并恢复标签页状态(用于应用启动时检查)
|
||||
const validateAndRecoverTabs = () => {
|
||||
const savedTabs = getStorage('tabs')
|
||||
const savedActiveTab = getStorage('activeTab')
|
||||
|
||||
// 如果存储中没有数据或者数据无效
|
||||
if (!savedTabs || !Array.isArray(savedTabs) || savedTabs.length === 0) {
|
||||
initDefaultTab()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证当前激活的标签页是否在列表中
|
||||
if (savedActiveTab && !savedTabs.some((t: TabItem) => t.path === savedActiveTab)) {
|
||||
// 激活的标签页不在列表中,切换到第一个有效的标签页
|
||||
if (savedTabs.length > 0) {
|
||||
activeTab.value = savedTabs[0].path
|
||||
saveTabs()
|
||||
} else {
|
||||
initDefaultTab()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置标签页列表(用于拖拽排序)
|
||||
const setTabs = (newTabs: TabItem[]) => {
|
||||
tabs.value = newTabs
|
||||
|
|
@ -293,6 +329,8 @@ export const useTabsStore = defineStore('tabs', () => {
|
|||
clearTabs,
|
||||
setTabs,
|
||||
refreshKey,
|
||||
refreshTab
|
||||
refreshTab,
|
||||
initDefaultTab,
|
||||
validateAndRecoverTabs
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -138,7 +138,10 @@ import { useUserStore } from '@/stores/modules/user'
|
|||
const userStore = useUserStore()
|
||||
console.log('userStore:', userStore);
|
||||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
||||
|
||||
const transferFormOptionsMap = ref({
|
||||
transferOutAccount: [],
|
||||
transferInAccount: [],
|
||||
}) as any
|
||||
const { t } = useI18n()
|
||||
/**
|
||||
* @description 转账类型
|
||||
|
|
@ -210,10 +213,7 @@ watch(
|
|||
)
|
||||
|
||||
const transferFormRules = ref(rules(transferForm.value))
|
||||
const transferFormOptionsMap = ref({
|
||||
transferOutAccount: [],
|
||||
transferInAccount: [],
|
||||
}) as any
|
||||
|
||||
|
||||
/**
|
||||
* @description 验证方式
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
<el-input v-model="searchForm.name" placeholder="请输入节假日名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch" v-auth="'contractVariety_addHoliday'">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button type="primary" @click="handleSearch" v-auth="'contractVariety_addHoliday'"
|
||||
:loading="loading">搜索</el-button>
|
||||
<el-button @click="handleReset" :loading="loading">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="search-right">
|
||||
|
|
@ -17,50 +18,25 @@
|
|||
</div>
|
||||
|
||||
<!-- 列表区域:使用公共 Table 组件 -->
|
||||
<Table
|
||||
ref="tableRef"
|
||||
:table-data="tableData"
|
||||
:columns="tableColumns"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
>
|
||||
<Table ref="tableRef" :table-data="tableData" :columns="tableColumns" row-key="id" :loading="loading">
|
||||
<!-- 操作列插槽 -->
|
||||
<template #action="{ row }">
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)" v-auth="'contractVariety_addHoliday'">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDel(row)" v-auth="'contractVariety_addHoliday'">删除</el-button>
|
||||
<el-button type="primary" size="small" @click="handleEdit(row)"
|
||||
v-auth="'contractVariety_addHoliday'">编辑</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDel(row)"
|
||||
v-auth="'contractVariety_addHoliday'">删除</el-button>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
:destroy-on-close="true"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<Form
|
||||
ref="formRef"
|
||||
:form-data="formData"
|
||||
:form-rules="formRules"
|
||||
:form-items="formItems"
|
||||
:options-map="optionsMap"
|
||||
>
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" :destroy-on-close="true"
|
||||
@close="handleDialogClose">
|
||||
<Form ref="formRef" :form-data="formData" :form-rules="formRules" :form-items="formItems"
|
||||
:options-map="optionsMap">
|
||||
<!-- 休市市场多选插槽 -->
|
||||
<template #marketSelect>
|
||||
<el-select
|
||||
v-model="formData.groupId"
|
||||
multiple
|
||||
placeholder="请选择休市市场"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="market in marketList"
|
||||
:key="market.value"
|
||||
:label="market.label"
|
||||
:value="market.value"
|
||||
/>
|
||||
<el-select v-model="formData.groupId" multiple placeholder="请选择休市市场" style="width: 100%" clearable>
|
||||
<el-option v-for="market in marketList" :key="market.value" :label="market.label" :value="market.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
</Form>
|
||||
|
|
@ -167,21 +143,21 @@ const formItems: Array<{
|
|||
valueFormat?: string
|
||||
slotName?: string
|
||||
}> = [
|
||||
{ type: 'input', label: '节假日名称', prop: 'name', placeholder: '请输入节假日名称', required: true },
|
||||
{
|
||||
type: 'date',
|
||||
label: '休市时间',
|
||||
prop: 'datePicker',
|
||||
dateType: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
required: true,
|
||||
},
|
||||
{ type: 'slot', label: '休市市场', prop: 'groupId', slotName: 'marketSelect' },
|
||||
{ type: 'textarea', label: '备注', prop: 'description', placeholder: '请输入备注' },
|
||||
]
|
||||
{ type: 'input', label: '节假日名称', prop: 'name', placeholder: '请输入节假日名称', required: true },
|
||||
{
|
||||
type: 'date',
|
||||
label: '休市时间',
|
||||
prop: 'datePicker',
|
||||
dateType: 'datetimerange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
required: true,
|
||||
},
|
||||
{ type: 'slot', label: '休市市场', prop: 'groupId', slotName: 'marketSelect' },
|
||||
{ type: 'textarea', label: '备注', prop: 'description', placeholder: '请输入备注' },
|
||||
]
|
||||
|
||||
// 市场选项 Map(仅保留给非 slot 类型字段使用)
|
||||
const optionsMap = computed(() => ({}))
|
||||
|
|
@ -350,8 +326,8 @@ const handleSubmit = async () => {
|
|||
* @description: 页面初始化
|
||||
*/
|
||||
onMounted(async () => {
|
||||
await getMarketList()
|
||||
await getList()
|
||||
await getMarketList()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue