使用vue自定義如何實現Tree組件和拖拽功能

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;
  }
}

總結

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: