处理异常页面

This commit is contained in:
2026-05-25 11:49:13 +08:00
parent 5c8204706e
commit d0204a5ab6
6 changed files with 245 additions and 64 deletions

View File

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

View File

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

View File

@ -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()

View File

@ -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
} }
}) })

View File

@ -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 验证方式

View File

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