一、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>