Vue2搜索框实现indexedList索引列表-锚点跳转,附带数据动态绑定、多选、单选、搜索功能

简述:如何在pc端实现像手机移动端才有的indexLIst索引列表呢,查阅很多组件库,都没能找到相应组件,那只能自己来手搓了

        这里vue版本vue2.0,deepClone为深拷贝方法,网上自行百度找吧,组件库用的elementUI,字体图标取自elementUI,本组件可适应任意key、value、children的键值,具体使用请看下方props配置,数据格式

效果预览

视频预览

这里代码中都有注释,样式,js一并贴入,看不懂那我也没办法了,强调一点,数据一定要让后端返回此类格式(自己整理数据成这样也可)如图:

其中字母索引,和children是必须的!!!!

  • 组件使用
<template>
    <div>
        <indexList placeholder="请选择保险公司" v-model="headId" style="width: 200px" :options="testList" :props="{ name: 'icName', value: 'id', letter: 'brandInitial', data: 'children', }" />
    </div>
</template>
  • 组件封装-输入框+下拉列表封装
<template>
  <div :class="['index_list_content' + client,'bg_fff']">
    <div class="relative">
      <transition name="el-zoom-in-top">
        <!-- 下拉列表区域 -->
        <index-options
          v-show="visible"
          class="index_area absolute bor_rad4"
          :style="{top:offsetTop + 'px'}"
          :multiple="multiple"
          :option="list"
          :selectValue="selectValue"
          :letterList="letterList"
          @deleteItem="deleteItem"
          @closeList="confirm"
        />
      </transition>
       <!-- 单选输入框区域 -->
      <div
        v-if="!multiple"
        ref="itembox"
        class="inlin_block"
      >
        <el-input
          style="width: 100%;"
          ref="input"
          :clearable="false"
          :selectValue="selectValue"
          v-model="valueName"
          :placeholder="innerPlaceholder"
          @input="fetchSuggestions"
          @focus="open"
          @blur="close"
        />
        <span
            v-if="iconT !== 'el-icon-circle-close'"
            class="input__suffix inline_block"
            @mouseover="iconChage"
            @mouseout="iconOut"
            @click="clear"
          >
            <i :class="iconT"></i>
          </span>
          <span
            v-else
            class="input__suffix pointer inline_block"
            @mouseover="iconChage"
            @mouseout="iconOut"
            @click="clear"
          >
            <i :class="iconT"></i>
          </span>
      </div>
 <!-- 多选输入框区域 -->
      <div
        v-if="multiple"
        ref="itembox"
        class="inlin_block"
      >
        <div
          v-show="valueNameArr.length > 0"
          :class="['flex flex-align-center input_box bor_rad4 pr30',{'is_focus':isFocus}]"
        >
          <div
            v-show="valueNameArr.length > 0"
            class="flex flex-align-center c_gap4 r_gap4"
            style="width: 100%;max-width: 178px;"
          >
            <span
              class="f12 inlien_block bor_rad4 tag"
              @click.stop="clickFocus"
            >
              {{valueNameArr[0]}}
              <i
                style="font-size: 12px;"
                class="iconfont icon_close_bg"
                @click.prevent.stop="deleteItem(valueNameArr[0])"
              ></i>
            </span>
            <span
              v-show="valueNameArr.length-1 > 0"
              @click.stop="clickFocus"
              class="f12 inlien_block bor_rad4 tag"
            > + {{ valueNameArr.length-1 }}
            </span>
          </div>
          <el-input
            style="width: 100% !important;"
            class="ml8"
            ref="input"
            :selectValue="selectValue"
            v-model="searchName"
            @input="fetchSuggestions"
            @focus="open"
            @blur="close"
            :clearable="false"
          />
          <span
            v-if="iconT !== 'el-icon-circle-close'"
            class="input__suffix inline_block"
            @mouseover="iconChage"
            @mouseout="iconOut"
            @click="clear"
          >
            <i :class="iconT"></i>
          </span>
          <span
            v-else
            class="input__suffix pointer inline_block"
            @mouseover="iconChage"
            @mouseout="iconOut"
            @click="clear"
          >
            <i :class="iconT"></i>
          </span>
        </div>
        <el-input
          v-show="valueNameArr.length === 0"
          :selectValue="selectValue"
          :placeholder="innerPlaceholder"
          v-model="searchName"
          :suffix-icon="iconT"
          @input="fetchSuggestions"
          @focus="open"
          @blur="close"
        />

      </div>
    </div>

  </div>
