功能8:页面权限控制
功能7:路由全局前置守卫
功能6:动态添加路由记录
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目
前言
在项目中,我们规定只有首页(/login),是不需要用户登录,就可以正常访问的。其他页面,都需要登录才能访问。如果用户在没有登录时,直接访问http://localhost:5173/index,就会被重定向到登录页。
一.操作步骤
1.安装依赖
js-cookie是一个方便操作cookie的工具包
pnpm add pinia
pnpm add js-cookie
2.创建 Pinia 实例
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'const app = createApp(App)
app.use(createPinia())
app.mount('#app')
3.定义userStore
用户的登录状态,统一保存在userStore里。
新建文件:src/stores/user.js
import { defineStore } from 'pinia'
import { setToken } from '@/utils/auth'const useUserStore = defineStore('user', () => {// 异步操作const login = async () => {await setToken('test-token')}return {login}
})export default useUserStore
4.修改登录按钮逻辑
登录时,调用userStore里的login方法。登录成功后,再进行页面跳转。
<template><div class="login-container"><div class="login-box"><h2 class="title">用户登录</h2><el-form ref="loginForm" :model="formData" :rules="rules" @submit.prevent="handleLogin"><el-form-item prop="username"><el-inputv-model="formData.username"placeholder="请输入用户名":prefix-icon="User"/></el-form-item><el-form-item prop="password"><el-inputv-model="formData.password"type="password"placeholder="请输入密码"show-password:prefix-icon="Lock"/></el-form-item><el-form-item><el-button type="primary" native-type="submit"class="login-btn">登录</el-button></el-form-item></el-form></div></div></template><script setup>import { ref, reactive, watch } from 'vue'import { ElMessage } from 'element-plus'import { User, Lock } from '@element-plus/icons-vue'import { useRouter, useRoute } from 'vue-router'import useUserStore from '@/stores/user'const userStore = useUserStore()const router = useRouter()const route = useRoute()// 表单数据const formData = reactive({username: '',password: ''})// 表单引用const loginForm = ref(null)// 验证规则const rules = {username: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],password: [{ required: true, message: '密码不能为空', trigger: 'blur' }]}const redirect = ref(undefined);watch(route, (newRoute) => {redirect.value = newRoute.query && newRoute.query.redirect;}, { immediate: true });// 登录处理const handleLogin = async () => {try {// 表单验证await loginForm.value.validate()// 这里添加实际登录逻辑try {userStore.login()router.push({ path: redirect.value || "/" });ElMessage.success('登录成功!')} catch (error) {console.error(error);}} catch (error) {ElMessage.error('请正确填写表单')}}</script><style scoped>.login-container {min-height: 100vh;display: flex;justify-content: center;align-items: center;background: url('@/assets/images/login-background.jpg') no-repeat center center;background-size: cover;}.login-box {width: 400px;padding: 40px;background: rgba(255, 255, 255, 0.9);border-radius: 8px;box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);}.title {text-align: center;margin-bottom: 30px;color: #333;}.login-btn {width: 100%;margin-top: 10px;}.el-form-item {margin-bottom: 28px;}</style>
5.包装cookie工具类
新建文件:src/utils/auth.js
import Cookies from 'js-cookie'const TokenKey = 'Admin-Token'export function getToken() {return Cookies.get(TokenKey)
}export function setToken(token) {return Cookies.set(TokenKey, token)
}export function removeToken() {return Cookies.remove(TokenKey)
}
6.定义permissionStore
新建文件:src/stores/permission.js
将请求getRouters接口和处理返回结果的逻辑,都放在该Store里。
import { ref } from 'vue'
import { defineStore } from 'pinia'
import Layout from '@/layout/index.vue'// 匹配views里面所有的.vue文件
const modules = import.meta.glob('@/views/**/*.vue')const usePermissionStore = defineStore('permission', () => {const arrayForMenu = ref([])const arrayForRouter = ref([])// 异步操作const generateRoutes = () => {// 模拟后端服务器返回const data1 = { "name": "System", "path": "/system", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统管理", "icon": "system", "noCache": false, "link": null }, "children": [{ "name": "User", "path": "user", "hidden": false, "component": "system/user/index", "meta": { "title": "用户管理", "icon": "user", "noCache": false, "link": null } }, { "name": "Role", "path": "role", "hidden": false, "component": "system/role/index", "meta": { "title": "角色管理", "icon": "peoples", "noCache": false, "link": null } }, { "name": "Menu", "path": "menu", "hidden": false, "component": "system/menu/index", "meta": { "title": "菜单管理", "icon": "tree-table", "noCache": false, "link": null } }, { "name": "Dept", "path": "dept", "hidden": false, "component": "system/dept/index", "meta": { "title": "部门管理", "icon": "tree", "noCache": false, "link": null } }, { "name": "Post", "path": "post", "hidden": false, "component": "system/post/index", "meta": { "title": "岗位管理", "icon": "post", "noCache": false, "link": null } }, { "name": "Dict", "path": "dict", "hidden": false, "component": "system/dict/index", "meta": { "title": "字典管理", "icon": "dict", "noCache": false, "link": null } }, { "name": "Config", "path": "config", "hidden": false, "component": "system/config/index", "meta": { "title": "参数设置", "icon": "edit", "noCache": false, "link": null } }, { "name": "Notice", "path": "notice", "hidden": false, "component": "system/notice/index", "meta": { "title": "通知公告", "icon": "message", "noCache": false, "link": null } }, { "name": "Log", "path": "log", "hidden": false, "redirect": "noRedirect", "component": "ParentView", "alwaysShow": true, "meta": { "title": "日志管理", "icon": "log", "noCache": false, "link": null }, "children": [{ "name": "Operlog", "path": "operlog", "hidden": false, "component": "monitor/operlog/index", "meta": { "title": "操作日志", "icon": "form", "noCache": false, "link": null } }, { "name": "Logininfor", "path": "logininfor", "hidden": false, "component": "monitor/logininfor/index", "meta": { "title": "登录日志", "icon": "logininfor", "noCache": false, "link": null } }] }] }const data2 = { "name": "Monitor", "path": "/monitor", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统监控", "icon": "monitor", "noCache": false, "link": null }, "children": [{ "name": "Online", "path": "online", "hidden": false, "component": "monitor/online/index", "meta": { "title": "在线用户", "icon": "online", "noCache": false, "link": null } }, { "name": "Job", "path": "job", "hidden": false, "component": "monitor/job/index", "meta": { "title": "定时任务", "icon": "job", "noCache": false, "link": null } }, { "name": "Druid", "path": "druid", "hidden": false, "component": "monitor/druid/index", "meta": { "title": "数据监控", "icon": "druid", "noCache": false, "link": null } }, { "name": "Server", "path": "server", "hidden": false, "component": "monitor/server/index", "meta": { "title": "服务监控", "icon": "server", "noCache": false, "link": null } }, { "name": "Cache", "path": "cache", "hidden": false, "component": "monitor/cache/index", "meta": { "title": "缓存监控", "icon": "redis", "noCache": false, "link": null } }, { "name": "CacheList", "path": "cacheList", "hidden": false, "component": "monitor/cache/list", "meta": { "title": "缓存列表", "icon": "redis-list", "noCache": false, "link": null } }] }const data3 = { "name": "Tool", "path": "/tool", "hidden": false, "redirect": "noRedirect", "component": "Layout", "alwaysShow": true, "meta": { "title": "系统工具", "icon": "tool", "noCache": false, "link": null }, "children": [{ "name": "Build", "path": "build", "hidden": false, "component": "tool/build/index", "meta": { "title": "表单构建", "icon": "build", "noCache": false, "link": null } }, { "name": "Gen", "path": "gen", "hidden": false, "component": "tool/gen/index", "meta": { "title": "代码生成", "icon": "code", "noCache": false, "link": null } }, { "name": "Swagger", "path": "swagger", "hidden": false, "component": "tool/swagger/index", "meta": { "title": "系统接口", "icon": "swagger", "noCache": false, "link": null } }] }// const data4 = {"name": "Http://ruoyi.vip","path": "http://ruoyi.vip","hidden": false,"component": "Layout","meta": {"title": "若依官网","icon": "guide","noCache": false,"link": "http://ruoyi.vip"}}const newRouteRecord = [data1, data2, data3]const res = {"msg": "操作成功","code": 200,"data":newRouteRecord}const menuData = JSON.parse(JSON.stringify(res.data))const routeData = JSON.parse(JSON.stringify(res.data))arrayForMenu.value = menuDatafilterAsyncRouter(routeData)arrayForRouter.value = routeDatareturn routeData}return {arrayForMenu,arrayForRouter,generateRoutes}
})/*** 异步路由过滤器 - 核心路由配置处理器* 功能: * 1. 递归处理路由配置树,动态加载Vue组件* 2. 特殊处理Layout组件和ParentView结构* 3. 规范化路由配置结构* * @param {Array} asyncRouterArr - 原始异步路由配置数组* @returns {Array} 处理后的标准化路由配置数组* * 处理逻辑:* 1. 遍历路由配置,处理子路由配置* 2. 动态加载组件(转换字符串路径为真实组件)* 3. 递归处理嵌套子路由* 4. 清理空children和redirect属性*/
const filterAsyncRouter = (asyncRouterArr) => {asyncRouterArr.filter(routeMap => {// 处理子路由if (routeMap.children) {routeMap.children = filterChildrenForRouter(routeMap.children);}if (routeMap.component) {// Layout 组件特殊处理if (routeMap.component === 'Layout') {routeMap.component = Layout} else {routeMap.component = loadView(routeMap.component)}}// 递归处理子路由if (routeMap.children?.length) {filterAsyncRouter(routeMap.children);} else {delete routeMap.children;delete routeMap.redirect;}return true;});}/*** 子路由结构转换器 - 路由层级扁平化处理器* 功能:* 1. 处理ParentView类型的路由结构* 2. 合并嵌套子路由路径* 3. 将多级路由转换为扁平结构* * @param {Array} childrenArr - 原子路由配置数组* @returns {Array} 转换后的扁平化子路由数组* * 处理逻辑:* 1. 当遇到ParentView组件时,将其子路由提升到当前层级* 2. 合并父级路径到子路由path* 3. 保留普通路由配置*/const filterChildrenForRouter = (childrenArr) => {let children = [];childrenArr.forEach(el => {if (el.children?.length && el.component === 'ParentView') {children.push(...el.children.map(c => ({...c,path: `${el.path}/${c.path}`})));return;}children.push(el);});return children;}/*** 动态组件加载器 - 模块解析器* 功能:* 根据组件路径字符串动态加载Vue组件* * @param {string} view - 组件路径字符串(例: "system/user/index")* @returns {Component} Vue组件* * 处理逻辑:* 1. 遍历预编译的模块集合(modules)* 2. 匹配views目录下的对应组件文件* 3. 返回组件异步加载函数*/const loadView = (view) => {let res;for (const path in modules) {const dir = path.split('views/')[1].split('.vue')[0];if (dir === view) {res = () => modules[path]();}}return res;}export default usePermissionStore
7.修改layout/index.vue
将渲染菜单需要的数据从permissionStore里获取。
<script setup>
import { ElContainer, ElAside } from 'element-plus'
import Sidebar from './components/Sidebar.vue'
import Navbar from './components/Navbar.vue'
import AppMain from './components/AppMain.vue'
import usePermissionStore from '@/stores/permission'
const permissionStore = usePermissionStore()
</script><template><el-container class="h-screen"><el-aside width="200px"><Sidebar :menu-data="permissionStore.arrayForMenu"/></el-aside><el-container><el-header height="48px"><Navbar /></el-header><AppMain /></el-container></el-container>
</template><style>
.el-header {--el-header-padding: 0;height: auto;
}
</style>
8.修改前置守卫
import router from './router'
import { getToken } from '@/utils/auth'
import usePermissionStore from '@/stores/permission'const whiteList = ['/login']
const whiteListDict = whiteList.reduce((acc, cur) => {acc[cur] = true;return acc;
}, {});router.beforeEach((to, from, next) => {if (getToken()) {if (whiteListDict[to.path]) {next({ path: '/' })} else {if (router.getRoutes().length <= 3) {try {const newRouteRecord = usePermissionStore().generateRoutes()newRouteRecord.forEach(route => {router.addRoute(route) // 动态添加可访问路由表})next({ ...to, replace: true })} catch (error) {console.error(error)}} else {next()}}} else {// 没有tokenif (whiteListDict[to.path]) {// 在免登录白名单,直接进入next()} else {next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页}}
})
二.功能验证
清理浏览器cookie。运行项目,浏览器访问http://localhost:5173/index。被重定向到登录页面。
登录后,可以正常进入首页。
三.知识点拓展
1. 路由守卫与权限控制
1.1 全局前置守卫
作用:在路由跳转前进行统一拦截处理
典型应用场景:
• 登录状态验证
• 动态路由加载
• 页面访问权限控制
实现示例:
router.beforeEach((to, from, next) => {if (需要登录 && 未登录) {next('/login')} else {next()}
})
执行流程图:
2. Pinia 状态管理
2.1 Store 核心结构
defineStore('storeName', () => {// 响应式状态const count = ref(0)// 计算属性const double = computed(() => count.value * 2)// 操作方法function increment() {count.value++}return { count, double, increment }
})
2.2 状态持久化方案
// 使用插件实现 localStorage 持久化
pinia.use(({ store }) => {const saved = localStorage.getItem(store.$id)if (saved) store.$patch(JSON.parse(saved))store.$subscribe((_, state) => {localStorage.setItem(store.$id, JSON.stringify(state))})
})
3. 动态路由加载
3.1 实现原理
// 添加单个路由
router.addRoute({path: '/system',component: Layout,children: [...]
})// 添加嵌套路由
router.addRoute('parentRoute', {path: 'user',component: User
})
3.2 路由加载时机
场景 | 加载方式 | 特点 |
---|---|---|
应用初始化 | 静态定义 | 适合固定路由 |
用户登录后 | 动态添加 | 实现权限路由 |
浏览器刷新 | 重新获取 | 需要持久化存储路由信息 |
4. 组件懒加载优化
4.1 动态导入语法
// 单个组件懒加载
component: () => import('@/views/user.vue')// 批量组件加载(Vite特性)
const modules = import.meta.glob('@/views/**/*.vue')
5. 响应式数据驱动
5.1 菜单数据绑定
<Sidebar :menu-data="permissionStore.arrayForMenu"/>
5.2 响应式更新机制
// 使用 ref 创建响应式数组
const arrayForMenu = ref([])// 更新数据自动触发视图更新
arrayForMenu.value = newMenuData
6. 安全增强实践
6.1 路由白名单机制
const whiteList = ['/login', '/404']function isAllowed(path) {return whiteList.includes(path)
}
6.2 登录重定向处理
// 携带原始访问路径
redirectUrl = encodeURIComponent(to.fullPath)
next(`/login?redirect=${redirectUrl}`)// 登录成功后跳转
router.push(redirectUrl || '/')
7. 错误处理策略
7.1 路由加载容错
try {await generateRoutes()
} catch (error) {console.error('路由加载失败:', error)router.push('/error/500')
}