【记录有趣的需求】之三.实现仿网盘文件管理器

1,145 阅读5分钟

需求:

这次要做的是一个类似百度网盘的文件管理器,因为处理的文件的太多太复杂,所以单一的文件上传组件不能满足对文件管理的要求,于是乎便诞生了这么一个需求。大概的功能就是,树形展示文件夹,并可无下限地创建子文件夹,新建根文件夹,文件夹内的文件上传,下载压缩所选文件,删除文件,文件夹路由,模糊搜索文件...而且这一切都是基于前端静态实现,后端只负责提交后的保存以及第一次进来时给我数据。

先上主体效果图:

image.png

下面我会根据功能分别描述实现过程

实现步骤

1.实现树形展示文件夹结构

一开始的做法是延续之前的递归组件的做法,最后也实现了需求的功能,结果pm来了一句要高亮点击的节点,想来想去也不知道怎么解决,因为递归组件间数据是独立的,所以可能出现两条甚至更多的高亮节点,后面只好老老实实用el-tree了。

一开始递归组件的做法,有人知道这种情况怎么实现高亮节点吗,一棵树只能有唯一一个高亮. image.png

后面改成element组件实现方便很多,api都基本满足需求

image.png

1651805025313.gif

因为要对节点名称前加入图标,并且展开后是打开状态,这里要用到slot-scope插入,并用v-if判断el-tree自带属性expanded来判断是使用闭合还是展开的文件夹图标。

另外这一块还有个需求是,在左侧文件夹目录里只显示文件夹类型的节点,所以这里需要两个树结构,左侧展示文件夹栏要把非文件夹类型过滤掉,点击文件夹时,根据原树形结构来查询。所以这里在刚进来时,用了前序遍历来筛选树形结构。用前序遍历的好处是 他能衍生出很多操作树形数据的骚操作

     this.showList = JSON.parse(JSON.stringify(this.originList))
      //前序遍历过滤树形结构
      const queue = [...this.showList]
      while (queue.length) {
        const folder = queue.shift()
        const data = folder.data
        if (data && data.length) {
          data.forEach((item) => {
            const index = data.findIndex((item) => item.fileType !== 'folder')
                if (index !== -1) data.splice(index, 1)
           })
              const length = data.length
              if (length) queue.push(...data)
          }
       }

2.实现文件夹路由

1651807154(1).png

这里实现的思路是,根据点击节点,判断其有无父节点,如果有就往路由数组里unshift一个对象,包含该节点名称以及id,因为路由也是设置为可点击的

  • 路由html
     <div class="bread-router">
        <el-breadcrumb separator-class="el-icon-arrow-right">
          <el-breadcrumb-item
            v-for="(item, index) in breadList"
            :key="index"
            style="cursor: pointer"
            @click.native="handleBreadClick(item.id)"
            ><a>{{ item.name }}</a></el-breadcrumb-item
          >
        </el-breadcrumb>
      </div>
  • 点击节点生成路由数组
// 树节点点击事件
    handleNodeClick(node, data, event) {
      debugger
      this.breadList = []
      let nodeData = data

      //记录面包屑路由
      while (nodeData.parent != null) {
        this.breadList.unshift({
          name: nodeData.data.fileValue,
          id: nodeData.data.id,
        })
        nodeData = nodeData.parent
      }
      //记录当前点击节点id
      this.currentId = node.id
      this.tableData = this.repalceTable()
    },
  • 点击文件夹对应路由
//面包屑点击事件
    handleBreadClick(id) {
      //根据点击节点 判断剔除哪些节点
      for (let i = this.breadList.length; i > 0; i--) {
        if (this.breadList[i - 1].id != id) {
          this.breadList.pop()
        } else {
          break
        }
      }
      //根据面包屑节点id,替换table
      this.currentId = id
      this.tableData = this.repalceTable()
    },

3.批量文件生成压缩包并下载

这里实现的是根据table勾选的文件,生成对应的压缩包,如果勾选的是文件夹类型则保留文件夹结构及其内部对应的文件。 在我看来,这一部分功能留给后端实现更好,因为前端做压缩会带来兼容问题,还有跨域问题,没办法,懒的后端会让前端进步哈哈,只好自己动手做

  • 效果图

image.png

  • 实现步骤

实现这个功能需要用到jszip的知识点,下面简单描述下常用的几个方法。

  • 创建一个JSZip实例
var zip = new JSZip()
  • 往压缩包添加文件
zip.file("Hello.txt", "Hello World\n");
  • 往压缩包新建文件夹
var fold = zip.folder("images");
  • 往新建文件夹里添加文件
fold.file("Hello.txt", "Hello World\n");
  • 生成压缩包
zip.generateAsync({type:"blob"})
    .then(function(content) {
        // 
    });

