vue3原生input实现级联选择

96 阅读1分钟

样式需要自定义,elementplus样式跟项目主题不太匹配。

 <div class="navigate">
      <!-- 搜索输入框 -->
      <input
        id="navInput"
        class="input"
        type="text"
        v-model="data.navigateWords"
        placeholder="指标导航"
        @focus="updateNavigateDropVisible(true)"
        @focusout="updateNavigateDropVisible(false)"
      />
      <el-icon
        style="
          position: absolute;
          top: 50%;
          right: 30px;
          transform: translateY(-50%);
        "
        size="20px"
        :color="'#fff'"
      >
        <ArrowUp v-if="data.navigateDropListVisible" />
        <ArrowDown v-else />
      </el-icon>
      <!-- 推荐词列表 -->
      <transition name="el-zoom-in-top">
        <!-- -->
        <div
          v-show="
            data.navigateDropListVisible &&
            data.navigateDropList.length &&
            data.navInside
          "
          v-clickinside="handleClickInside"
          class="navigate_drop"
          :style="{
            width: data.navigateDropSecondList.length ? '300px' : '150px',
          }"
        >
          <div
            :class="[
              'first',
              data.navigateDropSecondList.length ? 'border' : '',
            ]"
            style="width: 150px"
          >
            <div
              v-for="(item, index) in data.navigateDropList"
              :key="index"
              :class="[
                'keyword-item',
                data.navCurSelectFirstIndex === index ? 'active' : '',
              ]"
              :title="item.firstNavigate"
              @mousedown="selectNavFirst(item, index)"
            >
              {{ item.firstNavigate }}
              <el-icon size="20px" :color="'#666'">
                <ArrowRight />
              </el-icon>
            </div>
          </div>
          <div
            v-if="data.navigateDropSecondList.length"
            class="second"
            style="width: 150px"
          >
            <div
              v-for="(item, index) in data.navigateDropSecondList"
              :key="`${item}-${index}`"
              class="second keyword-item"
              :title="item"
              @mousedown="selectNavSecond(item)"
            >
              {{ item }}
            </div>
          </div>
        </div>
      </transition>
    </div>
const data = reactive({
  navigateWords: '',
  navigateDropListVisible: false,
  navigateDropList: [],
  navigateDropSecondList: [],
  navCurSelectFirstIndex: '',
  navInside: true,
})

/**
 * 设置导航的下拉是否显示
 * @param {Boolean} status
 */
const updateNavigateDropVisible = status => {
  if (status) {
    data.navigateDropListVisible = status
    data.navigateDropList = proxy._t.cloneDeep(dropList)
    data.navigateDropSecondList = []
    data.navCurSelectFirstIndex = ''
  }
}

/**
 * 初始化 navigate的数据
 */
const initNavigateDrop = async () => {
  const res = await proxy.$api.getIndexNavigateInfo()

  dropList = res.data
}

/**
 * 选择了一级
 */
const selectNavFirst = (item, index) => {
  data.navigateDropSecondList = item.secondNavigateList
  data.navCurSelectFirstIndex = index
}

/**
 * 选择了二级
 */
const selectNavSecond = item => {
  emit('search', item, item)
  data.navigateDropListVisible = false
}

// 当前鼠标是否点击到drop内,内的话保留,外的话drop消失
const handleClickInside = (e, target) => {
  // console.log(
  //   'eeeee',
  //   data.navigateDropListVisible,
  //   e,
  //   target.getAttribute('id'),
  // )
  const id = target.getAttribute('id')
  if (data.navigateDropListVisible && !e && id === 'navInput') return
  if (data.navigateDropListVisible) {
    data.navInside = e
  }

  if (!e) {
    data.navigateDropListVisible = false
    data.navInside = true
  }
}

自定义指令

  // 判断鼠标点在指定区域外
  clickinside: {
    // 初始化指令
    mounted(el, binding, vnode) {
      function documentHandler(e) {
        if (binding.value) {
          binding.value(el.contains(e.target), e.target)
        }
      }
      // 给当前元素绑定个私有变量,方便在unbind中可以解除事件监听
      el.__vueClickinside__ = documentHandler
      document.addEventListener('click', documentHandler)
    },
    beforeUnmount(el, binding) {
      // 解除事件监听
      document.removeEventListener('click', el.__vueClickinside__)
      delete el.__vueClickinside__
    },
  },

main.js

// 判断鼠标点在指定区域内
app.directive('clickinside', directive.clickinside)
.input {
  // width: 1000px;
  width: 690px;
  height: 56px;
  box-sizing: border-box;
  border: 1px solid #fff;
  // border-radius: 10px;
  font-size: 16px;
  padding: 0 180px 0 20px;
  line-height: 56px;
}
.input:focus {
  border: 1px solid $themColor;
}

.navigate {
  width: 150px;
  position: relative;
  .input {
    background: $themColor;
    border: 1px solid $themColor;
    border-radius: 10px 0 0 10px;
    color: #fff;
    &::placeholder {
      color: #fff;
    }
  }
}
.navigate_drop {
  box-sizing: border-box;
  display: flex;
  z-index: 99;
  position: absolute;
  top: 60px;
  left: 0;
  width: 150px;
  max-height: 372px;
  background-color: #fff;
  border: 1px solid $themColor;
  border-radius: 10px;
  padding: 10px 0;
  overflow-y: scroll;
  // padding-left: 20px;
  .keyword-item {
    padding: 0 20px;
    height: 44px;
    line-height: 44px;
    font-size: 16px;
    color: #666666;
    cursor: pointer;
    width: 150px;
    box-sizing: border-box;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    .el-icon {
      vertical-align: middle;
    }
    &.active {
      color: $themColor;
      .el-icon {
        color: $themColor;
      }
    }
  }
  .keyword-item:hover {
    background-color: #c7d5ff;
    font-weight: 500;
    color: $themColor;
    .el-icon {
      color: $themColor;
    }
  }
}
.first {
  overflow-y: scroll;
  max-height: 372px;
  text-align: center;
  &.border {
    border-right: 1px solid #ccc;
    // padding-left: 20px;
  }
}
.second {
  overflow-y: scroll;
  max-height: 372px;
}

image.png