</template>

<script>
import { deepClone } from '@/utils/util';
import indexOptions from './options';
/** 
 * @remarks
 * value:v-model绑定
 * options:array
 * placeholder:输入框背景文字
 * props:配置选项,具体:
 * {
        value: 'id',
        name: 'name',
        letter: 'letter',index索引
        data: 'data',索引下的list
    }
**/

export default {
  components: {
    indexOptions,
  },
  props: {
    value: {
      type: [String, Number, Array],
      default: () => {
        return null;
      },
    },
    options: {
      type: Array,
      default: () => {
        return [];
      },
    },
    placeholder: {
      type: String,
      default: '请选择',
    },
    props: {
      type: Object,
      default: () => {
        return {
          value: 'id',
          name: 'name',
          letter: 'letter',
          data: 'data',
        };
      },
    },
    multiple: {
      type: Boolean,
      default: false,
    },
// 组件唯一class尾缀,防止两个组件类名重复,导致组件无法下拉(可不传,这里取默认)
    client: {
      type: String,
      default: function name(params) {
        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '');
      },
    },
  },
  watch: {
    options: {
      deep: true,
      immediate: true,
      handler() {
        this.setList();
      },
    },
    selectValue(newVal) {
      this.$emit('input', this.selectValue);
    },
    valueName(newVal) {
      if (!newVal) {
        this.restoreData()
      }
    },
    value(newVal) {
      if (
        ((typeof newVal === 'string' && !newVal.length) || typeof newVal === 'object') &&
        !this.multiple
      ) {
        this.valueName = '';
      }
      if (!newVal.length && this.multiple) {
        this.valueNameArr = [];
      }
      this.selectValue = newVal;
    },
//优化筛选为空时,再次下拉无数据情况
    visible(val) {
      if (!val && !this.list.length) {
        this.restoreData();
      }
      if (this.list.length !== this.oldList.length) {
        this.restoreData();
      }
    },
  },
  data() {
    return {
      visible: false, //是否显示下拉
      searchName: '', //多选时用于搜索
      valueName: '', //单选显示的数据name
      selectValue: this.value, //选中数据
      valueNameArr: [], //多选选中数据的name
      iconT: 'el-icon-arrow-down',
      list: [], //数据
      letterList: [],
      oldList: [],
      oldLetterList: [],
      oldPlaceholder: '',
      innerPlaceholder: this.placeholder,
      offsetTop: 0, //下拉距离顶部距离
      isFocus: false, //记录是否获取焦点
    };
  },
  mounted() {
    // 创建点击监听
    document.addEventListener('click', this.handleDocumentClick);
    this.offsetTop = this.getBoundingClientRect();
  },
  destroyed() {
    document.removeEventListener('click', this.handleDocumentClick);
  },
  methods: {
    // 初始化数据
    setList() {
      this.oldPlaceholder = deepClone(this.innerPlaceholder);
      this.options.forEach((item, index) => {
        let arr = item[this.props.data || 'letter'].map((item2) => {
          return {
            value: item2[this.props.value || 'value'],
            name: item2[this.props.name || 'name'],
            letter: item[this.props.letter || 'letter'],
          };
        });
        this.list.push({ letter: item[this.props.letter || 'letter'], data: arr });
      });
      this.dataInit();
      this.letterList = this.options.map((item) => {
        return item[this.props.letter || 'letter'];
      });
      // this.letterList = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']
      this.oldList = deepClone(this.list);
      this.oldLetterList = deepClone(this.letterList);
    },
    // 初始化数据回显name
    dataInit() {
      this.list.forEach((item, index) => {
        item.data.forEach((item2) => {
         if ((typeof this.selectValue === 'array' || typeof this.selectValue === 'string') && this.selectValue.indexOf(item2.value) >= 0) {
            if (this.multiple) {
              this.valueNameArr.push(item2.name);
            } else {
              this.valueName = item2.name;
            }
          }else if(typeof this.selectValue === 'number' && !this.multiple && this.selectValue === item2.value) {
            this.valueName = item2.name;
          }
        });
      });
    },
restoreData() {
      setTimeout(() => {
        this.fetchSuggestions('');
      }, 50);
    },
    // 处理筛选数据
    fetchSuggestions: _.debounce(function (queryString) {
      if (!queryString) {
        this.list = deepClone(this.oldList);
        this.letterList = deepClone(this.letterList);
      } else {
        var restaurants = deepClone(this.oldList);
        const duplicates = [];
        restaurants.forEach((item) => {
          item.data.forEach((item2) => {
            if (item2.name.indexOf(queryString) >= 0) {
              duplicates.push(item2);
            }
          });
        });
        this.list = this.organizationData(duplicates);
        this.letterList = this.list.map((item) => item.letter);
      }
     
    }, 100),
    organizationData(arr) {
      return Object.values(
        arr.reduce((acc, cur) => {
          // 如果累加器对象中没有当前字母的键,则初始化它
          if (!acc[cur.letter]) {
            acc[cur.letter] = { letter: cur.letter, data: [] };
          }
          // 将当前对象添加到对应字母的数据数组中
          acc[cur.letter].data.push(cur);
          return acc;
        }, {})
      );
    },
    iconOut() {
      if (this.isFocus) {
        this.iconT = 'el-icon-arrow-up';
      } else {
        this.iconT = 'el-icon-arrow-down';
      }
    },
    // 图标hover效果
    iconChage() {
      this.iconT = 'el-icon-circle-close';
    },
    // 点击使输入框获取焦点
    clickFocus() {
      this.$refs.input.focus();
    },
    open(e) {
      this.isFocus = true;
      this.iconT = 'el-icon-arrow-up';
      this.visible = true;
      e.currentTarget.focus();
      if (this.valueName) {
        this.innerPlaceholder = deepClone(this.valueName);
        this.valueName = '';
      }
    },
    close() {
      this.$refs.input.blur();
      this.iconT = 'el-icon-arrow-down';
      // 单选
      if (!this.multiple) {
        if (!this.selectValue && this.valueName) {
          this.valueName = '';
          this.innerPlaceholder = deepClone(this.oldPlaceholder)
          this.$emit('input', this.selectValue);
          return;
        }
        if (this.selectValue && this.oldPlaceholder !== this.innerPlaceholder) {
          this.valueName = this.innerPlaceholder;
        }
      } else {
        //多选
        if (!this.selectValue) {
          this.selectValue = this.multiple ? [] : '';
          this.innerPlaceholder = this.placeholder;
        }
        this.$emit('input', this.selectValue);
      }
    },
    // 清除
    clear(e) {
      this.selectValue = this.multiple ? [] : '';
      if (this.multiple) {
        this.valueNameArr = [];
        this.iconT = 'el-icon-arrow-down';
      } else {
        this.valueName = '';
      }
      this.innerPlaceholder = this.placeholder;
      this.isFocus = false;
      this.visible = false;
    },
    // 下拉列表选择
    confirm(e) {
      if (this.multiple) {
        this.valueNameArr.push(e.name);
        this.selectValue.push(e.value);
       if (this.searchName) {
          this.searchName = '';
          this.restoreData();
        }
        this.$refs.input.focus();
      } else {
        this.valueName = e.name;
        this.selectValue = e.value;
        this.visible = false;
      }
      this.offsetTop = this.getBoundingClientRect();
    },
    // 获取输入框高度
    getBoundingClientRect() {
      return this.$refs.itembox.getBoundingClientRect().height + 12;
    },
    // // 节流删除操作
    deleteItem: _.throttle(function (item) {
      let index = this.valueNameArr.indexOf(item);
      if (index >= 0) {
        this.valueNameArr.splice(index, 1);
        this.selectValue.splice(index, 1);
        this.$emit('input', this.selectValue);
      }
        if (this.searchName) {
          this.searchName = '';
          this.restoreData();
        }
    }, 200),
    // 点击输入框外部元素隐藏下拉列表
    handleDocumentClick(e) {
      if (document.getElementsByClassName('index_list_content' + this.client)[0]) {
        if (!document.getElementsByClassName('index_list_content' + this.client)[0].contains(e.target)) {
          //这句话是说如果我们点击到了class为keywordContainer以外的区域
          this.visible = false;
          this.isFocus = false;
        }
      }
    },
  },
};
</script>

