Vue3+Scss实现横向树形结构

2 篇文章 0 订阅

效果:

 项目结构:

用的Vue-Cli的脚手架,配置使用Vue3+Scss

 

代码:

App.vue

<template>
  <div class="container">
    <tree-node :node-info='nodeInfo' v-for='(nodeInfo,index) in dataList' :key='index'
               :expand-status='true' level="0"
               ></tree-node>
  </div>
</template>

<script>
import {defineComponent, ref} from "vue";
import TreeNode from "./components/TreeNode";


export default defineComponent({
  name:"app",
  components: {
    'tree-node':TreeNode
  },
  setup() {
    // 数据源
    let dataList = ref([
      {
        id: 0,
        name: '总公司',
        children: [
          {
            id: 1,
            name: '公司1',
            children: []
          },
          {
            id: 2,
            name: '公司2',
            children: [
              {
                id: 4,
                name: '公司2-1',
                children: []
              },
              {
                id: 5,
                name: '公司2-2',
                children: []
              }
            ]
          },
          {
            id: 3,
            name: '公司3',
            children: []
          }
        ]
      }
    ]);
    return {
      dataList
    };
  }
})
</script>

<style lang="scss">
*{
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
html, body,#app, .container {
  width: 100%;
  height: 100%;
}
.container{
  display: flex;
  justify-content: center;
  align-items: center;
  background: #f5f5f5;
}
</style>

components/TreeNode.vue

<template>
  <div class='tree-node' :style="{'width':nodeWidth}">
    <!-- 上方链接线  -->
    <div class='line-box' v-if='!isRoot'>
      <div class='line-connect' v-if='isHaveBrother'></div>
      <div class='line-to-node' :class='direction'></div>
    </div>
    <!-- 内容  -->
    <div class='content' :class="{'root':isRoot}">
      <span>{{ nodeInfo.name }}-{{nodeInfo.id}}</span>
    </div>
    <!-- 下方连接线  -->
    <div class='bottom-line-box' v-if='nodeInfo.children.length>0' :class="{'isExpand':isExpand}">
      <div class='control-btn' :class="{'close':!isExpand}" @click='changeExpandStatus(nodeInfo)'>
        <div>{{isExpand?'-':'+'}}</div>
      </div>
    </div>
    <!-- 子节点  -->
    <div class='children' :class="{'close':!isExpand}">
      <TreeNode v-for='(childInfo,index) in nodeInfo.children' :key='index' :node-info='childInfo'
                    :direction='getChildDirection(index)'
                    :is-have-brother='(nodeInfo.children.length>1&&index!=0&&index!=(nodeInfo.children.length-1))'
                    :level='myLevel'
                    :expand-status='isExpand'
                    @changeExpandChildCount='handleChildCountChange'></TreeNode>
    </div>
  </div>
</template>

<script>
import {defineComponent, ref, computed, watch, onMounted} from "vue";

export default defineComponent({
  name:'TreeNode',
  props: [
    // 节点信息
    'nodeInfo',
    // 展开状态
    'expandStatus',
    // 方向 left center right
    'direction',
    // 是否拥有兄弟节点
    'isHaveBrother',
    // 层级
    'level'
  ],
  emits:[
    //提交整理后的子节点数量
    'changeExpandChildCount'
  ],
  setup(props, context) {
    // 当前层级
    const myLevel = computed(() => {
      return props.level + 1;
    })
    // 是否是根节点
    const isRoot = computed(() => {
      return myLevel.value == 1;
    })
    // 当前节点宽度
    const nodeWidth = computed(() => {
      let width = '192px';
      if (isExpand.value) {
        let sumWidth = childExpandCount.value * 192;
        if (sumWidth > 192)
          width = sumWidth + 'px';
      }
      return width;
    })

    // 子节点是否展开
    const isExpand = ref(false);

    // 子节点展开的数量
    const childExpandCount = ref(0);

    // 子节点产开的信息 {id:count}
    const childCountInfo = ref({})

    // 从父节点监听此节点是否打开
    watch(()=>props.expandStatus, () => {
      changeExpandChildCount(childExpandCount.value);
    })

    // 展开/收起子节点
    const changeExpandStatus = (nodeInfo) => {
      isExpand.value = !isExpand.value;
    }

    // 获取子节点的方向
    const getChildDirection = (index) => {
      let direction = 'center';
      let centerIndex = (props.nodeInfo.children.length - 1) / 2.0;
      if (centerIndex < index) {
        direction = 'right';
      } else if (centerIndex > index) {
        direction = 'left';
      }
      return direction;
    }

    // 处理当子节点展开之后的数据变化
    const handleChildCountChange = (countInfo) => {

      childCountInfo.value[countInfo.id] = countInfo.count;
      childExpandCount.value = 0;
      for (let key in childCountInfo.value) {
        if (childCountInfo.value[key] > 0) {
          childExpandCount.value += childCountInfo.value[key];
        }
      }
      changeExpandChildCount(childExpandCount.value);
      console.log(props.nodeInfo.id,childCountInfo.value)
    }

    //提交整理后的子节点数量
    const changeExpandChildCount = (count) => {
      let result = {'id': props.nodeInfo.id, 'count': 0};
      if (!props.expandStatus) {
        // 当前节点为不展开状态
        result.count = 0;
      } else if (isExpand.value) {
        // 子节点展开
        let children = props.nodeInfo.children;
        if (children) {
          // 有子节点 则使用计算后的结果
          result.count = count;
        } else {
          // 没有子节点 则为叶节点 计数1
          result.count = 1;
        }
      } else {
        // 子节点不展开 也计算为叶节点 计数1
        result.count = 1;
      }
      context.emit('changeExpandChildCount',result)
    }


    onMounted(()=>{
      if (isRoot.value == 1) {
        isExpand.value = true;
      }
      // 主动提交子节点状态
      changeExpandChildCount(0);
    })


    return {
      myLevel,
      isRoot,
      nodeWidth,
      isExpand,
      childExpandCount,
      childCountInfo,
      changeExpandStatus,
      getChildDirection,
      handleChildCountChange,
      changeExpandChildCount
    }
  }
})
</script>

