vue3 ui框架naiveui线形树实现

先看效果

框架内连线效果 最后一级没有转角 除了最后一级其他子节点没有转角线

原因:naiveui 树的渲染都是以dom渲染没有子节点元素包裹。而且没有兄弟节点的最后一级不会渲染show-line子dom 所以我做的处理 在原结构上面包裹一层,最后一个元素里面添加一个空节点,同级添加一个空元素。空元素默认给disabled: true, isLeaf: true, children: []标识用于css判断

// 默认空tree节点
const defaultTreeNode = { [props.keys]: "", [props.label]: "", disabled: true, isLeaf: true, children: [] };

//  树重组数据
const treeData = computed(() => {
  const treeArray = Array.isArray(props.data) ? recursiveArray(cloneDeep(props.data), props.children) : cloneDeep(props.data);
  if (treeArray.length) {
    treeArray[treeArray.length - 1][props.children].push(defaultTreeNode);
    treeArray.push(defaultTreeNode)
  }
  return treeArray;
})

效果如下:

每一块转角线的css和隐藏最后一个占位元素css

 .n-tree-node-wrapper {
          .n-tree-node-switcher__icon{
            color: $--theme-color !important;
          }
          &:has(.n-tree-node-switcher) {
            .n-tree-node-indent {
              display: block;
              &.n-tree-node-indent--show-line {
                &::after {
                  position: absolute;
                  content: "";
                  left: calc(50% + 0.5px);
                  right: 0;
                  bottom: 50%;
                  transition: border-color .3s var(--n-bezier);
                  border-bottom: 1px dashed $--theme-color !important;
                }
              }
            }
          }
          &:has(.n-tree-node--disabled):has(.n-tree-node-indent--is-leaf) {
            display: none;
          } 
}

效果如下:

所有非最后一级的转角css

.n-tree-node {
            .n-tree-node-indent {
              // 以下正常
              &.n-tree-node-indent--show-line {
                &::before {
                  transform: unset !important;
                  border-left: 1px dashed $--theme-color;
                }
                &::after {
                  border-bottom: 1px dashed $--theme-color !important;
                }
              }
              
            }
          }

最终组件代码

<template>
  <div class="line_tree">
    <slot name="title">
      <div class="line_tree-title" v-if="title">
        <span>{{ title }}</span>
      </div>
    </slot>
    <div class="line_tree-content scroll-y">
      <div class="tree-filter" v-if="filter">
        <n-input :placeholder="'请输入关键字'" v-model:value="treePattern">
          <template #suffix>
            <n-icon :component="SearchOutIcon" />
          </template>
        </n-input>
      </div>
      <n-tree
        class="line-tree" show-line cascade expand-on-click 
        :data="treeData" :children-field="children" :label-field="label"
        :pattern="treePattern" :key-field="keys" :last-label="false"
        :default-expanded-keys="defaultExpanded" :render-switcher-icon="renderSwitcherIcon"
        v-bind="$attrs" />
    </div>
  </div>
</template>

<script lang="ts" setup>
import { reactive, h, ref, watch, computed, onMounted } from 'vue'
import { NEllipsis, NIcon } from 'naive-ui'
import type { TreeOption } from 'naive-ui'
import { getAssetsFile } from '@/packages';
import { icon } from '@/plugins';
import cloneDeep from 'lodash/cloneDeep';

const { SearchOutIcon } = icon.ionicons5;
const { PlusSquareOutIcon, MinusSquareOutIcon } = icon.antd;

const props = defineProps({
  title: {
    type: String,
    default: () => ('')
  },
  data: {
    //  树数据
    type: Array,
    default: () => []
  } as any,
  keys: {
    //  节点key
    type: String,
    default: () => 'key'
  },
  label: {
    //  节点label字段
    type: String,
    default: () => 'label'
  },
  children: {
    //  子节点 字段
    type: String,
    default: () => 'children'
  },
  filter: {
    //  是否允许搜索
    type: Boolean,
    default: () => false
  },
  lastLabel: {
    //  是否最后一级显示省略号
    type: Boolean,
    default: () => true
  }
});
// 筛选的值
const treePattern = ref('');
// 默认空tree节点
const defaultTreeNode = { [props.keys]: "", [props.label]: "", disabled: true, isLeaf: true, children: [] };

