处理异常页面
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">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import TopLoading from '@/components/common/TopLoading.vue'
|
import TopLoading from '@/components/common/TopLoading.vue'
|
||||||
|
import { useTabsStore } from '@/stores/modules/tabs'
|
||||||
|
|
||||||
|
const tabsStore = useTabsStore()
|
||||||
|
|
||||||
const topLoadingRef = ref<InstanceType<typeof TopLoading>>()
|
const topLoadingRef = ref<InstanceType<typeof TopLoading>>()
|
||||||
|
|
||||||
|
|
@ -17,5 +20,17 @@ onMounted(() => {
|
||||||
// 将 TopLoading 实例挂载到 window 上,供 api 模块调用
|
// 将 TopLoading 实例挂载到 window 上,供 api 模块调用
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.__topLoading__ = topLoadingRef.value
|
window.__topLoading__ = topLoadingRef.value
|
||||||
|
|
||||||
|
// 检查并恢复标签页状态
|
||||||
|
tabsStore.validateAndRecoverTabs()
|
||||||
})
|
})
|
||||||
</script>
|
</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"
|
style="height: 100%; display: flex; flex-direction: column"
|
||||||
>
|
>
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<KeepAlive v-if="route.meta.keepAlive">
|
<!-- 使用 :key 确保每次路由变化都会创建新的 ErrorBoundary 实例 -->
|
||||||
<component :is="Component" :key="refreshKey" />
|
<ErrorBoundary :key="route.path + '-' + refreshKey">
|
||||||
</KeepAlive>
|
<KeepAlive v-if="route.meta.keepAlive">
|
||||||
<component v-else :is="Component" :key="refreshKey" />
|
<component :is="Component" :key="refreshKey" />
|
||||||
|
</KeepAlive>
|
||||||
|
<component v-else :is="Component" :key="refreshKey" />
|
||||||
|
</ErrorBoundary>
|
||||||
</router-view>
|
</router-view>
|
||||||
</transition>
|
</transition>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -40,6 +43,7 @@ defineOptions({
|
||||||
import Header from './Header.vue'
|
import Header from './Header.vue'
|
||||||
import Sidebar from './Sidebar.vue'
|
import Sidebar from './Sidebar.vue'
|
||||||
import Tabs from './Tabs.vue'
|
import Tabs from './Tabs.vue'
|
||||||
|
import ErrorBoundary from '@/components/common/ErrorBoundary.vue'
|
||||||
import { useTabsStore } from '@/stores/modules/tabs'
|
import { useTabsStore } from '@/stores/modules/tabs'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,42 @@ export const useTabsStore = defineStore('tabs', () => {
|
||||||
removeStorage('activeTab')
|
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[]) => {
|
const setTabs = (newTabs: TabItem[]) => {
|
||||||
tabs.value = newTabs
|
tabs.value = newTabs
|
||||||
|
|
@ -293,6 +329,8 @@ export const useTabsStore = defineStore('tabs', () => {
|
||||||
clearTabs,
|
clearTabs,
|
||||||
setTabs,
|
setTabs,
|
||||||
refreshKey,
|
refreshKey,
|
||||||
refreshTab
|
refreshTab,
|
||||||
|
initDefaultTab,
|
||||||
|
validateAndRecoverTabs
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,10 @@ import { useUserStore } from '@/stores/modules/user'
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
console.log('userStore:', userStore);
|
console.log('userStore:', userStore);
|
||||||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
||||||
|
const transferFormOptionsMap = ref({
|
||||||
|
transferOutAccount: [],
|
||||||
|
transferInAccount: [],
|
||||||
|
}) as any
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
/**
|
/**
|
||||||
* @description 转账类型
|
* @description 转账类型
|
||||||
|
|
@ -210,10 +213,7 @@ watch(
|
||||||
)
|
)
|
||||||
|
|
||||||
const transferFormRules = ref(rules(transferForm.value))
|
const transferFormRules = ref(rules(transferForm.value))
|
||||||
const transferFormOptionsMap = ref({
|
|
||||||
transferOutAccount: [],
|
|
||||||
transferInAccount: [],
|
|
||||||
}) as any
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 验证方式
|
* @description 验证方式
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
<el-input v-model="searchForm.name" placeholder="请输入节假日名称" clearable />
|
<el-input v-model="searchForm.name" placeholder="请输入节假日名称" clearable />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch" v-auth="'contractVariety_addHoliday'">搜索</el-button>
|
<el-button type="primary" @click="handleSearch" v-auth="'contractVariety_addHoliday'"
|
||||||
<el-button @click="handleReset">重置</el-button>
|
:loading="loading">搜索</el-button>
|
||||||
|
<el-button @click="handleReset" :loading="loading">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="search-right">
|
<div class="search-right">
|
||||||
|
|
@ -17,50 +18,25 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表区域:使用公共 Table 组件 -->
|
<!-- 列表区域:使用公共 Table 组件 -->
|
||||||
<Table
|
<Table ref="tableRef" :table-data="tableData" :columns="tableColumns" row-key="id" :loading="loading">
|
||||||
ref="tableRef"
|
|
||||||
:table-data="tableData"
|
|
||||||
:columns="tableColumns"
|
|
||||||
row-key="id"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
<!-- 操作列插槽 -->
|
<!-- 操作列插槽 -->
|
||||||
<template #action="{ row }">
|
<template #action="{ row }">
|
||||||
<el-button type="primary" size="small" @click="handleEdit(row)" v-auth="'contractVariety_addHoliday'">编辑</el-button>
|
<el-button type="primary" size="small" @click="handleEdit(row)"
|
||||||
<el-button type="danger" size="small" @click="handleDel(row)" v-auth="'contractVariety_addHoliday'">删除</el-button>
|
v-auth="'contractVariety_addHoliday'">编辑</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDel(row)"
|
||||||
|
v-auth="'contractVariety_addHoliday'">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
<el-dialog
|
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" :destroy-on-close="true"
|
||||||
v-model="dialogVisible"
|
@close="handleDialogClose">
|
||||||
:title="dialogTitle"
|
<Form ref="formRef" :form-data="formData" :form-rules="formRules" :form-items="formItems"
|
||||||
width="600px"
|
:options-map="optionsMap">
|
||||||
:destroy-on-close="true"
|
|
||||||
@close="handleDialogClose"
|
|
||||||
>
|
|
||||||
<Form
|
|
||||||
ref="formRef"
|
|
||||||
:form-data="formData"
|
|
||||||
:form-rules="formRules"
|
|
||||||
:form-items="formItems"
|
|
||||||
:options-map="optionsMap"
|
|
||||||
>
|
|
||||||
<!-- 休市市场多选插槽 -->
|
<!-- 休市市场多选插槽 -->
|
||||||
<template #marketSelect>
|
<template #marketSelect>
|
||||||
<el-select
|
<el-select v-model="formData.groupId" multiple placeholder="请选择休市市场" style="width: 100%" clearable>
|
||||||
v-model="formData.groupId"
|
<el-option v-for="market in marketList" :key="market.value" :label="market.label" :value="market.value" />
|
||||||
multiple
|
|
||||||
placeholder="请选择休市市场"
|
|
||||||
style="width: 100%"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="market in marketList"
|
|
||||||
:key="market.value"
|
|
||||||
:label="market.label"
|
|
||||||
:value="market.value"
|
|
||||||
/>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
@ -167,21 +143,21 @@ const formItems: Array<{
|
||||||
valueFormat?: string
|
valueFormat?: string
|
||||||
slotName?: string
|
slotName?: string
|
||||||
}> = [
|
}> = [
|
||||||
{ type: 'input', label: '节假日名称', prop: 'name', placeholder: '请输入节假日名称', required: true },
|
{ type: 'input', label: '节假日名称', prop: 'name', placeholder: '请输入节假日名称', required: true },
|
||||||
{
|
{
|
||||||
type: 'date',
|
type: 'date',
|
||||||
label: '休市时间',
|
label: '休市时间',
|
||||||
prop: 'datePicker',
|
prop: 'datePicker',
|
||||||
dateType: 'datetimerange',
|
dateType: 'datetimerange',
|
||||||
rangeSeparator: '至',
|
rangeSeparator: '至',
|
||||||
startPlaceholder: '开始时间',
|
startPlaceholder: '开始时间',
|
||||||
endPlaceholder: '结束时间',
|
endPlaceholder: '结束时间',
|
||||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{ type: 'slot', label: '休市市场', prop: 'groupId', slotName: 'marketSelect' },
|
{ type: 'slot', label: '休市市场', prop: 'groupId', slotName: 'marketSelect' },
|
||||||
{ type: 'textarea', label: '备注', prop: 'description', placeholder: '请输入备注' },
|
{ type: 'textarea', label: '备注', prop: 'description', placeholder: '请输入备注' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// 市场选项 Map(仅保留给非 slot 类型字段使用)
|
// 市场选项 Map(仅保留给非 slot 类型字段使用)
|
||||||
const optionsMap = computed(() => ({}))
|
const optionsMap = computed(() => ({}))
|
||||||
|
|
@ -350,8 +326,8 @@ const handleSubmit = async () => {
|
||||||
* @description: 页面初始化
|
* @description: 页面初始化
|
||||||
*/
|
*/
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getMarketList()
|
|
||||||
await getList()
|
await getList()
|
||||||
|
await getMarketList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue