表格实现嵌套文件夹功能

324 阅读7分钟

功能描述

在使用 el-table 展示数据时,有时候会遇到多级目录的情况,我们可以使用树形结构跟懒加载。但是如果子级目录数据比较多的话也可能会造成页面卡顿。这时我们可以实现类似文件夹的点击进入子文件夹的功能。

功能实现前提

  • 所有目录(包括子目录)的数据通过请求获取;
  • 后端返回的目录数据按照分页返回;
  • 可以获取到子目录的完整路径,即包含上一级目录路径。可以由后端拼接后返回,也可前端拼接;

涉及到的事件

涉及表格数据更新的事件:

  • 点击目录行更新 rowClick
  • 点击返回上一级更新 rowClick
  • 页面初始化更新 searchTableData
  • 目录查询更新 searchTableData
  • 点击分页更新 handleCurrentChangehandleSizeChange

涉及到的变量

  • 表格数据:tableData
  • 当前目录(默认目录):curSearchPath
  • 上级目录:lastPath
  • 分页相关变量:pageData

核心功能点

1. 点击进入下一目录

获取点击目录路径,发送请求刷新表格数据。

2. 返回上一目录

进入目录时获取到点击目录路径,通过截取获取到上一级目录路径,发送请求刷新数据;

3. 目录返回行的显隐

子目录最上面一行为返回上级目录行,但是在最外层目录不显示。

实现思路一: 定义一个全局的嵌套层级的计数变量,每次点击进入时++,点击返回时--,然后在点击行的事件函数中通过变量的值来判断属于第几级嵌套,如果是第一级则去掉返回行数据。

思路一存在一个bug,就是如果表格有查询功能,那么当点击目录后再进行查询,这时再点击目录就会出现问题,因为查询并没有清除原本的计数,我们不能在查询的时候清除变量,因为子目录也是可以查询的,当然,可以通过获取到查询目录的路径来处理计数变量。但是通过点击来增减计数变量的思路不符合逻辑,而且当分页时会出现问题。

实现思路二: 直接通过点击目录的路径来判断嵌套级数。

4. 实时显示当前目录

定义一个全局变量curSearchPath,点击目录时给它赋值。

ps:如果支持目录查询功能,可以直接在输入框中回显,也便于后续查询功能实现。

5. 目录支持查询

可以通过输入目录路径来进行查询,如果查询的目录为子目录,显示返回行并支持返回上一级。

6. 支持分页显示:要求如下

  • 分页要在不影响以上功能前提下实现;

  • 子目录也可分页,且无论上级目录在第几页,子目录统一展示第一页;

  • 返回上级目录时无论上级目录在第几页,直接返回第一页;

  • 如果想要实现记住上一级目录所在页码,需要定义一个数组,每次点击时把当前目录所在页码保存到数组中,索引值为当前目录级数,每次点击返回时根据当前目录级数从数组中取出对应页码;如果默认目录不是根目录,需要修改索引对应的逻辑;

  • 查询目录时默认查询第一页;

7. 页面加载时默认目录不是根目录时(非 / )

默认目录不是根目录时按照当前逻辑是可以返回上一级目录的,如果不允许返回,那么就需要修改返回行显示的代码逻辑。

默认目录为根目录是一种特殊情况。

代码实现

页面展示

<div class="file_table">
    <el-container>
      <el-header></el-header>
      <el-container>
        <el-aside width="200px"></el-aside>
        <el-main>
          <el-form
            :inline="true"
            :model="queryParams"
            class="demo-form-inline"
            size="mini"
          >
            <el-form-item label="路径">
              <!-- 当前选中目录 -->
              <el-input v-model="curSearchPath" placeholder="请输入路径"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="searchTableData"
                >查询</el-button
              >
            </el-form-item>
          </el-form>
          <el-table :data="tableData" style="width: 100%" @row-click="rowClick">
            <el-table-column prop="name" label="文件" width="180">
            </el-table-column>
            <el-table-column prop="address" label="地址"> </el-table-column>
          </el-table>
          <el-pagination
            :current-page.sync="pageData.curPage"
            :page-size.sync="pageData.pageSize"
            :total="pageData.total"
            :page-sizes="[10, 20, 30, 40]"
            @size-change="handleSizeChange"
            @current-change="handleCurrentChange"
            background
            layout="total, sizes, prev, pager, next, jumper"
          >
          </el-pagination>
        </el-main>
      </el-container>
    </el-container>
  </div>

功能实现

export default {
  mounted() {
    this.init()
  },
  data() {
    return {
      defaultPath: "/",
      // defaultPath: "/Program Files",
      curSearchPath: '',
      lastPath: '',
      tableData: [],
      pageData: {
        curPage: 1,
        pageSize: 10,
        total: 0
      },
    }
  },
  methods: {
    // 页面初始化方法
    init() {
      this.curSearchPath = this.defaultPath;
      this.searchTableData()
    },
    
    // 页面初始化更新/目录查询
    searchTableData() {
      this.pageData.curPage = 1;
      const path = this.curSearchPath || "/";
      this.updateTableData(path);
    }

    // 更新表格数据的方法
    updateTableData(path) {
      // 表格数据初始化
      this.tableData = []
      // 处理返回行的显示
      this.handleBackRowShow(path);
      // 回显当前目录
      this.curSearchPath = path

      // 获取新的表格数据
      const params = {
        path,
        pageNum: this.pageData.curPage,
        pageSize: this.pageData.pageSize
      };
      this.getTableDataAxios(params)
        .then((res) => {
          // 如果后端没有返回子目录的路径,需要前端拼接后再返回数据
          // res.map((i) => {
          //   const absolutePath = (path === '/' ? '' : path) + '/' + i.name
          //   i.path = absolutePath
          //   this.tableData.push(i)
          // })
          
          // 如果后端返回了子目录的路径直接返回数据
          this.tableData.push(...res.data)
          
          this.pageData.total = res.total;
        })
        .finally(() => {
          // 获取并保存上一级目录
          const pathArr = path.split("/");
          pathArr.splice(-1, 1)
          this.lastPath = pathArr.join('/') || '/'
        })
    },
    
    // 判断返回行显示与否的方法
    handleBackRowShow(path) {
      const pathArr = path.split("/");
      let pathCount = pathArr.length - 1;
      
      const defaultPathArr = this.defaultPath.split("/");
      let defaultPathCount = defaultPathArr.length - 1;

      // 最外层需要特殊处理一下
      if (path === "/") {
        pathCount = 0;
      }
      // 默认路径为根目录时需要处理一下
      if (this.defaultPath === "/") {
        defaultPathCount = 0;
      }
      
      // if (pathCount > 0) {
      if (pathCount > defaultPathCount) {
        this.tableData = [
          {
            name: "[上级目录]",
            path: "/back",
            goBack: true,
          },
        ];
      }
    },
    
    // 点击目录/点击返回上级目录
    rowClick(row) {
      // 判断哪些情况下不允许点击进入下一级
      if (false) return

      const path = row.goBack ? this.lastPath : row.path;
      this.pageData.curPage = 1;
      this.updateTableData(path);
    },
    
    // 页码改变
    handleCurrentChange() {
      const path = this.curSearchPath || "/";
      this.updateTableData(path)
    },
    
    // 每页数量改变
    handleSizeChange() {
      const path = this.curSearchPath || "/";
      this.updateTableData(path)
    }
  }
}

代码分析

  • 当前功能最重要的就是 表格数据的更新 方法,不论是点击进入子目录,还是点击返回上级目录,亦或是根据路径查询以及分页展示,都会触发表格数据的更新,只是触发更新时获取数据的参数不同而已,所以我们将获取表格数据的逻辑封装成一个方法,在其他功能逻辑中直接调用即可。
   // 更新表格数据的方法
    updateTableData(path) {
      // 表格数据初始化
      this.tableData = []
      // 处理返回行的显示
      this.handleBackRowShow(path);
      // 回显当前目录
      this.curSearchPath = path

      // 获取新的表格数据
      const params = {
        path,
        pageNum: this.pageData.curPage,
        pageSize: this.pageData.pageSize
      };
      this.getTableDataAxios(params)
        .then((res) => {
          // 如果后端没有返回子目录的路径,需要前端拼接后再返回数据
          // res.map((i) => {
          //   const absolutePath = (path === '/' ? '' : path) + '/' + i.name
          //   i.path = absolutePath
          //   this.tableData.push(i)
          // })
          
          // 如果后端返回了子目录的路径直接返回数据
          this.tableData.push(...res.data)
          // 设置分页显示
          this.pageData.total = res.total;
        })
        .finally(() => {
          // 获取并保存上一级目录
          const pathArr = path.split("/");
          pathArr.splice(-1, 1)
          this.lastPath = pathArr.join('/') || '/'
        })
    }
  • 页面初始化时获取数据,跟使用路径查询数据调用的方法相同,只是页面初始化时查询的路径为根路径 '/'
// 页面初始化时获取默认的表格数据
mounted() {
  this.searchTableData()
},

// 页面初始化更新/目录查询
searchTableData() {
  this.pageData.curPage = 1;
  const path = this.curSearchPath || "/";
  this.updateTableData(path);
}
  • 点击某一行进入下一目录及点击返回行返回上一目录只是请求的路径不同,也是调用相同的方法。
rowClick(row) {
  // 判断哪些情况下不允许点击进入下一级
  if (false) return;

  const path = row.goBack ? this.lastPath : row.path;
  this.pageData.curPage = 1;
  this.updateTableData(path);
},
  • 我们将目录返回行显示的处理逻辑单独封装成一个函数,这样在默认目录不同的情况下只需要修改这一块的逻辑代码即可,其他的地方不需要修改。
    // 判断返回行显示与否的方法
    handleBackRowShow(path) {
      const pathArr = path.split("/");
      let pathCount = pathArr.length - 1;
      
      const defaultPathArr = this.defaultPath.split("/");
      let defaultPathCount = defaultPathArr.length - 1;

      // 最外层需要特殊处理一下
      if (path === "/") {
        pathCount = 0;
      }
      // 默认路径为根目录时需要处理一下
      if (this.defaultPath === "/") {
        defaultPathCount = 0;
      }
      
      // if (pathCount > 0) {
      if (pathCount > defaultPathCount) {
        this.tableData = [
          {
            name: "[上级目录]",
            path: "/back",
            goBack: true,
          },
        ];
      }
    },
  • 在点击进入下一目录及返回上一目录时都需要用到当前目录的完整路径,如果后端没有返回,我们可以在前端进行拼接,这是因为后端肯定会返回目录的名称,而在文件夹中文件的名称就是路径的一部分,只要拼接上上一级的路径即可。
     this.getTableDataAxios(params)
        .then((res) => {
          // 如果后端没有返回子目录的路径,需要前端拼接后再返回数据
          res.map((i) => {
            const absolutePath = (path === '/' ? '' : path) + '/' + i.name
            i.path = absolutePath
            this.tableData.push(i)
          })
        })