级联选择器,用于多层级数据的选择,典型场景为省市区选择
1 简易版
1.1效果展示
1.2 组件源码
<template>
<div>
<div
v-show="visible"
class="mask"
@click="close"
/>
<transition name="fade">
<div
v-show="visible"
class="cascader"
>
<div class="title">
{{ title }}
<i
v-if="leftIcon"
:class="['back', {'arrow': leftIcon === 'arrow', 'cross': leftIcon === 'cross'}]"
@click="close"
/>
div>
<ul class="nav-list line">
<template v-for="(item, index) in (tabIndex + 1)">
<li
:key="index"
:class="['nav-list-item', {'active': index === tabIndex, 'more-hidden': moreHidden}]"
@click="changeIndex(index)"
>
{{ (selectedOptions[index] && selectedOptions[index][fieldNames.text]) || placeholderText }}
li>
template>
ul>
<ol
class="select-list"
ref="selectListRef"
>
<template v-for="(item, index) in pickerDataArr[tabIndex]">
<li
:key="index"
class="select-list-item line"
@click="selectItem(item)"
>
{{ item[fieldNames.text] }}
li>
template>
ol>
div>
transition>
div>
template>
<script>
export default {
name: 'cascaderPicker',
props: {
visible: {
type: Boolean,
default: false
},
options: {
type: Array,
default: () => []
},
title: {
type: String,
default: ''
},
placeholderText: {
type: String,
default: '请选择'
},
moreHidden: {
type: Boolean,
default: false
},
level: {
type: Number,
default: 3
},
fieldNames: {
type: Object,
default: () => ({
text: 'text',
value: 'value',
chlidren: 'children'
})
},
leftIcon: {
type: String,
default: 'arrow'
}
},
data() {
return {
tabIndex: 0,
selectedOptions: [],
pickerDataArr: []
}
},
watch: {
tabIndex () {
this.$refs['selectListRef'].scrollTop = 0
},
visible (val) {
if (val) {
this.tabIndex = 0
this.selectedOptions = []
this.pickerDataArr = []
this.pickerDataArr.push(this.options)
}
}
},
methods: {
changeIndex (index) {
if (this.visible && index < this.tabIndex) {
this.selectedOptions.splice(index)
this.pickerDataArr.splice(index + 1)
this.tabIndex = index
}
},
selectItem (item) {
if(!this.visible) return
this.selectedOptions.push(item)
if (!(item[this.fieldNames.children] && item[this.fieldNames.children].length) || this.tabIndex + 1 >= this.level) {
this.$emit('finish', this.selectedOptions)
return
}
this.pickerDataArr.push(item[this.fieldNames.children])
this.tabIndex++
},
close () {
this.$emit('close')
this.$emit('update:visible', false)
}
}
}
script>
<style lang="less" scoped>
.mask{
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 99;
}
.cascader{
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 80%;
background-color: #fff;
border-radius: 16px 16px 0 0;
display: flex;
flex-direction: column;
z-index: 999;
.title{
position: relative;
padding: 20px 0;
font-size: 18px;
line-height: 18px;
text-align: center;
.back{
position: absolute;
top: 19px;
left: 20px;
width: 20px;
height: 20px;
}
.arrow{
background: url('./images/icon-arrow.png') no-repeat;
background-size: 100%;
}
.cross{
background: url('./images/icon-close.png') no-repeat;
background-size: 100%;
}
}
.nav-list{
position: relative;
display: flex;
padding: 0 20px;
font-size: 16px;
text-align: center;
.nav-list-item{
padding: 14px 0;
margin-right: 16px;
max-width: 64px;
}
.nav-list-item:last-child{
margin-right: 0;
}
.active{
position: relative;
color: #1752ff;
}
.active::after{
position: absolute;
left: 0;
bottom: 0;
display: block;
content: '';
width: 100%;
height: 3px;
background-color: #1752ff;
}
.more-hidden{
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.select-list{
flex: 1;
overflow-y: auto;
font-size: 16px;
.select-list-item{
position: relative;
padding: 12px 20px;
}
}
.line::after{
position: absolute;
left: 0;
bottom: 0;
display: block;
content: '';
width: 100%;
height: 1px;
background-color: #ccc;
transform: scaleY(0.5);
}
.select-list::-webkit-scrollbar{
display: none;
}
}
.fade-enter,.fade-leave-to{
opacity: 0;
transform: translateY(100%);
}
.fade-enter-active,.fade-leave-active{
transition: all 0.3s ease;
}
style>
2 高级版
2.1 效果展示
2.2 组件源码
<template>
<div>
<div
v-show="visible"
class="mask"
@click="close"
/>
<transition name="fade">
<div
v-show="visible"
class="cascader"
>
<div class="title">
{{ title }}
<i
v-if="leftIcon"
:class="['back', {'arrow': leftIcon === 'arrow', 'cross': leftIcon === 'cross'}]"
@click="close"
/>
div>
<ul class="nav-list">
<template v-for="(item, index) in navListLen">
<li
:key="index"
:class="['nav-list-item', {'active': index === tabIndex, 'more-hidden': moreHidden}]"
@click="changeIndex(index)"
>
{{ (selectedOptions[index] && selectedOptions[index][fieldNames.text]) || placeholderText }}
li>
template>
ul>
<div
class="select-content"
>
<div
class="select-content-box"
:style="translateStyle"
>
<ol
class="select-list"
v-for="(item1, index1) in pickerDataArr" :key="index1"
ref="selectListRefs"
>
<template v-for="(item2, index2) in item1">
<li
:key="index2"
class="select-list-item"
@click="selectItem(item2, index1)"
>
<span class="text">{{ item2[fieldNames.text] }}span>
<i
v-if="selectedOptions.includes(item2)"
class="choose-active"
/>
li>
template>
ol>
div>
div>
div>
transition>
div>
template>
<script>
export default {
name: 'cascaderPicker',
props: {
visible: {
type: Boolean,
default: false
},
options: {
type: Array,
default: () => []
},
title: {
type: String,
default: ''
},
placeholderText: {
type: String,
default: '请选择'
},
moreHidden: {
type: Boolean,
default: false
},
level: {
type: Number,
default: 3
},
fieldNames: {
type: Object,
default: () => ({
text: 'text',
value: 'value',
chlidren: 'children'
})
},
leftIcon: {
type: String,
default: 'arrow'
}
},
data() {
return {
tabIndex: 0,
selectedOptions: [],
pickerDataArr: [],
finishedFlag: false
}
},
created () {
this.pickerDataArr.push(this.options)
},
computed: {
navListLen () {
return !this.finishedFlag && this.selectedOptions.length < this.level ? this.selectedOptions.length + 1 : this.selectedOptions.length
},
translateStyle () {
return {
transform:`translateX(-${this.tabIndex * 100}%)`,
transitionDuration: '0.3s'
}
}
},
methods: {
changeIndex (index) {
if (!this.visible) return
this.tabIndex = index
},
selectItem (item, clickIndex) {
if (!this.visible || clickIndex !== this.tabIndex) return
this.finishedFlag = false
this.selectedOptions.splice(this.tabIndex, this.selectedOptions.length, item)
if (!(item[this.fieldNames.children] && item[this.fieldNames.children].length) || this.tabIndex + 1 >= this.level) {
this.finishedFlag = true
this.$emit('finish', this.selectedOptions)
return
}
this.$refs['selectListRefs'].forEach((ele, index) => {
if (index > this.tabIndex) {
ele.scrollTop = 0
}
})
this.tabIndex++
this.pickerDataArr.splice(this.tabIndex, this.pickerDataArr.length, item[this.fieldNames.children])
},
close () {
this.$emit('close')
this.$emit('update:visible', false)
},
resetPicker () {
this.tabIndex = 0
this.finishedFlag = false
this.selectedOptions = []
this.pickerDataArr = []
this.pickerDataArr.push(this.options)
this.$nextTick(() => {
if(this.$refs['selectListRefs'][0]) {
this.$refs['selectListRefs'][0].scrollTop = 0
}
})
}
}
}
script>
<style lang="less" scoped>
.mask{
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .7);
z-index: 99;
}
.cascader{
position: fixed;
left: 0;
bottom: 0;
width: 100%;
height: 80%;
background-color: #fff;
border-radius: 16px 16px 0 0;
display: flex;
flex-direction: column;
z-index: 999;
.title{
position: relative;
padding: 20px 0;
font-size: 18px;
line-height: 18px;
text-align: center;
.back{
position: absolute;
top: 19px;
left: 20px;
width: 20px;
height: 20px;
}
.arrow{
background: url('./images/icon-arrow.png') no-repeat;
background-size: 100%;
}
.cross{
background: url('./images/icon-close.png') no-repeat;
background-size: 100%;
}
}
.nav-list{
display: flex;
padding: 0 20px;
font-size: 16px;
text-align: center;
.nav-list-item{
padding: 14px 0;
margin-right: 16px;
max-width: 64px;
}
.nav-list-item:last-child{
margin-right: 0;
}
.active{
position: relative;
color: #1752ff;
}
.active::after{
position: absolute;
left: 0;
bottom: 0;
display: block;
content: '';
width: 100%;
height: 3px;
background-color: #1752ff;
}
.more-hidden{
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.select-content{
flex: 1;
overflow: hidden;
.select-content-box{
width: 100%;
height: 100%;
display: flex;
.select-list{
flex: 0 0 100%;
width: 100%;
height: 100%;
overflow-y: auto;
font-size: 16px;
.select-list-item{
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
.text{
max-width: 264px;
}
.choose-active{
width: 20px;
height: 20px;
background: url('./images/icon-choose.png') no-repeat;
background-size: 100%;
}
}
}
.line{
position: relative;
}
.line::after{
position: absolute;
left: 0;
bottom: 0;
display: block;
content: '';
width: 100%;
height: 1px;
background-color: #ccc;
transform: scaleY(0.5);
}
.select-list::-webkit-scrollbar{
display: none;
}
}
}
}
.fade-enter,.fade-leave-to{
opacity: 0;
transform: translateY(100%);
}
.fade-enter-active,.fade-leave-active{
transition: all 0.3s ease;
}
style>
3 使用说明
3.1 参数说明
参数 |
说明 |
类型 |
默认值 |
visible |
是否显示级联选择器,支持 .sync 修饰符 |
Boolean |
false |
options |
可选数据源 |
Array |
[] |
title |
顶部标题 |
String |
- |
placeholder-text |
各级未选中时的提示文案 |
String |
‘请选择’ |
more-hidden |
各级选择的结果文字展示溢出是否隐藏 |
Boolean |
false |
level |
自定义选择层级数 |
Number |
3 |
field-names |
自定义options结构中的字段 |
Object |
{ text: ‘text’,value: ‘value’,chlidren: ‘children’} |
left-icon |
关闭图标:arrow(左箭头)/cross(交叉) |
String |
arrow |
3.2 事件说明
事件 |
说明 |
回调参数 |
finish |
各级选择完成后触发 |
selectedOptions(各级选择结果组合) |
close |
点击关闭图标或者遮罩时触发 |
—— |
3.3 方法说明
方法 |
说明 |
参数 |
resetPicker |
重置级联选择器(仅限高级版使用) |
—— |
4 使用示例代码
<template>
<div class="home">
<div class="flex">
<input type="text" v-model="cascaderValue">
<button @click="showPicker">请选择所在地区button>
div>
<cascader-picker
ref="cascaderPicker"
:visible.sync="isShow"
:options="options"
title="请选择所在地区"
:level="3"
:field-names="{text: 'name', value: 'code', children: 'childList'}"
@finish="onFinish"
/>
div>
template>
<script>
import CascaderPicker from '../components/cascaderPicker'
import chinaArea from './json/chinaArea.json'
export default {
name: 'Home',
data() {
return {
cascaderValue: '',
isShow: false,
options: chinaArea?.childList || []
}
},
components: {
CascaderPicker
},
methods:{
showPicker() {
this.isShow = true
},
onFinish(selectedOptions) {
console.log('选择结果==>', selectedOptions)
this.cascaderValue = selectedOptions.map(item => item.name).join('/')
this.isShow = false
}
}
}
script>
<style lang="less" scoped>
.flex{
display: flex;
align-items: center;
padding: 20px;
input{
flex: 1;
}
}
style>
5 省市区json数据
{
"code": 100000,
"name": "中华人民共和国",
"childList": [{
"code": 110000,
"name": "北京市",
"childList": [{
"code": 110101,
"name": "东城区"
},
{
"code": 110102,
"name": "西城区"
},
{
"code": 110105,
"name": "朝阳区"
},
{
"code": 110106,
"name": "丰台区"
},
{
"code": 110107,
"name": "石景山区"
},
{
"code": 110108,
"name": "海淀区"
},
{
"code": 110109,
"name": "门头沟区"
},
{
"code": 110111,
"name": "房山区"
},
{
"code": 110112,
"name": "通州区"
},
{
"code": 110113,
"name": "顺义区"
},
{
"code": 110114,
"name": "昌平区"
},
{
"code": 110115,
"name": "大兴区"
},
{
"code": 110116,
"name": "怀柔区"
},
{
"code": 110117,
"name": "平谷区"
},
{
"code": 110118,
"name": "密云区"
},
{
"code": 110119,
"name": "延庆区"
}
]
}
]
}