<template>
  <section class="tree-container">
    <!--    @node-drag-leave="currentConfig.nodeDragLeave"-->
    <!--    @node-drag-over="currentConfig.nodeDragOver"-->
    <!--    @node-drag-end="currentConfig.nodeDragEnd"-->
    <mu-search-input class="treeFilter" v-if="currentConfig.filterable" v-model="filterNode"
                     :config="{name:'treeFilter', placeholder: currentConfig.filterPlaceholder}"></mu-search-input>
    <el-tree class="mu-tree"
             :node-key="currentConfig.nodeKey"
             :data="value"
             :props="currentConfig.props"
             :accordion="currentConfig.accordion"
             :lazy="currentConfig.lazy"
             :load="currentConfig.loadNode"
             :draggable="currentConfig.draggable"
             :allowDrop="currentConfig.allowDrop"
             :allowDrag="currentConfig.allowDrag"
             :show-checkbox="currentConfig.showCheckbox"
             :default-checked-keys="currentConfig.defaultCheckedkeys"
             :default-expand-all="currentConfig.defaultExpandAll"
             :default-expanded-keys="currentConfig.defaultExpandedKeys"
             :check-on-click-node="currentConfig.checkOnClickNode"
             :expand-on-click-node="currentConfig.expandOnClickNode"
             :highlight-current="currentConfig.highlightCurrent"
             :current-node-key="currentConfig.currentNodeKey"
             :filter-node-method="filterTree"
             @node-expand="currentConfig.nodeExpand"
             @node-collapse="currentConfig.nodeCollapse"
             @check-change="handleCheckChange"
             @node-drag-start="currentConfig.nodeDragStart"
             @node-drag-enter="currentConfig.nodeDragEnter"
             @node-drop="currentConfig.nodeDrop"
             @node-click="currentConfig.nodeClick"
             @current-change="handleCurrentChange"
             @node-contextmenu="handleContextMenu"
             :ref="'tree'+ currentConfig.name">
      <div class="mu-tree-node" slot-scope="{ node, data }" :class="{'is-disabled': node.disabled}">
        <span class="title" v-if="!node.isEdit">
          <i :class="data[(currentConfig.props || {}).icon || 'icon'] || currentConfig.icon"></i>
          <span v-if="!$scopedSlots.title">{{ data[currentConfig.props.label]}}</span>
          <slot v-else name='title' :node="node" :data="data"></slot>
        </span>
        <span class="node-edit" v-else>
           <input type="text" class="edit-input" ref="inputEdit" @blur.stop="updateNode(data)"
                  @keyup.enter.stop="updateNode(data)" v-model="data[currentConfig.props.label]"/>
            <i class="icon-close-circle" @click="onHandleEdit('cancel', data, $event)"></i>
        </span>
        <span class="operation" v-if="currentConfig.getOperation && currentConfig.getOperation(node).length">
           <mu-btn :config="{type: 'text', size:'small' , text: item.text, icon:  item.icon}" :key="index"
                   @click="handleOperation(item, data, node, $event)"
                   v-for="(item, index) in currentConfig.getOperation(node)"></mu-btn>
        </span>
      </div>
    </el-tree>
    <section v-if="sideMenuShow" class="side-menu" v-position='{"x":x,"y":y}' v-clickoutside="closeContextMenu">
      <ul>
        <li v-for="(item, index) in activeOption" @click="handleMenuItem(item)" :key="index">
          <i v-if="item.icon" :class="item.icon"></i>{{item.text}}
        </li>
      </ul>
    </section>
  </section>
