目录
组件简介
组件功能
实现思路
组件源码
父组件
子组件
在使用Vue3做个人项目时,有基于树状菜单实现对地图图层管理、查看图层信息的需求,但是由于页面比较简单,也没有引入Element-UI整个框架的必要,因此,就自定义了如下图所示的树状菜单+右键菜单组件。
树状菜单
组件简介
组件功能
如上图所示,实现了树状菜单的折叠、展开效果,以及单个菜单的勾选、取消勾选,全部菜单的勾选、取消勾选的效果。此外,可以通过右键菜单查看单个图层的信息。具体的业务处理逻辑,可以在树状菜单的点击事件、右键菜单的点击事件回调函数中根据实际需求,进行个性化调整,此处只给出了简单的代码示例。
为了提升右键菜单的复用性,对该部分的代码进行了提取,形成了单个组件,可以自定义右键菜单列表、自定义右键菜单点击事件的处理逻辑。
实现思路
树状菜单+右键菜单的样式自然是通过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>