refactor: move app initialization from AppMain to main.ts and improve router guards

This commit is contained in:
chansee97 2025-09-08 00:44:44 +08:00
parent 423e79f64d
commit 1b28285b04
4 changed files with 188 additions and 150 deletions

View File

@ -1,18 +1,43 @@
<script setup lang="ts">
import AppMain from './AppMain.vue'
import AppLoading from './components/common/AppLoading.vue'
import { useAppStore } from '@/store'
import { naiveI18nOptions } from '@/utils'
import { darkTheme } from 'naive-ui'
// 使 Suspense
const appStore = useAppStore()
const naiveLocale = computed(() => {
return naiveI18nOptions[appStore.lang] ? naiveI18nOptions[appStore.lang] : naiveI18nOptions.enUS
})
const propOverrides = {
ProSearchForm: {
labelPlacement: 'left',
cols: 4,
},
ProModalForm: {
labelWidth: 100,
labelPlacement: 'left',
preset: 'card',
},
ProDataTable: {
paginateSinglePage: false,
},
}
</script>
<template>
<Suspense>
<!-- 异步组件 -->
<AppMain />
<!-- 加载状态 -->
<template #fallback>
<AppLoading />
</template>
</Suspense>
<pro-config-provider
:prop-overrides="propOverrides"
abstract
inline-theme-disabled
:theme="appStore.colorMode === 'dark' ? darkTheme : null"
:locale="naiveLocale.locale"
:date-locale="naiveLocale.dateLocale"
:theme-overrides="appStore.theme"
>
<naive-provider>
<router-view />
<Watermark :show-watermark="appStore.showWatermark" />
</naive-provider>
</pro-config-provider>
</template>

View File

@ -1,73 +0,0 @@
<script setup lang="ts">
import type { App } from 'vue'
import { installRouter } from '@/router'
import { installPinia } from '@/store'
import { naiveI18nOptions } from '@/utils'
import { darkTheme } from 'naive-ui'
import { useAppStore } from './store'
// Promise -
const initializationPromise = (async () => {
//
const app = getCurrentInstance()?.appContext.app
if (!app) {
throw new Error('Failed to get app instance')
}
// Pinia
await installPinia(app)
// Vue-router
await installRouter(app)
// /
const modules = import.meta.glob<{ install: (app: App) => void }>('./modules/*.ts', {
eager: true,
})
Object.values(modules).forEach(module => app.use(module))
return true
})()
// - 使 setup
await initializationPromise
const appStore = useAppStore()
const naiveLocale = computed(() => {
return naiveI18nOptions[appStore.lang] ? naiveI18nOptions[appStore.lang] : naiveI18nOptions.enUS
})
const propOverrides = {
ProSearchForm: {
labelPlacement: 'left',
cols: 4,
},
ProModalForm: {
labelWidth: 100,
labelPlacement: 'left',
preset: 'card',
},
ProDataTable: {
paginateSinglePage: false,
},
}
</script>
<template>
<pro-config-provider
:prop-overrides="propOverrides"
abstract
inline-theme-disabled
:theme="appStore.colorMode === 'dark' ? darkTheme : null"
:locale="naiveLocale.locale"
:date-locale="naiveLocale.dateLocale"
:theme-overrides="appStore.theme"
>
<naive-provider>
<router-view />
<Watermark :show-watermark="appStore.showWatermark" />
</naive-provider>
</pro-config-provider>
</template>

View File

@ -1,5 +1,41 @@
import type { App as AppType } from 'vue'
import App from './App.vue'
import AppLoading from './components/common/AppLoading.vue'
import { installPinia } from '@/store'
import { installRouter } from '@/router'
// 创建应用实例并挂载
const app = createApp(App)
app.mount('#app')
async function bootstrap() {
// 显示加载动画
const loadingApp = createApp(AppLoading)
loadingApp.mount('#app')
try {
// 创建应用实例
const app = createApp(App)
// 注册模块 Pinia
await installPinia(app)
// 注册模块 Vue-router
await installRouter(app)
// 注册模块 指令/静态资源
const modules = import.meta.glob<{ install: (app: AppType) => void }>('./modules/*.ts', {
eager: true,
})
Object.values(modules).forEach(module => app.use(module))
// 卸载加载动画并挂载主应用
loadingApp.unmount()
app.mount('#app')
}
catch (error) {
// 如果初始化失败,卸载加载动画并显示错误
loadingApp.unmount()
console.error('Application initialization failed:', error)
throw error
}
}
bootstrap().catch(console.error)

View File

