Vue3 Element-plus构建文件夹功能

<template>
  <div style="margin: 10px">
    <div style="width: 100%; display: flex; flex-direction: row">
      <div style="margin-right: 4px">
        <el-upload
          ref="uploadRef"
          multiple
          :auto-upload="false"
          :show-file-list="false"
          v-model:file-list="uploadFileList"
          :on-change="uploadFileChange"
        >
          <template #trigger>
            <el-button size="small" @click="openUploadFile($event, null)"
              >上传文件</el-button
            >
          </template>
        </el-upload>
      </div>
      <div style="margin-right: 4px">
        <el-button size="small" @click="openInsertFile($event, null)">
          添加文件
        </el-button>
      </div>
      <div style="margin-right: 4px">
        <el-button size="small" @click="openInsertFolder($event, null)">
          添加文件夹
        </el-button>
      </div>
    </div>
  </div>
  <el-tree
    style="max-width: 600px"
    empty-text="没有文件,快去上传吧!"
    :allow-drop="allowDrop"
    :allow-drag="allowDrag"
    :data="dataSource"
    draggable
    default-expand-all
    node-key="id"
    highlight-current
    @node-drag-start="handleDragStart"
    @node-drag-enter="handleDragEnter"
    @node-drag-leave="handleDragLeave"
    @node-drag-over="handleDragOver"
    @node-drag-end="handleDragEnd"
    @node-drop="handleDrop"
  >
    <template #default="scope">
      <div style="width: 100%; display: flex; justify-content: space-between">
        <div style="display: flex; justify-content: left">
          <div
            style="
              display: flex;
              flex-direction: column;
              justify-content: center;
            "
            v-if="scope.node.data.type === 'folder'"
          >
            <!-- 文件夹展示 -->
            <el-icon><Folder /></el-icon>
          </div>
          <div v-else>
            <!-- 文件展示 -->
            <el-icon><Document /></el-icon>
          </div>
          <div style="margin-left: 3px">{{ scope.node.label }}</div>
        </div>
        <div style="margin-right: 20px">
          <el-popover placement="right" trigger="hover">
            <template #reference>
              <el-icon><Tools /></el-icon>
            </template>
            <div class="but-list" style="display: flex; flex-direction: column">
              <div v-if="scope.node.data.type === 'folder'">
                <el-upload
                  ref="uploadRef"
                  multiple
                  :auto-upload="false"
                  :show-file-list="false"
                  v-model:file-list="uploadFileList"
                  :on-change="uploadFileChange"
                >
                  <template #trigger>
                    <el-button
                      style="width: 124px"
                      @click="openUploadFile($event, scope.node)"
                      >上传文件</el-button
                    >
                  </template>
                </el-upload>
              </div>

              <div v-if="scope.node.data.type === 'folder'" style="width: 100%">
                <el-button
                  style="width: 100%"
                  @click="openInsertFile($event, scope.node)"
                >
                  添加文件
                </el-button>
              </div>
              <div v-if="scope.node.data.type === 'folder'" style="width: 100%">
                <el-button
                  style="width: 100%"
                  @click="openInsertFolder($event, scope.node)"
                  >添加文件夹</el-button
                >
              </div>
              <div style="width: 100%">
                <el-button
                  style="width: 100%"
                  @click="openUpdate($event, scope.node)"
                >
                  修改
                </el-button>
              </div>
              <div style="width: 100%">
                <el-button
                  style="width: 100%"
                  @click="openDelete($event, scope.node)"
                  >删除</el-button
                >
              </div>
            </div>
          </el-popover>
        </div>
      </div>
    </template>
  </el-tree>

  <!-- 新增和修改文件火文件夹名称使用 -->
  <el-dialog
    v-model="fileDialogVisible"
    :title="dialogTitle"
    width="500"
    :before-close="handleClose"
    draggable
    class="rounded-dialog"
  >
    当前文件路径: {{ dialogPath }}
    <div style="display: flex; justify-content: center; margin-top: 10px">
      <el-input v-model="dialogData" placeholder="Please input" />
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="cancel">取消</el-button>
        <el-button type="primary" @click="confirm"> 确定 </el-button>
      </div>
    </template>
  </el-dialog>

  <el-dialog
    v-model="deleteDialogVisible"
    title="删除"
    width="500"
    draggable
    class="rounded-dialog"
  >
    你确定要删除该文件吗?
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="cancel">取消</el-button>
        <el-button type="primary" @click="deleteFileOrFolder"> 确定 </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script lang="ts" setup>
// TODO 确保文件地址的唯一性
import type Node from "element-plus/es/components/tree/src/model/node";
import type { DragEvents } from "element-plus/es/components/tree/src/model/useDragNode";
import type {
  AllowDropType,
  NodeDropType,
} from "element-plus/es/components/tree/src/tree.type";

const handleDragStart = (node: Node, ev: DragEvents) => {
  console.log("drag start", node);
};
const handleDragEnter = (
  draggingNode: Node,
  dropNode: Node,
  ev: DragEvents
) => {
  console.log("tree drag enter:", dropNode.label);
};
const handleDragLeave = (
  draggingNode: Node,
  dropNode: Node,
  ev: DragEvents
) => {
  console.log("tree drag leave:", dropNode.label);
};

const handleDragOver = (draggingNode: Node, dropNode: Node, ev: DragEvents) => {
  console.log("tree drag over:", dropNode.label);
};

const handleDragEnd = (
  draggingNode: Node,
  dropNode: Node,
  dropType: NodeDropType,
  ev: DragEvents
) => {
  console.log(
    "tree drag end:",
    draggingNode.data.label,
    dropNode && dropNode.label,
    dropType
  );
};
const handleDrop = (
  draggingNode: Node,
  dropNode: Node,
  dropType: NodeDropType,
  ev: DragEvents
) => {
  console.log("tree drop:", dropNode.label, dropType);
};
const allowDrop = (draggingNode: Node, dropNode: Node, type: AllowDropType) => {
  return dropNode.data.type === "folder";
};
const allowDrag = (draggingNode: Node) => {
  console.log("allowDrag");
  return !draggingNode.data.label.includes("Level three 3-1-1");
};

const dataSource = ref([
  {
    label: "Level one 1",
    type: "folder",
    path: "/Level one 1",
    children: [
      {
        label: "Level two 1-1",
        type: "folder",
        path: "/Level one 1/Level two 1-1",
        children: [
          {
            type: "file",
            path: "/Level one 1/Level three 1-1-1/Level three 1-1-1",
            label: "Level three 1-1-1",
          },
        ],
      },
    ],
  },
]);

const fileDialogVisible = ref(false);
const dialogTitle = ref(""); // 新增文件|文件夹,修改
const dialogData = ref("");
const dialogPath = ref("/");
const deleteDialogVisible = ref(false);
const fileOrFolderNode = ref();

const handleClose = (done: () => void) => {
  cancel();
  done();
};

/**
 * 取消
 */
const cancel = () => {
  fileDialogVisible.value = false;
};

/**
 * 确定
 */
const confirm = () => {
  console.log("confirm: ", fileOrFolderNode);
  if (dialogTitle.value === "新增文件" || dialogTitle.value === "新建文件夹") {
    let data = {
      label: dialogData.value,
      type: "",
      children: [],
      path: "/" + dialogData.value,
    };
    if (fileOrFolderNode.value) {
      data.path = fileOrFolderNode.value.data.path + "/" + dialogData.value;
    }
    if (dialogTitle.value === "新增文件") {
      data.type = "file";
    } else {
      data.type = "folder";
    }
    append(fileOrFolderNode.value, data);
  } else {
    // 修改处理
    let parent = null;
    let data = {
      label: dialogData.value,
      type: fileOrFolderNode.value.data.type,
      children: fileOrFolderNode.value.data.children,
      path: "/" + dialogData.value,
    };
    if (fileOrFolderNode.value.parent.level != 0) {
      parent = fileOrFolderNode.value.parent;
      data.path = parent.data.path + "/" + data.label;
    }
    updateTreeNode(parent, fileOrFolderNode.value.data, data);
  }
  fileDialogVisible.value = false;
};

const openInsertFile = (even, node) => {
  if (node) {
    dialogPath.value = node.data.path + "/";
  } else {
    dialogPath.value = "/";
  }
  dialogTitle.value = "新增文件";
  dialogData.value = "";
  fileOrFolderNode.value = node;
  fileDialogVisible.value = true;
};

/**
 * 开始修改
 * @param even
 * @param node
 */