<style lang="scss" scoped>

$color-black-400: rgba(183, 188, 199, 1);
$color-black-700: rgba(92, 102, 122, 1);
$lineStyle: 1px solid $color-black-400;
$gradient-primary-full: linear-gradient(225deg, #FA7D64 0%, #F65959 100%);

.tree-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  transition: all .32s ease-in;
  overflow: hidden;

  .line-box {
    height: 48px;
    width: 100%;
    position: relative;

    .line-connect {
      position: absolute;
      top: 0;
      height: 1px;
      width: 100%;
      background: $color-black-400;
    }

    .line-to-node {
      height: 100%;
      width: 50%;
      border-top: $lineStyle;
      position: absolute;
      top: 0;

      &.left {
        right: 0;
        border-left: $lineStyle;
        border-radius: 20px 0 0 0;
      }

      &.right {
        left: 0;
        border-right: $lineStyle;
        border-radius: 0 20px 0 0;
      }

      &.center {
        right: 0;
        border-top: none;
        border-left: $lineStyle;
      }
    }
  }

  .content {
    width: 160px;
    height: 40px;
    font-size: 14px;
    cursor: pointer;
    user-select: none;
    color: $color-black-700;
    background: #FFFFFF;
    box-shadow: 0px 0px 24px 0px rgba(70, 98, 139, 0.1);
    border-radius: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0 16px;
    transition: all .12s ease-in;

    &:hover {
      color: black;
    }
  }

  .bottom-line-box {
    height: 44px;
    width: 1px;
    position: relative;
    transition: all .32s ease-in;

    &.isExpand {
      background: $color-black-400;
    }
    // 展开关闭按钮
    .control-btn {
      width: 16px;
      height: 16px;
      border-radius: 50%;
      border: $lineStyle;
      background: white;
      position: absolute;
      top: 4px;
      left: 50%;
      transform: translateX(-50%);
      user-select: none;
      cursor: pointer;
      display: flex;
      justify-content: center;
      align-items: center;

      > div {
        font-size: 12px;
        transform: scale(0.7, 0.7);
      }

      &.close {
        background: $color-black-400;
        color: white;
      }
    }
  }

  .children {
    width: 100%;
    display: flex;
    justify-content: center;
    overflow: hidden;
    transition: all .32s ease-in;

    &.close {
      width: 0;
      opacity: 0;
    }
  }
}
</style>

 难点:

1.Vue3中的组件递归调用

在Vue3组件的递归调用是通过name来实现的。

比如TreeNode.vue中defineComponent声明的name是【TreeNode】,在其内部就可以直接使用TreeNode就行本身的调用。

2.宽度的动态变化

 明明可以直接通过子组件自动撑开父容器宽度,为什么还要实现节点宽度的动态变化?

子组件可以直接撑开父组件,但是展开、收起时动画变化就很生硬。组件宽度动态变化就可以实现较为平滑的过渡。

具体的实现方式在代码里面有详细的注释,这里只是介绍思路。计算宽度不是再用的从上到下的递归计算(其实这样也可以),我采用的是从叶节点到根节点的逐层上报的方式。哪里改变了(展开或者收起)就从哪开始上报,也算是减少了计算。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值