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)}`
|
||||
}
|
||||
Reference in New Issue
Block a user