<style scoped lang="scss">
.bg_fff{background-color: #fff;}
.index_area {
  width: 100%;
  background-color: #fff;
  z-index: 9;
  padding: 12px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  transform-origin: center top;
}
.input_box {
  border: 1px solid #c0c4cc;
  height: 36px;
  .tag {
    padding: 0px 8px;
    background-color: #f4f4f5;
    color: #909399;
    height: 24px;
    line-height: 24px;
    margin-left: 6px;
    white-space: nowrap;
  }
}
.is_focus {
  border: 1px solid #48c07f !important;
}
.input_box ::v-deep .el-input__inner:hover {
  border: none !important;
}
.input_box ::v-deep {
  .el-input__inner {
    border: none !important;
    width: 100% !important;
    padding: 0 !important;
    height: 24px !important;
    line-height: 24px !important;
  }
}
.input__suffix {
  position: absolute;
  width: 25px;
  height: 100%;
  right: 5px;
  top: 0;
  text-align: center;
  color: #c0c4cc;
  -webkit-transition: all 0.3s;
  transition: all 0.3s;
}
.pr30 {
  padding-right: 30px;
}
.inline_block {
  display: inline-block;
}
.pointer {
  cursor: pointer;
}
.ml8 {margin-left: 8px;}
.f12 {font-size: 12px;}
.flex{display: flex; flex-direction: row;flex-wrap: nowrap; justify-content: flex-start;}/***默认水平左对齐不换行**/
.flex-align-center{align-items: center;}/**垂直居中对齐**/
.c_gap4 {column-gap: 4px;}
.r_gap4 {row-gap: 4px;}
.relative {position: relative;}
.absolute {position: absolute;}
.bor_rad4{border-radius: 4px;}
</style>
  • 下拉列表的封装
<template>
  <div>
    <div class="popper__arrow"></div>
    <div
      class="menu"
      v-if="reFresh"
      :class="'menu' + client"
    >
    
      <div
        v-if="option.length > 0"
        class="w100"
      >
        <div
          class="city_menu_container"
          :class="'city_menu_container' + client"
          @scroll="onScroll"
        >
          <div
            v-for="(item,index) in option"
            :key="index"
            class="area"
            :class="'area' + client"
          >
            <div
              :id="item.letter"
              ref="item.letter"
              class="item_tit"
              :class="[{high_light: activeIndex === index },'item_tit' + client]"
            >{{ item.letter }}</div>
            <!-- 通过 typeof selectValue === 'number'区分value类型 -->
            <template v-if="typeof selectValue === 'number'">
              <div
                v-for="(list,newIndex) in item.data"
                :key="list.name"
                class="item action pointer"
                :class="{active_brand: selectValue ===  list.value}"
                @click="chose(list.name,index,list.value,newIndex)"
              >
                {{ list.name }}
              </div>
            </template>
            <template v-else>
              <div
                v-for="(list,newIndex) in item.data"
                :key="list.name"
                class="item action pointer"
                :class="{active_brand: selectValue.indexOf(list.value) >= 0}"
                @click="chose(list.name,index,list.value,newIndex)"
              >
                {{ list.name }}
              </div>
            </template>
          </div>

        </div>
        <ul
          class="city_list_container"
          :class="'city_list_container' + client"
        >
          <li
            v-for="(item,index) in letterList"
            :key="index"
            class="item_wort"
            :class="{active: activeIndex === index }"
            @click="jump(index,$event)"
          >{{ item }}</li>
        </ul>
      </div>
      <span
        v-else
        class="col_999 text-center w100"
      >
        无匹配数据
      </span>

    </div>
  </div>

</template>
<script>
export default {
  props: {
    selectValue: {
      type: [Array, String, Number],
      default: () => {
        return '';
      },
    },
    letterList: {
      type: Array,
      default: () => {
        return [];
      },
    },
    option: {
      type: Array,
      default: () => {
        return [];
      },
    },
    multiple: {
      type: Boolean,
    },
    client: {
      type: String,
      default: function name(params) {
        return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '');
      },
    },
  },
  data() {
    return {
      activeStep: null,
      activeIndex: 0,
      reFresh: true,
    };
  },

  watch: {
    option() {
      this.reFresh = false;
      this.$nextTick(() => {
        this.reFresh = true;
      });
    },
  },
  computed: {},
  activated() {
    // this.onScroll()
  },
  methods: {
    chose(name, index, value, newIndex) {
      // 单选操作
      console.log(!this.multiple);
      if (!this.multiple) {
        this.$emit('closeList', { name, value });
      } else {
        //多选
        if (this.selectValue.indexOf(value) < 0) {
          //判断去重
          this.$emit('closeList', { name, value });
        } else {
          //删除
          this.$emit('deleteItem', name);
        }
      }
    },
    onScroll(e) {
      const scrollItems = document.querySelectorAll('.item_tit' + this.client);
      for (let i = scrollItems.length - 1; i >= 0; i--) {
        // 判断滚动条滚动距离是否大于当前滚动项可滚动距离
        const judge = e.target.scrollTop >= scrollItems[i].offsetTop - scrollItems[0].offsetTop;
        if (judge) {
          this.activeStep = i;
          this.activeIndex = i;
          break;
        }
      }
    },

    jump(index, event) {
      this.activeIndex = index;
      const target = document.querySelector('.city_menu_container' + this.client);
      const scrollItems = document.querySelectorAll('.area' + this.client);
      // 判断滚动条是否滚动到底部
      if (target.scrollHeight <= target.scrollTop + target.clientHeight) {
        this.activeStep = index;
      }
      // console.log(this.activeIndex, index)
      const roll = scrollItems[index].offsetTop - scrollItems[0].offsetTop; // 锚点元素距离其滚动窗口顶部的距离(待滚动的距离)
      let distance = document.querySelector('.city_menu_container' + this.client).scrollTop; // 滚动条距离滚动区域顶部的距离
      // 滚动条距离滚动区域顶部的距离(滚动区域为窗口)
      // 滚动动画实现, 使用setTimeout的递归实现平滑滚动,将距离细分为10小段,10ms滚动一次
      // 计算每一小段的距离
      let step = roll / 10;
      if (roll > distance) {
        rollDown(target);
      } else {
        const newTotal = distance - roll;
        step = newTotal / 10;
        rollUp(target);
      }
      // node为滚动区域节点
      function rollDown(node) {
        if (distance < roll) {
          distance += step;
          node.scrollTop = distance;
          setTimeout(rollDown.bind(this, node), 10);
        } else {
          node.scrollTop = roll;
        }
      }
      // node为滚动区域节点
      function rollUp(node) {
        if (distance > roll) {
          distance -= step;
          node.scrollTop = distance;
          setTimeout(rollUp.bind(this, node), 10);
        } else {
          node.scrollTop = roll;
        }
      }
    },
  },
};
</script>
<style scoped>
.popper__arrow {
  z-index: 1000;

  filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03));
}
.popper__arrow,
.popper__arrow:after {
  position: absolute;
  display: block;
  width: 0;
  height: 0;
  top: -6px;
  left: 35px !important;
  margin-right: 3px;
  border-left: 0 solid transparent;
  border-right: 0 solid transparent;
  border-bottom: 0 solid #fff;
  border-width: 6px;
}

