使用AI一步一步实现若依前端(5)
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目
前言
在若依中,侧边栏显示的菜单项,是根据登录用户的角色动态显示的。不同角色的用户,看到的菜单可能是不一样的。
这就需要在代码中,能根据后端服务器返回的结果,动态显示菜单项。
一.操作步骤
1.获取getRouters接口的返回结果
{"msg": "操作成功","code": 200,"data": [{"name": "System","path": "/system","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统管理","icon": "system","noCache": false,"link": null},"children": [{"name": "User","path": "user","hidden": false,"component": "system/user/index","meta": {"title": "用户管理","icon": "user","noCache": false,"link": null}},{"name": "Role","path": "role","hidden": false,"component": "system/role/index","meta": {"title": "角色管理","icon": "peoples","noCache": false,"link": null}},{"name": "Menu","path": "menu","hidden": false,"component": "system/menu/index","meta": {"title": "菜单管理","icon": "tree-table","noCache": false,"link": null}},{"name": "Dept","path": "dept","hidden": false,"component": "system/dept/index","meta": {"title": "部门管理","icon": "tree","noCache": false,"link": null}},{"name": "Post","path": "post","hidden": false,"component": "system/post/index","meta": {"title": "岗位管理","icon": "post","noCache": false,"link": null}},{"name": "Dict","path": "dict","hidden": false,"component": "system/dict/index","meta": {"title": "字典管理","icon": "dict","noCache": false,"link": null}},{"name": "Config","path": "config","hidden": false,"component": "system/config/index","meta": {"title": "参数设置","icon": "edit","noCache": false,"link": null}},{"name": "Notice","path": "notice","hidden": false,"component": "system/notice/index","meta": {"title": "通知公告","icon": "message","noCache": false,"link": null}},{"name": "Log","path": "log","hidden": false,"redirect": "noRedirect","component": "ParentView","alwaysShow": true,"meta": {"title": "日志管理","icon": "log","noCache": false,"link": null},"children": [{"name": "Operlog","path": "operlog","hidden": false,"component": "monitor/operlog/index","meta": {"title": "操作日志","icon": "form","noCache": false,"link": null}},{"name": "Logininfor","path": "logininfor","hidden": false,"component": "monitor/logininfor/index","meta": {"title": "登录日志","icon": "logininfor","noCache": false,"link": null}}]}]},{"name": "Monitor","path": "/monitor","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统监控","icon": "monitor","noCache": false,"link": null},"children": [{"name": "Online","path": "online","hidden": false,"component": "monitor/online/index","meta": {"title": "在线用户","icon": "online","noCache": false,"link": null}},{"name": "Job","path": "job","hidden": false,"component": "monitor/job/index","meta": {"title": "定时任务","icon": "job","noCache": false,"link": null}},{"name": "Druid","path": "druid","hidden": false,"component": "monitor/druid/index","meta": {"title": "数据监控","icon": "druid","noCache": false,"link": null}},{"name": "Server","path": "server","hidden": false,"component": "monitor/server/index","meta": {"title": "服务监控","icon": "server","noCache": false,"link": null}},{"name": "Cache","path": "cache","hidden": false,"component": "monitor/cache/index","meta": {"title": "缓存监控","icon": "redis","noCache": false,"link": null}},{"name": "CacheList","path": "cacheList","hidden": false,"component": "monitor/cache/list","meta": {"title": "缓存列表","icon": "redis-list","noCache": false,"link": null}}]},{"name": "Tool","path": "/tool","hidden": false,"redirect": "noRedirect","component": "Layout","alwaysShow": true,"meta": {"title": "系统工具","icon": "tool","noCache": false,"link": null},"children": [{"name": "Build","path": "build","hidden": false,"component": "tool/build/index","meta": {"title": "表单构建","icon": "build","noCache": false,"link": null}},{"name": "Gen","path": "gen","hidden": false,"component": "tool/gen/index","meta": {"title": "代码生成","icon": "code","noCache": false,"link": null}},{"name": "Swagger","path": "swagger","hidden": false,"component": "tool/swagger/index","meta": {"title": "系统接口","icon": "swagger","noCache": false,"link": null}}]},{"name": "Http://ruoyi.vip","path": "http://ruoyi.vip","hidden": false,"component": "Layout","meta": {"title": "若依官网","icon": "guide","noCache": false,"link": "http://ruoyi.vip"}}]}
2.向AI提问,生成代码
尽量的把返回结果的数据结构描述清楚,让AI给出对应的代码。
分析AI给出的代码,做适当的修改。
2.SidebarMenu.vue
复制AI的代码。
<!-- SidebarMenu.vue -->
<template>
<el-menu
router
default-active="/"
class="el-menu-vertical"
:collapse="false"
>
<template v-for="item in menuData" :key="item.path">
<MenuItem :item="item" :level="0" />
</template>
</el-menu>
</template>
<script setup>
import { defineProps } from 'vue';
import MenuItem from './MenuItem.vue';
defineProps({
menuData: {
type: Array,
required: true
}
});
</script>
3.MenuItem.vue
新建文件src\layout\components\MenuItem.vue。将AI生成的代码复制到文件内。
<!-- MenuItem.vue -->
<template>
<template v-if="hasChildren">
<el-sub-menu :index="item.path">
<template #title>
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<span>{{ item.meta?.title }}</span>
</template>
<template v-for="child in item.children" :key="child.path">
<MenuItem :item="child" :level="level + 1" />
</template>
</el-sub-menu>
</template>
<template v-else>
<el-menu-item :index="item.path">
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<span>{{ item.meta?.title }}</span>
</el-menu-item>
</template>
</template>
<script setup>
import { defineProps, computed } from 'vue';
const props = defineProps({
item: {
type: Object,
required: true
},
level: {
type: Number,
default: 0
}
});
const hasChildren = computed(() => {
if (props.level >= 2) {
if (props.item.children) {
console.error('菜单层级超过限制,最多允许两层子菜单');
return false;
}
return false;
}
return props.item.children && props.item.children.length > 0;
});
</script>
4.修改layout/index.vue
根据AI的代码,使用Sidebar,需要向子组件传入menuData的菜单数组。暂时先根据后端返回的json串,固定返回。
二.功能验证
运行项目,打开浏览器的开发者工具,如果有报错就需要看代码处理错误。
浏览器访问http://localhost:5173/index
三.知识点拓展
1. 组件化开发
Vue采用组件化架构,每个.vue
文件都是一个独立组件。例如:
• SidebarMenu.vue
负责整个侧边栏容器
• MenuItem.vue
处理单个菜单项的渲染
• 通过import MenuItem from './MenuItem.vue'
实现组件复用
2. 组件通信(Props)
父子组件通过props传递数据:
// 父组件传递数据
<MenuItem :item="item" :level="0" />
// 子组件接收
defineProps({
item: { type: Object, required: true },
level: { type: Number, default: 0 }
})
3. 递归组件
组件可以调用自身处理嵌套结构:
<!-- 处理多级菜单 -->
<template v-for="child in item.children">
<MenuItem :item="child" :level="level + 1" />
</template>
4. 动态组件
通过<component>
实现图标动态加载:
<component :is="item.meta.icon" />
根据meta.icon的值动态渲染对应的图标组件
5. 计算属性
使用computed处理逻辑判断:
const hasChildren = computed(() => {
return props.item.children?.length > 0 && props.level < 2
})
6. 条件渲染
v-if/v-else控制不同状态的显示:
<template v-if="hasChildren">
<!-- 显示子菜单 -->
</template>
<template v-else>
<!-- 显示普通菜单项 -->
</template>
7. 列表渲染
v-for指令遍历菜单数据:
<template v-for="item in menuData" :key="item.path">
8. 路由集成
el-menu与Vue Router深度整合:
<el-menu router>
<el-menu-item index="/user">用户管理</el-menu-item>
</el-menu>
点击菜单自动跳转对应路由路径
9. 组合式API
使用<script setup>
语法:
import { defineProps, computed } from 'vue'
简化组件逻辑组织,自动暴露模板变量
10. 组件插槽
使用插槽定制子菜单标题:
<el-sub-menu>
<template #title>
<!-- 自定义标题内容 -->
<el-icon><component :is="icon"/></el-icon>
<span>{{ title }}</span>
</template>
</el-sub-menu>
11. UI框架集成
整合Element Plus组件:
• <el-menu>
菜单容器
• <el-sub-menu>
折叠菜单
• <el-menu-item>
菜单项
• <el-icon>
图标容器
四.思考
1.为什么菜单点击后,都是返回空白界面?
点击了侧边栏里的用户管理
后,跳转到空界面,Layout失效。说好的点击菜单,只有内容区的会变化,现在怎么Layout都不见了。
2.为什么在Sidebar里定义的是menuData,然后在父组件使用时,可以写成menu-data?
1. 前端领域的「潜规则」
• HTML 属性:HTML 规范要求属性名全小写(比如 <div class="xxx">
),若强行写驼峰形式 <div menuData="xxx">
会被当作字符串属性。
• JS 属性:JavaScript 习惯用驼峰命名变量(如 menuData
),写短横线形式 menu-data
需要加引号,不符合代码审美。
于是 Vue 在中间做了自动翻译:
<!-- 父组件模板(HTML 环境) -->
<ChildComponent menu-data="xxx" /> <!-- 这里用短横线 -->
<!-- 自动翻译成 JS 的驼峰形式 -->
{
menuData: "xxx" // 子组件用驼峰接收
}
2. 为什么要这样设计?
• 兼容性:HTML 不区分大小写,但 JS 区分,这种转换让两者和谐共处。
• 开发者友好:你在 JS 中写符合代码规范的驼峰变量,在模板中写符合 HTML 规范的短横线属性,各取所长。
3. 三种等价写法(Vue 都认)
父组件模板写法 | 被子组件接收的 prop 名 |
---|---|
menu-data="xxx" | menuData |
menuData="xxx" | menuData (不推荐,违反 HTML 规范) |
:menuData="xxx" | menuData (动态 prop 时可用,但依然不推荐) |
一句话总结:Vue 在底层帮你把 menu-data
翻译成 menuData
,这是框架的约定,就像翻译官把中文「你好」自动转成英文「Hello」一样自然。