Vue 组件设计:构建生动多彩的树形结构组件

简介: 本文介绍了如何使用 Vue 构建一个功能强大的树形结构组件。该组件支持递归渲染节点及其子节点,提供了自定义节点颜色、文本和布局的功能。通过独特的样式处理不同层级的节点,展示出丰富的视觉效果。组件还支持动态布局和缩放,确保灵活的界面展示和用户体验。文章提供了详细的代码实现,包括 HTML、JavaScript 和 SCSS,帮助开发者快速集成和定制自己的树形结构组件。

一个优雅展示树形结构数据的 Vue 组件,递归渲染每个节点及其子节点,支持自定义颜色、文本和布局。通过独特的样式巧妙处理不同层级,为用户打造丰富的视觉盛宴。

核心功能

- 递归渲染:组件可以递归地渲染每个节点及其子节点,形成树形结构;
- 自定义样式:支持通过传入的节点数据自定义节点颜色和文本;
- 动态布局:可以根据传入的属性决定节点是左布局、右布局还是左右布局;
- 层级颜色:根据节点的层级显示不同的颜色。

废话不多说,我们直接开始!

效果图:

Snipaste_2024-02-01_17-43-36

首先,我们在components文件夹下新建一个组件PathoNode.vue,该组件将负责渲染每个节点及其子节点。

其中,LEVEL_COLORS 为不同层级节点定义了颜色,isLeaf 计算属性用于判断当前节点是否为叶子节点。

代码如下:

<template>
  <div class="node-item" :class="{left: isLeft}">
    <div class="node-item_not-leaf" v-if="!isLeaf">
      <div class="node-name" :style="{background: node.color || LEVEL_COLORS[node.level] || LEVEL_COLORS[3]}" :class="{'round': node.level === 2}">{
  {node.text}}</div>
      <div class="node-children">
        <patho-node v-for="(childNode, i) in node.children" :key="'childNode' + i" :node="childNode" :isLeft="isLeft"/>
      </div>
    </div>
    <div class="node-item_leaf" v-else>
      <div class="node-name">{
  {node.text}}</div>
    </div>
  </div>
</template>

<script>
const LEVEL_COLORS = {
  1: '#1A4843',
  2: '#464885',
  3: '#46857E',
  4: '#857146',
  5: '#6A8546',
  6: '#854646',
  7: '#818076',
  8: '#979B33',
  9: '#336E9B',
  10: '#854683'
}
export default {
  name: 'PathoNode',
  props: {
    node: {
      type: Object,
      default () {
        return {}
      }
    },
    isLeft: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      LEVEL_COLORS
    }
  },
  computed: {
    isLeaf () {
      return !(this.node.children && this.node.children.length > 0)
    }
  },
  methods: {

  }
}
</script>

