This commit is contained in:
2026-03-22 01:07:31 +08:00
parent ffac6fc267
commit faf00ec231
333 changed files with 35212 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
import type { NavigationFailure, RouteLocationRaw, Router } from 'vue-router'
import pinia from '@/store'
function getId(router: Router) {
return router.currentRoute.value.fullPath
}
function extendPush(router: Router) {
const originalPush = router.push
router.push = function (to: RouteLocationRaw) {
const settingsStore = useSettingsStore(pinia)
if (settingsStore.settings.tabbar.enable) {
const tabbarStore = useTabbarStore(pinia)
const index = tabbarStore.list.findIndex(item => item.tabId === getId(router))
tabbarStore.$patch({
leaveIndex: index,
})
}
return originalPush(to)
}
}
function extendGo(router: Router) {
const originalGo = router.go
router.go = function (delta: number) {
const settingsStore = useSettingsStore(pinia)
if (settingsStore.settings.tabbar.enable) {
const tabId = getId(router)
const tabbarStore = useTabbarStore(pinia)
originalGo(delta)
if (delta < 0) {
tabbarStore.remove(tabId)
}
}
else {
originalGo(delta)
}
}
}
function extendReplace(router: Router) {
const originalReplace = router.replace
router.replace = function (to: RouteLocationRaw) {
const settingsStore = useSettingsStore(pinia)
if (settingsStore.settings.tabbar.enable) {
const tabId = getId(router)
const tabbarStore = useTabbarStore(pinia)
return originalReplace(to).then(() => {
tabbarStore.remove(tabId)
})
}
else {
return originalReplace(to)
}
}
}
declare module 'vue-router' {
interface Router {
/**
* 本方法为框架扩展语法,等同于 `push` 方法,并且同时会关闭当前标签页
*/
close: (to: RouteLocationRaw) => Promise<NavigationFailure | void | undefined>
}
}
function extendClose(router: Router) {
router.close = function (to: RouteLocationRaw) {
const currentRoute = router.currentRoute.value
const tabId = getId(router)
return router.push(to).then(() => {
const settingsStore = useSettingsStore(pinia)
if (settingsStore.settings.tabbar.enable && currentRoute.meta.tabMerge !== 'activeMenu') {
const tabbarStore = useTabbarStore(pinia)
tabbarStore.remove(tabId)
}
})
}
}
export default function setupExtensions(router: Router) {
extendPush(router)
extendGo(router)
extendReplace(router)
extendClose(router)
}

View File

