前提
箭头图标使用的是阿里巴巴矢量图标库
1.属性
1.1 datas
- 三级级联数据 - 树状数据
- 类型为Array
- 只要按照这个数据格式的数据,各种关系的三级级联组件都可以使用
数组中对象示例如下
- label: 城市名字(显示的字段)
- value: 城市对应的编码
- children: 该城市下的子级 (例: 省份下的市区、市区下的区县)
{
"label": "北京市",
"value": "110000",
"children": [
{
"label": "市辖区",
"value": "110100",
"children": [
{
"label": "东城区",
"value": "110101"
},
{
"label": "西城区",
"value": "110102"
},
{
"label": "朝阳区",
"value": "110105"
},
{
"label": "丰台区",
"value": "110106"
},
{
"label": "石景山区",
"value": "110107"
},
{
"label": "海淀区",
"value": "110108"
},
{
"label": "门头沟区",
"value": "110109"
},
{
"label": "房山区",
"value": "110111"
},
{
"label": "通州区",
"value": "110112"
},
{
"label": "顺义区",
"value": "110113"
},
{
"label": "昌平区",
"value": "110114"
},
{
"label": "大兴区",
"value": "110115"
},
{
"label": "怀柔区",
"value": "110116"
},
{
"label": "平谷区",
"value": "110117"
},
{
"label": "密云区",
"value": "110118"
},
{
"label": "延庆区",
"value": "110119"
}
]
}
]
},
2.实例
2.1 示例
<script setup>
import datas from '@/assets/data.json' // 三级级联组件的树状数据
import cascader from "./cascader.vue"; // 三级级联组件
</script>
<template>
<div>
<cascader :datas="datas"></cascader>
</div>
</template>
2.2 实现 cascader.vue
<script setup>
const props = defineProps({
datas: {
type: Array,
default: () => ([])
}
})
const datas = computed(() => props.datas)
const selProvinceRef = ref(null)
const selCityRef = ref(null)
const selAreaRef = ref(null)
/**
*
* @param {HTMLElement} select 要填充的下拉列表
* @param {Array} list 被填充的数据, 数组
*/
function fillSelect (select, list) {
// 如果下拉列表有值 得先清除
const ul = select.querySelector('.options');
ul.innerHTML.length && (ul.innerHTML = '');
if(!list.length) {
select.classList.add('disabled')
setTitleText(select, '请选择')
return
}
select.classList.remove('disabled')
const tip = select.dataset.tip
setTitleText(select, `请选择${tip}`)
select.datas = list // 将目前填充的数据; 添加到dom对象的属性datas中
const Fragment = document.createDocumentFragment()
for (const item of list) {
const li = document.createElement('li');
li.textContent = item.label;
Fragment.appendChild(li);
}
ul.appendChild(Fragment)
// 以下也可以实现li填充
// ul.innerHTML = list.map(obj => `<li>${obj.label}</li>`).join('')
}
/**
* 注册公共的事件处理
* @param {HTMLElement} select 下拉框dom元素
*/
function regCommonEvent (select) {
// 1.title点击事件 - 打开下拉框
const titleDom = select.querySelector('.title')
titleDom.addEventListener('click', () => {
// 禁用状态下无法操作
if(select.classList.contains('disabled')) return
// 清除所有下拉框的下拉状态
const selectDomList = document.querySelectorAll('.select.expand')
for (const sel of selectDomList) {
if(sel !== select) {
sel.classList.remove('expand')
}
}
// 切换当前的下拉状态即可
select.classList.toggle('expand')
})
// 2.ul点击事件 - 下拉框内容选择
const ulDom = select.querySelector('.options')
ulDom.addEventListener('click', (e) => {
if(e.target.tagName !== 'LI') return
// 获取到之前选中的li元素
const beforeActiveLi = select.querySelector('li.active')
// 当刚打开下拉框的时候 此前都未选中 所以得做一个判定
beforeActiveLi && beforeActiveLi.classList.remove('active')
e.target.classList.add('active')
// 设置当前选中的值
setTitleText(select, e.target.textContent)
select.classList.remove('expand')
})
}
/**
* 注册省份的特殊点击事件
*/
function regProvinceEvent () {
const ul = selProvinceRef.value.querySelector('.options')
ul.addEventListener('click', (e) => {
if(e.target.tagName !== 'LI') return
const li = e.target
// 填充城市
const pr = selProvinceRef.value.datas.find(item => item.label === li.textContent)
fillSelect(selCityRef.value, pr.children)
// 填充地区
fillSelect(selAreaRef.value, [])
})
}
/**
* 注册城市的特殊点击事件
*/
function regCityEvent () {
const ul = selCityRef.value.querySelector('.options')
ul.addEventListener('click', (e) => {
if(e.target.tagName !== 'LI') return
const li = e.target
// 填充城市
const city = selCityRef.value.datas.find(item => item.label === li.textContent)
fillSelect(selAreaRef.value, city.children)
})
}
function init () {
fillSelect(selProvinceRef.value, datas.value)
fillSelect(selCityRef.value, []) // 一开始,无法填充城市
fillSelect(selAreaRef.value, []) // 一开始,无法填充地区
regCommonEvent(selProvinceRef.value)
regCommonEvent(selCityRef.value)
regCommonEvent(selAreaRef.value)
regProvinceEvent()
regCityEvent()
}
/**
*
* @param {HTMLElement} select 下拉框DOM元素
* @param {string} textContent title下的span文字
*/
function setTitleText (select, textContent) {
const span = select.querySelector('.title span')
span.textContent = textContent
}
onMounted(() => {
init()
})
</script>
<template>
<div class="cascader">
<div class="select" ref="selProvinceRef" data-tip="省份">
<div class="title">
<span>请选择</span>
<i class="iconfont icon-xiangxiajiantou"></i>
</div>
<ul class="options"></ul>
</div>
<div class="select" ref="selCityRef" data-tip="市区">
<div class="title">
<span>请选择</span>
<i class="iconfont icon-xiangxiajiantou"></i>
</div>
<ul class="options"></ul>
</div>
<div class="select" ref="selAreaRef" data-tip="区县">
<div class="title">
<span>请选择</span>
<i class="iconfont icon-xiangxiajiantou"></i>
</div>
<ul class="options"></ul>
</div>
</div>
</template>
<style scoped>
.cascader {
box-sizing: border-box;
}
ul {
padding: 0;
margin: 0;
}
li {
list-style: none;
}
.select {
display: inline-block;
margin: 0 5px;
position: relative;
white-space: nowrap;
color: #666;
}
.title {
min-width: 150px;
height: 40px;
line-height: 40px;
padding: 0 10px;
display: flex;
justify-content: space-between;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 5px;
cursor: pointer;
}
.title .iconfont {
/* font-size: 10px; */
transition: all .25s;
}
.options {
font-size: 12px;
border: 1px solid #ccc;
border-radius: 5px;
max-height: 300px;
min-width: 100%;
position: absolute;
padding: 10px;
top: 50px;
background-color: #fff;
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(auto-fit, 20px);
/* 行间距 */
row-gap: 6px;
column-gap: 26px;
/* 每一项左对齐 默认拉伸 */
justify-items: left;
box-shadow: 0 0 3px rgba(0, 0, 0, .5);
transform: scaleY(0);
opacity: 0;
transition: all .23s;
transform-origin: 10px -10px;
}
.options li {
cursor: pointer;
padding: 3px 6px;
border-radius: 5px;
}
.options li.active {
background-color: #eec05a;
}
.options::before {
content: "";
width: 10px;
height: 10px;
position: absolute;
left: 70px;
top: -6px;
border: 1px solid #ccc;
transform: rotate(45deg);
border-bottom: none;
border-right: none;
background-color: #fff;
}
.select.expand .options {
transform: scaleY(1);
opacity: 1;
}
.select.expand .iconfont {
transform: rotate(180deg);
}
.select.disabled {
color: #ccc;
}
.select.disabled .title {
cursor: not-allowed;
}
</style>
最后
请各位大佬请教指正, 很想学封装组件这一类的知识。