欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 文化 > Vue3+Vite+TypeScript+Element Plus开发-10.多用户动态加载菜单

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

2025/7/5 21:46:34 来源:https://blog.csdn.net/sen_shan/article/details/147089971  浏览:    关键词:Vue3+Vite+TypeScript+Element Plus开发-10.多用户动态加载菜单

 系列文档目录

Vue3+Vite+TypeScript安装

Element Plus安装与配置

主页设计与router配置

静态菜单设计

Pinia引入

Header响应式菜单缩展

Mockjs引用与Axios封装

登录设计

登录成功跳转主页

多用户动态加载菜单

Pinia持久化

动态路由-配置  


文章目录

目录

 系列文档目录

文章目录

前言

一、API调整

二、Mock模拟菜单数据

三、mock API

四、stores

五、Login.Vue

七、运行效果

后续 


前言

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


一、API调整

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

// 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   文件,以扩展模拟数据集,包含针对不同用户的差异化菜单数据。将有助于在开发过程中更准确地模拟用户特定的菜单内容

// 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   文件中菜单部分,添加用户管理相关的模拟数据。将测试模拟用户管理功能的菜单项,确保菜单界面能够正确加载不同的用户菜单权限

// 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中定义的状态属性中

// src/stores/index.tsimport { defineStore } from 'pinia';// 定义公共 store
export const useAllDataStore = defineStore('useAllData', {// 定义状态state: () => ({isCollapse: false, // 定义初始状态username: '',token_key: '',menuData:[],}),// 定义 actionsactions: {// 设置用户名setUsername(username: string) {this.username = username;},// 获取用户名getUsername(): string {return this.username;},// 设置 token_keysetTokenKey(token_key: string) {this.token_key = token_key;},// 获取 token_keygetTokenKey(): 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 中。

2. 在   fetchLoginData   方法中调用   fetchMenuData  :

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

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

重点说明

1. 异步处理:

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

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

2. 数据存储:

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

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

3. 错误处理:

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

重点代码:

const fetchLoginData = async () => {try {const responseData: LoginResponse = await login(loginForm); // 假设 login 返回的是 LoginResponseif (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);}
};

完整代码: 

<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 返回的是 LoginResponseif (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,该方法负责异步获取菜单数据与存储。     

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

重点代码:

// 封装数据获取和处理逻辑
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);}
});

完整代码:

<template><el-menu:default-active="activeIndex"class="el-menu-vertical-demo":collapse="isCollapse"><h3 :key="TitleText">{{ TitleText }}</h3><!-- 渲染没有子菜单的项 --><el-menu-itemv-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-menuv-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-itemv-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持久化与动态路由

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词