系列文档目录
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.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 中。
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 返回的是 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);
}
};
完整代码:
<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,该方法负责异步获取菜单数据与存储。
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-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持久化与动态路由