Vue3+Vite+TypeScript+Element Plus开发-10.多用户动态加载菜单

系列文档目录

Vue3+Vite+TypeScript安装

Element Plus安装与配置

主页设计与router配置

静态菜单设计

Pinia引入

Header响应式菜单缩展

Mockjs引用与Axios封装

登录设计

登录成功跳转主页

多用户动态加载菜单

Pinia持久化

动态路由-配置


文章目录

目录

系列文档目录

文章目录

前言

一、API调整

二、Mock模拟菜单数据

[三、mock API](#三、mock API)

四、stores

五、Login.Vue

七、运行效果

后续


前言

本章节着重介绍如何实现基于用户角色的动态菜单加载功能


一、API调整

更新 src/api/menu.ts 文件,以增强菜单API功能,添加用户名作为参数,实现更精细化的菜单数据定制

TypeScript 复制代码
// src/api/menu.ts
// 引入 request、post 和 get 函数 
import { get } from '@/api/request'; // 绝对路径

// 菜单接口
/*
export const menuAPI = async () => {
  try {
    const result = await get('/menu'); // 使用封装的 get 方法
    return result  ;
  } catch (error) {
    console.error('获取菜单数据失败:', error);
    return [];
  }
};
*/
// 菜单接口 增加data
export const menuAPI = async (data: any) => {
  try {
    
    const result = await get('/menu',data); // 使用封装的 get 方法
    // console.log('result',result);
    console.log('result data',result.data );
    return result.data  ;// result.data  为了返回值统一,增加data
  } catch (error) {
    console.error('获取菜单数据失败:', error);
    return [];
  }
};

 

二、Mock模拟菜单数据

编辑 src/mock/mockData/menuData.ts 文件,以扩展模拟数据集,包含针对不同用户的差异化菜单数据。将有助于在开发过程中更准确地模拟用户特定的菜单内容

TypeScript 复制代码
// src/mock/mockData/menuData.ts
import Mock from 'mockjs';
import { Document, Setting } from '@element-plus/icons-vue'; // 假设你使用的是 Element Plus 的图标

// 模拟菜单数据,改为后面动态
/*
const menuData = Mock.mock({
  data: [
    { index: 'Home', label: '首页', icon: Document },
    {
      index: 'SysSettings',
      label: '系统设置',
      icon: Setting,
      children: [
        { index: 'UserInfo', label: '个人资料' },
        { index: 'AccountSetting', label: '账户设置' },
      ],
    },
  ],
});
*/

 
// 动态生成菜单数据
export default (data: any) => {
  // 解析传入的 data 参数
  const { username, password } = data;
 

  // 根据用户名和密码生成不同的响应
  if (username === 'admin') {
    return Mock.mock({
      status_code: 200,
      status: 'success',
      message: 'Operation successful.',
      data:  [
        { index: 'Home', label: '首页', icon: Document },
        {
          index: 'SysSettings',
          label: '系统设置',
          icon: Setting,
          children: [
            { index: 'UserInfo', label: '个人资料' },
            { index: 'AccountSetting', label: '账户设置' },
          ],
        },
      ],
    });
  } else if (username === 'user' ) {
    return Mock.mock({
      status_code: 200,
      status: 'success',
      message: 'Operation successful.',
      data: [
        { index: 'Home', label: '首页', icon: Document },
        {
          index: 'SysSettings',
          label: '系统设置',
          icon: Setting,
          children: [
            { index: 'UserInfo', label: '个人资料' },
           
          ],
        },
      ],
    });
  } else {
    return Mock.mock({
      status_code: 401,
      status: 'fail',
      message: 'Invalid username ,No Menu Data.',
      data: [],
    });
  }
};

三、mock API

编辑 src/mock/index.ts 文件中菜单部分,添加用户管理相关的模拟数据。将测试模拟用户管理功能的菜单项,确保菜单界面能够正确加载不同的用户菜单权限

TypeScript 复制代码
// src/mock/index.ts
import Mock from 'mockjs';
import menuData from '@/mock/mockData/menuData';
import loginData from '@/mock/mockData/loginData' ;

/*
Mock.mock(/menu/, 'get', (req: any) => {
  return menuData.data;
});
*/
Mock.mock(/menu/, 'get', (options) => {
  const { body } = options;
  const data = JSON.parse(body); // 解析请求体中的数据
  return menuData(data);
});

/*
Mock.mock(/login/, 'post', (req: any) => {
  return loginData.data;
});
*/
//  /\/ zheng'zhi'fa'zhe
Mock.mock(/\/login/, (options) => {
  const { body } = options;
  const data = JSON.parse(body); // 解析请求体中的数据
  return loginData(data); // 调用动态生成的登录数据函数
});

四、stores

说明:

文件路径:src/stores/index.ts

任务描述:增强现有的 Pinia store 以支持菜单数据的存储与获取功能。

具体步骤:

  1. 在 store 中定义一个新的状态(menuData)属性,用于存储从服务器获取的菜单数据。

  2. 创建一个 action setMenuData用于异步存储菜单数据。

  3. 创建一个 action getMenuData 用于异步获取菜单数据,并将获取到的数据保存到步骤2中定义的状态属性中

TypeScript 复制代码
// src/stores/index.ts

import { defineStore } from 'pinia';

// 定义公共 store
export const useAllDataStore = defineStore('useAllData', {
  // 定义状态
  state: () => ({
    isCollapse: false, // 定义初始状态
    username: '',
    token_key: '',
    menuData:[],
  }),

  // 定义 actions
  actions: {
    // 设置用户名
    setUsername(username: string) {
      this.username = username;
    },

    // 获取用户名
    getUsername(): string {
      return this.username;
    },

    // 设置 token_key
    setTokenKey(token_key: string) {
      this.token_key = token_key;
    },

    // 获取 token_key
    getTokenKey(): string {
      return this.token_key;
    },
    // 设置菜单数据
    setMenuData(menuData: any){
      this.menuData = menuData
    },
    // 获取菜单数据
    getMenuData(): [] {
      return this.menuData;
    },
  },
  
});

五、Login.Vue

说明:文件路径: src/views/Login.vue

任务描述:在登录视图中实现菜单数据的获取和存储功能。

具体步骤

  1. 增加 fetchMenuData

方法:• 实现一个名为 fetchMenuData 的方法,该方法负责异步获取菜单数据。

• 确保此方法能够处理异步操作,并在数据获取成功后将其存储在组件的状态中或 Pinia store 中。

  1. 在 fetchLoginData 方法中调用 fetchMenuData :

• 修改 fetchLoginData 方法,在用户登录成功后调用 fetchMenuData 。

• 确保 fetchMenuData 的调用在 fetchLoginData 的异步流程中正确等待,以便在后续操作中能够访问到菜单数据。

重点说明

  1. 异步处理:

• fetchMenuData 必须能够处理异步请求,这意味着它可能需要使用 async/await 语法或 .then() 方法来处理 Promise。

• 必须确保 fetchMenuData 在 fetchLoginData 中被等待,以避免在数据完全加载之前就尝试访问菜单数据。

  1. 数据存储:

• 获取到的菜单数据应该被存储在适当的地方,如组件的响应式数据中或 Pinia store 中,以便在整个应用中访问。

• 确保存储逻辑不会导致状态管理问题,如数据竞态条件或不一致的状态。

  1. 错误处理:

• 在 fetchMenuData 中添加错误处理逻辑,以便在请求失败时能够适当地处理错误,例如显示错误消息或进行重试。

重点代码:

TypeScript 复制代码
const fetchLoginData = async () => {
  try {
    const responseData: LoginResponse = await login(loginForm); // 假设 login 返回的是 LoginResponse

    if (responseData.status_code === 200 && responseData.status === 'success') {
      store.setUsername(responseData.data?.username || '');
      store.setTokenKey(responseData.data?.token_key || '');
      await fetchMenuData(); // 确保菜单数据更新
      router.push('/main'); // 导航到 MainAsideCont.vue
    } else {
      ElMessage.error(`登录失败: ${responseData.message || '未知错误'}`);
    }
  } catch (error) {
    ElMessage.error('登录请求失败,请稍后再试');
  }
};

// 获取菜单数据
const fetchMenuData = async () => {
  try {
    const result = await menuAPI(loginForm); // 假设 loginForm 包含必要的参数
    store.setMenuData(result);
    console.log('login result 返回的数据:', result);
    console.log('login menuAPI 返回的数据:', store.getMenuData());
  } catch (error) {
    console.error('获取菜单数据失败:', error);
  }
};

完整代码:

TypeScript 复制代码
<template>
  <div class="login-container">
    <el-card class="box-card">
      <template #header>
        <span>登录</span>
      </template>
      <el-form :model="loginForm" :rules="rules" ref="loginFormRef" label-width="100px" class="demo-loginForm">
        <el-form-item label="用户名" prop="username">
          <el-input v-model="loginForm.username"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input type="password" v-model="loginForm.password" show-password></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitForm">登录</el-button>
          <el-button @click="resetForm">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { ElForm, ElFormItem, ElInput, ElButton, ElCard, ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { login } from '@/api/user';
import { useAllDataStore } from '@/stores';
import { useRouter } from 'vue-router';
import { menuAPI } from '@/api/menu';

const router = useRouter();
const loginForm = reactive({
  username: '',
  password: ''
});

const rules: FormRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' }
  ]
};

