背景
- 需要实现:选择省级地址时,回传节点为 [ 省级地址 id], 选择市级地址时,回传节点为 [ 省级地址 id,市级地址 id], 选择区县地址时,回传节点为 [ 省级地址 id,市级地址 id,区县地址 id]。
- 但 el-cascader-panel 双向绑定数据-已选区域,仅支持到最末级,即无论选择哪一级,回传节点均为[ 省级地址 id,市级地址 id,区县地址 id],与要求的数据结构不符。
实现功能点
- 省市区三级地址多选,实现:选择省级地址时,回传节点为 [ 省级地址 id], 选择市级地址时,回传节点为 [ 省级地址 id,市级地址 id], 选择区县地址时,回传节点为 [ 省级地址 id,市级地址 id,区县地址 id]。
- 过滤 不可选区域
- checkDisabled 方法
- 遍历树状结构areaData,为其每个节点添加全路径 path 属性(省级地址 path 为 [ 省级地址 id],市级地址 path 为 [ 省级地址 id,市级地址 id], 区县地址 path 为 [ 省级地址 id,市级地址 id,区县地址 id])
- 遍历过程中,刷选属性 disabled 为 true 的节点,将其 path 添加到 result 变量中,从而实现将该节点及其祖先节点均添加到 result 变量中
- 遍历后,对 result 数组元素进行去重,得到当前节点或子孙节点 disabled 为 true 且元素唯一的 disabledIds 数组
- 在handleChange 方法中,通过过滤已选节点(disabledIds 中不包含该节点 id)实现,过滤不可选区域
- checkDisabled 方法
- 兼容后端返回已选区域(已选区域前端回显,以及变更选择区域后后端 api 入参)
- initSelectedPaths 方法
- 遍历后端返回的已选区域数组tempSelectedAreas,结合 areaData,将其转为 el-cascader-panel 双向绑定数据-已选区域要求的数据结构,即节点结构均为 [省级地址 id,市级地址 id,区县地址 id],从而实现前端正确回显已选区域
- 获取到后端数据tempSelectedAreas 后,讲其浅拷贝给selectedAreas,避免当未进行任何操作直接点击保存时,selectedAreas 为空,丢失原有已选区域的问题
- initSelectedPaths 方法
遗留问题
- 树状结构层级硬编码:仅实现了树状结构层级不超过 3 级的场景,更多层级或者无限层级问题并未解决
- 实现逻辑复杂,过多数据遍历:核心方法findDisabledIds、initSelectedPaths、handleChange 均存在频繁 forEach 遍历数组等情况,但未找到解决办法。
如有更好解决办法,欢迎大佬赐教!!!
完整代码参考
模拟数据-树状结构
[{
"id": 1,
"name": "河南省",
"children": [{
"id": 4,
"name": "郑州市",
"children": [{
"id": 7,
"name": "高新区",
"children": []
},
{
"id": 8,
"name": "中原区",
"children": []
}
]
},{
"id": 5,
"name": "信阳市",
"children": [{
"id": 9,
"name": "息县",
"children": []
}
]
}]
},{
"id": 2,
"name": "河北省",
"children": [{
"id": 10,
"name": "石家庄市",
"children": [{
"id": 11,
"name": "石家庄市区",
"children": []
}
]
}
]
},{
"id": 3,
"name": "北京市",
"children": [{
"id": 12,
"name": "北京市",
"children": [{
"id": 13,
"name": "朝阳区",
"children": []
}
]
}
]
}]
主要实现逻辑
<template>
<div>
<el-cascader-panel
ref="cascaderPanel"
v-model="selectedPaths"
:options="areaData"
:props="cascaderProps"
@change="handleChange"
/>
</div>
</template>
<script lang="ts" setup>
import {onMounted, ref} from "vue";
import areaData from "../express/areaData.json"; // 假设这是您的树形数据
import type {CascaderNode, CascaderProps} from "element-plus";
// el-cascader-panelde v-model动态绑定对象
const selectedPaths = ref<number[][]>([]);
// 后端返回已选择的区域
const tempSelectedAreas = ref<number[][]>([])
// 选中的区域(包含已选和新选)
const selectedAreas = ref<number[][]>([])
const cascaderPanel = ref();
/** 配置Cascader属性 **/
const cascaderProps: CascaderProps = {
value: "id", // 值字段名
label: "name", // 显示字段名
children: "children",
disabled: "disabled",
multiple: true, // 启用多选
};
type TreeNode = {
id: number;
name: string;
children?: TreeNode[];
disabled?: boolean | string; // 假设 disabled 是字符串类型
path?: number[]
};
/**
* 查找树形结构中当前节点或子孙节点 disabled 为 true 的所有 id
* @returns 包含符合条件的 id 数组
*/
const findDisabledIds = (): number[] => {
let result:number[] = [];
const checkDisabled = (node: TreeNode) => {
if (node.disabled === "true" || node.disabled === true) {
result = [...result, ...node.path as number[]];
}
}
// 格式化areaData(为每个节点添加全路径path),取出disabled的节点(包含其父节点、祖先节点)
areaData.forEach((parentNode: TreeNode ) => {
parentNode.path = [parentNode.id]
checkDisabled(parentNode)
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.forEach((node: TreeNode) => {
node.path = [parentNode.id, node.id]
checkDisabled(node)
if (node.children && node.children.length > 0) {
node.children.forEach((childrenNode: TreeNode) => {
childrenNode.path = [parentNode.id, node.id, childrenNode.id];
checkDisabled(childrenNode)
})
}
})
}
})
const tempResult = new Set(result)
return Array.from(tempResult)
}
// 当前节点或子孙节点 disabled 为 true 的所有 id
const disabledIds = findDisabledIds()
// 此处仅为模拟,应该调用后端接口
const getTempSelectedAreas = () => {
tempSelectedAreas.value = [[1]]
initSelectedPaths()
selectedAreas.value = [...tempSelectedAreas.value]
}
// 根据后端返回已选择的区域,初始化el-cascader-panelde v-model动态绑定对象
const initSelectedPaths = () => {
if(!tempSelectedAreas.value || !tempSelectedAreas.value.length) return
tempSelectedAreas.value.forEach(area => {
const parentNode:TreeNode | undefined = areaData.find(item => item.id == area[0])
if(area.length == 1){
if(!parentNode) return
if(parentNode.children && parentNode.children.length > 0){
parentNode.children.forEach((node:TreeNode) => {
if(node.children && node.children.length > 0){
node.children.forEach((childrenNode: TreeNode) => {
selectedPaths.value.push(childrenNode.path as number[])
})
}else {
selectedPaths.value.push(node.path as number[])
}
})
}else {
selectedPaths.value.push(parentNode.path as number[])
}
}else if(area.length == 2){
const node:TreeNode | undefined = parentNode?.children?.find(item => item.id == area[1])
if(!node) return
if(node.children && node.children.length > 0){
node.children.forEach((childrenNode:TreeNode) => {
selectedPaths.value.push(childrenNode.path as number[])
})
}else {
selectedPaths.value.push(node.path as number[])
}
}else if(area.length == 3){
const node:TreeNode | undefined = parentNode?.children?.find(item => item.id == area[1])
const childrenNode:TreeNode | undefined = node?.children?.find(item => item.id == area[2])
if(!childrenNode) return
selectedPaths.value.push(childrenNode.path as number[])
}
})
}
/** 处理选中变化 **/
const handleChange = () => {
selectedAreas.value = []
const checkedNodes = cascaderPanel.value.getCheckedNodes().filter((node:CascaderNode) => !disabledIds.includes(node.value as number))
if (!checkedNodes || !checkedNodes.length) return
checkedNodes.forEach( (node:CascaderNode) => {
if(node.level == 1 ) {
selectedAreas.value.forEach((area, index) => {
if(area[0] == node.value){
selectedAreas.value.splice(index, 1)
}
})
selectedAreas.value.push(node.pathValues as number[])
}else if(node.level == 2){
selectedAreas.value.forEach((area, index) => {
if(area[1] == node.value){
selectedAreas.value.splice(index, 1)
}
})
if(selectedAreas.value.findIndex(item => item.length == 1 && item[0] == node.pathValues[0]) == -1) {
selectedAreas.value.push(node.pathValues as number[])
}
}
else if(node.level == 3){
selectedAreas.value.forEach((area, index) => {
if(area[2] == node.value){
selectedAreas.value.splice(index, 1)
}
})
if(selectedAreas.value.findIndex(item => (item.length == 1 && item[0] == node.pathValues[0]) || (item.length == 2 && item[1] == node.pathValues[1])) == -1 ) {
selectedAreas.value.push(node.pathValues as number[])
}
}
})
}
onMounted(() => {
getTempSelectedAreas()
})
</script>