从事前端也算有几个年头,随着做项目的越来越多,需要一些自己用的轮子可能要自己做一些,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,但中间踩过了坑着时不少,在这里希望和我要样技术不高的同行可以互相学习指证,共同提高。