@ -1,92 +1,142 @@
import type { Router } from 'vue-router'
import type { NavigationGuardNext, RouteLocationNormalized, Router } from 'vue-router'
import { useAppStore, useRouteStore, useTabStore } from '@/store'
import { local } from '@/utils'
const title = import.meta.env.VITE_APP_NAME
// 路由上下文
interface RouteContext {
appStore: ReturnType<typeof useAppStore>
routeStore: ReturnType<typeof useRouteStore>
tabStore: ReturnType<typeof useTabStore>
isLogin: boolean
}
// 外链处理
function handleExternalLink(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
if (to.meta.isLink) {
window.open(to.meta.linkPath as string)
next(false)
return true
}
return false
}
// 根路径重定向
function handleRootPath(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, context: RouteContext) {
if (to.path === '/') {
if (context.isLogin) {
next({ path: import.meta.env.VITE_HOME_PATH, replace: true })
}
else {
next({ path: '/login', replace: true })
}
return true
}
return false
}
// 登录页面访问处理
function handleLoginPage(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, context: RouteContext) {
if (to.name === 'login' && context.isLogin) {
next({ path: import.meta.env.VITE_HOME_PATH, replace: true })
return true
}
return false
}
// 认证检查
function handleAuthentication(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, context: RouteContext) {
if (!context.isLogin && to.name !== 'login' && to.meta?.requiresAuth !== false) {
const redirect = to.name === 'not-found' ? undefined : to.fullPath
next({ path: '/login', query: { redirect } })
return true
}
return false
}
// 路由初始化
async function handleRouteInitialization(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, context: RouteContext) {
if (context.isLogin && to.name !== 'login' && !context.routeStore.isInitAuthRoute) {
try {
await context.routeStore.initAuthRoute()
if (to.name === 'not-found') {
next({
path: to.fullPath,
replace: true,
query: to.query,
hash: to.hash,
})
return true
}
}
catch (error) {
console.error('Route initialization failed:', error)
local.remove('accessToken')
context.routeStore.resetRouteStore()
const redirect = to.fullPath !== '/' && to.fullPath !== '/login' ? to.fullPath : undefined
next({ path: '/login', query: redirect ? { redirect } : undefined, replace: true })
return true
}
}
return false
}
// 路由处理器列表
const routeHandlers = [
handleExternalLink,
handleRootPath,
handleLoginPage,
handleAuthentication,
handleRouteInitialization,
]
// 处理路由
async function processRoute(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext, context: RouteContext) {
for (const handler of routeHandlers) {
const handled = await handler(to, from, next, context)
if (handled)
return
}
// 如果没有任何处理器处理该路由,则继续导航
next()
}
export function setupRouterGuard(router: Router) {
const appStore = useAppStore()
const routeStore = useRouteStore()
const tabStore = useTabStore()
router.beforeEach(async (to, from, next) => {
// 判断是否是外链,如果是直接打开网页并拦截跳转
if (to.meta.isLink) {
window.open(to.meta.linkPath)
next(false) // 取消当前导航
return
}
// 开始 loadingBar
appStore.showProgress && window.$loadingBar?.start()
// 判断有无TOKEN,登录鉴权
const isLogin = Boolean(local.get('accessToken'))
// 处理根路由重定向
if (to.path === '/') {
if (isLogin) {
// 已登录,重定向到首页
next({ path: import.meta.env.VITE_HOME_PATH, replace: true })
}
else {
// 未登录,重定向到登录页
next({ path: '/login', replace: true })
}
return
// 创建路由上下文
const context: RouteContext = {
appStore,
routeStore,
tabStore,
isLogin: Boolean(local.get('accessToken')),
}
// 如果用户未登录,重定向到登录页
if (!isLogin) {
const redirect = to.name === 'not-found' ? undefined : to.fullPath
next({ path: '/login', query: { redirect } })
return
}
// 如果用户已登录且访问login页面重定向到首页
if (to.name === 'login' && isLogin) {
next({ path: '/' })
return
}
// 判断路由有无进行初始化
if (!routeStore.isInitAuthRoute && to.name !== 'login') {
try {
await routeStore.initAuthRoute()
// 动态路由加载完回到根路由
if (to.name === 'not-found') {
// 等待权限路由加载好了,回到之前的路由,否则404
next({
path: to.fullPath,
replace: true,
query: to.query,
hash: to.hash,
})
return
}
}
catch {
// 如果路由初始化失败(比如 401 错误),重定向到登录页
local.remove('accessToken')
const redirect = to.fullPath !== '/' ? to.fullPath : undefined
next({ path: '/login', query: redirect ? { redirect } : undefined })
return
}
}
next()
// 使用函数式处理路由
await processRoute(to, from, next, context)
})
router.beforeResolve((to) => {
// 设置菜单高亮
routeStore.setActiveMenu(to.meta.activePath ?? to.fullPath)
// 添加tabs
tabStore.addTab(to)
// 设置高亮标签;
// 设置高亮标签
tabStore.setCurrentTab(to.fullPath as string)
})
router.afterEach((to) => {
// 修改网页标题
document.title = `${to.meta.title} - ${title}`
document.title = `${to.meta.title || 'Nova Admin'} - ${title}`
// 结束 loadingBar
appStore.showProgress && window.$loadingBar?.finish()
})