<style lang="scss" scoped>
$border-color-primary: #B5BDC4;
$text-color-primary: #b4babf;
$color-black: #000000;
@mixin firstNode {
  position: relative;
  border: none;
}
@mixin rightNode {
  position: relative;
  padding-left: 10px;
  border-left: 1px solid $border-color-primary;
}
@mixin leftNode {
  position: relative;
  padding: 0;
  padding-right: 10px;
  border: none;
  border-right: 1px solid $border-color-primary;
}
@mixin rightHorizonLine {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  width: 10px;
  border-top: 1px solid $border-color-primary;
}
@mixin leftHorizonLine {
  @include rightHorizonLine;
  left: auto;
  right: 0;
}
@mixin beforeRightFirstChild {
  bottom: 0;
  border-radius: 4px 0 0 0;
  border-left: 1px solid $border-color-primary;
}
@mixin beforeRightLastChild {
  left: 0;
  top: 0;
  bottom: 50%;
  border-radius: 0 0 0 4px;
  border: none;
  border-left: 1px solid $border-color-primary;
  border-bottom: 1px solid $border-color-primary;
}
// 左边第一个child
@mixin beforeLeftFirstChild {
  @include beforeRightFirstChild;
  border: none;
  border-radius: 0 4px 0 0;
  border-top: 1px solid $border-color-primary;
  border-right: 1px solid $border-color-primary;
}
// 左边最后一个child
@mixin beforeLeftLastChild {
  @include beforeRightLastChild;
  border-radius: 0 0 4px 0;
  border: none;
  border-right: 1px solid $border-color-primary;
  border-bottom: 1px solid $border-color-primary;
}
@mixin beforeRightOnlyOneChild {
  left: 0;
  top: 50%;
  bottom: auto;
  border: 0;
  border-radius: 0;
  border-top: 1px solid $border-color-primary;
}
@mixin beforeLeftOnlyOneChild {
  @include beforeRightOnlyOneChild;
  left: auto;
  right: 0;
}
.node-item {
  display: flex;
  position: relative;
  flex-direction: row;
  justify-content: flex-start;
  @include rightNode;
  &::before{
    @include rightHorizonLine;
  }
  &:first-child{
   @include firstNode;
    &::before{
      @include beforeRightFirstChild;
    }
  }
  &:last-child{
    border-left: none;
    &::before{
      @include beforeRightLastChild;
    }
  }
  &:first-child:last-child{
    border-left: none;
  }
  &:first-child:last-child::before{
    @include beforeRightOnlyOneChild;
  }
  .node-name{
    flex-shrink: 0;
    display: inline-block;
    font-size: 14px;
    border-radius: 1px;
    margin: 10px 0;
    padding: 5px 20px;
    width: auto;
    line-height: 20px;
    font-weight: 600;
    height: auto;
    border-radius: 3px;
    color: $color-black;
    &.round{
      width: 64px;
      height: 64px;
      padding: 0;
      border-radius: 50%;
      line-height: 64px;
      text-align: center;
    }
  }
  .node-children{
    @include rightNode;
    border-left: none;
    &::before{
      @include rightHorizonLine;
    }
  }
  .node-item_not-leaf{
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
    align-items: center;
    &::before{
      border-left: 1px solid $border-color-primary;
    }
  }
  .node-item_leaf{
    .node-name{
      background: $color-black;
      color: $text-color-primary;
      border: 1px solid $text-color-primary;
      margin-right: 20px;
    }
  }
  &.left{
    @include leftNode;
    &::before{
      @include leftHorizonLine;
    }
    &:first-child{
      @include firstNode;
    }
    &:first-child::before{
      @include beforeLeftFirstChild;
    }
    &:last-child{
      border: none;
      &::before{
        @include beforeLeftLastChild;
      }
    }
    &:first-child:last-child{
      border-right: none;
    }
    &:first-child:last-child::before{
      @include beforeLeftOnlyOneChild;
    }
    .node-item_not-leaf{
      &::before{
        border: none;
        border-right: 1px solid $border-color-primary;
      }
    }
    .node-name{
      &::after{
        content: '\200E';
      }
    }
    .node-children{
      @include leftNode;
      border-right: none;
      &::before{
        @include leftHorizonLine;
      }
    }
    .node-item_leaf{
      .node-name{
        margin-right: 0;
        margin-left: 20px;
      }
    }
  }
  // transition
  .node-fade-enter-acitve, .node-fade-leave-active {
    transition: all .5s;
  }
  .node-fade-enter, .node-fade-leave-to{
    opacity: 0;
  }
  .node-fade-enter-to, .node-fade-leave {
    opacity: 1;
  }
}
</style>

节点组件部分负责定义单个节点的渲染结构,并通过递归调用<patho-node>组件处理子节点。当然,根据具体业务需求,你也可以进一步封装此文件成为专用的业务组件。

<template>
  <div class="patho-tab" ref="pathoChartContainer">
    <div class="patho-tab__zoom" @click="clickHandler">
      <el-scrollbar style="height:100%; width:100%;">
        <transition name="patho-chart">
          <div class="patho-chart" :style="{ transform: `scale(${scaleRatio}) translate(${translateX}px, ${translateY}px)` }">
            <div class="patho-chart__section patho-chart__section_left" v-if="leftDatas.length > 0">
              <patho-node v-for="(node, i) in leftDatas" :key="'node' + i" :node="node" :isLeft="true" />
            </div>
            <div class="patho-chart__section patho-chart__section_center root-node" v-if="nodeData">{
  { nodeData.text }}
            </div>
            <div class="patho-chart__section patho-chart__section_right " v-if="rightDatas.length > 0">
              <patho-node v-for="(node, i) in rightDatas" :key="'node' + i" :node="node" :isLeft="false" />
            </div>
          </div>
        </transition>
      </el-scrollbar>
    </div>
  </div>
</template>
<script>
import pathoNode from '@/components/PathoNode.vue'
export default {
  components: {
    pathoNode
  },
  data() {
    return {
      scaleRatio: 1,
      translateX: 0,
      translateY: 0,
      // 组织结构图数据
      nodeData: {
        "text": "综合分析",
        "color": null,
        "level": 1,
        "children": [
          {
            "text": "血",
            "color": "#9BBA5B",
            "level": 2,
            "children": [
              {
                "text": "活血化瘀",
                "color": null,
                "level": 3,
                "children": [
                  {
                    "text": "岷归",
                    "color": null,
                    "level": 4,
                    "children": null
                  }
                ]
              }
            ]
          },
          {
            "text": "汗",
            "color": "#C58080",
            "level": 2,
            "children": [
              {
                "text": "固涩_止汗",
                "color": null,
                "level": 3,
                "children": [
                  {
                    "text": "白芍",
                    "color": null,
                    "level": 4,
                    "children": null
                  },
                  {
                    "text": "于朮",
                    "color": null,
                    "level": 4,
                    "children": null
                  }
                ]
              }
            ]
          }
          // ...
        ]
      }
    }
  },
  computed: {
    leftDatas() {
      const children = this.nodeData.children || []
      const len = children.length
      return children.slice(0, Math.floor(len / 2))
    },
    rightDatas() {
      const children = this.nodeData.children || []
      const len = children.length
      return children.slice(len / 2)
    }
  },
  methods: {
    clickHandler() {
      if (this.scaleRatio === 1) {
        this.scaleRatio = 1.2;
      } else {
        this.scaleRatio = 1;
      }
    }
  }
}
</script>
<style lang="scss" scoped>
$border-color-primary: #B5BDC4;
$color-primary: #49B8A3;
$color-black: #000000;

@mixin rightHorizonLine {
  content: '';
  position: absolute;
  top: 50%;
  left: 0;
  width: 10px;
  border-top: 1px solid $border-color-primary;
}

@mixin leftHorizonLine {
  @include rightHorizonLine;
  left: auto;
  right: 0;
}

.patho-tab {
  display: flex;
  flex-direction: column;
  height: 100%;
  position: relative;

  &__body {
    user-select: none;
    overflow: hidden;
    flex-grow: 1;
  }

  .patho-chart {
    position: relative;
    display: flex;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    margin: 0 auto;
    padding: 2em 0;
    width: max-content;
    cursor: pointer;

    .patho-chart__section {
      display: flex;
      flex-direction: column;
      flex-shrink: 0;
      flex-basis: auto;

      &.patho-chart__section_right {
        position: relative;
        padding: 0 0 0 10px;

        &::before {
          @include rightHorizonLine
        }
      }

      &.patho-chart__section_left {
        position: relative;
        direction: rtl;
        text-align: left;
        justify-content: flex-end;
        padding: 0 10px 0 0;

        &::before {
          @include leftHorizonLine;
        }
      }
    }

    .root-node {
      width: 90px;
      height: 90px;
      border-radius: 50%;
      background: #1A4843;
      color: $color-black;
      font-size: 16px;
      font-weight: 600;
      color: $color-primary;
      text-align: center;
      line-height: 90px;
    }
  }
}

/deep/ .el-scrollbar__view {
  min-height: 100%;
}

.patho-tab__zoom {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100vw;
  height: 100vh;
  background: rgba(0, 0, 0, 0.8);
  user-select: none;
  z-index: 100;

  .patho-chart {
    margin: 0;
    transform-origin: 0 0;
  }
}

.patho-tab__no-datas {
  margin-top: 20%;
  text-align: center;

  &_icon {
    width: 200px;
  }

  &_text {
    @include noDatas;
  }
}

.patho-chart-enter-active,
.patho-chart-leave-active {
  transition: opacity .5s;
}

.patho-chart-enter,
.patho-chart-leave-to {
  opacity: 0;
}

.patho-chart-enter-to,
.patho-chart-leave {
  opacity: 1;
}
</style>

通过以上设计,我们成功打造了一个多功能的树形结构组件,具备丰富的自定义选项,涵盖节点颜色、文本和布局等方面。这样的组件不仅能够实现功能性的树形结构展示,同时为用户提供了生动多彩的视觉体验。

目录
相关文章
|
1天前
|
JavaScript
在 Vue 3 中使用 DHTMLX 甘特图组件
本文将介绍如何在 Vue 3 项目中集成 DHTMLX 甘特图组件,详细讲解安装、模块导入以及基本用法。通过示例代码,您将学会如何配置甘特图的任务、样式和交互功能,帮助您在项目中更有效地管理和展示任务时间线。
7 0
|
1天前
|
JavaScript 前端开发 安全
如何在 Vue 页面中禁止选择、右键、复制及 F12 开发者工具
【10月更文挑战第3天】 在前端开发中,保护页面内容不被随意复制或查看是一个常见需求。本文介绍了如何在 Vue 应用中实现禁止文本选择、右键菜单、复制操作以及 F12 开发者工具的方法。通过结合 CSS 和 JavaScript 事件监听,我们可以增加用户查看和复制内容的难度,尽管无法完全阻止高级用户。适当的防护措施可以为内容提供一层额外的保护,帮助开发者提升页面安全性。
12 0
|
1天前
|
JavaScript
vue 组件中的 data 为什么是一个函数 ?
【10月更文挑战第8天】 在 Vue 组件中,`data` 被定义为一个函数而非普通对象,以确保每个组件实例拥有独立的数据空间,避免数据混乱。这种方式还支持数据的响应式更新、组件的继承与扩展,并有助于避免潜在问题,提升应用的可靠性和性能。
7 2
|
4天前
|
JavaScript
VUE学习二:事件监听(v-on)、条件判断(v-if/v-else-if/v-else)、循环遍历(v-for)
这篇文章是关于Vue.js的学习指南,涵盖了事件监听、条件判断、循环遍历、数组方法响应性、案例分析以及高阶函数的使用。
22 2
VUE学习二:事件监听(v-on)、条件判断(v-if/v-else-if/v-else)、循环遍历(v-for)
|
4天前
|
存储 JavaScript 前端开发
Vue中组件通信方式有哪些?
本文介绍了 Vue 中几种常见的组件间通信方式,包括 Props / $emit、provide / inject、ref / refs、eventBus、Vuex、$parent / $children、$attrs / $listeners 以及通过 vue-router 传参。每种方式都有其适用场景和注意事项,帮助开发者根据具体需求选择合适的通信方式。
12 3
Vue中组件通信方式有哪些?
|
5天前
|
JavaScript 数据可视化
vue-cli学习二:vue-cli3版本 创建vue项目后,Runtime-Compiler和Runtime-only两个模式详解;vue项目管理器;配置文件的配置在哪,以及如何配置
这篇文章详细介绍了Vue CLI 3版本创建项目时的Runtime-Compiler和Runtime-only两种模式的区别、Vue程序的运行过程、render函数的使用、eslint的关闭方法,以及Vue CLI 2和3版本配置文件的不同和脚手架3版本创建项目的配置文件配置方法。
15 3
vue-cli学习二:vue-cli3版本 创建vue项目后,Runtime-Compiler和Runtime-only两个模式详解;vue项目管理器;配置文件的配置在哪,以及如何配置
|
1天前
|
监控 JavaScript 开发者
在 Vue 中,子组件为何不可以修改父组件传递的 Prop,如果修改了,Vue 是如何监控到属性的修改并给出警告的
在 Vue 中,子组件不能直接修改父组件传递的 Prop,以确保数据流的单向性和可预测性。如果子组件尝试修改 Prop,Vue 会通过响应式系统检测到这一变化,并在控制台发出警告,提示开发者避免这种操作。
|
1天前
|
JavaScript 开发者
Vue Render函数
【10月更文挑战第11天】 Vue 的 Render 函数提供了一种强大而灵活的方法来创建虚拟 DOM 节点,使开发者能够更精细地控制组件的构建过程。通过 `createElement` 参数,可以动态生成各种元素和组件,实现复杂逻辑和高级定制。尽管使用 Render 函数需要更多代码和对虚拟 DOM 的深入理解,但它在处理复杂场景时展现出巨大的优势。
5 2
|
2天前
|
设计模式 JavaScript 前端开发
vue的MVVM模型
MVVM(Model-View-ViewModel)是Vue框架的核心设计模式,将应用分为模型(数据和业务逻辑)、视图(用户界面)和视图模型(数据转换与用户交互)。其特点包括数据驱动、组件化、双向数据绑定及响应式,提高了开发效率和应用质量。【10月更文挑战第2天】
|
2天前
|
存储 缓存 JavaScript
Vue 有哪些提高性能的优化技巧
【10月更文挑战第7天】 在 Vue 应用开发中,性能优化至关重要。本文介绍了十大优化技巧,包括数据结构优化、组件化设计、虚拟 DOM 优化、事件处理、数据绑定、图片优化、网络请求、代码优化、服务端渲染及其他技巧,帮助提升应用性能和用户体验。
10 1