SOURCE

<template>
  <div class="tree-catalog-page" :class="attr.class">
    <BaseCard :class="{ 'is-hidden': collapse }" padding="0" :border="false" :title="title">
      <el-tree
        :key="getData.length"
        ref="treeRef"
        v-bind="props"
        :data="getData"
        :indent="24"
        :node-key="getNodeKey"
        :current-node-key="activeNode[getNodeKey] || ''"
        :default-expanded-keys="defaultExpandedKeys"
        :props="getProps"
        title=""
        @node-click="handleCurrentChange"
      >
        <template #default="{ node, data }">
          <span class="custom-tree-node">
            <BaseToolTip placement="top" :rows="1" :content="node.label">{{
              node.label
            }}</BaseToolTip>
          </span>
        </template>
      </el-tree>
    </BaseCard>
    <div class="catalog-according" :class="{ 'is-active': collapse }">
      <i @click="collapse = !collapse" :title="collapse ? '展开' : '收起'"></i>
    </div>
    <div><slot></slot></div>
  </div>
</template>
<script setup>
import interfaceUrls from '@/api/interfaceUrl';
import request from '@/api';
import { toRefs, ref, computed, nextTick, useAttrs } from 'vue';

defineOptions({
  name: 'TreeCategory',
  inheritAttrs: false,
});

const props = defineProps({
  ...ElTree.props,
  catalogId: {
    type: String,
  },
  title: {
    type: String,
  },
});
const emits = defineEmits([...ElTree.emits]);
const attr = useAttrs();

const { data, nodeKey, props: prop, catalogId } = toRefs(props);
const treeRef = ref(null);
const collapse = ref(false);
const defaultData = ref([]);
// 默认展开项
const defaultExpandedKeys = ref([]);
// 当前被选中项
const activeNode = ref({});

const getNodeKey = computed(() => nodeKey.value || 'catalogId');
const getData = computed(() => (data && data.length ? data.value : defaultData.value));

const getActiveNodeData = val =>
  activeNode.value?.[getNodeKey.value] &&
  activeNode.value?.[getNodeKey.value] === val[getNodeKey.value];

// 自定义数节点样式类
const customNodeClass = (val, node) => {
  let str = '';
  if (!(val.children && val.children.length)) {
    str = str + ' is-leaf';
  }
  if (getActiveNodeData(val)) {
    str = str + ' is-active';
  }
  return str;
};

const getProps = computed(() => {
  return {
    ...toRefs(prop.value),
    label: 'catalogName',
    children: 'children',
    isLeaf: 'isLeaf',
    class: customNodeClass,
  };
});

// 重置高亮选择
const resetActiveNode = val => {
  activeNode.value = {};
  defaultExpandedKeys.value = defaultExpandedKeys.value.filter(
    item => item !== val[getNodeKey.value]
  );
  emits('current-change', { level: 0, isLeaf: false });
};

// 当前选中节点变化时回调事件
const handleCurrentChange = (val, node) => {
  if (getActiveNodeData(val)) {
    resetActiveNode(val);
    return;
  }
  activeNode.value = val;
  if (node?.expanded) {
    if (!defaultExpandedKeys.value.includes(val[getNodeKey.value]) && val?.children?.length) {
      defaultExpandedKeys.value.push(val[getNodeKey.value]);
    }
  } else {
    defaultExpandedKeys.value = defaultExpandedKeys.value.filter(
      item => item !== val[getNodeKey.value]
    );
  }
  emits('current-change', { ...val, level: node.level, isLeaf: node.isLeaf });
  nextTick(() => {
    treeRef.value.$el
      .querySelector('.el-tree-node.is-current.is-active')
      .scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
  });
  return;
};

const initTree = async () => {
  defaultData.value = [];
  try {
    let res = await request({
      method: 'post',
      url: interfaceUrls.assetMgmt.assetCatalogMgmt.queryAssetDomain,
      data: catalogId.value ? { catalogId: catalogId.value } : {},
    });
    defaultData.value = res.datas;
  } catch (error) {}
};

//  根据key更新高亮并自动触发选中回调
const updateCurrentKey = (key, reset = false) => {
  const node = treeRef.value.getNode(key);
  if (node) {
    treeRef.value.setCurrentKey(key);
    activeNode.value = node.data;
    emits('current-change', { ...node.data, level: node.level, isLeaf: node.isLeaf });
  } else {
    if (reset) {
      resetActiveNode(node.data);
    }
  }
};

