当前位置: 首页 > article >正文

使用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')
}

四.思考


http://www.kler.cn/a/582323.html

相关文章:

  • vue3 二次封装uni-ui中的组件,并且组件中有 v-model 的解决方法
  • Vue 实现AI对话和AI绘图(AIGC)人工智能
  • Excel多级联动下拉菜单设置
  • C盘清理技巧分享:释放空间,提升电脑性能
  • Networking Based ISAC Hardware Testbed and Performance Evaluation
  • [动手学习深度学习]13.丢弃法 Dropout
  • 修改jupyter notebook的工作空间
  • 二级Python通关秘籍:字符串操作符/函数/方法全解析与实战演练
  • Spike RISC-V ISA 模拟器
  • 三级嵌入式学习ing 考点25、26
  • python-leetcode-解决智力问题
  • 常见的死锁情况分析
  • JDBC编程六步详解:从注册驱动到释放资源
  • C++学习笔记(十七)——类之封装
  • LETTERS(dfs)
  • Spring 的三种注入方式?
  • Vue3 + Spring Boot前后端分离项目跨域问题完整解决方案
  • C++编程:进阶阶段—4.2对象
  • Spring MVC 工作原理和流程
  • ubuntu中用docker下载opengauss