需求:
这次要做的是一个类似百度网盘的文件管理器,因为处理的文件的太多太复杂,所以单一的文件上传组件不能满足对文件管理的要求,于是乎便诞生了这么一个需求。大概的功能就是,树形展示文件夹,并可无下限地创建子文件夹,新建根文件夹,文件夹内的文件上传,下载压缩所选文件,删除文件,文件夹路由,模糊搜索文件...而且这一切都是基于前端静态实现,后端只负责提交后的保存以及第一次进来时给我数据。
先上主体效果图:
下面我会根据功能分别描述实现过程
实现步骤
1.实现树形展示文件夹结构
一开始的做法是延续之前的递归组件的做法,最后也实现了需求的功能,结果pm来了一句要高亮点击的节点,想来想去也不知道怎么解决,因为递归组件间数据是独立的,所以可能出现两条甚至更多的高亮节点,后面只好老老实实用el-tree了。
一开始递归组件的做法,有人知道这种情况怎么实现高亮节点吗,一棵树只能有唯一一个高亮.
后面改成element组件实现方便很多,api都基本满足需求
因为要对节点名称前加入图标,并且展开后是打开状态,这里要用到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.实现文件夹路由
这里实现的思路是,根据点击节点,判断其有无父节点,如果有就往路由数组里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勾选的文件,生成对应的压缩包,如果勾选的是文件夹类型则保留文件夹结构及其内部对应的文件。 在我看来,这一部分功能留给后端实现更好,因为前端做压缩会带来兼容问题,还有跨域问题,没办法,懒的后端会让前端进步哈哈,只好自己动手做
效果图
实现步骤
实现这个功能需要用到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.实现右键菜单
这里是表格内部的行点击,刚好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块,通过display的block和none控制显示。
<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和空白处我也实现了右键菜单以实现不同需求
注意在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,感觉写博客的同时又把需求复习了一遍,对每一步的实现思路又更加清晰了