const store = useAllDataStore();

const loginFormRef = ref<FormInstance | null>(null);

// 封装登录请求处理逻辑
interface LoginResponse {
  status_code: number;
  status: string;
  message?: string;
  data?: {
    api_key: string;
    username: string;
    token_key: string;
    role: string;
    email: string;
  };
}

const fetchLoginData = async () => {
  try {
    const responseData: LoginResponse = await login(loginForm); // 假设 login 返回的是 LoginResponse

    if (responseData.status_code === 200 && responseData.status === 'success') {
      store.setUsername(responseData.data?.username || '');
      store.setTokenKey(responseData.data?.token_key || '');
      await fetchMenuData(); // 确保菜单数据更新
      router.push('/main'); // 导航到 MainAsideCont.vue
    } else {
      ElMessage.error(`登录失败: ${responseData.message || '未知错误'}`);
    }
  } catch (error) {
    ElMessage.error('登录请求失败,请稍后再试');
  }
};

// 获取菜单数据
const fetchMenuData = async () => {
  try {
    const result = await menuAPI(loginForm); // 假设 loginForm 包含必要的参数
    store.setMenuData(result);
    console.log('login result 返回的数据:', result);
    console.log('login menuAPI 返回的数据:', store.getMenuData());
  } catch (error) {
    console.error('获取菜单数据失败:', error);
  }
};

