列式树型选择组件(也称为树形选择器或级联选择器)是一种用户界面组件,用于以分层方式显示和选择数据。当用户选择一个节点时,会显示该节点的子节点,以便用户可以继续选择。这种组件非常适合用于具有层级关系的数据,如目录结构、分类系统等。
组件特点
双向绑定:使用 `v-model` 可以轻松地在父组件中获取和设置选中的地区。
支持父级半选状态:当部分子项被选中时,父级显示为半选状态。
联动选择:选中父级会选中所有子级,取消选中父级会取消选中所有子级。
向上更新:子级的选择状态变化会自动更新父级的状态。
自定义样式:使用自定义的复选框样式,以支持半选状态的显示
性能优化:使用了 scroll-view 处理长列表。
代码复用性强:可以用于各种多级选择场景,不限于地区选择。
高度灵活:可以适应不同的数据结构和层级数量。
用户友好:提供了搜索功能和多选功能,提高了使用效率。
易于扩展:可以根据需求轻松添加新的功能或修改现有功能。
组件库代码实现
<template>
<view class="region-picker">
<!-- 搜索栏 -->
<view class="search-bar">
<input type="text" v-model="searchQuery" :placeholder="searchPlaceholder" @input="filterRegions" />
</view>
<!-- 地区选择列 -->
<view v-if="isColumnTitle" class="columns-header">
<view class="column-header" v-for="(column, index) in visibleColumns">
<view class="column-title">{{ column.title }}</view>
</view>
</view>
<view class="columns-container" :class="{'columns-container-border':!isColumnTitle}">
<scroll-view v-for="(column, index) in visibleColumns" :key="index" scroll-y class="column"
:scroll-top="columnScrollTop[index]" @scroll="onColumnScroll($event, index)">
<view v-for="item in column.data" :key="item[valueName]" class="item"
:class="{ 'item-selected': isSelected(item) }" :style="getItemStyle(item)"
@tap="selectItem(item, index)">
<text>{{ item[labelName] }}</text>
<!-- 复选框 -->
<view class="checkbox-container" @tap="toggleSelection(item)">
<view class="checkbox" :class="{
'checkbox-checked': isFullySelected(item),
'checkbox-indeterminate': !isFullySelected(item)&&isPartiallySelected(item)
}" :style="getCheckboxStyle(item)"></view>
</view>
</view>
</scroll-view>
</view>
<!-- 操作按钮 -->
<!-- <view class="action-buttons">
<button @tap="resetSelection" class="btn btn-reset">重置</button>
<button @tap="confirmSelection" class="btn btn-confirm">确定</button>
</view> -->
</view>
</template>
<script>
export default {
emits: ["update:modelValue", "confirm"],
props: {
// 地区数据
regions: {
type: Array,
required: true
},
// 最大层级
maxLevel: {
type: Number,
default: 3
},
isColumnTitle:{
type: Boolean,
default: true
},
// 列标题
columnTitles: {
type: String,
default: '省份,城市,区县'
},
// 选中项背景颜色
selectedColor: {
type: String,
default: '#ffffff'
},
// 选中项文字颜色
selectedTextColor: {
type: String,
default: '#19be6b'
},
// v-model 绑定值
modelValue: {
type: Array,
default: () => []
},
// 自定义value属性名
valueName: {
type: String,
default: 'code'
},
// 自定义label属性名
labelName: {
type: String,
default: 'name'
},
},
data() {
return {
initColumnTitles:[],
selectedRegions: new Set(this.modelValue), // 已选择的地区集合
searchQuery: '', // 搜索查询
selectedItems: [], // 当前选中的项目
flattenedRegions: [], // 扁平化的地区数据
filteredRegions: [], // 过滤后的地区数据
columnScrollTop: [], // 列滚动位置
itemHeight: 44, // 每个项目的高度
}
},
computed: {
// 搜索框占位符
searchPlaceholder() {
return `搜索${this.initColumnTitles.join('、')}...`
},
// 可见列数据
visibleColumns() {
const columns = []
for (let i = 0; i < this.maxLevel; i++) {
columns.push({
title: this.initColumnTitles[i] || `级别 ${i + 1}`,
data: this.getColumnData(i)
})
}
return columns
}
},
watch: {
// 监听选中项变化,更新滚动位置
selectedItems: {
handler() {
this.$nextTick(() => {
this.scrollSelectedItemsToCenter()
})
},
deep: true
},
// 监听 modelValue 变化,更新选中状态
modelValue: {
handler(newValue) {
this.selectedRegions = new Set(newValue)
//this.initializeSelection()
},
deep: true
}
},
created() {
this.flattenRegions(this.regions) // 扁平化地区数据
this.filteredRegions = [...this.regions] // 初始化过滤后的地区数据
this.initializeSelection() // 初始化选择状态
this.$watch('searchQuery', this.filterRegions) // 监听搜索查询变化
if(this.columnTitles){
this.initColumnTitles = this.columnTitles.split(",")
}
},
methods: {
// 扁平化地区数据
flattenRegions(regions, level = 0, parent = null) {
regions.forEach(region => {
this.flattenedRegions.push({
...region,
level,
parent
})
if (region.children) {
this.flattenRegions(region.children, level + 1, region)
}
})
},
// 初始化选择状态
initializeSelection() {
this.selectedItems = []
if (this.selectedRegions.size > 0) {
this.selectInitialRegions(this.regions, [])
} else if (this.regions.length > 0) {
this.expandAllLevels(this.regions[0])
}
},
// 选择初始地区
selectInitialRegions(regions, path) {
for (const region of regions) {
const newPath = [...path, region]
if (this.selectedRegions.has(region[this.valueName])) {
this.selectedItems = newPath
return true
}
if (region.children && this.selectInitialRegions(region.children, newPath)) {
return true
}
}
return false
},
// 展开所有层级
expandAllLevels(item) {
this.selectedItems.push(item)
if (item.children && item.children.length > 0) {
this.expandAllLevels(item.children[0])
}
},
// 获取列数据
getColumnData(level) {
if (level === 0) {
return this.filteredRegions
}
const parentItem = this.selectedItems[level - 1]
if (!parentItem) return []
return parentItem.children || []
},
// 根据查询过滤地区
filterRegionsByQuery(regions, query) {
return regions.map(region => {
const matchesQuery = region[this.labelName].toLowerCase().includes(query)
let filteredChildren = []
if (region.children) {
if (matchesQuery) {
filteredChildren = region.children
} else {
filteredChildren = this.filterRegionsByQuery(region.children, query)
}
}
const childrenMatch = filteredChildren.length > 0
if (matchesQuery || childrenMatch) {
return {
...region,
children: filteredChildren
}
}
return null
}).filter(Boolean)
},
// 过滤地区
filterRegions() {
const query = this.searchQuery.toLowerCase()
if (!query) {
this.filteredRegions = [...this.regions]
this.selectedItems = []
this.initializeSelection()
} else {
this.filteredRegions = this.filterRegionsByQuery(this.regions, query)
this.updateSelectedItems()
}
this.$forceUpdate()
},
// 更新选中项
updateSelectedItems() {
this.selectedItems = this.selectedItems.filter(item =>
this.isItemInFilteredRegions(item, this.filteredRegions)
)
if (this.selectedItems.length === 0 && this.filteredRegions.length > 0) {
this.expandAllLevels(this.filteredRegions[0])
}
},
// 检查项目是否在过滤后的地区中
isItemInFilteredRegions(item, regions) {
for (const region of regions) {
if (region[this.valueName] === item[this.valueName]) return true
if (region.children) {
if (this.isItemInFilteredRegions(item, region.children)) return true
}
}
return false
},
// 选择项目
selectItem(item, level) {
this.selectedItems = this.selectedItems.slice(0, level)
this.selectedItems[level] = item
if (item.children && item.children.length > 0) {
this.selectItem(item.children[0], level + 1)
}
},
// 检查项目是否被选中
isSelected(item) {
return this.selectedRegions.has(item[this.valueName]) || this.isPartiallySelected(item)
},
// 检查项目是否完全选中
isFullySelected(item) {
if (!item.children) {
return this.selectedRegions.has(item[this.valueName])
}
return item.children.every(child => this.isFullySelected(child))
},
// 检查项目是否部分选中
isPartiallySelected(item) {
if (!item.children) {
return false
}
const selectedChildren = item.children.filter(child =>
this.isFullySelected(child) || this.isPartiallySelected(child)
)
return selectedChildren.length > 0 && selectedChildren.length <= item.children.length
// return selectedChildren.length > 0 && selectedChildren.length <= item.children.length
},
// 切换选择状态
toggleSelection(item) {
const shouldSelect = !this.isFullySelected(item)
this.setSelectionState(item, shouldSelect)
this.emitUpdate()
},
// 设置选择状态
setSelectionState(item, isSelected) {
if (!item.children) {
if (isSelected) {
this.selectedRegions.add(item[this.valueName])
} else {
this.selectedRegions.delete(item[this.valueName])
}
} else {
item.children.forEach(child => this.setSelectionState(child, isSelected))
}
this.$forceUpdate()
},
// 重置选择
resetSelection() {
this.selectedRegions.clear()
this.selectedItems = []
this.searchQuery = ''
this.filteredRegions = [...this.regions]
this.initializeSelection()
this.emitUpdate()
},
// 确认选择
confirmSelection() {
const selectedRegions = Array.from(this.selectedRegions)
this.emitUpdate()
// this.$emit('confirm', {
// value: selectedRegions,
// label: this.getSelectedRegionNames()
// })
this.$emit('confirm', {
code: selectedRegions,
name: this.getSelectedRegionNames(),
codes: this.getOptimizedSelectedRegionCodes(),
names: this.getOptimizedSelectedRegionNames(),
})
},
// 发出更新事件
emitUpdate() {
const selectedRegions = Array.from(this.selectedRegions)
this.$emit('update:modelValue', selectedRegions)
// this.$emit('update:modelValue', this.getOptimizedSelectedRegionCodes())
},
// 获取项目样式
getItemStyle(item) {
if (this.isSelected(item)) {
return {
backgroundColor: this.selectedColor,
color: this.selectedTextColor
}
}
return {}
},
// 获取复选框样式
getCheckboxStyle(item) {
if (this.isFullySelected(item) || this.isPartiallySelected(item)) {
return {
backgroundColor: this.selectedTextColor,
borderColor: this.selectedTextColor
}
}
return {}
},
// 滚动选中项到中心
scrollSelectedItemsToCenter() {
this.visibleColumns.forEach((column, index) => {
const selectedItemIndex = column.data.findIndex(item => item[this.valueName] === (this.selectedItems[index]?this.selectedItems[index][this.valueName]:this.selectedItems[index]))
if (selectedItemIndex !== -1) {
const scrollViewHeight = 300
const scrollTop = Math.max(0, (selectedItemIndex * this.itemHeight) - (scrollViewHeight /
2) + (this.itemHeight / 2))
this.$set(this.columnScrollTop, index, scrollTop)
}
})
},
// 列滚动事件处理
onColumnScroll(event, columnIndex) {
// 可以在这里添加额外的滚动逻辑
},
// 获取所有选中地区的名称
getSelectedRegionNames() {
const selectedNames = []
const traverse = (regions) => {
for (const region of regions) {
if (this.selectedRegions.has(region[this.valueName])) {
selectedNames.push(region[this.labelName])
}
if (region.children) {
traverse(region.children)
}
}
}
traverse(this.regions)
return selectedNames
},
// 获取所有选中地区的名称,包括上级省市
getSelectedRegionNamesWithParents() {
const selectedNamesWithParents = []
const traverse = (regions, parentNames = []) => {
for (const region of regions) {
const currentNames = [...parentNames, region[this.labelName]]
if (this.selectedRegions.has(region[this.valueName])) {
selectedNamesWithParents.push(currentNames.join('-'))
}
if (region.children) {
traverse(region.children, currentNames)
}
}
}
traverse(this.regions)
return selectedNamesWithParents
},
// 如果下级所有都选择只显示上级 或者只显示选中的下级
// getOptimizedSelectedRegionNames() {
// const result = []
// const traverse = (regions, parentNames = []) => {
// for (const region of regions) {
// const currentNames = [...parentNames, region[this.labelName]]
// if (this.selectedRegions.has(region[this.valueName])) {
// if (region.children && region.children.every(child => this.isFullySelected(child))) {
// // 如果所有子地区都被选中,只添加当前地区
// result.push(currentNames.join('-'))
// } else if (!region.children) {
// // 如果是叶子节点,添加完整路径
// result.push(currentNames.join('-'))
// }
// // 如果当前地区被选中但子地区没有全部被选中,不添加当前地区,继续遍历子地区
// }
// if (region.children) {
// traverse(region.children, currentNames)
// }
// }
// }
// traverse(this.regions)
// return result
// },
// 获取优化后的选中地区名称
getOptimizedSelectedRegionNames() {
const result = new Set()
const traverse = (regions, parent = null) => {
let allChildrenSelected = true
let someChildrenSelected = false
for (const region of regions) {
if (region.children && region.children.length > 0) {
const [childAllSelected, childSomeSelected] = traverse(region.children, region)
allChildrenSelected = allChildrenSelected && childAllSelected
someChildrenSelected = someChildrenSelected || childSomeSelected
} else {
const isSelected = this.selectedRegions.has(region[this.valueName])
allChildrenSelected = allChildrenSelected && isSelected
someChildrenSelected = someChildrenSelected || isSelected
}
if (this.selectedRegions.has(region[this.valueName]) && !region.children) {
result.add(region[this.labelName])
}
}
if (allChildrenSelected && parent) {
//移除所有子地区代码
for (const region of regions) {
result.delete(region[this.labelName])
}
result.add(parent[this.labelName])
}
return [allChildrenSelected, someChildrenSelected]
}
traverse(this.regions)
return Array.from(result)
},
//获取优化后的选中地区代码
getOptimizedSelectedRegionCodes() {
const result = new Set()
const traverse = (regions, parent = null) => {
let allChildrenSelected = true
let someChildrenSelected = false
for (const region of regions) {
if (region.children && region.children.length > 0) {
const [childAllSelected, childSomeSelected] = traverse(region.children, region)
allChildrenSelected = allChildrenSelected && childAllSelected
someChildrenSelected = someChildrenSelected || childSomeSelected
} else {
const isSelected = this.selectedRegions.has(region[this.valueName])
allChildrenSelected = allChildrenSelected && isSelected
someChildrenSelected = someChildrenSelected || isSelected
}
if (this.selectedRegions.has(region[this.valueName]) && !region.children) {
result.add(region[this.valueName])
}
}
if (allChildrenSelected && parent) {
// 移除所有子地区代码
for (const region of regions) {
result.delete(region[this.valueName])
}
result.add(parent[this.valueName])
}
return [allChildrenSelected, someChildrenSelected]
}
traverse(this.regions)
return Array.from(result)
}
}
}
</script>
<style scoped>
.region-picker {
display: flex;
flex-direction: column;
}
.search-bar {
padding: 20rpx;
}
.search-bar input {
width: 100%;
height: 72rpx;
padding: 0 20rpx;
border: 1rpx solid #dcdfe6;
border-radius: 8rpx;
font-size: 28rpx;
}
.columns-container,.columns-header {
display: flex;
flex: 1;
overflow: hidden;
}
.columns-container-border{
border-top: 1rpx solid #ebeef5;
}
.column-header{
flex: 1;
border-right: 1rpx solid #ebeef5;
}
.column-header:last-child {
border-right: none;
}
.column {
flex: 1;
border-right: 1rpx solid #ebeef5;
height: 600rpx;
}
.column:last-child {
border-right: none;
}
.column-title {
padding: 20rpx;
font-size: 28rpx;
font-weight: bold;
border-top: 1rpx solid #ebeef5;
border-bottom: 1rpx solid #ebeef5;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1rpx solid #ebeef5;
height: 88rpx;
}
.checkbox-container {
display: flex;
align-items: center;
justify-content: center;
width: 40rpx;
height: 40rpx;
}
.checkbox {
display: flex;
align-items: center;
justify-content: center;
width: 36rpx;
height: 36rpx;
border: 1rpx solid #dcdfe6;
border-radius: 4rpx;
background-color: #ffffff;
}
.checkbox-checked::after {
content: '';
width: 8rpx;
height: 16rpx;
border: solid white;
border-width: 0 4rpx 4rpx 0;
transform: rotate(45deg);
}
.checkbox-indeterminate::after {
content: '';
width: 16rpx;
height: 4rpx;
background-color: white;
}
.action-buttons {
display: flex;
justify-content: space-between;
padding: 20rpx;
background-color: #ffffff;
border-top: 1rpx solid #ebeef5;
}
.btn {
width: 45%;
height: 40px;
border-radius: 8rpx;
font-size: 32rpx;
}
.btn-reset {
background-color: #ffffff;
color: #606266;
border: 1rpx solid #dcdfe6;
}
.btn-confirm {
background-color: #409eff;
color: #ffffff;
border: none;
}
</style>
可视化拖拉设计
拖动一个多级级联树型组件进设计区即可。
查看源码
保存源码至本地
保存源码至本地即可快带查看效果。