mirror of
https://github.com/chansee97/nova-admin.git
synced 2026-07-08 02:21:08 +08:00
Compare commits
2 Commits
423e79f64d
...
df11b5a97c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df11b5a97c | ||
|
|
1b28285b04 |
2
.env
2
.env
@ -8,7 +8,7 @@ VITE_APP_NAME = Nova - Admin
|
||||
VITE_ROUTE_MODE = web
|
||||
|
||||
# 路由加载模式 static | dynamic
|
||||
VITE_ROUTE_LOAD_MODE = static
|
||||
VITE_ROUTE_LOAD_MODE = dynamic
|
||||
|
||||
# 设置登陆后跳转地址
|
||||
VITE_HOME_PATH = /home
|
||||
|
||||
49
src/App.vue
49
src/App.vue
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -8,7 +8,7 @@ import IconUser from '~icons/icon-park-outline/user'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { userInfo, logout } = useAuthStore()
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const options = computed(() => {
|
||||
@ -56,7 +56,7 @@ function handleSelect(key: string | number) {
|
||||
positiveText: t('common.confirm'),
|
||||
negativeText: t('common.cancel'),
|
||||
onPositiveClick: () => {
|
||||
logout()
|
||||
authStore.logout()
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -72,6 +72,10 @@ function handleSelect(key: string | number) {
|
||||
if (key === 'docs')
|
||||
window.open('https://nova-admin-docs.pages.dev/')
|
||||
}
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
return authStore.userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${authStore.userInfo?.username}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -83,7 +87,7 @@ function handleSelect(key: string | number) {
|
||||
<n-avatar
|
||||
round
|
||||
class="cursor-pointer"
|
||||
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
|
||||
:src="avatarUrl"
|
||||
>
|
||||
<template #fallback>
|
||||
<div class="wh-full flex-center">
|
||||
|
||||
42
src/main.ts
42
src/main.ts
@ -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)
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -5,7 +5,7 @@ import { useRouteStore } from './router'
|
||||
import { useTabStore } from './tab'
|
||||
|
||||
interface AuthStatus {
|
||||
userInfo: Entity.User
|
||||
userInfo: Partial<Entity.User>
|
||||
roles: string[]
|
||||
permissions: string[]
|
||||
}
|
||||
@ -38,14 +38,12 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
// 重置当前存储库
|
||||
this.$reset()
|
||||
// 重定向到登录页
|
||||
if (route.meta.requiresAuth) {
|
||||
router.push({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: route.fullPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
router.push({
|
||||
name: 'login',
|
||||
query: {
|
||||
redirect: route.fullPath,
|
||||
},
|
||||
})
|
||||
},
|
||||
clearAuthStorage() {
|
||||
local.remove('accessToken')
|
||||
@ -65,13 +63,7 @@ export const useAuthStore = defineStore('auth-store', {
|
||||
const { data } = await fetchLogin(loginData)
|
||||
|
||||
// 处理登录信息
|
||||
try {
|
||||
await this.handleLoginInfo(data)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to handle login info:', error)
|
||||
throw error
|
||||
}
|
||||
await this.handleLoginInfo(data)
|
||||
|
||||
// 更新用户信息
|
||||
await this.updataUserInfo()
|
||||
|
||||
@ -82,6 +82,7 @@ export function createMenus(userRoutes: Entity.Menu[]): MenuOption[] {
|
||||
|
||||
// render the returned routing table as a sidebar
|
||||
function transformAuthRoutesToMenus(userRoutes: Entity.Menu[]) {
|
||||
const routes = clone(userRoutes)
|
||||
const homeRoute: Entity.Menu = {
|
||||
id: 9999999999999,
|
||||
parentId: 0,
|
||||
@ -91,8 +92,8 @@ function transformAuthRoutesToMenus(userRoutes: Entity.Menu[]) {
|
||||
menuVisible: true,
|
||||
menuType: 'page',
|
||||
}
|
||||
userRoutes.unshift(homeRoute)
|
||||
return userRoutes
|
||||
routes.unshift(homeRoute)
|
||||
return routes
|
||||
// filter menus that do not need to be displayed
|
||||
.filter(route => route.menuVisible !== false)
|
||||
// Sort the menu according to the order size
|
||||
|
||||
@ -11,6 +11,7 @@ interface RoutesStatus {
|
||||
rowRoutes: Entity.Menu[]
|
||||
activeMenu: string | null
|
||||
cacheRoutes: string[]
|
||||
dynamicRouteNames: string[]
|
||||
}
|
||||
export const useRouteStore = defineStore('route-store', {
|
||||
state: (): RoutesStatus => {
|
||||
@ -20,6 +21,7 @@ export const useRouteStore = defineStore('route-store', {
|
||||
menus: [],
|
||||
rowRoutes: [],
|
||||
cacheRoutes: [],
|
||||
dynamicRouteNames: [],
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@ -28,8 +30,14 @@ export const useRouteStore = defineStore('route-store', {
|
||||
this.$reset()
|
||||
},
|
||||
resetRoutes() {
|
||||
if (router.hasRoute('appRoot'))
|
||||
router.removeRoute('appRoot')
|
||||
// Remove only dynamically added routes, preserve built-in routes
|
||||
this.dynamicRouteNames.forEach((routeName) => {
|
||||
if (router.hasRoute(routeName)) {
|
||||
router.removeRoute(routeName)
|
||||
}
|
||||
})
|
||||
// Clear the dynamic route names array
|
||||
this.dynamicRouteNames = []
|
||||
},
|
||||
// set the currently highlighted menu key
|
||||
setActiveMenu(key: string) {
|
||||
@ -68,9 +76,10 @@ export const useRouteStore = defineStore('route-store', {
|
||||
|
||||
// Generate actual route and insert
|
||||
const routes = createRoutes(rowRoutes)
|
||||
// Add each route as a child of appRoot
|
||||
// Add each route as a child of appRoot and track their names
|
||||
routes.forEach((route) => {
|
||||
router.addRoute('appRoot', route as any)
|
||||
this.dynamicRouteNames.push(route.name)
|
||||
})
|
||||
|
||||
// Generate side menu
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user