const openUpdate = (even, node) => {
  if (node) {
    dialogPath.value = node.data.path;
  } else {
    dialogPath.value = "/";
  }
  fileDialogVisible.value = true;
  fileOrFolderNode.value = node;
  dialogData.value = fileOrFolderNode.value.data.label;
  dialogTitle.value = "修改";
};

const openInsertFolder = (even, node) => {
  if (node) {
    dialogPath.value = node.data.path;
  } else {
    dialogPath.value = "/";
  }
  dialogData.value = "";
  fileOrFolderNode.value = node;
  dialogTitle.value = "新建文件夹";
  fileDialogVisible.value = true;
};

const openDelete = (even, node) => {
  fileOrFolderNode.value = node;
  deleteDialogVisible.value = true;
};

const deleteFileOrFolder = (even) => {
  // 删除该文件
  remove(fileOrFolderNode.value, fileOrFolderNode.value.data);
  deleteDialogVisible.value = false;
};

/**
 * 添加
 * @param node 父节点
 * @param data 要添加的数据
 */
const append = (node, data) => {
  if (isNameDuplicate(node, null, data)) {
    ElMessage.error("文件名重复");
    return;
  }
  const newChild = data;
  if (node) {
    if (!node.data.children) {
      node.data.children = [];
    }
    node.data.children.push(newChild);
  } else {
    dataSource.value.push(newChild);
  }
};

/**
 * 删除
 * @param node 节点
 * @param data 数据
 */
const remove = (node: Node, data) => {
  console.log("all data:", dataSource.value);
  const parent = node.parent;
  const children = parent.data.children || parent.data;
  const index = children.findIndex((d) => d.path === data.path);
  children.splice(index, 1);
  dataSource.value = [...dataSource.value];
};

const updateTreeNode = (parentNode, oldData, newData) => {
  console.log(
    "parentNode:",
    parentNode,
    "oldData:",
    oldData,
    "newData:",
    newData
  );
  if (isNameDuplicate(parentNode, oldData, newData)) {
    ElMessage.error("文件名重复");
    return;
  }
  let index: number;
  if (parentNode && parentNode.data) {
    // 查找 newData.path 在 parentNode.data.children 中的索引
    index = parentNode.data.children.findIndex(
      (child) => child.path === oldData.path
    );
    // 如果找到索引,则更新该位置的数据
    if (index !== -1) {
      parentNode.data.children[index] = newData;
    } else {
      console.error(
        "找不到, index:",
        index,
        "parentNode.data",
        parentNode.data,
        "newData",
        newData
      );
    }
  } else {
    index = dataSource.value.findIndex((item) => item.path === oldData.path);
    // 如果找到索引,则更新该位置的数据
    if (index !== -1) {
      dataSource.value[index] = newData;
    } else {
      console.error(
        "找不到, index:",
        index,
        "parentNode.data",
        parentNode.data,
        "newData",
        newData
      );
    }
  }

  console.log(dataSource.value);
};

/**
 * 判断名称是否有相同的
 */
const isNameDuplicate = (parentNode, oldData, newData) => {
  console.log(
    "parentNode:",
    parentNode,
    "oldData:",
    oldData,
    "newData: ",
    newData
  );
  if (oldData && oldData.label === newData.label) return false;
  if (parentNode) {
    for (let i = 0; i < parentNode.data.children.length; i++) {
      const child = parentNode.data.children[i];
      if (child.label === newData.label) {
        return true;
      }
    }
  } else {
    for (let i = 0; i < dataSource.value.length; i++) {
      const child = dataSource.value[i];
      if (child.label === newData.label) {
        return true;
      }
    }
  }

  return false;
};

/**
 * 文件上传操作
 */
const uploadFileList = ref([]);

const openUploadFile = (even, node) => {
  if (node) {
    dialogPath.value = node.data.path;
  } else {
    dialogPath.value = "/";
  }
  fileOrFolderNode.value = node;
};