</template>
<script lang="ts">
  import { Component, Vue, Prop, Watch } from 'vue-property-decorator';
  import { SystemApi } from '../../api';
  // import DtBtn from "@/components/Btn/index.vue";
  import util from "../../common/utils/util";
  // import DtSearchInput from "@/components/Form/Field/SearchInput.vue";

  type TreeConfig = {
    name?: string
    nodeKey: string
    sourceConfigs?: any
    icon?: string
    accordion?: boolean,
    defaultExpandAll?: boolean
    defaultExpandedKeys?: any
    highlightCurrent?: boolean
    currentNodeKey?: string | number,
    props?: any
    showCheckbox?: boolean
    expandOnClickNode?: boolean
    checkOnClickNode?: boolean,
    defaultCheckedkeys?: any
    checkChange?: Function
    operation?: any,
    lazy?: boolean,
    loadNode?: Function,
    filterable?: boolean,
    filterPlaceholder?: string,
    draggable?: boolean,
    allowDrop?: Function,
    allowDrag?: Function,
    nodeDragStart?: Function,
    nodeDragEnter?: Function,
    nodeDragLeave?: Function,
    nodeDragOver?: Function,
    nodeDragEnd?: Function,
    nodeDrop?: Function,
    nodeExpand?: Function,
    nodeCollapse?: Function,
    isCurrentChange?: Function,
    nodeClick?: Function,
    currentChange?: Function,
    getOperation?: Function,
    getContextMenu?: Function
  }

  @Component({
    // components: { DtSearchInput, DtBtn }
  })
  export default class MuTree extends Vue {
    @Prop({ default: () => [] }) private value: any;
    @Prop({ default: () => [] }) private config: TreeConfig;
    @Prop() private extension?: (table: any, value: any) => void;
    defaultValue = JSON.parse(JSON.stringify(this.value || []));
    defaultEditNode: any = {};
    currentSearchParams: any = {};
    isLoading: boolean = false;
    sideMenuShow: boolean = false;
    treeEditCancel: boolean = false;
    currentNode: any = {};
    currentCheckedNode: any = {};
    activeOption: any = [];
    filterNode: string = ""; // 筛选值
    editTree: boolean = false;
    private x: number = 0;
    private y: number = 0;
    private defaultConfig: any = {
      name: util.generateUUID(),
      props: {
        label: "label",
        disabled: "disabled",
        children: "children",
        isLeaf: "isLeaf"
      },
      nodeClick: () => {
      },
      nodeExpand: () => {
      },
      nodeCollapse: () => {
      },
      nodeDrop: () => {
      },
      nodeDragStart: () => {
      },
      nodeDragEnter: () => {
      }
    };

    // 合并默认数据
    get currentConfig() {
      const result = this.config;
      Object.keys(this.defaultConfig).forEach((key: string) => {
        if (this.config[key] === undefined) result[key] = this.defaultConfig[key];
      });
      if (this.config.currentChange && this.config.highlightCurrent === undefined) result.highlightCurrent = true;
      return result;
    }

    @Watch("filterNode")
    onFilterText(val: any, oldVal: any) {
      (this.$refs['tree' + this.currentConfig.name] as any).filter(val);
    }

    mounted() {
      // onHandleData为操作数据， onHandleEdit操作编辑, onHandleRefresh为刷新
      const tree: any = this.$refs[`tree${this.config.name}`];
      this.extension && this.extension(tree, {
        onHandleData: this.onHandleData,
        onHandleRefresh: (callback?: any) => this.onHandleData('load', this.currentSearchParams, callback),
        onHandleEdit: this.onHandleEdit,
        onShowEdit: (data?: any, callback?: any) => {
          if (!data && !this.currentCheckedNode) return;
          this.onHandleEdit('edit', data || this.currentCheckedNode.data, callback);
        },
        getNode: (data) => tree.getNode(data),
        getCheckedNodes: () => tree.getCheckedNodes(),
        setCheckedKeys: (data: any) => tree.setCheckedKeys(data),
        setCheckedNodes: (data: any) => tree.setCheckedKeys(data.map((item: any) => item[this.config.nodeKey])),
        clearChecked: () => tree.setCheckedNodes([]),
        getCurrentKey: () => tree.getCurrentKey(),
        getCurrentNode: () => tree.getCurrentNode(),
        setCurrentKey: (key: string) => tree.setCurrentKey(key),
        setCurrentNode: (node: any) => tree.setCurrentNode(node),
        clearCurrentNode: () => tree.setCurrentNode({}),
        setNodeLevel: this.setNodeLevel,
        closeContextMenu: this.closeContextMenu
      });
    }


    /**
     * 处理表格数据，增删改查
     * @author tanxj
     * @param type load/add/update/delete
     * @param params 数据参数
     * @param callback 回调
     * @param parent 父节点
     */
    onHandleData(type: string, params: any, callback?: Function, parent?: any) {
      const currentSource = (this.config.sourceConfigs || []).find((item: any) => item.type === type);
      switch (type) {
        case "load":
          if (!currentSource) return callback && callback();
          this.currentSearchParams = params;
          return this.handleLoad(currentSource, params, callback);
        case "add":
          return this.handleAdd(currentSource, params, parent, callback);
        case "update":
          if (!params) params = (this.$refs[`tree${this.config.name}`] as any).getCurrentNode();
          return this.handleUpdate(currentSource, params, callback);
        case "delete":
          if (!params) params = (this.$refs[`tree${this.config.name}`] as any).getCurrentNode();
          return this.handleDelete(currentSource, params, callback);
        default:
          return;
      }
    }

    /**
     * 加载数据
     * @author tanxj
     * @param source 数据源配置
     * @param params 加载数据参数
     * @param callback 回调
     */
    handleLoad(source: any, params: any, callback?: Function) {
      this.isLoading = true;
      const { api, onBeforeSubmit, onSuccess, onError } = source;
      const next = (currentParams: any) => {
        const { serviceName, funcName, config } = api;
        SystemApi.handleApi({ serviceName, funcName, params: currentParams }, config).then(async (ret: any) => {
          const result = onSuccess ? onSuccess(ret.data, this) : undefined;
          callback && callback(ret, result);
        }, (error: any) => {
          this.isLoading = false;
          onError && onError(error);
        });
      };
      onBeforeSubmit ? onBeforeSubmit({ ...api.params, ...params }, next, this.value) : next({ ...api.params, ...params });
    }

    /**
     * 新增数据
     * @author tanxj
     * @param source 数据源配置
     * @param data 可批量或者单条
     * @param callback 回调
     */
    handleAdd(source: any, data: any, parent?: any, callback?: Function) {
      const { api, onBeforeSubmit, onSuccess, onError } = (source || {}) as any;
      if (!source) return callback && callback();
      const next = (params: any) => {
        const { serviceName, funcName, config } = api;
        SystemApi.handleApi({ serviceName, funcName, params: params }, config).then((ret: any) => {
          onSuccess && onSuccess(ret.data, parent);
          callback && callback(ret);
          // 重新刷新
          this.onHandleData('load', this.currentSearchParams, () => {
            // 设置选中值
            (this.$refs[`tree${this.config.name}`] as any).setCurrentNode({ ...data, ...ret.data });
          });
        }, (error: any) => {
          onError && onError(error, parent);
        });
      };
      onBeforeSubmit ? onBeforeSubmit({ ...api.params, ...data }, next, parent, this) : next({ ...api.params, ...data });
    }

    /**
     * 修改数据
     * @author tanxj
     * @param source 数据源配置
     * @param data 可批量或者单条
     * @param callback 回调
     */
    handleUpdate(source: any, data: any, callback?: Function) {
      if (!source) {
        this.editTree && this.onHandleEdit('close', data);
        return callback && callback();
      }
      const { api, onBeforeSubmit, onSuccess, onError } = (source || {}) as any;
      const next = (params: any) => {
        const { serviceName, funcName, config } = api;
        SystemApi.handleApi({ serviceName, funcName, params: params }, config).then((ret: any) => {
          onSuccess && onSuccess(ret.data, this);
          this.editTree && this.onHandleEdit('close', data);
          callback && callback(ret);
        }, (error: any) => {
          setTimeout(() => (this.$refs.inputEdit as any).focus());
          onError && onError(error);
        });
      };
      onBeforeSubmit ? onBeforeSubmit({ ...api.params, ...data }, next, this) : next({ ...api.params, ...data });
    }

    /**
     * 删除数据
     * @author tanxj
     * @param source 数据源配置
     * @param data 可批量或者单条
     * @param callback 回调
     */
    handleDelete(source: any, data: any, callback?: Function) {
      const { api, onBeforeSubmit, onSuccess, onError } = source;
      if (!source) return callback && callback();
      const next = (params: any) => {
        const { serviceName, funcName, config } = api;
        SystemApi.handleApi({ serviceName, funcName, params: params }, config).then((ret: any) => {
          onSuccess && onSuccess(ret.data, this);
          callback && callback(ret);
        }, (error: any) => {
          onError && onError(error);
        });
      };
      onBeforeSubmit ? onBeforeSubmit({ ...api.params, ...data }, next, { ...api.params, ...data }) : next({ ...api.params, ...data });
    }

    /**
     * 更新编辑的数据
     * @author tanxj
     * @param data 操作行的数据
     */
    updateNode(data: any) {
      setTimeout(() => {
        if (this.treeEditCancel) return this.treeEditCancel = false;
        this.onHandleData('update', data);
      }, 200)
    }

    /**
     * 操作编辑
     * @author tanxj
     * @param type edit|close
     * @param data 操作行的数据
     * @param event 回调
     */
    onHandleEdit(type: string, data: any, event?: any) {
      this.closeContextMenu();
      switch (type) {
        case "edit": {
          const tree: any = this.$refs[`tree${this.config.name}`];
          const node = tree.getNode(data);
          this.$set(node, 'isEdit', true);
          setTimeout(() => (this.$refs.inputEdit as any).focus());
          this.editTree = true;
          this.defaultEditNode = JSON.parse(JSON.stringify(data));
          return;
        }
        case 'close': {
          const tree: any = this.$refs[`tree${this.config.name}`];
          const node = tree.getNode(data);
          this.$set(node, 'isEdit', false);
          this.editTree = false;
          return;
        }
        case 'cancel': {
          if (event) {
            event.preventDefault();
            event.stopPropagation();
          }
          this.treeEditCancel = true;
          data[this.currentConfig.props.label] = this.defaultEditNode[this.currentConfig.props.label];
          const tree: any = this.$refs[`tree${this.config.name}`];
          const node = tree.getNode(data);
          this.$set(node, 'isEdit', false);
          this.editTree = false;
          return;
        }
        default:
          return;
      }
    }

    /**
     * 过滤tree的数据
     * @param value 节点名称
     * @param data 当前节点
     */
    filterTree(value: string, data: any) {
      if (!value) return true;
      return data[this.currentConfig.props.label].indexOf(value) !== -1;
    }

    /**
     * 右击菜单事件
     * @param event 事件节点
     * @param data 该节点所对应的对象
     * @param 节点对应的 Node
     */
    handleContextMenu(event: any, data: any, node: any, el: any) {
      if (this.editTree || !this.config.getContextMenu) return;
      event.preventDefault();
      event.stopPropagation();
      this.sideMenuShow = false;
      this.y = event.clientY;
      this.x = event.clientX;
      this.currentNode.checked = false;
      node.checked = true;
      this.activeOption = this.config.getContextMenu(data, node) || [];
      this.currentNode = node;
      setTimeout(() => this.sideMenuShow = true);
    }

    /**
     * 设置层级
     * @author tanxj
     * @param level {number} 1为提高层级，-1为降低层级
     * @param callback 回调
     */
    setNodeLevel(level: number = 1, callback?: any) {
      const tree: any = this.$refs[`tree${this.config.name}`];
      const currentData = tree.getCurrentNode();
      if (!currentData) return;
      const node = tree.getNode(currentData);
      if (node.parent) {
        const cIndex = node.parent.childNodes.findIndex((item: any) => {
          return item.data[this.config.nodeKey] === currentData[this.config.nodeKey];
        });
        if (((cIndex - level) > -1) && ((cIndex - level) <= (node.parent.childNodes.length - 1))) {
          node.parent.childNodes =
            util.swapArray(node.parent.childNodes, cIndex, cIndex - level);
        }
        if (callback) callback(node, cIndex);
      }
    }


    /**
     * 选中操作
     * @author tanxj
     * @param data 节点数据
     * @param node 节点对象
     */
    handleCurrentChange(data: any, node: any) {
      this.closeContextMenu();
      if (node.disabled || (this.currentCheckedNode && this.config.isCurrentChange &&
        !this.config.isCurrentChange(data, this.currentCheckedNode))) {
        const tree: any = this.$refs[`tree${this.config.name}`];
        return (this.currentCheckedNode.data)
          ? tree.setCurrentKey(this.currentCheckedNode.data[this.currentConfig.nodeKey])
          : tree.setCurrentKey(null);
      }
      this.config.currentChange && this.config.currentChange(data, node);
      this.currentCheckedNode = node;
    }

    /**
     * 改变选中
     * @author tanxj
     * @param data 节点数据
     * @param checked 是否选中
     */
    handleCheckChange(data: any, checked: any) {
      this.config.checkChange && this.config.checkChange(checked, data);
    }

    /**
     * 关闭选中事件
     * @author tanxj
     * @param event 事件
     */
    closeContextMenu(event?: any) {
      if (event && event.which === 3) return;
      if (this.sideMenuShow) this.sideMenuShow = false;
      if (this.currentNode.checked) this.currentNode.checked = false;
    }

    /**
     * 关闭选中事件
     * @author tanxj
     * @param btnItem 按钮数据
     * @param data 节点数据
     * @param node 节点对象
     * @param event 事件
     */
    handleOperation(btnItem: any, data: any, node: any, event: any) {
      event.preventDefault();
      event.stopPropagation();
      btnItem.onClick(data, node);
    }

    /**
     * 操作用户操出框事件item
     * @author tanxj
     * @param menuItem 操作数据对象
     */
    handleMenuItem(menuItem: any) {
      this.closeContextMenu();
      menuItem.onClick && menuItem.onClick(this.currentNode.data, this.currentNode);
    }

  }
</script>

<style lang="scss">
  .tree-container {
    .treeFilter {
      margin-bottom: 10px;
    }

    .node-edit {
      width: 100%;

      .edit-input {
        height: 18px;
        line-height: 22px;
        border: none;
        border-bottom: 1px solid #666;
        background: transparent;
        outline: none;
        width: calc(100% - 24px);


      }

      .icon-close-circle {
        color: #333;
        cursor: pointer;
        padding-left: 8px;
      }
    }

    .mu-tree-node.is-disabled {
      color: rgba(41, 41, 41, 0.4);
      cursor: not-allowed;
    }

    .side-menu {
      position: fixed;
      background: #ffffff;
      border-radius: 2px;
      min-width: 84px;
      z-index: 1;
      box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.3);

      li {
        line-height: 28px;
        height: 28px;
        padding: 0 12px;
        font-size: 12px;
        font-weight: 400;
        color: #666;
        cursor: pointer;
        border-bottom: 1px solid #666;

        &:last-child {
          border-bottom: none;
        }

        &:hover {
          background-color: #666;
        }
      }
    }
  }
</style>
