vue3+el-cascader-panel+多选+动态加载+默认展开+选中查询节点并展开+查询到的这一条自动滚动到顶部+tooltip效果
动态加载之前已经发过一次,有想了解的的可在本人主页找下
一.效果展示
1.打开弹框默认展开
2.展示不全的节点才出现鼠标悬浮效果
3.选中查询的节点并展开各级
例如:选中节点第一条节点
备注:当前可选中的数据只是当下一级下的所有二三四级(避免卡顿,就没有实现所有的一级下的二三四级),当切换一级,更新下拉数据,因此只实现了当前被选中的一级下的子级展开
选中后自动展开对应的三四级(因为默认已经展开了某个一级下的所有二级)
功能总结:
- 可点击父级展开下一个子级(效果同elementPlus中的级联面板)
- 可在下拉中输入要查找的节点(边输入边更新与输入内容匹配的节点,与elementPlus中select可查询组件效果一致)
- 可在下拉选中某一条数据,并展开对应的三四级,并且自动滚动到顶部
- 部分节点展示不全,有鼠标悬浮展示这一条全部信息
二.默认展开节点
1.监听弹框打开,展开默认一级下的二级节点
watch(
() => props.visible,
(val) => {
searchData.value = '' // 清空搜索框
if (val) {
time2.value = setTimeout(() => {
const index = collectOptions.value.findIndex((item) => item.name === 'US') // 默认选中US,通过US去当下一级的option去获取对应的的index
toClickSecondCascaderCollect(index, 0) //将index传给toClickSecondCascaderCollect,0代表级联第一级数据,1代表第二级数据,2代表第三级数据
}, 1000)
}
}
)
2.通过操作dom,js实现点击对应的某一级
//触发点击事件,index 节点位置,number 级联面板四级中的一级
const toClickSecondCascaderCollect = (index, number?) => {
const el = document.querySelectorAll(`.el-cascader-menu`)[number].querySelectorAll(`.el-cascader-node`)
if (el && el[index]) {
return new Promise((resolve) => {
el[index].click() // 触发点击事件,展开传过来的index对应的节点
time.value = setTimeout(() => {
resolve() // 延迟1秒执行,防止执行过快,防止上一级没有展示出来就点击而导致找不到click报错
}, 1000)
})
}
return Promise.resolve()
}
三.鼠标悬浮效果
1.使用el-tooltip组件展示所有的节点的提示效果
<el-cascader-panel
ref="cascaderCollect"
v-model="collectValue"
:props="address"
:options="collectOptions"
@expandChange="handleExpandChange"
>
<template #default="{ node, data }">
//v-if为真,展示悬浮效果
<el-tooltip
v-if="isTextTruncated(data.name)"
effect="dark"
:content="data.name"
placement="top-start"
>
<span class="truncated-text">{{ data.name }}</span>
</el-tooltip>
//v-if为假,不展示悬浮效果,只展示纯文本
<span v-else class="regular-text">{{ data.name }}</span>
</template>
</el-cascader-panel>
//对应css
.truncated-text {
display: inline-block;
max-width: 213px; /* 设置你希望的宽度 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.regular-text {
white-space: normal; /* 显示完整文本,没有省略 */
}
2.判断当下节点文本长度,判断是否展示鼠标悬浮效果
const isTextTruncated = (text) => {
// 你可以实现逻辑检查文本是否超过某个长度
const maxLength = 28 // 可根据需要调整最大长度
return text.length > maxLength
}
四.展开选中节点
1.select下拉添加@change事件
<el-select-v2
v-model="searchData"
filterable
clearable
:options="searchDataOptions"
placeholder="请输入"
style="width: 700px"
@change="searchChange"
/>
2.select选中改变,就循环遍历选中节点,实现一级级逐层点击,以选择这一条为例"Health & Household,Food Wrap, Foils"
const searchChange = async (changeData) => {
// 点击后获取的changeData数据分3段数据,分别是对应二三四级要展开的节点
if (!changeData) return
const currdata = changeData?.split(',')
console.log('currdata', currdata) // ["Health & Household","Food Wrap", "Foils"]
for (let i = 0; i < 2; i++) {
let index = -1
if (i === 0) {
//先拿“Health & Household”去当前已经展开的第二级的options中去遍历,获取到index
index = optionSecond.value.findIndex((item) => item.name === currdata[0])
} else if (i === 1) {
//再拿“Food Wrap”去当前已经展开的第三级的options中去遍历,获取到index
let secondParams = optionSecond.value.find((item) => item.name === currdata[0])
console.log('secondParams', secondParams)
const res = await ApiBusiType.marketDataCollection.queryGraduallyCollectionConfig(secondParams)
optionThird.value = res.result || []
console.log('res1111', res)
index = optionThird.value.findIndex((item) => item.name === currdata[1])
}else if(i === 2){
//这一层判断仅仅是用来让第四级滚动到顶部
//再拿“Foils”去当前已经展开的第三级的options中去遍历,获取到index,
index = optionFourth.value.findIndex((item) => item.name === currdata[2])
}
if (index !== -1) {
await nextTick() // 等待数据渲染完成
await toClickSecondCascaderCollect(index, i + 1) 执行点击事件
}
}
}
//第一次index就是“Health & Household”在第二级所在的index,number是1,表示要点击第二级,展示出第三级别。
//第二次index就是“Food Wrap”在第二级所在的index,number是2,表示要点击刚展示的第三级,展示出第四级。此时就完成了展示一二三四级
const toClickSecondCascaderCollect = (index, number?) => {
const el = document.querySelectorAll(`.el-cascader-menu`)[number].querySelectorAll(`.el-cascader-node`)
if (el && el[index]) {
return new Promise((resolve) => {
el[index].click() // 触发点击事件,展开传过来的index对应的节点
time.value = setTimeout(() => {
resolve() // 延迟1秒执行,防止执行过快,防止上一级没有展示出来就点击而导致找不到click报错
}, 1000)
})
}
return Promise.resolve()
}
五.选中节点自动滚动到顶部
//触发点击事件,index 节点位置,number 级联面板四级中的一级
const toClickSecondCascaderCollect = (index, number?) => {
const el = document.querySelectorAll(`.el-cascader-menu`)[number].querySelectorAll(`.el-cascader-node`)
if (el && el[index]) {
return new Promise((resolve) => {
el[index].click() // 触发点击事件,展开传过来的index对应的节点
el[index].scrollIntoView({ behavior: 'smooth', block: 'start' }) //自动滚到顶部
time.value = setTimeout(() => {
resolve() // 延迟1秒执行,防止执行过快,防止上一级没有展示出来就点击而导致找不到click报错
}, 1000)
})
}
return Promise.resolve()
}
六.组件源码
<template>
<div>
<Dialog title="添加采集" :visible.sync="isShow" width="1030px" @close="handleClose">
<div class="header-title">
<div class="add-header">
<el-form :inline="true">
<el-form-item label="关键词搜索">
<el-select-v2
v-model="searchData"
filterable
clearable
:options="searchDataOptions"
placeholder="请输入"
style="width: 700px"
@change="searchChange"
/>
</el-form-item>
<el-form-item>
<!-- <el-button type="primary" @click="searchAddData">搜索</el-button> -->
</el-form-item>
</el-form>
</div>
</div>
<div class="selection-container">
<div class="selection-column">
<h3>商店名称</h3>
<!-- <el-checkbox-group v-model="ruleForm.collectSite">
<el-checkbox v-for="item in collectSiteOptions" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group> -->
</div>
<div class="selection-column">
<h3>类别</h3>
<!-- <el-checkbox-group v-model="ruleForm.category">
<el-checkbox v-for="item in categoryOptions" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group> -->
</div>
<div class="selection-column">
<h3>商品类型</h3>
<!-- <el-checkbox-group v-model="ruleForm.productType">
<el-checkbox v-for="item in productTypeOption" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group> -->
</div>
<div class="selection-column">
<h3>商品类型关键词</h3>
<!-- <el-checkbox-group v-model="ruleForm.productTypeKeyword">
<el-checkbox v-for="item in keywordOption" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group> -->
</div>
</div>
<el-cascader-panel
ref="cascaderCollect"
v-model="collectValue"
:props="address"
:options="collectOptions"
@expandChange="handleExpandChange"
>
<template #default="{ node, data }">
<el-tooltip
v-if="isTextTruncated(data.name)"
effect="dark"
:content="data.name"
placement="top-start"
>
<span class="truncated-text">{{ data.name }}</span>
</el-tooltip>
<span v-else class="regular-text">{{ data.name }}</span>
</template>
</el-cascader-panel>
<template #footer>
<div class="footer">
<el-button @click="handleClose" plain>取消</el-button>
<el-button type="primary" @click="handleSumit">确认添加</el-button>
</div>
</template>
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, defineProps, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { ApiBusiType } from '@/api/index'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { convertLegacyProps } from 'ant-design-vue/es/button/buttonTypes'
import { deepCopy } from '@/utils/helper'
const props = defineProps({
visible: {
type: Boolean,
default: false
},
data: {
type: Object || Array,
default: () => {
return {}
}
}
})
const ruleForm = reactive({
collectSite: '',
category: '',
productType: '',
productTypeKeyword: ''
})
const cascaderCollect = ref()
const collectOptions = ref([])
const currentOptions = ref([])
const otherOptions = ref([])
const searchData = ref('')
const selectData = ref([])
// 我是选中的值
const checkData = ref([])
const currentPathNode = ref('')
const currentPathNode2 = ref('')
const currentPathNode1 = ref('')
const searchDataOptions = ref([])
const secondNode = ref('')
const fourCollectOptions = ref([])
const loading = ref(false)
const secondParams = reactive({})
const productTypeOption = ref([])
const keywordOption = ref([])
const collectValue = ref([])
const optionFirst = ref([])
const optionSecond = ref([])
const optionThird = ref([])
const optionFourth = ref([])
const optionAll = ref([])
const oneOptions = ref([])
const twoOptions = ref([])
const time = ref()
const time2 = ref()
const $emit = defineEmits(['update:visible', 'close'])
let address = {
value: 'name',
label: 'name',
children: 'children',
multiple: true,
leaf: 'leaf',
lazy: true, // 开启懒加载
// checkStrictly: true, //可选择任意节点
/**
* 异步懒加载节点数据的函数
* @param {Object} node - 当前被点击的节点对象
* @param {Function} resolve - 数据加载完成后的回调函数,必须调用
* 该函数根据当前节点的信息构造查询条件,调用接口获取下一级节点数据。
* 当节点层级达到 4 级时,不再请求接口。获取到的数据经过处理后通过 resolve 返回。
*/
async lazyLoad(node, resolve) {
console.log('node', node)
const { level } = node
// level 节点层级
console.log('level', level)
const nodes = []
const params = {
managerCombination: node.pathLabels?.join(',') || '',
code: node.data.code || '',
name: node.data.name || '',
note: node.data.note || '',
parentCode: node.data.parentCode || ''
}
const res = await ApiBusiType.marketDataCollection.queryGraduallyCollectionConfig(params)
currentOptions.value = res.result || []
switch (level) {
case 0:
optionFirst.value = res.result || []
break
case 1:
optionSecond.value = res.result || []
twoOptions.value.push(res.result)
break
case 2:
optionThird.value = res.result || []
break
case 3:
optionFourth.value = res.result || []
break
default:
break
}
if (level === 0) {
collectOptions.value = res.result || []
resolve(collectOptions.value)
} else {
res.result.map((item) => {
let obj = {
code: item?.code,
name: item?.name,
note: item?.note,
disabled: item.disabled,
parentCode: item?.parentCode,
leaf: node?.level >= 3
}
nodes.push(obj)
})
resolve(nodes)
}
}
}
const isShow = computed({
get() {
return props.visible
},
set(val: boolean) {
$emit('update:visible', val)
}
})
//触发点击事件,index 节点位置,number 级联面板四级中的一级
const toClickSecondCascaderCollect = (index, number?) => {
const el = document.querySelectorAll(`.el-cascader-menu`)[number].querySelectorAll(`.el-cascader-node`)
if (el && el[index]) {
return new Promise((resolve) => {
el[index].click() // 触发点击事件,展开传过来的index对应的节点
el[index].scrollIntoView({ behavior: 'smooth', block: 'start' })
time.value = setTimeout(() => {
resolve() // 延迟1秒执行,防止执行过快,防止上一级没有展示出来就点击而导致找不到click报错
}, 1000)
})
}
return Promise.resolve()
}
const isTextTruncated = (text) => {
// 你可以实现逻辑检查文本是否超过某个长度
const maxLength = 28 // 可根据需要调整最大长度
return text.length > maxLength
}
const handleSumit = async () => {
console.log('collectValue', collectValue.value)
if (collectValue.value.length === 0) {
ElMessage.warning('无可添加采集节点,请重新选择')
return
}
const params = {
categoryList: collectValue.value.map((item) => item?.join(',')) || []
}
console.log('params', params)
const res = await ApiBusiType.marketDataRelationship.marketCollection(params)
if (res.code === '1') {
ElMessage.success('操作成功!')
$emit('close', 'refresh')
}
}
// 转换数据结构的函数
const transformToArray = (data) => {
return data.map((item) => {
const keyName = Object.keys(item)[0] // 获取每个对象的第一个属性名
return {
value: keyName,
label: keyName // 将 value 和 label 都设置为同一个属性名
}
})
}
const searchChange = async (changeData) => {
// 点击后获取的数据分3段数据
// 第一段数据返回后触发 toClickSecondCascaderCollect 第一段数据在2里面的位置 2
// 第二段数据返回后触发 toClickSecondCascaderCollect 第二段数据在3里面的位置 3
// 第三段数据返回后触发 toClickSecondCascaderCollect 第三段数据在4里面的位置 4
if (!changeData) return
const currdata = changeData?.split(',')
console.log('currdata', currdata)
for (let i = 0; i < 3; i++) {
let index = -1
if (i === 0) {
index = optionSecond.value.findIndex((item) => item.name === currdata[0])
} else if (i === 1) {
let secondParams = optionSecond.value.find((item) => item.name === currdata[0])
console.log('secondParams', secondParams)
const res = await ApiBusiType.marketDataCollection.queryGraduallyCollectionConfig(secondParams)
optionThird.value = res.result || []
console.log('res1111', res)
index = optionThird.value.findIndex((item) => item.name === currdata[1])
}else if(i === 2){
index = optionFourth.value.findIndex((item) => item.name === currdata[2])
}
if (index !== -1) {
await nextTick() // 等待数据渲染完成
await toClickSecondCascaderCollect(index, i + 1)
}
}
}
const searchAddData = async () => {
// loading.value = true
const params = {
name: currentPathNode.value,
search: searchData.value
}
const res = await ApiBusiType.marketDataRelationship.queryLikeConfig(params)
if (res.code === '1') {
const result = transformToArray(res.result)
searchDataOptions.value = result || []
}
}
// 获取第二级当前点击的节点
// 遍历二级的数据拿到这一集的参数,去调用第三级
const handleExpandChange = (val) => {
console.log('展开节点触发了', val)
if (val.length === 1) {
currentPathNode.value = val[0]
}
if (val.length > 1) secondNode.value = val[1]
}
const handleClose = () => {
isShow.value = false
collectValue.value = []
$emit('close')
}
watch(
() => currentPathNode.value,
(val) => {
if (val) {
let isupdate=oneOptions.value.includes(val)
if (oneOptions.value.length > 1 && isupdate) {
console.log('oneOptions.value312', oneOptions.value)
console.log('twoOptions.value213', twoOptions.value)
let index = oneOptions.value.findIndex((item) => item === val)
optionSecond.value = twoOptions.value[index]
}
isupdate ? '' : oneOptions.value.push(val)
searchData.value = ''
searchAddData()
}
}
)
watch(
() => props.visible,
(val) => {
searchData.value = '' // 清空搜索框
if (val) {
time2.value = setTimeout(() => {
const index = collectOptions.value.findIndex((item) => item.name === 'US') // 默认选中US,通过US去当下一级的option去获取对应的的index
toClickSecondCascaderCollect(index, 0) //将index传给toClickSecondCascaderCollect,0代表级联第一级数据,1代表第二级数据,2代表第三级数据
}, 1000)
}
}
)
onUnmounted(() => {
time.value && clearTimeout(time.value)
time2.value && clearTimeout(time2.value)
})
</script>
<style scoped lang="less">
.selection-container {
display: flex;
}
.selection-column {
flex: 1;
margin-left: 10px;
}
.footer {
float: right;
}
::v-deep(.el-cascader-menu:nth-child(1) .el-checkbox),
::v-deep(.el-cascader-menu:nth-child(2) .el-checkbox) {
display: none;
}
::v-deep(.el-cascader-menu:nth-child(1)) {
min-width: 135px;
}
.truncated-text {
display: inline-block;
max-width: 213px; /* 设置你希望的宽度 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.regular-text {
white-space: normal; /* 显示完整文本,没有省略 */
}
</style>