const uploadFileChange = (uploadFile, uploadFiles) => {
  let data = {
    label: uploadFile.name,
    type: "file",
    path: "/" + uploadFile.name,
    uid: uploadFile.uid,
  };
  if (fileOrFolderNode.value) {
    data.path = fileOrFolderNode.value.data.path + "/" + uploadFile.name;
  }
  append(fileOrFolderNode.value, data);
};
function flattenTree(trees) {
  const result = [];

  function traverse(node) {
    // 添加当前节点到结果数组
    result.push({
      label: node.label,
      type: node.type,
      path: node.path,
    });

    // 如果节点有children,则递归遍历它们
    if (Array.isArray(node.children)) {
      node.children.forEach(traverse);
    }
  }

  // 遍历输入的树数组
  trees.forEach((tree) => {
    traverse(tree); // 从每个树的根节点开始遍历
  });

  // 返回结果数组
  return result;
}

console.log("dataSource.value:", dataSource.value);
console.log("result:", flattenTree(dataSource.value));
</script>

<style>
.rounded-dialog {
  border-radius: 10px;
}

.but-list > div {
  margin: 2px;
}
</style>

封装成组件,直接拿出来用就行,没有配后端调用API

### 使用 Vue3Element Plus 构建后台管理系统 #### 项目初始化与环境搭建 为了创建一个基于 Vue3Element Plus 的后台管理系统,首先需要安装必要的依赖项并设置开发环境。这可以通过 Vue CLI 或 Vite 来快速完成。 ```bash npm init vite@latest my-vue-app --template vue cd my-vue-app npm install element-plus pinia axios ``` 上述命令会建立一个新的 Vue 应用程序,并安装 `element-plus` 组件库以及用于状态管理和 HTTP 请求的工具包[^2]。 #### 引入 Element Plus 及配置 为了让整个应用程序能够访问到 Element Plus 提供的各种 UI 控件,在项目的入口文件 main.js 中全局注册该插件: ```javascript import { createApp } from &#39;vue&#39; import App from &#39;./App.vue&#39; import ElementPlus from &#39;element-plus&#39; import &#39;element-plus/dist/index.css&#39; const app = createApp(App) app.use(ElementPlus) app.mount(&#39;#app&#39;) ``` 这样就完成了对 Element Plus 的基本集成工作。 #### 创建布局组件 Layouts 一个好的后台管理平台通常会有固定的顶部导航栏、侧边菜单区和主要内容区域。可以新建几个基础性的 layout 文件夹来组织这些部分的内容结构。 - **Header.vue**: 负责显示网站Logo及用户信息等; - **Sidebar.vue**: 展现左侧的功能列表链接; - **MainContent.vue**: 动态渲染各个页面视图的位置; 通过组合以上三个子组件形成完整的页面框架[^3]。 #### 实现 Tab 页面切换功能 针对多标签页浏览的需求场景,可以在路由守卫里监听路径变化事件,从而控制是否重新加载当前激活选项卡的数据还是仅更新 URL 地址而不触发服务端交互行为。 ```javascript // router/index.js router.beforeEach((to, from, next) => { const toFullPath = to.fullPath; let openTabs = JSON.parse(sessionStorage.getItem(&#39;openTabs&#39;)) || []; if (!openTabs.find(item => item.path === toFullPath)) { openTabs.push({ title: to.meta.title, path: toFullPath }); sessionStorage.setItem(&#39;openTabs&#39;, JSON.stringify(openTabs)); } store.commit(&#39;SET_CURRENT_TAB&#39;, toFullPath); next(); }); ``` 这段逻辑实现了当用户点击不同的菜单项时自动保存已打开过的标签记录至浏览器本地存储中,并同步更新选中的Tab索引位置。 #### 数据缓存机制 为了避免频繁发起网络请求造成资源浪费,对于那些不会经常变动的信息(如字典表),可以选择将其暂存在内存变量或是 Local Storage 内部一段时间内重复利用。 ```javascript async function fetchDictionaryData() { try { const cachedData = localStorage.getItem(&#39;dictionary&#39;); if (cachedData && Date.now() - parseInt(localStorage.getItem(&#39;cacheTime&#39;), 10) < CACHE_EXPIRATION_TIME) { return JSON.parse(cachedData); } const response = await api.get(&#39;/api/dictionary&#39;); localStorage.setItem(&#39;dictionary&#39;, JSON.stringify(response.data)); localStorage.setItem(&#39;cacheTime&#39;, String(Date.now())); return response.data; } catch(error){ console.error("Failed to load dictionary data", error); } } ``` 这里展示了如何判断是否存在有效期内的有效副本,若有则直接返回之;反之才向服务器获取最新的版本并写回持久化层以便下次调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈Demo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值