HTML+CSS绘制树状组织结构

1,236 阅读7分钟

写在开头

Hello,各位UU好呀! 😀

截止到今天,小编在新企业已经工作了一个月零一天。这段时间里,日常工作也挺繁忙,每天的生活节奏基本就是起床后便投入到工作中,忙碌一天结束后,洗个澡就准备睡觉了,几乎没有其他时间去做别的事情。虽然感觉比较枯燥,不过,这样也好,生活充实,没时间多想,也算无忧无虑了。👻

还有,就是关注关于尤大大新公司的事情...😂

希望在未来,Javascript 生态系统的构建真的能够拥有一套完整统一且开源的高性能开发工具链。

e4930e010bb3528ad88ce271c43ecea.jpg

回到正题,这次要分享的是通过HTML+CSS绘制树状组织结构,请诸君按需食用。

基本绘制

树状组织结构常常被用于展示层级关系的数据,如文件目录、组织结构图等。虽然现在很多时候咱们都是使用现成的第三方插件来完成,但是掌握从零开始绘制的过程也是非常有必要的。😋

咱们先来瞧瞧代码情况:

<!DOCTYPE html>
<html >
  <head>
    <style>
      body {
        width: 100%;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 0;
        margin: 0;
      }
      /* 三个主要元素给一个相关定位 */
      .tree,
      .node,
      .content {
        position: relative;
      }
      /* 内容展示的节点给点默认样式 */
      .content {
        border: 2px solid #000;
        border-radius: 4px;
        margin: 0 4px 8px 4px;
        padding: 4px 10px;
        width: fit-content;
      }
      /* 给所有节点一些公共样式 */
      .node {
        padding: 8px 0;
      }
      /* 同层节点保持同一行 */
      .tree {
        display: flex;
      }
      /* 树下面就是节点,节点下面就是内容与子树,让它们居中上下排列 */
      .node {
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      /* 第一根线,注意伪元素请使用 "::" */
      .tree-box .node::before {
        content: "";
        position: absolute;
        top: 0;
        outline: 1px solid #000;
        width: 100%;
      }
      .tree-box .node:first-child::before {
        width: 50%;
        right: 0;
      }
      
      .tree-box .node:last-child::before {
        width: 50%;
        left: 0;
      }
      /* 选择元素底下仅只有一个元素的情况 */
      .tree .node:first-child:last-child::before {
        width: 0;
        outline: none;
      }
      /* 第二根线,由内容提供向上的线条,由子树来提供向下的线条 */
      .tree-box .tree:before,
      .tree-box .content::before{
        outline: 1px solid #000;
        content: "";
        height: 8px;
        left: 50%;
        position: absolute;
      }
      .tree-box .tree::before {
        top: -8px;
      }
      .tree-box .content::before {
        /* 内容边框是2px */
        top: -9px;
      }
      /* 隐藏多余的线条 */
      .tree-box > .node > .content::before{
        outline: none;
      }
    </style>
  </head>
  <body>
    <div class="tree-box tree">
      <div class="node">
        <div class="content">html</div>
        <div class="tree">
          <div class="node">
            <div class="content">head</div>
            <div class="tree">
              <div class="node">
                <div class="content">title</div>
              </div>
            </div>
          </div>
          <div class="node">
            <div class="content">body</div>
            <div class="tree">
              <div class="node">
                <div class="content">header</div>
              </div>
              <div class="node">
                <div class="content">banner</div>
              </div>
              <div class="node">
                <div class="content">main</div>
                <div class="tree">
                  <div class="node">
                    <div class="content">a</div>
                  </div>
                  <div class="node">
                    <div class="content">p</div>
                  </div>
                  <div class="node">
                    <div class="content">div</div>
                  </div>
                  <div class="node">
                    <div class="content">span</div>
                  </div>
                </div>
              </div>
              <div class="node">
                <div class="content">footer</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

效果:

image.png

HTML部分咱们记住一个核心思想:树下面就只有节点,节点下面只有内容与子树。

小编的HTML部分采用纯 div 绘制,看到网上大部分都是采用 ul+li 来绘制,可能更加语义化一些,不过影响不大,看情况使用吧。👀

CSS部分...Em...可以直接看注释,应该写得很清晰明了啦。

其主要围绕三个关键类名,树(.tree)、节点(node)、内容(.content),并通过伪元素来绘制线条。还有小编采用 flex 的形式使节点水平排列,看网上很多方案是使用 float 浮动布局的形式,这也是一个区别。

通过CSS选择器,选择元素底下仅只有一个元素的情况:

  • :only-child:这个选择器可以,但有兼容性问题,算了。
  • :first-child:last-child:组合选择器,既选择第一个子元素又选择最后一个子元素,这实际上意味着该元素是其父元素的唯一子元素。(小编推荐)
  • :nth-child(1):nth-last-child(1):组合选择器,也可以,但是稍微复杂一丢丢。

🍊额外扩展小知识:

伪类:CSS伪类是用来修饰一个元素的特殊状态的,以一个冒号作为前缀。

伪元素:用于创建一些不在文档树中的元素,并为其添加样式,以两个冒号作为前缀。

伪元素(:after)与伪元素(::after)的区别:

这是由于CSS2与CSS3新旧标准的规定不同而导致的,CSS2中的伪元素使用一个冒号,但在CSS3中为了区别伪类和伪元素,规定伪元素使用两个冒号作为前缀。但对于老的CSS2中的伪元素,比如 :before、:after、:first-line、:first-letter 等写一个冒号也能被识别,不过对于新的CSS3中的伪元素就必须写两个冒号,如 ::selection

  • 伪元素
::before - 在每个 <p> 元素之后插入内容。
::after - 在每个 <p> 元素之前插入内容。
::first-letter - 选择每个 <p> 元素的首字母。
::first-line - 选择每个 <p> 元素的首行。
::selection - 选择用户选择的元素部分。
  • 伪类
:link
:visited
:hover
:active

:focus
:blur

:first-child - 父元素下的第一个元素
:first-type-child - 父元素下的同种类型的第一个元素
:last-child
:last-type-child

:nth-child()
:nth-last-child()
:nth-of-type()

:not()
:only-child

另一种风格绘制

咱们换一种风格来绘制树状组织结构。

<!DOCTYPE html>
<html>
  <head>
    <style>
      body {
        width: 100%;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 0;
        margin: 0;
        --main-color: #000;
      }
      .tree,
      .node,
      .content {
        position: relative;
      }
      .tree {
        padding-top: 20px;
        transition: all 0.5s;
        display: flex;
      }
      .node {
        padding: 20px 5px 0 5px;
        transition: all 0.5s;
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      .content {
        border: 1px solid var(--main-color);
        padding: 10px;
        text-decoration: none;
        color: #000;
        font-family: arial, verdana, tahoma;
        font-size: 0.85em;
        display: inline-block;
        transition: all 0.5s;
        border-radius: 4px;
      }
      /* 开始绘制线条 */
      .tree-box .node::before,
      .tree-box .node::after {
        content: "";
        position: absolute;
        top: 0;
        right: 50%;
        border-top: 1px solid var(--main-color);
        width: 50%;
        height: 20px;
      }
      .tree-box .node::after {
        left: 50%;
        border-left: 1px solid var(--main-color);
      }
      .tree-box .node:first-child:last-child::after,
      .tree-box .node:first-child:last-child::before {
        display: none;
        border: 0 none;
      }
      .tree-box .node:first-child:last-child {
        padding-top: 0;
      }
      .tree-box .node:last-child::before {
        border-right: 1px solid var(--main-color);
        border-radius: 0 5px 0 0;
      }
      .tree-box .node:first-child::after {
        border-radius: 5px 0 0 0;
      }
      .tree-box .node:first-child::before,
      .tree-box .node:last-child::after {
        border: 0 none;
      }
      .tree-box .tree::before {
        content: "";
        position: absolute;
        top: 0;
        left: 50%;
        border-left: 1px solid var(--main-color);
        width: 0;
        height: 20px;
      }
      /* 垂直形式 */
      .tree.vertical {
        display: flex;
        flex-direction: column;
        left: 50%;
        padding-top: 0px;
        margin-right: 50px;
      }
      .tree.vertical::before {
        display: none;
      }
      .tree.vertical .node {
        align-items: flex-start;
      }
      .tree.vertical .node::before {
        right: auto;
        border: none;
      }
      .tree.vertical .node::after {
        display: none;
      }
      .tree.vertical .node .content {
        margin-left: 15px;
        margin-right: 20px;
        white-space: nowrap;
      }
      .tree.vertical .node::before {
        top: -20px;
        left: 0px;
        border-bottom: 1px solid var(--main-color);
        border-left: 1px solid var(--main-color);
        width: 20px;
        height: 60px;
        box-sizing: border-box;
      }
      .tree.vertical .node:first-child::before {
        top: 0px;
        height: 40px;
      }
    </style>
  </head>
  <body>
    <div class="tree-box tree">
      <div class="node">
        <div class="content">html</div>
        <div class="tree">
          <div class="node">
            <div class="content">head</div>
            <div class="tree vertical">
              <div class="node">
                <div class="content">meta</div>
              </div>
              <div class="node">
                <div class="content">title</div>
              </div>
              <div class="node">
                <div class="content">style</div>
              </div>
            </div>
          </div>
          <div class="node">
            <div class="content">body</div>
            <div class="tree">
             <div class="node">
                <div class="content">header</div>
              </div>
              <div class="node">
                <div class="content">banner</div>
              </div>
              <div class="node">
                <div class="content">main</div>
                <div class="tree vertical">
                  <div class="node">
                    <div class="content">a</div>
                  </div>
                  <div class="node">
                    <div class="content">p</div>
                  </div>
                  <div class="node">
                    <div class="content">div</div>
                  </div>
                  <div class="node">
                    <div class="content">span</div>
                  </div>
                </div>
              </div>
              <div class="node">
                <div class="content">footer</div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

效果:

image.png

HTML部分大体是没什么改变,只是在"最后一个子树"上添加一个类名(.vertical),实际上,不增加类名直接通过 CSS 选择器来解决也是可行的。然而,为了保持代码的灵活性,将垂直排列的形式单独抽离出来是一个不错的做法啦。😋

当需要对垂直排列的子树进行样式调整或功能修改时,只需要针对这个类名进行操作,而不会影响到其他部分的代码。当不想要垂直排列的子树时,直接删除这个类名就可以了。

咱们还可以给它增加一点交互效果:

body {
  /* ... */
  --main-color: #8A2BE2; /* 改个好看的颜色吧 */
}
.tree .node .content:hover,
.tree .node .content:hover + .tree .node .content {
    background-color: var(--main-color);
    color: #fff;
}

效果:

20241013-1.gif

用Vue封装成组件(递归)

最后,为了使树状组织结构更加容易被使用,咱们来将其封装成一个Vue组件。

使用第一种风格作为演示:

<template>
  <div class="tree" :class="{'tree-box': first}">
    <div v-for="item in treeData" :key="item.content" class="node">
      <div class="content">{{ item.content }}</div>
      <template v-if="item.children && item.children.length">
        <tree :treeData="item.children" :first="false" />
      </template>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Tree',
  props: {
    treeData: {
      type: Array,
      default: () => []
    },
    first: {
      type: Boolean,
      default: true
    },
  }
}
</script>

<style>
/* 样式就省略了,与上面的一致 */
</style>

使用时:

<template>
  <tree :treeData="treeData"  />
</template>

<script>
import Tree from './components/Tree.vue'

export default {
  components: { Tree },
  data() {
    return {
      treeData: [
        {
          content: 'html',
          children: [
            {
              content: 'head',
              children: [
                { content: 'title' }
              ]
            },
            {
              content: 'body',
              children: [
                { content: 'header' },
                { content: 'banner' },
                { content: 'main', children: [ { content: 'a' }, { content: 'p' }, { content: 'div' }, { content: 'span' } ] },
                { content: 'footer' },
              ]
            }
          ]
        },
      ]
    }
  }
}
</script>

效果是一致的。这里的难点主要在于通过递归的形式来实现组件的无限层级展示,进而达成树状组织结构的呈现,也不难哈。😋

通过以上三种方式,咱们可以使用 HTML 和 CSS 绘制树状组织结构,并根据实际需求选择不同的风格和封装方式。无论是基本绘制、另一种风格绘制还是用 Vue 封装成组件,都可以根据具体情况进行调整和扩展,以满足不同的项目需求,希望对你有所帮助吧。🍊





至此,本篇文章就写完啦,撒花撒花。

image.png