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

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

实现功能:树结构、右键菜单、拖拽

效果图

使用vue自定义如何实现Tree组件和拖拽功能_第1张图片

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






/components/drag-tree/index.vue






使用:Test.vue




vue2 + ts 版

只有两个组件的ts部分文件不一样,其他一样

/components/drag-tree/node.vue






/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






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

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

你可能感兴趣的:(使用vue自定义如何实现Tree组件和拖拽功能)