const submitForm = () => {
  if (!loginFormRef.value) return;
  loginFormRef.value.validate((valid) => {
    if (valid) {
      fetchLoginData();
    } else {
      console.log('验证失败!');
      ElMessage.error('验证失败!');
    }
  });
};

const resetForm = () => {
  if (!loginFormRef.value) return;
  loginFormRef.value.resetFields();
};
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f0f2f5;
}

.box-card {
  width: 480px;
}
</style>

六、Aside

说明:文件路径: src/components/MainAsideCont.vue

任务描述:在Aside视图中实现菜单数据的获取。

具体步骤

1.删除原获取menu数据的函数

2、增加 fetchMenuData,该方法负责异步获取菜单数据与存储。

  1. 在 生命周期方法中调用 fetchMenuData 与获取store的menuData

重点代码:

TypeScript 复制代码
// 封装数据获取和处理逻辑
const fetchMenuData = () => {
  try {
    const result = store.getMenuData(); // 调用 store 获取数据
    console.log('main menuAPI 返回的数据:', store.getMenuData());
    console.error('main menuAPI :', result);

    if (Array.isArray(result)) {
      menuData.value = result as MenuItem[];
    } else {
      console.error('menuAPI 返回的数据不是数组:', result);
    }
  } catch (error) {
    console.error('获取菜单数据失败:', error);
  }
};
 
onMounted(() => {
  if (!store.getMenuData().length) {
    console.warn('菜单数据为空,尝试重新获取');
    fetchMenuData();
  } else {
    console.log('菜单数据已存在,无需重新获取');
    menuData.value = store.getMenuData() as MenuItem[];
    console.log('menuData.value:', menuData.value);
  }
});

完整代码:

TypeScript 复制代码
<template>
  <el-menu
    :default-active="activeIndex"
    class="el-menu-vertical-demo"
    :collapse="isCollapse"
  >
    <h3 :key="TitleText">{{ TitleText }}</h3>
    <!-- 渲染没有子菜单的项 -->
    <el-menu-item
      v-for="item in noChilden"
      :key="item.index"
      :index="item.index"
      @click="handlemenu(item)"
    >
      <component v-if="item.icon" class="icon" :is="item.icon.name"></component>
      <span>{{ item.label }}</span>
    </el-menu-item>

    <!-- 渲染有子菜单的项 -->
    <el-sub-menu
      v-for="item in hasChilden"
      :key="item.index"
      :index="item.index"
    >
      <template #title>
        <component v-if="item.icon" class="icon" :is="item.icon.name"></component>
        <span>{{ item.label }}</span>
      </template>
      <el-menu-item
        v-for="subItem in item.children"
        :key="subItem.index"
        :index="subItem.index"
        @click="handlemenuchild(item, subItem)"
      >
        <span>{{ subItem.label }}</span>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useAllDataStore } from '@/stores';

const store = useAllDataStore();

interface MenuItem {
  index: string;
  label: string;
  icon?: { name: string; __name: string };
  children?: MenuItem[];
}

// 确保 menuAPI 是一个数组,并赋值给 menuData
const menuData = ref<MenuItem[]>([]); // 初始化为空数组

// 封装数据获取和处理逻辑
const fetchMenuData = () => {
  try {
    const result = store.getMenuData(); // 调用 store 获取数据
    console.log('main menuAPI 返回的数据:', store.getMenuData());
    console.error('main menuAPI :', result);

    if (Array.isArray(result)) {
      menuData.value = result as MenuItem[];
    } else {
      console.error('menuAPI 返回的数据不是数组:', result);
    }
  } catch (error) {
    console.error('获取菜单数据失败:', error);
  }
};
 
onMounted(() => {
  if (!store.getMenuData().length) {
    console.warn('菜单数据为空,尝试重新获取');
    fetchMenuData();
  } else {
    console.log('菜单数据已存在,无需重新获取');
    menuData.value = store.getMenuData() as MenuItem[];
    console.log('menuData.value:', menuData.value);
  }
});


const hasChilden = computed(() => menuData.value.filter(item => item.children && item.children.length > 0));
const noChilden = computed(() => menuData.value.filter(item => !item.children || item.children.length === 0));

const activeIndex = ref('Home');
const router = useRouter();

const handlemenu = (item: MenuItem) => {
  router.push(item.index);
};

const handlemenuchild = (item: MenuItem, subItem: MenuItem) => {
  router.push(subItem.index);
};

const TitleText = computed(() => {
  return store.isCollapse ? '平台' : '测试平台管理';
});

const isCollapse = computed(() => store.isCollapse);

 
</script>

<style>
.el-menu {
  height: 100%; /* 设置整个布局的高度为 100%,确保布局占满整个视口 */
  border-right: none; /* 去掉右边框 */
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
  width: 180px;
  min-height: 400px;
}
.el-menu-vertical-demo.el-menu--collapse {
  width: 60px; /* 收缩时的宽度 */
}

.icon {
  margin-right: 8px; /* 图标与文字之间的间距 */
  font-size: 18px; /* 图标的大小 */
  width: 18px;
  height: 18px;
  size: 8px;
  color: #606266; /* 图标的默认颜色 */
  vertical-align: middle; /* 垂直居中对齐 */
}

/* 鼠标悬停时的样式 */
.icon:hover {
  color: #409eff; /* 鼠标悬停时图标的颜色 */
}
</style>

七、运行效果

登录输入admin后菜单

输入user后菜单


后续

后面将重点解决,pinia持久化与动态路由

相关推荐
相见曾相识1 小时前
前端-HTML+CSS+JavaScript+Vue+Ajax概述
前端·vue.js·html
guhy fighting1 小时前
vue项目中渲染markdown并处理报错
前端·javascript·vue.js
阿珊和她的猫9 小时前
钩子函数和参数:Vue组件生命周期中的自定义逻辑
前端·javascript·vue.js
勘察加熊人9 小时前
vue展示graphviz和dot流程图
前端·vue.js·流程图
武昌库里写JAVA13 小时前
Java 设计模式
java·vue.js·spring boot·课程设计·宠物管理
程序员小刚16 小时前
基于SpringBoot + Vue 的火车票订票系统
vue.js·spring boot·后端
武昌库里写JAVA19 小时前
iview 如何设置sider宽度
java·vue.js·spring boot·学习·课程设计
阿珊和她的猫20 小时前
动态指令参数:根据组件状态调整指令行为
前端·javascript·vue.js
xiegwei20 小时前
vue+element 导航 实现例子
前端·javascript·vue.js
露临霜21 小时前
vue实现AI问答Markdown打字机效果
前端·javascript·vue.js·ai·github