基于vue3+elementplus的一个根据中国地区大区分区的选择器,效果图如下,需求说实话还是挺合(bian)理(tai)的
需求如下
- 根据中国地理大区划分显示对应的省份
- 如果选择了广东省,那就代表着选择了这个省下面的所有市,所有区,但是发请求的时候不要传,说是为了减少请求的数据量,举例说明,界面中勾选了广东省,保存之后,在表格中看到的数据就只有广东省,但是如果展开广东省下面的市级,包括区级,都是全选中的状态,但是如果去掉一个区,也就是不是全选的情况下,那展示的时候就要变成广东省-广州市-天河区....全部一一展开
- 因为业务是电商的指定配送区域,并且设置快递费用,所以需要 去重,就是上一条数据选择过了广东省,那就不能再选择广东省了,如果整个华北被选择过了,也需要在新增的时候不显示整个华北区域
需求明确之后,那就是一步一步的去实现了
1,根据中国地理大区划分显示对应的省份
这个数据我感觉也是应该是一个树状的数据结构的,我找了一下,发现并没有发现现成的JSON数据,所以也只能通过一个通用的省市区JSON数据,然后通过node写一个脚本来实现了,脚本如下,也相对简单
const fs = require('fs');
// 读取省市区数据的 JSON 文件
fs.readFile('./pca-code.json', 'utf8', (err, jsonString) => {
if (err) {
console.log("Error reading file from disk:", err);
return;
}
try {
// 解析 JSON 数据
const jsonData = JSON.parse(jsonString);
let data = [
{
name:'东北',
code:-1,
includeProvinces:['辽宁省','吉林省','黑龙江省'],
children:[]
},
{
name:'华北',
code:-2,
includeProvinces:['北京市','天津市','河北省','山西省','内蒙古自治区'],
children:[]
},
{
name:'华东',
code:-3,
includeProvinces:['上海市','江苏省','浙江省','安徽省','福建省','江西省','山东省'],
children:[]
},
{
name:'华南',
code:-3,
includeProvinces:['广东省','广西壮族自治区','海南省'],
children:[]
},
{
name:'华中',
code:-4,
includeProvinces:['河南省','湖北省','湖南省'],
children:[]
},
{
name:'西南',
code:-5,
includeProvinces:['重庆市','四川省','贵州省','云南省','西藏自治区'],
children:[]
},
{
name:'西北',
code:-6,
includeProvinces:['陕西省','甘肃省','青海省','宁夏回族自治区','新疆维吾尔自治区'],
children:[]
},
{
name:'港澳台',
code:-7,
includeProvinces:['台湾省','香港特别行政区','澳门特别行政区'],
children:[]
},
]
let result = {};
data.map(t=>{
let provinces = t.includeProvinces;
jsonData.map((t2,index)=>{
if(provinces.includes(t2.name)){
t.children.push(t2)
}
})
})
// 将结果转换为 JSON 字符串并输出
const resultJsonString = JSON.stringify(data, null, 2);
// 将结果写入新的 JSON 文件
fs.writeFile('group-pca.json', resultJsonString, 'utf8', (err) => {
if (err) {
console.log("Error writing file:", err);
} else {
console.log("New JSON file has been saved!");
}
});
console.log(result);
} catch (err) {
console.log("Error parsing JSON data:", err);
}
});
第一个需求我们就已经完成了
2,当全选子级的时候,显示与数据传输都只是当前级,如果不是全选,那就要平铺显示
全选广东省,传输与显示的时候就只有广东省,但是如果把广州市的越秀区去掉的话,那传输与展示的时候就是平铺,广东省-广州市-天河区,广东省-广州市-黄埔区...但是仅仅只是广州市的区级平铺,其他的市级由于是区级全选,所以其他的市也只显示到市级,所以数据是这样的:广东省-广州市-天河区...,广东省-深圳市,广东省-清远市...
需求理清楚了,先把界面展示出来,根据效果图我们可以看出是一个弹窗的方式,一开始我想要使用el-tree组件实现,但是效果太卡了,每次打开弹窗都好几秒,因为整体的数据量还是挺大的,无奈放弃,只能自己写了
1,新建一个area-form.vue,使用el-checkbox来进行嵌套地区大区的划分,子级的显示的话,使用el-popover来进行嵌套展示
<template>
<el-dialog
v-model="dialogVisible"
title="指定可配送区域"
width="80%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<div>
<template v-for="group in areaData" :key="group.code">
<div class="flex flex-wrap">
<el-checkbox
:value="group.code"
v-model="group.selected"
class="w-100px"
>
{{ group.name }}
</el-checkbox>
<el-checkbox-group
v-model="group.selectedChildren"
>
<template v-for="province in group.children" :key="province.code">
<el-checkbox
:value="province"
>
<el-popover width="300" placement="right" trigger="click" :teleported="false">
<template #reference>
<div>
{{ province.name }}
<el-icon>
<ArrowDown />
</el-icon>
</div>
</template>
</el-popover>
</el-checkbox>
</template>
</el-checkbox-group>
</div>
</template>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import {ref,defineEmits} from "vue"
import groupPca from './group-pca.json'
import {ArrowDown} from "@element-plus/icons-vue"
import {cloneDeep} from "lodash-es"
const areaData = ref(cloneDeep(groupPca))
const dialogVisible = ref(true)
const emits = defineEmits(['close'])
const handleClose = () => {
emits('close')
}
const save = ()=>{
}
</script>
<style scoped lang="scss">
.flex{
display: flex;
}
.flex-wrap{
flex-wrap: wrap;
}
.items-center{
align-items: center;
}
.w-100px{
width:100px;
}
</style>
华北,华南等大区是不需要传输到后台的,所以作用只是作为前端的一个快捷选择,单独把它拿出来,使用el-checkbox-group为了更好的拿子级选择的一个数据
第一层的省级数据我们已经展示出来了,接下来展示市级跟区级,树状结构的数据展示,最直接方式的就是写一个组件出来,然后递归式的调用组件本身,所以我们新建一个area-popover.vue组件
1, 界面的话,通过传入的区域数据来进行checkbox,并且需要判断是否还有子级,有的话就展示箭头图标,并且再调用组件本身,没有的话,就直接显示就好了
<template>
<div>
<el-checkbox-group>
<template v-for="item in areaData" :key="item.code">
<el-checkbox
style="display: block"
:value="item"
>
<el-popover
v-if="item.children?.length"
width="300"
placement="right"
trigger="click"
:teleported="false"
>
<template #reference>
<div class="inline-block">
{{ item.name }}
<el-icon v-if="item.children?.length"><ArrowDown /></el-icon>
</div>
</template>
<div v-if="item.children?.length">
<areaPopover
:area-data="item.children"
:indeterminate="item.isIndeterminate"
></areaPopover>
</div>
</el-popover>
<div v-else>
{{ item.name }}
</div>
</el-checkbox>
</template>
</el-checkbox-group>
</div>
</template>
<script setup name="areaPopover">
import {defineProps,onMounted,ref,computed} from 'vue'
import {ArrowDown} from "@element-plus/icons-vue"
const props = defineProps({
areaData: {
type: Array,
required: true
},
})
</script>
<style scoped lang="scss"></style>
在组件中使用它,修改的area-form组件
<template>
<el-dialog
v-model="dialogVisible"
title="指定可配送区域"
width="80%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<div>
<template v-for="group in areaData" :key="group.code">
<div class="flex flex-wrap">
<el-checkbox
:value="group.code"
v-model="group.selected"
class="w-100px"
>
{{ group.name }}
</el-checkbox>
<el-checkbox-group
v-model="group.selectedChildren"
>
<template v-for="province in group.children" :key="province.code">
<el-checkbox
:value="province"
>
<el-popover width="300" placement="right" trigger="click" :teleported="false">
<template #reference>
<div>
{{ province.name }}
<el-icon>
<ArrowDown />
</el-icon>
</div>
</template>
<div>
<!-- 引用子级区域的组件-->
<areaPopover
:area-data="province.children"
>
</areaPopover>
</div>
</el-popover>
</el-checkbox>
</template>
</el-checkbox-group>
</div>
</template>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="save">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import {ref,defineEmits} from "vue"
import groupPca from './group-pca.json'
import {ArrowDown} from "@element-plus/icons-vue"
import {cloneDeep} from "lodash-es"
import areaPopover from './area-popover.vue'
const areaData = ref(cloneDeep(groupPca))
const dialogVisible = ref(true)
const emits = defineEmits(['close'])
const handleClose = () => {
emits('close')
}
const save = ()=>{
}
</script>
<style scoped lang="scss">
.flex{
display: flex;
}
.flex-wrap{
flex-wrap: wrap;
}
.items-center{
align-items: center;
}
.w-100px{
width:100px;
}
</style>
到目前为止显示是正常的,但是很卡...dialog显示都好几秒,思考了一下,是因为el-popover其实它不显示,但是它的dom树却会生成,所以数据量大的时候会导致卡顿
优化卡顿问题
上面提到了是因为数据量大量的同时渲染导致的卡顿,所以我们优化的逻辑就是"懒加载"的方式,交互的逻辑是点击某个区域,展示它的子区域,所以我的思路就是从点击事件入手,记录唯一的code值来进行判断,是否渲染子级的el-popover,并且由于要在多个组件中用到,所以我们直接封装成一个hook
useShowAreaCode的hook
import {ref} from "vue"
export const useShowAreaCode = () => {
const currentAreaCode = ref('')
const showPopoverHandle = code => {
currentAreaCode.value = code
}
return {
currentAreaCode,
showPopoverHandle
}
}
修改area-form.vue
<template>
...
<areaPopover
v-if="currentAreaCode === province.code"
:area-data="province.children"
>
</areaPopover>
</template>
<script setup>
import { useShowAreaCode } from './hooks'
const { currentAreaCode, showPopoverHandle } = useShowAreaCode()
</script>
修改area-popover.vue组件
<template>
<div>
<el-checkbox-group>
<template v-for="item in areaData" :key="item.code">
<el-checkbox
style="display: block"
:value="item"
>
<el-popover
v-if="item.children?.length"
width="300"
placement="right"
trigger="click"
:teleported="false"
>
<template #reference>
<div class="inline-block" @click="showPopoverHandle(item.code)">
{{ item.name }}
<el-icon v-if="item.children?.length"><ArrowDown /></el-icon>
</div>
</template>
<div v-if="item.children?.length && currentAreaCode === item.code">
<areaPopover
:area-data="item.children"
></areaPopover>
</div>
</el-popover>
<div v-else>
{{ item.name }}
</div>
</el-checkbox>
</template>
</el-checkbox-group>
</div>
</template>
<script setup>
import { useShowAreaCode } from './hooks'
const { currentAreaCode, showPopoverHandle } = useShowAreaCode();
...
</script>
再次运行,发现打开的弹窗的时间的确快的多了,而且子级的显示也没有问题,但是交互上好像有一点点冲突,点击区域名称的时候应该是展开子区域,复选框自身不受影响
解决交互冲突,这里我直接给showPopoverHandle这个点击事件换成lick.prevent来禁止默认事件即可
...
<div @click.prevent="showPopoverHandle(province.code)">
{{ province.name }}
<el-icon>
<ArrowDown />
</el-icon>
</div>
到目前为止,弹窗的展示跟选择暂时是没有问题了,接下来开始做比较麻烦的数据处理的问题了
数据处理
首先先确认界面有多少种状态,因为需要通过数据来驱动界面,所以界面的状态关系到我们需要怎么改造我们的数据
- 选中状态,也就是自身打钩,这个状态代表所有后代全选
- 半选状态,代表后代区域有被勾选.
- 不显示,也就是数据去重的时候,发现该区域已经被选中过了,是直接不在界面中展示,但是同时需要是否为真的全选中了子级,所以做法是给这个区域数据打一个标识
先做第一个,选中状态,因为是el-checkbox-group的方式,所以直接给数据加上一个selectedChildren的数据来控制,同时也方便保存时拿到数据,修改area-form.vue组件
const initData = () => {
areaData.value.map(t => {
t.selectedChildren = []
t.children.map(t2 => {
t2.selectedChildren = []
})
})
}
onMounted(() => {
initData()
})
这里因为弹窗默认的情况下,其实只是展示到了省级,所以只需要给省级加上我们需要的字段就好,子级的话,在用户操作展示出子级的时候再进行添加,这样可以减少循环的次数
areaPopover.vue加上v-model的方式来通过父级传输的selectedChildren来进行显示并且数据的统一性
<template>
<el-checkbox-group v-model="selectedData">
<template v-for="item in areaData" :key="item.code">
<el-checkbox
style="display: block"
:value="item"
>
<el-popover
v-if="item.children?.length"
width="300"
placement="right"
trigger="click"
:teleported="false"
>
<template #reference>
<div class="inline-block" @click.prevent="showPopoverHandle(item.code)">
{{ item.name }}
<el-icon v-if="item.children?.length"><ArrowDown /></el-icon>
</div>
</template>
<div v-if="item.children?.length && currentAreaCode === item.code">
<areaPopover
:area-data="item.children"
v-model="item.selectedChildren"
></areaPopover>
</div>
</el-popover>
<div v-else>
{{ item.name }}
</div>
</el-checkbox>
</template>
</el-checkbox-group>
</template>
<script setup>
...
const props = defineProps({
areaData: {
type: Array,
required: true
},
modelValue: {
type: Array,
defalut: []
}
})
const emits = defineEmits(['update:modelValue'])
const selectedData = computed({
get: () => {
return props.modelValue || []
},
set: val => {
emits('update:modelValue', val)
}
})
</script>
area-form.vue直接传入v-model
<template>
...
<areaPopover
v-if="currentAreaCode === province.code"
:area-data="province.children"
v-model="province.selectedChildren"
>
</areaPopover>
</template>
现在复选框的自身勾选是正常的了,但是父级勾选之后,子级展开的时候并没有全选
父级勾选,子级全选
根据我们上面类似于"懒加载"的方法,所以我的做法就是在子区域的界面显示的话,传入一个值告诉它目前父区域的一个状态,从而来判断是否要全选
在area-popover.vue这个组件中,新增一个props的字段,来代表父级目前是否是已选中的状态,并且在onMounted的生命周期里做初始化的判断
<template>
<areaPopover
:area-data="item.children"
:parent-selected="currentIsCheck(item)"
v-model="item.selectedChildren"
>
</areaPopover>
</template>
<script setup>
const props = defineProps({
areaData: {
type: Array,
required: true
},
modelValue: {
type: Array,
defalut: []
},
parentSelected: {
// 父区域是否被选择
type: Boolean,
default: false
},
})
const currentIsCheck = computed(() => {
// 判断当前的区域是否是选中的状态
return function (item) {
return selectedData.value.some(t => t.code == item.code)
}
})
onMounted(() => {
if (props.parentSelected) {
// 父区域选中的话,就代表所有子级全选,需要把选中的状态重置
selectedData.value = props.areaData.map(t => t)
}
})
</script>
area-form.vue的组件中传入parentSelected这个值,这里使用一个计算属性来判断当前这个区域是否是选中的状态
<template>
<areaPopover
v-if="currentAreaCode === province.code"
:area-data="province.children"
v-model="province.selectedChildren"
:parentSelected="currentIsCheck(group, province)"
>
</areaPopover>
</template>
<script setup>
const currentIsCheck = computed(() => {
return function (groupData, item) {
return groupData.selectedChildren && groupData.selectedChildren.some(t => t.code == item.code)
}
})
</script>
子级选择,父级区域半选状态的响应
其实这里有两种做法
- 通过watch的方式来监听selectedChildren数组的改变
- 通过子级的checkbox-group的change事件发生时,emit通知父级来改变选中状态
这里的话,我选择的是第一种的方式,虽然我没有去测试过,但是我觉的第2种的方式性能消耗会更大,因为需要深监测的方式进行watch监听
我们新增一个 isIndeterminate字段来驱动checkbox组件的半选状态,然后监听checkbox-group的change事件, 然后在回调的函数中去判断当前是否全选的状态,然后通过emit事件来通知父级改变isIndeterminate这个字段
<el-checkbox
style="display: block"
:indeterminate="item.isIndeterminate"
:value="item"
@change="
value => {
checkboxChangeHandle(value, item)
}
"
>
...
</el-checkbox>
const changeHandle = formCheckbox => {
/*
* 每个checkbox有3个状态,选中,选中部分,不选中
* */
setTimeout(() => {
let isIndeterminate = props.areaData.some(t => t.isIndeterminate)
if (isIndeterminate) {
// 如果当前数据有任何一个数据的indeterminate为true,往前的所有父级的indeterminate都为true
emits('parentSelected', false)
emits('indeterminate', true)
} else if (selectedData.value.length === props.areaData.length) {
// 如果所有数据的indeterminate都为false,就判断选中的数据是否跟渲染的数据相同,相同的话就是全选
emits('parentSelected', true)
emits('indeterminate', false)
} else if (selectedData.value.length) {
emits('parentSelected', false)
emits('indeterminate', true)
} else if (!selectedData.value.length) {
// 如果一个选中项都没有,那勾选状态为空
emits('parentSelected', false)
emits('indeterminate', false)
}
if (!formCheckbox) return
props.areaData.map(t => {
// 需要手动判断数据是否被选中,被选中之后需要手动修改isIndeterminate的状态,否则显示的选中状态不正确
let isExist = selectedData.value.some(t2 => {
return t2.code == t.code
})
if (isExist) {
t.isIndeterminate = false
}
})
}, 100)
}
这里要添加一个定时器,来让通知事件在checkbox的自身事件发生之后再触发,不然会出现你界面显示不准确的问题
在组件上监听这两个事件
<areaPopover
:area-data="item.children"
:indeterminate="item.isIndeterminate"
:parent-selected="currentIsCheck(item)"
v-model="item.selectedChildren"
@parent-selected="
value => {
changeParentSelected(value, item)
}
"
@indeterminate="
value => {
changeIndeterminate(value, item)
}
"
>
</areaPopover>
<script setup>
const changeParentSelected = (value, item) => {
let arr = selectedData.value
let index = arr.findIndex(t => t.code == item.code)
if (value && index == -1) {
arr.push(item)
} else if (!value && index != -1) {
arr.splice(index, 1)
}
emits('update:modelValue', arr)
}
const changeIndeterminate = (value, item) => {
// 子级的区域选中状态发生变动的话,需要计算父区域的状态值
item.isIndeterminate = value
changeHandle(false)
}
</script>
area-form组件因为有华南这种地理大区比较特殊的存在,所以回调事件会稍微有点不一样,可以直接看源码,主要逻辑是相同,这里就不赘述了
到这里会发现el-popover的弹出的组件在点击非常边缘的时候,会触发到父级的checkbox选择事件,导致了选择结果不正确,通过控制台我们会发现是因为它自带了padding,由于我们之前阻止的是checkbox的本身的原生事件,所以点到padding这里的话,就会导致它的父盒子被触发了原生事件,也就是下面图中的位置
解决的方式,我们通过 事件代理的方式来禁止冒泡事件就可以了.在el-dialog的内容最外层的div上监听点击事件,然后通过类名判断是否是el-popover,是的话就禁用冒泡
<template>
<el-dialog>
<div @click="handleClick">
...
</div>
</el-dialog>
</template>
<script setup>
const handleClick = event => {
if (event.target.classList.contains('el-popover')) {
// 防止点击el-popover的空白处导致父级区域的反选
event.preventDefault()
}
}
</script>
全选区域,显示已选择区域
通过递归的方式获取已经选择到的区域,并且累计相加, filterSelectedFn这个方法里面的一些disabled等字段,后面会说到作用
<template>
...
<div class="flex items-center">
<el-checkbox v-model="checkAll" @change="changeAllHandle">全选</el-checkbox>
<div style="color:#ccc;margin-left: 12px">已选择{{ areaCount }}区域</div>
</div>
</template>
<script setup>
const changeAllHandle = value => {
areaData.value.map(t => {
t.isIndeterminate = false
if (value) {
t.selected = true
checkboxAllChildren(t, value)
} else {
t.selected = false
checkboxAllChildren(t, value)
}
})
}
const filterSelectedFn = (isDeleteChldren = false) => {
let treeData = areaData.value.filter(t => {
// 遍历大区域的分组,只返回半选中或者有选中子级的数据
return t.isIndeterminate || t.selectedChildren?.length
})
treeData = cloneDeep(treeData)
const deleteExtraData = item => {
item.isIndeterminate !== undefined && delete item.isIndeterminate
item.selectedChildren !== undefined && delete item.selectedChildren
item.disabled !== undefined && delete item.disabled
// item.childrenExistDisabled !== undefined && delete item.childrenExistDisabled
}
const filterVoildFn = (items, parent) => {
return items.filter(item => {
// 如果半选中状态为false,而且父级的选中数据中没有该元素,则代表是没有选中该数据
if (item.disabled) return false
if (
!item.isIndeterminate &&
parent &&
!parent.selectedChildren?.some(t => t.code == item.code)
) {
return false
}
if (item.isIndeterminate || item.childrenExistDisabled) {
// 半选中状态,就需要遍历子级
item.children = filterVoildFn(item.children, item)
if (isDeleteChldren) {
deleteExtraData(item)
}
return true
}
if (
!item.isIndeterminate &&
!item.selectedChildren?.length &&
parent.selectedChildren?.some(t => t.code == item.code)
) {
// 这样也代表子级全选
if (isDeleteChldren) {
deleteExtraData(item)
item.children !== undefined && delete item.children
}
return true
}
if (item.selectedChildren && item.selectedChildren.length === item.children.length) {
// 子级全选
if (isDeleteChldren) {
deleteExtraData(item)
item.children !== undefined && delete item.children
}
return true
}
})
}
let finalArr = []
treeData.map(t => {
finalArr = finalArr.concat(filterVoildFn(t.children, t))
})
return finalArr
}
const areaCount = computed(() => {
let count = 0
let arr = filterSelectedFn()
const recursion = item => {
// 递归到最小颗粒度
if (item.children && item.children.length) {
item.children.map(t => {
recursion(t)
})
} else {
count++
}
}
arr.map(t => {
recursion(t)
})
return count
})
</script>
保存方法
通过上面的 filterSelectedFn获取选中的区域就可以了
const save = ()=>{
if (!areaCount.value) {
ElMessage.warning('请先选择区域')
return
}
filterSelectedFn(true)
emits('save', filterSelectedFn(true))
}
至此,新增的逻辑已经完成了,接下来就是编辑,还有再次新增时去重的逻辑了
编辑
- 编辑时需要回显已经选择的区域,所以props定义一个editItem
- 如果已经有数据的时候新增,需要去重掉已经被选择的区域.不能再选择 ,所以props需要定义一个existAreaList数据来进行去重的判断
编辑时回显方法
利用递归的方式来进行两个数组的对比,从而改变数据中的 isIndeterminate和 selectedChildren
const initEditData = () => {
const editData = cloneDeep(props.editItem.areaList)
const recursiveUpdate = (areaList, editList, parent) => {
areaList.forEach(area => {
const editItem = editList.find(item => item.code === area.code)
if (editItem) {
const areaArr = area.children?.filter(t => !t.disabled) || []
if (!parent.selectedChildren) {
parent.selectedChildren = []
}
if (editItem.children && editItem.children.length && areaArr.length) {
if (isAllChildrenSelected(area, editItem)) {
parent.selectedChildren.push(area)
} else {
area.isIndeterminate = true
}
recursiveUpdate(area.children, editItem.children, area)
} else {
parent.selectedChildren.push(area)
}
}
})
}
areaData.value.map(t => {
recursiveUpdate(t.children, editData, t)
})
// 判断区域的状态
areaData.value.map(t => {
let isIndeterminate = t.children.some(t2 => t2.isIndeterminate)
if (
isIndeterminate ||
(t.selectedChildren?.length && t.selectedChildren?.length < t.children.length)
) {
t.isIndeterminate = true
return
}
let isAll = t.selectedChildren?.length == t.children.length
if (isAll) {
t.selected = true
}
})
}
已选区域去重
跟编辑的回显函数核心逻辑差不多,通过递归来判断数据中相同的数据,并且加上 disabled和 childrenExistDisabled来记录上
const initDisabledData = () => {
// 禁用已经被选择过的数据
const existArr = cloneDeep(props.existAreaList)
const recursiveUpdate = (areaList, existList) => {
areaList.forEach(area => {
let existItem = { children: [] }
existList
.filter(item => item.code == area.code)
.map(t => {
existItem.name = t.name
existItem.code = t.code
existItem.children = existItem.children.concat(t.children || [])
})
if (existItem.code && existItem.children && existItem.children.length) {
// 记录一下子区域是否存在被去重的数据,在保存提交时需要用来判断
area.childrenExistDisabled = true
recursiveUpdate(area.children, existItem.children)
} else if (existItem.code) {
area.disabled = true
}
if (!area.disabled) {
// 子级全选的话,父级也需要变成禁用
area.disabled = area.children && area.children.every(t => t.disabled)
}
})
}
areaData.value.map(t => {
recursiveUpdate(t.children, existArr)
t.disabled = t.children.every(t => t.disabled)
})
}
disabled目前我公司的业务逻辑是直接不显示的,所以我就直接在v-if中加上这个条件就可以了,这个大家可以根据自己的业务情况来进行调整
<el-checkbox
style="display: block"
:indeterminate="item.isIndeterminate"
:value="item"
v-if="!item.disabled"
@change="
value => {
checkboxChangeHandle(value, item)
}
"
>
</el-checkbox>
childrenExistDisabled这个字段主要用于保存的时候的判断逻辑,如果一个区域这个字段为true,就代表需要向下递归循环去判断子级,因为只要子级有数据被disabled,那就代表着你选择不可能是全选某个省,或者某个市.,比如广东省-广州市被去重了,那用户再次进来勾选广东省保存,这个时候要把广东省下所有市的数据返回出去,并不是只返回广东省就可以了,如果广东省没有区域被去重,就只返回广东省就可以了
组件的使用
<template>
<div style="width: 100%;height:100%">
<el-button type="primary" class="mb-[12px]" @click="addArea()">
指定可配送区域
</el-button>
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="date" label="可配送区域">
<template #default="{ row }">
{{ areaListToString(row) }}
</template>
</el-table-column>
<el-table-column
:prop="item.prop"
:label="item.label"
v-for="item in currentConfig.columns"
:key="item.prop"
>
<template #default="{ row }">
<el-input-number
v-model="row[item.prop]"
:precision="currentConfig.precision"
:min="0"
/>
</template>
</el-table-column>
<el-table-column label="操作" >
<template #default="{ row, $index }">
<div >
<el-button text type="text" @click="editHandle(row,$index)">
编辑
</el-button>
<el-button text type="text" @click="deleteHandle($index)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-[12px]">
<el-button type="primary" @click="submitHandle" :loading="saveLoading">
保存
</el-button>
</div>
<!-- 指定可配送区域-->
<areaForm
v-if="showAreaForm"
@close="showAreaForm = false"
@save="saveAreaForm"
:existAreaList="existAreaList"
:editItem="currentTableItem"
></areaForm>
</div>
</template>
<script setup >
import areaForm from './components/selectAreaDialog/area-form.vue'
import {ref} from "vue"
const saveLoading = ref(false)
const showAreaForm = ref(false)
const existAreaList = ref([])
const tableData = ref([])
const currentTableItem = ref('')
const currentConfig = ref({
category: 2,
unit: '个',
precision: 0,
columns: [
{
prop: 'calculateStartValue',
label: '首件(个)'
},
{
prop: 'calculateStartPrice',
label: '运费(元)'
},
{
prop: 'calculateRenewalValue',
label: '续件(个)'
},
{
prop: 'calculateRenewalPrice',
label: '续费(元)'
}
]
})
const deleteHandle = (index) => {
tableData.value.splice(index, 1)
}
const areaListToString = row => {
let arr = []
const recursionFn = (item, str = '') => {
str = str ? `${str},${item.name}` : item.name
if (item.children?.length) {
item.children.map(t => {
recursionFn(t, str)
})
} else {
arr.push(str)
}
}
row.areaList.map(item => {
recursionFn(item)
})
arr.map((item, index) => {
arr[index] = item.replaceAll(',', '-')
})
return arr.join(',')
}
const getExistArea = (index) => {
// index,就代表是编辑,需要得到的是除了自身之外的所有已选区域的数据
existAreaList.value = []
let arr = tableData.value.filter((t, i) => {
return i !== index
})
existAreaList.value = arr.reduce((acc, t) => {
return acc.concat(t.areaList)
}, [])
}
const addArea = item => {
getExistArea()
currentTableItem.value = ''
showAreaForm.value = true
}
const editHandle = (row,index) => {
getExistArea(index)
currentTableItem.value = row
showAreaForm.value = true
}
const saveAreaForm = values => {
showAreaForm.value = false
if (currentTableItem.value) {
// 编辑状态下
currentTableItem.value.areaList = values
return
}
tableData.value.push({
areaList: values,
calculateStartValue: '',
calculateStartPrice: '',
calculateRenewalValue: '',
calculateRenewalPrice: ''
})
}
const submitHandle = async () => {
console.log(tableData.value);
}
</script>
<style lang="scss" scoped>
.form-item-width {
width: 400px;
}
</style>
总结
逻辑上比较复杂的其实是父子区域的联动,这一块的话我感觉我写的也不是特别的好,小伙伴有更好的思路也可以提出来,虚心请教~下面是源码,需要的小伙伴自取就好