.menu {
  /* position: relative; */
  display: flex;
  justify-content: space-between;
}
.item_tit {
  width: 100%;
  height: 26px;
  background: #f7f7f7;
  line-height: 26px;
  padding-left: 8px;
}
.item_wort {
  cursor: pointer;
  text-align: center;
  height: 20px;
  line-height: 20px;
  /* padding: 4px 8px; */
}
.city_menu_container {
  width: 90%;
  max-height: 540px;
  overflow: scroll;
  position: relative;
}
.city_menu_container::-webkit-scrollbar {
  width: 4px !important;
  height: 1px !important;
}
.city_menu_container::-webkit-scrollbar-thumb:horizontal:hover {
  background-color: #c5c5c5 !important;
  transition: 0.5s all;
}
.city_menu_container::-webkit-scrollbar-thumb:vertical:hover {
  background-color: #c5c5c5 !important;
  transition: 0.5s all;
}
.city_menu_container::-webkit-scrollbar-thumb:vertical {
  height: 1px;
  background-color: #d9d9d9 !important;
  -webkit-border-radius: 5px !important;
}
.city_menu_container::-webkit-scrollbar-thumb:horizontal {
  width: 4px;
  background-color: #d9d9d9 !important;
  -webkit-border-radius: 0px !important;
}
.item {
  width: 100%;
  height: 30px;
  line-height: 30px;
  padding-left: 8px;
}
.item span {
  cursor: pointer;
}
.item:hover {
  background-color: #f5f7fa;
}
.city_list_container {
  list-style-type: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: absolute;
  top: 1.72rem;
  right: 12px;
  bottom: 0;
  z-index: 999999;
  max-height: 500px;
}
.active {
  color: #48c07f;
}
.active_brand {
  color: #48c07f;
}
.high_light {
  background: linear-gradient(
    90deg,
    rgba(72, 192, 127, 0.15) 0%,
    rgba(72, 192, 127, 0.03) 100%
  ) !important;
  color: #48c07f;
}
</style>

