从事前端也算有几个年头,随着做项目的越来越多,需要一些自己用的轮子可能要自己做一些,vue用了也有一年多点,初步算能用起来,现在想自己封装一个级联弹窗
之前,用vue写组件大部分是用的 :参数名 这样传入,可有些组件是有交互性的,可能要在里面进行输入或者变更一些值,那如何传递到父级上哪?你可以回绑定一些回调方法,或者定义方法直接调用。但如果只为控制组件的开关或者展示的话就有点大材小用的感觉,可用$refs的方法又必须要注明,感觉好LOW。
之前看到别人用v-model绑定变量就可以实现,于是研究了一下。
vue本身是支持v-model用于自定义组件。不废话直接上代码
parent
<div class="view" @contextmenu="rightClick($event)">
<Menu
v-model="menuShow"
ref="menuComponent"
:menuList="menuList"
:x="menuX"
:y="menuY"
/>
export default {
name: 'demo',
data: function(){
return {
menuShow: false,
// 菜单列表
menuList: [
{
id: 1,
key: '菜单一菜单一菜单一菜单一菜单一菜单一菜单一',
children: [
{
id: 11,
key: '1-1',
children: [
{
id: 111,
key: '1-1-1',
},{
id: 112,
key: '1-1-2',
}
]
}
]
},{
id: 2,
key: '菜单二',
children: [
{
id: 21,
key: '2-1',
children: [
{
id: 111,
key: '2-1-1',
}
]
},{
id: 22,
key: '2-2',
children: [
{
id: 111,
key: '2-2-1',
}
]
},{
id: 23,
key: '2-3',
}
]
},{
id: 3,
key: '菜单三',
}
],
// 菜单坐标
menuX: 0,
menuY: 0,
}
},
components: {
Menu,
},
methods: {
rightClick: function (e) {
e.preventDefault();
console.log("当前被右击了");
// 传入点击时的视口坐标
this.menuX = e.clientX;
this.menuY = e.clientY;
this.menuShow = true;
}
}
}
child
<template>
<div
v-if="value"
:style="{top: positionY, left: positionX}"
class="menuMain"
:class="direction==1?'right':'left'"
@mouseleave="leaveMenu"
@mouseover="subTitEnter($event)">
<ul v-html="menuDomStr" @click="selectClick($event)">
</ul>
</div>
</template>
<script>
export default {
name: 'menuMain',
props: {
value: Boolean,
// menuShow: Boolean,
menuList: Array,
callback: Function,
x: Number,
y: Number,
},
data: function() {
return {
// 解析后的菜单DOM字符串
menuDomStr: "",
// 是左弹还是右弹 1 右弹 2 左弹
direction: 1,
// 菜单最大深度
depth: 1,
// 单层菜单的最大宽度
maxWidth: 102,
// 定位坐标
positionX: '0px',
positionY: '0px',
}
},
methods: {
// 解析传入菜单数据 递归解析菜单数据
parsing: function(data, dep) {
var html = "";
this.depth = this.depth>dep?this.depth:dep;
for(var i=0; i<data.length; i++){
var one = data[i];
// console.log(one);
if(one.children && one.children.length){
// 有子菜单
html += "<li class='item subMenuItem'><span class='tit' title='"+one.key+"'>" + one.key + "</span><ul class='subMenu'>" + this.parsing(one.children, dep+1) + "</ul></li>";
}else{
// 当前是最底层菜单
html += "<li class='item'><span class='menuNode' title='"+one.key+"' data-id='" + one.id + "'>" + one.key + "</span></li>";
}
}
return html;
},
// 选中事件
selectClick: function(e) {
// console.log(e);
var targetEl = e.target;
if(targetEl.className == 'menuNode'){
console.log("当前最后子节点");
var itemId = targetEl.getAttribute("data-id");
if(this.callback){
this.callback(itemId);
}
this.show = false;
console.log(targetEl.getAttribute("data-id"));
}else{
console.log("子菜标题");
}
},
// 鼠标移出菜单
leaveMenu: function() {
// this.value = false;
this.$emit('input', false);
},
// 子级菜单 标题进入
subTitEnter: function(e) {
var targetEl = e.target;
// console.log(e);
if(targetEl.className == "tit"){
console.log(targetEl.getClientRects());
}
}
},
watch: {
// 自定义组件 v-model传入的值是value
value: function(newValue, oldValue) {
if(newValue){
// 当要展示菜单时
// 解析传入菜单数据
this.menuDomStr = this.parsing(this.menuList, 1);
// 当前视口宽度
var viewW = window.innerWidth;
var viewH = window.innerHeight;
// 当前菜单最大深度展开宽度
var menuMaxWidth = this.maxWidth * this.depth;
// 横向超屏检测
if(this.x+this.maxWidth > viewW){
// 一层都展不开的超屏 向左展开菜单
this.direction = 2;
this.positionX = viewW - (this.maxWidth+5)+'px';
}else if(this.x+(this.maxWidth*this.depth) > viewW){
// 能展开第一层,但展不开最大深度
this.direction = 2;
this.positionX = (this.x?this.x-5:0)+'px';
}else{
// 都没有问题
this.direction = 1;
this.positionX = (this.x?this.x:0)+'px';
}
// 纵向超屏检测
this.positionY = viewH>this.menuList.length*32+this.y?this.y-5+'px':(viewH-this.menuList.length*32-5+'px');
}else{
// 自定义组件要用 父级input回传数据
this.$emit("input", false);
}
}
}
}
</script>
<style lang="less" >
ul,li{
list-style: none;
padding: 0;
margin: 0;
background-color: #fff;
}
.menuMain{
position: fixed;
z-index: 99999;
ul{
border: 1px solid #aaa;
&.subMenu{
display: none;
position: absolute;
top: -1px;
}
}
&.right ul.subMenu{
left: 100%;
}
&.left ul.subMenu{
right: 100%;
}
li{
position: relative;
white-space: nowrap;
font-size: 12px;
cursor: pointer;
&:not(:last-child){
padding-bottom: 1px;
&:after{
content: "";
display: block;
position: absolute;
left: 5px;
right: 5px;
bottom: 0;
height: 1px;
background-color: #aaa;
}
}
>span{
display: block;
padding: 0 10px;
height: 30px;
line-height: 30px;
max-width: 100px;
text-overflow: ellipsis;
overflow: hidden;
}
&.subMenuItem:hover>ul{
display: block;
}
}
}
</style>
这里面有几个比较坑的地方
1、自定义级件如果用v-model接收传值的话,prop 接收的值是value,等同于input,可能是vue一开始v-model的传值是给input这样的表单组件设置的吧
2、当value值发生变化后,要用 $emit('input', value) 进行回传,回传的事件然一定是input,原因我也不知道
这是我写的一个很初步的弹窗组件,写的还是很low,但中间踩过了坑着时不少,在这里希望和我要样技术不高的同行可以互相学习指证,共同提高。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。