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 thirdNode = 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 threeOptions = ref([])
const fourOptions = 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 || []
        fourOptions.value.push(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)
  }
})
// 查找子节点
const findCascaderNodes = (element) => {  
  // 确保传入的元素是有效的  
  if (!element) return [];  
  const nodes = [];  
  // 查找当前层级的 .el-cascader-node 节点  
  const cascaderNodes = element.querySelectorAll('.el-cascader-node');  
  nodes.push(...cascaderNodes);  
  // 递归查找子节点  
  const children = element.children;  
  for (let i = 0; i < children.length; i++) {  
    const childNodes = findCascaderNodes(children[i]);  
    nodes.push(...childNodes);  
  }  
  return nodes;  
}
//触发点击事件,index 节点位置,number 级联面板四级中的一级
const toClickSecondCascaderCollect = (index, number?) => {
  const el = cascaderCollect.value?.$el.querySelectorAll('.el-cascader-menu')[number]
  const cascaderNodes = findCascaderNodes(el)
  if (cascaderNodes && cascaderNodes[index]) {
    return new Promise((resolve) => {
      cascaderNodes[index].click() // 触发点击事件,展开传过来的index对应的节点
      cascaderNodes[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(',')
  if (currdata.length !== 3) return

  console.log('currdata', currdata)
  try {
    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])
        console.log("第四级index", index)
      }
      if (index !== -1) {
        await nextTick() // 等待数据渲染完成
        await toClickSecondCascaderCollect(index, i + 1)
      }
    }
  } catch (error) {
    console.log(error)
  }
}
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)
  currentPathNode.value = val.length === 1 ? val[0] : currentPathNode.value
  // secondNode.value = val.length > 1 ? val[1] : secondNode.value
  thirdNode.value= val.length > 2 ? val[2] : thirdNode.value
}
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(
  () => thirdNode.value,
  (val) => {
    if (val) {
      let isupdate3 = threeOptions.value.includes(val)
      if(threeOptions.value.length > 1 && isupdate3){
        let index3 = threeOptions.value.findIndex((item) => item === val)
        optionFourth.value = fourOptions.value[index3]
        // console.log("我要更新四级", optionFourth.value)
      }
      isupdate3 ? '' : threeOptions.value.push(val)
    }
  })
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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值