项目创建

This commit is contained in:
浅念
2025-07-04 17:16:38 +08:00
parent ce47b39fa5
commit 719911918f
372 changed files with 105035 additions and 32 deletions
+10
View File
@@ -0,0 +1,10 @@
import { useSettingStore } from '@/store/modules/setting'
import { Router } from 'vue-router'
import NProgress from 'nprogress'
/** 路由全局后置守卫 */
export function setupAfterEachGuard(router: Router) {
router.afterEach(() => {
if (useSettingStore().showNprogress) NProgress.done()
})
}
+245
View File
@@ -0,0 +1,245 @@
import type { Router, RouteLocationNormalized, NavigationGuardNext } from 'vue-router'
import NProgress from 'nprogress'
import { useSettingStore } from '@/store/modules/setting'
import { useUserStore } from '@/store/modules/user'
import { useMenuStore } from '@/store/modules/menu'
import { setWorktab } from '@/utils/navigation'
import { setPageTitle, setSystemTheme } from '../utils/utils'
import { menuService } from '@/api/menuApi'
import { registerDynamicRoutes } from '../utils/registerRoutes'
import { AppRouteRecord } from '@/types/router'
import { RoutesAlias } from '../routesAlias'
import { menuDataToRouter } from '../utils/menuToRouter'
import { asyncRoutes } from '../routes/asyncRoutes'
import { loadingService } from '@/utils/ui'
import { useCommon } from '@/composables/useCommon'
import { useWorktabStore } from '@/store/modules/worktab'
// 是否已注册动态路由
const isRouteRegistered = ref(false)
/**
* 路由全局前置守卫
* 处理进度条、获取菜单列表、动态路由注册、404 检查、工作标签页及页面标题设置
*/
export function setupBeforeEachGuard(router: Router): void {
router.beforeEach(
async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) => {
try {
await handleRouteGuard(to, from, next, router)
} catch (error) {
console.error('路由守卫处理失败:', error)
next('/exception/500')
}
}
)
}
/**
* 处理路由守卫逻辑
*/
async function handleRouteGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
router: Router
): Promise<void> {
const settingStore = useSettingStore()
const userStore = useUserStore()
// 处理进度条
if (settingStore.showNprogress) {
NProgress.start()
}
// 设置系统主题
setSystemTheme(to)
// 处理登录状态
if (!(await handleLoginStatus(to, userStore, next))) {
return
}
// 处理动态路由注册
if (!isRouteRegistered.value && userStore.isLogin) {
await handleDynamicRoutes(to, router, next)
return
}
// 处理已知的匹配路由
if (to.matched.length > 0) {
setWorktab(to)
setPageTitle(to)
next()
return
}
// 尝试刷新路由重新注册
if (userStore.isLogin) {
isRouteRegistered.value = false
await handleDynamicRoutes(to, router, next)
return
}
// 如果以上都不匹配,跳转到404
next(RoutesAlias.Exception404)
}
/**
* 处理登录状态
*/
async function handleLoginStatus(
to: RouteLocationNormalized,
userStore: ReturnType<typeof useUserStore>,
next: NavigationGuardNext
): Promise<boolean> {
if (!userStore.isLogin && to.path !== RoutesAlias.Login && !to.meta.noLogin) {
userStore.logOut()
next(RoutesAlias.Login)
return false
}
return true
}
/**
* 处理动态路由注册
*/
async function handleDynamicRoutes(
to: RouteLocationNormalized,
router: Router,
next: NavigationGuardNext
): Promise<void> {
try {
await getMenuData(router)
next({
path: to.path,
query: to.query,
hash: to.hash,
replace: true
})
} catch (error) {
console.error('动态路由注册失败:', error)
next('/exception/500')
}
}
/**
* 获取菜单数据
* @param router 路由实例
*/
async function getMenuData(router: Router): Promise<void> {
try {
if (useCommon().isFrontendMode.value) {
await processFrontendMenu(router) // 前端控制模式
} else {
await processBackendMenu(router) // 后端控制模式
}
} catch (error) {
handleMenuError(error)
}
}
/**
* 处理前端控制模式的菜单逻辑
*/
async function processFrontendMenu(router: Router): Promise<void> {
const closeLoading = loadingService.showLoading()
const menuList = asyncRoutes.map((route) => menuDataToRouter(route))
// const userStore = useUserStore()
// const roles = userStore.info.roles
const roles = ['R_SUPER']
if (!roles) {
closeLoading()
throw new Error('获取用户角色失败')
}
const filteredMenuList = filterMenuByRoles(menuList, roles)
await new Promise((resolve) => setTimeout(resolve, 300))
await registerAndStoreMenu(router, filteredMenuList, closeLoading)
}
/**
* 处理后端控制模式的菜单逻辑
*/
async function processBackendMenu(router: Router): Promise<void> {
const closeLoading = loadingService.showLoading()
const { menuList } = await menuService.getMenuList()
await registerAndStoreMenu(router, menuList, closeLoading)
}
/**
* 注册路由并存储菜单数据
*/
async function registerAndStoreMenu(
router: Router,
menuList: AppRouteRecord[],
closeLoading: () => void
): Promise<void> {
if (!isValidMenuList(menuList)) {
closeLoading()
throw new Error('获取菜单列表失败,请重新登录')
}
const menuStore = useMenuStore()
menuStore.setMenuList(menuList)
registerDynamicRoutes(router, menuList)
isRouteRegistered.value = true
useWorktabStore().validateWorktabs(router)
closeLoading()
}
/**
* 处理菜单相关错误
*/
function handleMenuError(error: unknown): void {
console.error('菜单处理失败:', error)
useUserStore().logOut()
throw error instanceof Error ? error : new Error('获取菜单列表失败,请重新登录')
}
/**
* 根据角色过滤菜单
*/
const filterMenuByRoles = (menu: AppRouteRecord[], roles: string[]): AppRouteRecord[] => {
return menu.reduce((acc: AppRouteRecord[], item) => {
const itemRoles = item.meta?.roles
const hasPermission = !itemRoles || itemRoles.some((role) => roles?.includes(role))
if (hasPermission) {
const filteredItem = { ...item }
if (filteredItem.children?.length) {
filteredItem.children = filterMenuByRoles(filteredItem.children, roles)
}
acc.push(filteredItem)
}
return acc
}, [])
}
/**
* 验证菜单列表是否有效
*/
function isValidMenuList(menuList: AppRouteRecord[]): boolean {
return Array.isArray(menuList) && menuList.length > 0
}
/**
* 重置路由相关状态
*/
export function resetRouterState(router: Router): void {
isRouteRegistered.value = false
// 清理动态注册的路由
router.getRoutes().forEach((route) => {
if (route.meta?.dynamic) {
router.removeRoute(route.name as string)
}
})
// 清空菜单数据
const menuStore = useMenuStore()
menuStore.setMenuList([])
}
+21
View File
@@ -0,0 +1,21 @@
import type { App } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router'
import { staticRoutes } from './routes/staticRoutes'
import { configureNProgress } from './utils/utils'
import { setupBeforeEachGuard } from './guards/beforeEach'
import { setupAfterEachGuard } from './guards/afterEach'
// 创建路由实例
export const router = createRouter({
history: createWebHashHistory(),
routes: staticRoutes, // 静态路由
scrollBehavior: () => ({ left: 0, top: 0 }) // 滚动行为
})
// 初始化路由
export function initRouter(app: App<Element>): void {
configureNProgress() // 顶部进度条
setupBeforeEachGuard(router) // 路由前置守卫
setupAfterEachGuard(router) // 路由后置守卫
app.use(router)
}
+362
View File
@@ -0,0 +1,362 @@
import { RoutesAlias } from '../routesAlias'
import { AppRouteRecord } from '@/types/router'
import { WEB_LINKS } from '@/utils/constants'
/**
* 菜单列表、异步路由
*
* 支持两种模式:
* 1. 前端静态配置 - 直接使用本文件中定义的路由配置
* 2. 后端动态配置 - 后端返回菜单数据,前端解析生成路由
*
* 菜单标题(title):
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
*/
export const asyncRoutes: AppRouteRecord[] = [
{
name: 'Dashboard',
path: '/dashboard',
component: RoutesAlias.Home,
meta: {
title: 'menus.dashboard.title',
icon: '&#xe721;'
},
children: [
{
path: 'console',
name: 'Console',
component: RoutesAlias.Dashboard,
meta: {
title: 'menus.dashboard.console',
keepAlive: false,
fixedTab: true
}
},
{
path: 'analysis',
name: 'Analysis',
component: RoutesAlias.Analysis,
meta: {
title: 'menus.dashboard.analysis',
keepAlive: false
}
},
{
path: 'ecommerce',
name: 'Ecommerce',
component: RoutesAlias.Ecommerce,
meta: {
title: 'menus.dashboard.ecommerce',
keepAlive: false
}
}
]
},
{
path: '/work',
name: 'Work',
component: RoutesAlias.Home,
meta: {
title: 'menus.work.title',
icon: '&#xe88b;',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'order',
name: 'Order',
component: RoutesAlias.Order,
meta: {
title: 'menus.work.order',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
},
{
path: 'type',
name: 'Type',
component: RoutesAlias.OrderType,
meta: {
title: 'menus.work.type',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
}
]
},
{
path: '/system',
name: 'System',
component: RoutesAlias.Home,
meta: {
title: 'menus.system.title',
icon: '&#xe7b9;',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'user',
name: 'User',
component: RoutesAlias.User,
meta: {
title: 'menus.system.user',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
},
{
path: 'role',
name: 'Role',
component: RoutesAlias.Role,
meta: {
title: 'menus.system.role',
keepAlive: true,
roles: ['R_SUPER']
}
},
{
path: 'user-center',
name: 'UserCenter',
component: RoutesAlias.UserCenter,
meta: {
title: 'menus.system.userCenter',
isHide: true,
keepAlive: true,
isHideTab: true
}
},
{
path: 'menu',
name: 'Menus',
component: RoutesAlias.Menu,
meta: {
title: 'menus.system.menu',
keepAlive: true,
roles: ['R_SUPER'],
authList: [
{
title: '新增',
auth_mark: 'add'
},
{
title: '编辑',
auth_mark: 'edit'
},
{
title: '删除',
auth_mark: 'delete'
}
]
}
},
{
path: 'nested',
name: 'Nested',
component: '',
meta: {
title: 'menus.system.nested',
keepAlive: true
},
children: [
{
path: 'menu1',
name: 'NestedMenu1',
component: RoutesAlias.NestedMenu1,
meta: {
title: 'menus.system.menu1',
icon: '&#xe676;',
keepAlive: true
}
},
{
path: 'menu2',
name: 'NestedMenu2',
component: '',
meta: {
title: 'menus.system.menu2',
icon: '&#xe676;',
keepAlive: true
},
children: [
{
path: 'menu2-1',
name: 'NestedMenu2-1',
component: RoutesAlias.NestedMenu21,
meta: {
title: 'menus.system.menu21',
icon: '&#xe676;',
keepAlive: true
}
}
]
},
{
path: 'menu3',
name: 'NestedMenu3',
component: '',
meta: {
title: 'menus.system.menu3',
icon: '&#xe676;',
keepAlive: true
},
children: [
{
path: 'menu3-1',
name: 'NestedMenu3-1',
component: RoutesAlias.NestedMenu31,
meta: {
title: 'menus.system.menu31',
icon: '&#xe676;',
keepAlive: true
}
},
{
path: 'menu3-2',
name: 'NestedMenu3-2',
component: '',
meta: {
title: 'menus.system.menu32',
icon: '&#xe676;',
keepAlive: true
},
children: [
{
path: 'menu3-2-1',
name: 'NestedMenu3-2-1',
component: RoutesAlias.NestedMenu321,
meta: {
title: 'menus.system.menu321',
icon: '&#xe676;',
keepAlive: true
}
}
]
}
]
}
]
}
]
},
{
path: '/car',
name: 'Car',
component: RoutesAlias.Home,
meta: {
title: 'menus.car.title',
icon: '&#xe7fd;',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'usedcar',
name: 'Usedcar',
component: RoutesAlias.Usedcar,
meta: {
title: 'menus.car.used',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
},
{
path: 'newcar',
name: 'Newcar',
component: RoutesAlias.Newcar,
meta: {
title: 'menus.car.newcar',
keepAlive: true,
roles: ['R_SUPER']
}
}
]
},
{
path: '/article',
name: 'Article',
component: RoutesAlias.Home,
meta: {
title: 'menus.article.title',
icon: '&#xe7ae;',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'article-list',
name: 'ArticleList',
component: RoutesAlias.ArticleList,
meta: {
title: 'menus.article.articleList',
keepAlive: true,
authList: [
{
title: '新增',
auth_mark: 'add'
},
{
title: '编辑',
auth_mark: 'edit'
}
]
}
},
{
path: 'detail',
name: 'ArticleDetail',
component: RoutesAlias.ArticleDetail,
meta: {
title: 'menus.article.articleDetail',
isHide: true,
keepAlive: true,
activePath: '/article/article-list' // 激活菜单路径
}
},
{
path: 'comment',
name: 'ArticleComment',
component: RoutesAlias.Comment,
meta: {
title: 'menus.article.comment',
keepAlive: true
}
},
{
path: 'publish',
name: 'ArticlePublish',
component: RoutesAlias.ArticlePublish,
meta: {
title: 'menus.article.articlePublish',
keepAlive: true,
authList: [
{
title: '发布',
auth_mark: 'article/article-publish/add'
}
]
}
}
]
},
{
path: '/safeguard',
name: 'Safeguard',
component: RoutesAlias.Home,
meta: {
title: 'menus.safeguard.title',
icon: '&#xe816;',
keepAlive: false
},
children: [
{
path: 'server',
name: 'SafeguardServer',
component: RoutesAlias.Server,
meta: {
title: 'menus.safeguard.server',
keepAlive: true
}
}
]
}
]
+72
View File
@@ -0,0 +1,72 @@
import { AppRouteRecordRaw } from '../utils/utils'
import { RoutesAlias, HOME_PAGE } from '../routesAlias'
import Home from '@views/index/index.vue'
/**
* 静态路由配置
* 不需要权限就能访问的路由
*/
export const staticRoutes: AppRouteRecordRaw[] = [
{
path: '/',
redirect: HOME_PAGE
},
{
path: RoutesAlias.Login,
name: 'Login',
component: () => import('@views/auth/login/index.vue'),
meta: { title: 'menus.login.title', isHideTab: true, setTheme: true }
},
{
path: RoutesAlias.Register,
name: 'Register',
component: () => import('@views/auth/register/index.vue'),
meta: { title: 'menus.register.title', isHideTab: true, noLogin: true, setTheme: true }
},
{
path: RoutesAlias.ForgetPassword,
name: 'ForgetPassword',
component: () => import('@views/auth/forget-password/index.vue'),
meta: { title: 'menus.forgetPassword.title', isHideTab: true, noLogin: true, setTheme: true }
},
{
path: '/exception',
component: Home,
name: 'Exception',
meta: { title: 'menus.exception.title' },
children: [
{
path: RoutesAlias.Exception403,
name: 'Exception403',
component: () => import('@views/exception/403/index.vue'),
meta: { title: '403' }
},
{
path: '/:catchAll(.*)',
name: 'Exception404',
component: () => import('@views/exception/404/index.vue'),
meta: { title: '404' }
},
{
path: RoutesAlias.Exception500,
name: 'Exception500',
component: () => import('@views/exception/500/index.vue'),
meta: { title: '500' }
}
]
},
{
path: '/outside',
component: Home,
name: 'Outside',
meta: { title: 'menus.outside.title' },
children: [
{
path: '/outside/iframe/:path',
name: 'Iframe',
component: () => import('@/views/outside/Iframe.vue'),
meta: { title: 'iframe' }
}
]
}
]
+58
View File
@@ -0,0 +1,58 @@
/**
* 路由别名,方便快速找到页面,同时可以用作路由跳转
*/
export enum RoutesAlias {
Home = '/index/index', // 布局容器
Login = '/auth/login', // 登录
Register = '/auth/register', // 注册
ForgetPassword = '/auth/forget-password', // 忘记密码
Exception403 = '/exception/403', // 403
Exception404 = '/exception/404', // 404
Exception500 = '/exception/500', // 500
Success = '/result/success', // 成功
Fail = '/result/fail', // 失败
Dashboard = '/dashboard/console', // 工作台
Analysis = '/dashboard/analysis', // 分析页
Ecommerce = '/dashboard/ecommerce', // 电子商务
IconList = '/widgets/icon-list', // 图标列表
IconSelector = '/widgets/icon-selector', // 图标选择器
ImageCrop = '/widgets/image-crop', // 图片裁剪
Excel = '/widgets/excel', // Excel
Video = '/widgets/video', // 视频
CountTo = '/widgets/count-to', // 计数
WangEditor = '/widgets/wang-editor', // 富文本编辑器
Watermark = '/widgets/watermark', // 水印
ContextMenu = '/widgets/context-menu', // 上下文菜单
Qrcode = '/widgets/qrcode', // 二维码
Drag = '/widgets/drag', // 拖拽
TextScroll = '/widgets/text-scroll', // 文字滚动
Fireworks = '/widgets/fireworks', // 礼花效果
Chat = '/template/chat', // 聊天
Cards = '/template/cards', // 卡片
Banners = '/template/banners', // 横幅
Charts = '/template/charts', // 图表
Map = '/template/map', // 地图
Calendar = '/template/calendar', // 日历
Pricing = '/template/pricing', // 定价
ArticleList = '/article/list', // 文章列表
ArticleDetail = '/article/detail', // 文章详情
Comment = '/article/comment', // 评论
ArticlePublish = '/article/publish', // 文章发布
User = '/system/user', // 账户
Role = '/system/role', // 角色
Usedcar = '/car/usedcar', // 角色
Newcar = '/car/newcar', // 角色
UserCenter = '/system/user-center', // 用户中心
Menu = '/system/menu', // 菜单
NestedMenu1 = '/system/nested/menu1', // 嵌套菜单1
NestedMenu21 = '/system/nested/menu2', // 嵌套菜单2-1
NestedMenu31 = '/system/nested/menu3', // 嵌套菜单3-1
NestedMenu321 = '/system/nested/menu3/menu3-2', // 嵌套菜单3-2-1
Server = '/safeguard/server', // 服务器
ChangeLog = '/change/log', // 更新日志
Order = '/work/order', // 工单
OrderType = '/work/type' // 工单
}
// 主页路由
export const HOME_PAGE = RoutesAlias.Dashboard
+73
View File
@@ -0,0 +1,73 @@
import { AppRouteRecord } from '@/types/router'
/**
* 将菜单数据转换为路由配置
* @param route 菜单数据对象
* @param parentPath 父级路径
* @returns 处理后的路由配置
*/
export const menuDataToRouter = (route: AppRouteRecord, parentPath = ''): AppRouteRecord => {
const { id, name, component, meta, children } = route
const fullPath = buildRoutePath(route, parentPath)
return {
id,
name,
path: fullPath,
component,
meta,
children: processChildren(children || [], fullPath)
}
}
/**
* 构建路由完整路径
* @param route 菜单数据对象
* @param parentPath 父级路径
* @returns 构建后的完整路径
*/
const buildRoutePath = (route: AppRouteRecord, parentPath: string): string => {
if (!route.path) return ''
// iframe 类型路由直接使用原始路径
if (route.meta?.isIframe) return route.path
// 拼接并规范化路径
return parentPath ? `${parentPath}/${route.path}`.replace(/\/+/g, '/') : route.path
}
/**
* 处理子路由
* @param children 子路由数组
* @param parentPath 父级路径
* @returns 处理后的子路由数组
*/
const processChildren = (children: AppRouteRecord[], parentPath: string): AppRouteRecord[] => {
if (!Array.isArray(children) || children.length === 0) return []
return children.map((child) => menuDataToRouter(child, parentPath))
}
/**
* 保存 iframe 路由到 sessionStorage 中
* @param list iframe 路由列表
*/
export const saveIframeRoutes = (list: AppRouteRecord[]): void => {
if (list.length > 0) {
sessionStorage.setItem('iframeRoutes', JSON.stringify(list))
}
}
/**
* 获取 iframe 路由
* @returns iframe 路由列表
*/
export const getIframeRoutes = (): AppRouteRecord[] => {
try {
return JSON.parse(sessionStorage.getItem('iframeRoutes') || '[]')
} catch (error) {
console.error('解析 iframe 路由失败:', error)
return []
}
}
+251
View File
@@ -0,0 +1,251 @@
/**
* 动态路由处理
* 根据接口返回的菜单列表注册动态路由
*/
import type { Router, RouteRecordRaw } from 'vue-router'
import type { AppRouteRecord } from '@/types/router'
import { saveIframeRoutes } from './menuToRouter'
import { RoutesAlias } from '../routesAlias'
import { h } from 'vue'
/**
* 动态导入 views 目录下所有 .vue 组件
*/
const modules: Record<string, () => Promise<any>> = import.meta.glob('../../views/**/*.vue')
/**
* 注册异步路由
* 将接口返回的菜单列表转换为 Vue Router 路由配置,并添加到传入的 router 实例中
* @param router Vue Router 实例
* @param menuList 接口返回的菜单列表
*/
export function registerDynamicRoutes(router: Router, menuList: AppRouteRecord[]): void {
// 用于局部收集 iframe 类型路由
const iframeRoutes: AppRouteRecord[] = []
// 检测菜单列表中是否有重复路由
checkDuplicateRoutes(menuList)
// 遍历菜单列表,注册路由
menuList.forEach((route) => {
// 只有还没注册过的路由才进行注册
if (route.name && !router.hasRoute(route.name)) {
const routeConfig = convertRouteComponent(route, iframeRoutes)
router.addRoute(routeConfig as RouteRecordRaw)
}
})
// 保存 iframe 路由
saveIframeRoutes(iframeRoutes)
}
/**
* 路径解析函数:处理父路径和子路径的拼接
*/
function resolvePath(parent: string, child: string): string {
return [parent.replace(/\/$/, ''), child.replace(/^\//, '')].filter(Boolean).join('/')
}
/**
* 检测菜单中的重复路由(包括子路由)
*/
function checkDuplicateRoutes(routes: AppRouteRecord[], parentPath = ''): void {
// 用于检测动态路由中的重复项
const routeNameMap = new Map<string, string>() // 路由名称 -> 路径
const componentPathMap = new Map<string, string>() // 组件路径 -> 路由信息
const checkRoutes = (routes: AppRouteRecord[], parentPath = '') => {
routes.forEach((route) => {
// 处理路径拼接
const currentPath = route.path || ''
const fullPath = resolvePath(parentPath, currentPath)
// 名称重复检测
if (route.name) {
if (routeNameMap.has(String(route.name))) {
console.warn(`[路由警告] 名称重复: "${String(route.name)}"`)
} else {
routeNameMap.set(String(route.name), fullPath)
}
}
// 组件路径重复检测
if (route.component) {
const componentPath = getComponentPathString(route.component)
if (componentPath && componentPath !== RoutesAlias.Home) {
const componentKey = `${parentPath}:${componentPath}`
if (componentPathMap.has(componentKey)) {
console.warn(`[路由警告] 路径重复: "${componentPath}"`)
} else {
componentPathMap.set(componentKey, fullPath)
}
}
}
// 递归处理子路由
if (route.children?.length) {
checkRoutes(route.children, fullPath)
}
})
}
checkRoutes(routes, parentPath)
}
/**
* 获取组件路径的字符串表示
*/
function getComponentPathString(component: any): string {
if (typeof component === 'string') {
return component
}
// 对于其他别名路由,获取组件名称
for (const key in RoutesAlias) {
if (RoutesAlias[key as keyof typeof RoutesAlias] === component) {
return `RoutesAlias.${key}`
}
}
return ''
}
/**
* 根据组件路径动态加载组件
* @param componentPath 组件路径(不包含 ../../views 前缀和 .vue 后缀)
* @param routeName 当前路由名称(用于错误提示)
* @returns 组件加载函数
*/
function loadComponent(componentPath: string, routeName: string): () => Promise<any> {
// 如果路径为空,直接返回一个空的组件
if (componentPath === '') {
return () =>
Promise.resolve({
render() {
return h('div', {})
}
})
}
// 构建可能的路径
const fullPath = `../../views${componentPath}.vue`
const fullPathWithIndex = `../../views${componentPath}/index.vue`
// 先尝试直接路径,再尝试添加/index的路径
const module = modules[fullPath] || modules[fullPathWithIndex]
if (!module) {
console.error(
`[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}`
)
return () =>
Promise.resolve({
render() {
return h('div', `组件未找到: ${routeName}`)
}
})
}
return module
}
/**
* 转换后的路由配置类型
*/
interface ConvertedRoute extends Omit<RouteRecordRaw, 'children'> {
id?: number
children?: ConvertedRoute[]
component?: RouteRecordRaw['component'] | (() => Promise<any>)
}
/**
* 转换路由组件配置
*/
function convertRouteComponent(
route: AppRouteRecord,
iframeRoutes: AppRouteRecord[],
depth = 0
): ConvertedRoute {
const { component, children, ...routeConfig } = route
// 基础路由配置
const converted: ConvertedRoute = {
...routeConfig,
component: undefined
}
// 是否为一级菜单
const isFirstLevel = depth === 0 && route.children?.length === 0
if (route.meta.isIframe) {
handleIframeRoute(converted, route, iframeRoutes)
} else if (isFirstLevel) {
handleLayoutRoute(converted, route, component as string)
} else {
handleNormalRoute(converted, component as string, String(route.name))
}
// 递归时增加深度
if (children?.length) {
converted.children = children.map((child) =>
convertRouteComponent(child, iframeRoutes, depth + 1)
)
}
return converted
}
/**
* 处理 iframe 类型路由
*/
function handleIframeRoute(
converted: ConvertedRoute,
route: AppRouteRecord,
iframeRoutes: AppRouteRecord[]
): void {
converted.path = `/outside/iframe/${String(route.name)}`
converted.component = () => import('@/views/outside/Iframe.vue')
iframeRoutes.push(route)
}
/**
* 处理一级菜单路由
*/
function handleLayoutRoute(
converted: ConvertedRoute,
route: AppRouteRecord,
component: string | undefined
): void {
converted.component = () => import('@/views/index/index.vue')
converted.path = `/${(route.path?.split('/')[1] || '').trim()}`
converted.name = ''
route.meta.isFirstLevel = true
converted.children = [
{
id: route.id,
path: route.path,
name: route.name,
component: loadComponent(component as string, String(route.name)),
meta: route.meta
}
]
}
/**
* 处理普通路由
*/
function handleNormalRoute(
converted: ConvertedRoute,
component: string | undefined,
routeName: string
): void {
if (component) {
const aliasComponent = RoutesAlias[
component as keyof typeof RoutesAlias
] as unknown as RouteRecordRaw['component']
converted.component = aliasComponent || loadComponent(component as string, routeName)
}
}
+58
View File
@@ -0,0 +1,58 @@
import { useTheme } from '@/composables/useTheme'
import { useSettingStore } from '@/store/modules/setting'
import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
import AppConfig from '@/config'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { $t } from '@/locales'
/** 扩展的路由配置类型 */
export type AppRouteRecordRaw = RouteRecordRaw & {
hidden?: boolean
}
/** 顶部进度条配置 */
export const configureNProgress = () => {
NProgress.configure({
easing: 'ease',
speed: 600,
showSpinner: false,
trickleSpeed: 200,
parent: 'body'
})
}
/**
* 设置页面标题,根据路由元信息和系统信息拼接标题
* @param to 当前路由对象
*/
export const setPageTitle = (to: RouteLocationNormalized): void => {
const { title } = to.meta
if (title) {
setTimeout(() => {
document.title = `${formatMenuTitle(String(title))} - ${AppConfig.systemInfo.name}`
}, 150)
}
}
/**
* 根据路由元信息设置系统主题
* @param to 当前路由对象
*/
export const setSystemTheme = (to: RouteLocationNormalized): void => {
if (to.meta.setTheme) {
useTheme().switchThemeStyles(useSettingStore().systemThemeType)
}
}
/**
* 格式化菜单标题
* @param title 菜单标题,可以是 i18n 的 key,也可以是字符串
* @returns 格式化后的菜单标题
*/
export const formatMenuTitle = (title: string): string => {
if (title) {
return title.startsWith('menus.') ? $t(title) : title
}
return ''
}