Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
10
src/router/guards/afterEach.ts
Normal file
10
src/router/guards/afterEach.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
333
src/router/guards/beforeEach.ts
Normal file
333
src/router/guards/beforeEach.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
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'
|
||||
import {
|
||||
isInWhiteList,
|
||||
hasRoutePermission,
|
||||
isTokenValid,
|
||||
buildLoginRedirect
|
||||
} from './permission'
|
||||
|
||||
// 是否已注册动态路由
|
||||
const isRouteRegistered = ref(false)
|
||||
|
||||
// 临时开发模式:跳过所有权限验证(开发静态页面时使用)
|
||||
const DEV_MODE_SKIP_AUTH = true
|
||||
|
||||
/**
|
||||
* 路由全局前置守卫
|
||||
* 处理进度条、获取菜单列表、动态路由注册、404 检查、工作标签页及页面标题设置
|
||||
*/
|
||||
export function setupBeforeEachGuard(router: Router): void {
|
||||
router.beforeEach(
|
||||
async (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext
|
||||
) => {
|
||||
try {
|
||||
// 开发模式:直接放行,无需登录
|
||||
if (DEV_MODE_SKIP_AUTH) {
|
||||
const settingStore = useSettingStore()
|
||||
if (settingStore.showNprogress) {
|
||||
NProgress.start()
|
||||
}
|
||||
setSystemTheme(to)
|
||||
setPageTitle(to)
|
||||
|
||||
// 开发模式下,首次访问时注册所有路由
|
||||
if (!isRouteRegistered.value) {
|
||||
const menuList = asyncRoutes.map((route) => menuDataToRouter(route))
|
||||
const menuStore = useMenuStore()
|
||||
menuStore.setMenuList(menuList)
|
||||
registerDynamicRoutes(router, menuList)
|
||||
isRouteRegistered.value = true
|
||||
|
||||
// 重新导航到目标路由
|
||||
next({ path: to.path, query: to.query, hash: to.hash, replace: true })
|
||||
return
|
||||
}
|
||||
|
||||
// 设置工作标签页
|
||||
setWorktab(to)
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
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> {
|
||||
const { isLogin, accessToken, info } = userStore
|
||||
|
||||
// 如果访问的是白名单路由,直接放行
|
||||
if (isInWhiteList(to.path) || to.meta.noLogin) {
|
||||
// 如果已登录且访问登录页,重定向到首页
|
||||
if (isLogin && to.path === RoutesAlias.Login) {
|
||||
next(RoutesAlias.Home || '/')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
if (!isLogin || !accessToken) {
|
||||
userStore.logOut()
|
||||
next(buildLoginRedirect(to.fullPath))
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查 Token 是否有效
|
||||
if (!isTokenValid(accessToken)) {
|
||||
console.warn('Token 已过期,需要重新登录')
|
||||
userStore.logOut()
|
||||
next(buildLoginRedirect(to.fullPath))
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查页面级权限
|
||||
if (!hasRoutePermission(to, info)) {
|
||||
console.warn('无权限访问该页面:', to.path)
|
||||
next(RoutesAlias.Exception403 || '/exception/403')
|
||||
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()
|
||||
|
||||
try {
|
||||
const menuList = asyncRoutes.map((route) => menuDataToRouter(route))
|
||||
const userStore = useUserStore()
|
||||
const roles = userStore.info.roles
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
console.warn('用户角色信息不存在,清除登录状态')
|
||||
closeLoading()
|
||||
userStore.logOut()
|
||||
throw new Error('获取用户角色失败')
|
||||
}
|
||||
|
||||
const filteredMenuList = filterMenuByRoles(menuList, roles)
|
||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
||||
await registerAndStoreMenu(router, filteredMenuList, closeLoading)
|
||||
} catch (error) {
|
||||
closeLoading()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理后端控制模式的菜单逻辑
|
||||
*/
|
||||
async function processBackendMenu(router: Router): Promise<void> {
|
||||
const closeLoading = loadingService.showLoading()
|
||||
|
||||
try {
|
||||
const { menuList } = await menuService.getMenuList()
|
||||
await registerAndStoreMenu(router, menuList, closeLoading)
|
||||
} catch (error) {
|
||||
closeLoading()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册路由并存储菜单数据
|
||||
*/
|
||||
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
|
||||
|
||||
const worktabStore = useWorktabStore()
|
||||
worktabStore.validateWorktabs(router)
|
||||
|
||||
// 刷新后只保留固定标签页和当前标签页
|
||||
worktabStore.initializeAfterRefresh()
|
||||
|
||||
closeLoading()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理菜单相关错误
|
||||
*/
|
||||
function handleMenuError(error: unknown): void {
|
||||
console.error('菜单处理失败:', error)
|
||||
// 确保 loading 被关闭
|
||||
loadingService.hideLoading()
|
||||
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([])
|
||||
}
|
||||
128
src/router/guards/permission.ts
Normal file
128
src/router/guards/permission.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 权限验证相关工具函数
|
||||
*/
|
||||
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import type { UserInfo } from '@/types/api'
|
||||
|
||||
/**
|
||||
* 不需要登录的路由白名单
|
||||
*/
|
||||
export const LOGIN_WHITE_LIST = [
|
||||
'/auth/login',
|
||||
'/auth/register',
|
||||
'/auth/forget-password',
|
||||
'/exception/403',
|
||||
'/exception/404',
|
||||
'/exception/500'
|
||||
]
|
||||
|
||||
/**
|
||||
* 检查路由是否在白名单中
|
||||
*/
|
||||
export const isInWhiteList = (path: string): boolean => {
|
||||
return LOGIN_WHITE_LIST.some((whitePath) => {
|
||||
if (whitePath.endsWith('*')) {
|
||||
// 支持通配符匹配
|
||||
const prefix = whitePath.slice(0, -1)
|
||||
return path.startsWith(prefix)
|
||||
}
|
||||
return path === whitePath
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有权限访问路由
|
||||
*/
|
||||
export const hasRoutePermission = (
|
||||
route: RouteLocationNormalized,
|
||||
userInfo: Partial<UserInfo>
|
||||
): boolean => {
|
||||
const { roles = [], permissions = [] } = userInfo
|
||||
|
||||
// 如果路由没有设置权限要求,直接通过
|
||||
if (!route.meta?.roles && !route.meta?.permissions) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (route.meta.roles) {
|
||||
const routeRoles = route.meta.roles as string[]
|
||||
const hasRole = routeRoles.some((role) => roles.includes(role as any))
|
||||
if (!hasRole) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查操作权限
|
||||
if (route.meta.permissions) {
|
||||
const routePermissions = route.meta.permissions as string[]
|
||||
const hasPermission = routePermissions.some((permission) => {
|
||||
// 支持通配符权限 *:*:*
|
||||
if (permissions.includes('*:*:*')) {
|
||||
return true
|
||||
}
|
||||
// 精确匹配或前缀匹配
|
||||
return permissions.some((userPermission) => {
|
||||
if (userPermission.endsWith('*')) {
|
||||
const prefix = userPermission.slice(0, -1)
|
||||
return permission.startsWith(prefix)
|
||||
}
|
||||
return userPermission === permission
|
||||
})
|
||||
})
|
||||
if (!hasPermission) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Token 是否有效
|
||||
* 简单检查,真实项目中应该验证 JWT 或者调用后端接口
|
||||
*/
|
||||
export const isTokenValid = (token: string): boolean => {
|
||||
if (!token) return false
|
||||
|
||||
// Mock Token 格式: mock_token_{key}_{timestamp}
|
||||
if (token.startsWith('mock_token_')) {
|
||||
// Mock Token 永不过期(开发环境)
|
||||
return true
|
||||
}
|
||||
|
||||
// 真实 Token 可以在这里添加 JWT 解析和过期检查
|
||||
// 例如:
|
||||
// try {
|
||||
// const decoded = jwt_decode(token)
|
||||
// const isExpired = decoded.exp * 1000 < Date.now()
|
||||
// return !isExpired
|
||||
// } catch {
|
||||
// return false
|
||||
// }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取重定向路径
|
||||
* 登录后跳转到之前访问的页面
|
||||
*/
|
||||
export const getRedirectPath = (route: RouteLocationNormalized): string => {
|
||||
const redirect = route.query.redirect as string
|
||||
if (redirect && !isInWhiteList(redirect)) {
|
||||
return redirect
|
||||
}
|
||||
return '/'
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建登录重定向 URL
|
||||
*/
|
||||
export const buildLoginRedirect = (currentPath: string): string => {
|
||||
if (isInWhiteList(currentPath)) {
|
||||
return '/auth/login'
|
||||
}
|
||||
return `/auth/login?redirect=${encodeURIComponent(currentPath)}`
|
||||
}
|
||||
21
src/router/index.ts
Normal file
21
src/router/index.ts
Normal 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)
|
||||
}
|
||||
1024
src/router/routes/asyncRoutes.ts
Normal file
1024
src/router/routes/asyncRoutes.ts
Normal file
File diff suppressed because it is too large
Load Diff
72
src/router/routes/staticRoutes.ts
Normal file
72
src/router/routes/staticRoutes.ts
Normal 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' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
116
src/router/routesAlias.ts
Normal file
116
src/router/routesAlias.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 路由别名,方便快速找到页面,同时可以用作路由跳转
|
||||
*/
|
||||
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', // 文章发布
|
||||
Role = '/system/role', // 角色
|
||||
Permission = '/system/permission', // 权限管理
|
||||
Shop = '/product/shop', // 店铺管理
|
||||
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', // 更新日志
|
||||
|
||||
// 物联网卡管理系统模块
|
||||
// 我的网卡
|
||||
CardList = '/card-management/card-list', // 网卡管理
|
||||
CardDetail = '/card-management/card-detail', // 网卡明细
|
||||
SingleCard = '/my-simcard/single-card', // 单卡信息
|
||||
CardAssign = '/card-management/card-assign', // 网卡分配
|
||||
CardShutdown = '/card-management/card-shutdown', // 停机管理
|
||||
MyCards = '/card-management/my-cards', // 我的网卡
|
||||
OfflineBatchRecharge = '/card-management/offline-batch-recharge', // 线下批量充值
|
||||
CardTransfer = '/card-management/card-transfer', // 网卡转接
|
||||
CardReplacement = '/card-management/card-replacement', // 换卡管理
|
||||
PackageGift = '/card-management/package-gift', // 套餐赠送
|
||||
CardChangeCard = '/card-management/card-change-card', // 换卡网卡
|
||||
|
||||
// 我的套餐
|
||||
PackageCreate = '/package-management/package-create', // 新建套餐
|
||||
PackageBatch = '/package-management/package-batch', // 批量管理
|
||||
PackageList = '/package-management/package-list', // 我的套餐
|
||||
PackageChange = '/package-management/package-change', // 套餐变更
|
||||
PackageAssign = '/package-management/package-assign', // 套餐分配
|
||||
PackageSeries = '/package-management/package-series', // 套餐系列
|
||||
PackageCommission = '/package-management/package-commission', // 套餐佣金网卡
|
||||
|
||||
// 账号管理
|
||||
Account = '/account-management/account', // 账号管理
|
||||
PlatformAccount = '/account-management/platform-account', // 平台账号管理
|
||||
CustomerManagement = '/account-management/customer', // 客户管理
|
||||
CustomerRole = '/account-management/customer-role', // 客户角色
|
||||
AgentManagement = '/account-management/agent', // 代理商管理
|
||||
CustomerAccount = '/account-management/customer-account', // 客户账号管理
|
||||
ShopAccount = '/account-management/shop-account', // 代理账号管理
|
||||
EnterpriseCustomer = '/account-management/enterprise-customer', // 企业客户管理
|
||||
CustomerCommission = '/account-management/customer-commission', // 客户账号佣金
|
||||
|
||||
// 设备管理
|
||||
DeviceList = '/device-management/devices', // 设备管理
|
||||
|
||||
// 产品管理
|
||||
SimCardManagement = '/product/sim-card', // 网卡产品管理
|
||||
SimCardAssign = '/product/sim-card-assign', // 号卡分配
|
||||
|
||||
// 资产管理
|
||||
AssetAssign = '/asset-management/asset-assign', // 资产分配
|
||||
CardReplacementRequest = '/asset-management/card-replacement-request', // 换卡申请
|
||||
|
||||
// 账户管理
|
||||
CustomerAccountList = '/finance/customer-account', // 客户账号
|
||||
WithdrawalManagement = '/finance/withdrawal', // 佣金提现
|
||||
WithdrawalSettings = '/finance/withdrawal-settings', // 佣金提现设置
|
||||
MyAccount = '/finance/my-account', // 我的账户
|
||||
|
||||
// 设置管理
|
||||
PaymentMerchant = '/settings/payment-merchant', // 支付商户
|
||||
DeveloperApi = '/settings/developer-api', // 开发者API
|
||||
CommissionTemplate = '/settings/commission-template', // 分佣模板
|
||||
|
||||
// 批量操作
|
||||
SimImport = '/batch/sim-import', // 网卡批量导入
|
||||
DeviceImport = '/batch/device-import', // 设备批量导入
|
||||
CardChangeNotice = '/batch/card-change-notice' // 换卡通知
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
export const HOME_PAGE = RoutesAlias.Dashboard
|
||||
73
src/router/utils/menuToRouter.ts
Normal file
73
src/router/utils/menuToRouter.ts
Normal 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
src/router/utils/registerRoutes.ts
Normal file
251
src/router/utils/registerRoutes.ts
Normal 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
src/router/utils/utils.ts
Normal file
58
src/router/utils/utils.ts
Normal 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 ''
|
||||
}
|
||||
Reference in New Issue
Block a user