目录

组件简介

组件功能

实现思路

组件源码

父组件

子组件


        在使用Vue3做个人项目时,有基于树状菜单实现对地图图层管理、查看图层信息的需求,但是由于页面比较简单,也没有引入Element-UI整个框架的必要,因此,就自定义了如下图所示的树状菜单+右键菜单组件。


element tabs 在右侧添加操作按钮_右键菜单

树状菜单

组件简介

组件功能

        如上图所示,实现了树状菜单的折叠、展开效果,以及单个菜单的勾选、取消勾选,全部菜单的勾选、取消勾选的效果。此外,可以通过右键菜单查看单个图层的信息。具体的业务处理逻辑,可以在树状菜单的点击事件、右键菜单的点击事件回调函数中根据实际需求,进行个性化调整,此处只给出了简单的代码示例。

        为了提升右键菜单的复用性,对该部分的代码进行了提取,形成了单个组件,可以自定义右键菜单列表、自定义右键菜单点击事件的处理逻辑。

实现思路

        树状菜单+右键菜单的样式自然是通过CSS来控制的,菜单折叠/展开时,最左侧可以旋转的倒三角是通过伪元素+字体图标实现的。

        交互逻辑是借助Vue的响应式特性实现的,用的是Vue3,因此,每一个组件内部都维护了一个通过reactive方法定义的state状态值,根据实际需求,该状态值可以在onMounted()阶段进行初始化。

        至于右键菜单,是作为图层管理父组件的子组件存在的,涉及到Vue3里面的props和emit两种方式的组件传值,以及自定义事件的注册和监听、ref引用子组件与子组件的方法调用等等。具体可参见《组件源码》部分。

组件源码

父组件

        父组件LayerManager.vue,对应图层管理器的展开、折叠、单个/全部勾选效果,源码如下,

<style lang="less" scoped>
.layer-piacker {
  width: 100%;
  height: 250px;
  background-color: rgb(94, 92, 92);
  overflow: hidden;

  .title {
    padding: 0px 15px;
    height: 35px;
    line-height: 35px;
    border-bottom: 1px solid #fff;
  }

  .box {
    height: calc(100% - 35px);
    background-color: rgb(61, 58, 58);
    overflow-y: scroll;

    .layerGroup {
      width: 100%;
      padding: 0px 25px;

      .item {
        margin: 5px 0px;
        display: flex;
        flex-direction: row;
        align-items: center;
        pointer-events: none;

        .checkbox {
          margin-right: 5px;
          cursor: pointer;
          pointer-events: auto;
        }

        .layer-name {
          cursor: pointer;
          pointer-events: auto;
        }
      }

      // 选中
      .item-wrap::before {
        margin-right: 5px;
        content: "\e600";
        font-family: "iconfont" !important;
        font-size: 16px;
        font-style: normal;
        color: #ccc;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        pointer-events: auto;
      }

      // 未选中
      .item-unwrap::before {
        margin-right: 5px;
        content: "\e601";
        font-family: "iconfont" !important;
        font-size: 16px;
        font-style: normal;
        color: #ccc;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        pointer-events: auto;
      }

      // 图层样式
      .layer {
        padding: 0 35px;
      }

    }
  }

  .box::-webkit-scrollbar {
    /*滚动条整体样式*/
    width: 5px;
    /*高宽分别对应横bai竖滚动条的尺寸*/
    height: 5px;
    scrollbar-arrow-color: rgb(255, 255, 255);
  }
  // 自定义滚动条样式
  .box::-webkit-scrollbar-thumb {
    /*滚动条里面小方块*/
    border-radius: 5px;
    -webkit-box-shadow: inset 0 0 5px rgba(111, 0, 255, 0.2);
    background: rgba(0, 0, 0, 0.2);
    scrollbar-arrow-color: red;
  }

  .box::-webkit-scrollbar-track {
    /*滚动条里面轨道*/
    -webkit-box-shadow: inset 0 0 5px rgba(1, 255, 149, 0.2);
    border-radius: 0;
    background: rgba(0, 0, 0, 0.1);
  }
}
</style>
<template>
  <div class="layer-piacker">
    <p class="title">{{ state.title }}</p>
    <div class="box">
      <div class="layerGroup" v-for="(item, index) in state.layerList" :key="item.id">
        <div :class="item.wrap ? 'item item-wrap' : 'item item-unwrap'" @click="showLayers(item, index)">
          <input class="checkbox" type="checkbox" v-model="item.checked" @change="changeLayerState(item, index, $event)"
            @click.stop="() => { }" />
          <p class="layer-name" @contextmenu.stop.prevent="RightMenuhandler(item, index, $event)">{{ item.name }}</p>
        </div>
        <div v-show="item.wrap">
          <div class="layer" v-for="(layer, no) in item.children" :key="layer.id">
            <div class="item"><input class="checkbox" type="checkbox" v-model="layer.checked"
                @change="changeLayerState(layer, index, $event)" @click.stop="() => { }" />
              <p class="layer-name" @contextmenu.stop.prevent="RightMenuhandler(item, index, $event)">{{ layer.name }}
              </p>
            </div>
          </div>
        </div>

      </div>
    </div>
    <ContextMenuList ref="ContextMenuList_Dom" v-show="state.contextMenuState" :menuListProp="state.contextMenuList"
      @clicMenukHandler="clicMenukHandler" />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { nextTick } from "process"
