Vue3 实现树结构目录样式

320 阅读1分钟

一、Tree.vue

<!-- 树 -->
<template>
  <div>
    <TreeItem :node="rootNode" />
  </div>
</template>
<script lang="ts" setup>
import { onMounted, reactive } from "vue";
import TreeItem from "./TreeItem.vue";

enum NodeType {
  Folder = "folder",
  File = "file",
}

interface TreeNode {
  id: string;
  name: string;
  type: NodeType;
  expanded?: boolean;
  children?: TreeNode[];
}

const generateUniqueId = (): string => {
  return "_" + Math.random().toString(36).substr(2, 9);
};

const rootNode: TreeNode = reactive({
  id: generateUniqueId(),
  name: "根文件夹",
  type: NodeType.Folder,
  expanded: true,
  children: [
    {
      id: generateUniqueId(),
      name: "文件夹 1",
      type: NodeType.Folder,
      expanded: true,
      children: [
        {
          id: generateUniqueId(),
          name: "文件夹 1.1",
          type: NodeType.Folder,
          expanded: true,
          children: [
            {
              id: generateUniqueId(),
              name: "文件夹 1.1.1",
              type: NodeType.Folder,
              expanded: true,
              children: [
                {
                  id: generateUniqueId(),
                  name: "文件 1.1.1.1",
                  type: NodeType.Folder,
                },
                {
                  id: generateUniqueId(),
                  name: "文件 1.1.1.2",
                  type: NodeType.File,
                },
              ],
            },
            {
              id: generateUniqueId(),
              name: "文件 1.1.2",
              type: NodeType.File,
            },
          ],
        },
        {
          id: generateUniqueId(),
          name: "文件 1.2",
          type: NodeType.File,
        },
      ],
    },
    {
      id: generateUniqueId(),
      name: "文件 2",
      type: NodeType.File,
    },
  ],
});

function initPage() {}

onMounted(() => {
  initPage();
});
</script>
<style lang="scss" scoped></style>

二、TreeItem.vue

<!-- 树 -->
<template>
  <div class="tree-node">
    <v-row>
      <v-col cols="12">
        <div @click="toggleNode" class="tree-node-content">
          <v-icon
            v-if="localNode.type === NodeType.Folder && localNode.expanded"
            :class="{ 'icon-rotate': localNode.expanded }"
          >
            mdi-folder-open-outline
          </v-icon>
          <v-icon
            v-else-if="
              localNode.type === NodeType.Folder && !localNode.expanded
            "
            :class="{ 'icon-rotate': localNode.expanded }"
          >
            mdi-folder-outline
          </v-icon>
          <v-icon v-else>mdi-file</v-icon>
          <span>{{ localNode.name }}</span>
          <v-btn
            v-if="localNode.type === NodeType.Folder"
            variant="text"
            icon
            @click.stop="addChildNode(NodeType.Folder)"
          >
            <v-icon>mdi-folder-plus</v-icon>
          </v-btn>
          <v-btn variant="text" icon @click.stop="addChildNode(NodeType.File)">
            <v-icon>mdi-file-plus</v-icon>
          </v-btn>
        </div>
      </v-col>
    </v-row>
    <v-row v-if="localNode.expanded && hasChildren">
      <v-col cols="12" sm="6">
        <ul>
          <li v-for="childNode in localNode.children" :key="childNode.id">
            <TreeItem :node="childNode" />
          </li>
        </ul>
      </v-col>
    </v-row>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, computed, reactive } from "vue";

enum NodeType {
  Folder = "folder",
  File = "file",
}

interface TreeNode {
  id: string;
  name: string;
  type: NodeType;
  expanded?: boolean;
  children?: TreeNode[];
}

const generateUniqueId = (): string => {
  return "_" + Math.random().toString(36).substr(2, 9);
};

const props = defineProps({
  node: {
    type: Object as () => TreeNode,
    required: true,
  },
});

const localNode = reactive(props.node);

const toggleNode = (): void => {
  if (localNode.type === NodeType.Folder) {
    localNode.expanded = !localNode.expanded;
  }
};

const addChildNode = (type: NodeType): void => {
  if (!localNode.children) {
    localNode.children = [];
  }

  const newNode: TreeNode = {
    id: generateUniqueId(),
    name: type === NodeType.Folder ? "新文件夹" : "新文件",
    type,
  };
  localNode.children.push(newNode);
  console.log("localNode", localNode);
};

const hasChildren = computed(() => {
  return Boolean(localNode.children && localNode.children.length > 0);
});

function initPage() {}

onMounted(() => {
  initPage();
});
</script>

<style lang="scss" scoped>/* 调整树节点的样式 */
.tree-node {
  margin-bottom: 10px;

  /* 使用 SCSS 变量 */
  $node-padding: 10px;
  padding-left: $node-padding;
}

/* 调整节点内容的样式 */
.tree-node .tree-node-content {
  display: flex;
  align-items: center;
  cursor: pointer;
}

/* 调整节点名称的样式 */
.tree-node .tree-node-content span {
  margin-left: 10px;
}

/* 调整展开图标的样式 */
.tree-node .tree-node-content .v-icon {
  margin-right: 5px;
}

/* 调整添加按钮的样式 */
.tree-node .tree-node-content .v-btn {
  margin-left: 5px;
}

/* 调整子节点的样式 */
.tree-node ul {
  margin-left: 20px;
  list-style-type: none;
}
</style>