//  树重组数据
const treeData = computed(() => {
  const treeArray = Array.isArray(props.data) ? recursiveArray(cloneDeep(props.data), props.children) : cloneDeep(props.data);
  if (treeArray.length) {
    treeArray[treeArray.length - 1][props.children].push(defaultTreeNode);
    treeArray.push(defaultTreeNode)
  }
  return treeArray;
})

// 默认展开第一层
const defaultExpanded: any = computed(() => {
  if (props.data.length && props.data[0]) {
    if (props.data[0][props.children] && props.data[0][props.children].length) {
      return [props.data[0][props.keys]]
    }
    return []
  }
  return []
});
/**
 * 递归数组
 * @param array 数组
 * @param key 判断children key
 */
function recursiveArray(array: any[], key: string): any {
  array.map((item: any) => {
    return {
      ...item,
      [props.children]: item[key] && item[key].length ? recursiveArray(item[key], key) : null
    }
  });
  // array.push({[props.keys]: "",[props.label]: "", disabled: true,isLeaf: true, children: []})
  return array
}
/**
 * 渲染线的switch图标
 */
function renderSwitcherIcon(data: { option: TreeOption, expanded: boolean, selected: boolean }) {
  const { expanded } = data
  return h(NIcon, {
    component: expanded ? MinusSquareOutIcon : PlusSquareOutIcon,
    style: {
      transform: expanded ? 'rotate(-90deg)' : 'rotate(0)'
    }
  })
}
</script>

<style lang="scss" scoped>
.line_tree {
  width: 100%;
  height: 100%;
  padding: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;

  &.unrem {}

  &-title {
    height: 39px;
    line-height: 39px;
    width: 100%;
    font-size: 16px;
    font-weight: 500;
    text-align: center;
    color: #fff;
    @include fetch-bg-color('active-color');
  }

  &-content {
    flex: 1;
    position: relative;

    * {
      user-select: none;
    }

    :deep(.n-tree) {
      &.line-tree {
        --n-line-offset-top: -16px !important;
        --n-line-offset-bottom: -18px !important;
        --n-node-wrapper-padding: 6px 0 !important;
        .n-tree-node-wrapper {
          .n-tree-node-switcher__icon{
            color: $--theme-color !important;
          }
          &:has(.n-tree-node-switcher) {
            .n-tree-node-indent {
              display: block;
              &.n-tree-node-indent--show-line {
                &::after {
                  position: absolute;
                  content: "";
                  left: calc(50% + 0.5px);
                  right: 0;
                  bottom: 50%;
                  transition: border-color .3s var(--n-bezier);
                  border-bottom: 1px dashed $--theme-color !important;
                }
              }
            }
          }
          &:has(.n-tree-node--disabled):has(.n-tree-node-indent--is-leaf) {
            display: none;
          } 
          &:has(.n-tree-node-indent--show-line) {
            &+.n-tree-node-wrapper :has(.n-tree-node-indent--show-line:nth-child(2)){
              .n-tree-node-indent--show-line:first-child::after{
                display: none;
              }
            }
          }
          .n-tree-node {
            .n-tree-node-indent {
              // 以下正常
              &.n-tree-node-indent--show-line {
                &::before {
                  transform: unset !important;
                  border-left: 1px dashed $--theme-color;
                }
                &::after {
                  border-bottom: 1px dashed $--theme-color !important;
                }
              }
              
            }
          }
        }
        .n-tree-node-content {
          padding: 0 6px 0 0;
        }
        // .n-tree-node-indent--show-line
      }
    }

    .tree-filter {
      position: sticky;
      top: 0;
      width: 100%;
      min-height: 30px;
      margin-bottom: 12px;
      z-index: 9;
      @include fetch-bg-color('background-color');
    }
  }
}
</style>

组件调用截图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值