关于树级结构懒加载 + 精准定位高亮选中问题的总结
需求前提:高级检索中选中的机关数据需对应高亮,检索条件删除搜索的机关需取消高亮,且每次改变搜索条件时该机关结构数据按条件变化。
初次发版:一次加载所有发布机关数据,可根据el-tree
的current-node-key
属性进行默认值选中,highlight-current
属性高亮匹配上的id值
现存问题:机关类数据共上万条,一次加载接口时间长达半分钟
解决问题:改进为树级数据懒加载形式,并默认展开高亮对应所选
- 查找
el-plus
的文档发现,可以配置lazy :load
来进行数据懒加载,并在数据中添加leaf
来判断当前是否为子节点or有子节点的父节点,
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
if (!node.level) return
let curId = node.data.id ? node.data.id : ''
let nextLevel = node.data.nextLevel ? node.data.nextLevel : ''
httpRequest
.post({
url: props.state.url,
params: {
[props.state.key]: curId,
[props.state.keyLevel]: nextLevel
}
})
.then((res: any) => {
nextTick(() => {
defaultKey.value = props.state.defaultId
if (defaultKey.value === '') defaultKey.value = null
popularTree.value.setCurrentKey(defaultKey.value)
checkedArr.value = [defaultKey.value]
})
return resolve(res.data)
})
.catch((err) => {
console.log(err)
})
}
-
设置
:default-expanded-keys="expandArr" :default-checked-keys="checkedArr"
来解决默认展开及高亮搜索的对应id -
每次点击树节点更改搜索条件,都会重新调取接口,树级整体进行刷新,又回到了只有一级数据的情况,因此需要用递归来找到当前所选id的所有父节点id并push到
expandArr
中,记得每次调用接口时要把expandArr
清空
const handleNodeClick = (data: any, Node: any) => {
expandArr.value = []
function seekParent(o: any) {
o.parent.level > 0 && expandArr.value.push(o.parent.data.id)
o.parent.level > 1 && seekParent(o.parent)
}
seekParent(Node)
checkedArr.value = [data.id]
}
- 高级检索搜索及删除搜索条件时会修改树节点的
defaultId
,由于在首页/搜索页都有高级检索,跨页面的检索条件会在窗口新开时重置为空,因此选择将搜索获取到的parentArr
存储在sessionStorage
,在关闭高级检索框or删除搜索条件时removeItem
,在树级组件中监听defaultId
的变化,当sessionStorage
中有我们存储的父级数组时,则给expandArr
赋值,并setCurrentKey
,这里需要注意的是由于sessionStorage
只能存储字符串,因此需要在setItem
前将数组parentArr
转为字符串setItem('parentArr', JSON.strinity(parentArr))
,在我们需要getItem
时,再转为数组形式,JSON.parse(getItem('parentArr'))
完整代码如下:
/parentView
template:
<tree
ref="fbjgRef"
:state="fbjgState"
@change="fbjgState.handleItemClick"
></tree>
watch(
() => store.state.form,
(newV, oldV) => {
if (newV.institutionId && newV.institutionId !== oldV.institutionId)
return
;[fbjgState.defaultId, fbjgState.level] = [
store.state.form.institutionId,
store.state.form.institutionLevel
]
fbjgState.getData()
},
{
deep: true
}
)
// 发布机关
const fbjgRef = ref()
const fbjgState: any = reactive({
title: '发布机关',
key: 'institutionId',
keyLevel: 'institutionLevel',
data: [],
url: 'institutionList',
defaultId: store.state.form.institutionId,
level: store.state.form.institutionLevel,
parentId: '',
nextLevel: '',
loading: true,
getData() {
fbjgState.loading = true
const form = JSON.parse(JSON.stringify(store.state.form))
form.institutionId = ''
form.institutionLevel = ''
httpRequest
.post({
url: 'institutionList',
params: form
})
.then((res: any) => {
fbjgState.data = res.data
fbjgState.loading = false
})
.catch((err) => {
console.log(err)
})
},
handleItemClick(obj: any) {
const filterObj = JSON.parse(JSON.stringify(store.state.form))
filterObj.institutionId = fbjgState.defaultId = obj.id
filterObj.institutionLevel = fbjgState.level = obj.level
store.commit('setForm', filterObj)
// 此处提交文字到vuex数组中,用于检索条件展示
searchListAction(obj)
}
})
fbjgState.getData()
/tree.vue
<template>
<div class="tree" v-loading="state.loading">
<el-scrollbar height="100%">
<el-tree
ref="popularTree"
:data="state.data"
:props="defaultProps"
node-key="id"
lazy
:load="loadNode"
check-strictly
check-on-click-node
:expand-on-click-node="false"
:current-node-key="defaultKey"
:default-expanded-keys="expandArr"
:default-checked-keys="checkedArr"
highlight-current
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<div class="item-column">
<span
class="tree-label"
:style="{ width: widthCal(data.value) }"
:title="node.label"
>{{ node.label }}</span
>
<span class="tree-value">({{ data.value }})</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch, nextTick } from 'vue'
import httpRequest from '@/service'
import type Node from 'element-plus/es/components/tree/src/model/node'
interface Tree {
name: string
id: string
value: number
level: string
leaf?: boolean
children?: Tree[]
parentId?: string
nextLevel?: string
}
export default defineComponent({
name: 'tree',
props: {
state: {
type: Object,
default: () => {
return {}
}
}
},
emit: ['change'],
setup(props, { emit }) {
const expandArr = ref<string[]>([])
const checkedArr = ref<string[]>([])
const defaultProps = {
label: 'name',
value: 'value',
children: 'children',
isLeaf: 'leaf'
}
const defaultKey = ref(props.state.defaultId)
const popularTree = ref()
const widthCal = (e: any) => {
return ` cal(100%-${String(e).length}*8)`
}
watch(
() => [props.state.loading, props.state.defaultId],
() => {
if (props.state.key === 'institutionId') {
expandArr.value = JSON.parse(
sessionStorage.getItem('institutionParent') as string
)
}
nextTick(() => {
defaultKey.value = props.state.defaultId
if (defaultKey.value === '') defaultKey.value = null
popularTree.value.setCurrentKey(defaultKey.value)
checkedArr.value = [defaultKey.value]
})
}
)
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
if (!node.level) return
let curId = node.data.id ? node.data.id : ''
let nextLevel = node.data.nextLevel ? node.data.nextLevel : ''
httpRequest
.post({
url: props.state.url,
params: {
[props.state.key]: curId,
[props.state.keyLevel]: nextLevel
}
})
.then((res: any) => {
nextTick(() => {
defaultKey.value = props.state.defaultId
if (defaultKey.value === '') defaultKey.value = null
popularTree.value.setCurrentKey(defaultKey.value)
checkedArr.value = [defaultKey.value]
})
return resolve(res.data)
})
.catch((err) => {
console.log(err)
})
}
const handleNodeClick = (data: any, Node: any) => {
expandArr.value = []
const obj = {
id: data.id,
level: data.level,
parentId: data.parentId,
nextLevel: data.nextLevel,
type: props.state.key,
name: props.state.title + ':' + data.name
}
function seekParent(o: any) {
o.parent.level > 0 && expandArr.value.push(o.parent.data.id)
o.parent.level > 1 && seekParent(o.parent)
}
seekParent(Node)
if (props.state.key === 'institutionId') {
sessionStorage.setItem(
'institutionParent',
JSON.stringify(expandArr.value)
)
}
checkedArr.value = [data.id]
emit('change', obj)
}
return {
expandArr,
checkedArr,
popularTree,
defaultProps,
defaultKey,
handleNodeClick,
loadNode,
widthCal
}
}
})
</script>
<style lang="less" scoped>
.tree {
height: 100%;
padding: 0 10px;
overflow: auto;
user-select: none;
::v-deep(.el-tree-node.is-current > .el-tree-node__content) {
color: #4486e0;
}
::v-deep(.el-tree-node__content) {
height: auto;
}
::v-deep(.el-tree-node__label) {
font-size: 16px;
line-height: 36px;
}
::v-deep(.el-tree-node) {
font-size: 16px;
line-height: 36px;
}
&:deep(.expanded) {
width: 15px;
height: 15px;
color: transparent;
background: url('~@/assets/images/icon-close.png') no-repeat center !important;
}
&:deep(.expanded .el-tree-node__children .el-tree-node__expand-icon) {
background: url('~@/assets/images/icon-expand.png') no-repeat center !important;
}
&:deep(.el-tree-node__expand-icon) {
width: 15px;
height: 15px;
color: transparent;
background: url('~@/assets/images/icon-expand.png') no-repeat center;
}
&:deep(.el-tree-node__expand-icon.is-leaf) {
width: 8px;
height: 8px;
padding: 0;
margin-left: 10px;
margin-right: 10px;
background: #e8e8e8;
border-radius: 50%;
}
&:deep(.is-current.is-checked
.el-tree-node__children
.el-tree-node__expand-icon.is-leaf) {
background: #e8e8e8;
}
&:deep(.is-current.is-checked .el-tree-node__expand-icon.is-leaf) {
background: #4486e0;
}
.item-column {
width: 90%;
display: flex;
.tree-label {
display: block;
// max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>