import ContextMenuList from './ContextMenuList.vue';

//响应式属性
const ContextMenuList_Dom = ref(null);

//组件状态
const state = reactive({
  title: "图层管理器",
  //图层列表
  layerList: [
    {
      id: 1,
      name: "基础底图",
      wrap: true,
      checked: true,
      children: [
        {
          id: "1-1",
          name: "GeoServer黑色底图",
          checked: true,
        }
      ]
    },
    {
      id: 2,
      name: "业务图层",
      wrap: false,
      checked: false,
      children: [
        {
          id: "2-1",
          name: "停车框图层",
          checked: false,
        },
        {
          id: "2-2",
          name: "临时图层",
          checked: false,
        },
        {
          id: "2-3",
          name: "停车框图层",
          checked: false,
        },
        {
          id: "2-4",
          name: "临时图层",
          checked: false,
        },
        {
          id: "2-5",
          name: "停车框图层",
          checked: false,
        },
        {
          id: "2-6",
          name: "临时图层",
          checked: false,
        },
        {
          id: "2-7",
          name: "停车框图层",
          checked: false,
        },
        {
          id: "2-8",
          name: "临时图层",
          checked: false,
        }
      ]
    }
  ],
  contextMenuState: false,//是否显示
  //右键菜单列表
  contextMenuList: [
    {
      id: 1,
      name: "定位到图层",
    },
    {
      id: 2,
      name: "删除图层",
    },
    {
      id: 3,
      name: "属性信息",
    },
    {
      id: 4,
      name: "关闭菜单"
    }
  ],
});

//图层管理器点击事件
const showLayers = (item, index) => {
  console.log(item, index);
  item['wrap'] = !item['wrap'];
}

//修改图层显示/隐藏状态
const changeLayerState = (item, index, event) => {
  // event.preventDefault();
  // console.log(event);
  console.log(item);
  const { checked, children } = item;
  console.log(checked, children);
  //修改子图层状态
  if (Array.isArray(item.children) && children.length > 0) {
    children.forEach(element => {
      element['checked'] = checked;
    });
  }
}

//右键菜单事件
const RightMenuhandler = (item, index, event) => {
  console.log("右键菜单事件")
  console.log(item, index, event);
  const { x, y } = event;
  console.log(x, y)
  //修改状态值
  state.contextMenuState = true;
  nextTick(() => {
    // 调用子组件方法-[nexttTick()-等待子组件装载->状态更新]
    ContextMenuList_Dom.value.setPosition(x + 15, y + 5, item);
  });
}

//右键菜单点击事件-[自定义处理逻辑示例]
const clicMenukHandler = (menu, properties) => {
  console.log(menu, properties);
  const { id, name } = menu;
  //判断执行逻辑
  switch (name) {
    case "定位到图层": {
      console.warn(`定位到图层`);
      state.contextMenuState = false;
      break;
    }
    case "删除图层": {
      console.warn(`删除图层`);
      state.contextMenuState = false;
      break;
    }
    case "属性信息": {
      console.warn(`属性信息`);
      state.contextMenuState = false;
      break;
    }
    case "关闭菜单": {
      console.warn(`关闭菜单`);
      state.contextMenuState = false;
      break;
    }
  }
}

</script>

子组件

        子组件ContextMenuList,对应右键菜单部分,源码如下,

<style lang="less" scoped>
.context-menu-list {
  position: absolute;
  top: 0px;
  left: 0px;
  padding: 15px;
  width: 200px;
  border: 1px solid #ccc;
  border-radius: 5px;
  background-color: rgba(0, 0, 0, 0.8);

  // 菜单项
  .menu-item {
    margin: 5px 0px;
    cursor: pointer;
  }
}
</style>
<template>
  <div class="context-menu-list" ref="contextMenuList">
    <div class="menu-item" v-for="(item, index) in state.menuList" :key="item.id" @click="clickHandle(item, index)">
      <p class="name">{{ item.name }}</p>
    </div>
  </div>
</template>
<script setup>
/**
 * 使用 <script setup> 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。
 * 通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性
 */
import { ref, reactive, onMounted } from 'vue';

//定义props传值
const props = defineProps({
  menuListProp: Array,//菜单列表
})

//定义emit
const emit = defineEmits(["clicMenukHandler"])

//ref-DOM元素引用
const contextMenuList = ref(null);

//定义状态值
const state = reactive({
  selectMenu: undefined,//当前要展示的对象
  menuList: [],//菜单列表
})

//生命周期钩子函数
onMounted(() => {
  state.menuList = props.menuListProp;//初始化state
});


//业务逻辑函数

//修改当前菜单的位置
const setPosition = (x, y, item) => {
  //修改组件的top、left属性
  contextMenuList.value.style.top = `${y}px`;
  contextMenuList.value.style.left = `${x}px`;
  //修改state状态值
  state.selectMenu = item;
}

//事件处理函数
const clickHandle = (item, index) => {
  emit("clicMenukHandler", item,state.selectMenu);//向外传递自定义函数
}

//定义要向外暴露的函数
defineExpose({
  setPosition
})

</script>