vue3+element-plus实现省市区三级地址多选

背景

  • 需要实现:选择省级地址时,回传节点为 [ 省级地址 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)实现,过滤不可选区域
  • 兼容后端返回已选区域(已选区域前端回显,以及变更选择区域后后端 api 入参)
    • initSelectedPaths 方法
      • 遍历后端返回的已选区域数组tempSelectedAreas,结合 areaData,将其转为 el-cascader-panel 双向绑定数据-已选区域要求的数据结构,即节点结构均为 [省级地址 id,市级地址 id,区县地址 id],从而实现前端正确回显已选区域
      • 获取到后端数据tempSelectedAreas 后,讲其浅拷贝给selectedAreas,避免当未进行任何操作直接点击保存时,selectedAreas 为空,丢失原有已选区域的问题

遗留问题

  • 树状结构层级硬编码:仅实现了树状结构层级不超过 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>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值