因为我是根据table勾选文件进行压缩的,所以要循环勾选文件,并根据文件类型决定是新建文件夹还是添加文件,如果涉及下一层,则要递归调用


    var isTopNode = true //定义变量判断是否第一层
    
    //循环勾选文件
    const downloadFn = (downloadList, f) => {
            downloadList.forEach((obj) => {
              obj.downCount++
              const that = this
              //压缩类型为文件夹时
              if (obj.fileType === 'folder') {
              
              //这里判断是否第一层的作用是,
              //第一层是根据jszip实例创建文件夹,其他是文件夹下创建文件夹
                if (isTopNode) var fold = zip.folder(obj.fileValue)
                else var fold = f.folder(obj.fileValue)

                //如果是非空文件夹,则需递归继续向下搜索子节点
                if (obj.data.length > 0) {
                  isTopNode = false
                  downloadFn(obj.data, fold)
                }
              }
              
              //压缩类型为其他类型的
              else {
                if (isTopNode) {
                  zip.file(obj.fileValue, that.urlToPromise(obj.fileUrl), { binary: true })
                } else {
                  f.file(obj.fileValue, that.urlToPromise(obj.fileUrl), { binary: true })
                }
              }
            })
          }

          downloadFn(this.multipleSelection, zip)

处理完成后生成压缩包并下载

       zip.generateAsync({ type: 'blob' }).then((blob) => {
            const ZipFileName = `${zipName}.zip`

            //IE 下载
            if (window.navigator.msSaveOrOpenBlob)
              return window.navigator.msSaveOrOpenBlob(blob, ZipFileName), (this.downloadMode = false)

            const a = document.createElement('a')
            a.href = URL.createObjectURL(blob)
            a.download = ZipFileName
            a.click()
            URL.revokeObjectURL(a.href)
            a.remove()

            // this.destroyHandler();
            this.downloadMode = false
          })

4.实现右键菜单

image.png

这里是表格内部的行点击,刚好element有相关的api:@row-contextmenu,同时可以通过回调参数获取对应的行属性。

rightClick(row, column, event) {
        this.editId = row.id
        let rowMenu = document.querySelector('#menu')
        //阻止元素发生默认的行为
        event.preventDefault()
        // 根据事件对象中鼠标点击的位置,进行定位
        rowMenu.style.left = event.clientX - 220 + 'px'
        rowMenu.style.top = event.clientY - 120 + 'px'
        // 改变自定义菜单的隐藏与显示
        rowMenu.style.display = 'block'
        rowMenu.style.zIndex = 1000
      
    },

这里右键点击显示的菜单其实就是一个div块,通过displayblocknone控制显示。

     <div id="menu" class="rowMenuDiv">
        <ul class="menuUl">
          <li v-for="(item, index) in menus" :key="index" @click.stop="infoClick(item.operType)">{{ item.name }}</li>
        </ul>
      </div>

另外在el-tree和空白处我也实现了右键菜单以实现不同需求

image.png

image.png

注意在div处右击时,要用.prevent阻止默认右菜单的出现,同时传入$event以获取原生DOM事件的事件对象,才能获取当前鼠标点击坐标。

<div id="operatePush" class="cas-div"  @contextmenu.prevent="casRightClick($event)">

5.实现文件模糊搜索

这里分为全局搜索以及当前所在文件夹下的搜索,这里还是用上文提到的前序遍历来查询树形结构,相比递归查询,他的性能会更佳,骚操作更多。

searchData() {
      if (this.queryWord === '') {
        this.tableData = []
        return
      }
      //queryArr存放找到的数据
      var queryArr = []
      //全局下
      if (this.currentId === 0) {
        const queue = [...this.newList]
        while (queue.length) {
          const folder = queue.shift()
          const data = folder.data
          if (folder.fileValue.indexOf(this.queryWord) !== -1) {
            queryArr.push(folder)
          }
          const length = data.length
          if (length) queue.push(...data)
        }
        if (queryArr.length) {
          this.tableData = []
          this.tableData = queryArr
        }
      } 
      //当前文件夹下
      else {
        //先用当前文件夹id去查询文件,再在这些文件中前序遍历
      }
    },

6.新建文件夹、上传文件和删除文件

这三个功能都是对数组的操作,如果是文件夹则对源数组和目录数组同时操作,如果是其他则只需添加到源数组。同时为了保持树结构,要对添加节点使用pid记录其上级节点的id。

小结

一个丐版仿网盘的文件管理器就这样实现啦,一开始看着貌似很难,其实只要把功能疏通一遍,一步一步实现 会发现其实不难的。 btw,感觉写博客的同时又把需求复习了一遍,对每一步的实现思路又更加清晰了