实现效果如图:
前言
假如我们的树形图含有大量的数据,一次全部加载出来会很慢,用户体验不好,我们要做数据的分级加载,也就是懒加载,但如果每级的数据量也很大,就需要每一级都做分页加载了。
思路
思路二是思路一的优化版,为可以分开阅读,两版内容部分文字会一致
思路一:在节点下方添加表示点击加载更多的icon
效果如图:
- 使用
el-tree
组件的懒加载lazy
,及加载子树数据的方法load
- 由于是树形结构,所以不适合使用
el-pagination
这个分页器组件,所以需要在每一页数据的最后一条下方加一个表示加载更多的图标,如果当前数据小于设定的每页数据返回数limit
则不需要加这个图标, - 所以每个
node
节点需要加一个变量:isShow
用于控制箭头⬇️的展示
//...以上省略了代码,主要是请求树形结构数据
let treeData = res.data || [];
treeData.forEach((item, index) => {
// 控制点击加载更多按钮是否显示
if (index == treeData.length - 1 && treeData.length == 20) {
item.isShow = true;
}
...
});
- 点击加载更多时,传入的参数需要根据后端要求,所以这里主要记录传入参数
page
、limit
,limit
可以限制死,比如20
,像page
的话,初始化可以为1
,后续的数值需要+1
,所以需要一个页数映射表pageMap
,可以new Map()
,简单的Map
大家肯定看得懂,粗略代码如下:
handleTreeParams(params) {
params = {
appUri: this.$route.query.appUri,
urlNode: params.urlNode,
urlPath: params.urlPath,
page: params.page,
limit: 20,
}
return params
}
pageMap: new Map(),
const urlNode = node.parent.data.urlNode;
const urlPath = node.parent.data.urlPath;
this.pageMap.set(`${urlNode}_${urlPath}`, this.pageMap.has(`${urlNode}_${urlPath}`) ? this.pageMap.get(`${urlNode}_${urlPath}`) + 1 : this.pageCount);
page: this.pageMap.get(`${urlNode}_${urlPath}`)
-
后一页数据返回后就需要将数据添加到上一页数据的下面了,从
elementUI
文档中可以知道有三种方式可以加节点:append
:为 Tree 中的一个节点追加一个子节点insertBefore
:为 Tree 的一个节点的前面增加一个节点insertAfter
:为 Tree 的一个节点的后面增加一个节点
-
这里我们需要用到
append
,它接受两个参数:(data, parentNode)
接收两个参数,1. 要追加的子节点的data
, 2. 子节点的parent
的data、key
或者node
-
将下一页请求到的数据使用
append
添加到节点后面
treeData.forEach((item) => {
if (node && node.parent) {
this.$refs.tree.append(item, node.parent.data);
}
});
- 需要注意,我们使用的是
load
方法,它传入两个参数node
和resolve
,我们在点击加载更多之后并不需要再执行resolve
这个方法,如再执行这个方法,会将下一页返回的数据以根节点的形式再次渲染在第一个根节点的下方,如下图所示:
- 所以需要在首次渲染完毕之后不再执行
resolve
,定义一个isGetMore
变量,用于控制是否点击加载更多
// 第一次加载
if (!isGetMore) {
resolve(treeData);
} else {
treeData.forEach((item) => {
if (node && node.parent) {
this.$refs.tree.append(item, node.parent.data);
}
});
}
思路二:在节点之后添加一个节点表示加载更多(思路一的优化版)
效果如图:
- 使用
el-tree
组件的懒加载lazy
,及加载子树数据的方法load
- 由于是树形结构,所以不适合使用
el-pagination
这个分页器组件,所以需要在每一页数据的最后一条节点下加一个加载更多
节点,如果当前数据小于设定的每页数据返回数limit
则不需要加这个节点, - 在每次返回的数据节点后面,添加一个自定义的
node
节点,这个自定义节点内容如下:- apiId、id,需要保证唯一性,因为在每次点击加载更多后,需要将被点击的这个加载更多节点进行删除,以保证树形结构数据的连续性
// 树中添加加载更多节点
addGetMoreNode(node, treeData) {
// handleNodePage是保存node页数的一个Map结构
if (!this.handleNodePage('has', node)) {
this.handleNodePage('set', node, 1);
}
treeData.push({
// apiId、id需要保证唯一性
apiId: `${node.data.urlPath}_loadMore_${this.handleNodePage(
'get',
node
)}`,
id: `${node.data.urlPath}_loadMore_${this.handleNodePage('get', node)}`,
isLeaf: true,
urlNode: 2,
urlCount: '',
urlPath: node.data.urlPath,
urlPathList: node.data.urlPathList,
isGetMore: true
});
},
isGetMore
为true
是必须的,为判断点击的节点是否是加载更多节点,以执行加载更多的方法
// 点击节点
handleNodeClick(data, Node) {
this.isAutoMode = false;
if (Node.isLeaf && !data.isGetMore) {
this.id = data.apiId;
}
if (data.isGetMore) {
// 执行加载更多的方法
this.getNodeMore(Node);
}
},
- 点击加载更多时,传入的参数需要根据后端要求,所以这里主要记录传入参数
page
、limit
,limit
可以限制死,比如20
,像page
的话,初始化可以为1
,后续的数值需要+1
,所以需要一个页数映射表pageMap
,可以new Map()
,简单的Map
大家肯定看得懂,粗略代码如下:
// 操作页数
handleNodePage(type, node, val = '') {
const urlNode = node.parent.data.urlNode;
const urlPath = node.parent.data.urlPath;
if (type === 'get') {
return this.pageMap.get(`${urlNode}_${urlPath}`);
} else if (type === 'set') {
return this.pageMap.set(`${urlNode}_${urlPath}`, val);
} else {
return this.pageMap.has(`${urlNode}_${urlPath}`);
}
},
-
后一页数据返回后就需要将数据添加到上一页数据的下面了,从
elementUI
文档中可以知道有三种方式可以加节点:append
:为 Tree 中的一个节点追加一个子节点insertBefore
:为 Tree 的一个节点的前面增加一个节点insertAfter
:为 Tree 的一个节点的后面增加一个节点
-
这里我们需要用到
append
,它接受两个参数:(data, parentNode)
接收两个参数,1. 要追加的子节点的data
, 2. 子节点的parent
的data、key
或者node
-
将下一页请求到的数据使用
append
添加到节点后面 -
在后一页数据返回后,我们也得将点击的
加载更多
节点进行删除,那可以使用remove
方法remove
:删除 Tree 中的一个节点,使用此方法必须设置 node-key 属性
-
需要注意,我们使用的是
load
方法,它传入两个参数node
和resolve
,我们在点击加载更多之后并不需要再执行resolve
这个方法,如再执行这个方法,会将下一页返回的数据以根节点的形式再次渲染在第一个根节点的下方,如下图所示:
- 所以需要在首次渲染完毕之后不再执行
resolve
,定义一个isGetMore
变量,用于控制是否点击加载更多
// 获取树形数据
async getTree(params, resolve, node, isGetMore = false) {
// 省略部分代码...
if (!isGetMore) {
resolve(treeData);
} else {
treeData.forEach((item) => {
if (node && node.parent) {
this.$refs.tree.append(item, node.parent.data);
}
});
node && this.$refs.tree.remove(node.data);
}
})
.catch((err) => {
this.apiDetail = {};
this.$message.error(err.msg);
});
},
实现
qz-pro-virtual-tree
是基于el-tree
封装的组件,使用el-tree
效果也是一样的
思路一主要代码:
文中一些未出现的方法,为实际项目中使用到的方法、参数,使用时替换为自己的即可
<div class="tree-container">
<qz-pro-virtual-tree
lazy
highlight-current
ref="tree"
class="tree"
node-key="apiId"
icon-class="expand-icon"
:indent="0"
:props="props"
:load="loadNode"
:default-expanded-keys="defaultExpandedKeys"
@node-click="handleNodeClick"
>
<div class="row-context" slot-scope="{ node, data }">
<qz-icon
class="icon-style icon-folder"
v-if="!node.isLeaf"
></qz-icon>
<span
class="method-style"
v-if="data.urlCount === 0"
:class="{
red: data.methods && data.methods[0] == 'POST',
green: data.methods && data.methods[0] == 'GET'
}"
>
【{{ data.methods && data.methods[0] }}】
</span>
<el-tooltip
class="item"
placement="top"
v-if="data.urlCount === 0"
>
<div slot="content">
<qz-copy btn-class="copy-btn">{{ data.url }}</qz-copy>
</div>
<span class="url">{{ data.label }}</span>
</el-tooltip>
<span v-else>
{{ data.label }}
</span>
<!-- 去拆分区合并按钮 -->
<div class="api-choice" v-if="data.urlCount === 0">
<span class="api-choice--right">
<el-tooltip
class="item"
effect="dark"
content="去拆分"
placement="top"
>
<qz-icon
class="icon-split color-main"
@click.native="goSplit(data.url)"
></qz-icon>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="去合并"
placement="top"
>
<qz-icon
@click.native="goMerge(data.url)"
class="icon-join color-main"
></qz-icon>
</el-tooltip>
</span>
</div>
<!-- 加载更多 -->
<div class="get-more">
<div class="get-more--icon" :ref="data.isShow ? 'more' : ''">
<el-tooltip
class="item"
effect="dark"
content="加载更多"
placement="top"
>
<qz-icon
@click.native="getNodeMore(node)"
class="icon-arrow-down-copy color-main"
v-if="data.isShow"
></qz-icon>
</el-tooltip>
</div>
</div>
</div>
</qz-pro-virtual-tree>
</div>
pageCount: 2,
pageMap: new Map(),
// 加载节点
loadNode(node, resolve) {
getAppUriData(this.$route.query.appUri).then((res) => {
// 渲染第一级
if (node.level === 0) {
this.getTree(
{ urlNode: 0, urlPath: res.data.app.host, page: 1 },
resolve,
node
);
this.node = node;
this.resolve = resolve;
}
if (node.isLeaf) {
return resolve([]);
}
// 处理其余节点
if (node.data) {
this.getTree(
{ urlNode: node.data.urlNode, urlPath: node.data.urlPath, page: 1 },
resolve
);
}
});
},
// 获取树形数据
getTree(params, resolve, node, isGetMore = false) {
Object.assign(this.condition, params);
let postParams = JSON.parse(JSON.stringify(this.condition));
postParams = this.handleTreeParams(postParams);
this.searchParams = deepCopy(postParams);
getAppStructureTreeData(postParams)
.then((res) => {
let activeIndex = undefined;
let treeData = res.data || [];
treeData.forEach((item, index) => {
// 控制点击加载更多按钮是否显示
if (index == treeData.length - 1 && treeData.length == 20) {
item.isShow = true;
}
item['_locateIndex'] = index;
item.isLeaf = item.urlCount == 0;
item.url = formatUri(item.apiUri);
item.apiId = item.apiId || item.urlPath;
if (item.isLeaf) {
item.label = item.urlPath;
} else {
item.label = `${item.urlPath}(${item.urlCount})`;
}
if (treeData.length > 0) {
activeIndex =
this.siblingType == 'prev'
? treeData.length - 1
: activeIndex || 0;
let defaultData = treeData[activeIndex];
if (!defaultData.isLeaf && this.isAutoMode) {
this.defaultExpandedKeys.push(defaultData.apiId);
} else if (defaultData.isLeaf && this.isAutoMode) {
this.$nextTick(() => {
this.id = defaultData.apiId;
});
}
}
if (treeData.length == 0) {
this.apiDetail = {};
}
});
// 第一次加载
if (!isGetMore) {
resolve(treeData);
} else {
treeData.forEach((item) => {
if (node && node.parent) {
this.$refs.tree.append(item, node.parent.data);
}
});
}
})
.catch((err) => {
this.apiDetail = {};
this.$message.error(err.msg);
});
},
// 点击加载更多
getNodeMore(node) {
this.$refs.more.style.display = 'none';
const urlNode = node.parent.data.urlNode;
const urlPath = node.parent.data.urlPath;
this.pageMap.set(
`${urlNode}_${urlPath}`,
this.pageMap.has(`${urlNode}_${urlPath}`)
? this.pageMap.get(`${urlNode}_${urlPath}`) + 1
: this.pageCount
);
this.getTree(
{
urlNode: urlNode,
urlPath: urlPath,
page: this.pageMap.get(`${urlNode}_${urlPath}`)
},
this.resolve,
node,
true
);
}
思路二主要代码:
文中一些未出现的方法,为实际项目中使用到的方法、参数,使用时替换为自己的即可
<div class="tree-container" v-loading="isLoading">
<qz-pro-virtual-tree
lazy
highlight-current
ref="tree"
class="tree"
node-key="apiId"
icon-class="expand-icon"
:indent="0"
:props="props"
:load="loadNode"
:default-expanded-keys="defaultExpandedKeys"
@node-click="handleNodeClick"
>
<div class="row-context" slot-scope="{ node, data }">
<qz-icon
class="icon-style icon-folder"
v-if="!node.isLeaf"
></qz-icon>
<span
class="method-style"
v-if="data.urlCount === 0"
:class="{
red: data.methods && data.methods[0] == 'POST',
green: data.methods && data.methods[0] == 'GET'
}"
>
【{{ data.methods && data.methods[0] }}】
</span>
<el-tooltip
class="item"
placement="top"
v-if="data.urlCount === 0"
>
<div slot="content">
<qz-copy btn-class="copy-btn">{{ data.url }}</qz-copy>
</div>
<span class="url">{{ data.label }}</span>
</el-tooltip>
<span
v-else
:class="data.label == '加载更多' ? 'get-more' : ''"
>
{{ data.label }}
<i
v-if="data.label == '加载更多'"
class="el-icon-d-arrow-left"
style="transform: rotate(-90deg)"
></i>
</span>
<!-- 去拆分区合并按钮 -->
<div class="api-choice" v-if="data.urlCount === 0">
<span class="api-choice--right">
<el-tooltip
class="item"
effect="dark"
content="去拆分"
placement="top"
>
<qz-icon
class="icon-split color-main"
@click.native="goSplit(data.url)"
></qz-icon>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
content="去合并"
placement="top"
>
<qz-icon
@click.native="goMerge(data.url)"
class="icon-join color-main"
></qz-icon>
</el-tooltip>
</span>
</div>
</div>
</qz-pro-virtual-tree>
</div>
// 点击加载更多
getNodeMore(node) {
let nodePage = this.handleNodePage('get', node);
let hasNodePage = this.handleNodePage('has', node);
this.handleNodePage(
'set',
node,
hasNodePage ? nodePage + 1 : this.pageCount
);
this.getTree(
{
urlNode: node.parent.data.urlNode,
urlPath: node.parent.data.urlPath,
page: this.handleNodePage('get', node),
urlPathList: node.data.urlPathList
},
this.resolve,
node,
true
);
},
// 树中添加加载更多节点
addGetMoreNode(node, treeData) {
if (!this.handleNodePage('has', node)) {
this.handleNodePage('set', node, 1);
}
treeData.push({
apiId: `${node.data.urlPath}_loadMore_${this.handleNodePage(
'get',
node
)}`,
id: `${node.data.urlPath}_loadMore_${this.handleNodePage('get', node)}`,
isLeaf: true,
urlNode: 2,
urlCount: '',
urlPath: node.data.urlPath,
urlPathList: node.data.urlPathList,
isGetMore: true
});
},
// 获取树形数据
async getTree(params, resolve, node, isGetMore = false) {
if (node.level === 0) {
this.isLoading = true;
}
Object.assign(this.condition, params);
let postParams = JSON.parse(JSON.stringify(this.condition));
postParams = this.handleTreeParams(postParams);
this.searchParams = postParams;
await getAppStructureTreeData(postParams)
.then((res) => {
let activeIndex = undefined;
let treeData = res.data || [];
if (treeData.length == 20) {
this.addGetMoreNode(node, treeData);
}
treeData.forEach((item, index) => {
item['_locateIndex'] = index;
item.isLeaf = item.urlCount == 0;
item.url = formatUri(item.apiUri);
item.apiId = item.apiId || item.urlPath;
if (item.isLeaf) {
item.label = item.isGetMore ? '加载更多' : item.urlPath;
} else {
item.label = `${item.urlPath}(${item.urlCount})`;
}
if (treeData.length > 0) {
activeIndex =
this.siblingType == 'prev'
? treeData.length - 1
: activeIndex || 0;
let defaultData = treeData[activeIndex];
if (!defaultData.isLeaf && this.isAutoMode) {
this.defaultExpandedKeys.push(defaultData.apiId);
} else if (defaultData.isLeaf && this.isAutoMode) {
this.$nextTick(() => {
this.id = defaultData.apiId;
});
}
}
if (treeData.length == 0) {
this.apiDetail = {};
}
});
if (!isGetMore) {
resolve(treeData);
} else {
treeData.forEach((item) => {
if (node && node.parent) {
this.$refs.tree.append(item, node.parent.data);
}
});
node && this.$refs.tree.remove(node.data);
}
})
.catch((err) => {
this.apiDetail = {};
this.$message.error(err.msg);
});
this.isLoading = false;
this.searchDisabled = false;
},
// 加载节点
loadNode(node, resolve) {
getAppUriData(this.$route.query.appUri).then((res) => {
// 渲染第一级
if (node.level === 0) {
this.getTree(
{
urlNode: 0,
urlPath: res.data.app.host,
parentPath: node.parent?.data?.urlPath || '',
page: 1,
urlPathList: []
},
resolve,
node
);
this.node = node;
this.resolve = resolve;
}
if (node.isLeaf) {
return resolve([]);
}
// 处理其余节点
if (node.level > 0) {
this.getTree(
{
urlNode: node.data.urlNode,
urlPath: node.data.urlPath,
parentPath: node.parent?.data?.urlPath || '',
page: 1,
urlPathList: node.data.urlPathList
},
resolve,
node
);
}
});
},
// 点击节点
handleNodeClick(data, Node) {
this.isAutoMode = false;
if (Node.isLeaf && !data.isGetMore) {
this.id = data.apiId;
}
if (data.isGetMore) {
this.getNodeMore(Node);
}
},
// 操作页数
handleNodePage(type, node, val = '') {
const urlNode = node.parent.data.urlNode;
const urlPath = node.parent.data.urlPath;
if (type === 'get') {
return this.pageMap.get(`${urlNode}_${urlPath}`);
} else if (type === 'set') {
return this.pageMap.set(`${urlNode}_${urlPath}`, val);
} else {
return this.pageMap.has(`${urlNode}_${urlPath}`);
}
},