动态加载之前已经发过一次,有想了解的的可在本人主页找下
一.效果展示
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-v2v-model="searchData"filterableclearable: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-panelref="cascaderCollect"v-model="collectValue":props="address":options="collectOptions"@expandChange="handleExpandChange"><template #default="{ node, data }"><el-tooltipv-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 || []breakcase 1:optionSecond.value = res.result || []twoOptions.value.push(res.result)breakcase 2:optionThird.value = res.result || []breakcase 3:optionFourth.value = res.result || []breakdefault: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里面的位置 4if (!changeData) returnconst currdata = changeData?.split(',')console.log('currdata', currdata)for (let i = 0; i < 3; i++) {let index = -1if (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 = trueconst 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 = falsecollectValue.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去获取对应的的indextoClickSecondCascaderCollect(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>