vue3实现自定义select下拉框内容之城市区域篇

这篇具有很好参考价值的文章主要介绍了vue3实现自定义select下拉框内容之城市区域篇。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

欢迎点击领取 -《前端开发面试题进阶秘籍》:前端登顶之巅-最全面的前端知识点梳理总结

需求分析:
1、实现一个区域下拉选项与现有ui组件库不同,支持多选、单选需求
2、支持选中区域后-全选中当前区域下的所有城市信息
3、只能选中当前一个区域的内的城市其余城市禁用

扩展思路:
1、封装公共组件或者封装在组件库内
2、出入参相关api透明好理解
3、支持单选或者多选,支持只选择当前区域下的城市或者全面区域下的城市
4、在原有的element plus下进行扩展延伸满足需求
5、缺陷:未做maxLength-标签最大展示的api;这个按需自己修改一下就行

1、第一种模式:显示区域信息
vue3实现自定义select下拉框内容之城市区域篇,vue3,element plus,vite,状态模式
2、第二种模式:只展示城市内容
vue3实现自定义select下拉框内容之城市区域篇,vue3,element plus,vite,状态模式

1、相关开发代码篇

创建文件:custom-select.vue文件;复制copy当下代码;
使用方式:
1、外部入参例如城市:dataSource=[{ label: 华北,value: '华东', children: [{ label: '山东',value: 'shandong'}]],树形结构
2、标签引用:<custom-select :disabled="true" :multilevel="true" height="32" v-model="checkGroup" :dataSource="cityList"></custom-select>
3、相关api说明文档在文章底部文章来源地址https://www.toymoban.com/news/detail-646050.html

<template>
  <div
    tabindex="1"
    ref="customSelectRef"
    @click="handleClickDiv"
    @mouseenter="handelMouseEnter"
    @mouseleave="handleMouseLeave"
    :style="{ width: modelLabel && modelValue?.length ? '166px' : '100px', height: (height + 'px') || '25px' }"
    :class="['custom-select_contaniner-i', isShowDropdown && 'custom-select_background']"
  >
    <div>
      <span v-if="modelLabel" class="custom-tag">
        <span>{{ modelLabel }}</span>
        <i
          class="custom_tag_delete"
          @mouseenter="handelIconMouseEnter"
          @mouseleave="handleIconMouseLeave"
          @click.stop="handleDeleteIcon"
        >
          <svg
            v-if="!ishShowIconDeleteText"
            t="1678090923023"
            class="icon"
            viewBox="0 0 1024 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            p-id="6709"
            width="11"
            height="11"
          >
            <path
              d="M263.802377 224.219482a7.964444 7.964444 0 0 1 11.263425 0l236.934198 236.934198 236.934198-236.934198a7.964444 7.964444 0 0 1 11.263425 0l39.582895 39.582895a7.964444 7.964444 0 0 1 0 11.263425l-236.934198 236.934198 236.934198 236.934198a7.964444 7.964444 0 0 1 0 11.263425l-39.582895 39.582895a7.964444 7.964444 0 0 1-11.263425 0l-236.934198-236.934198-236.934198 236.934198a7.964444 7.964444 0 0 1-11.263425 0l-39.582895-39.582895a7.964444 7.964444 0 0 1 0-11.263425l236.934198-236.934198-236.934198-236.934198a7.964444 7.964444 0 0 1 0-11.263425l39.582895-39.582895z"
              fill="#8a8a8a"
              p-id="6710"
            />
          </svg>
          <svg
            v-else
            t="1678091410677"
            class="icon"
            viewBox="0 0 1024 1024"
            version="1.1"
            xmlns="http://www.w3.org/2000/svg"
            p-id="6936"
            width="22"
            height="22"
          >
            <path
              d="M479.072 512l-98.72-98.72c-9.152-9.152-9.088-23.84 0-32.928 9.152-9.152 23.84-9.088 32.928 0l98.72 98.72 98.72-98.72c9.152-9.152 23.84-9.088 32.928 0 9.152 9.152 9.088 23.84 0 32.928l-98.72 98.72 98.72 98.72c9.152 9.152 9.088 23.84 0 32.928-9.152 9.152-23.84 9.088-32.928 0l-98.72-98.72-98.72 98.72c-9.152 9.152-23.84 9.088-32.928 0-9.152-9.152-9.088-23.84 0-32.928l98.72-98.72zM512 837.824c179.936 0 325.824-145.888 325.824-325.824s-145.888-325.824-325.824-325.824c-179.936 0-325.824 145.888-325.824 325.824s145.888 325.824 325.824 325.824z"
              fill="#B7B8B9"
              p-id="6937"
            />
          </svg>
        </i>
      </span>
      <span v-if="modelLabel && modelValue?.length > 1" class="custom-tag">+ {{ modelValue.length - 1 }}</span>
      <span v-if="!modelLabel" class="cus_placeholder">{{ placeholder }}</span>
    </div>
    <i class="arrow-top-icon" v-if="!isShowIconRemove || !modelLabel" :class="[!isShowDropdown && 'arrow-top-icon-active']">
      <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
        <path
          fill="currentColor"
          d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"
        />
      </svg>
    </i>
    <i class="remove-icon" v-if="isShowIconRemove && modelLabel" @click.stop="handleRemove">
      <svg
        t="1678084213981"
        class="icon"
        viewBox="0 0 1024 1024"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
        p-id="4480"
        width="11"
        height="11"
      >
        <path
          d="M512 32c265.097 0 480 214.903 480 480S777.097 992 512 992 32 777.097 32 512 246.903 32 512 32z m0 64C282.25 96 96 282.25 96 512s186.25 416 416 416 416-186.25 416-416S741.75 96 512 96z m169.706 246.294c12.496 12.497 12.496 32.758 0 45.255L557.256 512l124.45 124.452c12.496 12.497 12.496 32.758 0 45.255-12.497 12.496-32.758 12.496-45.255 0L512 557.254 387.549 681.706c-12.497 12.496-32.758 12.496-45.255 0-12.496-12.497-12.496-32.758 0-45.255l124.452-124.452-124.452-124.45c-12.496-12.497-12.496-32.758 0-45.255 12.497-12.496 32.758-12.496 45.255 0l124.452 124.45 124.45-124.45c12.497-12.496 32.758-12.496 45.255 0z"
          fill="#8a8a8a"
          p-id="4481"
        />
      </svg>
    </i>
  </div>
  <transition>
    <div
      v-if="isShowDropdown"
      ref="cusSelectDropdown"
      class="cus_select_background"
      :style="{ minWidth: popperOffestWidth + 'px', zIndex: 99999 }"
    >
      <div v-if="multilevel" style="padding: 5px 20px;">
        <div :key="key" v-for="(opt, key) in cusDataListChecked" class="multilevel_box">
          <el-checkbox
            style="width: 60px;"
            v-model="opt.checkAll"
            @change="handleCheckAllChange($event, opt)"
            :indeterminate="opt.isIndeterminate"
            :disabled="disabled && checkList.length ? !opt.checkList.length : false"
            >
            {{ opt.label }}
          </el-checkbox>
          <el-checkbox-group 
            v-model="opt.checkList"
            v-if="opt.children"
            @change="handleCheckedCitiesChange($event, opt)"
            style="display: inline-block; padding-left: 20px" 
          >
              <el-checkbox 
                :label="item.value" 
                style="width: 60px"
                :key="index + Math.random()" 
                v-for="(item, index) in opt.children" 
                :disabled="disabled && checkList.length ? !opt.checkList.length : false"
                >
                {{ item.label }}
              </el-checkbox>
          </el-checkbox-group>
        </div>
      </div>
      <div class="cus_select_contaniner" v-else>
        <div class="cus_select_left">中国</div>
        <div class="cus_select_right">
          <el-checkbox-group 
            v-model="checkList" 
            @change="handelCheckGroup"
            style="display: inline-block; padding-left: 20px" 
          >
            <el-checkbox 
              :key="index" 
              :label="item.value" 
              style="width: 60px"
              v-for="(item, index) in dataSource" 
            >
              {{ item.label }}
            </el-checkbox>
          </el-checkbox-group>
        </div>
      </div>
      <span class="el-popper__arrow" data-popper-arrow="" style="position: absolute; left: 140px;"></span>
    </div>
  </transition>
</template>
<script setup lang="ts">
import { createPopper } from '@popperjs/core'
import { ref, onMounted, nextTick, watch, onUnmounted, toRaw, onBeforeMount, computed } from 'vue'

const props = withDefaults(
  defineProps<{
    height?: string | number
    dataSource: any
    modelValue?: any
    placeholder?: string
    multilevel?: boolean
    disabled?: boolean
  }>(),
  {
    height: 25,
    disabled: false,
    multilevel: false,
    dataSource: [],
    modelValue: [],
    placeholder: '请选择'
  }
)

const emit = defineEmits(['update:modelValue'])

const customSelectRef = ref()

const cusSelectDropdown = ref()

const cusDataListChecked = ref<any[]>([])

const checkList = ref<string[]>([])

const popperOffestWidth = ref<number>(0)

const isShowDropdown = ref<boolean>(false)

const modelLabel = ref<string>('')

const isShowIconRemove = ref<boolean>(false)

const ishShowIconDeleteText = ref<boolean>(false)

const handleClickDiv = () => {
  isShowDropdown.value = !isShowDropdown.value
}

const handelCheckGroup = (value) => {
  const obj = props.dataSource.filter((item) => item.value === value[0])[0]
  modelLabel.value = obj?.label
  emit('update:modelValue', value)
}

const handelMouseEnter = () => {
  isShowIconRemove.value = true
}

const handleMouseLeave = () => {
  isShowIconRemove.value = false
}

const handleRemove = () => {
  modelLabel.value = ''
  checkList.value = []
  if (isShowDropdown.value) {
    isShowDropdown.value = false
  }
  if (props.multilevel) {
    cusDataListChecked.value = addCheckProperties(props.dataSource)
  }
  emit('update:modelValue', [])
}

const handleDeleteIcon = () => {
  isShowDropdown.value = false
  checkList.value.splice(0, 1)
  if (props.multilevel) return cusDataListChecked.value = findTreeChecked(cusDataListChecked.value)
  const info = toRaw(checkList.value)[0]
  const obj = props.dataSource.filter((item) => item.value === info)[0]
  modelLabel.value = obj?.label || ''
}

const handelIconMouseEnter = () => {
  ishShowIconDeleteText.value = true
}

const handleIconMouseLeave = () => {
  ishShowIconDeleteText.value = false
}

// 点击某个DOM元素之外的方法
const handlerDocClick = (event) => {
  const isSelf = customSelectRef.value?.contains(event.target) 
  || cusSelectDropdown.value?.contains(event.target)
  if (!isSelf) {
    isShowDropdown.value = false
  }
}

/**
 * 展示区域省份的逻辑
 * */ 
const handleCheckAllChange = (bool: any, option) => {
  const allCity = option.children ? option.children.map(item => item.value) : [option.value]
  bool ? option.checkList = allCity : option.checkList = []
  option.isIndeterminate = false
  checkList.value = option.checkList
  const newLabelArr = option.children 
  ? option.children.filter(item => checkList.value.includes(item.value)) 
  : checkList.value?.length ? [{ label: '默认' }] : []
  modelLabel.value = newLabelArr?.[0]?.label || ''
  emit('update:modelValue', checkList.value)
}

const handleCheckedCitiesChange = (value: any[], option) => {
  const checkedCount = value.length
  const allCity = option.children ? option.children.map(item => item.value) : [option.value]
  option.checkAll = checkedCount === allCity.length
  option.isIndeterminate = checkedCount > 0 && checkedCount < allCity.length
  checkList.value = option.checkList
  const newLabelArr = option.children 
  ? option.children.filter(item => checkList.value.includes(item.value)) 
  : checkList.value?.length ? [{ label: '默认' }] : []
  modelLabel.value = newLabelArr?.[0]?.label || ''
  emit('update:modelValue', checkList.value)
}

const addCheckProperties = (treeData) => {
  let result = []
  result = JSON.parse(JSON.stringify(treeData))
  result.forEach(node => {
    const child = node.children;
    node.checkAll = false;
    node.isIndeterminate = false;
    node.checkList = [];
    if (child && child.length > 0) {
      addCheckProperties(child);
    }
  });
  return result
}

const findTreeChecked = (treeData) => {
  let newLabel
  const val = toRaw(checkList.value)
  const defaultBool = val.some(item => item.includes('default'))
  treeData.forEach(node => {
    if (node.children?.length) {
      const child = node.children;
      const bool = child.some(opt => val.includes(opt.value))
      !newLabel ? newLabel = child.filter(item => val.includes(item.value))[0] : void null
      if (bool) {
        node.checkAll = val.length === child?.length;
        node.isIndeterminate = val.length > 0 && val.length < child?.length;
        node.checkList = val;
      } else {
        node.isIndeterminate = false
      }
    }
  })
  treeData[0].isIndeterminate = false;
  treeData[0].checkAll = defaultBool ? true : false;
  treeData[0].checkList = defaultBool ? ['default'] : [];
  modelLabel.value = defaultBool ? '默认' : newLabel?.label || ''
  return treeData
}

watch(
  [customSelectRef, cusSelectDropdown],
  () => {
    if (customSelectRef.value && cusSelectDropdown.value) {
      createPopper(customSelectRef.value, cusSelectDropdown.value, {
        placement: 'bottom',
        modifiers: [
          {
            name: 'offset',
            options: {
              offset: [80, 8]
            }
          }
        ]
      })
    }
  },
  {
    deep: true,
    immediate: true
  }
)

watch(
  props.modelValue,
  (newval) => {
    if (!newval || !newval.length) return
    checkList.value = props.modelValue
    if (props.multilevel) return
    const obj = props.dataSource.filter((item) => item.value === newval[0])[0]
    modelLabel.value = obj?.label
  },
  {
    deep: true,
    immediate: true
  }
)

onBeforeMount(() => {
  if (props.multilevel) {
    cusDataListChecked.value = addCheckProperties(props.dataSource)
  }
})

onMounted(async () => {
  await nextTick()
  popperOffestWidth.value = customSelectRef.value.offsetWidth
  document.addEventListener('click', handlerDocClick, true)
  if (props.multilevel && props.modelValue.length) { 
    cusDataListChecked.value = findTreeChecked(cusDataListChecked.value)
  }
})

onUnmounted(() => {
  document.removeEventListener('click', handlerDocClick, true)
})
</script>

<script lang="ts">
export default { name: 'CustomSelect' }
</script>

<style lang="scss" scoped>
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

.custom-select_contaniner-i {
  width: 100%;
  height: 25px;
  padding: 7px 9px;
  padding-left: 5px;
  border-radius: 4px;
  line-height: 1;
  cursor: pointer;
  position: relative;
  user-select: none;
  word-wrap: break-word;
  word-break: break-all;
  font-size: 13px;
  flex-grow: 1;
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  justify-content: space-between;
  color: var(--el-input-text-color, var(--el-text-color-regular));
  background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
  box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
}

.custom-tag {
  color: var(--el-color-info);
  display: inline-flex;
  justify-content: center;
  align-items: center;
  height: 18px;
  padding: 0 9px;
  line-height: 1;
  border-radius: 4px;
  white-space: nowrap;
  font-size: 12px;
  background-color: var(--el-fill-color);
}

.custom_tag_delete {
  width: 18px;
  margin-left: 5px;
  font-size: 0px;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  color: var(--el-color-info);
}

.custom-tag:first-child {
  margin-right: 6px;
  padding-right: 4px;
}

.arrow-top-icon {
  width: 14px;
  transform: rotateX(-180deg);
  color: var(--el-text-color-placeholder);
}

.remove-icon {
  margin-top: 2px;
  color: var(--el-text-color-placeholder);
}

.arrow-top-icon-active {
  transform: rotateX(0deg);
}

.custom-select:hover {
  box-shadow: 0 0 0 1px var(--el-border-color-hover) inset;
}

.custom-select:focus {
  outline: none;
  box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}

.custom-select_background {
  box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}

::-webkit-scrollbar {
  width: 4px;
  height: 4px;
  background-color: transparent;
}

/*滚动条的轨道*/
::-webkit-scrollbar-track {
  background-color: transparent;
}

/*滚动条的滑块按钮*/
::-webkit-scrollbar-thumb {
  border-radius: 8px;
  background-color: rgba(0, 0, 0, 0.1);
  box-shadow: inset 0 0 2px rgba(#000000, 0.04);
}

/*滚动条的上下两端的按钮*/
::-webkit-scrollbar-button {
  height: 0;
  background-color: transparent;
}

.cus_select_contaniner {
  padding: 5px 10px;
  display: flex;
}

.cus_select_left {
  width: 60px;
  margin-top: 5px;
}

.cus_select_right {
  flex: 1;
  width: 480px;
}

.cus_select_background {
  min-height: 200px;
  box-sizing: border-box;
  border-radius: 4px;
  font-size: var(--el-font-size-base);
  color: var(--el-text-color-regular);
  background: var(--el-bg-color-overlay);
  border: 1px solid var(--el-border-color-light);
  .multilevel_box {
    display: flex; 
    padding: 5px; 
    border-bottom: 1px solid #e4e7ed;
  }
  .multilevel_box:last-child {
    border-bottom: none;
  }
}

.cus_placeholder {
  color: var(--el-text-color-placeholder);
}

.el-popper__arrow {
  top: -5px;
}

.el-popper__arrow {
  position: absolute;
  width: 10px;
  height: 10px;
  z-index: -1;
}

.el-popper__arrow::before {
  border: 1px solid var(--el-border-color-light);
  background: var(--el-bg-color-overlay);
  right: 0;
  border-bottom-color: transparent!important;
  border-right-color: transparent!important;
}
</style>
2、组件-相关api说明
参数 说明 类型 默认值 必填项
height 输入框的高度 String/Number 25
dataSource [{}]-label,value;树形结构 Array[] []
modelValue 当前选中项内容 Array []
placeholder 输入框内容 String 请输入
multilevel 是否开启跨层级模式 Boolean false
disabled 是否开启跨层级禁用 Boolean false

到了这里,关于vue3实现自定义select下拉框内容之城市区域篇的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Vue实现select下拉框二级联动数据后台获取

    一、二级联动在vue中实现selected的二级联动取其实很简单,可以使用select下拉框的表单改变事件。  @change在表单内容发生改变时出发方法。然后在下面的methods中声明方法,通过this.form.ks获取到当前下拉框的数据。  然后调用后两级访问数据库的方法,获取到联动的数据,添加

    2024年02月16日
    浏览(41)
  • vue+el-select下拉实现:全选、反选、清空功能

    问题描述: el-select下拉框要求实现全选功能。具体功能包括: 当选择【全选】时,所有选项全部被勾选; 当选择【反选】时,已选择选项变为未选择选项,未选项变为已选项 当选择【清空】时,所有选项变为未选 如下图: 1、给el-select增加【全选】【清空】【反选】按钮

    2024年02月10日
    浏览(67)
  • Vue+EleMentUI实现el-table-colum表格select下拉框可编辑

    在进行采购入库的过程中,有必要对表格中的一行进行快速编辑保存,节省时间,提高工作效率!,而不是每次编辑都要弹窗才可编辑 源码:https://gitee.com/charlinchenlin/store-pos 控制是否显示select下拉框 如果showInput的值与当前的inboundId相同,则显示下拉选项,否则显示数据信息

    2024年02月01日
    浏览(85)
  • 如何vue使用ant design Vue中的select组件实现下拉分页加载数据,并解决存在的一个问题。

        需求:拉下菜单中数据过多,200条以上,就会导致select组件卡死。所以需要使用滑动到底部使其分页加载     可以借助 onPopupScroll 事件来监听下拉菜单的滚动事件,并判断当前是否已经到达了下拉菜单底部。具体可以通过以下步骤实现:     1、在组件中绑定 @popupScro

    2023年04月20日
    浏览(52)
  • Vue 3 + Element UI Plus 实现 Select 下拉框的虚拟滚动效果详解与代码示例

    在 Vue 3 项目中,当下拉框中的选项过多时,使用虚拟滚动可以提升性能和用户体验。本文将介绍如何使用 Vue 3 和 Element UI Plus(el-select-plus)组件实现 Select 下拉框的虚拟滚动效果,并提供详细的代码示例。 首先,确保你已经安装了 Element UI Plus,它是 Element UI 的扩展版本,支

    2024年02月08日
    浏览(50)
  • Vue + element ui 实现后台数据渲染到下拉框选项中,同时将input框与下拉框联动,select选定之后,input显示对应的值

    实现过程: 使用element 的select以及input输入框 在data中定义一个数组用于接收后台请求的数据 method中定义一个方法,用于请求数据 在created中实现显示: listMidMapping为封装好的get请求 至此实现后台数据渲染到下拉框选项中,效果图: 以上借鉴于: (21条消息) Vue + element 实现动

    2024年02月08日
    浏览(37)
  • vue3实现区域打印功能(vue3-print-nb)

    场景 大多数后台系统中都存在打印的需求,在有打印需求时,对前端来说当然是直接打印页面更容易,那么本篇文章就是在vue3中,使用vue3-print-nb插件来区域打印,实现指哪打哪! 一.安装vue3-print-nb 二.在main.ts中引入 三.HTML 四.参数配置 推荐文章 Print.js实现打印pdf,HTML,图片

    2024年02月12日
    浏览(42)
  • Vue3 实现下拉选择框多选的功能实现

    项目采用vue3+elmentui-plus +ts 搭建成的。用户界面设计上需要一个带全选的下拉选择框。而element plus 官网 提供的下拉多选框是 这种形式是没有全选按钮的,并且样式也不符合想像中的全选的操作。故开始想法子去结合一个新的条件去封装一个新的组件。 由于用户更加热衷于带

    2024年02月12日
    浏览(37)
  • Bootstrap select2之下拉框可自定义输入和选择

    1. 引入css文件 2. 引入js文件 3. select标签引入class 我是在项目搜索框部分要加一个下拉框,本来甚至不需要上面那么多引入,都可以有一个下拉框,但是要求点击可以出现输入框自定义输入来筛选,然后想到了这个。但是想要利用表单来实现,因为不是一种表单,所以样式混乱

    2024年02月05日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包