@@ -0,0 +1,226 @@
import type { Router, RouteRecordRaw } from 'vue-router'
import { useNProgress } from '@vueuse/integrations/useNProgress'
import { asyncRoutes, asyncRoutesByFilesystem } from './routes'
import '@/assets/styles/nprogress.css'
function setupRoutes(router: Router) {
router.beforeEach(async (to, _from, next) => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
const routeStore = useRouteStore()
const menuStore = useMenuStore()
// 是否已登录
if (userStore.isLogin) {
// 是否已根据权限动态生成并注册路由
if (routeStore.isGenerate) {
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
settingsStore.settings.menu.mode !== 'single' && menuStore.setActived(to.path)
// 如果已登录状态下,进入登录页会强制跳转到主页
if (to.name === 'login') {
next({
path: settingsStore.settings.home.fullPath,
replace: true,
})
}
// 如果未开启主页,但进入的是主页,则会进入侧边栏导航第一个模块
else if (!settingsStore.settings.home.enable && to.fullPath === settingsStore.settings.home.fullPath) {
if (menuStore.sidebarMenus.length > 0) {
next({
path: menuStore.sidebarMenusFirstDeepestPath,
replace: true,
})
}
// 如果侧边栏导航第一个模块均无法命中,则还是进入主页
else {
next()
}
}
// 正常访问页面
else {
next()
}
}
else {
try {
// 获取用户权限
settingsStore.settings.app.enablePermission && await userStore.getPermissions()
// 生成动态路由
switch (settingsStore.settings.app.routeBaseOn) {
case 'frontend':
routeStore.generateRoutesAtFront(asyncRoutes)
break
case 'backend':
await routeStore.generateRoutesAtBack()
break
case 'filesystem':
routeStore.generateRoutesAtFilesystem(asyncRoutesByFilesystem)
// 文件系统生成的路由,需要手动生成导航数据
switch (settingsStore.settings.menu.baseOn) {
case 'frontend':
menuStore.generateMenusAtFront()
break
case 'backend':
await menuStore.generateMenusAtBack()
break
}
break
}
// 注册并记录路由数据
// 记录的数据会在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
const removeRoutes: (() => void)[] = []
routeStore.routes.forEach((route) => {
if (!/^(?:https?:|mailto:|tel:)/.test(route.path)) {
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
}
})
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
routeStore.systemRoutes.forEach((route) => {
removeRoutes.push(router.addRoute(route as RouteRecordRaw))
})
}
routeStore.setCurrentRemoveRoutes(removeRoutes)
}
catch {
userStore.logout()
}
// 动态路由生成并注册后,重新进入当前路由
next({
path: to.path,
query: to.query,
replace: true,
})
}
}
else {
if (to.name !== 'login') {
next({
name: 'login',
query: {
redirect: to.fullPath !== settingsStore.settings.home.fullPath ? to.fullPath : undefined,
},
})
}
else {
next()
}
}
})
}
// 当父级路由未配置重定向时,自动重定向到有访问权限的子路由
function setupRedirectAuthChildrenRoute(router: Router) {
router.beforeEach((to, _from, next) => {
const { auth } = useAuth()
const currentRoute = router.getRoutes().find(route => route.path === (to.matched.at(-1)?.path ?? ''))
if (!currentRoute?.redirect) {
const findAuthRoute = currentRoute?.children?.find(route => route.meta?.menu !== false && auth(route.meta?.auth ?? ''))
if (findAuthRoute) {
next(findAuthRoute)
}
else {
next()
}
}
else {
next()
}
})
}
// 进度条
function setupProgress(router: Router) {
const { isLoading } = useNProgress()
router.beforeEach((_to, _from, next) => {
const settingsStore = useSettingsStore()
if (settingsStore.settings.app.enableProgress) {
isLoading.value = true
}
next()
})
router.afterEach(() => {
const settingsStore = useSettingsStore()
if (settingsStore.settings.app.enableProgress) {
isLoading.value = false
}
})
}
// 标题
function setupTitle(router: Router) {
router.afterEach((to) => {
const settingsStore = useSettingsStore()
if (settingsStore.settings.app.routeBaseOn !== 'filesystem') {
settingsStore.setTitle(to.matched?.at(-1)?.meta?.title ?? to.meta.title)
}
else {
settingsStore.setTitle(to.meta.title)
}
})
}
// 页面缓存
function setupKeepAlive(router: Router) {
router.afterEach(async (to, from) => {
const keepAliveStore = useKeepAliveStore()
if (to.fullPath !== from.fullPath) {
if (to.meta.cache) {
const componentName = to.matched.at(-1)?.components?.default.name
if (componentName) {
// 缓存当前页面前,先判断是否需要进行清除缓存,判断依据:
// 1. 如果 to.meta.cache 为 boolean 类型,并且不为 true则需要清除缓存
// 2. 如果 to.meta.cache 为 string 类型,并且与 from.name 不一致,则需要清除缓存
// 3. 如果 to.meta.cache 为 array 类型,并且不包含 from.name则需要清除缓存
// 4. 如果 to.meta.noCache 为 string 类型,并且与 from.name 一致,则需要清除缓存
// 5. 如果 to.meta.noCache 为 array 类型,并且包含 from.name则需要清除缓存
// 6. 如果是刷新页面,则需要清除缓存
let shouldClearCache = false
if (typeof to.meta.cache === 'boolean') {
shouldClearCache = !to.meta.cache
}
else if (typeof to.meta.cache === 'string') {
shouldClearCache = to.meta.cache !== from.name
}
else if (Array.isArray(to.meta.cache)) {
shouldClearCache = !to.meta.cache.includes(from.name as string)
}
if (to.meta.noCache) {
if (typeof to.meta.noCache === 'string') {
shouldClearCache = to.meta.noCache === from.name
}
else if (Array.isArray(to.meta.noCache)) {
shouldClearCache = to.meta.noCache.includes(from.name as string)
}
}
if (from.name === 'reload') {
shouldClearCache = true
}
if (shouldClearCache) {
keepAliveStore.remove(componentName)
await nextTick()
}
keepAliveStore.add(componentName)
}
else {
// turbo-console-disable-next-line
console.warn('[Fantastic-admin] 该页面组件未设置组件名,会导致缓存失效,请检查')
}
}
}
})
}
// 其他
function setupOther(router: Router) {
router.afterEach(() => {
document.documentElement.scrollTop = 0
})
}
export default function setupGuards(router: Router) {
setupRoutes(router)
setupRedirectAuthChildrenRoute(router)
setupProgress(router)
setupTitle(router)
setupKeepAlive(router)
setupOther(router)
}

