index.vue 10.6 KB
<script lang="ts" setup name="Tree">
import { ref, computed } from "vue";
import { ElTree } from "element-plus";
import { Search, Delete, MoreFilled } from "@element-plus/icons-vue";
import { debounce } from 'lodash-es'

const props = defineProps({
  treeInfo: {
    type: Object,
    default: {},
  },
});

const emits = defineEmits(["onQueryChanged", "nodeClick", "loadNode", "nodeCheck", "nodeCheckChange", 'nodeSelectChange', 'itemMenuClick', 'filterTree']);

const queryValue = ref('');
const queryPlaceholder = computed(() => {
  return props.treeInfo.filter ? props.treeInfo.queryPlaceholder : "";
});
const treeLoading = computed(() => {
  return props.treeInfo.loading ?? false;
});
const treeProps: any = computed(() => {
  return props.treeInfo.props ?? {};
});
const showCheckbox = computed(() => {
  return props.treeInfo.showCheckbox ?? false;
});
const checkStrictly = computed(() => {
  return props.treeInfo.checkStrictly ?? false;
});
const expandOnNodeClick = computed(() => {
  return props.treeInfo.expandOnNodeClick ?? true;
});
const nodeKey = computed(() => {
  return props.treeInfo.nodeKey ?? '';
});
const expandedKey = computed(() => {
  return props.treeInfo.expandedKey ?? [];
});
const checkKeys = computed(() => {
  return props.treeInfo.checkedKey ?? []
});
const currentNodeKey = computed(() => {
  // if (props.treeInfo.currentNodeKey) {
  //   nextTick(() => {
  //     let domItems = treeRef.value?.$el.getElementsByClassName('el-tree-node');
  //     let clientHeight = treeRef.value?.$el.clientHeight;
  //     for (const item of domItems) {
  //       if (item.getAttribute('data-key') == props.treeInfo.currentNodeKey) {
  //         if (item.offsetTop > clientHeight) {
  //           item.scrollIntoView({ block: "end", inline: "nearest" });
  //         }
  //         break;
  //       }
  //     }
  //   })
  // }
  return props.treeInfo.currentNodeKey ?? ''
});
const customInfo = computed(() => {
  return props.treeInfo.custom ?? null
})
const prefixInfo = computed(() => {
  return props.treeInfo.prefix ?? null
})
const editTreeItem = computed(() => {
  return props.treeInfo.editTreeItem ?? false;
});
const expendAll = computed(() => {
  return props.treeInfo.expendAll ?? false
})

interface Tree {
  [key: string]: any;
}
const treeRef: any = ref();
const treeData = computed(() => {
  return props.treeInfo.data;
});
const checkedNodes = ref([])
const checkedKeys = ref([])

const typeValue = (row, type) => {
  let tag = ''
  if (type == 'menuType') {
    switch (row[type]) {
      case "M":
        tag = '目录'
        break;
      case "C":
        tag = '菜单'
        break;
      case "P":
        tag = '页面'
        break;
      case "F":
        tag = '按钮'
        break;
      default:
        tag = '--'
        break;
    }
  }
  return tag;
}

const onQueryChanged = (query: string) => {
  const render = debounce(() => {
    if (props.treeInfo.blurValue === true) {
      return;
    }
    if ( props.treeInfo.customFilter === true) {
      emits("filterTree", query?.trim());
    } else {
      treeRef.value!.filter(query?.trim());
    }
  }, 300)
  render();
};

const queryInputChange = (val) => {
  if (props.treeInfo.customFilter === true && props.treeInfo.blurValue === true) { //失去焦点值改变后再触发
    emits("filterTree", val?.trim());
  }
}

const filterNode = (value: string, data: Tree, node) => {
  if (!value) return true;
  return data[treeProps.value.label].includes(value);
};
const nodeClick = (nodeObj, node, treeNode, event) => {
  emits("nodeClick", node.data, node);
};

const loadNode = (node, resolve) => {
  emits("loadNode", node, resolve);
}

const nodeCheckChange = (node, checked, checkedChildren) => {
  emits("nodeCheckChange", node, checked, checkedChildren);
}

const handleTreeSelectChange = (nodeObj, node, treeNode, event) => {
  emits("nodeSelectChange", node);
}

const nodeCheck = (nodeObj, checkedObj) => {
  checkedNodes.value = checkedObj.checkedNodes
  checkedKeys.value = checkedObj.checkedKeys
  emits('nodeCheck', checkedObj, props.treeInfo.id)
}

const handleItemMenuClick = (node, type) => {
  emits('itemMenuClick', node, type);
}

const getCheckedNodes = () => {
  const checkedNodes = treeRef.value!.getCheckedNodes(false, true)
  return checkedNodes;
}
const getCheckedKeys = () => {
  const checkedKeys = treeRef.value!.getCheckedKeys(false)
  return checkedKeys;
}

const setCheckedKeys = (keys, leafOnly) => {
  treeRef.value!.setCheckedKeys(keys, leafOnly ?? checkStrictly.value)
}

const setCurrentKey = (key) => {
  treeRef.value!.setCurrentKey(key, true);
}

const resetChecked = () => {
  treeRef.value!.setCheckedKeys([], false)
}

const appendChildren = (key, data) => {
  treeRef.value!.updateKeyChildren(key, data)
}

const visibleNodes = ref({}); //解决点击树item,下拉菜单不消失的问题,原因是树的点击事件阻止冒泡了。

defineExpose({
  getCheckedNodes,
  getCheckedKeys,
  setCheckedKeys,
  resetChecked,
  setCurrentKey,
  appendChildren,
  treeRef
});
</script>

