最近有一个业务,需要自定义一个树状结构,并实现一系列的操作
实现效果


需要对查询结果展开对应项,加载数据并展开子项,然后再获取子项,然后展开
实现代码
html
<template>
<section :style="{ height: `${tableHeight - 50}px` }" class="tree-section">
<el-tree
:key="treeKey"
:load="loadNode"
lazy
ref="treeRef"
class="overflow-auto p-[5px] flex-1 no-scrollbar"
:data="treeData"
:props="defaultProps"
highlight-current
show-checkbox
node-key="id"
:default-expanded-keys="defaultExpandedKeys"
@node-contextmenu="onContextMenu"
@node-click="handleNodeClick"
@check="handleTreeCheck"
/>
<div class="search-result" v-if="searchResult.length">
<div class="close-with-icon">
<div class="result-text">搜索结果</div>
<el-icon @click="searchResult = []"><CircleCloseFilled /></el-icon>
</div>
<ul class="search-result_list">
<li v-for="item in searchResult" :key="item.id" @click="handleSearchResultClick(item)">
{{ item.bdzName }}--{{ item.xlName }}
</li>
</ul>
</div>
</section>
</template>
<script setup lang="ts">
import type Node from 'element-plus/es/components/tree/src/model/node'
import { Http } from '@/utils/http'
import { useApi } from '../../api'
import { useUserStore } from '@/store'
import { dayjs } from 'element-plus'
const { stationLineTree, lineNameSearch, generatrixAllocation } = useApi(Http)
type IEmit = {
(e: 'titleClick'): any
}
const emit = defineEmits<IEmit>()
const { user } = useUserStore()
// 是否管理员
const isAdmin = user.role == 2
const treeRef = ref<InstanceType<typeof ElTree>>()
const queryParams = reactive({
/**线路名称 */
xlName: '',
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
})
const show = ref(false)
//For component
const optionsComponent = reactive({
theme: 'mac',
zIndex: 3,
minWidth: 150,
x: 0,
y: 0,
})
function onContextMenu(e: MouseEvent, node: any) {
e.preventDefault()
if (!node.isLeaf || !isAdmin || !treeCheck.value.length) return
optionsComponent.x = e.clientX
optionsComponent.y = e.clientY
show.value = true
}
function onContextMenuClick(type: string) {
// generatrix 母线 0未分配,1一段,2二段,3三段
let generatrix
switch (type) {
case '添加I段':
generatrix = 1
break
case '添加II段':
generatrix = 2
break
case '添加III段':
generatrix = 3
break
case '移除':
generatrix = 0
break
}
const data = {
unitCode: user.unitCode,
generatrix,
ids: treeCheck.value,
}
generatrixAllocation(data).then(() => {
ElMessage.success('操作成功')
show.value = false
treeKey.value += 1
getTreeList()
})
}
const handleNodeClick = () => {}
const treeCheck = ref<string[]>([])
const defaultExpandedKeys = ref<string[]>([])
/**
* 判断数组A的所有元素是否都存在于数组B中
* @param subset 待检查的数组 (例: [16, 17])
* @param fullset 目标数组 (例: [16, 17, 18])
* @returns 当且仅当subset每个元素都在fullset中存在时返回true
*/
const isAllElementsIncluded = <T>(subset: T[], fullset: T[]): boolean => {
// 使用 Set 提升包含判断性能
const fullsetSet = new Set(fullset)
// 双重验证:元素存在性 + 数量控制
return subset.length <= fullset.length && subset.every((item) => fullsetSet.has(item))
}
const handleTreeCheck = (node: any, detail: any) => {
// 判断是否选择的父节点
if (node.children?.length) {
const childNodesKey = node.children?.map((i: any) => i.id)
// 判断选择的节点是否包含子节点&选择的节点有长度(存在)
if (isAllElementsIncluded(treeCheck.value, childNodesKey) && treeCheck.value.length) {
treeCheck.value = []
treeRef.value?.setCheckedKeys([])
} else {
treeCheck.value = childNodesKey
treeRef.value?.setCheckedKeys(childNodesKey)
}
return
}
const { checkedKeys } = detail
// 1. 获取当前节点实例
const currentNode = treeRef.value?.getNode(node)
// 2. 获取父节点实例
const parentNode = currentNode?.parent
// 3. 判断是否都在同一级
const parentKeys = parentNode?.childNodes.map((i: any) => i.data.id)
treeCheck.value = (checkedKeys as any[]).filter(Boolean)
if (!isAllElementsIncluded(treeCheck.value, parentKeys as any[])) {
treeRef.value?.setCheckedKeys([node.id])
treeCheck.value = [node.id]
}
}
/**组件key,为了更新 */
const treeKey = ref(0)
// 树形数据
const treeData = ref<any[]>([
{
id: '000',
nodeCode: '000',
nodeName: 'xxx公司',
disabled: true,
children: [],
},
])
type ISearchResult = {
id: number
bdzName: string
bdzCode: string
xlName: string
xlCode: string
nodeType: string
/**
* generatrix 母线 0未分配,1一段,2二段,3三段
*/
generatrix: '0' | '1' | '2' | '3'
unitName: string
unitCode: string
}
const searchResult = ref<ISearchResult[]>([])
const defaultProps = {
children: 'children',
label: 'nodeCode',
isLeaf: 'isLeaf',
}
const refresh = () => {
queryParams.time = dayjs().format('YYYY-MM-DD HH:mm:ss')
}
const getSearchResult = () => {
const data = {
unitCode: user.unitCode,
xlName: queryParams.xlName,
}
lineNameSearch(data).then((res) => {
const { result } = res
searchResult.value = result
})
}
const handleSearchResultClick = (item: ISearchResult) => {
const { bdzCode, generatrix, xlCode } = item
// 点击的变电站
const clickedBdz = treeData.value[0].children.find((i: any) => i.nodeCode == bdzCode)
if (!clickedBdz) return
// 找到变电站
const bzdNode = treeRef.value?.getNode(clickedBdz.id)
if (!bzdNode) return
if (!bzdNode.expanded) {
// 展开变电站
bzdNode.expand()
const stopWatch = watch(
() => loadNodeLoading.value,
(loading) => {
if (!loading) {
if (bzdNode?.childNodes.length) {
// 找到对应母线
const generatrixNode = bzdNode.childNodes.find((i: any) => i.data.generatrix == generatrix) as Node
if (!generatrixNode.expanded) {
// 展开母线
generatrixNode.expand()
}
// 找到对应线路高亮显示
const xlNode = generatrixNode.childNodes.find((i: any) => i.data.xlCode == xlCode) as Node
if (xlNode) {
treeRef.value?.setCurrentKey(xlNode.data.id)
}
// stopWatch()
}
}
},
)
}
}
const getTreeList = () => {
const data = {
unitCode: user.unitCode,
nodeType: 'bdz',
}
treeData.value[0].children = []
stationLineTree(data).then((res) => {
treeData.value[0].children = res.result.children.map((i: any) => ({ ...i, disabled: true }))
nextTick().then(() => {
// 获取第一个节点的 key
const firstNode = treeData.value[0]
defaultExpandedKeys.value = [firstNode.id]
// 设置展开状态
})
})
}
interface Tree {
generatrix: '0' | '1' | '2' | '3'
nodeName: string
isLeaf?: boolean
disabled?: boolean
children: any[]
}
const loadNodeLoading = ref(false)
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
// todo
loadNodeLoading.value=true
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
loadNodeLoading.value=false
resolve()
}, 1000)
})
...
}
onMounted(() => {
getTreeList()
})
</script>
<style scoped lang="scss">
</style>
主要实现
typescript
// 用于保存当前活动的监听器
const activeStop = ref<() => void>()
const handleSearchResultClick = (item: ISearchResult) => {
// 清理之前的监听器
if (activeStop.value) {
activeStop.value() // 停止旧监听器
activeStop.value = undefined
}
const { bdzCode, generatrix, xlCode } = item
// 点击的变电站
const clickedBdz = treeData.value[0].children.find((i: any) => i.nodeCode == bdzCode)
if (!clickedBdz) return
// 找到变电站
const bzdNode = treeRef.value?.getNode(clickedBdz.id)
if (!bzdNode) return
if (!bzdNode.expanded) {
// 展开变电站
bzdNode.expand()
// 创建新监听器
activeStop.value = watch(
() => loadNodeLoading.value,
(loading) => {
if (!loading) {
if (bzdNode?.childNodes.length) {
// 找到对应母线
const generatrixNode = bzdNode.childNodes.find((i: any) => i.data.generatrix == generatrix) as Node
if (!generatrixNode.expanded) {
// 展开母线
generatrixNode.expand()
}
// 找到对应线路高亮显示
const xlNode = generatrixNode.childNodes.find((i: any) => i.data.xlCode == xlCode) as Node
if (xlNode) {
treeRef.value?.setCurrentKey(xlNode.data.id)
}
// 自动销毁监听器
activeStop.value?.() // 停止当前监听器
activeStop.value = undefined
}
}
},
)
}
}
通过监听方式实现判断是否加载完成,然后继续向下执行
typesript
ok!记录一下