// 更新整个树,同时如果当前选中节点还存在,则触发自动选中,否则,根据传入的key选中
const updateTree = async (activeNodeKey = '') => {
  await initTree();
  await nextTick();
  const activeNodeId = activeNode.value[getNodeKey.value];
  const node = treeRef.value.getNode(activeNodeId);
  if (node) {
    updateCurrentKey(activeNodeId);
  } else {
    if (activeNodeKey) {
      updateCurrentKey(activeNodeKey, true);
    }
  }
};
initTree();
defineExpose({
  treeRef,
  updateTree,
});
</script>
<style lang="scss">
.tree-catalog-page {
  display: flex;
  & > .base-card:first-child {
    width: 280px;
    position: sticky;
    top: 20px;
    height: calc(100vh - 80px);
    .base-card-header {
      border: 1px solid #eeeeee;
    }
  }
  & > div:last-child {
    .base-form.is-search-form .el-form > .base-card.is-collapse.is-active > .base-card-content {
      padding: 20px 0 !important;
      .sc-button {
        margin-left: 24px;
      }
    }
    & > .base-card.is-custom-table-list {
      & > .base-card-header {
        position: relative;
        p {
          text-indent: 16px;
          span {
            color: var(--el-color-primary);
          }
        }
        .is-custom-table-list-keyword {
          width: 260px;
          position: absolute;
          right: 20px;
          top: 7px;
        }
      }
      & > .base-card-content .el-pagination {
        justify-content: flex-end;
        padding: 20px 0 0;
      }
    }
  }
  .catalog-according {
    width: 41px;
    position: sticky;
    top: 20px;
    height: calc(100vh - 80px);
    &::before {
      content: '';
      position: absolute;
      width: 1px;
      top: -20px;
      left: 20px;
      bottom: -20px;
      background: #eee;
    }
    i {
      position: absolute;
      left: 20px;
      top: 50%;
      transform: translateY(-50%);
      width: 14px;
      height: 78px;
      border: 1px solid #eee;
      border-left: none;
      cursor: pointer;
    }
    &::after {
      pointer-events: none;
      content: '';
      position: absolute;
      left: 20px;
      top: 50%;
      transform: translateY(-5px);
      width: 0;
      height: 0;
      border: 5px solid transparent;
      border-right-color: #999999;
    }
    &.is-active {
      width: 21px;
      &::before {
        left: 0;
      }
      i {
        left: 0;
      }
      &::after {
        left: 5px;
        border-right-color: transparent;
        border-left-color: #999999;
      }
    }
  }
  & > div:last-child {
    flex: 1;
  }
}

.el-tree {
  padding: 20px 0 0;
  max-height: calc(100vh - 125px);
  overflow-y: auto;
  .el-tree-node__content:hover {
    background-color: #e7eff9;
  }
  .el-tree-node:focus {
    > .el-tree-node__content {
      background-color: #fff;
    }
  }
  .el-tree-node {
    &.is-current.is-active > .el-tree-node__content,
    &.is-leaf.is-active > .el-tree-node__content {
      color: #2367f1;
      background-color: #e7eff9;
    }
  }
  .el-icon.el-tree-node__expand-icon {
    width: 16px;
    height: 16px;
    margin: 0 8px;
    color: transparent;
    transform: rotate(180deg);
    background: url(@/assets/images/icon-tree-accordion.png) center center no-repeat;
    &.expanded {
      transform: rotate(0);
    }
    &.is-leaf {
      visibility: visible;
      transform: rotate(0);
      background: url(@/assets/images/icon-tree-title.png) center center no-repeat;
    }
  }
  .custom-tree-node {
    flex: 1;
    line-height: 32px;
    align-items: center;
    display: flex;
    overflow: hidden;
    white-space: normal;

    & > span {
      &:nth-child(1):not(:last-child) {
        display: inline-block;
        width: calc(100% - 36px);
        vertical-align: middle;
      }
      &:nth-child(2) {
        width: 20px;
        height: 20px;
        display: inline-block;
        padding: 0 8px 0 8px;
        vertical-align: middle;
        .el-dropdown {
          display: none;
          width: 20px;
          height: 20px;
        }
      }
    }
  }
  & > .el-tree-node {
    & > .el-tree-node__content {
      .custom-tree-node {
        & > span {
          &:nth-child(1) {
            font-weight: 700;
          }
        }
      }
    }
  }
  .el-tree-node__content {
    height: 32px;
    font-size: 14px;
    color: #333333;
    display: flex;
    align-items: center;
    cursor: pointer;
  }
  .el-tree__empty-block {
    text-align: center;
    font-size: 14px;
    color: #999;
  }
}
</style>
console 命令行工具 X clear

                    
>
console