使用AI一步一步实现若依前端(8)
功能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-input
v-model="formData.username"
placeholder="请输入用户名"
:prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-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 = menuData
filterAsyncRouter(routeData)
arrayForRouter.value = routeData
return 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 {
// 没有token
if (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')
}