vue3 菜单面板实现
最近手写项目,写到了下拉菜单面板,感觉挺常用的,记录一下,下次就直接使用了。直接上代码,复制即用。
效果图
鼠标放置菜单项,右侧出现子菜单
实现过程
1.新建一个子组件menuPanel.vue
<template>
<div class="dropdown-menu" :data-theme="theme">
<ul class="droupdown-menu-list">
<li
v-for="item in menuData"
:key="item.key"
class="dropdown-menu-item"
:class="{ hover: !item?.disabled }"
@mouseenter="onMouseEnter(item)"
@mouseleave="onMouseLeave"
@click.stop="onItem(item)"
>
<div class="menu-item-content" :class="{ disabled: item?.disabled }">
<div class="item-prefix" v-if="hasIcon">
<i class="iconfont " v-if="item.icon" :class="item.icon"></i>
</div>
<span class="item-label">{{ item.label }}</span>
<div class="item-suffix" v-if="item.children && item.children.length">
<i class="iconfont icon-jiantoujiacu-xiangyou"></i>
</div>
<span v-else-if="!item.children && item?.quickValue" class="item-label-text">
{{item?.quickValue}}</span>
</div>
<menuPanel
v-if="item.children && item.children.length && expandKey == item.key"
class="child-panel"
:data-theme="theme"
:menuList="item.children"
@select="onItem"
></menuPanel>
</li>
</ul>
</div>
</template>
<script setup>
import { ref , computed } from 'vue';
const emits = defineEmits(['select']);
const props = defineProps({
menuList: {
type: Array,
default: () => []
},
theme: {
type: String,
default: 'Dark'
}
});
const menuData = computed(()=> props.menuList) ;
const theme = computed(()=> props.theme) ;
const expandKey = ref('');
const onItem = (data) => {
if (!data.children && !data.disabled) {
emits('select', data);
expandKey.value = '';
}
};
const onMouseEnter = (data) => {
if (!data.disabled && Array.isArray(data.children) && data.children.length) {
expandKey.value = data.key;
}
};
const onMouseLeave = () => {
expandKey.value = '';
};
const hasIcon = computed(()=>{
return menuData.value.some(group=>{
return group.some(item=>{
return item.icon != undefined;
});
});
});
</script>
<style lang="scss" scoped>
ul {
list-style-type: none;
padding-left: 0;
}
.dropdown-menu {
position: relative;
width: 200px;
z-index: 999;
box-sizing: border-box;
display: block;
padding: 8px;
border-radius: 6px;
background: var(--overall-surface-default, #23252d);
box-shadow: 0px 2px 16px 0px rgba(0, 0, 0, 0.30), 0px 0px 1px 0px rgba(255, 255, 255, 0.50) inset, 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
.droupdown-menu-list {
width: 100%;
box-sizing: border-box;
padding: 4px 0;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
}
& + .droupdown-menu-list {
border-top: 1px solid var(--border-on-surface-weak, rgba(255, 255, 255, 0.06));
}
}
.dropdown-menu-item {
width: 100%;
position: relative;
cursor: pointer;
box-sizing: border-box;
display: flex;
height: 24px;
padding: 0px 8px 0 8px;
align-items: center;
gap: 4px;
align-self: stretch;
border-radius: 4px;
background: var(--list-background-default, #23252d);
transition: all 0.3s ease;
.menu-item-content {
overflow: hidden;
color: var(--text-on-surface-primary, #e1e4ec);
text-overflow: ellipsis;
width: 100%;
font-size: 12px;
font-style: normal;
font-weight: 400;
letter-spacing: 0.4px;
display: flex;
align-items: center;
gap: 4px;
}
.item-prefix {
width: 14px;
height: 14px;
display: flex;
align-items: center;
margin-right: 4px;
flex-shrink: 0;
i {
font-size: 14px;
color: var(--text-on-surface-primary, #e1e4ec);
}
}
.item-suffix {
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.item-label {
overflow: hidden;
color: var(--text-on-surface-primary, #e1e4ec);
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
flex: 1 0 0;
font-size: 12px;
font-style: normal;
font-weight: 400;
letter-spacing: 0.4px;
}
.item-label-text {
overflow: hidden;
color: var(--text-on-surface-primary, #e1e4ec);
text-overflow: ellipsis;
font-size: 12px;
font-style: normal;
font-weight: 400;
letter-spacing: 0.4px;
}
.iconfont {
font-size: 14px;
color: var(--icon-on-surface-secondary, rgba(197, 199, 208, 1));
}
}
.hover:hover {
background: var(--list-background-selected, #387ffc);
> .menu-item-content {
.item-label,
.iconfont,.item-label-text {
color: var(--text-on-brand-primary, #fff);
}
}
}
.disabled {
.item-label,
.iconfont {
color: var(--text-on-surface-placeholder);
}
}
.child-panel {
position: absolute;
left: 184px;
top: -8px;
}
}
</style>
2.父组件引用菜单子组件
<div style="position: relative;" @click.stop="showMenuPanel = !showMenuPanel">
点击一下
<MenuPanel v-if="showMenuPanel" :menu-list="menuList" style="position: absolute;left: 0px;top: 20px;" @select="onSelectMenu" />
</div>
<script setup>
...
const showMenuPanel = ref(false)
const menuList = ref([
{
label: '返回列表',
key: 'back',
isDivider: true
},
{
label: '文件',
key: 'file',
children: [
{
label: '撤销',
key: 'undo',
quickValue: 'Ctrl Z' // 快捷提示
},
{
label: '恢复',
key: 'redo',
isDivider: true,
quickValue: 'Ctrl Y'
},
{
label: '导入',
key: 'import',
quickValue: 'Ctrl Z'
},
{
label: '导出',
key: 'export',
quickValue: 'Ctrl Z'
},
{
label: '清空',
key: 'clear',
quickValue: 'Ctrl Z'
},
{
label: '复制',
key: 'copy',
children: [
{
label: '粘贴',
key: 'paste',
quickValue: 'Ctrl Z'
},
{
label: '粘贴替换',
key: 'replace',
disabled: true,
quickValue: 'Ctrl Z'
},
{
label: '删除',
key: 'delete',
disabled: true,
isDivider: true,
quickValue: 'Ctrl Z'
}
]
}
]
},
{
label: '编辑',
children: []
},
{
label: '查看',
isDivider: true,
children: []
},
{
label: '偏好设置',
children: []
},
{
label: '帮助与账号',
children: []
}
])
...
</script>
结尾
我这里的背景颜色和字体颜色是通过是否是暗黑模式来控制的,也就是传入的data-theme的值,有需要的小伙伴根据自己需求修改样式即可。