区域选择组件(PC)
从零到一:开发高性能的PC端多选区域选择组件
在招聘需求发布场景中,常常需要选择多个地址,但组件库自带的级联选择往往存在局限性:只能选择到具体城市、无法多选父级地区、展示不够直观等。今天我将带大家从零开始,开发一个支持平铺多级展示、多选、搜索功能的PC端区域选择组件,并分享一些优化思路。
一、需求分析与组件设计
1. 为什么需要自定义区域选择组件?
在招聘系统中,HR发布职位时经常需要选择多个工作地点,比如"北京-海淀区"、"上海-浦东新区"等组合。现有组件存在以下问题:
- 原生级联选择器只能单选,无法满足多选需求
- 无法选择父级地区(如只选择"北京市"而不选择具体区)
- 展示方式为树形结构,不够直观,操作效率低
- 缺少已选地区的集中管理
原生级联选择组件功能展示:

优化后的组件:

2. 核心功能需求梳理
基于实际业务场景,我们需要实现以下功能:
- 支持多选地区,包括父级和子级地区
- 采用平铺方式展示地区数据,提高选择效率
- 通过面包屑导航实现层级切换
- 右侧面板展示已选地区,支持单独删除和一键清空
- 支持国内/国外地区切换
- 提供搜索功能快速定位地区
二、基础版本实现
如何实现地区数据的获取与处理?
首先,我们需要从接口获取地区数据并进行扁平化处理,以便后续操作:
async getTreeData() {
try {
const res = await request.get(
`${service.atsService}/base/atsArea/queryAreaTree`,
{
homeAbroad: '1', // 1:国内,2:国外,这是接口约定的参数值
}
)
const { flatArr, idMap } = this.getFlattenTree(res.result)
this.treeData = flatArr
this.treeIdObj = idMap
// 100000是根节点ID,表示全国,这里获取所有省份数据
this.showCity = this.treeData.filter(
(item) => item.parentId === '100000'
)
if (this.value) {
this.getCurrent()
}
} catch (e) {
console.log('获取地区数据失败->', e)
}
}
// 树结构平铺,将嵌套的树形结构转换为一维数组,便于后续查找和操作
getFlattenTree(tree) {
const flatArr = []
const idMap = {} // 创建ID到节点的映射,实现O(1)复杂度查找
tree.forEach((itemNode) => {
flattenTree(itemNode)
})
function flattenTree(node) {
const newNode = {
title: node.title, // 地区名称
id: node.id, // 地区ID
key: node.key, // 用于React/Vue列表渲染的唯一key
checked: false, // 选中状态
parentId: node.parentId, // 父级ID
leaf: node.leaf, // 是否为叶子节点(是否还有子级)
}
flatArr.push(newNode)
idMap[node.id] = newNode
// 递归处理子节点
if (node.children && node.children.length > 0) {
node.children.forEach((child) => flattenTree(child))
}
}
return { flatArr, idMap }
}
这段代码的关键在于将树形结构的数据扁平化为一维数组,并创建一个以ID为键的映射表,大大提高了后续查找的效率。
如何实现地区多选功能?
多选是本组件的核心功能之一,实现方式如下:
// 点击了省市区的checkbox
onCheckboxChange(checked, checkItem) {
if (checked) {
// 将完整的地区路径保存(从省份到当前选择项)
this.alreadyCheckedCity.push([...this.stackProvince, checkItem])
}
}
// 处理保存操作
handleSave() {
this.selectCityCode = []
this.selectOption = this.alreadyCheckedCity.map((item) => {
if (Array.isArray(item) && item.length > 0) {
// 拼接地区名称(如:北京/海淀区)
const label = item.map((v) => v.title).join('/')
// 拼接地区编码(如:110000,110108)
const value = item.map((v) => v.key).join(',')
this.selectCityCode.push(value)
return { label, value }
}
return
})
// 使用'/'分隔多个地区选择(如:110000,110108/310000,310115)
this.$emit('input', this.selectCityCode.join('/'))
this.$emit('change', this.selectCityCode.join('/'))
this.dropOpen = false
}
我们使用一个二维数组 alreadyCheckedCity 来存储已选地区,每条记录包含从省份到选择项的完整路径,这样既可以保存完整的地区层级信息,又能方便地拼接展示文本。
如何实现层级导航和展示?
为了实现直观的层级导航,我们使用面包屑组件并结合动态渲染:
// 点击了省市区的文字,进入下一级
childCheck(child) {
if (child.leaf) return // 如果是叶子节点(如:北京市的区),则不继续深入
this.stackProvince.push(child) // 将当前地区添加到路径栈中
// 过滤出当前地区的所有子地区
this.showCity = this.treeData.filter((item) => item.parentId === child.id)
}
// 面包屑点击事件,返回上一级
onBreadClick(item, index) {
// 显示对应层级的子地区
this.showCity = this.treeData.filter(child => child.parentId === item.id)
// 截断路径栈,保留到点击的层级
this.stackProvince = this.stackProvince.slice(0, index)
}
// 渲染地区列表
renderContent() {
return (
<div class="city-box">
<div class="provice-title">
<a-breadcrumb separator=">">
<a-breadcrumb-item>
<span style="cursor:pointer" onClick={() => {
// 点击"全部省份"返回根层级
this.showCity = this.treeData.filter(item => item.parentId === '100000')
this.stackProvince = []
}}>
全部省份
</span>
</a-breadcrumb-item>
{this.stackProvince.map((item, index) => {
return (
!item.leaf && (
<a-breadcrumb-item href="javascript:void(0);">
<span onClick={() => this.onBreadClick(item, index)}>
{item.title}
</span>
</a-breadcrumb-item>
)
)
})}
</a-breadcrumb>
</div>
{this.showCity.length > 0 &&
this.showCity.map((item) => (
<div class="city-item">
<span class="check-content">
<a-checkbox
v-model={item.checked}
onChange={(e) => this.onCheckboxChange(e.target.checked, item)}
></a-checkbox>
</span>
<div style="display:flex;align-items:center;" onClick={() => this.childCheck(item)}>
<a-tooltip title={item.title.length > 4 ? item.title : null}>
<span class={item.leaf ? 'city-name' : 'city-name city-name--hover'}>
{item.title}
</span>
</a-tooltip>
{!item.leaf && <span class="arrow">{this.rightArrowSvg()}</span>}
</div>
</div>
))}
</div>
)
}
这种实现方式让用户可以像浏览文件夹一样逐层深入选择地区,同时保持了清晰的层级关系。
[展示层级导航效果图]
如何实现已选地区的管理?
为了方便用户管理已选地区,我们在右侧面板展示所有已选项,并提供删除功能:
// 清空已选或删除单个选项
onClear(index) {
if (index === undefined) {
// 清空所有选中项
this.alreadyCheckedCity.forEach(item => {
if (Array.isArray(item) && item.length > 0) {
const lastChild = item[item.length - 1]
lastChild.checked = false
}
})
this.alreadyCheckedCity = []
} else {
// 删除单个选中项
const item = this.alreadyCheckedCity[index]
if (Array.isArray(item) && item.length > 0) {
const lastChild = item[item.length - 1]
lastChild.checked = false
}
this.alreadyCheckedCity.splice(index, 1)
}
}
// 渲染右侧已选面板
renderRightShow() {
return (
<div class="right-show" ref="rightShowRef">
<div>
<div class="check-header">
<div>
<span class="show__title">已选地区</span>
<span class="show__count">
<span class="cur__count">{this.alreadyCheckedCity.length}</span>
/99
</span>
</div>
<span class="clear_check" onClick={() => this.onClear()}>清空已选</span>
</div>
<ul class="selected-box">
{this.alreadyCheckedCity.map((item, index) => (
<li class="show-item">
<span class="show__name">
{Array.isArray(item) && item.map((item) => item.title).join('/')}
</span>
<span class="show__close" onClick={() => this.onClear(index)}>
<a-icon type="close-circle" style="color:#bfc3c7;width: 8px;" />
</span>
</li>
))}
</ul>
</div>
</div>
)
}
三、组件优化方案
1. 性能优化:如何避免重复渲染和无效计算?
问题分析:
当前实现中,每次数据变化都可能导致组件的重新渲染,影响性能。
优化方案:
// 优化前:直接在模板中使用方法
renderContent() {
// ... 渲染逻辑
}
// 优化后:使用计算属性缓存结果
computed: {
displayCityList() {
// 只在依赖数据变化时重新计算
return this.treeData.filter(item => item.parentId === this.currentParentId)
}
}
// 优化前:频繁操作DOM
mounted() {
document.addEventListener('mousedown', this.handleOutsideClick)
}
beforeDestroy() {
document.removeEventListener('mousedown', this.handleOutsideClick)
}
// 优化后:使用Vue指令或ref
// 在组件内部使用自定义指令或ref来处理点击外部关闭的逻辑
2. 用户体验优化:如何实现搜索功能?
问题分析:
当前组件缺少搜索功能,当地区数量较多时,用户查找困难。
优化方案:
// 添加搜索功能
data() {
return {
// ... 原有数据
searchKeyword: ''
}
},
computed: {
filteredCities() {
if (!this.searchKeyword) {
return this.showCity
}
return this.showCity.filter(item =>
item.title.includes(this.searchKeyword)
)
}
},
methods: {
// 搜索方法
handleSearch(e) {
this.searchKeyword = e.target.value
}
}
// 在模板中添加搜索框
// <input type="text" v-model="searchKeyword" placeholder="搜索地区" />
[展示搜索功能效果图]
3. 代码健壮性:如何处理异步加载和错误状态?
问题分析:
当前组件在数据加载过程中没有状态提示,错误处理也比较简单。
优化方案:
data() {
return {
// ... 原有数据
loading: false,
error: null
}
},
async getTreeData() {
this.loading = true
this.error = null
try {
// ... 原有数据获取逻辑
} catch (e) {
console.error('获取地区数据失败:', e)
this.error = '获取地区数据失败,请稍后重试'
} finally {
this.loading = false
}
}
// 在模板中添加加载和错误状态展示
// {this.loading && <div class="loading">加载中...</div>}
// {this.error && <div class="error">{this.error}</div>}
4. 代码结构优化:如何分离关注点?
问题分析:
当前组件既负责数据获取又负责展示,不符合单一职责原则。
优化方案:
// 1. 将数据获取逻辑抽离为单独的服务
// src/services/areaService.js
import request from '@/utils/http'
import service from '@/api/service'
export const getAreaTree = async (type = '1') => {
try {
// 调用地区树接口,参数type=1表示国内,type=2表示国外
const res = await request.get(`${service.atsService}/base/atsArea/queryAreaTree`, {
homeAbroad: type
})
return res.result
} catch (error) {
console.error('获取地区树失败:', error)
throw error
}
}
export const flattenAreaTree = (tree) => {
// 扁平化处理逻辑...
}
// 2. 在组件中使用
import { getAreaTree, flattenAreaTree } from '@/services/areaService'
// 组件内使用
async getTreeData() {
this.loading = true
try {
const treeData = await getAreaTree('1') // 获取国内地区数据
const { flatArr, idMap } = flattenAreaTree(treeData)
// ... 后续处理
} catch (error) {
// 错误处理
} finally {
this.loading = false
}
}
5. 功能扩展:如何支持更多自定义配置?
问题分析:
当前组件的配置项较少,难以适应不同的业务场景。
优化方案:
props: {
// ... 原有属性
maxSelectCount: {
type: Number,
default: 99 // 默认最大选择数量
},
showSearch: {
type: Boolean,
default: true // 是否显示搜索框
},
showForeign: {
type: Boolean,
default: true // 是否显示国外选项
},
placeholder: {
type: String,
default: '请选择地区'
},
// 支持自定义地区数据,适用于没有接口的情况
customAreaData: {
type: Array,
default: () => []
}
},
// 在数据获取时优先使用自定义数据
async getTreeData() {
if (this.customAreaData && this.customAreaData.length > 0) {
const { flatArr, idMap } = this.getFlattenTree(this.customAreaData)
this.treeData = flatArr
this.treeIdObj = idMap
this.showCity = this.treeData.filter(item => item.parentId === '100000')
if (this.value) {
this.getCurrent()
}
return
}
// 原有接口获取逻辑...
}
四、最佳实践与总结
- 性能优先:合理使用计算属性、缓存数据、避免不必要的DOM操作
- 用户体验:提供搜索、加载状态、错误提示等增强功能
- 代码健壮性:完善错误处理、边界情况检查
- 可维护性:遵循单一职责原则,分离数据获取和展示逻辑
- 扩展性:提供足够的配置项,支持不同业务场景
通过以上优化,我们的地区选择组件不仅功能完善,而且性能优良,能够为用户提供流畅直观的操作体验。在实际项目中,你还可以根据具体需求进一步定制和扩展。
五、完整优化版本代码
下面是整合了以上优化点的完整组件代码:
import request from '@/utils/http'
import service from '@/api/service'
export default {
name: 'AreaSelect',
props: {
value: {
type: String,
default: '',
// 父组件传递的已选地区值,格式为:110000,110108/310000,310115
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择'
},
maxSelectCount: {
type: Number,
default: 99 // 最大选择数量限制
},
showSearch: {
type: Boolean,
default: true // 是否显示搜索框
},
showForeign: {
type: Boolean,
default: true // 是否显示国外选项
},
customAreaData: {
type: Array,
default: () => [] // 自定义地区数据,优先级高于接口数据
}
},
data() {
return {
treeData: [], // 扁平化后的完整地区数据
treeIdObj: {}, // 以ID为键的地区数据映射,用于快速查找
showCity: [], // 当前页面展示的地区数据
dropOpen: false, // 下拉面板是否展开
tabKey: '1', // 当前选中的标签页(1:国内,2:国外)
stackProvince: [], // 当前面包屑路径栈
alreadyCheckedCity: [], // 已选地区列表,二维数组保存完整路径
selectOption: [], // 已选地区的选项格式,用于select组件显示
selectCityCode: [], // 已选地区的编码列表
searchKeyword: '', // 搜索关键词
loading: false, // 数据加载状态
error: null // 错误信息
}
},
computed: {
// 过滤后的地区列表,支持搜索功能
filteredCities() {
if (!this.searchKeyword) {
return this.showCity
}
return this.showCity.filter(item =>
item.title.toLowerCase().includes(this.searchKeyword.toLowerCase())
)
},
// 是否可以继续添加地区
canAddMore() {
return this.alreadyCheckedCity.length < this.maxSelectCount
},
// 当前父级ID,用于计算属性依赖追踪
currentParentId() {
return this.stackProvince.length > 0
? this.stackProvince[this.stackProvince.length - 1].id
: '100000'
}
},
watch: {
// 监听value变化,同步选中状态
value: {
immediate: true,
handler(val) {
if (val && this.treeData.length > 0) {
this.getCurrent()
}
}
}
},
created() {
this.getTreeData()
},
mounted() {
// 添加点击外部关闭下拉面板的事件监听
document.addEventListener('mousedown', this.handleOutsideClick)
},
beforeDestroy() {
// 组件销毁前移除事件监听
document.removeEventListener('mousedown', this.handleOutsideClick)
},
methods: {
// 点击外部关闭下拉面板
handleOutsideClick(e) {
const dropRenderBox = document.querySelector('.drop-render-box')
if (dropRenderBox && !dropRenderBox.contains(e.target)) {
this.dropOpen = false
}
},
// 获取地区树数据
async getTreeData() {
this.loading = true
this.error = null
try {
// 优先使用自定义数据
if (this.customAreaData && this.customAreaData.length > 0) {
const { flatArr, idMap } = this.getFlattenTree(this.customAreaData)
this.treeData = flatArr
this.treeIdObj = idMap
} else {
// 调用接口获取数据,1表示国内地区
const res = await request.get(`${service.atsService}/base/atsArea/queryAreaTree`, {
homeAbroad: '1',
})
const { flatArr, idMap } = this.getFlattenTree(res.result)
this.treeData = flatArr
this.treeIdObj = idMap
}
// 默认显示全国所有省份(parentId='100000')
this.showCity = this.treeData.filter(item => item.parentId === '100000')
// 如果有初始值,同步选中状态
if (this.value) {
this.getCurrent()
}
} catch (e) {
console.error('获取地区数据失败:', e)
this.error = '获取地区数据失败,请稍后重试'
} finally {
this.loading = false
}
},
// 根据传入的value同步选中状态
getCurrent() {
const val = this.value
// 按'/'分割多个地区选择
const selectedAllCode = val.split('/')
this.selectCityCode = [...selectedAllCode]
// 处理每个地区选择
this.selectCityCode.forEach((item) => {
const codes = item.split(',') // 按','分割地区编码路径
// 查找每个编码对应的地区信息
const arr = codes.reduce((pre, cur) => {
pre.push(this.treeIdObj[cur])
return pre
}, [])
this.alreadyCheckedCity.push(arr)
})
// 合并所有选中的地区编码并去重
const selectedCode = [
...new Set(
selectedAllCode.reduce((pre, cur) => {
pre.push(...cur.split(','))
return pre
}, [])
),
]
// 生成select组件的选项格式
this.selectOption = selectedAllCode.map((value) => {
const label = value
.split(',')
.map((c) => this.treeIdObj[c].title)
.join('/')
return { label, value }
})
// 更新选中状态
selectedCode.forEach((code) => {
const item = this.treeIdObj[code]
if (item) {
item.checked = true
}
})
},
// 树结构平铺处理
getFlattenTree(tree) {
const flatArr = []
const idMap = {}
// 递归处理树节点
function flattenTree(node) {
const newNode = {
title: node.title,
id: node.id,
key: node.key,
checked: false,
parentId: node.parentId,
leaf: node.leaf,
}
flatArr.push(newNode)
idMap[node.id] = newNode
// 递归处理子节点
if (node.children && node.children.length > 0) {
node.children.forEach((child) => flattenTree(child))
}
}
// 处理顶层节点
tree.forEach((itemNode) => {
flattenTree(itemNode)
})
return { flatArr, idMap }
},
// 渲染右侧箭头图标
rightArrowSvg() {
return (
<svg
width="10px"
height="10px"
viewBox="0 0 18 18"
color="#146aff"
class="area-icon-right"
>
<g fill="none" fill-rule="evenodd">
<path d="M0 0h18v18H0z"></path>
<path
d="M10.5 9L5.304 3.04a1.32 1.32 0 0 1 .003-1.691.956.956 0 0 1 1.473-.004l5.914 6.786c.21.24.31.555.305.869.006.314-.096.63-.305.87l-5.914 6.785a.956.956 0 0 1-1.473-.004 1.32 1.32 0 0 1-.003-1.69L10.499 9z"
fill="currentColor"
></path>
</g>
</svg>
)
},
// 处理checkbox选择事件
onCheckboxChange(checked, checkItem) {
if (checked) {
// 添加到已选列表,保存完整路径
this.alreadyCheckedCity.push([...this.stackProvince, checkItem])
} else {
// 取消选择时,从已选列表中移除
const index = this.alreadyCheckedCity.findIndex(item =>
item.length > 0 && item[item.length - 1].id === checkItem.id
)
if (index > -1) {
this.alreadyCheckedCity.splice(index, 1)
}
}
},
// 点击地区名称进入下一级
childCheck(child) {
if (child.leaf) return // 叶子节点不能继续深入
this.stackProvince.push(child)
// 过滤出当前地区的子地区
this.showCity = this.treeData.filter((item) => item.parentId === child.id)
},
// 面包屑点击事件
onBreadClick(item, index) {
// 显示对应层级的子地区
this.showCity = this.treeData.filter(
(child) => child.parentId === item.id
)
// 截断路径栈
this.stackProvince = this.stackProvince.slice(0, index)
},
// 搜索处理
handleSearch(e) {
this.searchKeyword = e.target.value
},
// 清空已选或删除单个选项
onClear(index) {
if (index === undefined) {
// 清空所有选中项
this.alreadyCheckedCity.forEach(item => {
if (Array.isArray(item) && item.length > 0) {
const lastChild = item[item.length - 1]
lastChild.checked = false
}
})
this.alreadyCheckedCity = []
} else {
// 删除单个选中项
const item = this.alreadyCheckedCity[index]
if (Array.isArray(item) && item.length > 0) {
const lastChild = item[item.length - 1]
lastChild.checked = false
}
this.alreadyCheckedCity.splice(index, 1)
}
},
// 处理取消操作
handleCancel() {
this.dropOpen = false
},
// 处理保存操作
handleSave() {
this.selectCityCode = []
this.selectOption = this.alreadyCheckedCity.map((item) => {
if (Array.isArray(item) && item.length > 0) {
const label = item.map((v) => v.title).join('/')
const value = item.map((v) => v.key).join(',')
this.selectCityCode.push(value)
return { label, value }
}
return
}).filter(Boolean)
// 触发input和change事件,将选中的地区编码通过'/'分隔的字符串形式传递给父组件
this.$emit('input', this.selectCityCode.join('/'))
this.$emit('change', this.selectCityCode.join('/'))
this.dropOpen = false
},
// 渲染下拉内容
dropdownRender() {
const tabOptions = [
{ title: '国内', key: '1' },
{ title: '国外', key: '2' }
]
// 如果不显示国外选项,过滤掉国外标签
const displayTabs = this.showForeign ? tabOptions : tabOptions.filter(tab => tab.key === '1')
return (
<div class="drop-render-box">
<div class="content-box">
<div class="left-check">
<a-tabs animated={false} v-model={this.tabKey}>
{displayTabs.map((item) => (
<a-tab-pane key={item.key} tab={item.title}></a-tab-pane>
))}
</a-tabs>
{/* 加载状态 */}
{this.loading && <div class="loading">加载中...</div>}
{/* 错误状态 */}
{this.error && <div class="error">{this.error}</div>}
{/* 国内地区内容 */}
{!this.loading && !this.error && this.tabKey === '1' && this.renderContent()}
{/* 国外地区内容 */}
{!this.loading && !this.error && this.tabKey === '2' && this.renderForeignContent()}
</div>
{/* 右侧已选地区面板 */}
{this.renderRightShow()}
</div>
{/* 底部按钮 */}
<div class="drop-footer-btn">
<a-button
size="small"
style="margin-right: 12px;"
onClick={this.handleCancel}
>
取消
</a-button>
<a-button
size="small"
type="primary"
onClick={() => this.handleSave()}
>
确定
</a-button>
</div>
</div>
)
},
// 渲染国内地区内容
renderContent() {
return (
<div class="city-box">
{/* 面包屑导航 */}
<div class="provice-title">
<a-breadcrumb separator=">">
<a-breadcrumb-item>
<span
style="cursor:pointer"
onClick={() => {
this.showCity = this.treeData.filter(
(item) => item.parentId === '100000'
)
this.stackProvince = []
}}
>
全部省份
</span>
</a-breadcrumb-item>
{this.stackProvince.map((item, index) => {
return (
!item.leaf && (
<a-breadcrumb-item href="javascript:void(0);">
<span
onClick={() => {
this.onBreadClick(item, index)
}}
>
{item.title}
</span>
</a-breadcrumb-item>
)
)
})}
</a-breadcrumb>
</div>
{/* 搜索框 */}
{this.showSearch && (
<div class="search-container">
<input
type="text"
placeholder="搜索地区"
value={this.searchKeyword}
onChange={this.handleSearch}
/>
</div>
)}
{/* 地区列表 */}
{this.filteredCities.length > 0 &&
this.filteredCities.map((item) => (
<div class="city-item">
<span class="check-content">
<a-checkbox
v-model={item.checked}
disabled={!this.canAddMore && !item.checked}
onChange={(e) => {
this.onCheckboxChange(e.target.checked, item)
}}
></a-checkbox>
</span>
<div
style="display:flex;align-items:center;"
onClick={() => this.childCheck(item)}
>
<a-tooltip title={item.title.length > 4 ? item.title : null}>
<span
class={
item.leaf ? 'city-name' : 'city-name city-name--hover'
}
>
{item.title}
</span>
</a-tooltip>
{!item.leaf && (
<span class="arrow">{this.rightArrowSvg()}</span>
)}
</div>
</div>
))}
{/* 无搜索结果提示 */}
{this.searchKeyword && this.filteredCities.length === 0 && (
<div class="no-result">暂无匹配结果</div>
)}
</div>
)
},
// 渲染国外地区内容
renderForeignContent() {
return <div class="city-box">暂无数据</div>
},
// 渲染右侧已选面板
renderRightShow() {
return (
<div class="right-show" ref="rightShowRef">
<div>
<div class="check-header">
<div>
<span class="show__title">已选地区</span>
<span class="show__count">
<span class="cur__count">
{this.alreadyCheckedCity.length}
</span>
/{this.maxSelectCount}
</span>
</div>
<span
class="clear_check"
onClick={() => this.onClear()}
style={{cursor: this.alreadyCheckedCity.length > 0 ? 'pointer' : 'not-allowed'}}
>
清空已选
</span>
</div>
<ul class="selected-box">
{this.alreadyCheckedCity.map((item, index) => {
return (
<li class="show-item">
<span class="show__name">
{Array.isArray(item) &&
item.map((item) => item.title).join('/')}
</span>
<span
class="show__close"
onClick={() => {
this.onClear(index)
}}
>
<a-icon
type="close-circle"
style="color:#bfc3c7;width: 8px;"
/>
</span>
</li>
)
})}
</ul>
</div>
</div>
)
}
},
// 组件渲染
render() {
return (
<div class="area-select">
<a-select
style="width:100%;max-width:300px"
placeholder={this.placeholder}
open={this.dropOpen}
maxTagCount={1}
autoClearSearchValue={true}
mode="multiple"
disabled={this.disabled}
v-model={this.selectCityCode}
dropdownRender={this.dropdownRender}
onFocus={() => {
this.dropOpen = true
}}
options={this.selectOption}
></a-select>
</div>
)
},
}
</script>
<style lang="less" scoped>
.drop-render-box {
position: relative;
width: 580px;
height: 350px;
background: #fff;
padding: 10px 16px 16px;
border-radius: 8px;
border: 1px solid #ccc;
font-size: 12px;
color: #0e1114;
.search-container {
padding: 0px 10px 10px;
/deep/ .ant-input {
border-radius: 8px;
height: 30px;
}
}
.content-box {
width: 100%;
display: flex;
border-bottom: 1px solid #f0f2f5;
.left-check {
width: 80%;
box-sizing: border-box;
border-right: 1px solid #f0f2f5;
}
.right-show {
width: 40%;
.check-header {
height: 40px;
line-height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 8px;
.show__title {
margin-right: 6px;
}
.show__count {
color: #bfc3c7;
.cur__count {
color: #565e66;
}
}
.clear_check {
cursor: pointer;
}
}
.selected-box {
overflow-y: auto;
max-height: 250px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
}
.show-item {
display: inline-block;
margin-left: 10px;
padding: 4px 0;
.show__name {
margin-right: 4px;
}
.show__close {
cursor: pointer;
}
}
}
.city-box {
height: 230px;
overflow-y: auto;
display: flex;
align-items: center;
align-content: flex-start;
flex-wrap: wrap;
/deep/ .ant-checkbox-inner {
width: 12px;
height: 12px;
&::after {
top: 4px;
left: 1px;
width: 5px;
height: 10px;
}
}
row-gap: 8px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
}
&::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
}
.provice-title {
width: 100%;
padding-bottom: 2px;
background: #fff;
z-index: 99;
position: sticky;
top: 0;
}
.city-item {
width: 110px;
margin-left: 4px;
display: flex;
align-items: center;
.check-content {
margin-right: 6px;
}
.city-name {
display: inline-block;
max-width: 60px;
font-size: 12px;
cursor: pointer;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.city-name--hover {
&:hover {
color: #1890ff;
+ .arrow {
display: inline;
}
}
}
.arrow {
display: none;
}
}
.loading,
.error,
.no-result {
width: 100%;
height: 230px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
.error {
color: #ff4d4f;
}
}
.drop-footer-btn {
position: absolute;
bottom: 0;
right: 0;
display: flex;
justify-content: flex-end;
padding: 8px 10px;
/deep/ .ant-btn {
display: inline-block;
width: 60px;
height: 28px;
text-align: center;
line-height: 28px;
}
}
}
</style>

浙公网安备 33010602011771号