前言
Hello 大家好,我是虚竹,前段时间忙着寻找大哥和三弟的踪迹,耽误了技术文章输出,趁中途栖息时间赶紧补上一篇水文。今天给各位看官讲一讲我的刨坑之旅,因为最近开发了一个项目业务上遇到一个感觉有点啪啪的复杂需求(目录树+复选框+分页+检索 )。多数大牛们看了这个功能需求,都会立马跳出来说,不难不难,这不就是开发一个网盘么,类似百度网盘那种。非也非也,先看看再说!
当时有两种选择方案要么用el-tree,要么用el-table,简单的过了一下这两个UI组件的用法就动手了,并根据实际业务需求,也为了偷个懒就采用ElementUI中的el-table,中途开发很顺利,做着做着却发现不对劲,于是乎推倒重来,我的天哪,程序猿就是这样磨练出来的,加油!通过深思熟虑研究一番后,业务组件最终选用el-tree和el-table两者结合使用,经过不断折腾,摸索多次尝试,已经可以满足大部分业务需求了。做出来后有的需求感觉不是很合理,就跟产品后端沟通一下做了一些业务实现上的调整。
应用场景
根据项目实际业务需求自己做了一个Demo简约版,演示地址👉:http://106.55.168.13:8083/
欢迎各位大佬一起讨论交流,多多给点建议或其他最优解决方案,也可提些BUG,提些各种奇葩的需求,看我是否能实现,哈哈。🤟
目前有个新需求想寻求大佬们指点迷津,是这样的总共分两级目录嵌套,一二级目录列表需分页展示含检索功能,并保证二级目录也可以分页(加载更多)。👋
功能需求描述
- 目录树结构列表显示(后端不支持分页)
- 目录树默认全部展开
- 处理时间日期JS库 Day.js
- 根据文件后缀显示不同图标
- 配置全局管道方法
- 文件大小单位换上
- 目录树复选框单选
- 全选反选
- 鼠标移入图标文件名提示
- 递归算法统计文件总数
- 快速检索目录或文件(后端不支持批量下载和分享)
- 批量下载、分享
- 单个下载、分享
此功能开发涉及到递归算法,我们一起先来简单普及一下什么是递归算法,他会出现在什么场景,能解决什么问题。
递归的概念
就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。
递归的步骤
- 假设递归函数已经写好
- 寻找递推关系
- 将递推关系的结构转换为递归体
- 将临界条件加入到递归体中
介绍递归的文章随处可见,小编推荐几篇看看:
代码实现
采用技术栈:Vue2.6 + Vue-router + ElementUI + Less + Flex布局
el-tree 组件
采用ElementUI中的el-tree组件进行二次开发,自定义做成表格形式。 特点是:非常清晰的层级结构嵌套展示信息,可选择、懒加载、可展开与折叠或可拖拽节点。比如选中某父节点会同时选中其下所有子节点,选中子节点同时会保留父节点被选中总的来说可以满足大部分业务需求。唯一不足的地方是el-tree不合适做分页结合关键词检索功能。
template代码如下:
<div class="tree-box">
<div class="tree-nav">
<div class="item">
<el-checkbox
v-model="isCheckedAll"
:indeterminate="isIndeterminate"
:disabled="treeData.length === 0"
class="checkbox-style"
@change="handleCheckAllChange"
>
</el-checkbox
>名称
</div>
<div class="item">大小</div>
<div class="item">修改时间</div>
<div class="item">上传时间</div>
<div class="item">加密级别</div>
<div class="item">下载级别</div>
<div class="item">操作</div>
</div>
<div v-loading="loading" class="tree-content">
<el-tree
ref="tree"
:data="treeData"
node-key="directoryId"
:props="props"
show-checkbox
default-expand-all
@check="handleCheckChange"
@check-change="handleCurChange"
>
<span slot-scope="{ node, data }" class="custom-tree-node">
<template>
<div v-if="data.directoryType === 1" class="node_div">
<span class="name-box">
<el-tooltip effect="dark" placement="left">
<div slot="content">
{{ node.label }}
</div>
<i class="file-icon icon-folder"></i>
</el-tooltip>
{{ node.label }}
</span>
</div>
<div v-if="data.directoryType === 2" class="node_div">
<span class="name-box" :title="node.label">
<el-tooltip effect="dark" placement="left">
<div slot="content">
{{ node.label }}
</div>
<i :class="node.label | getIcon"></i>
</el-tooltip>
{{ node.label }}
</span>
<span class="size-box">
{{ data.size | renderSize }}
</span>
<span class="time-box">
{{ $DayTime(data.gmtUpdate).format("YYYY-MM-DD HH:mm") }}
</span>
<span class="upload-box">
{{ $DayTime(data.gmtUpload).format("YYYY-MM-DD HH:mm") }}
</span>
<span class="secret-box">
{{ data.secretType | secretType }}
</span>
<span class="download-box">
{{ data.downloadType | downloadStatus }}
</span>
<span class="operate-box">
<el-button
v-if="data.downloadType === 1 && data.directoryType === 2"
type="text"
size="small"
@click="() => handleDownload(data, 2)"
>下载</el-button
>
<el-button
v-if="data.directoryType === 2"
type="text"
size="small"
@click="() => handleShare(data, 2)"
>分享</el-button
>
</span>
</div>
</template>
</span>
</el-tree>
</div>
</div>
data代码如下:
data() {
return {
isCheckedAll: false, // 是否全选状态
isIndeterminate: false, // 是否半选状态
isMultipleDownload: true, // 批量下载按钮是否禁用
isDownloadFile: true, // 选中的所有文件是否可下载
isDownloadFileBtn: true, // 文件是否可下载
newTreeArray: [], // 过滤保留被选中的新数组
totalNum: 0, // 统计文件总数
selectTotalNum: 0, // 选中文件总数
props: { // 配置选项
children: "children",
label: "name",
isLeaf: "leaf",
},
treeData: [ // 初始化目录树列表数据
{
directoryId: 1,
directoryType: 2, // 1:目录 2:文件
downloadType: 1,
secretType: 0,
size: 12367,
name: "前端大厂面试宝典.pdf",
gmtUpdate: 1630825270483,
gmtUpload: 1630825248029,
children: [],
},
{
directoryId: 2,
directoryType: 2,
downloadType: 1,
secretType: 1,
size: 5236700,
name: "前端高级工程师内功秘籍.docx",
gmtUpdate: 1630825270483,
gmtUpload: 1630825248029,
children: [],
},
{
directoryId: 3,
directoryType: 2,
downloadType: 0,
secretType: 1,
size: 2267,
name: "前端学习路线图.png",
gmtUpdate: 1630834889072,
gmtUpload: 1630825248029,
children: [],
},
{
directoryId: 4,
directoryType: 1,
downloadType: 1,
secretType: 0,
name: "前端开源项目汇总",
gmtUpdate: 1630825270483,
gmtUpload: 1630825248029,
children: [
{
directoryId: 41,
directoryType: 2,
downloadType: 1,
secretType: 0,
size: 13200,
name: "小程序个性简历源码.zip",
gmtUpdate: 1630825270483,
gmtUpload: 1630825248029,
children: [],
},
{
directoryId: 42,
directoryType: 1,
downloadType: 1,
secretType: 0,
name: "电商网站项目",
gmtUpdate: 1630825270483,
gmtUpload: 1630825248029,
children: [
{
directoryId: 421,
directoryType: 2,
downloadType: 1,
secretType: 0,
size: 132008,
name: "饿了么H5移动端源码.zip",
gmtUpdate: 1630825270483,
gmtUpload: 1630825248029,
children: [],
},
],
},
],
},
{
directoryId: 5,
directoryType: 1,
downloadType: 0,
secretType: 1,
name: "前端工程化知识体系",
gmtUpdate: 1630834889072,
gmtUpload: 1630834889072,
children: [
{
directoryId: 51,
directoryType: 2,
downloadType: 0,
secretType: 1,
size: 13200,
name: "CI/CD项目部署.doc",
gmtUpdate: 1630834889072,
gmtUpload: 1630834889072,
children: [],
},
{
directoryId: 52,
directoryType: 2,
downloadType: 0,
secretType: 1,
size: 335200,
name: "前端开发规范秘籍.xlsx",
gmtUpdate: 1630834889072,
gmtUpload: 1630834889072,
children: [],
},
],
},
]
}
}
script代码如下:
methods: {
async handleCheckAllChange(val) {
let tree = this.treeData;
this.isIndeterminate = false;
if (val) {
this.isMultipleDownload = tree[0].downloadType === 0;
this.isDownloadFileBtn = this.isMultipleDownload;
this.$refs.tree.setCheckedNodes(tree);
} else {
this.$refs.tree.setCheckedNodes([]);
this.isMultipleDownload = true;
this.isDownloadFile = true;
}
this.selectTotalNum = 0;
await this.getRecursion(tree);
this.newTreeArray = await this.getFilterFile(tree);
},
async handleCheckChange(data, node) {
let tree = this.treeData;
this.selectTotalNum = 0;
await this.getRecursion(tree);
this.isDownloadFileBtn = data.downloadType === 0 && data.isChecked;
this.newTreeArray = await this.getFilterFile(tree);
this.isCheckedAll = this.newTreeArray.length === tree.length;
this.isIndeterminate =
this.newTreeArray.length > 0 && this.newTreeArray.length < tree.length;
if (node.checkedNodes.length > 0) {
this.isMultipleShare = false;
this.isMultipleDownload = node.checkedNodes[0].downloadType === 0;
} else {
this.isCheckedAll = false;
this.isIndeterminate = false;
this.isMultipleDownload = true;
this.isDownloadFile = true;
this.newTreeArray = [];
}
},
handleCurChange(data, checked, indeterminate) {
let isChecked = checked;
let arr = [];
arr.push(data);
this.getCheckedChild(arr, [], isChecked, indeterminate);
},
async getCheckedChild(data, arr, flag, isParent) {
return data.map(async (item) => {
if (flag) {
item.isChecked = true;
} else {
item.isChecked = false;
}
if (isParent && item.directoryType === 1) {
item.isChecked = true;
}
if (item.children) {
await this.getCheckedChild(item.children, arr, flag, isParent);
}
return item;
});
}
async getRecursion(tree) {
this.$nextTick(async () => {
tree.map(async (item) => {
if (item.directoryType === 2 && item.isChecked) {
this.selectTotalNum += 1;
}
if (item.children) {
await this.getRecursion(item.children);
}
});
});
},
getFilterFile(tree) {
return tree
.filter((item) => item.isChecked === true)
.map((item) => {
item = Object.assign({}, item);
if (item.children) {
item.children = this.getFilterFile(item.children);
}
return item;
});
},
}
el-table 组件
采用ElementUI中的el-table组件进行二次开发实现目录树列表结构。
特点是:本身table表格列表样式呈现简约,支持分页检索功能,展示多条结构类似的数据,可对数据进行排序、筛选、对比或其他自定义操作。结合tree-props 属性配置选项,通过row-key 绑定数据唯一值变量directoryId ,很好的支持树类型的数据显示。比如table自带复选框全选反选,选中某父节点同时会选中所有子节点。唯一美中不足的地方是el-table选中子节点没法让父节点一起选中,因为找不到父节点层级嵌套太多就更没辙了,当时绞尽脑汁,怎么也想不出有效的解决方法,如有大佬知道的,请不吝赐教。
template代码如下:
<div class="tree-header">
<div class="tree-btn">
<el-button
type="primary"
size="small"
plain
:disabled="multiple"
@click="handleDownload(null, 1)"
>
批量下载</el-button
>
<el-button
type="primary"
size="small"
plain
:disabled="multiple"
@click="handleShare(null, 1)"
>批量分享</el-button
>
</div>
<div class="total-num">共 {{ totalNum }} 个文件</div>
</div>
<div class="tree-box">
<el-table
ref="table"
v-loading="loading"
:data="tableData"
class="w100"
row-key="directoryId"
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@select="selectRow"
@select-all="selectAll"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
:selectable="selectable"
width="45"
align="center"
label="全选"
></el-table-column>
<el-table-column label="名称" show-overflow-tooltip>
<template slot-scope="scope">
<el-tooltip effect="dark" placement="left">
<div slot="content">
{{ scope.row.name }}
</div>
<i
v-if="scope.row.directoryType === 1"
class="file-icon icon-folder"
></i>
<i v-else :class="scope.row.name | getIcon"></i>
</el-tooltip>
{{ scope.row.name }}
</template>
</el-table-column>
<el-table-column
prop="size"
label="大小"
align="center"
width="100"
show-overflow-tooltip
>
<template slot-scope="scope" v-if="scope.row.directoryType === 2">
<span> {{ scope.row.size | renderSize }} </span>
</template>
</el-table-column>
<el-table-column
prop="gmtUpdate"
label="修改时间"
align="center"
width="200"
show-overflow-tooltip
>
<template slot-scope="scope" v-if="scope.row.directoryType === 2">
<span>{{
scope.row.gmtUpdate
? $DayTime(scope.row.gmtUpdate).format("YYYY-MM-DD HH:mm")
: null
}}</span>
</template>
</el-table-column>
<el-table-column
label="上传时间"
align="center"
width="200"
show-overflow-tooltip
>
<template slot-scope="scope" v-if="scope.row.directoryType === 2">
<span>{{
$DayTime(scope.row.gmtUpload).format("YYYY-MM-DD HH:mm")
}}</span>
</template>
</el-table-column>
<el-table-column
prop="secretType"
label="加密级别"
align="center"
width="100"
>
<template slot-scope="scope" v-if="scope.row.directoryType === 2">
{{ scope.row.secretType | secretType }}
</template>
</el-table-column>
<el-table-column
prop="downloadType"
label="下载级别"
align="center"
width="100"
>
<template slot-scope="scope" v-if="scope.row.directoryType === 2">
{{ scope.row.downloadType | downloadStatus }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200">
<template slot-scope="scope">
<el-button
v-if="
scope.row.downloadType === 1 && scope.row.directoryType === 2
"
type="text"
size="small"
@click="() => handleDownload(scope.row, 2)"
>下载</el-button
>
<el-button
v-if="scope.row.directoryType === 2"
type="text"
size="small"
@click="() => handleShare(scope.row, 2)"
>分享</el-button
>
</template>
</el-table-column>
</el-table>
</div>
data代码如下:
data() {
return {
totalNum: 0, // 统计文件总数
selectTotalNum: 0, // 选中文件数
ids: [], // 选中数组
single: true, // 非单个禁用
multiple: true, // 非多个禁用
downloadTypeArr: [],
tableData: [], // 目录树结构同el-tree一样
}
}
script代码如下:
methods: {
selectAll() {
console.log("全选==", this.tableData);
this.selectTotalNum = 0;
this.getRecursion(this.tableData);
let data = this.tableData;
this.isAllSelect = !this.isAllSelect;
this.toggleSelect(data, this.isAllSelect, "all");
},
selectRow(selection, row) {
console.log("选择某行===", row);
this.selectTotalNum = 0;
this.getRecursion(this.tableData);
this.$set(row, "isChecked", !row.isChecked);
this.$nextTick(() => {
this.isAllSelect = row.isChecked;
this.toggleSelect(selection, row.isChecked, "tr");
this.toggleSelect(row, row.isChecked, "tr");
});
},
toggleSelection(row, flag) {
this.$set(row, "isChecked", flag);
this.$nextTick(() => {
if (flag) {
this.$refs.table.toggleRowSelection(row, flag);
} else {
this.$refs.table.clearSelection();
}
});
},
toggleSelect(data, flag, type) {
if (type === "all") {
if (data.length > 0) {
data.forEach((item) => {
this.toggleSelection(item, flag);
if (item.children && item.children.length > 0) {
this.toggleSelect(item.children, flag, type);
}
});
}
} else {
if (data.children && data.children.length > 0) {
data.children.forEach((item) => {
item.isChecked = !item.isChecked;
this.$refs.table.toggleRowSelection(item, flag);
this.toggleSelect(item, flag, type);
});
}
}
},
getTotalNum(tree) {
for (let item of tree) {
if (item.directoryType === 2) {
this.totalNum += 1;
}
if (item.children) {
this.getTotalNum(item.children);
}
}
},
getRecursion(tree) {
this.$nextTick(async () => {
tree.map(async (item) => {
if (item.directoryType === 2 && item.isChecked) {
this.selectTotalNum += 1;
}
if (item.children) {
this.getRecursion(item.children);
}
});
});
},
handleSelectionChange(selection) {
this.ids = selection.map((item) => item.directoryId);
this.downloadTypeArr = selection.map((item) => item.downloadType);
this.single = selection.length !== 1;
this.multiple = !selection.length;
},
}
结语
写到这里以上是我花了两个通宵实践之后,总结整理出来的一些实操笔记。感觉做完这个给我最大的收获,学会了递归的多种用法,以及树状结构的挖坑填坑,更多是提升了解决各种复杂业务需求的能力。为了想追求尽善尽美(前提不改需求不砍需求 ),特写了这篇水文,希望能收获更多大佬们的宝贵意见或建议。由于本人功力尚浅,还需闭关修炼。
?? 感谢支持
如果本文对你有一丢丢帮助,就点个赞支持下吧,你的「赞」是我创作的动力。
关注我的公众号【懒人码农】,获取更多项目实战经验及各种源码资源。如果你也一样对技术热爱并且为之着迷,欢迎加我的微信【lazycode520】,将会邀请你加入我们的前端实战学习群一起fix问题,一起面向快乐编程~ 🦄
|