1. 问题:请介绍一下你们项目中动态菜单的递归生成机制是如何实现的?
答案:
我们项目中的动态菜单递归生成主要通过 RecursiveMenu.vue 组件实现:
<template>
<div class="recursive-menu">
<template v-for="item in menuItems">
<!-- 有子菜单的情况 -->
<el-sub-menu v-if="item.children && item.children.length > 0"
:key="item._id"
:index="item.path">
<template #title>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span>{{ item.title }}</span>
</template>
<!-- 递归渲染子菜单 -->
<RecursiveMenu :menu-items="item.children" :level="level + 1" />
</el-sub-menu>
<!-- 没有子菜单的情况 -->
<el-menu-item v-else :key="item._id" :index="item.path">
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span>{{ item.title }}</span>
</el-menu-item>
</template>
</div>
</template>
核心机制:
- 递归条件判断:通过
item.children && item.children.length > 0判断是否有子菜单 - 组件自调用:在子菜单中使用
<RecursiveMenu :menu-items="item.children" :level="level + 1" /> - 层级传递:通过
level参数记录当前菜单层级,便于样式控制
2. 问题:菜单数据是如何从后端获取并管理的?
答案: 我们项目使用 Pinia 状态管理菜单数据,具体流程如下:
// store/modules/menu.ts
export const useMenuStore = defineStore('menu', {
state: () => ({
menuTree: [] as MenuItem[],
menuList: [] as MenuItem[],
loading: false,
error: ''
}),
actions: {
async fetchMenuTree() {
this.loading = true;
try {
const response = await menuApi.getMenuTree();
if (Array.isArray(response.data) && response.data.length === 0) {
// 如果菜单为空,自动初始化
const initRes = await menuApi.initDefaultMenus();
if (initRes.code === 0) {
const retryRes = await menuApi.getMenuTree();
this.menuTree = retryRes.data;
}
} else {
this.menuTree = response.data;
}
} catch (error) {
this.error = error.message || '获取菜单失败';
} finally {
this.loading = false;
}
}
}
});
在 SideMenu.vue 中使用:
onMounted(async () => {
if (menuStore.getMenuTree.length === 0) {
await menuStore.fetchMenuTree()
}
})
3. 问题:菜单的权限控制是如何实现的?
答案: 我们项目的菜单权限控制通过以下机制实现:
// SideMenu.vue 中的权限过滤
const filteredMenuItems = computed(() => {
const userRole = useTool.userInfo.role
return menuStore.getMenuTree.filter((menu) => {
if (menu.requireAdmin && userRole !== 1) {
return false
}
return true
})
})
权限控制特点:
- 角色判断:通过
userRole判断用户角色(1为管理员,其他为普通用户) - 菜单标记:菜单项通过
requireAdmin字段标记是否需要管理员权限 - 视觉区分:管理员专用菜单有特殊样式标识
.admin-only {
&.el-menu-item {
background-color: rgba(255, 193, 7, 0.1);
&:hover {
background-color: rgba(255, 193, 7, 0.2) !important;
}
}
}
4. 问题:菜单折叠功能是如何实现的?
答案: 我们项目的菜单折叠功能通过以下机制实现:
// store/index.ts 中的折叠状态管理
export const useToolStore = defineStore('tool', {
state: () => ({
isCollapsed: false,
// ...其他状态
}),
actions: {
changeCollapsed() {
this.isCollapsed = !this.isCollapsed;
}
},
persist: {
key: 'tool',
storage: localStorage,
paths: ['isCollapsed', 'userInfo'],
},
});
在 SideMenu.vue 中应用折叠状态:
<el-aside :width="useTool.isCollapsed ? '64px' : '220px'">
<el-menu :collapse="useTool.isCollapsed"
:collapse-transition="false">
<RecursiveMenu :menu-items="filteredMenuItems" />
</el-menu>
</el-aside>
在 TopHeader.vue 中触发折叠:
const handleCollapsed = () => {
useTool.changeCollapsed()
}
5. 问题:菜单的响应式设计和移动端适配是如何处理的?
答案: 我们项目的菜单响应式设计通过以下方式实现:
// MainBox.vue 中的移动端处理
const handleResize = () => {
isMobile.value = window.innerWidth <= 768
// 在移动端自动收起侧边栏
if (isMobile.value && !useTool.isCollapsed) {
useTool.changeCollapsed()
}
}
CSS 响应式样式:
// 移动端样式
@media screen and (max-width: 768px) {
:deep(.el-aside) {
position: fixed;
height: 100vh;
z-index: 1000;
}
}
// 标题响应式
.title {
font-size: 16px;
&.mobile-title {
font-size: 14px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
6. 问题:菜单的加载状态和错误处理是如何实现的?
答案: 我们项目的菜单加载状态和错误处理机制:
<!-- SideMenu.vue 中的加载状态 -->
<el-menu>
<!-- 加载状态 -->
<div v-if="menuStore.isLoading" class="menu-loading">
<el-skeleton :rows="5" animated />
</div>
<!-- 动态菜单 -->
<RecursiveMenu v-else :menu-items="filteredMenuItems" />
</el-menu>
状态管理中的错误处理:
// store/modules/menu.ts
async fetchMenuTree() {
this.loading = true;
this.error = '';
try {
const response = await menuApi.getMenuTree();
// 处理响应...
} catch (error: any) {
this.error = error.message || '获取菜单失败';
console.error('获取菜单树失败:', error);
} finally {
this.loading = false;
}
}
7. 问题:菜单数据的持久化是如何实现的?
答案: 我们项目使用 Pinia 的持久化插件实现菜单数据持久化:
// store/modules/menu.ts
export const useMenuStore = defineStore('menu', {
// ... state 和 actions
persist: {
key: 'menu',
storage: localStorage,
paths: ['menuTree'] // 只持久化菜单树数据
}
});
// store/index.ts 中的折叠状态持久化
export const useToolStore = defineStore('tool', {
// ... state 和 actions
persist: {
key: 'tool',
storage: localStorage,
paths: ['isCollapsed', 'userInfo'], // 持久化折叠状态和用户信息
},
});
持久化配置:
// store/index.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
const store = createPinia();
store.use(piniaPluginPersistedstate);
8. 问题:菜单的图标系统是如何设计的?
答案: 我们项目的菜单图标系统通过动态组件实现:
<!-- RecursiveMenu.vue 中的图标渲染 -->
<template #title>
<el-icon v-if="item.icon">
<component :is="item.icon" />
</el-icon>
<span>{{ item.title }}</span>
</template>
图标数据结构:
// types/menu.ts
export interface MenuItem {
_id: string;
name: string;
path: string;
component: string;
icon: string; // 图标名称,对应 Element Plus 图标
title: string;
// ...其他字段
}
图标样式优化:
.el-icon {
font-size: 18px;
margin-right: 5px;
vertical-align: middle;
}
9. 问题:菜单的路由集成和激活状态是如何处理的?
答案: 我们项目的菜单路由集成通过以下方式实现:
<!-- SideMenu.vue 中的路由集成 -->
<el-menu :router="true"
:default-active="route.fullPath">
<RecursiveMenu :menu-items="filteredMenuItems" />
</el-menu>
路由激活状态处理:
import { useRoute } from 'vue-router'
const route = useRoute()
// 菜单项的路由配置
<el-menu-item :index="item.path"> // index 对应路由路径
菜单项的路由结构:
// 菜单数据结构中的路由信息
{
_id: "1",
name: "news-manage",
path: "/news-manage", // 对应路由路径
component: "NewsManage", // 对应路由组件
title: "新闻管理",
// ...
}
10. 问题:菜单的性能优化和缓存策略有哪些?
答案: 我们项目的菜单性能优化策略:
- 计算属性缓存:
// 使用 computed 缓存过滤后的菜单数据
const filteredMenuItems = computed(() => {
const userRole = useTool.userInfo.role
return menuStore.getMenuTree.filter((menu) => {
if (menu.requireAdmin && userRole !== 1) {
return false
}
return true
})
})
- 条件渲染优化:
<!-- 只在非加载状态时渲染菜单 -->
<RecursiveMenu v-else :menu-items="filteredMenuItems" />
- 数据持久化:
persist: {
key: 'menu',
storage: localStorage,
paths: ['menuTree'] // 避免重复请求
}
- 懒加载策略:
onMounted(async () => {
if (menuStore.getMenuTree.length === 0) {
await menuStore.fetchMenuTree() // 只在需要时加载
}
})
- 组件复用:
<!-- 使用 key 确保组件正确复用 -->
<el-sub-menu :key="item._id" :index="item.path">