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

 系列文档目录

Vue3+Vite+TypeScript安装

Element Plus安装与配置

主页设计与router配置

静态菜单设计

Pinia引入

Header响应式菜单缩展

Mockjs引用与Axios封装

登录设计

登录成功跳转主页

多用户动态加载菜单

Pinia持久化

动态路由-动态增加路由 

动态路由-动态删除路由 

路由守卫-无路由跳转404 

路由守卫-未登录跳转登录界面

 登录退出

Tags-组件构建

Tags-与菜单联动 

Pinia持久化优化

按钮权限

客制按钮组件

客制Table组件

客制Form组件

国际化

配置文件


文章目录

目录

 系列文档目录

文章目录

前言

一、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持久化与动态路由

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值