• 使用vue自定义实现Tree组件和拖拽功能


    实现功能:树结构、右键菜单、拖拽
    效果图
    请添加图片描述

    vue2 + js版

    /components/drag-tree/utils/utils.js

    let _treeId = 0;
    /**
     * 初始化树
     * @param {Array} tree 树的原始结构
     * @param {Object} props 树的字段值
     * @param {Boolean} defaultExpandAll 是否展开节点
     */
    function initTree(tree, props, defaultExpandAll: boolean) {
      let right = localStorage.getItem("right");
      right = JSON.parse(right);
      return initTreed(tree, 1, props, defaultExpandAll, [], right);
    }
    /**
     * 初始化树
     * @param {Array} tree 树的原始结构
     * @param {Number} layer 层级
     * @param {Object} props 树的字段值
     * @param {Boolean} defaultExpandAll 是否展开节点
     * @param {Array} props 新树
     * @param {Array} right 判断节点展不展开
     */
    function initTreed(tree, layer, props, defaultExpandAll, newTree, right) {
      for (let i = 0; i < tree.length; i++) {
        let obj {};
        for (const item in tree[i]) {
          if (item === props.label) {
            obj.label = tree[i][item];
          } else if (item === props.id) {
            obj.id = tree[i][item];
          } else if (item === props.children && tree[i][props.children].length) {
            obj.children = [];
          } else {
            obj[item] = tree[i][item];
            if (item === "children") {
              delete obj.children
            }
          }
        }
        if (right) {
          right.indexOf(obj.id) !== -1 ?
            (obj.defaultExpandAll = true) :
            (obj.defaultExpandAll = false);
        } else {
          obj.defaultExpandAll = defaultExpandAll;
        }
    
        obj._treeId = _treeId++;
        obj.layer = layer;
        obj.data = JSON.parse(JSON.stringify(tree[i]));
        newTree.push(obj);
        if ("children" in obj) {
          initTreed(
            tree[i][props.children],
            layer + 1,
            props,
            defaultExpandAll,
            newTree[i].children,
            right
          );
        }
        obj = {};
      }
      return newTree;
    }
    
    /**
     *
     * @param {Array} tree 树
     * @param {Number} layer 层级
     * @returns
     */
    function draggableTree(tree: IAnyType[], layer) {
      for (let i = 0; i < tree.length; i++) {
        tree[i].layer = layer;
        if ("children" in tree[i]) {
          draggableTree(tree[i].children, layer + 1);
        }
      }
      return tree;
    }
    /**
     * 寻找
     */
    function findNearestComponent(element, componentName) {
      let target = element;
      while (target && target.tagName !== "BODY") {
        if (target.__vue__ && target.__vue__.$options.name === componentName) {
          return target.__vue__;
        }
        target = target.parentNode;
      }
      return null;
    }
    export {
      initTree,
      draggableTree,
      findNearestComponent
    };
    

    /components/drag-tree/node.vue

    <template>
      <div
        class="drag-tree item"
        :draggable="tree.draggable"
        @dragstart.stop="dragstart"
        @dragover.stop="dragover"
        @drop.stop="drop"
        @contextmenu="($event) => this.handleContextMenu($event)"
        ref="item"
        :id="data._treeId"
      >
        <!-- 每一行 -->
        <div
          style="height: 1px"
          :style="{ background: dropType == 'before' ? `#${draggableColor}` : '' }"
        ></div>
        <div
          @click="itemClick($event, data)"
          :class="['text', active === data.id ? 'is-current' : '']"
          :style="{
            height: height,
            lineHeight: height,
            fontSize: fontSize,
            position: 'relative',
            margin: '0 auto',
          }"
        >
          <span
            :style="{
              display: 'inline-block',
              width: (data.layer - 1) * 18 + 'px',
            }"
          ></span>
          <img
            :class="[data.defaultExpandAll ? 'iconBottom' : 'iconRight']"
            v-show="data.children && data.children.length !== 0"
            :src="iconImg"
            :style="{
              width: fontSize,
              height: fontSize,
              display: 'inline-block',
              verticalAlign: 'middle',
              marginRight: '3px',
            }"
            alt=""
          />
          <span
            v-show="!data.children || data.children.length == 0"
            :style="{
              width: fontSize,
              height: fontSize,
              display: 'inline-block',
              verticalAlign: 'middle',
              marginRight: '3px',
            }"
          ></span>
          <img
            v-if="data.TreeImg"
            :src="dataImg"
            :style="{
              width: fontSize,
              height: fontSize + 5,
              display: 'inline-block',
              verticalAlign: 'middle',
              marginRight: '3px',
            }"
          />
          <span
            :style="{
              background: dropType == 'inner' ? `#${draggableColor}` : '',
              height: fontSize + 5,
              color: dropType == 'inner' ? '#fff' : '#7d90b2',
              overflow: 'hidden',
            }"
            >{{ data.label }}{{ data.isCurrent }}</span
          >
          <node-content :node="data"></node-content>
        </div>
        <div
          style="height: 1px"
          :style="{ background: dropType == 'after' ? `#${draggableColor}` : '' }"
        ></div>
        <div
          v-if="data.children && data.children.length != 0"
          :class="[data.defaultExpandAll ? 'sonShow' : 'sonVanish', 'son']"
        >
          <my-node
            v-for="item in data.children"
            :key="item._treeId"
            :render-content="renderContent"
            :data="item"
            :active-id.sync="active"
          ></my-node>
        </div>
      </div>
    </template>
    
    <script>
    import { findNearestComponent } from "./utils/utils.ts";
    export default {
      name: "MyNode",
      props: {
        data: {
          // 接收的数据
          type: Object,
        },
        activeId: {
          type: [Number, String]
        },
        renderContent: Function,
      },
      components: {
        NodeContent: {
          props: {
            node: {
              required: true
            }
          },
          render(h) {
            const parent = this.$parent;
            const tree = parent.tree;
            const node = this.node;
            const { data, store } = node;
            return (
              parent.renderContent
                ? parent.renderContent.call(parent._renderProxy, h, { _self: tree.$vnode.context, node, data, store })
                : tree.$scopedSlots.default
                  ? tree.$scopedSlots.default({ node, data })
                  : ''
            );
          }
        }
      },
      inject: ["draggableColor", "height", "fontSize", "icon"],
      data() {
        return {
          curNode: null,
          tree: "", // 最上一级
          dropType: "none",
          iconImg: "",
          dataImg: "",
        };
      },
      computed: {
        active: {
          set (val) {
            this.$emit("update:activeId", val);
          },
          get () {
            return this.activeId;
          }
        }
      },
      created() {
        let parent = this.$parent;
        if (parent.isTree) {
          this.tree = parent;
        } else {
          this.tree = parent.tree;
        }
        // console.log(this.$parent)
        // console.log(this.tree)
        // console.log(parent)
        // console.log(parent.isTree)
        // console.log(parent.tree)
        // 有没有自定义icon
        if (this.icon.length != 0) {
          let s = this.icon.slice(0, 2);
          let url = this.icon.slice(2);
          if (s == "@/") {
            this.iconImg = require(`@/${url}`);
          } else {
            this.iconImg = this.icon;
          }
        } else {
          this.iconImg = require("@/assets/images/business/tree/right.png");
        }
        if (this.data.TreeImg) {
          let s = this.data.TreeImg.slice(0, 2);
          let url = this.data.TreeImg.slice(2);
          if (s == "@/") {
            this.dataImg = require(`@/${url}`);
          } else {
            this.dataImg = this.data.TreeImg;
          }
        }
      },
      mounted() {
        document.body.addEventListener('click', this.closeMenu);
      },
      destroyed() {
        document.body.removeEventListener('click', this.closeMenu);
      },
      methods: {
        closeMenu() {
          this.tree.$emit('close-menu');
        },
        handleContextMenu(event) {
          if (this.tree._events['node-contextmenu'] && this.tree._events['node-contextmenu'].length > 0) {
            event.stopPropagation();
            event.preventDefault();
          }
          this.tree.$emit('node-contextmenu', event, this.data, this);
        },
        // 选择要滑动的元素
        dragstart(ev) {
          if (!this.tree.draggable) return;
          this.tree.$emit("node-start", this.data, this, ev);
        },
        // 滑动中
        dragover(ev) {
          if (!this.tree.draggable) return;
          ev.preventDefault();
          this.tree.$emit("node-over", this.data, this, ev);
        },
        // 滑动结束
        drop(ev) {
          if (!this.tree.draggable) return;
          this.tree.$emit("node-drop", this.data, this, ev);
        },
        // 行点击事件
        itemClick(ev, data) {
          let dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点
          this.active = data.id;
          this.data.defaultExpandAll = !this.data.defaultExpandAll; // 改变树的伸缩状态
          this.tree.$emit("tree-click", this.data, dropNode);
          let right = localStorage.getItem("right");
          if (this.data.defaultExpandAll === true) {
            if (right) {
              right = JSON.parse(right);
              right.push(this.data.id);
            } else {
              right = [];
              right.push(this.data.id);
            }
          } else {
            if (right) {
              right = JSON.parse(right);
              right.indexOf(this.data.id) !== -1
                ? right.splice(right.indexOf(this.data.id), 1)
                : "";
            }
          }
          localStorage.setItem("right", JSON.stringify(right));
        },
      },
    };
    </script>
    
    <style lang="less">
    .drag-tree {
      .text {
        color: #7d90b2;
        font-size: 14px;
        height: 32px;
        line-height: 32px;
        cursor: pointer;
        &.is-current {
          background: #f5f7fa;
        }
      }
      .text:hover {
        background: #f5f7fa;
      }
      .iconBottom {
        transition: 0.3s;
        transform: rotate(90deg);
      }
      .iconRight {
        transition: 0.3s;
        transform: rotate(0deg);
      }
      .son {
        max-height: 0px;
        overflow: hidden;
        transition: 0.3s max-height;
      }
      .sonVanish {
        max-height: 0px;
      }
      .sonShow {
        max-height: 1000px;
      }
      &-popover {
        width: 100px;
        height: auto;
        position: fixed;
        background: #fff;
        border: 1px solid #ddd;
        box-shadow: 0 1px 6px rgba(54, 54, 54, 0.2);
        z-index: 9999;
        border-radius: 4px;
        &-item {
          color: #515a6e;
          line-height: 35px;
          text-align: center;
          cursor: pointer;
          transition: background .2s ease-in-out;
          &:hover, &:active {
            background: #f3f3f3;
          }
        }
      }
    }
    </style>
    

    /components/drag-tree/index.vue

    <template>
      <div style="width: 100%; height: 100%">
        <Node
          :render-content="renderContent"
          v-for="item in root"
          :key="item._treeId"
          :data="item"
          :active-id.sync="activeId"
          :isTree="true"
        ></Node>
      </div>
    </template>
    
    <script>
    import Node from "./node.vue";
    import { initTree, findNearestComponent } from "./utils/utils.ts";
    export default {
      name: "TreeDrag",
      components: {
        Node,
      },
      provide() {
        return {
          draggableColor: this.draggableColor,
          height: this.height,
          fontSize: this.fontSize,
          icon: this.icon,
        };
      },
      props: {
        data: {
          type: Array,
        },
        renderContent: Function,
        draggable: {
          // 是否开启拖拽
          type: Boolean,
          default: false,
        },
        defaultExpandAll: {
          // 是否默认展开所有节点
          type: Boolean,
          default: false,
        },
        draggableColor: {
          // 拖拽时的颜色
          type: String,
          default: "409EFF",
        },
        height: {
          // 每行高度
          type: String,
          default: "40px",
        },
        fontSize: {
          type: String,
          default: "14px",
        },
        icon: {
          type: String,
          default: "",
        },
        props: {
          type: Object,
          default() {
            return {
              label: "label",
              children: "children",
            };
          },
        },
      },
      watch: {
        data(nerVal) {
          this.root = initTree(nerVal, this.props, this.defaultExpandAll); // 新树
          if (this.root?.length && !this.activeId) {
            this.activeId = this.root[0].id;
          }
        },
        deep: true
      },
      data() {
        return {
          activeId: 0,
          startData: {}, // 拖拽时被拖拽的节点
          lg1: null, // 拖拽经过的最后一个节点
          lg2: null, // 拖拽经过的最后第二个节点
          root: null, // data的数据
          dragState: {
            showDropIndicator: false,
            draggingNode: null, // 拖动的节点
            dropNode: null,
            allowDrop: true,
          },
          odata: "",
        };
      },
      created() {
        this.odata = this.data;
        this.isTree = true; // 这是最高级
        this.root = initTree(this.data, this.props, this.defaultExpandAll); // 新树
    
        // 选择移动的元素 事件
        this.$on("node-start", (data, that, ev) => {
          this.startData = data;
          this.dragState.draggingNode = that;
          this.$emit("tree-start", that.data.data, that.data, ev);
        });
    
        // 移动事件
        this.$on("node-over", (data, that, ev) => {
          console.log(2222)
          console.log(ev.target)
          if (that.$refs.item.id != this.lg1) {
            this.lg2 = this.lg1;
            this.lg1 = that.$refs.item.id;
          }
          let dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点
          const oldDropNode = this.dragState.dropNode; // 上一个节点
          if (oldDropNode && oldDropNode !== dropNode) {
            // 判断节点改没改变
            oldDropNode.dropType = "none";
          }
    
          const draggingNode = this.dragState.draggingNode; // 移动的节点
          console.log(draggingNode)
          console.log(dropNode)
          console.log(this.dragState)
          if (!draggingNode || !dropNode) return;
    
          console.log(33333)
          let dropPrev = true; // 上
          let dropInner = true; // 中
          let dropNext = true; // 下
          ev.dataTransfer.dropEffect = dropInner ? "move" : "none";
          this.dragState.dropNode = dropNode;
    
          const targetPosition = dropNode.$el.getBoundingClientRect();
          const prevPercent = dropPrev
            ? dropInner
              ? 0.25
              : dropNext
              ? 0.45
              : 1
            : -1;
          const nextPercent = dropNext
            ? dropInner
              ? 0.75
              : dropPrev
              ? 0.55
              : 0
            : 1;
          var dropType = "";
    
          const distance = ev.clientY - targetPosition.top;
          if (distance < targetPosition.height * prevPercent) {
            // 在上面
            dropType = "before";
          } else if (distance > targetPosition.height * nextPercent) {
            // 在下面
            dropType = "after";
          } else if (dropInner) {
            dropType = "inner";
          } else {
            dropType = "none";
          }
          if (this.digui(draggingNode.data, dropNode.data._treeId)) {
            dropType = "none";
          }
          dropNode.dropType = dropType;
          console.log(1111111)
          console.log(dropType)
          this.$emit("tree-over", that.data.data, that.data, ev, dropType);
        });
    
        // 移动结束 事件
        this.$on("node-drop", (data, that, ev) => {
          console.log(data, that, ev)
          console.log(this.startData)
          let sd = JSON.stringify(this.startData.data);
          let ad = JSON.stringify(this.data);
          let ss = ad.split(sd);
          let newData;
          ss = ss.join("");
          console.log(that.dropType)
          if (that.dropType == "none") {
            return;
          }
          console.log(that.dropType)
          if (this.lg2 != null && this.lg1 != this.startData._treeId) {
            // 删除startData
            ss = this.deleteStr(ss);
            let od = JSON.stringify(data.data);
            let a = ss.indexOf(od);
            console.log(newData)
            if (that.dropType == "after") {
              newData = JSON.parse(
                ss.substring(0, a + od.length) +
                  "," +
                  sd +
                  ss.substring(a + od.length)
              );
            } else if (that.dropType == "before") {
              if (a == -1) {
                let s = this.deleteStr(od.split(sd).join(""));
                newData = JSON.parse(
                  ss.substring(0, ss.indexOf(s)) +
                    sd +
                    "," +
                    ss.substring(ss.indexOf(s))
                );
              } else {
                newData = JSON.parse(
                  ss.substring(0, a) + sd + "," + ss.substring(a)
                );
              }
            } else if (that.dropType == "inner") {
              ss = JSON.parse(ss);
              this.oldData(ss, data.data, JSON.parse(sd));
              newData = ss;
            }
            console.log(newData)
            this.root = initTree(newData, this.props, this.defaultExpandAll); // 新树
            this.$parent.data = newData;
            this.lg1 = null;
            this.lg2 = null;
          }
    
          this.$emit(
            "tree-drop",
            this.data.data,
            this.data,
            ev,
            this.startData.id,
            data.id,
            that.dropType,
            this.root
          );
          that.dropType = "none";
        });
      },
      methods: {
        /**
         * 修改data,添加输入
         * @param {Array} ss 需要被加入的数据
         * @param {Object} data 落点
         * @param {Object} sd 需要加入的数据
         */
        oldData(ss, data, sd) {
          for (let i = 0; i < ss.length; i++) {
            if (JSON.stringify(ss[i]) == JSON.stringify(data)) {
              if ("children" in ss[i]) {
                ss[i].children.push(sd);
              } else {
                ss[i].children = [];
                ss[i].children.push(sd);
              }
              break;
            } else if ("children" in ss[i]) {
              this.oldData(ss[i].children, data, sd);
            }
          }
        },
        // 判断拖拽时贴近的是不是自己的子元素
        digui(data, id) {
          if (data.children && data.children.length != 0) {
            for (let i = 0; i < data.children.length; i++) {
              if (data.children[i]._treeId == id) {
                return true;
              }
              let s = this.digui(data.children[i], id);
              if (s == true) {
                return true;
              }
            }
          }
        },
        deleteStr(ss) {
          if (ss.indexOf(",,") !== -1) {
            ss = ss.split(",,");
            if (ss.length !== 1) {
              ss = ss.join(",");
            }
          } else if (ss.indexOf("[,") !== -1) {
            ss = ss.split("[,");
            if (ss.length !== 1) {
              ss = ss.join("[");
            }
          } else if (ss.indexOf(",]") !== -1) {
            ss = ss.split(",]");
            if (ss.length !== 1) {
              ss = ss.join("]");
            }
          }
          return ss;
        },
      },
    };
    </script>
    
    <style scoped>
    .drag {
      font-size: 14px;
      text-align: right;
      padding-right: 5px;
      cursor: pointer;
    }
    </style>
    

    使用:Test.vue

    <template>
      <div style="width: 100%; height: 100%;">
        <tree-drag
          ref="dragTree"
          @node-contextmenu="handleContextMenu"
          @tree-click="treeClick"
          @tree-drop="treeDrop"
          @close-menu="closeMenu"
          :data="data"
          :props="defaultProps"
          :draggable="true"
        >
        </tree-drag>
        <div class="drag-tree-popover" :style="style" v-if="isShowPopover">
          <div
            class="drag-tree-popover-item"
            v-for="(item, index) in popoverList"
            :key="index"
            @click="menuClick(item)"
          >
            <i class="iconfont" :class="'icon-' + item.type"></i>
            {{ item.name }}
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: "Test",
      data() {
        return {
          parent_id: "0",
          data: [],
          defaultProps: {
            children: "children",
            label: "name",
          },
          popoverLeft: 0, // 距离左边的距离
          popoverTop: 0, // 距离顶部的距离
          isShowPopover: false, // 是否展示右键内容
          popoverList: [
            { name: "新增", type: "xinzeng" },
            { name: "编辑", type: "bianji" },
            { name: "删除", type: "shanchu" },
          ],
          treeNode: null,
          activeId: 0,
        };
      },
      created() {
        this.getTreeData();
      },
      computed: {
        // 计算出距离
        style() {
          return {
            left: this.popoverLeft + "px",
            top: this.popoverTop + "px",
          };
        },
      },
      methods: {
        // 显示自定义菜单
        handleContextMenu(event, node, that) {
          this.popoverLeft = event.clientX + 10;
          this.popoverTop = event.clientY;
          this.isShowPopover = true;
        },
        // 关闭菜单
        closeMenu() {
          this.isShowPopover = false;
        },
        treeClick(data) {
          this.activeId = data.id;
        },
        treeDrop(node, data, ev, startId, targetId, dropType, root) {
          console.log(startId, targetId, dropType, root);
        },
        // 菜单某一项被点击
        menuClick(item) {
          // 操作
          this.closeMenu();
        },
        // 判断activeId是否存在
        findIdIsExit(data, id) {
          if (data && data.length) {
            for (let i = 0; i < data.length; i++) {
              if (data[i].id == id) {
                return true;
              }
              if (data[i].children && data[i].children.length) {
                let s = this.findIdIsExit(data[i].children, id);
                if (s === true) {
                  return true;
                }
              }
            }
          }
        },
        async getTreeData() {
          let res = await this.$service.invoke({});
          this.data = res?.result ? res.result : [];
          this.activeId = this.data[0].id;
          this.$refs.dragTree.activeId = this.activeId;
        },
      },
    };
    </script>
    

    vue2 + ts 版

    只有两个组件的ts部分文件不一样,其他一样
    /components/drag-tree/node.vue

    <template>
      <div
        class="drag-tree item"
        :draggable="tree.draggable"
        @dragstart.stop="dragstart"
        @dragover.stop="dragover"
        @drop.stop="drop"
        @contextmenu="($event) => this.handleContextMenu($event)"
        ref="item"
        :id="data._treeId"
      >
        <!-- 每一行 -->
        <div
          style="height: 1px"
          :style="{ background: dropType == 'before' ? `#${draggableColor}` : '' }"
        ></div>
        <div
          @click="itemClick($event, data)"
          :class="['text', active === data.id ? 'is-current' : '']"
          :style="{
            height: height,
            lineHeight: height,
            fontSize: fontSize,
            position: 'relative',
            margin: '0 auto',
          }"
        >
          <span
            :style="{
              display: 'inline-block',
              width: (data.layer - 1) * 18 + 'px',
            }"
          ></span>
          <img
            :class="[data.defaultExpandAll ? 'iconBottom' : 'iconRight']"
            v-show="data.children && data.children.length !== 0"
            :src="iconImg"
            :style="{
              width: fontSize,
              height: fontSize,
              display: 'inline-block',
              verticalAlign: 'middle',
              marginRight: '3px',
            }"
            alt=""
          />
          <span
            v-show="!data.children || data.children.length == 0"
            :style="{
              width: fontSize,
              height: fontSize,
              display: 'inline-block',
              verticalAlign: 'middle',
              marginRight: '3px',
            }"
          ></span>
          <img
            v-if="data.TreeImg"
            :src="dataImg"
            :style="{
              width: fontSize,
              height: fontSize + 5,
              display: 'inline-block',
              verticalAlign: 'middle',
              marginRight: '3px',
            }"
          />
          <span
            :style="{
              background: dropType == 'inner' ? `#${draggableColor}` : '',
              height: fontSize + 5,
              color: dropType == 'inner' ? '#fff' : '#7d90b2',
              overflow: 'hidden',
            }"
            >{{ data.label }}{{ data.isCurrent }}</span
          >
          <node-content :node="data"></node-content>
        </div>
        <div
          style="height: 1px"
          :style="{ background: dropType == 'after' ? `#${draggableColor}` : '' }"
        ></div>
        <div
          v-if="data.children && data.children.length != 0"
          :class="[data.defaultExpandAll ? 'sonShow' : 'sonVanish', 'son']"
        >
          <my-node
            v-for="item in data.children"
            :key="item._treeId"
            :render-content="renderContent"
            :data="item"
            :active-id.sync="active"
          ></my-node>
        </div>
      </div>
    </template>
    
    <script lang="ts">
    import node from "./node";
    export default node;
    </script>
    
    <style lang="less">
    .drag-tree {
      .text {
        color: #7d90b2;
        font-size: 14px;
        height: 32px;
        line-height: 32px;
        cursor: pointer;
        &.is-current {
          background: #f5f7fa;
        }
      }
      .text:hover {
        background: #f5f7fa;
      }
      .iconBottom {
        transition: 0.3s;
        transform: rotate(90deg);
      }
      .iconRight {
        transition: 0.3s;
        transform: rotate(0deg);
      }
      .son {
        max-height: 0px;
        overflow: hidden;
        transition: 0.3s max-height;
      }
      .sonVanish {
        max-height: 0px;
      }
      .sonShow {
        max-height: 1000px;
      }
      &-popover {
        width: 100px;
        height: auto;
        position: fixed;
        background: #fff;
        border: 1px solid #ddd;
        box-shadow: 0 1px 6px rgba(54, 54, 54, 0.2);
        z-index: 9999;
        border-radius: 4px;
        &-item {
          color: #515a6e;
          line-height: 35px;
          text-align: center;
          cursor: pointer;
          transition: background .2s ease-in-out;
          &:hover, &:active {
            background: #f3f3f3;
          }
        }
      }
    }
    </style>
    

    /components/drag-tree/node.ts

    import { Vue, Component, Prop, PropSync, Inject } from "vue-property-decorator";
    import { findNearestComponent } from "./utils/utils";
    @Component({
      name: "MyNode",
      components: {
        NodeContent: {
          props: {
            node: {
              required: true
            }
          },
          render(h) {
            const parent = this.$parent;
            const tree = parent.tree;
            const node = this.node;
            const { data, store } = node;
            return (
              parent.renderContent
                ? parent.renderContent.call(parent._renderProxy, h, { _self: tree.$vnode.context, node, data, store })
                : tree.$scopedSlots.default
                  ? tree.$scopedSlots.default({ node, data })
                  : ''
            );
          }
        }
      }
    })
    export default class node extends Vue {
      @Prop() data: IAnyType
      @PropSync("activeId", { type: [Number, String] }) active!: string | number
      @Prop(Function) renderContent
    
      @Inject("draggableColor") readonly draggableColor!: string
      @Inject("height") readonly height!: string
      @Inject("fontSize") readonly fontSize!: string
      @Inject("icon") readonly icon!: string
    
      curNode = null
      tree: IAnyType // 最上一级
      dropType = "none"
      iconImg = ""
      dataImg = ""
    
      created(): void {
        const parent: any = this.$parent;
        if (parent.isTree) {
          this.tree = parent;
        } else {
          this.tree = parent.tree;
        }
        // 有没有自定义icon
        if (this.icon.length != 0) {
          const s = this.icon.slice(0, 2);
          const url = this.icon.slice(2);
          if (s == "@/") {
            this.iconImg = require(`@/${url}`);
          } else {
            this.iconImg = this.icon;
          }
        } else {
          this.iconImg = require("@/assets/images/business/tree/right.png");
        }
        if (this.data.TreeImg) {
          const s = this.data.TreeImg.slice(0, 2);
          const url = this.data.TreeImg.slice(2);
          if (s == "@/") {
            this.dataImg = require(`@/${url}`);
          } else {
            this.dataImg = this.data.TreeImg;
          }
        }
      }
      mounted(): void {
        document.body.addEventListener("click", this.closeMenu);
      }
      destroyed(): void {
        document.body.removeEventListener("click", this.closeMenu);
      }
      closeMenu(): void {
        this.tree.$emit("close-menu");
      }
      handleContextMenu(event: DragEvent): void {
        if (this.tree._events["node-contextmenu"] && this.tree._events["node-contextmenu"].length > 0) {
          event.stopPropagation();
          event.preventDefault();
        }
        this.tree.$emit("node-contextmenu", event, this.data, this);
      }
      // 选择要滑动的元素
      dragstart(ev: DragEvent): void {
        if (!this.tree.draggable) return;
        this.tree.$emit("node-start", this.data, this, ev);
      }
      // 滑动中
      dragover(ev: DragEvent): void {
        if (!this.tree.draggable) return;
        ev.preventDefault();
        this.tree.$emit("node-over", this.data, this, ev);
      }
      // 滑动结束
      drop(ev: DragEvent): void {
        if (!this.tree.draggable) return;
        this.tree.$emit("node-drop", this.data, this, ev);
      }
      // 行点击事件
      itemClick(ev: DragEvent, data: IAnyType): void {
        const dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点
        this.active = data.id;
        this.data.defaultExpandAll = !this.data.defaultExpandAll; // 改变树的伸缩状态
        this.tree.$emit("tree-click", this.data, dropNode);
        const right: string = localStorage.getItem("right");
        let rightArr: IAnyType[];
        if (right) {
          rightArr = JSON.parse(right);
        }
        if (this.data.defaultExpandAll === true) {
          if (right) {
            rightArr.push(this.data.id);
          } else {
            rightArr = [];
            rightArr.push(this.data.id);
          }
        } else {
          if (right) {
            rightArr.indexOf(this.data.id) !== -1
              ? rightArr.splice(rightArr.indexOf(this.data.id), 1)
              : "";
          }
        }
        localStorage.setItem("right", JSON.stringify(rightArr));
      }
    }
    

    /components/drag-tree/index.vue

    <template>
      <div style="width: 100%; height: 100%">
        <Node
          :render-content="renderContent"
          v-for="item in root"
          :key="item._treeId"
          :data="item"
          :active-id.sync="activeId"
          :isTree="true"
        ></Node>
      </div>
    </template>
    
    <script lang="ts">
    import index from "./index";
    export default index;
    </script>
    
    <style scoped>
    .drag {
      font-size: 14px;
      text-align: right;
      padding-right: 5px;
      cursor: pointer;
    }
    </style>
    

    /components/drag-tree/index.ts

    import { Vue, Component, Provide, Prop, Watch } from "vue-property-decorator";
    import Node from "./node.vue";
    import { initTree, findNearestComponent } from "./utils/utils";
    
    @Component({
      name: "TreeDrag",
      components: {
        Node
      }
    })
    export default class index extends Vue {
    
      @Prop({ default: [] }) data?: any[]
      @Prop(Function) renderContent
      @Prop({ default: true }) isTree?: boolean
      // 是否开启拖拽
      @Prop({ default: false }) draggable?: boolean
      // 是否默认展开所有节点
      @Prop({ default: false }) defaultExpandAll?: boolean
      // 拖拽时的颜色
      @Prop({ default: "409EFF" }) dragColor: string
      // 每行高度
      @Prop({ default: "40px" }) lineHeight: string
      @Prop({ default: "14px" }) lineFontSize: string
      @Prop({ default: "" }) iconName: string
      @Prop({
        default: () => {
          return {
            label: "label",
            children: "children",
          }
        }
      }) props: IAnyType
    
      @Provide("draggableColor")
      draggableColor = "409EFF"
      @Provide("height")
      height = "40px"
      @Provide("fontSize")
      fontSize = "14px"
      @Provide("icon")
      icon = ""
      activeId = 0
      startData = {
        data: [],
        _treeId: "",
        id: ""
      } // 拖拽时被拖拽的节点
      lg1 = null // 拖拽经过的最后一个节点
      lg2 = null // 拖拽经过的最后第二个节点
      root = null // data的数据
      dragState = {
        showDropIndicator: false,
        draggingNode: null, // 拖动的节点
        dropNode: null,
        allowDrop: true,
      }
      odata = []
    
      @Watch("data", { deep: true })
      onData(nerVal) {
        this.root = initTree(nerVal, this.props, this.defaultExpandAll); // 新树
        if (this.root?.length && !this.activeId) {
          this.activeId = this.root[0].id;
        }
      }
      @Watch("dragColor", { immediate: true })
      onDragColor(nerVal) {
        this.draggableColor = nerVal;
      }
      @Watch("lineHeight", { immediate: true })
      onHeight(nerVal) {
        this.height = nerVal;
      }
      @Watch("lineFontSize", { immediate: true })
      onFontSize(nerVal) {
        this.fontSize = nerVal;
      }
      @Watch("iconName", { immediate: true })
      onIconName(nerVal) {
        this.icon = nerVal;
      }
    
      created(): void {
        this.odata = this.data;
        this.root = initTree(this.data, this.props, this.defaultExpandAll); // 新树
    
        // 选择移动的元素 事件
        this.$on("node-start", (data, that, ev) => {
          this.startData = data;
          this.dragState.draggingNode = that;
          this.$emit("tree-start", that.data.data, that.data, ev);
        });
    
        // 移动事件
        this.$on("node-over", (data, that, ev) => {
          if (that.$refs.item.id != this.lg1) {
            this.lg2 = this.lg1;
            this.lg1 = that.$refs.item.id;
          }
          const dropNode = findNearestComponent(ev.target, "MyNode"); // 现在的节点
          const oldDropNode = this.dragState.dropNode; // 上一个节点
          if (oldDropNode && oldDropNode !== dropNode) {
            // 判断节点改没改变
            oldDropNode.dropType = "none";
          }
    
          const draggingNode = this.dragState.draggingNode; // 移动的节点
          if (!draggingNode || !dropNode) return;
    
          const dropPrev = true; // 上
          const dropInner = true; // 中
          const dropNext = true; // 下
          ev.dataTransfer.dropEffect = dropInner ? "move" : "none";
          this.dragState.dropNode = dropNode;
    
          const targetPosition = dropNode.$el.getBoundingClientRect();
          const prevPercent = dropPrev
            ? dropInner
              ? 0.25
              : dropNext
              ? 0.45
              : 1
            : -1;
          const nextPercent = dropNext
            ? dropInner
              ? 0.75
              : dropPrev
              ? 0.55
              : 0
            : 1;
          let dropType = "";
    
          const distance = ev.clientY - targetPosition.top;
          if (distance < targetPosition.height * prevPercent) {
            // 在上面
            dropType = "before";
          } else if (distance > targetPosition.height * nextPercent) {
            // 在下面
            dropType = "after";
          } else if (dropInner) {
            dropType = "inner";
          } else {
            dropType = "none";
          }
          if (this.digui(draggingNode.data, dropNode.data._treeId)) {
            dropType = "none";
          }
          dropNode.dropType = dropType;
          this.$emit("tree-over", that.data.data, that.data, ev, dropType);
        });
    
        // 移动结束 事件
        this.$on("node-drop", (data, that, ev) => {
          const sd = JSON.stringify(this.startData.data);
          const ad = JSON.stringify(this.data);
          let ss: string | string[] = ad.split(sd);
          let newData;
          ss = ss.join("");
          if (that.dropType == "none") {
            return;
          }
          if (this.lg2 != null && this.lg1 != this.startData._treeId) {
            // 删除startData
            ss = this.deleteStr(ss);
            const od = JSON.stringify(data.data);
            const a = ss.indexOf(od);
            if (that.dropType == "after") {
              newData = JSON.parse(
                ss.substring(0, a + od.length) +
                  "," +
                  sd +
                  ss.substring(a + od.length)
              );
            } else if (that.dropType == "before") {
              if (a == -1) {
                const s = this.deleteStr(od.split(sd).join(""));
                newData = JSON.parse(
                  ss.substring(0, ss.indexOf(s)) +
                    sd +
                    "," +
                    ss.substring(ss.indexOf(s))
                );
              } else {
                newData = JSON.parse(
                  ss.substring(0, a) + sd + "," + ss.substring(a)
                );
              }
            } else if (that.dropType == "inner") {
              ss = JSON.parse(ss);
              this.oldData(ss, data.data, JSON.parse(sd));
              newData = ss;
            }
            this.root = initTree(newData, this.props, this.defaultExpandAll); // 新树
            const parent: any = this.$parent;
            parent.data = newData;
            this.lg1 = null;
            this.lg2 = null;
          }
    
          this.$emit(
            "tree-drop",
            this.data,
            ev,
            this.startData.id,
            data.id,
            that.dropType,
            this.root
          );
          that.dropType = "none";
        });
      }
    
      /**
       * 修改data,添加输入
       * @param {Array} ss 需要被加入的数据
       * @param {Object} data 落点
       * @param {Object} sd 需要加入的数据
       */
      oldData(ss, data, sd): void {
        for (let i = 0; i < ss.length; i++) {
          if (JSON.stringify(ss[i]) == JSON.stringify(data)) {
            if ("children" in ss[i]) {
              ss[i].children.push(sd);
            } else {
              ss[i].children = [];
              ss[i].children.push(sd);
            }
            break;
          } else if ("children" in ss[i]) {
            this.oldData(ss[i].children, data, sd);
          }
        }
      }
      // 判断拖拽时贴近的是不是自己的子元素
      digui(data, id): boolean {
        if (data.children && data.children.length != 0) {
          for (let i = 0; i < data.children.length; i++) {
            if (data.children[i]._treeId == id) {
              return true;
            }
            const s = this.digui(data.children[i], id);
            if (s == true) {
              return true;
            }
          }
        }
      }
      deleteStr(ss): string {
        if (ss.indexOf(",,") !== -1) {
          ss = ss.split(",,");
          if (ss.length !== 1) {
            ss = ss.join(",");
          }
        } else if (ss.indexOf("[,") !== -1) {
          ss = ss.split("[,");
          if (ss.length !== 1) {
            ss = ss.join("[");
          }
        } else if (ss.indexOf(",]") !== -1) {
          ss = ss.split(",]");
          if (ss.length !== 1) {
            ss = ss.join("]");
          }
        }
        return ss;
      }
    }
    
  • 相关阅读:
    PHP页面之间传递参数的三种方法
    xbox game bar无法打开/安装怎么办?
    java毕业生设计校园闲置物品交换平台系统计算机源码+系统+mysql+调试部署+lw
    Mybatis执行器BatchExecutor、ReuseExecutor、SimpleExecutor介绍
    如何做好一个管理者
    四大特性模块(module)
    探索设计模式:从组合到享元的软件架构之旅 (软件设计师笔记)
    ahooks解决React闭包问题方法示例
    【C#语言】DataGridView删除行
    十大开源机器人 智能体
  • 原文地址:https://blog.csdn.net/qq_38157825/article/details/127123996