vue使用树形结构展示文件和文件夹

1. 树形结构显示
  • 显示文件夹和文件 :使用 el-tree 组件展示树形结构,文件夹和文件的图标通过 el-icon 进行动态显示。文件夹使用 Folder 图标,文件使用 Files 图标。
  • 节点点击 :点击树形节点后,会将选中的节点保存到 selectedNode 中,方便后续操作(如重命名、删除)。
2. 创建文件夹
  • 在根目录或者当前选中的文件夹下,点击 新建文件夹 按钮,触发 createFolder 方法创建一个新的文件夹。
  • 新文件夹将被添加到当前选中节点的 children 数组中,或者如果没有选中任何节点,则将文件夹添加到根目录。
3. 创建文件
  • 类似于文件夹,点击 新建文件 按钮,触发 createFile 方法创建一个新的文件,并将其添加到当前选中的文件夹下或者根目录。
4. 删除文件/文件夹
  • 通过点击 删除文件/文件夹 按钮,触发 removeFile 方法,删除当前选中的文件或文件夹。
  • 删除操作会检查所选文件夹或文件的父级节点,如果有父节点,直接从父节点的 children 中移除该项;如果是根节点,则从根目录数组中删除。
5. 重命名文件/文件夹
  • 通过右键点击某个节点,弹出自定义的上下文菜单。选择 重命名 选项后,触发 renameNode 方法,通过 prompt 弹窗输入新的名称,并更新节点的 label
6. 删除文件/文件夹(右键菜单)
  • 右键点击文件夹或文件,弹出自定义右键菜单,选择 删除 后会弹出确认对话框,确认删除后,调用 deleteNode 删除当前选中的文件/文件夹。
7. 上下文菜单
  • 在树形结构中,右键点击文件夹或文件时,会弹出自定义的上下文菜单,显示 重命名删除 选项。
  • 上下文菜单会根据鼠标点击的位置动态显示,通过 contextMenuPosition 控制菜单的位置。
8. 事件监听
  • 点击外部区域隐藏上下文菜单 :使用 document.addEventListener("click", handleOutsideClick) 监听点击事件,点击树形组件外部区域时,隐藏上下文菜单并清空选中的节点。
  • 组件卸载时移除事件监听 :在组件卸载时,通过 onBeforeUnmount 钩子移除点击外部区域的事件监听,避免内存泄漏。
javascript 复制代码
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { Folder, Files } from "@element-plus/icons-vue";

// 在组件挂载后添加事件监听
onMounted(() => {
  document.addEventListener("click", handleOutsideClick);
});
// 在组件卸载前移除事件监听
onBeforeUnmount(() => {
  document.removeEventListener("click", handleOutsideClick);
});
// 定义树形数据
const data = ref([
  {
    label: "文件夹1",
    type: "folder",
    children: [
      {
        label: " 1-1",
        type: "folder",
        children: [
          {
            label: "1-1-1",
            type: "file",
          },
        ],
      },
    ],
  },
  {
    label: "文件夹2",
    type: "folder",
    children: [
      {
        label: "2-1",
        type: "folder",
        children: [
          {
            label: "2-1-1",
            type: "file",
          },
        ],
      },
      {
        label: "2-2",
        type: "file",
      },
    ],
  },
  {
    label: "文件夹3",
    type: "folder",
    children: [],
  },
]);

// 配置树形数据的显示属性,指定文件夹使用 Folder 图标
const defaultProps = {
  children: "children",
  label: "label",
  icon: "icon", // 图标属性
};
// 当前选中的节点
const selectedNode = ref(null);
const handleNodeClick = (data) => {
  selectedNode.value = data;
};
// 监听点击事件,判断点击是否在树形组件外
const handleOutsideClick = (event) => {
  const treeElement = document.querySelector(".el-tree"); // 获取树形组件的 DOM 元素
  const buttonContainer = document.querySelector(".header"); // 获取按钮区域
  // 判断点击区域是否在树形组件或按钮区域外
  if (
    !treeElement.contains(event.target) &&
    !buttonContainer.contains(event.target)
  ) {
    selectedNode.value = null; // 如果点击区域不是树或按钮区域,则清除选中的节点
    showContextMenu.value = false; // 隐藏菜单
  }
};
// 创建文件夹
const createFolder = () => {
  const newFolder = {
    label: "新文件夹",
    type: "folder",
    children: [],
  };

  if (selectedNode.value && selectedNode.value.type === "folder") {
    // 在选中的文件夹下创建
    selectedNode.value.children.push(newFolder);
  } else {
    // 根目录下创建
    data.value.push(newFolder);
  }
};

// 创建文件
const createFile = () => {
  const newFile = {
    label: "新文件",
    type: "file",
  };
  if (selectedNode.value && selectedNode.value.type === "folder") {
    // 在选中的文件夹下创建
    selectedNode.value.children.push(newFile);
  } else {
    // 根目录下创建
    data.value.push(newFile);
  }
};

