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

相关推荐
炒毛豆44 分钟前
vue3.4中的v-model的用法~
前端·vue.js
阳火锅1 小时前
都2025年了,来看看前端如何给刘亦菲加个水印吧!
前端·vue.js·面试
夕水2 小时前
ew-vue-component:Vue 3 动态组件渲染解决方案的使用介绍
前端·vue.js
codehub2 小时前
TypeScript 高频面试题与核心知识总结
typescript
我麻烦大了2 小时前
实现一个简单的Vue响应式
前端·vue.js
aklry3 小时前
uniapp三步完成一维码的生成
前端·vue.js
张志鹏PHP全栈3 小时前
TypeScript 第一天,认识TypeScript
typescript
用户26124583401615 小时前
vue学习路线(11.watch对比computed)
前端·vue.js
阑梦清川5 小时前
Java后端项目前端基础Vue(二)
vue.js
雪碧聊技术6 小时前
深入解析Vue中v-model的双向绑定实现原理
前端·javascript·vue.js·v-model