vue手写一个组件

31次阅读

共计 4172 个字符,预计需要花费 11 分钟才能阅读完成。

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

正文完
 0