vue3 菜单面板实现

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的值,有需要的小伙伴根据自己需求修改样式即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值