到此基本就可以使用了,若有不足之处请点评,谢幕!!!!

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js 中使用 Element-UI 实现下拉列表(通常称为 Select)的多选功能,Element UI 提供了一个名为 `el-select` 的组件,它可以方便地设置为多选模式。以下是基本步骤: 1. 引入依赖:首先确保在你的项目中已经安装了 Element UI 和 Vue,并且在组件中导入 `el-select` 和相关的样式。 ```html <template> <div> <el-select v-model="selectedValues" multiple placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" ></el-option> </el-select> </div> </template> ``` 2. 定义数据:在你的 Vue 实例中,定义一个数组 `selectedValues` 来存储用户选择的值,以及一个选项列表 `options`。 ```javascript export default { data() { return { selectedValues: [], // 存储选中的值 options: [ { value: 'option1', label: 'Option 1' }, { value: 'option2', label: 'Option 2' }, // 更多选项... ], }; }, }; ``` 3. 设置属性:`v-model` 绑定了 `selectedValues`,`multiple` 属性表示可多选,`placeholder` 则是默认提示文字。 4. 使用 `el-option`:对于每个选项,用 `v-for` 指令遍历 `options` 数组,`:value` 和 `:label` 分别绑定选项的值和标签。 当你在下拉列表中点击选项时,`selectedValues` 就会自动更新,显示或隐藏多选框也会相应改变。如果需要进一步定制行为(如添加搜索功能、分页等),可以查阅 Element UI 的文档进行详细配置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值