21
admin-web/src/router/index.ts Executable file
View File

@@ -0,0 +1,21 @@
import { loadingFadeOut } from 'virtual:app-loading'
import { createRouter, createWebHashHistory } from 'vue-router'
import pinia from '@/store'
import setupExtensions from './extensions'
import setupGuards from './guards'
// 路由相关数据
import { constantRoutes, constantRoutesByFilesystem } from './routes'
const router = createRouter({
history: createWebHashHistory(),
routes: useSettingsStore(pinia).settings.app.routeBaseOn === 'filesystem' ? constantRoutesByFilesystem : constantRoutes,
})
setupGuards(router)
setupExtensions(router)
router.isReady().then(() => {
loadingFadeOut()
})
export default router

View File

@@ -0,0 +1,69 @@
import type { RouteRecordRaw } from 'vue-router'
function Layout() {
return import('@/layouts/index.vue')
}
const routes: RouteRecordRaw = {
path: '/multilevel_menu_example',
component: Layout,
name: 'multilevelMenuExample',
meta: {
title: '多级导航',
icon: 'i-heroicons-solid:menu-alt-3',
},
children: [
{
path: 'page',
name: 'multilevelMenuExample1',
component: () => import('@/views/multilevel_menu_example/page.vue'),
meta: {
title: '导航1',
},
},
{
path: 'level2',
name: 'multilevelMenuExample2',
meta: {
title: '导航2',
},
children: [
{
path: 'page',
name: 'multilevelMenuExample2-1',
component: () => import('@/views/multilevel_menu_example/level2/page.vue'),
meta: {
title: '导航2-1',
},
},
{
path: 'level3',
name: 'multilevelMenuExample2-2',
meta: {
title: '导航2-2',
},
children: [
{
path: 'page1',
name: 'multilevelMenuExample2-2-1',
component: () => import('@/views/multilevel_menu_example/level2/level3/page1.vue'),
meta: {
title: '导航2-2-1',
},
},
{
path: 'page2',
name: 'multilevelMenuExample2-2-2',
component: () => import('@/views/multilevel_menu_example/level2/level3/page2.vue'),
meta: {
title: '导航2-2-2',
},
},
],
},
],
},
],
}
export default routes

86
admin-web/src/router/routes.ts Executable file
View File

@@ -0,0 +1,86 @@
import type { Route } from '#/global'
import type { RouteRecordRaw } from 'vue-router'
import generatedRoutes from 'virtual:generated-pages'
import { setupLayouts } from 'virtual:meta-layouts'
import MultilevelMenuExample from './modules/multilevel.menu.example'
// 固定路由(默认路由)
const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: () => import('@/views/login.vue'),
meta: {
title: '登录',
},
},
{
path: '/:all(.*)*',
name: 'notFound',
component: () => import('@/views/[...all].vue'),
meta: {
title: '找不到页面',
},
},
]
// 系统路由
const systemRoutes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/index.vue'),
meta: {
title: () => useSettingsStore().settings.home.title,
breadcrumb: false,
},
children: [
{
path: '',
component: () => import('@/views/index.vue'),
meta: {
title: () => useSettingsStore().settings.home.title,
icon: 'i-ant-design:home-twotone',
breadcrumb: false,
},
},
{
path: 'reload',
name: 'reload',
component: () => import('@/views/reload.vue'),
meta: {
title: '重新加载',
breadcrumb: false,
},
},
],
},
]
// 动态路由(异步路由、导航栏路由)
const asyncRoutes: Route.recordMainRaw[] = [
{
meta: {
title: '演示',
icon: 'i-uim:box',
},
children: [
MultilevelMenuExample,
],
},
]
const constantRoutesByFilesystem = generatedRoutes.filter((item) => {
return item.meta?.enabled !== false && item.meta?.constant === true
})
const asyncRoutesByFilesystem = [...setupLayouts(generatedRoutes.filter((item) => {
return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false
}))]
export {
asyncRoutes,
asyncRoutesByFilesystem,
constantRoutes,
constantRoutesByFilesystem,
systemRoutes,
}