// 删除文件或文件夹
const removeFile = () => {
  if (!selectedNode.value) {
    alert("请先选择一个文件或文件夹");
    return;
  }
  const parentNode = findParentNode(data.value, selectedNode.value);
  if (parentNode) {
    const index = parentNode.children.findIndex(
      (item) => item.label === selectedNode.value.label
    );
    if (index !== -1) {
      parentNode.children.splice(index, 1);
      selectedNode.value = null; // 删除后清空选中的节点
    }
  } else {
    // 如果没有父节点,表示是根节点,直接从根节点删除
    const index = data.value.findIndex(
      (item) => item.label === selectedNode.value.label
    );
    if (index !== -1) {
      data.value.splice(index, 1);
      selectedNode.value = null; // 删除后清空选中的节点
    }
  }
};
// 查找父节点
const findParentNode = (nodes, targetNode) => {
  for (let node of nodes) {
    if (node.children && node.children.includes(targetNode)) {
      return node;
    } else if (node.children) {
      const result = findParentNode(node.children, targetNode);
      if (result) return result;
    }
  }
  return null;
};
// 控制上下文菜单显示
const showContextMenu = ref(false); 
// 上下文菜单位置
const contextMenuPosition = ref({ top: 0, left: 0 }); 
//右键显示菜单
const handleContextMenu = (event, data, node) => {
  event.preventDefault(); // 阻止默认右键菜单
  selectedNode.value = node.data; // 保存当前节点
  showContextMenu.value = true; // 显示自定义右键菜单
  contextMenuPosition.value = { top: event.clientY, left: event.clientX }; // 设置菜单显示位置
};
//重命名
const renameNode = () => {
  if (selectedNode.value) {
    const newName = prompt("请输入新名称", selectedNode.value.label);
    if (newName) {
      selectedNode.value.label = newName; // 修改节点名称
    }
  }
  showContextMenu.value = false; // 隐藏菜单
};
//删除
const deleteNode = () => {
  if (selectedNode.value) {
    const confirmDelete = confirm(`确定删除 ${selectedNode.value.label} 吗?`);
    if (confirmDelete) {
      removeFile()
    }
  }
  showContextMenu.value = false; // 隐藏菜单
};
</script>

<template>
  <div class="container">
    <header class="header">
      <!-- 新建文件和文件夹的按钮 -->
      <el-button type="primary" @click="createFolder">新建文件夹</el-button>
      <el-button type="primary" @click="createFile">新建文件</el-button>
      <el-button type="primary" @click="removeFile">删除文件/文件夹</el-button>
    </header>
    <div class="main">
      <aside class="aside">
        <el-tree
          style="max-width: 600px"
          :data="data"
          :props="defaultProps"
          @node-click="handleNodeClick"
          @node-contextmenu="handleContextMenu"
        >
          <!-- 使用 render-content 插槽自定义节点显示 -->
          <template #default="{ node, data }">
            <span>
              <!-- 判断节点类型来动态显示图标 -->
              <el-icon v-if="data.type === 'folder'"><Folder /></el-icon>
              <el-icon v-if="data.type === 'file'"><Files /></el-icon>
              {{ data.label }}
            </span>
          </template>
        </el-tree>
        <div
        class="dropdown"
          v-if="showContextMenu"
          :style="{
            position: 'absolute',
            top: contextMenuPosition.top + 'px',
            left: contextMenuPosition.left + 'px',
          }"
        >
          <ul slot="dropdown">
            <li @click="renameNode">重命名</li>
            <li @click="deleteNode">删除</li>
          </ul>
        </div>
      </aside>
      <div class="content">
        <!-- 内容区域 -->
      </div>
    </div>
  </div>
</template>

<style scoped>
.container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}

.header {
  height: 60px;
  border:1px solid #ccc;
}

.main {
  flex: 1;
  display: flex;
}

.aside {
  width: 200px;
  border-right: 1px solid #ccc;
}

.content {
  flex: 1;
  padding: 10px;
}
.dropdown {
  position: absolute;
  background-color: #fff;
  padding:20px;
  border-radius: 5px;
  border:1px solid #ccc;
}
</style>
相关推荐
奇偶变不变17 分钟前
30分钟学会HTML
前端·html
cwtlw24 分钟前
Element-plus表单总结
前端·javascript·笔记·学习·其他
奇偶变不变25 分钟前
30分钟学会LaTex
开发语言·前端·javascript
桂月二二40 分钟前
解锁2025编程新高度:深入探索编程技术的最新趋势
前端·人工智能·flutter·neo4j·wasm
学如逆水,不进则退1 小时前
uniapp实现后端数据i18n国际化
前端·javascript·uni-app
caridle1 小时前
使用SSH建立内网穿透,能够访问内网的web服务器
服务器·前端·ssh
罗_三金1 小时前
react优势劣势及案列
前端·javascript·前端框架·react·jsx
GISer_Jing2 小时前
React知识盲点——组件通信、性能优化、高级功能详解(大纲)
javascript·react.js·前端框架
谷哥的小弟2 小时前
解决npm报错:sill idealTree buildDeps
前端·npm·node.js
fly_vip2 小时前
npm install --global windows-build-tools --save 失败
前端·npm·node.js