<template>
  <div class="tree_panel" :id="props.treeInfo.id">
    <el-input class="tree_search_input" v-model.trim="queryValue" :placeholder="queryPlaceholder" :prefix-icon="Search"
      @input="onQueryChanged" v-if="props.treeInfo.filter" @change="queryInputChange" clearable />
    <el-tree ref="treeRef" class="filter-tree" :class="[props.treeInfo.className]" :data="treeData" :props="treeProps"
      v-loading="treeLoading" :show-checkbox="showCheckbox" :check-strictly="checkStrictly"
      :expand-on-click-node="expandOnNodeClick" :node-key="nodeKey" :default-expand-all="expendAll"
      :default-checked-keys="checkKeys" :default-expanded-keys="expandedKey" :current-node-key="currentNodeKey"
      :filter-node-method="filterNode" :highlight-current="props.treeInfo.highlight ?? true" :lazy="props.treeInfo.lazy"
      :load="loadNode" @node-click="nodeClick" @check-change="nodeCheckChange" @current-change="handleTreeSelectChange" @check="nodeCheck">
      <template #default="{ node, data }">
        <span v-if="customInfo" class="custom-tree-node">
          <span v-if="customInfo.type == 'prefixIcon'" class="custom_icon">
            <el-icon v-if="node.expanded">
              <svg-icon name="file-open" />
            </el-icon>
            <el-icon v-else-if="!node.expanded">
              <svg-icon name="file-closed" />
            </el-icon>
          </span>
          <span>{{ node.label }}</span>
          <span>
            <span v-if="customInfo.type == 'icon'" class="custom_icon">
              <el-icon v-if="customInfo.icon == 'Delete'">
                <Delete />
              </el-icon>
            </span>
            <span v-else class="desc_text">{{ typeValue(data, 'menuType') }}</span>
          </span>
        </span>
        <template v-else-if="editTreeItem && data.showEdit !== false">
          <span class="list-item-text">
            <ellipsis-tooltip v-if="!customInfo" :content="node.label" class-name="w100f"
              :refName="'tooltipOver' + node.id"></ellipsis-tooltip>
          </span>
          <div class="tags-list-right" :class="visibleNodes[node.data.guid] ? 'active' : ''">
            <el-popover v-model:visible="visibleNodes[node.data.guid]" placement="bottom" width="96" trigger="click"
              popper-class="tree-item-edit-menu" :show-arrow="false" :hide-after="0">
              <template #reference>
                <el-icon class="list-more" color="#666">
                  <MoreFilled />
                </el-icon>
              </template>
              <div class="levitation-ul">
                <span class="levitation-li" @click="handleItemMenuClick(node, 'edit')">编辑</span>
                <span class="levitation-li" @click="handleItemMenuClick(node, 'delete')">删除</span>
              </div>
            </el-popover>
          </div>
        </template>

        <template v-else-if="prefixInfo">
          <span class="prefix-icon" style="margin-right: 4px">
            <template v-if="data.type < 2">
              <el-icon v-if="node.expanded">
                <svg-icon name="file-open" />
              </el-icon>
              <el-icon v-else-if="!node.expanded">
                <svg-icon name="file-closed" />
              </el-icon>
            </template>
            <el-icon v-else="data.type == 2">
              <svg-icon name="icon-table" />
            </el-icon>
          </span>
          <ellipsis-tooltip :content="node.label" class-name="w100f mr8-i"
            :refName="'tooltipOver' + node.id"></ellipsis-tooltip>
        </template>
        <ellipsis-tooltip v-else :content="node.label" class-name="w100f mr8-i"
          :refName="'tooltipOver' + node.id"></ellipsis-tooltip>
      </template>
    </el-tree>
  </div>
</template>

<style lang="scss" scoped>
.tree_panel {
  padding: 8px 0;

  .el-input {
    margin: 0 8px 8px;
    width: calc(100% - 16px);
  }

  :deep(.el-tree) {
    height: calc(100% - 38px);
    font-size: 14px;
    overflow: hidden;

    .el-tree-node {
      &.is-checked {
        background-color: #EBF6F7;

        .desc_text {
          color: var(--el-color-regular) !important;
        }
      }
    }

    .el-tree-node__content {
      height: 32px;

      .list-item-text {
        width: calc(100% - 22px);
        display: inline-block;
      }

      .tags-list-right {
        opacity: 0;

        &.active {
          opacity: 1;
        }
      }

      &:hover>.tags-list-right {
        opacity: 1;
      }

      .prefix-icon {

        .el-icon {
          width: 20px;
          height: 20px;
          svg {
            width: 16px;
            height: 16px;
          }
        }
      }

      .custom-tree-node {
        flex: 1;
        display: flex;
        align-items: center;
        justify-content: space-between;
        font-size: 14px;
        // padding-right: 8px;

        .desc_text {
          color: #b2b2b2;
        }
      }

      .custom_icon {
        display: none;

        .el-icon {
          color: var(--el-text-color-regular);
        }
      }

      &:hover {
        .custom_icon {
          display: block;
        }
      }
    }

    &.custom-check {
      .el-checkbox {
        display: none;
      }

      .el-tree-node {
        &.is-selectable {
          .el-checkbox {
            display: inline-flex;
          }
        }
      }
    }

    &.tree-list {
      .is-leaf {
        width: 0;
        padding-left: 0;
      }
    }

    /** 左侧树列表型管理,可编辑删除。 */
    &.tree-list-manage {
      .is-leaf {
        width: 0;
        padding: 0px 0px 0px 8px;
      }

      .el-tree-node__content {
        .list-item-text {
          width: calc(100% - 32px);
        }
      }
    }
  }
}
</style>